Posted on July 15, 2023
Before we start, if you want to watch a video instead of reading this article, CLICK HERE
Don’t get it twisted. This is NOT a Solidity 101 course, but HOW I used phenomenal resources like Patrick Collin’s course or Solidity by Example to master the basics in Solidity
I share:
Courses like Patrick Collin’s course and Solidity by Example are GOLD. Legit never has it been easier to learn Solidity. Especially for someone like me who get’s scared learning anything new. I’m talking about proper anxiety with a sprinkle of conviction I will never be good at programming. Enough about me.
When I first saw the explanation I understood the code. But my personal wish was, that I was able to write up such a basic contract, without looking up how to write a Struct or how to write up a contract.
Practise is the answer. I learn in a very old fashioned way. I’m old. The way we used to learn in school is by just rewriting the outcome a bunch a time. Now that is inefficient. However the way that I learned math, was perfect. Which was:
By doing the exercises, my level of understanding of the theory went to a deeper level. Today when someone asks me to write a Simple Storage contract or an Ether Wallet, I don’t need to look anything up.
When I first dove into Solidity, I was looking for syntax practise. So I’m starting to dissect simple contracts, building on the knowledge I learned from a course. Then come up with exercises (powered by gpt), just like math, to get to a deeper level of understanding. Finally come back to the contract. Here’s a breakdown of what this article will give you:
Ok, so this is the contract that you learned about in the course. Awesome.
Le code:
// I'm a comment! // SPDX-License-Identifier: MIT pragma solidity 0.8.19; // pragma solidity ^0.8.0; // pragma solidity >=0.8.0 <0.9.0; contract SimpleStorage { uint256 myFavoriteNumber; struct Person { uint256 favoriteNumber; string name; } // uint256[] public anArray; Person[] public listOfPeople; mapping(string => uint256) public nameToFavoriteNumber; function store(uint256 _favoriteNumber) public { myFavoriteNumber = _favoriteNumber; } function retrieve() public view returns (uint256) { return myFavoriteNumber; } function addPerson(string memory _name, uint256 _favoriteNumber) public { listOfPeople.push(Person(_favoriteNumber, _name)); nameToFavoriteNumber[_name] = _favoriteNumber; } }
This is my visual of what the contract does:
Breaking it down simple. I like to look at the functions and then use that angle to define what the contract does. Looking at the above visual we have 4 functions:
For this level of contract I need to be familiar with:
Cool now I knew what the contract should do and what basics are involved. Next I focused on the basic knowledge of Solidity and rewriting it in my own words. Elaborate, I know.
So for this contract we’re just using basic variables, strings and integers. If you’re looking for notes on variables, you can find my notes here.
Here’s a snippet:
I know it might seem “extensive”, but I literally tried to memorise what each of these do. At some point if you build “enough”, it becomes second nature to knowing what each visibility keyword does. But at the START, it really was daunting to me. So I took time to go over these. In my experience, it goes a long way to memorise what each visibility does. Also I like to take baby steps.
Q: Define a variable
uint256 myFavoriteNumber;
I know this is very elaborate. But this is how I learn. It’s not for everyone.
Now comes the time where I did brain dead stuff. Type out every visibility. Write behind each variable in comments what the visibility allows. Here is where I learn.
uint256 public owner; //variable that anyone can see uint256 internal owner; //if a contract creates another contract and the newly created contract wants to interact with this variable in the parent contract, it is allowed to do so. uint256 external owner; //cant be accessed from within contract, only external contracts or accounts uint256 private owner; //cant be accessed by external contract or account, only from within contract
OK, so I remember back in the days, I found structs tricky. Especially combining them with lists. And here is where mindless practise really helped me. So here’s a short explanation and then we dive into practise of just Structs. Finally we quickly move on to how to use structs and lists combined.
In a simple way, a struct is a template. It’s a way to define a custom data type that can hold multiple related variables. It allows you to create complex data structures to organise and store different pieces of information.
This is how I visualise a Struct. Imagine a Spreadsheet, where you name the column names and the sheet. The column names are the variables you can use inside a struct and the spreadsheet name is the name of the struct. Here’s what that looks like:
struct Token { string name; string symbol; uint256 totalSupply; }
And here is the spreadsheet:
So now this template can be used to fill in data. But it’s just a template, so in order to ‘store’ data, we need something like a variable or list, that can make use of this template or struct.
contract myToken{ //define a struct struct Token{ string name; string symbol; uint256 totalSupply; } //use the struct for a variable Token public razaCoin //so now 'razaCoin' uses 'Token' as a template }
Let’s assign some data using this template
pragma solidity ^0.8.0; contract myToken { struct Token { string name; string symbol; uint256 totalSupply; } Token public razaCoin; function setTokenValues() external { razaCoin.name = "RazaCoin"; razaCoin.symbol = "RZC"; razaCoin.totalSupply = 1000000; } }
So inside a sheet this looks like this. Now for a variable, there is only 1 row, if we want an example with multiple rows, we could use a list. That’s for later.
An array is a list. Simple as. You can store items in a list, just like Python and Javascript.
Defining an array is similar to defining a variable.
TYPE+’[]’+VISIBILITY+NAME
Example:
uint256[] public userAccounts;
So an array is like a container that holds related pieces of data.
Once a Struct is defined, you can use it to type a list like this:
Person[] public listOfPeople;
Now this list can be filled with the template we defined. So to visualise it means, we can add ‘rows’ to our sheet. Each row we fill out a column. We use the push method for that, but more on that later.
Mapping in Solidity is like a dictionary or a phone book that helps you pair one value with another. It allows you to store and access information based on an unique key.
For a mapping the structure is as follows:
mapping(KEY ⇒ VALUE) + VISIBILITY + NAME
Example:
mapping(address => uint256) public balances
Here’s how I visualise it:
Note, that a mapping is not indexed. You retrieve a value, by using the key. So you can’t use a number to retrieve a value from a mapping like this:
balances[0]
Functions take input do something with it and return an output. Functions felt a lot more complex than doing them in Python. You got modifiers, you type the parameters, you need to tell the type of value it returns sometimes. For this contract you need to be familiar with:
I’m just glancing over my notes. If you’ve been watching tutorials though, you will for sure have touched upon the basics. So here are exercises specifically to functions that I practised. Yes they were created by chatGPT.
The answers:
You better not be looking at the answers before trying. For real though, this is the level you should be able to crush. If not, now you know where your weakness lies. Work on it.
pragma solidity ^0.8.0; contract Exercise1 { function addNumbers(uint256 _a, uint256 _b) external pure returns (uint256) { return _a + _b; } }
pragma solidity ^0.8.0; contract Exercise2 { function calculateSquare(uint256 _number) external pure returns (uint256) { return _number * _number; } }
OK. That’s basically it for the fundamentals in syntax. Now I didn’t address everything for a simple reason, because I’m assuming you watch an actual tutorial before you came here. These are just my notes for when I was going through the course and served as a refresher before I dove into exercises.
Exercise 1: Define a function named "addNumbers" that takes two uint256 arguments and returns their sum. Code:
pragma solidity ^0.8.0; contract BasicFunctionsExercise { function addNumbers(uint256 _a, uint256 _b) public pure returns (uint256) { return _a + _b; } }
Exercise 2: Declare a function named "sayHello" that does not take any arguments and returns a string "Hello, World!". Code:
pragma solidity ^0.8.0; contract BasicFunctionsExercise { function sayHello() public pure returns (string memory) { return "Hello, World!"; } }
Exercise 3: Define a function named "isEven" that takes an integer as an argument and returns a boolean indicating whether the number is even or not. Code:
pragma solidity ^0.8.0; contract BasicFunctionsExercise { function isEven(uint256 _number) public pure returns (bool) { return _number % 2 == 0; } }
Exercise 1: Declare a variable of type integer with the name "age" and assign it a value of 25 with a function. Code:
pragma solidity ^0.8.0; contract VariablesExercise { uint256 public age; function setAge() public { age = 25; } }
Exercise 2: Declare a variable of type string with the name "name" and assign it the value "Alice" with a function. Code:
pragma solidity ^0.8.0; contract VariablesExercise { string public name; function setName() public { name = "Alice"; } }
Exercise 3: Declare a private variable of type boolean with the name "isStudent" and assign it the value true with a function. Code:
pragma solidity ^0.8.0; contract VariablesExercise { bool private isStudent; function setIsStudent() public { isStudent = true; } }
Exercise 1: Define a struct named "Person" with two fields: "name" of type string and "age" of type uint256. Write a function that uses the Struct Person for a variable with the values: “Alice” and “25”. Code:
pragma solidity ^0.8.0; contract StructsExercise { struct Person { string name; uint256 age; } Person public person; function setPerson() public { person = Person("Alice", 25); } }
Exercise 2: Declare an array of type "Person" with the name "people" and initialise it with two Person structs. Push 2 “people” with their age to the list: Alice &25, Bob & 30. Code:
pragma solidity ^0.8.0; contract StructsExercise { struct Person { string name; uint256 age; } Person[] public people; function setPeople() public { people.push(Person("Alice", 25)); people.push(Person("Bob", 30)); } }
Exercise 3: Define a function named "getPersonName" that takes a Person struct as an argument and returns the name of the person. Code:
pragma solidity ^0.8.0; contract StructsExercise { struct Person { string name; uint256 age; } function getPersonName(Person memory _person) public pure returns (string memory) { return _person.name; } }
Exercise 1: Declare a dynamic array of type uint256 with the name "numbers" and initialise it with three numbers: 1, 2, and 3 using a function. Code:
pragma solidity ^0.8.0; contract ArraysExercise { uint256[] public numbers; function setNumbers() public { numbers = [1, 2, 3]; } }
Exercise 2: Define a function named "sumArray" that takes an array of uint256 as an argument and returns the sum of all the numbers in the array. This might be a spicy one. Code:
pragma solidity ^0.8.0; contract ArraysExercise { function sumArray(uint256[] memory _array) public pure returns (uint256) { uint256 sum = 0; for (uint256 i = 0; i < _array.length; i++) { sum += _array[i]; } return sum; } }
Exercise 3: Declare a fixed-size array of type address with the name "accounts" and initialise it with three Ethereum addresses using a function. Code:
pragma solidity ^0.8.0; contract ArraysExercise { address[3] public accounts; function setAccounts() public { accounts = [address(0x123), address(0x456), address(0x789)]; } }
Exercise 1: Declare a mapping named "balances" that maps addresses to uint256 values. Write a function to set the balance of an address. Code:
pragma solidity ^0.8.0; contract MappingExercise { mapping(address => uint256) public balances; function setBalance(address _address, uint256 _balance) public { balances[_address] = _balance; } }
Exercise 2: Define a function named "getBalance" that takes an address as an argument and returns the balance associated with that address from the "balances" mapping. Code:
pragma solidity ^0.8.0; contract MappingExercise { mapping(address => uint256) public balances; function getBalance(address _address) public view returns (uint256) { return balances[_address]; } }
Exercise 3: Declare a mapping named "studentGrades" that maps strings (student names) to uint256 values (grades). Write a function that sets the grade of a student. Code:
pragma solidity ^0.8.0; contract MappingExercise { mapping(string => uint256) public studentGrades; function setGrade(string memory _studentName, uint256 _grade) public { studentGrades[_studentName] = _grade; } }
This part is simple. Based on the exercises you should be able to:
Here’s a blank template with comments. Write it out.
// I'm a comment! // SPDX-License-Identifier: MIT pragma solidity 0.8.19; // pragma solidity ^0.8.0; // pragma solidity >=0.8.0 <0.9.0; //define the contract //define a variable for a favorite number //write a struct for a Person containing favorite number and a name //write an array containing the list of people. hint use the struct to type the array //write a mapping to link the name to the favorite number //write a function to store the favorite number //write a function to retrieve the favorite number //write a function to add a person to the list of people
I added a function to the contract to retrieve the whole list. Try it out yourself.
Here’s my code (it’s fine if you glanced at it, just switch tabs and write it out yourself)
function retrieve() public view returns (Person[] memory) { return listOfPeople; }
Remix is great, but pretty much straight away I started hands on with deployment frameworks. The one’s I use are Brownie and Truffle.
I like Brownie better, because it’s Python. Python is much cleaner and easier to use. Having said that, probably most devs will opt for JavaScript. Then my preferred tool is Truffle, because of Truffle dashboard.
OK, let’s install Truffle and then use it to write some basic test and then deploy it. If you’ve never touched Truffle, no worries I got you.
Truffle is a popular development environment, testing framework, and asset pipeline for Ethereum. To deploy with Truffle, follow the steps below:
Install Truffle globally using npm:
npm install -g truffle
You can run the following command to create a starter project:
truffle unbox metacoin
If you want to create a complete blank project run:
truffle init
Remove the old contracts and test files that came with the Metacoin project. Replace the old contract file name with your contract. This is the new contract you're deploying.
We’re going to use Truffle Dashboard, what that is, I’ll show in a minute. Truffle Dashboard should work out of the box, but just in case it doesn’t…adjust your truffle.config.js file like this:
// Configure your compilers compilers: { solc: { version: "0.8.17", // Fetch exact version from solc-bin } }, dashboard: { port: 24013, host:"localhost" , }, networks: { // ... network configurations, including the network named 'dashboard'
Now you if you know a little bit dev tools, you might be asking, where do i pass the rpc url. Well you don’t have to. Let’s take a look.
Normally you test first, then deploy. But if you’re starting out, you might not be interested in tests. So we’ll do deploying first and then dive into tests. We’re going to need two windows in your terminal. In the first we run Truffle Dashboard, which spins up a local host server. The second where we will compile the contract and send it to the dashboard to deploy.
Terminal 1: Run dashboard
truffle dashboard
Go to your browser (if it doesn’t pop up automagically) and the dashboard should be running on localhost:24013
Connect your wallet. I use MetaMask, and connect to the network you want to use.
Terminal 2: Compile and send contract
truffle migrate --network dashboard
Now when you run the above command, go to your dashboard and you should see the metadata of your contract. Make sure your wallet is connected and you’re on the desired network (probably a test net). Click deploy and that’s it!
Tests basically run a function and check if the function returns the desired outcome. So you need to tell a test what function to run, the parameters(if any) and then what the desired outcome is. That’s it.
In the test folder create a file. I usually stick with the name of the contract so: simpleStorage.js
Use the following command in your terminal to run the tests:
truffle test
If you want to run a specific test file use:
$ truffle test ./path/to/test/file.js
Here are some example tests. Please note, be careful with GPT. I tried it with GPT and the first couple of tests were actually not working.
// SPDX-License-Identifier: MIT const SimpleStorage = artifacts.require('SimpleStorage'); contract('SimpleStorage', (accounts) => { let simpleStorage; before(async () => { simpleStorage = await SimpleStorage.deployed(); }); it('should store the favorite number', async () => { const favoriteNumber = 42; await simpleStorage.store(favoriteNumber); const storedFavoriteNumber = await simpleStorage.retrieve(); assert.equal(storedFavoriteNumber, favoriteNumber, 'Favorite number was not stored correctly'); }); it('should add a person to the list', async () => { const name = 'Alice'; const favoriteNumber = 7; await simpleStorage.addPerson(name, favoriteNumber); const person = await simpleStorage.listOfPeople(0); assert.equal(person.favoriteNumber, favoriteNumber, 'Favorite number of the person was not added correctly'); assert.equal(person.name, name, 'Name of the person was not added correctly'); }); it('should map a name to a favorite number', async () => { const name = 'Bob'; const favoriteNumber = 10; await simpleStorage.addPerson(name, favoriteNumber); const mappedFavoriteNumber = await simpleStorage.nameToFavoriteNumber(name); assert.equal(mappedFavoriteNumber, favoriteNumber, 'Favorite number was not mapped correctly'); }); });
Brownie is a Python framework for Ethereum smart contract testing, debugging, interaction and deployment.
You can make use of the pipx library. It makes it easier to create a virtual environment on the fly. A virtual environment is just a sandbox on your computer, where you can install stuff locally, instead of globally. So its a closed play area. However I like to do it the standard way, which is creating your own python virtual environment and then installing everything. Just works everytime.
python3 -m venv ./myv source ./myv/bin/activate
Then you can go ahead and use the pip to install brownie and the dotenv library at the same time.
pip install python-dotenv eth-brownie
Set up a new Brownie project with a standard ERC20 smartcontract
brownie bake token cd token
Rename the contract to simpleStorage.sol and paste the code in there.
There are different ways to add networks to Brownie. I like to do it like this.
Create a network-config.yaml
file with the Scroll Alpha Testnet information.
live: - name: Ethereum networks: - chainid: 534353 explorer: https://blockscout.scroll.io/ host: https://scroll-alphanet.public.blastapi.io id: testnet-scroll name: Testnet (scroll)
Import the network configuration and create a new account with your private key. my-new-account is the name I gave, you can pass any name you want here. After this prompt, you will be asked to input the private key and choose a password.
brownie networks import ./network-config.yaml brownie accounts new my-new-account
Run the following command:
brownie compile
Make sure to adjust the deploy.py script. Our contract doesn’t take in any parameters to deploy, so we can remove everything, except for the account part.
from brownie import SimpleStorage, accounts account = accounts.load("my-new-account") def main(): return SimpleStorage.deploy({'from': account})
Now go to your terminal and run:
brownie run simplestorage.py --network testnet-scroll
That’s it
The following tests, go into the test folder. They are the same as the JavaScript tests, but instead written in Python. Make sure you have ganache installed, which is a local blockchain to test stuff:
npm install -g ganache
As for the tests:
import pytest from brownie import SimpleStorage, accounts def test_simple_storage(): # Deploy the contract simple_storage = SimpleStorage.deploy({'from': accounts[0]}) # Test storing and retrieving a favorite number favorite_number = 42 simple_storage.store(favorite_number, {'from': accounts[0]}) stored_favorite_number = simple_storage.retrieve({'from': accounts[0]}) assert stored_favorite_number == favorite_number, "Favorite number was not stored correctly" # Test adding a person to the list name = "Alice" favorite_number = 7 simple_storage.addPerson(name, favorite_number, {'from': accounts[0]}) person = simple_storage.listOfPeople(0, {'from': accounts[0]}) print(person) assert person[1] == name, "Name of the person was not added correctly" assert person[0] == favorite_number, "Favorite number of the person was not added correctly" # Test mapping a name to a favorite number name = "Bob" favorite_number = 10 simple_storage.addPerson(name, favorite_number, {'from': accounts[0]}) mapped_favorite_number = simple_storage.nameToFavoriteNumber(name, {'from': accounts[0]}) assert mapped_favorite_number == favorite_number, "Favorite number was not mapped correctly"
A quick note about the test. You will notice when comparing the javascript test with the python test, that in the second test person is accessed differently. And that has to do with the following two lines of code:
//truffle const person = await simpleStorage.listOfPeople(0);
and for python:
person = simple_storage.listOfPeople(0, {'from': accounts[0]})
If you will console.log and print both of these in your terminal, you will see different outputs. That is because in Python the return is an array of values, whereas its an object in JavaScript. Would the return value have been a dictionary in Python, you could have used the person.favoriteNumber instead of person[0].
My goal was to go end to end. Building a smart contract was just a part of what I want to do. The cool thing is, that with simple smart contracts like this, it makes everything easier. From deploying, to tests, to building out the front end. So that’s what I’m sharing in this section. How to build out a frontend for this smart contract. We’re going to use 2 frameworks for that:
I love nextJS, its easy to build with and easy to deploy. This is how I interact with the smart contract in a react framework like NextJS. Here is the Github link if you want to go over the code.
I’m going to assume you have nodeJS installed by now. If not…Google.
I created a new project with:
npx create-next-app
I didn’t use TS, because I don’t like torturing myself and I used Next 13 with the App Router. Quick disclaimer, you can still choose to not use the App Router. All you need to do is, copy paste whatever is in the page.jsx file to index.jsx. I’ll write about it again in a bit. This is what the choices look like. Again I chose NO for TypeScript and src/
directory.
Install ethers, and a very specific version:
npm install ethers@5.7.2
Why the specific version? Because in the latest version of ethers, they changed fundamental shit under the hood (how you instantiate the signer and provider I believe).
At some point I’ll need to dive into that, but for now, this is just easier.
This is what the front end looks like:
I didn’t include any styling (except some inline border colours), so you can play around with it however you want. Maybe later I’ll use Chakra later to spice it up.
This is what my code looks like in the page.js
file inside the app
folder
'use client' import { useState } from 'react'; import { ethers } from 'ethers'; import { abi } from './abi'; export default function Home() { const [favoriteNumber, setFavoriteNumber] = useState(''); const [storedNumber, setStoredNumber] = useState(''); const [personName, setPersonName] = useState(''); const [personFavoriteNumber, setPersonFavoriteNumber] = useState(''); const [mappedFavoriteNumber, setMappedFavoriteNumber] = useState(''); const contractAddress = '0x5544cbe52b0797f71B4186469a0797Bdb260abf6'; // Connect to the contract async function connectToContract() { if (window.ethereum) { await window.ethereum.enable(); //connect to blockchain via metamask const provider = new ethers.providers.Web3Provider(window.ethereum); //connect your wallet via metamask const signer = provider.getSigner(); const contract = new ethers.Contract(contractAddress, abi, signer); return contract; } else { console.error('Please install MetaMask to interact with the contract.'); } } // Store favorite number async function storeFavoriteNumber() { const contract = await connectToContract(); if (contract) { const tx = await contract.store(favoriteNumber); await tx.wait(); setFavoriteNumber(''); retrieveFavoriteNumber(); } } // Retrieve favorite number async function retrieveFavoriteNumber() { const contract = await connectToContract(); if (contract) { console.log("contract OK") console.log(abi) const number = await contract.retrieve(); setStoredNumber(number.toString()); } } // Retrieve favorite number of a person async function retrievePersonFavoriteNumber(personName) { const contract = await connectToContract(); console.log(personName) if (contract) { const number = await contract.nameToFavoriteNumber(personName); setMappedFavoriteNumber(number.toString()); } } // Add a person async function addPerson() { const contract = await connectToContract(); if (contract) { const tx = await contract.addPerson(personName, personFavoriteNumber); await tx.wait(); setPersonName(''); setPersonFavoriteNumber(''); retrievePersonFavoriteNumber(personName); } } return ( <> <div> <h1>SimpleStorage Contract Interaction</h1> <div> <h2>Store Favorite Number</h2> <input type="number" value={favoriteNumber} onChange={(e) => setFavoriteNumber(e.target.value)} style={{ border: '1px solid black' }} /> <button onClick={storeFavoriteNumber} style={{ border: '1px solid black' }}>Store</button> </div> <div> <h2>Retrieve Favorite Number</h2> <button onClick={retrieveFavoriteNumber} style={{ border: '1px solid black' }}>Retrieve</button> <p>Stored Number: {storedNumber}</p> </div> <div> <h2>Add Person</h2> <input type="text" value={personName} onChange={(e) => setPersonName(e.target.value)} placeholder="Name" style={{ border: '1px solid black' }} /> <input type="number" value={personFavoriteNumber} onChange={(e) => setPersonFavoriteNumber(e.target.value)} placeholder="Favorite Number" style={{ border: '1px solid black' }} /> <button onClick={addPerson} style={{ border: '1px solid black' }}>Add</button> </div> <div> <h2>Retrieve Favorite Number of a Person</h2> <input type="text" value={personName} onChange={(e) => setPersonName(e.target.value)} placeholder="Name" style={{ border: '1px solid black' }} /> <button onClick={() => retrievePersonFavoriteNumber(personName)} style={{ border: '1px solid black' }}> Retrieve </button> <p>Mapped Favorite Number: {mappedFavoriteNumber}</p> </div> </div> </> ); }
Make sure to replace the contract address with the actual address of your SimpleStorage contract.
Create a new file called abi.js
in the root folder of your Next.js project with the following code:
export const abi = [ // Paste the ABI of the SimpleStorage contract here ];
Replace // Paste the ABI of the SimpleStorage contract here
with the actual ABI of your SimpleStorage contract. Make sure to export the abi
variable using export const
to make it accessible to other files.
To run the Next.js project, use the following command:
npm run dev
Open your browser and visit http://localhost:3000
to see the contract interaction page.