Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: add support for vyper archives (.vyz) #328

Open
wants to merge 7 commits into
base: master
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
132 changes: 107 additions & 25 deletions boa/interpret.py
Original file line number Diff line number Diff line change
@@ -1,23 +1,30 @@
import binascii
import sys
import textwrap
from base64 import b64decode
from importlib.abc import MetaPathFinder
from importlib.machinery import SourceFileLoader
from importlib.util import spec_from_loader
from pathlib import Path
from io import BytesIO
from pathlib import Path, PurePath
from typing import TYPE_CHECKING, Any, Union
from zipfile import BadZipFile, ZipFile

import vvm
import vyper
from vyper.ast.parse import parse_to_ast
from vyper.cli.compile_archive import NotZipInput
from vyper.cli.vyper_compile import get_search_paths
from vyper.compiler.input_bundle import (
ABIInput,
CompilerInput,
FileInput,
FilesystemInputBundle,
JSONInputBundle,
)
from vyper.compiler.phases import CompilerData
from vyper.compiler.settings import Settings, anchor_settings
from vyper.compiler.settings import Settings, anchor_settings, merge_settings
from vyper.exceptions import BadArchive
from vyper.semantics.analysis.module import analyze_module
from vyper.semantics.types.module import ModuleT
from vyper.utils import sha256sum
Expand Down Expand Up @@ -129,22 +136,19 @@ def get_module_fingerprint(


def compiler_data(
source_code: str, contract_name: str, filename: str | Path, deployer=None, **kwargs
source_code: str | bytes,
contract_name: str,
filename: str | Path,
deployer=None,
**kwargs,
) -> CompilerData:
global _disk_cache, _search_path
global _disk_cache

path = Path(contract_name)
resolved_path = Path(filename).resolve(strict=False)

file_input = FileInput(
contents=source_code, source_id=-1, path=path, resolved_path=resolved_path
)

search_paths = get_search_paths(_search_path)
input_bundle = FilesystemInputBundle(search_paths)

settings = Settings(**kwargs)
ret = CompilerData(file_input, input_bundle, settings)
ret = _create_compiler_data(path, resolved_path, source_code, settings)
if _disk_cache is None:
return ret

Expand All @@ -160,6 +164,7 @@ def get_compiler_data():
with anchor_settings(ret.settings):
# force compilation to happen so DiskCache will cache the compiled artifact:
_ = ret.bytecode, ret.bytecode_runtime

return ret

assert isinstance(deployer, type) or deployer is None
Expand All @@ -173,20 +178,30 @@ def load(filename: str | Path, *args, **kwargs) -> _Contract: # type: ignore
# TODO: investigate if we can just put name in the signature
if "name" in kwargs:
name = kwargs.pop("name")
with open(filename) as f:
return loads(f.read(), *args, name=name, **kwargs, filename=filename)
with open(filename, "rb") as f:
source_code = f.read()
try:
source_code = source_code.decode() # type: ignore
except UnicodeDecodeError:
pass # source might be an archive file. Try to compile it.
return loads(
source_code, *args, name=name, dedent=False, **kwargs, filename=filename
)


def loads(
source_code,
source_code: str | bytes,
*args,
as_blueprint=False,
name=None,
filename=None,
compiler_args=None,
dedent=True,
**kwargs,
):
d = loads_partial(source_code, name, filename=filename, compiler_args=compiler_args)
d = loads_partial(
source_code, name, filename=filename, dedent=dedent, compiler_args=compiler_args
)
if as_blueprint:
return d.deploy_as_blueprint(**kwargs)
else:
Expand Down Expand Up @@ -233,7 +248,7 @@ def loads_vyi(source_code: str, name: str = None, filename: str = None):


def loads_partial(
source_code: str,
source_code: str | bytes,
name: str = None,
filename: str | Path | None = None,
dedent: bool = True,
Expand All @@ -242,17 +257,17 @@ def loads_partial(
name = name or "VyperContract"
filename = filename or "<unknown>"

if dedent:
source_code = textwrap.dedent(source_code)
if isinstance(source_code, str):
if dedent:
source_code = textwrap.dedent(source_code)

version = _detect_version(source_code)
if version is not None and version != vyper.__version__:
filename = str(filename) # help mypy
# TODO: pass name to loads_partial_vvm, not filename
return _loads_partial_vvm(source_code, version, filename)
version = _detect_version(source_code)
if version is not None and version != vyper.__version__:
filename = str(filename) # help mypy
# TODO: pass name to loads_partial_vvm, not filename
return _loads_partial_vvm(source_code, version, filename)

compiler_args = compiler_args or {}

deployer_class = _get_default_deployer_class()
data = compiler_data(source_code, name, filename, deployer_class, **compiler_args)
return deployer_class(data, filename=filename)
Expand Down Expand Up @@ -286,6 +301,73 @@ def _compile():
return _disk_cache.caching_lookup(cache_key, _compile)


def _create_compiler_data(
path: Path, resolved_path: Path, source_code: str | bytes, settings: Settings
) -> CompilerData:
try:
return _create_archive_compiler_data(source_code, settings)
except NotZipInput:
pass

if isinstance(source_code, bytes):
source_code = source_code.decode()

global _search_path
file_input = FileInput(
contents=source_code, source_id=-1, path=path, resolved_path=resolved_path
)
input_bundle = FilesystemInputBundle(get_search_paths(_search_path))
return CompilerData(file_input, input_bundle, settings)


def _create_archive_compiler_data(
zip_contents: str | bytes, settings: Settings
) -> CompilerData:
with _open_zip(zip_contents) as archive:
# read the whole zip into memory so it can be serialized to the cache
files = {name: archive.read(name).decode() for name in archive.namelist()}

targets = files["MANIFEST/compilation_targets"].splitlines()
if len(targets) != 1:
raise BadArchive("Multiple compilation targets not supported!")

input_bundle = JSONInputBundle(
input_json={
PurePath(name): {"content": content} for name, content in files.items()
},
search_paths=[PurePath(p) for p in files["MANIFEST/searchpaths"].splitlines()],
)
Comment on lines +334 to +339
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🫣


main_path = PurePath(targets[0])
file = input_bundle.load_file(main_path)
assert isinstance(file, FileInput) # help mypy
settings_json = json.loads(files["MANIFEST/settings.json"])
settings = merge_settings(
settings,
Settings.from_dict(settings_json),
lhs_source="command line",
rhs_source="archive settings",
)
integrity_sum = files["MANIFEST/integrity"].strip()
return CompilerData(file, input_bundle, settings, integrity_sum)


def _open_zip(zip_contents):
if isinstance(zip_contents, str):
zip_contents = zip_contents.encode()
try:
buf = BytesIO(zip_contents)
return ZipFile(buf, mode="r")
except BadZipFile as e1:
try:
# don't validate base64 to allow for newlines
zip_contents = b64decode(zip_contents, validate=False)
buf = BytesIO(zip_contents)
return ZipFile(buf, mode="r")
except (BadZipFile, binascii.Error):
raise NotZipInput() from e1

Comment on lines +304 to +369
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

i think we should refactor the compiler so that we can return CompilerData

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

So no vyz before 0.4.0b2 at the very least? 😞

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

i suppose we could keep it this way but with a note to delete after refactored in vyper

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

that conversion of the input bundle is a bit awkward though

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

it is, but this is the workaround I found instead of that archive._lock=None


def from_etherscan(
address: Any, name: str = None, uri: str = None, api_key: str = None
):
Expand Down
16 changes: 16 additions & 0 deletions tests/unitary/fixtures/module_contract.b64
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
UEsDBBQAAAAIAFZSSVkDkp2MFwAAABUAAAAZAAAATUFOSUZFU1QvY29tcGlsZXJfdmVyc2lvbisz0DPRM9BOzs/NzSzRS7VMSbJIsQQAUEs
DBBQAAAAIAFZSSVkPijJQFAAAABIAAAAcAAAATUFOSUZFU1QvY29tcGlsYXRpb25fdGFyZ2V0c8vNTynNSY1Pzs8rKUpMLtErqwQAUEsDBB
QAAAAIAFZSSVlC4tQOAwAAAAEAAAAUAAAATUFOSUZFU1Qvc2VhcmNocGF0aHPTAwBQSwMEFAAAAAgAVlJJWUO/pqMEAAAAAgAAABYAAABNQ
U5JRkVTVC9zZXR0aW5ncy5qc29uq64FAFBLAwQUAAAACABWUklZAAAAAAIAAAAAAAAAGQAAAE1BTklGRVNUL2NsaV9zZXR0aW5ncy50eHQD
AFBLAwQUAAAACABWUklZC8Ct+zUAAABAAAAAEgAAAE1BTklGRVNUL2ludGVncml0eQ3LwREAIAgDsJUqIOI4rZz7j6CvvLJipF8VPrMgENG
1hg8kM7ZJfjZk7DDyulqTFgnv89sDUEsDBBQAAAAIAFZSSVkkrxroaQAAAIYAAAANAAAAbW9kdWxlX2xpYi52eVWLsQqDQBBE+/2KQZukEQ
srIZAm3yELjnrFuWFX7rp8ewwpQqZ68N60eLquWVHokWzH69Z3Q9eL3EtilZkLjs2tXq6j4JxrCqJ5uJujpmNDZoSubOQXTzPL5NSw/f/X4
jQjwjI/hG8jb1BLAwQUAAAACABWUklZ861qTWgAAACjAAAAEgAAAG1vZHVsZV9jb250cmFjdC52eXXLMQ6DMAxA0d2nsNQFloiBqRMnIXIV
01pKYmRC6PHLgoChf37/gbPROxFWtkU049i53nUAkma1gknDGtlHeQEM/C1smSIMVXiDwBNOJLFpn4B7p3XlY7o17fU5tA9cvTEtmv+NNwI
/UEsBAhQDFAAAAAgAVlJJWQOSnYwXAAAAFQAAABkAAAAAAAAAAAAAAIABAAAAAE1BTklGRVNUL2NvbXBpbGVyX3ZlcnNpb25QSwECFAMUAA
AACABWUklZD4oyUBQAAAASAAAAHAAAAAAAAAAAAAAAgAFOAAAATUFOSUZFU1QvY29tcGlsYXRpb25fdGFyZ2V0c1BLAQIUAxQAAAAIAFZSS
VlC4tQOAwAAAAEAAAAUAAAAAAAAAAAAAACAAZwAAABNQU5JRkVTVC9zZWFyY2hwYXRoc1BLAQIUAxQAAAAIAFZSSVlDv6ajBAAAAAIAAAAW
AAAAAAAAAAAAAACAAdEAAABNQU5JRkVTVC9zZXR0aW5ncy5qc29uUEsBAhQDFAAAAAgAVlJJWQAAAAACAAAAAAAAABkAAAAAAAAAAAAAAIA
BCQEAAE1BTklGRVNUL2NsaV9zZXR0aW5ncy50eHRQSwECFAMUAAAACABWUklZC8Ct+zUAAABAAAAAEgAAAAAAAAAAAAAAgAFCAQAATUFOSU
ZFU1QvaW50ZWdyaXR5UEsBAhQDFAAAAAgAVlJJWSSvGuhpAAAAhgAAAA0AAAAAAAAAAAAAAIABpwEAAG1vZHVsZV9saWIudnlQSwECFAMUA
AAACABWUklZ861qTWgAAACjAAAAEgAAAAAAAAAAAAAAgAE7AgAAbW9kdWxlX2NvbnRyYWN0LnZ5UEsFBgAAAAAIAAgAGQIAANMCAAAAAA==
Binary file added tests/unitary/fixtures/module_contract.vyz
Binary file not shown.
6 changes: 3 additions & 3 deletions tests/unitary/test_modules.py
Original file line number Diff line number Diff line change
Expand Up @@ -7,9 +7,9 @@
FIXTURES = Path(__file__).parent / "fixtures"


@pytest.fixture
def module_contract():
return boa.load(FIXTURES / "module_contract.vy")
@pytest.fixture(params=["vyz", "vy", "b64"])
def module_contract(request):
return boa.load(FIXTURES / f"module_contract.{request.param}")


def test_user_raise(module_contract):
Expand Down
Loading