The Brownie Python tutorial series—Part 2
The journey so far
Alright, you are about to read Part 2 of the Brownie tutorial series:
- Part 1 – Introduction.
- Part 2 – Scripts, tests, and testnets. <– You are here.
- Part 3 – Brownie deep dive.
- Part 4 – Move forward with Brownie.
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:
- Create an account using MetaMask.
- Copy the private key of the account.
- Use the following command to add a new account:
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:
- Head over to Chainstack and set up an account.
- Deploy a node in Chainstack.
Note: Since we are working on the Ethereum network, any Ethereum testnet node will be fine. This article, for instance, uses a Goerli node.
- Once you have the node up and running, get the node endpoint (HTTPS).
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”.
- Discover how you can save thousands in infra costs every month with our unbeatable pricing on the most complete Web3 development platform.
- Input your workload and see how affordable Chainstack is compared to other RPC providers.
- Connect to Ethereum, Solana, BNB Smart Chain, Polygon, Arbitrum, Base, Optimism, Avalanche, TON, Ronin, zkSync Era, Starknet, Scroll, Aptos, Fantom, Cronos, Gnosis Chain, Klaytn, Moonbeam, Celo, Aurora, Oasis Sapphire, Polygon zkEVM, Bitcoin, Tezos and Harmony mainnet or testnets through an interface designed to help you get the job done.
- To learn more about Chainstack, visit our Developer Portal or join our Discord server and Telegram group.
- Are you in need of testnet tokens? Request some from our faucets. Multi-chain faucet, Sepolia faucet, Holesky faucet, BNB faucet, zkSync faucet, Scroll faucet.
Have you already explored what you can achieve with Chainstack? Get started for free today.