The Brownie tutorial series—Part 3
Welcome to part 3 of the Brownie tutorial series
- Part 1 – Introduction.
- Part 2 – Scripts, tests, and testnets.
- Part 3 – Brownie deep dive. <– You are here.
- Part 4 – Move forward with Brownie.
By now, we have mastered the art of:
- Deploying and interacting with smart contracts using Python scripts.
- Writing testing scripts for your contract.
- Using actual testnets for development.
In this article, we will push the envelope further and learn some advanced features of Brownie that will let you work with persistent data, enable detailed configuration of your projects and help you “bake” a complete Brownie project, so let’s get to it.
More on testnets
Note: This article will be expanding upon the previous article, so if you are new, I request you to check out part 1 and part 2 of the series.
By the end of the previous article, we had our smart contract running atop an actual Ethereum testnet. By doing so, we are no longer limited by non-persistent networks and all its shortcomings, i.e, we no longer have to redeploy our contracts, every state change that we make is permanent and we can have complete records of our transactions. Once we deploy the contract onto a “persistent” network, Brownie stores the deployment details (artifacts) inside the /build /deployments directory. Brownie sorts the details based on the chain ID of the networks that we use for deploying our contracts.
# Path to deployment artifacts
build/deployments/[CHAINID]/[CONTRACT_ADDRESS].json
Brownie also stores a list of all the deployed contract addresses using a JSON file (map.json), which is also stored inside the /build/deployments directory. One of the major advantages of storing all this information is that it allows us to access the same (deployed) contract instance across multiple files. This means that instead of cramming the code for contract deployment and interaction into one single Python file, we can create separate files that deal with these different functionalities and run them independently, all the while, making them access the same contract instance. To try it out, go inside our /scripts folder and create two new files: deploy.py
and interact.py
.
Note: The given scripts extend the codebase of the previous article. So, make sure you have that codebase in your system.
Inside the deploy.py
file, add the following code:
from Brownie import BasicContract , accounts
def main():
# Fetch already stored account
account = accounts.load('my-new-account')
# deploy contract
deploy_contract = BasicContract.deploy({"from":account})
# print contract address
print(f"contract deployed at {deploy_contract}")
As you can see, this code exclusively deals with contract deployment. You can run this code by opening a terminal in the project root folder and typing:
$ Brownie run deploy.py --network goerli-chainstack
This will deploy the contract onto the Goerli testnet using your Chainstack node (yes, the one that we set up in the previous article).
Once deployed, you can check the /deployments folder (and the map.json
file) to see all information regarding the deployment.
Now, in our interact.py file add the following code:
from Brownie import BasicContract, accounts
def main():
# Accessing the latest deployment instance
contract_instance = BasicContract[-1]
# loading the stored account
account = accounts.load("my-new-account")
# storing a number
transaction_receipt = contract_instance.storeNumber(15, {"from": account})
# Wait for transaction confirmation
print(transaction_receipt)
# Retrieve the number
retrieved_number = contract_instance.readNumber()
# Print the retrieved number
print(f"Number Retrieved: {retrieved_number}")
Here, we have the code for interacting with the contract. As you can see, we are using the BasicContract object to access the projectContract object of the contract that we recently deployed.
contract_instance = BasicContract[-1]
The persistence of the projectContract
object is only available while using actual persistent testnets. In the above code, we are storing the projectContract
object inside the contract_instance
variable and we are using it to interact with the contract. If you have deployed multiple instances of the contract, you can access each of them separately by adjusting the index ([-1]).
The separation of contract deployment and interactions comes in handy when we are dealing with bigger contracts with tons of functions. If need be, we can create specific files for handling the various functions themselves and since we can make all these files access the same instance of the contract, it will give us a much-needed structure for the project.
To run the contract interaction code, open a terminal and type:
$ Brownie run interact.py --network goerli-chainstack
This will generate the same results as our “crammed up” (deploy_interact.py
) file, minus the deployment part.
Now that we can split and keep the different functionalities of our contract in separate files, the next step in ironing out our development process comes with the removal of the redundant stuff. For example, remember how we keep on specifying the networks while running our scripts or how we must use a long list of commands to add a new key to our accounts, well, Brownie lets us remove all these redundancies by allowing us to “configure” them. Let us put this feature to the test and see how we can configure certain things in our project.
A file to configure
Brownie allows users to define custom configurations for their projects using the Brownie configuration file. It is a YAML file that must always be saved under the name ‘brownie-config.yaml’. Users can define everything from the default network to the compiler version and even add custom information like file location, etc. inside the file. Once you create the configuration file, you can save it to the root directory of your project and Brownie will automatically load the configurations when you run the project.
To set up a configuration file for your project:
- Head over to the project root directory.
- Create a new YAML file,
brownie-config.yaml
.
Inside the YAML file, add the following lines
networks:
default: goerli-chainstack
Here, we are configuring the default network that we should be using while deploying and testing our contracts. Once you save the file, you can deploy the contract using the brownie run
command and you need not specify the network while doing so:
$ brownie run deploy.py
Since we have configured our default network, Brownie will automatically deploy the contract onto that particular network.
Another cool thing about the configuration file is that we can access the configuration details from within our python scripts. In Brownie, we use the config object to access data from the configuration file. To test it out, let us try to add some custom data like our MetaMask account private key and try to fetch it from our deploy.py
script, using the config
object.
Now, while dealing with sensitive information like our account’s private key, stating it directly in your code is not recommended. The best way to do it (in a development scenario) would be to declare it as an environment variable and to access that variable from within our configuration file:
- Create a
.env
file in the root directory of your project. - Add the following lines to your
.env
file.
export ACCOUNT_PRIVATE_KEY = 0x<YOUR-ACCOUNT-PRIVATE_KEY>
Note: While adding the private key, make sure to put a ‘0x’ in front of the key
Now, we can add the details of the .env
file inside our configuration file and Brownie will automatically load the environment variables, when we run the project. To do all this, add these lines to your YAML file:
dotenv: .env
Brownie also allows us to use POSIX-style variable expansion to access our environment variables, meaning, I can access the environment variables from within my configuration file using the given lines (don’t forget to add these lines to your YAML file):
account-keys:
private-key: ${ACCOUNT_PRIVATE_KEY}
Here, I have created a custom YAML collection, Account-keys
, and inside that I have provided a key, labeled private-key
and the value of this key is refereeing to the environment variable that we defined, using a POSIX-style expression ${ACCOUNT_PRIVATE_KEY}
.
Note: You can provide any configuration values (default network, compiler version, etc.) as an environment variable and access it using POSIX-style expressions, from inside the configuration file.
Now, to fetch the private key value from inside our script, open deploy.py
and import the config object:
from Brownie import BasicContract , accounts, config
Now, replace the following line:
account = accounts.load(“my-new-account”)
With this:
account = accounts.add(config [" account-keys "] [" private-key"])
This line will fetch the private key value from our configuration file, and it will be used as the account key. This is how you can use the config
object to access different configuration details from within the script.
Note: Here’s the complete codebase for reference: Brownie tutorial—part 3.
Know that all the fields in the Brownie configuration file are optional and while building a project, you need only configure what is necessary. The full extent of the configuration file allows you to define a lot of things like the gas limits for various networks, contract dependencies, data reports, etc.
Note: You can refer to the Brownie offical doc to learn more about the various configuration settings.
All right, with the help of the brownie-config
file, we can configure certain aspects of the project and reduce redundancies in various tasks. Now, if you have become a fan of such low-redundancy workflow, Brownie has a lot more in store for you, chief of which is a way to generate entire projects (templates) that you can modify and build upon! So, without wasting many sentences, let’s understand one of the coolest Brownie features.
Baking some code using Brownie mixes
Ok, now this is arguably the piece de resistance when it comes to Brownie features, and it is called the Brownie mixes. Brownie mixes are a collection of premade project templates that you can use as a starting point for your applications. The template will include some basic contracts, scripts, testing files and even a configuration file pertaining to the project. Using these templates, we can build complex things like DAOs or NFT applications. The code base for each of these templates is kept in the Brownie mixes GitHub repository. In order to generate any of these templates, we can use the Brownie bake command.
To get a taste of the Brownie mixes, let’s try and use a project template. For this tutorial, we will be trying to set up a basic ERC20 token project and for that, we will be using the token–mix.
To set up the project:
- Create a new directory.
- Open a terminal in the directory and type:
$ brownie bakes token-mix
This will generate a barebone Brownie project for creating ERC-20 tokens. If you go through the project structure, it will contain everything from smart contracts, scripts, test files and even a configuration file:
├── brownie-config.yaml
├── build
│ ├── contracts
│ ├── deployments
│ └── interfaces
├── contracts
│ ├── SafeMath.sol
│ └── Token.sol
├── interfaces
├── LICENSE
├── README.md
├── reports
├── requirements.txt
├── scripts
│ └── token.py
└── tests
├── conftest.py
├── test_approve.py
├── test_transferFrom.py
└── test_transfer.py
Now, even though it is just a template, this project does come with some pre-defined code for defining and generating a token. You can check it out by running
$ brownie run token.py
And with that, we generated a basic ERC-20 project without writing even a single line of code.
Wrapping up
This article was all about learning how we can extend some of the features of Brownie and tinkering with some new ones. By levering the potential of persistent networks, we saw how we structure our project better and introduce modularity to our code. We also saw the level of detailing that we can provide in a brownie project using some rich configuration options and finally, we saw how we can generate a complete Brownie project (though basic) without having to write a single line of code.
In the next part of the article, we will expand open our ERC-20 project and see how we can add a nice little user interface to it.
- 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.