Brownie

Setup eth-brownie

Let us build a simple web3 application using Brownie and Ganache for simple storage application which stores any given value.

create a folder for project and then open vs code in that directory.

mkdir brownie_simple_storage
code brownie_simple_storage

(Recommended) Install brownie with pipx for virtual environment.

Just go to the terminal and run the following:

python3 -m pip install --user pipx
python3 -m pipx ensurepath 

And then install brownie.

pipx install eth-brownie

Check if installed correctly using

brownie --version

Setup Ganache

Install ganache from it’s website and spawn a local blockchain for development purposes.

Create a Project

Required steps:

  1. Create folder structure
  2. Write a contract
  3. Compile the contract
  4. Write deployment script

Run brownie init to create a series of folders for project. It may create the following folders:

+ build 
    + contracts     (saves all built contracts)
    + deployments   (automates all deployments)
    + interfaces    
+ contracts         (all contracts are written here)
+ interfaces        (makes it super easy to interact with other apps)
+ reports           (basic reports)
+ scripts           (automation scripts)
+ tests             (tests all contracts)
+ .gitattributes
+ .gitignore

Then write your contract in the contracts folder in main level.

contracts/SimpleStorage.sol

// SPDX-License-Identifier: MIT

pragma solidity >=0.6.0 <0.9.0;

contract SimpleStorage {

    uint256 favoriteNumber;

    // This is a comment!
    struct People {
        uint256 favoriteNumber;
        string name;
    }

    People[] public people;
    mapping(string => uint256) public nameToFavoriteNumber;

    function store(uint256 _favoriteNumber) public {
        favoriteNumber = _favoriteNumber;
    }
    
    function retrieve() public view returns (uint256){
        return favoriteNumber;
    }

    function addPerson(string memory _name, uint256 _favoriteNumber) public {
        people.push(People(_favoriteNumber, _name));
        nameToFavoriteNumber[_name] = _favoriteNumber;
    }
}

and then run

brownie compile

You may see json file for the contracts in the contracts dir.

Deployment Script

goto scripts folder and write a simple deployment script.

In web3.py version we did everything manually which are automated for us.

  • Compilation
  • Dumped to a file
  • ABI & Bytecode
  • Spawing & stopping ganache
  • Get private & public addresses
  • deploy simple storage

scripts/deploy.py

def deploy_simple_storage():
    ...

def main():
    deploy_simple_storage()

and run brownie run scripts/deploy.py

brownie independantly create a ganache server and turns it down after use. Everything is made simple for us.

Here, we need to add our private account (which will be encrypted by brownie). Go to terminal and run:

brownie accounts new {name-of-account}

It will ask to enter your private key (securely), export it from metamask and paste it in the terminal input section.

Now, we will have a new account natively integrated with brownie. See it by running

brownie accounts list

Remove any account by running:

brownie accounts delete {my-account-name-2}

Now, just access the account def deploy_simple_storage by using the following code. Note that it will ask for private key to match the password and by following this approach we are not exposing our keys in github history / internet.

scripts/deploy.py

from brownie import accounts

def deploy_simple_storage():
    account = accounts.load("my-account-name")
    print("safely encrypted:", account) 
    # the above line will pront the public key, not private.

    # if you want to randomly use any account, you may
    # instead use
    account = accounts[0]
    print("randomly generated:", account) 

def main():
    deploy_simple_storage()

And then run brownie run scripts/deploy.py.

Another way to do this (less securely) is to use .env files and mapping the file using brwonie-config.yaml in main directory. If done so, we just need to use inside brwonie-config.yaml:

dotenv: .env

inside .env

export PRIVATE_KEY=0xhasbdjjuwfoiQDOIWCFOIACS324
from brownie import accounts

def deploy_simple_storage():
    account = accounts.add(os.getenv("PRIVATE_KEY))

Alternatively by adding couple of lines in the brwonie-config.yaml file, we can do the same thing more explicitly.

dotenv: .env
wallets:
    from_key: ${PRIVATE_KEY}
from brownie import config
from brownie import accounts

def deploy_simple_storage():
    account = accounts.add(config["wallets"]["from_key"])

Deploy

Brownie is pretty intelligent, just import the written contract by name and then deploy it using .deploy("from": account). Now, use the deploy.py script to do so:

from brownie import config, accounts, SimpleStorage

def deploy_simple_storage():
    # grab account address
    account = accounts[0]
    
    # transaction needs from account 
    simple_storage = SimpleStorage.deploy("from": account)
    
    # call does not need from account
    stored_value = simple_storage.retrieve()
    print(stored_value)

    # transaction
    transaction = simple_storage.store(15, {"from":account})
    transaction.wait(1) # 1 is number of blocks

    # check updates
    stored_value = simple_storage.retrieve()
    print(stored_value)

There are two types of functions - either call or transaction. View functions are call functions (eg. retrieve) that do not change the state of blockchain and just shows number.

Tests

Goto the tests directory and create test_simple_storage.py. Good tests are essential so let us write some tests for our contract. A test can be divided into one of these categories

tests/test_simple_storage.py

from brownie import accounts, SimpleStorage

def test_deploy():
    # arrange
    account = accounts[0]

    # act
    simple_storage = SimpleStorage.deploy({"from": account})
    starting_value = simple_storage.retrieve()
    expected_value = 0

    # assert
    assert starting_value == expected_value

def test_updating_storage():
    # arrange
    account = accounts[0]
    simple_storage = SimpleStorage.deploy({"from": account})

    # act
    expected_value = 15
    transaction = simple_storage.store(expected_value, {"from":account})

    # assert
    assert expected_value == simple_storage.retrieve()

And then run

brownie test

to run all tests. Otherwise run brownie test -k test_updating_storage for running specific test. If you want to debug by spawning a terminal when test case fails, use:

brownie test --pdb

Deploy to Test Network