# ApeworX

# Getting Started

Ape Github
https://github.com/ApeWorX/ape

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 -I flag 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 -s flag to enable terminal inputs and outputs
ape test -k test_my_contract -I -s

# 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)