#
ApeworX
#
Getting Started
To create a blank ape project, type the following in the terminal, to create three empty folders (contracts, scripts and tests) and an ape-config.yml file.
ape init
This is what a ape project structure looks like:
project # The root project directory
├── contracts/ # Project source files, such as '.sol' or '.vy' files
│ └── smart_contract_example.sol # Sample of a smart contract
├── tests/ # Project tests, ran using the 'ape test' command
│ └── test_sample.py # Sample of a test to run against your sample contract
├── scripts/ # Project scripts, such as deploy scripts, ran using the 'ape run <`name>' command
│ └── deploy.py # Sample script to automate a deployment of an ape project
└── ape-config.yaml
#
Accounts
To see the available options for the account command, type:
ape accounts
Commands:
change-password Change the password of an existing account
delete Delete an existing account
export Export an account private key
generate Create an account with a random mnemonic seed phrase
import Import an account by private key or seed phrase
list List available local accounts
To generate a new account with the alias "workshop", type:
ape accounts generate workshop
#
Automation
If you use your keyfile accounts in automation, such as CI/CD, you may need to programmatically unlock them and enable autosign. WARNING: We don’t recommend using this approach but it is possible due to sometimes being needed. Ensure you are using a secure environment and are aware of what you are doing.
from ape import accounts
account = accounts.load("<ALIAS>")
account.set_autosign(True, passphrase="<PASSPHRASE>")
#
Plugins
Plugins are core to Ape’s architecture. To see the plugins that are installed, run the following command:
ape plugins list
For this workshop, you will need the following:
Installed Plugins:
vyper 0.5.1
alchemy 0.6.1
polygon 0.5.1a1
(not necessarily these versions)
#
Scripts
You can write scripts that run using the ape run command. The ape run command will register and run Python files defined under the scripts/ directory that do not start with an _ underscore.
Assume we named the script helloworld.py. To execute the script, run the following:
ape run helloworld
You can also execute scripts in subdirectories. For example, assuming we have script scripts/hello/helloworld.py, we would execute it by running:
ape run hello helloworld
To do this, define a method named main() in your script:
def main():
print("Hello world!")
NOTE: main-method scripts will always provide a network option to the call and thus will always connect to a network.
To demonstrate, use the following script:
from ape import networks
import click
def main():
ecosystem_name = networks.provider.network.ecosystem.name
network_name = networks.provider.network.name
provider_name = networks.provider.name
click.echo(f"You are connected to network '{ecosystem_name}:{network_name}:{provider_name}'.")
You can use some click helper function inside your scripts. For example, the following script will prompt to select and accounts from the list available in your ApeworX installation:
from ape.cli import get_user_selected_account
def main():
user = get_user_selected_account()
print(f'The account balance is: {user.balance / 1e18}')
#
Deployment
Using the project:
from ape import project, accounts
def main():
owner = accounts.load("test")
deployed_contract = project.Storage.deploy(sender=owner)
print(deployed_contract.address)
Using the account:
from ape import project, accounts
def main():
owner = accounts.load("test")
deployed_contract = owner.deploy(project.Storage)
print(deployed_contract.address)
#
Transactions
value = convert ("0.1 ETH", int)
workshop_account = accounts.load("workshop")
test_account = accounts.load("test")
workshop_account.transfer(test_account, value)
#
Transaction Logs
In Ape, you can easily get all the events on a receipt. Use the .events property to access the (ContractLog) objects. Each object represents an event emitted from the call.
receipt = contract.fundMyContract(value="1 gwei", type="0x0", sender=sender)
print(receipt.events)
To only get specific log types, use the decode_logs() method and pass the event ABIs as arguments:
for log in receipt.decode_logs(contract.FooEvent.abi, contract.BarEvent.abi):
print(log.amount) # Assuming 'amount' is a property on the event.
You can also use the ContractEvent.from_receipt(receipt) method:
receipt = contract.fooMethod(value="1 gwei", type="0x0", sender=sender)
for log in contract.FooEvent.from_receipt(receipt):
print(log.amount) # Assuming 'amount' is a property on the event.
#
Contracts
You can interact with contracts pythonically using ape! First, we need to obtain a contract instance. One way to do this is to deploy a contract. The other way is to initialize an already-deployed contract using its address.
#
From Deploy
Deploy contracts from your project using the project root-level object. The names of your contracts are properties on the project object (e.g. project.MyContract)
When you deploy contracts, you get back a ContractInstance:
from ape import accounts, project
dev = accounts.load("dev")
contract = project.MyContract.deploy(sender=dev)
You can alternatively use this syntax instead:
from ape import accounts, project
dev = accounts.load("dev")
contract = dev.deploy(project.MyContract)
If your contract requires constructor arguments then you will need to pass them to the contract in the args when deploying like this:
from ape import accounts, project
dev = accounts.load("dev")
contract = project.MyContract.deploy("argument1", "argument2", sender=dev)
you can alternatively use this syntax instead:
from ape import accounts, project
dev = accounts.load("dev")
contract = accounts.dev.deploy(project.MyContract, "argument1", "argument2")
#
From Project Contract Address
You can also use the at() method from the same top-level project manager when you know the address of an already-deployed contract:
from ape import project
contract = project.MyContract.at("0x68b3465833fb72A70ecDF485E0e4C7bD8665Fc45")
#
From Previous Deployment
Ape keeps track of your deployments for you so you can always refer back to a version that you deployed previously. On live networks, this history of deployments is saved; on local networks, this history lasts for the duration of your script.
Let’s say you previously deployed a smart contract called MyContract on the rinkeby test network. You could then refer back to it like so:
from ape import project, chain, accounts
def main():
account = accounts.test_accounts[0]
my_contract = chain.contracts.get_deployments(project.MyContract)[-1]
or
from ape import project, accounts
def main():
account = accounts.test_accounts[0]
my_contract = project.MyContract.deployments[-1]
my_contract will be of type ContractInstance. get_deployments returns a list of deployments you made of that contract type.
#
Contract Interaction
Then, after you have a contract instance, you can call methods on the contract. For example, let’s say you have a Vyper contract containing some functions:
@pure
@external
def get_static_list() -> DynArray[uint256, 3]:
return [1, 2, 3]
@external
def set_number(num: uint256):
assert msg.sender == self.owner, "!authorized"
self.prevNumber = self.myNumber
self.myNumber = num
You can call those functions by doing:
assert contract.get_static_list() == [1, 2, 3]
# Mutable calls are transactions and require a sender
receipt = contract.set_number(sender=dev)
#
Tests
Tests must be located in a project’s tests/ directory. Each test file must start with test_ and have the .py extension, such as test_my_contract.py. Each test method within the file must also start with test_. The following is an example test:
def test_add():
assert 1 + 1 == 2
Next, we need to call a method on our contract. Let’s assume there is an authorized_method() that requires the owner of the contract to make the transaction. If the sender of the transaction is not the owner, the transaction will fail to complete and will revert.
This is an example of how that test may look:
def test_authorization(my_contract, owner, not_owner):
my_contract.set_owner(sender=owner)
assert owner == my_contract.owner()
with ape.reverts("!authorized"):
my_contract.authorized_method(sender=not_owner)
#
Running Tests
ape test
To run a particular test file:
ape test -k test_account
- Use the
-Iflag to open the interactive mode at the point of exception. This allows the user to inspect the point of failure in your tests. - Use the
-sflag to enable terminal inputs and outputs
ape test -k test_my_contract -I -s
note
Ape has built-in test and fixture isolation for all pytest scopes. To disable isolation add the --disable-isolation flag when running ape test
#
Fixtures
Fixtures are any type of reusable instances of something with configurable scopes. pytest handles passing fixtures into each test method as test-time. To learn more about fixtures
Define fixtures for static data used by tests. This data can be accessed by all tests in the suite unless specified otherwise. This could be data as well as helpers of modules which will be passed to all tests.
A common place to define fixtures are in the conftest.py which should be saved under the test directory:
conftest.py is used to import external plugins or modules. By defining the following global variable, pytest will load the module and make it available for its test.
#
accounts fixture
You have access to test accounts. These accounts are automatically funded, and you can use them to transact in your tests. Access each test account by index from the accounts fixture:
def test_my_method(accounts):
owner = accounts[0]
receiver = accounts[1]
For code readability and sustainability, create your own fixtures using the accounts fixture:
import pytest
@pytest.fixture
def owner(accounts):
return accounts[0]
@pytest.fixture
def receiver(accounts):
return accounts[1]
def test_my_method(owner, receiver):
...
#
project fixture
You have access to the project you are testing. You will need this to deploy your contracts in your tests.
import pytest
@pytest.fixture
def owner(accounts):
return accounts[0]
@pytest.fixture
def my_contract(project, owner):
# ^ use the 'project' fixture from the 'ape-test' plugin
return owner.deploy(project.MyContract)
#
networks fixture
Use the networks fixture to change the active provider in tests.
def test_multi_chain(networks):
assert "Something" # Make assertion in root network
# NOTE: Assume have ecosystem named "foo" with network "local" and provider "bar"
with networks.foo.local.use_provider("bar"):
assert "Something else"
#
Console
Ape provides an IPython interactive console with useful pre-defined locals to interact with your project.
ape console --network polygon:mumbai:alchemy
In console:
In [1]: chain.blocks.head.timestamp
Out[1]: 1647323479
In [2]: user = accounts.load("test")
In [3]: user.balance
Out[3]: 2298491536054317027
#
Config
The ape-config.yaml file allows you to configure ape.
default_ecosystem: fantom
contracts_folder: src
deployments:
ethereum:
mainnet:
- contract_type: MyContract
address: 0xc123aAacCcbBbaAa444777000111222111222111
ropsten:
- contract_type: MyContract
address: 0xc222000cCcbBbaAa444777000111222111222222
ethereum:
default_network: mainnet-fork
mainnet_fork:
default_provider: hardhat
ganache:
request_timeout: 20 # Defaults to 30
fork_request_timeout: 600 # Defaults to 300
fork:
ethereum:
mainnet:
upstream_provider: infura
#block_number: 13038769
test:
mnemonic: test test test test test test test test test test test junk
number_of_accounts: 5
More relevant for Solidity, you can specify dependencies to use in imports:
dependencies:
- name: OpenZeppelin
github: OpenZeppelin/openzeppelin-contracts
version: 4.4.2
solidity:
import_remapping:
- "@openzeppelin=OpenZeppelin/4.4.2"
Now, in your solidity files, import OpenZeppelin sources via:
import "@openzeppelin/token/ERC721/ERC721.sol";
(the dependencies will be downloaded into contracts/.cache folders and compiled)
#
Utils
value = convert ("0.1 ETH", int)