The Brownie tutorial series—Part 2

The-Brownie-tutorial-series-part-2-banner

The journey so far 

Alright, you are about to read Part 2 of the Brownie tutorial series:

So far, in our journey to master the Brownie framework, we learned how to:

  • Install the Brownie package and all its dependencies.
  • Set up a Brownie project.
  • Compile contracts using Brownie. 
  • Deploy and interact with the contracts using the Brownie console.

In this article, we will see how to work with Python scripts, and we will also learn how to use actual Ethereum testnets for contract deployment and testing. 

Learning the scripts 

In Brownie, scripts enable automation. They help encapsulate all the necessary contract deployment, interaction and testing commands into a single (or multiple, your choice!) neat Python file that frees us from the incessant typing of commands. Once we create our scripts, we can use the brownie run command to automatically execute them. In this tutorial, we will see how to create two different kinds of scripts:

  • The interaction script — For contract deployment and interaction.
  • The testing script — For contract testing.

So, let’s get to it. 

A script to deploy 

Note: This article will be expanding upon our previous project (the one we created in Part 1), so if you are new, I request you to check out the previous article and set up a basic project (it will only take a few minutes!). 

In Brownie, the contract deployment and interaction scripts are stored inside the /scripts directory of the project. To create a new script, 

  • Go to the /scripts directory 
  • Create a new Python file, deploy_interact.py (or <any_other_name>.py

Now, as with the Brownie console, we need access to the contract ABI (Application Binary Interface), bytecode and an Ethereum account address to deploy our contract. In the console, we used the contractContainer object of our contract (BasicContract, remember) and the Brownie accounts object for deploying our contract. We can essentially do the same thing in our script, so do the following:

  • Open the new file in a code editor.
  • Add the following import statement.
from brownie import BasicContract , accounts 

Using the above statement, we are enabling access to the contractContainer object of our contract (which has the same name as the contract) and the Brownie accounts object. 

Now, for the deployment part, create a main function in your script and add the following code:

def main(): 
    # Fetch the account 
    account = accounts[0] 
    # Deploy contract 
    deploy_contract = BasicContract.deploy({"from":account}) 
    # Print contract address 
    print(f"contract deployed at {deploy_contract}")

Here, just like with the console, we are:

  • Accessing one of the accounts (provided by Ganache CLI) using the accounts object.
  • Calling the deploy function of the contractContainer object 
  • Passing the account as a parameter to the deploy function.
  • Returning the ProjectContract of the contract to the deploy_contract variable.

To run the script, open a terminal in your project folder and type:

brownie run deploy_interact.py #or <any_other_name>.py

The command will do the following:

  • Compile all the new/modified contracts. 
  • Spin up a local blockchain using Ganache CLI.
  • Execute the script.
  • Deploy the contract onto the local network.

Once the script is executed, you will see the following output:

Launching 'ganache-cli --accounts 10 --hardfork istanbul --gasLimit 12000000 --mnemonic brownie --port 8545'... 
 
Running 'scripts/deploy_interact.py::main'... 
Transaction sent: 0x94011b7fa3c7fedd25b589adfbd4588ba5f5df9785709ef184192376157d2f8f 
  Gas price: 0.0 gwei   Gas limit: 12000000   Nonce: 0 
  BasicContract.constructor confirmed   Block: 1   Gas used: 90551 (0.75%) 
  BasicContract deployed at: 0x3194cBDC3dbcd3E11a07892e7bA5c3394048Cc87 
 
contract deployed at 0x3194cBDC3dbcd3E11a07892e7bA5c3394048Cc87

Note: Remember that each time we use the brownie run command, Ganache is spinning up a new temporary network. Once the execution ends, the network along with all its data gets taken down. Yes, that includes the deployed contract also. (Do not worry, we will discuss persistent networks, later in the article).

OK, now that we took care of the deployment part, we can work on the contract interaction.  


There are two ways in which we can interact with the functions in our contract, we can either call them or send transactions. 

You use the call methodology to interact with the functions that do not cause any state changes (like the view functions). These interactions are free of cost and the call method executes the code without broadcasting a transaction to the network. The send method, on the other hand, is used for invoking functions that alter the state of the chain. They cost you gas, and they generate transactions that are broadcasted throughout the network. 

In our basicContract, we have, 

  • The storeNumber function — stores a number onto the chain (and alters its state).
  • The readNumber function — reads the stored number from the chain.

Let’s see how we can interact with each of these functions. We will start with storeNumber():

# Store a number 
transaction_receipt = deploy_contract.storeNumber(15, {"from":account}) 
# Wait for transaction confirmation 
transaction_receipt.wait(1) #you can change this number

Here, we are invoking the storeNumber method using the deploy_contract variable (which stores the ProjectContract object) and since the function alters the state of the chain, we need to pass the account address responsible for the transaction. The function will return a TransactionReceipt object, and in the code, we are using the wait function of the receipt object to wait for transaction confirmation. The number (1) means that we will wait for a single new block to be mined before we confirm the transaction finality.

To read the data, we can use any of the following codes:

retrieved_number = deploy_contract.readNumber.call() 
#or 
retrieved_number = deploy_contract.readNumber()

In the first statement, we are explicitly using the call method to invoke the readNumber function and in the second statement, Brownie will detect that the function is a non-state-altering function and hence it will automatically make “calls” to the function. You can use any of these statements for “calling” a function. 

Alright, once you add the whole contract interaction codes to your script, it should look something like this:

from brownie import accounts, BasicContract

def main():
    # fetch the account
    account = accounts[0]
    # deploy contract
    deploy_contract = BasicContract.deploy({"from":account})
    # print contract address
    print(f"contract deployed at {deploy_contract}")
    # store a number
    transaction_receipt = deploy_contract.storeNumber(15,{"from":account})
    # wait for transaction confirmation
    transaction_receipt.wait(1)
    # retrieve the number
    retrieved_number = deploy_contract.readNumber()
    # print the retrieved number
    print(f"Number Retrieved : {retrieved_number}")

You can run the entire script using the brownie run command, and it will do the following:

  • Deploy your contract.
  • Store a number to the chain.
  • Retrieve and display the stored number.

$ brownie run deploy_interact.py
BasicProject is the active project. 
 
Launching 'ganache-cli --accounts 10 --hardfork istanbul --gasLimit 12000000 --mnemonic brownie --port 8545'... 
 
Running 'scripts/deploy_interact.py::main'... 
Transaction sent: 0x7b5c26796992b52d3e22378ec0c484be90741a4539fa35b1d4623d20e84d3930 
  Gas price: 0.0 gwei   Gas limit: 12000000   Nonce: 0 
  BasicContract.constructor confirmed   Block: 1   Gas used: 119208 (0.99%) 
  BasicContract deployed at: 0x3194cBDC3dbcd3E11a07892e7bA5c3394048Cc87 
 
contract deployed at 0x3194cBDC3dbcd3E11a07892e7bA5c3394048Cc87 
Transaction sent: 0x9dda4a967fa7fcd1ea2cb64d9b01258ae22b4b955ea7dc916a201609da7f0b92 
  Gas price: 0.0 gwei   Gas limit: 12000000   Nonce: 1 
  BasicContract.storeNumber confirmed   Block: 2   Gas used: 41394 (0.34%) 
 
  BasicContract.storeNumber confirmed   Block: 2   Gas used: 41394 (0.34%) 
 
Number Retrieved : 15

And with that, we have deployed and interacted with our contract using a Python script. 

Now let’s do some unit testing. 

A fair bit of testing 

As mentioned in the previous article, Brownie uses the pytest framework for unit testing. This means that we can leverage the features of this tried and tested framework and write simple yet powerful test cases for our contract. 

To create our testing script:

  • Open the /tests directory in your project.
  • Create a new Python file, test_contract.py.

Note: The name of your test scripts should either begin with a “test_” or end with a “_test”.

  • Add the following “test cases” to the file:
from brownie import BasicContract, accounts

def test_default_value():
    # fetch the account
    account = accounts[0]    
    # deploy contract
    deploy_contract = BasicContract.deploy({"from":account})
    #retrieve default number
    retrieved_number = deploy_contract.readNumber()
    expected_result = 0
    assert retrieved_number == expected_result

def test_stored_value():
    # fetch the account
    account = accounts[0]
    # deploy contract
    deploy_contract = BasicContract.deploy({"from":account})
    # store a number
    transaction_receipt = deploy_contract.storeNumber(1,{"from":account})
    transaction_receipt.wait(1)
    #retrieve number
    retrieved_number = deploy_contract.readNumber()
    expected_result = 1
    assert retrieved_number == expected_result

Note: While writing the test case functions, make sure you add the word “test” at the beginning of the function name. While running the tests, Brownie will ignore the functions that do not have the “test” prefix. 

Here, we have two simple test cases for our contract, the first one (test_default_value) checks for proper contract deployment (by trying to retrieve the default value) and the second one (test_stored_value) makes sure that our storeNumber function is working properly. In both these cases, we use the assert keyword to verify the outcomes of our contract functions. 

To run the tests:

  • Open a terminal in your project directory and type:
brownie test 

Brownie will automatically detect and execute our test cases. 

============================= test session starts ==========================
platform linux -- Python 3.6.9, pytest-6.2.5, py-1.10.0, pluggy-1.0.0
rootdir: /home/sethuraman/Documents/Workspace/Chainstack/Blog-Snippets/brownie-tutorial-series/Part-1/basic-project
plugins: eth-brownie-1.16.4, hypothesis-6.21.6, forked-1.3.0, web3-5.29.2, xdist-1.34.0
collected 2 items                                                              
Launching 'ganache-cli --accounts 10 --hardfork istanbul --gasLimit 12000000 --mnemonic brownie --port 8545'...
tests/test_contract.py ..                                                [100%]
============================== 2 passed in 2.92s ============================

The output indicates that both our tests were successful, and our contract is good to go. 

To run a specific test case, use:

$ brownie test -k "<test-case-name>" 
# In our case use test_default_value* or test_stored_value

This is how we test our contracts using Python scripts. Now, let’s go a bit further and see if we could do all the same stuff atop an actual Ethereum testnet. 

Using testnets 

This section is all about moving away from the default Ganache CLI network and using some real testnets. The Ganache CLI has been quite handy and provides an easy way to deploy and test our contract, but it is all but a simulation of a blockchain network and not the real deal. To truly test the functionalities of our contract, we must put it up against an actual test network. Also, the whole “temporary” nature of the default Ganache network does prevent us from trying out some cool stuff with our contracts (more on that later), so without further ado, let us deploy our contracts onto an actual Ethereum testnet. 

Brownie offers you a ton of pre-configured network options that you can use in order to deploy and test your contract. You can view all these options by using the following command:

brownie networks list 

The command will display a long list of networks:

Ethereum
  ├─Mainnet (Infura): mainnet
  ├─Ropsten (Infura): ropsten
  ├─Rinkeby (Infura): rinkeby
  ├─Goerli (Infura): goerli
  └─Kovan (Infura): kovan
Ethereum Classic
  ├─Mainnet: etc
  └─Kotti: kotti
Arbitrum
  └─Mainnet: arbitrum-main
Avalanche
  ├─Mainnet: avax-main
  └─Testnet: avax-test
Aurora
  ├─Mainnet: aurora-main
  └─Testnet: aurora-test
Binance Smart Chain
  ├─Testnet: bsc-test
  └─Mainnet: bsc-main
Fantom Opera
  ├─Testnet: ftm-test
  └─Mainnet: ftm-main
Harmony
  └─Mainnet (Shard 0): harmony-main
Moonbeam
  └─Mainnet: moonbeam-main
Optimistic Ethereum
  ├─Mainnet: optimism-main
  └─Kovan: optimism-test
Polygon
  ├─Mainnet (Infura): polygon-main
  └─Mumbai Testnet (Infura): polygon-test
XDai
  ├─Mainnet: xdai-main
  └─Testnet: xdai-test
Development
  ├─Ganache-CLI: development
  ├─Geth Dev: geth-dev
  ├─Hardhat: hardhat
  ├─Hardhat (Mainnet Fork): hardhat-fork
  ├─Ganache-CLI (BSC-Mainnet Fork): bsc-main-fork
  ├─Ganache-CLI (FTM-Mainnet Fork): ftm-main-fork
  ├─Ganache-CLI (Polygon-Mainnet Fork): polygon-main-fork
  ├─Ganache-CLI (XDai-Mainnet Fork): xdai-main-fork
  ├─Ganache-CLI (Avax-Mainnet Fork): avax-main-fork
  └─Ganache-CLI (Aurora-Mainnet Fork): aurora-mai

The networks mentioned under the “Development” label are local, temporary networks and the other ones in the list are essentially live, non-local, persistent Ethereum or EVM-based networks (both main and testnets). To use any of these networks, we simply add the —network flag and the network identifier (the one after the colon symbol) along with the brownie run command.

brownie run deploy_interact.py  --network <network-identifier>

You can do the same with the test command:

brownie test --network <network-identifier>

Note: Actually, to use any of the live networks (and some of the fork networks), Brownie needs to connect to a node (remote or otherwise) that is part of that particular network. Unless we explicitly add the details of the nodes, Brownie won’t be able to connect to any of these networks. Fret not! We will discuss this in just a bit.

Now, in order to deal with the live networks (the ”non-development” ones) in the list, you need to make certain arrangements. From proper accounts to (test) token balances, we need to make sure that we have all these things before we get to play with the OG networks.

To set up a proper, valid account, we can actually use our trusted MetaMask wallet. Once we set up a MetaMask account, we can use the account private key and some slick Brownie commands in order to add the account into the fold of our Brownie accounts object. To try it out:

brownie accounts new my-new-account 

Here, my-new-account is the unique id for referring to the new account. You can give your own id for the account. When we execute this command, Brownie will ask us to enter the private key of the account and also prompt us for a password for encrypting the account details.

Once you generate the new account, you can view it using the following command:

brownie accounts list 

This will display all the local (ones that are stored in the system) accounts that we can access:

Brownie v1.16.4 - Python development framework for Ethereum 
 
Found 2 accounts: 
 ├─SampleAccount: 0x23d1B3E3dE8235e8b15EdD030E2D69959eE88835 
 └─my-new-account: 0xc4f02d6b1bE3804Dc8c4fDD6c2A890DbAFf60c62

To use this account in our deployment and testing scripts, all you have to do is to change the account retrieval statement in our script from:

account = accounts[0] 

to:

account = accounts.load("my-new-account") # use your own account id 

Now when we run the scripts, we will be using the newly added accounts. 

Note: Yes, you can use the newly added accounts with both the development networks and live ones. While using them, Brownie will ask us to enter the encryption password, each time we execute the scripts.

OK, now that the account is ready, let’s use a real testnet. 


As mentioned before, most of the listed networks in Brownie work by connecting to a node that is part of the given network and Brownie does come with a set of predefined node configurations. But Brownie is cool. It allows us to configure and use our own nodes for contract deployment and testing.

To set up our own node:

Note: Since we are working on the Ethereum network, any Ethereum testnet node will be fine. This article, for instance, uses a Goerli node

Now, we can use the brownie networks add command to add the new node configuration onto Brownie. The command uses the following arguments:

  • environment — the label of the network, i.e. whether it is “Ethereum”, “Ethereum Classic”, or “Development” 
  • id — a unique identifier for the network.

Note: We can also provide a separate name for our network using the “name” parameter. If we don’t provide a name, Brownie will automatically assign the id as the network name.

  • host — the node endpoint.
  • chainid — the chain identifier.

By using all these parameters, you can add a new node configuration to Brownie:

brownie networks add Ethereum goerli-chainstack host=<chainstack-node-endpoint> chainid=5 

Here, we are adding a new Goerli node under the Ethereum label with the id goerli-chainstack. The chainid for the Goerli test network is 5.

Note: If you are using a different testnet, you can find the corresponding chain IDs here. 

Now when you use the brownie networks list command, we can the new configuration under the Ethereum label. 

Ethereum
  ├─Mainnet (Infura): mainnet
  ├─Ropsten (Infura): ropsten
  ├─Rinkeby (Infura): rinkeby
  ├─Goerli (Infura): goerli
  ├─Kovan (Infura): kovan
  ├─ganache-local: ganache-local
  └─goerli-chainstack: goerli-chainstack
Ethereum Classic
  ├─Mainnet: etc
  └─Kotti: kotti
Arbitrum
  └─Mainnet: arbitrum-main
Avalanche
  ├─Mainnet: avax-main
  └─Testnet: avax-test
Aurora
  ├─Mainnet: aurora-main
  └─Testnet: aurora-test
Binance Smart Chain
  ├─Testnet: bsc-test
  └─Mainnet: bsc-main
Fantom Opera
  ├─Testnet: ftm-test
  └─Mainnet: ftm-main
Harmony
  └─Mainnet (Shard 0): harmony-main
Moonbeam
  └─Mainnet: moonbeam-main
Optimistic Ethereum
  ├─Mainnet: optimism-main
  └─Kovan: optimism-test
Polygon
  ├─Mainnet (Infura): polygon-main
  └─Mumbai Testnet (Infura): polygon-test
XDai
  ├─Mainnet: xdai-main
  └─Testnet: xdai-test
Development
  ├─Ganache-CLI: development
  ├─Geth Dev: geth-dev
  ├─Hardhat: hardhat
  ├─Hardhat (Mainnet Fork): hardhat-fork
  ├─Ganache-CLI (BSC-Mainnet Fork): bsc-main-fork
  ├─Ganache-CLI (FTM-Mainnet Fork): ftm-main-fork
  ├─Ganache-CLI (Polygon-Mainnet Fork): polygon-main-fork
  ├─Ganache-CLI (XDai-Mainnet Fork): xdai-main-fork
  ├─Ganache-CLI (Avax-Mainnet Fork): avax-main-fork
  └─Ganache-CLI (Aurora-Mainnet Fork): aurora-mai

To use the new network node, all we have to do is to give the network id as a parameter to our run/test commands.

Note: Since we are using real testnets, we need actual test tokens to deploy and test our contracts. So, before you run the scripts make sure you have a sufficient token balance in your account. You can get test tokens for your account using the various faucets available online. 

#For running the deploy_interact script 
$ brownie run deploy_interact.py --network goerli-chainstack 
  #For testing the contract 
$ brownie test --network goerli-chainstack 

With that, you have successfully used an actual Ethereum testnet for contract deployment and testing.

Note: You can find all of our scripts in this repository : Brownie tutorial—Part 2.

The usage of persistent networks allows us to further extend our deployment and testing capabilities. Using such networks, we get to mimic production-level scenarios and fine-tune our contract to make it more powerful and efficient. In the coming articles, we will see how we can leverage the full potential of these networks and build bigger and better smart contracts.

Wrap up 

From script creation to account generation and testnet usage, we have covered a lot of ground in this tutorial. All these are essentially the basic functionalities of Brownie, you can tinker around with them and further explore Brownie. 

In the next article, we will be expanding upon the testnet functionalities and we will see how we can add custom configurations for our project. We will also check out yet another cool Brownie feature called “the Brownie mix”.

  • Connect to the Ethereum, Polygon, BNB Smart Chain, Avalanche, Fantom, Solana, Harmony, Tezos and StarkNet mainnet or testnets through the interface designed to help you get the job done.
  • Get access to the Ethereum, Polygon, BNB Smart Chain, Avalanche, Fantom, and Tezos archive nodes to query the entire history of the mainnet—starting at just $49 per month.
  • Choose where you want to deploy, and we will provide you with the dedicated managed infrastructure that can handle high-volume, high-velocity read/write access to the network.
  • To learn more about Chainstack, visit our Knowledge Center or join our Discord server and Telegram group. 

Have you already explored what you can achieve with Chainstack? Get started for free today.

Chainstack uses cookies to provide you with a secure and
personalized experience on its website. Learn more.