Solidity's code is encapsulated in 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.
An empty contract named HelloWorld would look like this:
pragma solidity >=0.5.0 <0.6.0; contract HelloWorld { }
State variables are permanently stored in contract storage. This means they're written to the Ethereum blockchain. Think of them like writing to a DB.
contract Example { // This will be stored permanently in the blockchain uint myUnsignedInteger = 100; }
Sometimes you need a more complex data type. For this, Solidity provides structs:
struct Person { uint age; string name; }
Structs allow you to create more complicated data types that have multiple properties.
When you want a collection of something, you can use an array. There are two types of arrays in Solidity: fixed arrays and dynamic arrays.
You can also create an array of structs. Using the previous chapter's Person struct:
// Array with a fixed length of 2 elements: uint[2] fixedArray; // another fixed Array, can contain 5 strings: string[5] stringArray; // a dynamic Array - has no fixed size, can keep growing: uint[] dynamicArray; Person[] people; // dynamic Array, we can keep adding to it
You can declare an array as public, and Solidity will automatically create a getter method for it. The syntax looks like:
Person[] public people;
Other contracts would then be able to read from, but not write to, this array. So this is a useful pattern for storing public data in your contract.
pragma solidity >=0.5.0 <0.6.0; contract ZombieFactory { uint dnaDigits = 16; uint dnaModulus = 10 ** dnaDigits; struct Zombie { string name; uint dna; } Zombie[] public zombies; }
function eatHamburgers(string memory _name, uint _amount) public { }
This is a function named eatHamburgers
that takes 2 parameters: a string
and a uint
. 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.
This code outines how to add a new zombie to the factory:
pragma solidity >=0.5.0 <0.6.0; contract ZombieFactory { uint dnaDigits = 16; uint dnaModulus = 10 ** dnaDigits; struct Zombie { string name; uint dna; } Zombie[] public zombies; function createZombie(string memory _name, uint _dna) public { zombies.push(Zombie(_name, _dna)); } }
In Solidity, functions are public
by default. This means anyone (or any other contract) can call your contract's function and execute its code.
Obviously this isn't always desirable, and can make your contract vulnerable to attacks. Thus it's good practice to mark your functions as private
by default, and then only make public the functions you want to expose to the world.
Let's look at how to declare a private
function:
uint[] numbers; function _addToArray(uint _number) private { numbers.push(_number); }
This means only other functions within our contract will be able to call this function and add to the numbers array.
As you can see, we use the keyword private
after the function name. And as with function parameters, it's convention to start private
function names with an underscore (_
).
An example:
string greeting = "What's up dog"; function sayHello() public returns (string memory) { return greeting; }
The example in the return function above doesn't change the state, so we can declare it as a view
function:
string greeting = "What's up dog"; function sayHello() public view returns (string memory) { return greeting; }
Pure functions are functions that you are not even accessing in the app:
function _multiply(uint a, uint b) private pure returns (uint) { return a * b; }
Note: It may be hard to remember when to mark functions as pure/view. Luckily the Solidity compiler is good about issuing warnings to let you know when you should use one of these modifiers.
Ethereum has the hash function keccak256 built in, which is a version of SHA3. A hash function basically maps an input into a random 256-bit hexadecimal number. A slight change in the input will cause a large change in the hash.
It's useful for many purposes in Ethereum, but for right now we're just going to use it for pseudo-random number generation.
Also important, keccak256 expects a single parameter of type bytes. This means that we have to "pack" any parameters before calling keccak256:
//6e91ec6b618bb462a4a6ee5aa2cb0e9cf30f7a052bb467b0ba58b8748c00d2e5 keccak256(abi.encodePacked("aaaab")); //b1f078126895a1424524de5321b339ab00408010b7cf0e6ed451514981e58aa9 keccak256(abi.encodePacked("aaaac"));
Note: Secure random-number generation in blockchain is a very difficult problem. Our method here is insecure, but since security isn't top priority for our Zombie DNA, it will be good enough for our purposes.
An example of uint
to uint8
typecasting:
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);
An example of typecasting and Keccak256 working together:
pragma solidity ^0.4.25; contract ZombieFactory { uint dnaDigits = 16; uint dnaModulus = 10 ** dnaDigits; struct Zombie { string name; uint dna; } Zombie[] public zombies; function _createZombie(string memory _name, uint _dna) private { zombies.push(Zombie(_name, _dna)); } function _generateRandomDna(string memory _str) private view returns (uint) { uint rand = uint(keccak256(abi.encodePacked(_str))); return rand % dnaModulus; } }
We are done with the random zombie generator, we just need to create the zombie with a public function:
pragma solidity >=0.5.0 <0.6.0; contract ZombieFactory { uint dnaDigits = 16; uint dnaModulus = 10 ** dnaDigits; struct Zombie { string name; uint dna; } Zombie[] public zombies; function _createZombie(string memory _name, uint _dna) private { zombies.push(Zombie(_name, _dna)); } function _generateRandomDna(string memory _str) private view returns (uint) { uint rand = uint(keccak256(abi.encodePacked(_str))); return rand % dnaModulus; } function createRandomZombie(string memory _name) public { uint randDna = _generateRandomDna(_name); _createZombie(_name, randDna); } }
Events are a way for your contract to communicate that something happened on the blockchain to your app front-end, which can be 'listening' for certain events and take action when they happen.
// declare the event event IntegersAdded(uint x, uint y, uint result); function add(uint _x, uint _y) public returns (uint) { uint result = _x + _y; // fire an event to let the app know the function was called: emit IntegersAdded(_x, _y, result); return result; }
Your app front-end could then listen for the event. A javascript implementation would look something like:
YourContract.IntegersAdded(function (error, result) { // do something with result })
For our new Zombie, we emitted the data with the following:
pragma solidity >=0.5.0 <0.6.0; contract ZombieFactory { event NewZombie(uint zombieId, string name, uint dna); uint dnaDigits = 16; uint dnaModulus = 10 ** dnaDigits; struct Zombie { string name; uint dna; } Zombie[] public zombies; function _createZombie(string memory _name, uint _dna) private { uint id = zombies.push(Zombie(_name, _dna)) - 1; emit NewZombie(id, _name, _dna); } function _generateRandomDna(string memory _str) private view returns (uint) { uint rand = uint(keccak256(abi.encodePacked(_str))); return rand % dnaModulus; } function createRandomZombie(string memory _name) public { uint randDna = _generateRandomDna(_name); _createZombie(_name, randDna); } }
Our Solidity contract is complete! Now we need to write a javascript frontend that interacts with the contract.
Ethereum has a Javascript library called Web3.js
.
In a later lesson, we'll go over in depth how to deploy a contract and set up Web3.js. But for now let's just look at some sample code for 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 }