An effective way to expand your skills is taking what you already know and attempting to do it with new tools.
After nearly two years playing with Brownie, we should be very familiar with doing the following tasks:
Creating an account with private key and address
Connecting to a blockchain on the console
Loading contracts from a block explorer
Starting a local fork
Reading data from a contract
Deploying a contract
Sending transactions
This lesson will explore each of these tasks using the Ape Framework.
What is Ape Framework?
Ape Framework (referred to hereafter as Ape) is really two things: a standalone command line tool, and a software development kit (SDK). The standalone tool allows you to use configure and run Ape “on rails” via predefined commands. The SDK exposes more of the internals of Ape and allows you to write apps and scripts that drive Ape remotely.
Brownie operates in much the same way, and degenbot started as a wrapper over the Brownie SDK, allowing me to automate Uniswap arbitrage trading.
Ape is built with similar goals, but improves the platform by building on a modular concept. Where Brownie is a monolithic code base, Ape consists of a core plus modules. The Ape team publishes several official plugins, and the core provides a clean way to publish and use 3rd-party plugins.
At this time, Ape is at version 0.6.21.
I encourage you to refer to the Ape documentation frequently. I will repeat some key points as we go, but the official docs are the gold standard and you should expect some things to change with new releases.
Installing Ape
I recommend creating a dedicated Python virtual environment for specific tools. I use the official venv
Python module to manage my virtual environments, and pyEnv to manage specific Python versions.
Ape supports Python versions 3.8 through 3.11.
Here, let’s create a directory and a virtual environment, then activate it:
[btd@main ~]$ mkdir -p ~/code/ape_sandbox
[btd@main ~]$ cd ~code/ape_sandbox
[btd@main ape_sandbox]$ python3 --version
Python 3.11.5
[btd@main ape_sandbox]$ python3 -m venv .venv
[btd@main ape_sandbox]$ source .venv/bin/activate
(.venv) [btd@main ape_sandbox]$
Now install Ape with a set of official plugins:
[btd@main ape_sandbox]$ pip install eth-ape'[recommended-plugins]'
[...]
Successfully installed [...]
If you encounter build issues, make sure that you’ve installed the appropriate Python development package for your distribution. On Fedora, that is python3-devel
, and on Ubuntu it is python3-dev
.
Test that Ape runs on the command line:
(.venv) [btd@main ape_sandbox]$ ape --version
0.6.21
Creating and Importing Accounts
Ape accounts are created similarly to Brownie. Here let’s create and then remove a test account:
(.venv) [btd@main ape_sandbox]$ ape accounts generate test_acct
Add extra entropy for key generation...:
Show mnemonic? [Y/n]: y
INFO: Newly generated mnemonic is: bonus crater replace blood cloth image they enter gather power position about
Create Passphrase to encrypt account:
Repeat for confirmation:
SUCCESS: A new account '0xb08f5106888D101d7F1C9cfa2CC9E1eDc8513E60' with HDPath m/44'/60'/0'/0/0 has been added with the id 'test_acct'
The step that prompts for extra entropy expects you to type random characters to generate more random numbers for the private key algorithm. So type for a while and press enter when done.
You cannot enter an empty pass phrase, so write yours down and remember it.
Please note that the mnemonic for the account above is considered COMPROMISED and INSECURE. It is for demonstration only, and this account should never be used for production purposes.
Now let’s delete the account:
(.venv) [btd@main ape_sandbox]$ ape accounts delete test_acct
Enter passphrase to delete 'test_acct' []:
SUCCESS: Account 'test_acct' has been deleted
And then re-import it from the mnemonic:
(.venv) [btd@main ape_sandbox]$ ape accounts import test_acct --use-mnemonic
Enter mnemonic seed phrase:
Create Passphrase to encrypt account:
Repeat for confirmation:
SUCCESS: A new account '0xb08f5106888D101d7F1C9cfa2CC9E1eDc8513E60' has been added with the id 'test_acct'
Note that when you’re entering the seed phrase, it will not be displayed on screen. Be careful when typing the seed phrase, or use copy-paste.
You’ll notice that the imported account has the same address as the randomly-generated original. There is an --hd-path
argument that you can use for custom derivation paths, if you need it.
Importing Accounts from Brownie
Ape uses the same internal account format as Brownie, so accounts you’ve already created can be directly re-used.
Brownie’s accounts are stored in ~/.brownie/accounts
in JSON format. Ape’s accounts are stored in ~/.ape/accounts
in JSON format. You can copy the JSON files straight across and get access to them in Ape.
Here, check the current list of Ape accounts, copy the Brownie accounts, then confirm they are available in Ape:
(.venv) [btd@main ape_sandbox]$ ape accounts list
Found 1 account:
0xb08f5106888D101d7F1C9cfa2CC9E1eDc8513E60 (alias: 'test_acct')
(.venv) [btd@main ape_sandbox]$ cp ~/.brownie/accounts/*.json ~/.ape/accounts/
(.venv) [btd@main ape_sandbox]$ ape accounts list
Found 4 accounts:
0xb08f5106888D101d7F1C9cfa2CC9E1eDc8513E60 (alias: 'test_acct')
0x[...............redacted...............] (alias: 'arbitrum_bot')
0x[...............redacted...............] (alias: 'flashbots_id')
0x[...............redacted...............] (alias: 'mainnet_bot')
When sending transactions from Ape, you will be prompted for the passphrase the first time. You may then leave the account unlocked, and Ape will cache the private key for use again during the session.
This differs from Brownie, which prompts for the password at the time of account loading, and then never again.
You can approximate the behavior in Ape by using the unlock
method after loading:
In [1]: acct = accounts.load('test_acct')
In [2]: acct.unlock()
Enter passphrase to permanently unlock 'test_acct':
When this is done, the key is stored in a private variable called __cached_key
, which is mangled slightly by the Python interpreter with a prefix of an underscore, followed by the object’s class name. The key is managed by the KeyfileAccount class, so if you really need to access the private key for a file, you can do it like this:
In [3]: acct._KeyfileAccount__cached_key
Out[3]: HexBytes('0x2da30966e71071b676812b2c0c4154c104d7c6236bdbd7ed031f98a8a3e06b5d')
You can lock the account again with the lock
method, but the value stored in the __cached_key
attribute will not be removed!
The Big Idea
I’ll get into the weeds below, but I want you to keep a simple idea in mind as you read.
In Ape, everything is a plugin.
Ape is highly flexible, but this flexible behavior causes frustration for those just learning to use it. Much of the behavior you’d expect to just work will do so, under two tightly controlled conditions:
You have the plugin installed to “do the thing”
The plugin is configured correctly
You will quickly fall off the happy path when you don’t have a plugin, or you have the plugin but it is not configured correctly.
Error messages are not particularly helpful either, since most configuration exceptions are related to Pydantic validation.
Plugin configuration is set by editing a text file, but the official documentation’s Configuring Ape page is quite short and the YAML file format is not clearly explained there.
Examples of editing the configuration file are peppered across other pages and in the README files of various plugin repos on Github.
Luckily most plugins are small enough that you can read the source code to discover how they work and make reasonable guesses about setting their configuration options.
Ape is still in active development, so things are likely to change and the documentation should improve a lot. I highly recommend joining the ApeWorX Discord and asking questions — the dev team is highly responsive and very helpful.
Connecting to a Blockchain Network
Ape is natively multi-chain, which is a huge improvement over Brownie. Brownie uses singleton chain
and network
objects, which provide communication and access to the network. Most of the time this is just fine, but using Brownie with multi-chain requires several clunky hacks that I’d rather not cover.
Connecting on the console requires a network to be specified. The network specification is a semicolon-delimited string with format ecosystem:network:provider
.
As an example, to connect to mainnet Ethereum, the network specification is ethereum:mainnet:[provider]
. The provider specification is handled by a provider plugin.
The Ape team has created two plugins for immediate use: alchemy
and infura
. The Ape core also includes a geth
provider that can be used for any endpoint that supports the standard JSON-RPC methods.
Network Configuration
View the networks included by default:
(.venv) [btd@main ape_sandbox]$ ape networks list
ethereum (default)
├── mainnet
│ ├── alchemy
│ ├── geth (default)
│ └── infura
├── mainnet-fork
│ ├── foundry (default)
│ └── hardhat
├── goerli
│ ├── alchemy
│ ├── geth (default)
│ └── infura
├── goerli-fork
│ ├── foundry (default)
│ └── hardhat
├── sepolia
│ ├── alchemy
│ ├── geth (default)
│ └── infura
├── sepolia-fork
│ ├── foundry (default)
│ └── hardhat
└── local (default)
├── foundry
├── hardhat
├── geth
└── test (default)
Network are added via YAML configuration files. You can specify global options in ~/.ape/networks/ape-config.yaml
, or project-specific options in some local directory.
To illustrate, add a geth
provider for the ethereum
ecosystem and mainnet
network. Set the endpoint for the geth
plugin to the free Ankr endpoint:
(.venv) [btd@main ape_sandbox]$ vim ~/.ape/ape-config.yaml
### ape-config.yaml
default_ecosystem: ethereum
ethereum:
mainnet:
default_provider: geth
geth:
ethereum:
mainnet:
uri: https://rpc.ankr.com/eth
###
Now launch Ape with the appropriate network option and confirm that the RPC is synced up with the chain:
(.venv) [btd@main ape_sandbox]$ ape console --network ethereum:mainnet:geth
INFO: Connecting to existing Geth node at https://rpc.ankr.com/[hidden].
In [1]: chain.blocks.head.number
Out[1]: 18300101
The Ape core supports the Ethereum ecosystem and a limited set of networks.
Ecosystems:
ethereum
Networks (reference):
mainnet
mainnet_fork
goerli
goerli_fork
sepolia
sepolia_fork
local
Ecosystems and networks are added by including additional plugins.
If you want to work on Fantom, for example, install the appropriate plugin and the fantom
will be recognized as an ecosystem:
(.venv) [btd@main ape_sandbox]$ ape plugins install fantom
INFO: Installing fantom...
SUCCESS: Plugin 'fantom' has been installed.
(.venv) [btd@main ape_sandbox]$ ape networks list
ethereum (default)
[...]
### THIS ECOSYSTEM IS NEW
fantom
├── opera
│ └── geth (default)
├── opera-fork
│ ├── hardhat (default)
│ └── foundry
├── testnet
│ └── geth (default)
├── testnet-fork
│ ├── hardhat (default)
│ └── foundry
└── local (default)
├── hardhat
├── test (default)
└── foundry
There are similar ecosystem plugins for Avalanche, Starknet, Arbitrum, and many others.
Ad-Hoc Networking
If you don’t need to fully define a provider, you can use an HTTP(S) endpoint as the third option. This will do an in-place substitution of the URI option to the geth
provider. This is useful for scripts and quick testing:
(.venv) [btd@main ape_sandbox]$ ape console --network fantom:opera:https://rpc.ankr.com/fantom
WARNING: Connecting Geth plugin to non-Geth client 'go-opera'.
In [1]: chain.blocks.head.number
Out[1]: 69133283
I’ve done zero configuration for the fantom
plugin above, so this is a useful demonstration of how ad-hoc networking can be used before a permanent configuration can be defined.
Creating a Project
Ape can be used with a project workflow. We’ve been working in the ape_sandbox
directory, but it’s otherwise empty. Let’s create an Ape project:
(.venv) [btd@main ape_sandbox]$ ape init
Please enter project name: Ape Sandbox
SUCCESS: Ape Sandbox is written in ape-config.yaml
Initializing a project will create several empty directories, named appropriately for what they contain:
contracts
scripts
tests
Loading Contracts
Ape provides interaction with external contracts using a similar approach to Brownie. You may either provide the contract itself, a complete specification of the contract in JSON, or fetch the verified source from a block explorer.
The early Brownie lessons focused on getting familiar with blockchain interaction through the console. Let’s try that again with a focus on Ethereum mainnet.
(.venv) [btd@main ape_sandbox]$ ape console --network ethereum:mainnet
INFO: Connecting to existing Erigon node at https://rpc.ankr.com/[hidden].
In [1]: chain.blocks.head.number
Out[1]: 18300196
In [2]: weth = Contract('0xC02aaA39b223FE8D0A0e5C4F27eAD9083C756Cc2')
Now let’s check the WETH balance of our favorite Uniswap V3 pool (WBTC-WETH):
In [3]: weth.balanceOf('0xCBCdF9626bC03E24f779434178A73a0B4bad62eD')
Out[3]: 90688833729806347278830
# Close the session with CTRL+d
In [4]:
Do you really want to exit ([y]/n)? y
When we created the weth
object via the Contract
method, Ape did some helpful stuff behind the scenes. It recognized that the address we provided was not part of a previous deployment, so it looked for an appropriate plugin to retrieve information about that contract. It used the etherscan
plugin to retrieve contract type info, then saved it inside a special directory specific to the particular ecosystem and network.
Take a look in ~/.ape
and you’ll notice a new ethereum
directory. Follow the trail and you’ll find a JSON file labeled with the WETH contract address:
(.venv) [btd@main ape_sandbox]$ ls ~/.ape/ethereum/mainnet/contract_types/
0xC02aaA39b223FE8D0A0e5C4F27eAD9083C756Cc2.json
Check out the contents:
(.venv) [btd@main ape_sandbox]$ cat ~/.ape/ethereum/mainnet/contract_types/0xC02aaA39b223FE8D0A0e5C4F27eAD9083C756Cc2.json
{
"abi": [
{
"inputs": [],
"name": "name",
"outputs": [{"name": "", "type": "string"}],
"stateMutability": "view",
"type": "function",
},
{
"inputs": [
{"name": "guy", "type": "address"},
{"name": "wad", "type": "uint256"},
],
"name": "approve",
"outputs": [{"name": "", "type": "bool"}],
"stateMutability": "nonpayable",
"type": "function",
},
{
"inputs": [],
"name": "totalSupply",
"outputs": [{"name": "", "type": "uint256"}],
"stateMutability": "view",
"type": "function",
},
{
"inputs": [
{"name": "src", "type": "address"},
{"name": "dst", "type": "address"},
{"name": "wad", "type": "uint256"},
],
"name": "transferFrom",
"outputs": [{"name": "", "type": "bool"}],
"stateMutability": "nonpayable",
"type": "function",
},
{
"inputs": [{"name": "wad", "type": "uint256"}],
"name": "withdraw",
"outputs": [],
"stateMutability": "nonpayable",
"type": "function",
},
{
"inputs": [],
"name": "decimals",
"outputs": [{"name": "", "type": "uint8"}],
"stateMutability": "view",
"type": "function",
},
{
"inputs": [{"name": "", "type": "address"}],
"name": "balanceOf",
"outputs": [{"name": "", "type": "uint256"}],
"stateMutability": "view",
"type": "function",
},
{
"inputs": [],
"name": "symbol",
"outputs": [{"name": "", "type": "string"}],
"stateMutability": "view",
"type": "function",
},
{
"inputs": [
{"name": "dst", "type": "address"},
{"name": "wad", "type": "uint256"},
],
"name": "transfer",
"outputs": [{"name": "", "type": "bool"}],
"stateMutability": "nonpayable",
"type": "function",
},
{
"inputs": [],
"name": "deposit",
"outputs": [],
"stateMutability": "payable",
"type": "function",
},
{
"inputs": [
{"name": "", "type": "address"},
{"name": "", "type": "address"},
],
"name": "allowance",
"outputs": [{"name": "", "type": "uint256"}],
"stateMutability": "view",
"type": "function",
},
{"stateMutability": "payable", "type": "fallback"},
{
"anonymous": false,
"inputs": [
{"indexed": true, "name": "src", "type": "address"},
{"indexed": true, "name": "guy", "type": "address"},
{"indexed": false, "name": "wad", "type": "uint256"},
],
"name": "Approval",
"type": "event",
},
{
"anonymous": false,
"inputs": [
{"indexed": true, "name": "src", "type": "address"},
{"indexed": true, "name": "dst", "type": "address"},
{"indexed": false, "name": "wad", "type": "uint256"},
],
"name": "Transfer",
"type": "event",
},
{
"anonymous": false,
"inputs": [
{"indexed": true, "name": "dst", "type": "address"},
{"indexed": false, "name": "wad", "type": "uint256"},
],
"name": "Deposit",
"type": "event",
},
{
"anonymous": false,
"inputs": [
{"indexed": true, "name": "src", "type": "address"},
{"indexed": false, "name": "wad", "type": "uint256"},
],
"name": "Withdrawal",
"type": "event",
},
],
"contractName": "WETH9",
}
It’s a simple dictionary with two keys: abi
and contractName
.
The next time you interact with the WETH contract at this address on Ethereum mainnet, this JSON will be used and the Etherscan fetch will be skipped.
If you want to load a contract from an ABI directly, you can do that by passing an abi=
argument to the Contract()
constructor. Ape supports a raw JSON string or a formatted Python list.
Let’s take the string from Etherscan and try:
(.venv) [btd@main ape_sandbox]$ ape console --network ethereum:mainnet
INFO: Connecting to existing Geth node at https://rpc.ankr.com/[hidden].
In [1]: weth = Contract(
address='0xC02aaA39b223FE8D0A0e5C4F27eAD9083C756Cc2',
abi='''
[{"constant":true,"inputs":[],"name":"name","outputs":[{"name":"","type":"string"}],"payable":false,"stateMutability":"view","type":"function"},{"constant":false,"inputs":[{"name":"guy","type":"address"},{"name":"wad","type":"uint256"}],"name":"approve","outputs":[{"name":"","type":"bool"}],"payable":false,"stateMutability":"nonpayable","type":"function"},{"constant":true,"inputs":[],"name":"totalSupply","outputs":[{"name":"","type":"uint256"}],"payable":false,"stateMutability":"view","type":"function"},{"constant":false,"inputs":[{"name":"src","type":"address"},{"name":"dst","type":"address"},{"name":"wad","type":"uint256"}],"name":"transferFrom","outputs":[{"name":"","type":"bool"}],"payable":false,"stateMutability":"nonpayable","type":"function"},{"constant":false,"inputs":[{"name":"wad","type":"uint256"}],"name":"withdraw","outputs":[],"payable":false,"stateMutability":"nonpayable","type":"function"},{"constant":true,"inputs":[],"name":"decimals","outputs":[{"name":"","type":"uint8"}],"payable":false,"stateMutability":"view","type":"function"},{"constant":true,"inputs":[{"name":"","type":"address"}],"name":"balanceOf","outputs":[{"name":"","type":"uint256"}],"payable":false,"stateMutability":"view","type":"function"},{"constant":true,"inputs":[],"name":"symbol","outputs":[{"name":"","type":"string"}],"payable":false,"stateMutability":"view","type":"function"},{"constant":false,"inputs":[{"name":"dst","type":"address"},{"name":"wad","type":"uint256"}],"name":"transfer","outputs":[{"name":"","type":"bool"}],"payable":false,"stateMutability":"nonpayable","type":"function"},{"constant":false,"inputs":[],"name":"deposit","outputs":[],"payable":true,"stateMutability":"payable","type":"function"},{"constant":true,"inputs":[{"name":"","type":"address"},{"name":"","type":"address"}],"name":"allowance","outputs":[{"name":"","type":"uint256"}],"payable":false,"stateMutability":"view","type":"function"},{"payable":true,"stateMutability":"payable","type":"fallback"},{"anonymous":false,"inputs":[{"indexed":true,"name":"src","type":"address"},{"indexed":true,"name":"guy","type":"address"},{"indexed":false,"name":"wad","type":"uint256"}],"name":"Approval","type":"event"},{"anonymous":false,"inputs":[{"indexed":true,"name":"src","type":"address"},{"indexed":true,"name":"dst","type":"address"},{"indexed":false,"name":"wad","type":"uint256"}],"name":"Transfer","type":"event"},{"anonymous":false,"inputs":[{"indexed":true,"name":"dst","type":"address"},{"indexed":false,"name":"wad","type":"uint256"}],"name":"Deposit","type":"event"},{"anonymous":false,"inputs":[{"indexed":true,"name":"src","type":"address"},{"indexed":false,"name":"wad","type":"uint256"}],"name":"Withdrawal","type":"event"}]
'''
)
In [2]: weth.balanceOf('0xCBCdF9626bC03E24f779434178A73a0B4bad62eD')
Out[2]: 90688833729806347278830
Forking
Forking from a live chain can be done through (you guessed it) a specialized network plugin. I like the Foundry plugin for this purpose, but Hardhat is also available.
Edit the global Ape config file to include a fork network for Ethereum mainnet:
ape-config.yaml
default_ecosystem: ethereum
ethereum:
default_network: mainnet
mainnet:
default_provider: geth
mainnet_fork:
default_provider: foundry
geth:
ethereum:
mainnet:
uri: https://rpc.ankr.com/eth
foundry:
host: auto
The foundry
plugin will use the default ecosystem and network, so the Anvil process started by the provider will connect to the path specified in the geth
section.
If you want to run the fork with explicit options, or to connect to a different ecosystem, specify a full set of options starting with the fork
key, indented to follow the ecosystem-network-provider schema. Finally setting a path to the host with upstream_provider
:
foundry:
host: auto
fork:
ethereum:
mainnet:
upstream_provider: http://localhost:8545/
Launching this way will preserve the mainnet defaults while connecting to a different provider for forks. This is useful when you’re doing specific work with differing node requirements (such as a full node for production, and archive node for slower queries and testing).
Launch the fork using the --network ethereum:mainnet-fork:foundry
switch:
(.venv) [btd@main ape_sandbox]$ ape console --network ethereum:mainnet-fork:foundry
INFO: Starting 'anvil' process.
INFO: Connecting to existing Geth node at https://rpc.ankr.com/[hidden].
In [1]: chain.blocks.head.number
Out[1]: 18308436
[... a few minutes later, the chain height is the same]
In [2]: chain.blocks.head.number
Out[2]: 18308436
Reading Data from Contracts
Interacting with a contract object is very similar to Brownie. Functions and events are represented attributes and methods attached to the contract object. Functions accept arguments that match the arguments in the smart contract.
The Ape console uses IPython, so you can get some help while you’re in it.
To see the arguments required for the WETH contract’s balanceOf
method, for example, add a question mark to the end of the function:
In [1]: weth.balanceOf?
Signature: weth.balanceOf(*args, **kwargs) -> Any
Type: ContractCallHandler
String form: balanceOf(address) -> uint256
File: ~/code/ape_sandbox/.venv/lib64/python3.11/site-packages/ape/contracts/base.py
Docstring: <no docstring>
Inspecting the “String form” reveals that balanceOf
takes an address
argument and returns a uint256
.
Multicall
Ape provides a multicall
object that can be used to group calls. They are lazy-loaded, which means you can add several calls together before calling for them to be read all together.
For example, let’s get the WETH balance for several Uniswap pools.
Calls can be added to the multicall using the add
method. The method expects arguments in the form of call, *args
. This different from the standalone function call because it is split into pieces. Consider this example:
In [1]: call.add(weth.balanceOf,'0xCBCdF9626bC03E24f779434178A73a0B4bad62eD')
In [2]: call.add(weth.balanceOf,'0x4e68Ccd3E89f51C3074ca5072bbAC773960dFa36')
In [3]: call.add(weth.balanceOf,'0xe8c6c9227491C0a8156A0106A0204d881BB7E531')
In [4]: list(call())
Out[4]: [91300815337915950280759, 23580218276884357375118, 4560343146754211682372]
For those less familiar with Python, *args
means that all of the arguments after the first one will be packed together into a list
. The multicall object handles calling each function, which is why we only provided the function reference and individual arguments, instead of a full explicit call.
Deploying Contracts
Let’s write a simple Hello World contract in Vyper, then deploy it to a fork:
hello_world.vy
#pragma version ^0.3
OWNER: immutable(address)
greeting: public(String[24])
@external
@nonpayable
def __init__(greeting: String[24]):
OWNER = msg.sender
self.greeting = 'Hello, World!'
A difference between Ape and Brownie is that contract compilation is not automatic. You must explicitly compile your contracts using ape compile
:
(.venv) [btd@main ape_sandbox]$ ape compile
INFO: Compiling 'hello_world.vy'.
Afterwards, you can deploy this to a fork or local test provider:
(.venv) [btd@main ape_sandbox]$ ape console --network ethereum:mainnet-fork:foundry
INFO: Starting 'anvil' process.
INFO: Connecting to existing Geth node at https://rpc.ankr.com/[hidden].
In [1]: deployer = accounts.test_accounts[0]
In [2]: hello_world = deployer.deploy(project.hello_world,"Hello, World!")
WARNING: There are no token lists installed
ValueError: Default token list has not been set.
(Use `--verbosity DEBUG` to see full stack-trace)
WARNING: There are no token lists installed
ValueError: Default token list has not been set.
INFO: Confirmed 0x2c582a6f304cbd647c68c10951df2d25173c4a19e9c50b882e525ec1f7a0f660 (total fees paid = 0)
SUCCESS: Contract 'hello_world' deployed to: 0x274b028b03A250cA03644E6c578D81f019eE1323
In [3]: hello_world.greeting()
Out[3]: 'Hello, World!'
A key difference between Brownie and Ape is that deployments are sent from an account object, instead of a contract object. The contract object has no deploy
method, but the account object does.
Sending Transactions
Sending transactions with Ape is very similar to Brownie.
Here let’s take a test account and wrap some Ether:
In [1]: weth.deposit(value=1*10**18, sender=acct)
INFO: Confirmed 0x434a6480b0d009a7ef4bef649ac6cd183efab87f6d7c753dd8eff03787270a07 (total fees paid = 0)
Out[1]: <Receipt 0x434a6480b0d009a7ef4bef649ac6cd183efab87f6d7c753dd8eff03787270a07>
In [2]: weth.balanceOf(acct)
Out[2]: 1000000000000000000
Ape supports legacy (type 0), and type 2 (EIP-1559) transactions.
I’m not sure if type 1 (EIP-2718) transactions are supported, but it largely doesn’t matter. They are rare, and type 1 functionality is included in type 2.
Ape will construct the transaction depending on the keyword arguments passed to the transaction call:
gas_price
(Legacy — Type 0)max_priority_fee
+max_fee
(EIP-1559 — Type 2)
These arguments can be provided as integers (value in wei) or as strings with “wei”, “gwei”, “ether” suffixes.
Debugging and Tracing
Executing a transaction will return a receipt. This receipt can be queried for a trace report and a gas usage report:
(.venv) [btd@main ape_sandbox]$ ape console --network ethereum:mainnet-fork:foundry
INFO: Starting 'anvil' process.
INFO: Connecting to existing Geth node at https://rpc.ankr.com/[hidden].
In [1]: acct = accounts.test_accounts[0]
In [2]: weth = Contract('0xC02aaA39b223FE8D0A0e5C4F27eAD9083C756Cc2')
In [3]: rcpt = weth.deposit(value=1*10**18, sender=acct)
INFO: Confirmed 0xbc35c2ed448f91c1f4f0d60e0499d5c5ddca07939fb5549188f3300e725b4c5a (total fees paid = 0)
In [4]: rcpt.show_trace()
Call trace for '0xbc35c2ed448f91c1f4f0d60e0499d5c5ddca07939fb5549188f3300e725b4c5a'
tx.origin=0x1e59ce931B4CFea3fe4B875411e280e173cB7A9C
WETH.deposit() [23974 gas]
In [5]: rcpt.show_gas_report()
WETH Gas
Method Times called Min. Max. Mean Median
─────────────────────────────────────────────────────────
deposit 1 23974 23974 23974 23974
Fetching Events
Events emitted by a contract are accessed from the contract object, via the attribute corresponding to the event name.
Queries to this event name will return a Pandas DataFrame, which is great for you data science nerds.
For example, to retrieve the last Deposit
event from the WETH contract, run this query:
In [1]: df = weth.Deposit.query("*", start_block=-1)
In [2]: df
Out[2]:
event_name contract_address \
0 Deposit 0xC02aaA39b223FE8D0A0e5C4F27eAD9083C756Cc2
event_arguments \
0 {'dst': '0x1e59ce931B4CFea3fe4B875411e280e173c...
transaction_hash block_number \
0 0xb92cfb4fbe908dc7e1e4f3aec643a06559351719cb81... 18308799
block_hash log_index \
0 0x8672323f766e30269569d327fd11ce218c2e3e923b03... 0
transaction_index
0 0
If you need to adjust options for pandas, the module is imported as ape.contracts.base.pd
.
Caching
Ape supports a cache for permanently available networks. Initialize it for particular networks that you care about:
(.venv) [btd@main ape_sandbox]$ ape cache init --network ethereum:mainnet
SUCCESS: Caching database initialized for ethereum:mainnet.
The feature is apparently in beta, but the doc page lists the database tables as blocks
, transactions
, and contract_events
. I have not tried this feature yet, but speeding access to historic data retrieval is a huge win so I’m definitely going to explore it.
Accessing Low-Level Objects
When Ape is started on the console, it created a series of manager objects that expose various parts of the API:
accounts
chain
compilers
config
convert
networks
project
reverts
These objects exposed mostly what you’d expect. The most interesting to me are chain
and config
.
If you need access to the web3 object inside Ape, you can find it at the chain.provider._web3
. You can use it directly, similar to object provided by Brownie, brownie.web3
:
# Abstraction of chain height
In [1]: chain.blocks.head.number
Out[1]: 18308907
# Low-level access to web3 object
In [2]: chain.provider._web3.eth.get_block_number()
Out[2]: 18308907
Additional Links
Ape Loves Brownie: A guide focusing on porting projects from Brownie to Ape.
ApeWorX Academy: Focused tutorials on Ape features
Moving Forward
I will detail more features of Ape as I discover them. Ape is feature-complete enough for me to replace Brownie for smart contract development examples, and future exploratory lessons will be written using Ape.
geth plugin seems to be deprecated in latest version (0.8.9)
Not sure if an error, but in the hello_world.vy example above, the line <greeting: String[24]> should be <greeting: public(String[24]). I got an ApeAttributeError when I tried the code exactly above.