Working with Web3.py within SimpleStorage contract
Table of contents
- General info
- Learned experience during the project
- Setup
General info
This project was my first contact with Web3.py. Before that, I played around with Remix IDE and created a SimpleStorage contract. Here I was mainly working with ganache via desktop app, ganache-cli (console version of the desktop app), and TestNet (Rinkeby).
This project was created by freecodecamp in tutorial. I'm extremely grateful for giving this cost-free opportunity to learn all the insights of Solidity, Blockchain, and Smart Contracts.
At first, I went through this project, created some notes, and fast moved to the more advanced ones. Later I understood, that I should go back and create this documentation in my own words to remember all the necessary basic concepts.
Before that, I always tried to create documentation after the project finished, but I never have put that amount of time as here.
Learned experience during the project
SimpleStorage contract
I studied the basic structure of the contract. I wanted to share things that were new for me when entering the Solidity language.
SPDX License
-
Solidity and the Ethereum community found that trust in a smart contract can be better established if source code is available and in terms of legality and copyright it just makes life a lot easier if you add that license identifier right at the top of your solidity. I chose the MIT license identifier because it’s the most open license out there. It means that anybody can use this code and we don’t care. We put the line below at the top of any Solidity file.
// SPDX-License-Identifier: MIT
Keywords:
-
contract
- Stands for the smart contract, that we're going to create. We can think of this keyword, similarly to class keyword in Java or other Object-Oriented languages.
-
interface
- Some contracts don't start with the contract keyword, only the interface keyword. Interfaces don't have full-function implementations.
-
import
- Allows us to add additional code from certain Github repositories.
-
mapping
-
A dictionary-like data structure, with 1 value per key.
mapping(string => uint256) public nameToFavoriteNumber;
-
-
pragma
- It's used to enable certain compiler features or checks.
-
struct
-
A way to define new types in Solidity. They're almost like creating new objects as well.
struct People { uint256 favoriteNumber; string name; } People public person = People({ favoriteNumber: 69, name: "Jakub" });
-
-
uint256 vs int256
- Due to the fact of the Ethereum characteristic type uint256 is crucial. It's an unsigned integer with a minimum value of 0. It's just can not be negative, unlike int256. It's an integer of size 256 bits, which gave us 32 bytes.
Visibility Quantifiers
Following are various visibility quantifiers for functions/state variables of a contract.
- external
- External functions are not meant to be called by the same contract. It has to be called by an external contract.
- For state variables, external is not possible.
- public
- Public functions/variables can be called by anybody. Variables are a function call to just look at them and return whatever that variable is.
- internal
- Internal functions/variables can only be called by other functions/variables inside of this contract or in its derived contract.
- The reason that we cannot see this variable in our original contract deployment is that we don't give a state variable a visibility. It'll automatically get set to internal.
uint256 favoriteNumber; // internal uint256 public favoriteNumber; // public
- private
- Private is the most restrictive as private functions and state variables are only visible for the contract they are defined in and not even by derived contracts.
Functions
Functions or methods are self-contained modules, that will execute some task for us.
- state-changing function calls are transactions
- transactions === smart contracts interactions === function calls
On a blockchain whenever you're calling a function or whenever you make some state change to the blockchain, you're also making a transaction. That's why making a function call or deploying a contract costs a little bit of gas.
Functions that are view or pure
These two keywords define functions that you don't have to make a transaction on, which means that they are non-state changing functions.
- view
- These function means that we want to read some state of the blockchain, so we're just reading off the blockchain. We're not making a state change then, we don't need to make a transaction.
- public variables automatically, are also view functions
- pure
- These are functions, that purely do some type of math.
Memory
In Solidity there are two ways to store information:
-
memory
- Data will only be stored during the execution of the function or the contract call.
- string is a dynamically-sized byte array (a special type of array, that we can append text to), so because it's technically an object, we have to decide where we want to store it in memory or storage.
- string is not a value-type!
- In this case, since we only need parameter _name during the execution, we can have it be string memory _name.
function addPerson(string memory _name, uint256 _favoriteNumber) public { people.push(People(_favoriteNumber, _name)); }
- Data will only be stored during the execution of the function or the contract call.
-
storage
- if we hold it here, that means data will persist even after
In short:
- memory means, that after execution it deletes this variable.
- storage means keep it forever.
EVM
- All the solidity code that I wrote and when I interacted with this blockchain was compiled down to the EVM, also known as the Ethereum Virtual Machine. A lot of the blockchains out there today are called EVM compatible and that means all this solidity code that we’re creating can still compile down to EVM and deployed on their blockchain.
Compilation of smart contract
I needed to write our compiler using Web3 to extract bytecode and ABI from our SimpleStorage contract to enable EVM to read it.
Ganache ecosystem
Ganache is a simulated or a fake blockchain, that we can use to deploy our smart contracts. It's something similar to JavaScript VM in remix IDE. It's much faster and easier to test things rather than standard TestNet like Rinkeby.
Building a transaction
We need to build our transaction, because in this case we'd be deploying a contract, which is going to make a state change.
I've learned that a transaction consists of several things like:
-
building a transaction
In web3.py we always have to give at least a couple of parameters.
transaction = SimpleStorage.constructor().buildTransaction( {"gasPrice": w3.eth.gas_price, "chainId": chain_id, "from": my_address, "nonce": nonce} )
-
signing a transaction
We're signing a transaction using private keys. More precisely we're signing a transaction that is deploying a contract to the blockchain.
signed_tx = w3.eth.account.sign_transaction(transaction, private_key=private_key)
-
sending a transaction
We want to send this to the blockchain, so it actually can deploy.
print("Deploying contract...") tx_hash = w3.eth.send_raw_transaction(signed_tx.rawTransaction) tx_receipt = w3.eth.wait_for_transaction_receipt(tx_hash) print("Deployed!")
Working with deployed contract
I studied that whenever we work with a contract we always need two things:
- contract address
- contract ABI (application binary interface)
# Working with Contract -> 1. Contract Address | 2. Contract ABI
simple_storage = w3.eth.contract(address=tx_receipt.contractAddress, abi=abi)
Keeping safe your private keys
It's not so great idea to have plain text on our computer, but still better than hard coding it into our script. For purpose of private key safety, it's worth building healthy habits around that concept, even if at this moment we're not working with real money.
Later I'll introduce the safest way to keep your private key in your project.
Interacting with the blockchain - call vs transact
We can interact with functions and variables of the Solidity code within two ways:
-
call
-
It doesn't make a state change to the blockchain (nothing on the blockchain would change). We can simply view the values, without making a state change.
-
Two examples from the SimpleStorage contract:
// this will get initialized to 0! uint256 favoriteNumber; function retrieve() public view returns(uint256) { return favoriteNumber; }
struct People { uint256 favoriteNumber; string name; } People public person = People({ favoriteNumber: 69, name: "Jakub" });
-
-
transact
-
Make a state change.
-
Two examples from the SimpleStorage contract:
// this will get initialized to 0! uint256 favoriteNumber; function store(uint256 _favoriteNumber) external { favoriteNumber = _favoriteNumber; }
// this will get initialized to 0! uint256 favoriteNumber; struct People { uint256 favoriteNumber; string name; } People public person = People({ favoriteNumber: 69, name: "Jakub" }); People[] public people; mapping(string => uint256) public nameToFavoriteNumber; function addPerson(string memory _name, uint256 _favoriteNumber) public { people.push(People(_favoriteNumber, _name)); nameToFavoriteNumber[_name] = _favoriteNumber; }
-
Setup
There are three different ways of working with this project and each way requires a different approach with certain things like (changing public/private keys, HTTPProvider, and chain_id).
- Using Ganache
- Using ganache-cli
- Using TestNet (e.g Rinkeby)
Ganache and ganache-cli are quite similar. The difference is that in ganache-cli you're using a command-line instead of the desktop app.
Additional file for environment variable
You must create a file named .env to put there your exported private key (no matter, which way above you choose). It has to be in hexadecimal version, so we put 0x at the beginning (only when you use TestNet, in ganache is right away, so check it carefully).
Below I put the private key from the first wallet, that you're gonna see when you try to run your local blockchain within ganache-cli with --deterministic flag. So feel free to copy & paste it.
export PRIVATE_KEY=0x4f3edf983ac636a65a842ce7c78d9aa706d3b113bce9c46f30d7d21715b23b1d
.env has to be in the .gitignore file! Here it's done. Remember to put it in your projects!
Specification for using ganache-cli within deploy file
Currently the deploy.py is set to run on the Rinkeby TestNet, but if you prefer to work with ganache-cli, just copy & replace the current code with the below.
# for connecting to ganache-cli
def get_w3():
return Web3(Web3.HTTPProvider("http://127.0.0.1:8545"))
def get_chain_id() -> int:
return 1337
def getPublicKey() -> str:
return "0x90F8bf6A479f320ead074411a4B0e7944Ea8c9C1"
Specification for using Rinkeby TestNet within deploy file
-
Go to Infura and create an account. After that, you have to create a new project from which you need to get HTTPProvider.
-
Here you can check ChainID for Rinkeby.
-
You need to create a crypto wallet, I suggest MetaMask.
-
Firstly extract public key, which is right below your account name in your MetaMask.
-
Secondly, take out private key. Open MM, then click on the 3 vertical dots (Accounts Options), then Account details and Export Private Key.
Here remember to add 0x at the beginning of the private key in the .env file, because it has to be in the hexadecimal version.
-
# for connecting to Rinkeby TestNet
def get_w3():
return Web3(Web3.HTTPProvider("https://rinkeby.infura.io/v3/7abda71ad2fa49b18ca946c72c6b558a"))
def get_chain_id() -> int:
return 4
def getPublicKey() -> str:
return "0xD3E4842d2bD11E18E96Ad08D2Fd6264C66A5D52f"
Installing dependencies
To clone and run this application, you'll need Git and Node.js (which comes with npm) installed on your computer. In this case, Node.js is only needed for installing a prettier-plugin for Solidity. Furthermore, you'll have to download Python 3.6+ version to install all the required packages via pip. From your command line:
# Clone this repository
$ git clone https://github.com/pawlovskiii/web3_simple_storage
# Go into the repository
$ cd web3_simple_storage
# To download all the necessarily Python packages needed for the project
$ pip install -r requirements.txt
# Install ganache-cli
$ npm install -g ganache-cli
# Install dependencies from package.json (not required)
$ npm install
Available commands for the project
Within the Ganache approach firstly open the desktop app to spin up the local blockchain (remember to check HTTPProvider address and public/private keys). If you prefer the command line, simply type the below command. With the --deterministic flag you should have the same public key as shown before here.
# Run a local blockchain (always spin up with the exact same private/public keys)
$ ganache-cli --deterministic
# In the second terminal - run the app
$ python .\main.py
If you want to use Rinkeby set this configuration and type the below command.
# Run the app
$ python .\main.py