Friends of web3.py: Intro to Ape

[Last update: Jan 3, 2024]

Ready to do the Ethereum thing, but just can't bear to JavaScript? Well, have I got a post for you! What lies ahead is an introduction to the Ape development framework, written entirely in Python.

The Goal

This article will provide a tour of features within the Ape framework. You'll get a sense of what Ape can help you accomplish and how to get started with a project of your own.

We'll reference the Wrapped Ether ("WETH") smart contract to highlight some of Ape's features, so as a bonus, you may also leave with a basic understanding of that fundamental Solidity contract.

What Exactly is Ape?

Ape is a smart contract development framework for Python developers. Alternative frameworks include Hardhat, Foundry, and Wake, and each have their strengths and trade-offs. Under the hood, Ape makes use of web3.py and additional libraries maintained by the EF Python team – the authors of this blog.

Inspired by frameworks like Hardhat, Ape utilizes a plugin architecture, making it highly customizable. Plugins are available for a number of language compilers, remote node providers, code analyzers, and so on, allowing you to optimize your workflow in whichever configuration meets your needs.

No JavaScript?

If you have a website in mind as a final product, JavaScript tooling is a very reasonable choice. Two rebuttals: 1) not everything requires a web interface, and 2) you can still build UIs for contracts deployed with Ape.

Within the confines of this tutorial, your mission is to fundamentally understand how the WETH contract works. Ape is a great tool to help you accomplish this goal and offers three options: the console, tests, and scripting. We'll cover each in the sections ahead.

Prerequisites

I'll assume you have a Python environment set up locally. As always, the use of a virtual environment is strongly recommended to reduce dependency frustrations along the way. If you're not yet familiar, taking a few minutes to understand those will save you a lot of headaches throughout your Python career.

No advanced Solidity or Python experience is required, but I'm assuming a very basic understanding of Ethereum concepts. If necessary, you can catch up on those fundamentals in this series.

Coding along is encouraged, but you may be able to follow along with the concepts even with minimal programming experience. If you'd like to peek ahead or save it, a reference repo can be found here.

Setup

We're going to initialize a new Ape project. This process creates a few files and directories, so first create a new directory for your project to live.

$ mkdir ape-weth-demo && cd ape-weth-demo

Create a new virtual environment, then install Ape:

$ pip install eth-ape

Finally, initialize the new Ape project:

$ ape init

You'll be prompted to enter a project name, e.g., ape-weth-demo, then Ape will generate three directories (contracts, scripts, and tests), a .gitignore file, and an ape-config.yaml file. Ape will assume the location of these directories and the config file in future commands, so don't go moving those around.

Compiling Contracts

Let's pull that WETH contract into our project so we can start to put Ape to work. The verified contract can be found on Etherscan, a block explorer. A web search for "etherscan weth" should return the contract page as the first link or two. From within the contract tab, we can retrieve the contract source code.

Copy the verified WETH contract from Etherscan.

Within the contracts directory, create a new file called WETH9.sol, then paste the source code in.

FAQ: Why WETH9? What does the nine signify?
The WETH contract makes use of Kelvin versioning. Starting at nine, each new version is decremented, eventually reaching "absolute" zero. This (controversial) versioning scheme encourages very few and thoughtful iterations to be made before the contract is set in stone.

Though we'll discuss the contents of the contract shortly, you're welcome to take a moment to read through it, noting the public variables, events, and methods. In order to deploy or interact with the contract locally however, we'll need to first compile it.

Compiling a smart contract is the process of converting it into useful metadata, including the resulting bytecode that can be deployed on the blockchain and the contract's ABI.

FAQ: What is an ABI?
The acronym stands for Application Binary Interface. It's a JSON payload that includes each of the contract methods, events and their argument types. TL;DR – tools like web3.py can parse an ABI, then supply a human-friendly interface for using a contract.

Now that we've got a contract, let's ask Ape to compile it:

$ ape compile

WARNING: No compilers detected for the following extensions: .sol

Sad trombone. Ape alerts us that it doesn't yet know how to compile a Solidity file. Out of the box, Ape forms a skeleton of a platform until you install your plugins of choice. Here, you can either run ape plugins install solidity, or add the plugin to your config file. We'll do the latter, as it's an easier way to share a project with collaborators.

name: ape-weth-demo

plugins:
   - name: solidity
     version: 0.7.0

Plugins can be specified in the ape-config.yaml. Omit the version number to always install the latest version.

You can install all plugins listed in a config file using an Ape command:

$ ape plugins install .

The period above references the current directory, which is where Ape will go looking for a config file. Once the Solidity plugin is installed, you'll get a success message similar to the following:

SUCCESS: Plugin 'solidity==0.7.0' has been installed.

Now, try compiling the contract again:

$ ape compile

INFO: Compiling 'WETH9.sol'.

If you don't see any error messages, all is well and you've got a compiled contract! What actually got created is a new directory, .build, where a couple of JSON files now live. This .build directory includes artifacts that are not meant for you to consume or manipulate manually, but Ape will make use of under the hood.

Still with me? This is a good point to pause and top up your coffee or do a lap around the kitchen after a quick recap. So far, we've covered the basic workflow for getting a new project off the ground: initializing a new project, installing plugins, and compiling contracts. If, along the way, you've wondered "how do I know what plugins are available?", many are listed on Ape's homepage, but you can also get a full list by running the following:

$ ape plugins list -a

Next up, we'll take a tour of Ape's console before deploying and interacting with the WETH contract.

Exploring the Console

One of the best ways to gain an understanding of Ape's features is to open its console and explore its powerful manager objects. To get started, run the following in the terminal:

$ ape console

This will open up an interactive Python environment in the terminal with a few extra goodies.

chain

For starters, Ape can tell you about the blockchain you're connected to:

# `chain` references a ChainManager object
In [1]: chain
Out[1]: <ChainManager (id=1337)>

# web3.py version:
In [2]: chain.provider.web3.api
Out[2]: '6.13.0'

# client details:
In [3]: chain.provider.web3.client_version
Out[3]: 'EthereumTester/0.9.1b1/darwin/python3.11.5'

# latest block number (genesis block is block zero):
In [4]: chain.blocks.height
Out[4]: 0

# latest block (the genesis block, in this case):
In [5]: chain.blocks.head
Out[5]: Block(num_transactions=0, hash=HexBytes('0xea66c...'), ...)

The chain manager object exposes blockchain and provider data.

Unless you configure another testnet or mainnet network, Ape defaults to an eth-tester test environment. We'll stick with eth-tester for this tutorial, but after learning the basics, you may want to explore the Hardhat and Foundry plugins to gain additional features like tracing and gas tracking. Network configuration options can be viewed here.

accounts

Ape can be used to generate new accounts or manage existing private keys. Additionally, funded test accounts are automatically available when using eth-tester. Each account object has a few convenience methods built in. A transfer function, for example, can save you a few keystrokes when sending ether from one account to another.

In [1]: accounts.test_accounts
Out[1]: [0x1e59ce931B4CFea3fe4B875411e280e173cB7A9C, 0xc89D42...]

In [2]: acct1 = accounts.test_accounts[0]
In [3]: acct2 = accounts.test_accounts[1]

In [4]: acct2.balance
Out[4]: 1000000000000000000000000

In [5]: acct1.transfer(acct2, 100)
Out[5]: <Receipt 0x4b0caaf...>

In [6]: acct2.balance
Out[6]: 1000000000000000000000100

Note that values are denominated in wei, not ether.

project

Finally, the project manager object provides convenient access to contracts, deployments, and dependencies. Of particular note is the ability to reference any contract that Ape has already compiled:

In [1]: project.contracts
Out[1]: {'WETH9': <ContractType WETH9>}

In [2]: project.WETH9
Out[2]: <WETH9>

We'll put this knowledge to use by deploying an instance of that WETH contract in the next section.

Understanding the Contract

Finally we get to the point where it's useful to know what the WETH contract actually seeks to accomplish. "Wrapped Ether" is an ERC-20 token version of the native currency of Ethereum, ether. This is useful, because decentralized finance ("DeFi") protocols can optimize around that single (ERC-20) standard.

In practice, you can send ether to the contract and receive WETH tokens in return. One WETH will always be exchangeable for one ether. There are no fees for this exchange, in either direction, outside of the variable gas fee required for every Ethereum transaction. Now, let's deploy a WETH contract locally and poke around.

Deploying a contract can be done in at least two ways. The first is to use the deploy method of the contract manager, specifying the sending account:

In [3]: project.WETH9.deploy(sender=acct1)
SUCCESS: Contract 'WETH9' deployed to: 0xBcF7FF...
Out[3]: <WETH9 0xBcF7FF...>

The second option is to use the deploy method on the account manager, specifying the contract:

In [4]: acct1.deploy(project.WETH9)
SUCCESS: Contract 'WETH9' deployed to: 0xF2Df0b...
Out[4]: <WETH9 0xF2Df0b...>

Once we've got a deployed contract, we can begin to explore its properties and methods.

In [5]: weth_contract = acct1.deploy(project.WETH9)
SUCCESS: Contract 'WETH9' deployed to: 0x4B3E65...

In [6]: weth_contract.name()
Out[6]: 'Wrapped Ether'

In [7]: weth_contract.symbol()
Out[7]: 'WETH'

In [8]: weth_contract.totalSupply()
Out[8]: 0

Use the deposit method to send in some ether and have the balance of WETH incremented by the same amount:

In [9]: weth_contract.deposit(value=100, sender=acct1)
Out[9]: <Receipt 0x219b8c...>

In [10]: weth_contract.totalSupply()
Out[10]: 100

In [11]: weth_contract.balanceOf(acct1)
Out[11]: 100

Withdrawals are the same concept in reverse:

In [12]: weth_contract.withdraw(50, sender=acct1)
Out[12]: <Receipt 0xe7d1c1...>

In [13]: weth_contract.balanceOf(acct1)
Out[13]: 50

Did you catch the difference in how the ether amount is specified in each method? Sending ether in any transaction requires specifying a transaction value, so that's what we've done in the deposit method. When calling withdraw, we're not sending any ether along, merely specifying how much WETH we want to convert back into ether and have returned. If you compare the two functions in the Solidity contract, you'll find that the deposit method references msg.value, while withdraw includes an argument for the wad (amount) of WETH.

As an exercise, see if you can make use of the approve and transferFrom methods. As the names suggest, you can approve another account to move WETH tokens on your behalf. This is a common practice when using a decentralized exchange, for example, where the exchange contract will perform the swap of one token for another on your behalf.

This wraps up the introduction to Ape's console feature. Up next, there are still two more tools in your tool belt: tests and scripts.

Testing the Contract

Writing tests for a contract is another great way to understand how it works. Ape leverages pytest and makes available the same manager objects. In this fashion, we can prepare any contracts and accounts, then test each state in isolation. Let's run through a quick example of just that.

Create a conftest.py file within the tests directory. Within it, we'll create a few fixtures. Fixtures are pre-populated variables available to pull into any test. Generally, they're useful for saving a few keystrokes, making your tests less repetitive and more legible.

import pytest

@pytest.fixture
def acct1(accounts):
	return accounts[0]

@pytest.fixture
def acct2(accounts):
	return accounts[1]

@pytest.fixture
def acct3(accounts):
	return accounts[2]
    
@pytest.fixture
def weth_contract(acct1, project):
	return acct1.deploy(project.WETH9)

/tests/conftest.py

Thanks to Ape, pytest can make sense of the accounts and project variables passed in as arguments to these fixtures. Now, any of these fixtures can be utilized within the project's tests. Create a new file in the tests directory called test_weth.py and include a smoke test to serve as a sanity check:

def test_smoke(acct1, acct2, acct3, weth_contract):
    assert acct1.balance > 0
    assert acct2.balance > 0
    assert acct3.balance > 0
    assert weth_contract.balance == 0
    assert weth_contract.totalSupply() == 0

To run the tests, execute ape test in your terminal. You should see one green dot and a 1 passed message:

$ ape test

======= test session starts =======
platform darwin -- Python 3.10.8, pytest-7.3.2, pluggy-1.0.0
rootdir: /Users/mg/projects/eth/ape-weth-demo
plugins: eth-ape-0.6.10, web3-6.4.0
collected 1 item

tests/test_weth.py .       [100%]

======== 1 passed in 0.67s ========

Great! We're off to the races. Try your hand at writing a couple tests of your own using the available contract methods. To get the gears turning, below is an example of a test of the transfer method. In this test, acct1 deposits 100 wei, then transfers 35 to acct2, before we make some assertions about the resulting balances.

def test_transfer(acct1, acct2, weth_contract):
    weth_contract.deposit(value=100, sender=acct1)
    tx_receipt = weth_contract.transfer(acct2, 35, sender=acct1)

    assert weth_contract.balanceOf(acct1) == 65
    assert weth_contract.balanceOf(acct2) == 35
    assert weth_contract.totalSupply() == 100

While you're writing and running tests, know that pytest's normal flags are available to you. Here are a few that you might find useful:

# run tests that match a pattern, e.g., includes 'deposit':
$ ape test -k deposit

# increase the verbosity level to see more info/print statements:
$ ape test -s

# exit as soon as one failure occurs:
$ ape test -x

# upon failure, open an interactive REPL for debugging:
$ ape test --pdb

# multiple flags may be used:
$ ape test --pdb -x -s -k deposit

Scripting

Scripts are one more way to explore and interact with a contract. They work in much the same way as the console or tests: Ape can run any arbitrary Python code, while making its manager objects accessible. Scripts get interesting in that Ape also uses click to enable powerfully configurable command line options.

The most obvious usage example is a deploy script. Leveraging the network manager object, you can write a script to deploy one or more contracts to a variety of blockchains or with varying accounts, based on flags you supply. Here's a paraphrased example from the Ape docs:

import click
from ape import project
from ape.cli import get_user_selected_account, NetworkBoundCommand, ape_cli_context, network_option

@click.command(cls=NetworkBoundCommand)
@ape_cli_context()
@network_option()
def cli(cli_ctx, network):
    network = cli_ctx.provider.network.name
    if network == "local" or network.endswith("-fork"):
        account = cli_ctx.account_manager.test_accounts[0]
    else:
        account = get_user_selected_account()
    
    account.deploy(project.WETH9)

/scripts/deploy.py

The decorators and cli_ctx argument will almost certainly be unfamiliar to you; this CLI guide in the Ape documentation will help clarify what's going on here. The primary takeaway is that this script will deploy using test accounts if interacting with a test network, and real accounts otherwise. The get_user_selected_account method will even pause the script to ask you to specify an account.

You can then run this script via the run command:

# default to local eth-tester environment:
$ ape run deploy

# can specify another chain/provider:
$ ape run deploy --network ethereum:mainnet:infura

An exercise for the reader: what else might you want to write a script for?

Now what?

To review this example project with accompanying scripts and tests, you'll find the repo here.

We've covered many of the basic concepts, but there's plenty more to learn on the path to building production-worthy projects. When you're ready for more, here are a few ideas to help you choose your next adventure:

  • Write additional tests for the approve, transfer, and transferFrom functions.
  • Try out a Vyper contract within Ape; here's a Vyper iteration of the WETH9 contract: WETH8.
  • Query for events emitted by each of the contract's functions.
  • Deploy the WETH contract to an Ethereum testnet, like Sepolia.
  • Track your deployments and utilize an already deployed contract.
  • Create your own ERC-20 token, perhaps using the OpenZeppelin contract wizard.
  • Deploy and test a contract that interacts with (or even deploys) another contract.
  • Use Hardhat or Foundry to clone mainnet for "live" testing a contract.
  • Create your own Ape plugin.
  • Build a UI to interact with your contract.

Some of these may appear simple, but are loaded with "gotchas." No better way to learn, right? Pro-tip: document and share your learnings along the way. Writing helps solidify understanding, is great for the resume, and helps those following in your footsteps.

Ape is a relatively young tool, too. You'll find plenty of opportunities to contribute to documentation, bug reports, and feature work if you want to get involved in this open source community – and please do!

If you have follow-up questions or want to share your learning/writing, join the ApeWorX and Ethereum Python Community Discord servers. Got corrections, suggestions, or new content requests? Reach me on Twitter or within those Discord servers. Happy buidling! 🍻