Discover more from Degen Code
Introduction to Brownie
Brownie is described in their documentation as
… a Python-based development and testing framework for smart contracts targeting the Ethereum Virtual Machine.
It’s perfect for our purpose, which is to develop and test programs that interact with the Ethereum Virtual Machine (EVM for short).
Brownie is used extensively by Solidity smart contract developers because it makes testing contracts trivially easy on local machines and official Ethereum testnets. Once tested, the final deployment and verification of the smart contract can be made using Brownie.
We will not be writing or deploying any Solidity (yet), but we can still take advantage of Brownie’s built-in functionality.
Brownie, at a high level, is a framework that relies on and wraps the functionality of the web3.py module, which has a more single-minded purpose: send and receive blockchain data and interact with smart contracts.
We could use web3.py exclusively, but that entails doing a lot of extra work like managing accounts, connecting to networks by hand, keeping track of keys, signing transactions, waiting for confirmation, etc.
We can use web3.py alongside Brownie, so this isn’t an either-or question. I have not seen the need to call web3.py functions directly yet, but I will not hesitate to tap it as needed later.
A Brief Aside about Formatting
Whenever I include
monospace-formatted text inside a normal paragraph, I am highlighting a command, variable, or some programming element relevant to the topic.
Whenever I show a block of text inside a code block, it signifies some program output or input that I want you to either enter on your machine or review from mine.
This is an example of a code block. It may contain new lines and special characters, but it will be continuous from top to bottom.
I will include shell commands from a Linux / Mac / Windows terminal in a code block. Whenever you see a code block with any of these symbols, you can be reasonably assured that I’m showing you a shell command:
$ # >>>
The installation page lists two ways to install Brownie. I prefer to work with pip directly and I recommend it for you, too.
The pipx method will automatically manage the virtual environment for you, but it carries a critical downside: modules installed with pipx are only suitable for running directly. You cannot import them in a script!
Brownie installed via pipx will work normally from the command line, but attempting to use it in a script will fail with a
CRITICAL INFO ABOUT BROWNIE
I’ve edited this post nearly two years after writing it.
The Brownie package is no longer maintained, and I have moved away from using it for examples and in production.
The introductory lessons are still valuable, so I want to provide a clean method for installing it and following along. All of the code continues to work, but Brownie will no longer receive updates or maintenance, and many of its dependencies are quite old.
The cleanest method I’ve found for installing Brownie requires Python 3.9. If you are on an up-to-date system, Python 3.9 is likely not the default. Luckily you do not need to use the system Python binary, and can override Python versions on a per-directory or per-environment basis using PyEnv.
If Python 3.9 is not available to you, first follow the PyEnv installation instructions HERE.
Afterwards, use PyEnv to install Python 3.9 and create a dedicated virtual environment for use with Brownie:
devil@hades:~$ pyenv install 3.9 devil@hades:~$ pyenv virtualenv 3.9 brownie devil@hades:~$ pyenv activate brownie (brownie) devil@hades:~$
Now, install Brownie using
(brownie) devil@hades:~$ mkdir brownie_test (brownie) devil@hades:~$ cd brownie_test (brownie) devil@hades:~/brownie_test$ pip3 install eth-brownie Collecting eth-brownie Downloading eth_brownie-1.19.3-py3-none-any.whl (232 kB) ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ 232.6/232.6 kB 9.6 MB/s eta 0:00:00 Collecting packaging==21.3 Downloading packaging-21.3-py3-none-any.whl (40 kB) ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ 40.8/40.8 kB 15.8 MB/s eta 0:00:00 [...] Successfully installed aiohttp-3.8.3 aiosignal-1.2.0 asttokens-2.0.5 async-timeout-4.0.2 attrs-22.1.0 base58-2.1.1 bitarray-2.6.0 black-22.10.0 certifi-2022.9.24 charset-normalizer-2.1.1 click-8.1.3 cytoolz-0.12.0 dataclassy-0.11.1 eip712-0.1.0 eth-abi-2.2.0 eth-account-0.5.9 eth-brownie-1.19.3 eth-event-1.2.3 eth-hash-0.3.3 eth-keyfile-0.5.1 eth-keys-0.3.4 eth-rlp-0.2.1 eth-typing-2.3.0 eth-utils-1.10.0 execnet-1.9.0 frozenlist-1.3.1 hexbytes-0.2.3 hypothesis-6.27.3 idna-3.4 inflection-0.5.0 iniconfig-1.1.1 ipfshttpclient-0.8.0a2 jsonschema-3.2.0 lazy-object-proxy-1.7.1 lru-dict-1.1.8 multiaddr-0.0.9 multidict-6.0.2 mypy-extensions-0.4.3 mythx-models-1.9.1 netaddr-0.8.0 packaging-21.3 parsimonious-0.8.1 pathspec-0.10.1 platformdirs-2.5.2 pluggy-1.0.0 prompt-toolkit-3.0.31 protobuf-3.19.5 psutil-5.9.2 py-1.11.0 py-solc-ast-1.2.9 py-solc-x-1.1.1 pycryptodome-3.15.0 pygments-2.13.0 pygments-lexer-solidity-0.7.0 pyjwt-1.7.1 pyparsing-3.0.9 pyrsistent-0.18.1 pytest-6.2.5 pytest-forked-1.4.0 pytest-xdist-1.34.0 python-dateutil-2.8.1 python-dotenv-0.16.0 pythx-1.6.1 pyyaml-5.4.1 requests-2.28.1 rlp-2.0.1 semantic-version-2.10.0 six-1.16.0 sortedcontainers-2.4.0 toml-0.10.2 tomli-2.0.1 toolz-0.12.0 tqdm-4.64.1 typing-extensions-4.4.0 urllib3-1.26.12 varint-1.0.2 vvm-0.1.0 vyper-0.3.7 wcwidth-0.2.5 web3-5.31.3 websockets-9.1 wheel-0.37.1 wrapt-1.14.1 yarl-1.8.1
At this point, we have created a new directory for our basic Brownie exploration (
brownie_test) and built a new virtual environment to hold our Python executable and all of the modules needed to work with Brownie.
Wherever you see a terminal with a word in parentheses before the path, that is likely a prompt that indicates an actual Python virtual environment, e.g.:
Note that any modules installed inside a virtual environment can only be (easily) accessed by re-activating that virtual environment. If you tried to run Brownie in a new session, it would not start because that session does not know where the Brownie module is located.
Now to be sure that everything is installed correctly, run the following simple check:
(brownie) devil@hades:~/brownie_test$ brownie --version Brownie v1.19.3 - Python development framework for Ethereum
If you see that (or something very similar), you were successful and you’re ready to use Brownie!
Create a User
Brownie natively understands the concept of multiple users, private keys, and wallet addresses. It provides an internally encrypted keystore to hold account private keys securely on disk, which you can read about HERE.
We are not going to trust Brownie with any real money (yet), so we will simply ask Brownie to create a new user and give it a password to see how it works.
(brownie) devil@hades:~/brownie_test$ brownie accounts generate test_account Brownie v1.19.3 - Python development framework for Ethereum Generating a new private key... mnemonic: 'start ecology client situate rib ecology rare gaze festival leave view upper' Enter the password to encrypt this account with: SUCCESS: A new account '0xC763A439754eEB4e9D55dE9E264668272CCbE126' has been generated with the id 'test_account'
This account command generates a random private key via a 12-word mnemonic (aka seed phrase). The 12-word seed phrase approach comes from BIP-0039.
Note that this particular seed phrase should be considered COMPROMISED and INSECURE since I’ve shown it to you in plain text. I have done this only to illustrate what the command does, and I will throw the account away after writing this post.
You should have a different 12-word mnemonic, which you are free to write down or not. This account is for testing purposes only, and we are not going to send any assets to it.
NEVER SHARE YOUR 12-WORD MNEMONIC WITH ANYONE ELSE!
You can also create an account with a known private key, such as one exported from Metamask. This is useful if you want to have control over the bot’s assets without having to send & receive from the Brownie console. I prefer to keep my bot account isolated from any web browser plugins, but this is up to you.
Use the Console
Brownie comes with a very helpful
console command that will start a Python console with all of the default Brownie modules pre-loaded, plus tab-completion commands and helper functions.
Brownie comes with pre-configured settings for several networks, and we are going to use it to connect to a public API on the Avalanche network. Since Avalanche provides a public API that does not require any authentication, we’ll use it as our example. Later, we will discuss how to add and connect to any EVM-compatible blockchain network later, including ones that require API keys.
First, ensure that your Brownie version has a network called
avax-main with the following settings:
(brownie) devil@hades:~/brownie_test$ brownie networks modify avax-main Brownie v1.19.3 - Python development framework for Ethereum SUCCESS: Network 'Mainnet' has been modified └─Mainnet ├─id: avax-main ├─chainid: 43114 ├─explorer: https://api.snowtrace.io/api └─host: https://api.avax.network/ext/bc/C/rpc
Note that the
modify option does nothing by itself, only prints the current settings.
If Brownie gives you an error, or does not show exactly the settings above, enter the following command (thanks to BowTiedBeest in the comments) to add the network:
(brownie) devil@hades:~/brownie_test$ brownie networks add Avalanche avax-main host=https://api.avax.network/ext/bc/C/rpc explorer=https://api.snowtrace.io/api chainid=43114 name=Mainnet
Now, start the Brownie console with the
--network avax-main switch as follows:
(brownie) devil@hades:~/brownie_test$ brownie console --network avax-main Brownie v1.19.3 - Python development framework for Ethereum No project was loaded. Brownie environment is ready. >>>
>>> prompt signifies that you are at the Python console, and you can begin typing commands.
>>> user = accounts.load('test_account') Enter password for "test_account": >>>
We will explore more of Python later, but what we’ve done here is create a new object named
user using the
load method of the Brownie
Yikes! Lots of jargon there.
Since some of you are starting slow and learning Python as we go, you can think of
user as some “thing” known as an object. This object represents some set of related information about an account, and the object allows Python programs to get and set information about it.
In this case, we called
.load (which is an internal set of instructions called a method) with the string
‘test_account’ which instructs Brownie to load the saved private keys from our earlier account with the COMPROMISED and INSECURE generated seed phrase.
(I’m being dramatic and repetitive on purpose, because seed phrase security is important to understand.)
OK, now that we have this
user object, let’s explore its internal methods and variables using Python’s built-in
dir() method, passing in the
user object as its argument.
>>> dir(user) [address, balance, deploy, estimate_gas, gas_used, get_deployment_address, nonce, private_key, public_key, save, sign_defunct_message, sign_message, transfer] >>> user.address '0xC763A439754eEB4e9D55dE9E264668272CCbE126' >>> user.balance() 0 >>> user.private_key '0xfaeb97eb1a00d5330ed59a93a965479bdec0ca97a6cfc374bca9bdc9faffb99c' >>> user.public_key '0x0cec9869d19688e5ba98315b0f324d2d30db597fba3ece5089876f2e24890d13cec52cd8d801a424c8034fa61316d85210f76a967f0234686eb3edf8384b49a9'
The formatting does not come through in Substack, but you should notice that
public_key are dark blue and
balance is light blue. This signifies that
balance() is a method and the others are variables.
A parameter can be viewed or set just by calling its name with a dot (
A method needs to be called with an argument (options passed to the internal function). The
balance()method requires no arguments, so I call it “empty”
Again I remind you that this generated account is COMPROMISED and INSECURE, and I’ve really doubled down by showing you the
private_key variable. Holy crap! Make sure to never send any real assets to this address, because everyone can remove them now.
Plus, you have your own account that you’re going to keep secure. Right, anon?
There are other methods that we may explore later. But let’s do one more thing before we go. Let’s see the balance of an actual wallet to prove that Brownie works correctly.
>>> someone_else = network.account.PublicKeyAccount('0xB31f66AA3C1e7853 63F0875A1B74E27b85FD66c7') >>> someone_else.balance() 17274835433013535532550836
Wow that’s a big balance! Also what the heck is
'0xB31f66AA3C1e7853 63F0875A1B74E27b85FD66c7' supposed to be?
If you’re inclined to look, visit SnowTrace (Avalanche’s official block explorer) for that address and you’ll discover that this is the address for the Wrapped AVAX contract. It will naturally have a large balance since it holds AVAX (the native gas token for the Avalanche network) in exchange for WAVAX (an ERC-20 compliant version of AVAX used for token swaps).
We created the
someone_else object using Brownie’s
network.account.PublicKeyAccount() method, which provides an object allowing us to interact with an external account outside of our control. We can’t send any transactions from that account since we don’t know its private key, but we can ask the object to provide some publicly-known information, such as its stored balance.
This has been a pretty wide view of Brownie, and it illustrates how simple Brownie makes loading blockchain data into a Python program.
We will be exploring Brownie for a bit, and in the next post I will cover how to load an external smart contract into Brownie and interact with it using calls.
And as a bonus, we’ll learn why the WAVAX contract’s balance was so large and why it didn’t have any decimal places.
If you had any trouble with this lesson, please comment below and always remember that
test_account is COMPROMISED and INSECURE.
Degen Code is a reader-supported publication. To receive new posts and support my work, consider becoming a free or paid subscriber.