This is a stduy note of the Youtube videoSolidity, Blockchain, and Smart Contract Course.

1 Python Ethereum Dev Tools

  • Web3.py is a Python library for interacting with Ethereum. - Brownie, built on top of Web3.py, is a Python-based development and testing framework for smart contracts targeting the Ethereum Virtual Machine.
  • Ganache is a personal blockchain for rapid Ethereum and Corda distributed application development. You can use Ganache across the entire development cycle; enabling you to develop, deploy, and test your dApps in a safe and deterministic environment.
  • Infura: Infura API provides instant access over HTTPS and WebSockets to the Ethereum network. Ensure transactions go through smoothly and quickly at the best prices with Infura Transactions (ITX).

China ids are list in Chainlist.org.

Use pip3 install web3 to install Web3.py.

Use pip3 install solcx to install py-solc-x. The solcx is a Python module that let you interact with solc compiler. You need to install the required solc compiler before compiling code:

1
2
from solcx import install_solc
install_solc("0.8.0")

The flow to create a new contract in chain:

  • Create a contructor in Python with abi and bytecode: MyConstructor = w3.eth.contract(abi = abi, bytecode = bytecode)
  • Get the nonce: nonce = w3.eth.getTransactionCount(my_address)
  • Build a new contract, use the Python contract’s constructor().buildTransaction() with chainId, from, and nonce.
  • To sing a new contract transaction: signed_tx = w3.eth.account.sign_transaction(transaction, private_key)
  • To deploy a new contract: tx_hash = w3.eth.send_raw_transaction(signed_tx.rawTransaction)
  • To wait and get transaction receipt: tx_receipt = w3.eth.wait_for_transaction_receipt(tx_hash)

Working with a contract needs its address from its receipt and its ABI: my_contract = w3.eth.contract(address=tx_receipt.contractAddress, abi=abi). Then you can interact with the on-chain contract in two way:

  • call, no change to transaction states. Call is a simulation of making the call and getting a return value. For example: my_contract.functions.retrieve().call.
  • transact, needs gas to make a state change. you need to build transaction, sign it, deploy it, and optionally wait for it.
    • build: my_tx = my_contract.functions.my_function(arg).buildTransaction(chainId, from, nonce).
    • sign: signed_tx = w3.eth.account.sign_transaction(my_tx, private_key)
    • deploy: send_tx = w3.eth.send_raw_transaction(signed_tx.rawTransaction)
    • wait: tx_receipt = w3.eth.wait_for_transaction_receipt(send_tx).

Use yarn global add ganache-cli to install Ganache CLI. Use ganache-cli --deterministic to run Ganache CLI in deterministic way – the same accounts and private keys.

To call the mainnet or test ethereum network, use Infura to create and project and find the connection URL for each network.

The source code is below:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
from solcx import compile_standard
import json
from web3 import Web3
import os
from dotenv import load_dotenv

# read .env exports
load_dotenv()

with open("./SimpleStorage.sol", "r") as file:
    simple_storage_file = file.read()

compiled_sol = compile_standard(
    {
        "language": "Solidity",
        "sources": {"SimpleStorage.sol": {"content": simple_storage_file}},
        "settings": {
            "outputSelection": {
                "*": {"*": ["abi", "metadata", "evm.bytecode", "evm.sourceMap"]}
            }
        },
    },
    solc_version="0.8.0",
)

# with open("compiled_code.json", "w") as file:
#     json.dump(compiled_sol, file)

bytecode = compiled_sol["contracts"]["SimpleStorage.sol"]["SimpleStorage"]["evm"][
    "bytecode"
]["object"]

abi = compiled_sol["contracts"]["SimpleStorage.sol"]["SimpleStorage"]["abi"]

# setup the chain parameters
w3 = Web3(
    Web3.HTTPProvider("https://kovan.infura.io/v3/d6110844e3c04b09b1468207156c03e0")
)
chain_id = 42
my_address = "0xD288019f9d3ABaD26093efABb13793a83C279146"
private_key = os.getenv("PRIVATE_KEY")

# create the contract in Python
SimpleStorage = w3.eth.contract(abi=abi, bytecode=bytecode)

# get the latest transancation nonce
nonce = w3.eth.getTransactionCount(my_address)

transaction = SimpleStorage.constructor().buildTransaction(
    # the Ganache requires gasPrice and ChainId 1337.
    {
        "gasPrice": w3.eth.gas_price,
        "chainId": chain_id,
        "from": my_address,
        "nonce": nonce,
    }
)

signed_tx = w3.eth.account.sign_transaction(transaction, private_key)

# deploy returns transaction hash
tx_hash = w3.eth.send_raw_transaction(signed_tx.rawTransaction)

# get receipt from transaction hash. The receipt has the transaction address
tx_receipt = w3.eth.wait_for_transaction_receipt(tx_hash)

# working with a contract needs its address and its ABI
simple_storage = w3.eth.contract(address=tx_receipt.contractAddress, abi=abi)

# two ways to interact with a transaction
# 1. call, no change to transaction states.
# Call is a simulation of making the call and getting a return value
# 2. transact, try to make a state change. You can transact a view.
init_favorite_number = simple_storage.functions.retrieve().call()
print(init_favorite_number)

# no change in state
print(simple_storage.functions.store(17).call())

store_transaction = simple_storage.functions.store(37).buildTransaction(
    {
        "gasPrice": w3.eth.gas_price,
        "chainId": chain_id,
        "from": my_address,
        "nonce": nonce + 1,
    }
)
signed_store_tx = w3.eth.account.sign_transaction(
    store_transaction, private_key=private_key
)
send_store_tx = w3.eth.send_raw_transaction(signed_store_tx.rawTransaction)
tx_receipt = w3.eth.wait_for_transaction_receipt(send_store_tx)

favorite_number = simple_storage.functions.retrieve().call()

2 The Brownie Framework

Working with different chains and different contracts requires a lot of low level tasks. Brownie is a Python-based development and testing framework for smart contracts targeting the Ethereum Virtual Machine. Popular products such as Yearn Finance (lending management), Curve.Fi (exchange), Badger.com (bitcoin yield DAO) use the Brownie framework.

Use pipx to install brownie as the following:

1
2
3
4
5
6
7
8
9
python3 -m pip install --user pipx
python3 -m pipx ensurepath

# after close and reopen the terminal
pipx install eth-brownie

## close and reopen the terminal, check

brownie --version

Then use brownie init to intialize a project in the current folder. Put contracts into the contracts folder. Run brownie compile to compile contracts and put outputs in build/contracts folder.

Put deploy scripts in the scripts folder and use brownie run my_script.py to run the Python script. Brownie will spin up a local Ganache CLI by default as the deploy Chain.

Brownie performs many tasks automatically such as open a contract file, compile it and connect to a chain. We only need to provide an address and a private key. Use brownie accounts new my-account to create an account named my-account with a specified private key and password. Then you can load an account with account = accounts.load("my-account").

Alternatively, you can load a private key from an environment variable. First, export your private key in .env file. Then config brownie-config.yaml with dotenv: .env. Use the code account = accounts.add(os.getenv("PRIVATE_KEY")) to get the private key. You can also define wallets in Brownie configuration file. Then use brownie’s config to get an account.

The code to deploy, read and change data using Brownie is as following:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
from brownie import accounts, config, SimpleStorage, network


def deploy_simple_storage():
    account = get_account()
    simple_storage = SimpleStorage.deploy({"from": account})
    print(simple_storage)

    stored_value = simple_storage.retrieve()
    print(stored_value)

    transaction = simple_storage.store(15, {"from": account})
    transaction.wait(1)
    updated_value = simple_storage.retrieve()
    print(updated_value)


def get_account():
    if network.show_active() == "development":
        return accounts[0]
    else:
        return accounts.add(config["wallets"]["from_key"])


def main():
    deploy_simple_storage()

To run it in a specifed network such as kovan, run brownie run deploy.py --network kovan. If the network is not a development network, Brownie creates a record in the deployments/42 where 42 is the chain id of kovan.