Skip to content

Commit

Permalink
Merge branch 'master' of github.com:vyperlang/titanoboa into 104/refa…
Browse files Browse the repository at this point in the history
…ctor-pyevm
  • Loading branch information
DanielSchiavini committed Mar 7, 2024
2 parents f232387 + 1e518b8 commit 3e9ac33
Show file tree
Hide file tree
Showing 28 changed files with 444 additions and 357 deletions.
1 change: 1 addition & 0 deletions .github/workflows/.pre-commit-config.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ on:
jobs:
pre-commit:
runs-on: ubuntu-latest
timeout-minutes: 5
steps:
- uses: actions/checkout@v3
- uses: actions/setup-python@v4
Expand Down
62 changes: 62 additions & 0 deletions .github/workflows/integration.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,62 @@
# These tests are separated into a separate file to be able to use secrets when running in a fork.
# To avoid leaking secrets, we only allow contributors to run the tests.
# Note that pull_request_trigger runs from the base branch, not the head branch.
# So, we need to manually check out the head ref and merge the base branch into it.
name: integration

on:
pull_request_target:
push: # all

jobs:
integration:
name: "integration tests (Alchemy: fork mode and Sepolia)"
runs-on: ubuntu-latest
timeout-minutes: 5
steps:

# given we use the pull_request_trigger, only allow contributors to run tests with secrets
- name: Check if the user is a contributor
uses: actions/github-script@v7
with:
script: |
const { actor: username, repo: { owner, repo } } = context;
const collaborator = await github.rest.repos.getCollaboratorPermissionLevel({ owner, repo, username });
if (!collaborator.data.user.permissions.push) {
core.setFailed(username + ' is not a contributor');
}
# this will check out the base branch, not the head branch
- uses: actions/checkout@v4
with:
fetch-depth: 0 # we need the history to be able to merge

# now merge the head branch into the base branch, so we can run the tests with the head branch's changes
- name: Merge head branch
run: |
git fetch origin ${{ github.head_ref }}
git merge origin/${{ github.head_ref }} --no-edit
- name: Setup Python 3.11
uses: actions/setup-python@v4
with:
python-version: "3.11"
cache: "pip"

- name: Install Requirements
run: |
pip install -r dev-requirements.txt
pip install .
- name: Run Fork Mode Tests
run: pytest -n auto tests/integration/fork/
env:
MAINNET_ENDPOINT: ${{ secrets.ALCHEMY_MAINNET_ENDPOINT }}
ETHERSCAN_API_KEY: ${{ secrets.ETHERSCAN_API_KEY }}

- name: Run Sepolia Tests
# disable xdist, otherwise they can contend for tx nonce
run: pytest -n 0 tests/integration/network/sepolia/
env:
SEPOLIA_ENDPOINT: ${{ secrets.ALCHEMY_SEPOLIA_ENDPOINT }}
SEPOLIA_PKEY: ${{ secrets.SEPOLIA_PKEY }}
52 changes: 5 additions & 47 deletions .github/workflows/main.yaml
Original file line number Diff line number Diff line change
@@ -1,19 +1,18 @@
name: unitary

on:
pull_request_target:
push:
branches:
- master
pull_request:
push: # all

jobs:
unitary:
runs-on: ubuntu-latest
strategy:
matrix:
python-version: [ "3.10", "3.11" ]
python-version: [ "3.10", "3.11", "3.12" ]

name: "unit tests: python ${{ matrix.python-version }}"
timeout-minutes: 5

steps:
- uses: actions/checkout@v4
Expand Down Expand Up @@ -48,6 +47,7 @@ jobs:
anvil:
name: "integration tests (anvil)"
runs-on: ubuntu-latest
timeout-minutes: 5
steps:
- uses: actions/checkout@v4

Expand All @@ -68,45 +68,3 @@ jobs:
- name: Run Networked Tests against anvil
# run separately to clarify its dependency on outside binary
run: pytest -n auto tests/integration/network/anvil/

integration:
name: "integration tests (Alchemy: fork mode and Sepolia)"
runs-on: ubuntu-latest
steps:
- name: Check if the user is a contributor
uses: actions/github-script@v7
with:
script: |
const { actor: username, repo: { owner, repo } } = context;
const collaborator = await github.rest.repos.getCollaboratorPermissionLevel({ owner, repo, username });
if (!collaborator.data.user.permissions.push) {
core.setFailed(username + ' is not a contributor');
}
- uses: actions/checkout@v4
with:
ref: ${{ github.event.pull_request.merge_commit_sha }}

- name: Setup Python 3.11
uses: actions/setup-python@v4
with:
python-version: "3.11"
cache: "pip"

- name: Install Requirements
run: |
pip install -r dev-requirements.txt
pip install .
- name: Run Fork Mode Tests
run: pytest -n auto tests/integration/fork/
env:
MAINNET_ENDPOINT: ${{ secrets.ALCHEMY_MAINNET_ENDPOINT }}
ETHERSCAN_API_KEY: ${{ secrets.ETHERSCAN_API_KEY }}

- name: Run Sepolia Tests
# disable xdist, otherwise they can contend for tx nonce
run: pytest -n 0 tests/integration/network/sepolia/
env:
SEPOLIA_ENDPOINT: ${{ secrets.ALCHEMY_SEPOLIA_ENDPOINT }}
SEPOLIA_PKEY: ${{ secrets.SEPOLIA_PKEY }}
71 changes: 59 additions & 12 deletions boa/contracts/abi/abi_contract.py
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
from collections import defaultdict
from copy import deepcopy
from functools import cached_property
from os.path import basename
from typing import Any, Optional, Union
Expand Down Expand Up @@ -83,12 +84,17 @@ def is_encodable(self, *args, **kwargs) -> bool:
for abi_type, arg in zip(self.argument_types, parsed_args)
)

def prepare_calldata(self, *args, **kwargs) -> bytes:
"""Prepare the call data for the function call."""
abi_args = self._merge_kwargs(*args, **kwargs)
return self.method_id + abi_encode(self.signature, abi_args)

def _merge_kwargs(self, *args, **kwargs) -> list:
"""Merge positional and keyword arguments into a single list."""
if len(kwargs) + len(args) != self.argument_count:
raise TypeError(
f"Bad args to `{repr(self)}` "
f"(expected {self.argument_count} arguments, got {len(args)})"
f"Bad args to `{repr(self)}` (expected {self.argument_count} "
f"arguments, got {len(args)} args and {len(kwargs)} kwargs)"
)
try:
kwarg_inputs = self._abi["inputs"][len(args) :]
Expand All @@ -102,11 +108,10 @@ def __call__(self, *args, value=0, gas=None, sender=None, **kwargs):
if not self.contract or not self.contract.env:
raise Exception(f"Cannot call {self} without deploying contract.")

args = self._merge_kwargs(*args, **kwargs)
computation = self.contract.env.execute_code(
to_address=self.contract.address,
sender=sender,
data=self.method_id + abi_encode(self.signature, args),
data=self.prepare_calldata(*args, **kwargs),
value=value,
gas=gas,
is_modifying=self.is_mutable,
Expand Down Expand Up @@ -151,11 +156,35 @@ def __init__(self, functions: list[ABIFunction]):
def name(self) -> str:
return self.functions[0].name

def __call__(self, *args, disambiguate_signature=None, **kwargs):
def prepare_calldata(self, *args, disambiguate_signature=None, **kwargs) -> bytes:
"""Prepare the calldata for the function that matches the given arguments."""
function = self._pick_overload(
*args, disambiguate_signature=disambiguate_signature, **kwargs
)
return function.prepare_calldata(*args, **kwargs)

def __call__(
self,
*args,
value=0,
gas=None,
sender=None,
disambiguate_signature=None,
**kwargs,
):
"""
Call the function that matches the given arguments.
:raises Exception: if a single function is not found
"""
function = self._pick_overload(
*args, disambiguate_signature=disambiguate_signature, **kwargs
)
return function(*args, value=value, gas=gas, sender=sender, **kwargs)

def _pick_overload(
self, *args, disambiguate_signature=None, **kwargs
) -> ABIFunction:
"""Pick the function that matches the given arguments."""
if disambiguate_signature is None:
matches = [f for f in self.functions if f.is_encodable(*args, **kwargs)]
else:
Expand All @@ -166,7 +195,7 @@ def __call__(self, *args, disambiguate_signature=None, **kwargs):

match matches:
case [function]:
return function(*args, **kwargs)
return function
case []:
raise Exception(
f"Could not find matching {self.name} function for given arguments."
Expand All @@ -181,19 +210,22 @@ def __call__(self, *args, disambiguate_signature=None, **kwargs):


class ABIContract(_BaseEVMContract):
"""A contract that has been deployed to the blockchain and created via an ABI."""
"""A deployed contract loaded via an ABI."""

def __init__(
self,
name: str,
abi: dict,
functions: list[ABIFunction],
address: Address,
filename: Optional[str] = None,
env=None,
):
super().__init__(env, filename=filename, address=address)
self._name = name
self._abi = abi
self._functions = functions

self._bytecode = self.env.evm.get_code(address)
if not self._bytecode:
warn(
Expand All @@ -202,14 +234,18 @@ def __init__(
)

overloads = defaultdict(list)
for f in functions:
for f in self._functions:
overloads[f.name].append(f)

for name, group in overloads.items():
setattr(self, name, ABIOverload.create(group, self))

self._address = Address(address)

@property
def abi(self):
return self._abi

@cached_property
def method_id_map(self):
"""
Expand Down Expand Up @@ -254,7 +290,7 @@ def deployer(self) -> "ABIContractFactory":
"""
Returns a factory that can be used to retrieve another deployed contract.
"""
return ABIContractFactory(self._name, self._functions)
return ABIContractFactory(self._name, self._abi, self._functions)

def __repr__(self):
file_str = f" (file {self.filename})" if self.filename else ""
Expand All @@ -270,25 +306,36 @@ class ABIContractFactory:
"""

def __init__(
self, name: str, functions: list["ABIFunction"], filename: Optional[str] = None
self,
name: str,
abi: dict,
functions: list[ABIFunction],
filename: Optional[str] = None,
):
self._name = name
self._abi = abi
self._functions = functions
self._filename = filename

@cached_property
def abi(self):
return deepcopy(self._abi)

@classmethod
def from_abi_dict(cls, abi, name="<anonymous contract>"):
functions = [
ABIFunction(item, name) for item in abi if item.get("type") == "function"
]
return cls(basename(name), functions, filename=name)
return cls(basename(name), abi, functions, filename=name)

def at(self, address: Address | str) -> ABIContract:
"""
Create an ABI contract object for a deployed contract at `address`.
"""
address = Address(address)
contract = ABIContract(self._name, self._functions, address, self._filename)
contract = ABIContract(
self._name, self._abi, self._functions, address, self._filename
)
contract.env.register_contract(address, contract)
return contract

Expand Down
2 changes: 1 addition & 1 deletion boa/contracts/vyper/ir_executor.py
Original file line number Diff line number Diff line change
Expand Up @@ -247,7 +247,7 @@ def compile_main(self, contract_path=""):
self.builder.extend("\n\n")
func.compile_func()

py_file = contract_path + str(self.compile_ctx.uuid) + ".py"
py_file = f"{contract_path}{self.compile_ctx.uuid}.py"

# uncomment for debugging the python code:
# with open(py_file, "w") as f:
Expand Down
30 changes: 24 additions & 6 deletions boa/contracts/vyper/vyper_contract.py
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,7 @@
from vyper.codegen.module import generate_ir_for_module
from vyper.compiler import CompilerData
from vyper.compiler import output as compiler_output
from vyper.compiler.output import build_abi_output
from vyper.compiler.settings import OptimizationLevel
from vyper.evm.opcodes import anchor_evm_version
from vyper.exceptions import VyperException
Expand Down Expand Up @@ -96,16 +97,29 @@ def deploy_as_blueprint(self, *args, **kwargs):
self.compiler_data, *args, filename=self.filename, **kwargs
)

def stomp(self, address: Any, data_section=None) -> "VyperContract":
address = Address(address)

ret = self.deploy(override_address=address, skip_initcode=True)
vm = ret.env.vm
old_bytecode = vm.state.get_code(address.canonical_address)
new_bytecode = self.compiler_data.bytecode_runtime

immutables_size = self.compiler_data.global_ctx.immutable_section_bytes
if immutables_size > 0:
data_section = old_bytecode[-immutables_size:]
new_bytecode += data_section

vm.state.set_code(address.canonical_address, new_bytecode)
ret.env.register_contract(address, ret)
ret._set_bytecode(new_bytecode)
return ret

# TODO: allow `env=` kwargs and so on
def at(self, address: Any) -> "VyperContract":
address = Address(address)

ret = VyperContract(
self.compiler_data,
override_address=address,
skip_initcode=True,
filename=self.filename,
)
ret = self.deploy(override_address=address, skip_initcode=True)
bytecode = ret.env.evm.get_code(address)

ret._set_bytecode(bytecode)
Expand All @@ -129,6 +143,10 @@ def __init__(
with anchor_compiler_settings(self.compiler_data):
_ = compiler_data.bytecode, compiler_data.bytecode_runtime

@cached_property
def abi(self):
return build_abi_output(self.compiler_data)


# create a blueprint for use with `create_from_blueprint`.
# uses a ERC5202 preamble, when calling `create_from_blueprint` will
Expand Down
2 changes: 1 addition & 1 deletion boa/coverage.py
Original file line number Diff line number Diff line change
Expand Up @@ -68,7 +68,7 @@ def dynamic_source_filename(self, filename, frame):
if contract is None:
return None

return contract.filename
return str(contract.filename)

def has_dynamic_source_filename(self):
return True
Expand Down
Loading

0 comments on commit 3e9ac33

Please sign in to comment.