Skip to content

Commit

Permalink
Merge pull request #121 from moonstream-to/foundry-projects
Browse files Browse the repository at this point in the history
Generate Brownie interface for Foundry project
  • Loading branch information
zomglings authored Nov 8, 2023
2 parents e8b12a5 + ec3e5bc commit 05274f3
Show file tree
Hide file tree
Showing 4 changed files with 143 additions and 8 deletions.
42 changes: 36 additions & 6 deletions moonworm/cli.py
Original file line number Diff line number Diff line change
Expand Up @@ -106,16 +106,34 @@ def handle_brownie_generate(args: argparse.Namespace):

project_directory = args.project
build_directory = os.path.join(project_directory, "build", "contracts")
intermediate_dirs: List[str] = []
if args.foundry:
build_directory = os.path.join(project_directory, "out")

build_file_path = os.path.join(build_directory, f"{args.name}.json")
if not os.path.isfile(build_file_path):
raise IOError(
f"File does not exist: {build_file_path}. Maybe you have to compile the smart contracts?"
)
if args.foundry:
if args.sol_filename is not None:
build_file_path = os.path.join(
build_directory, args.sol_filename, f"{args.name}.json"
)
intermediate_dirs.append(args.sol_filename)
else:
build_file_path = os.path.join(
build_directory, f"{args.name}.sol", f"{args.name}.json"
)
intermediate_dirs.append(f"{args.name}.sol")
else:
if not os.path.isfile(build_file_path):
raise IOError(
f"File does not exist: {build_file_path}. Maybe you have to compile the smart contracts?"
)

with open(build_file_path, "r") as ifp:
build = json.load(ifp)

if args.foundry:
build["contractName"] = args.name

relpath = os.path.relpath(project_directory, args.outdir)
splitted_relpath = [
f'"{item}"' for item in relpath.split(os.sep)
Expand All @@ -129,6 +147,8 @@ def handle_brownie_generate(args: argparse.Namespace):
args.name,
splitted_relpath_string,
prod=args.prod,
foundry=args.foundry,
intermediate_dirs=intermediate_dirs,
)
write_file(interface, os.path.join(args.outdir, args.name + ".py"))

Expand Down Expand Up @@ -355,12 +375,22 @@ def generate_argument_parser() -> argparse.ArgumentParser:
"-p",
"--project",
required=True,
help=f"Path to brownie project directory",
help=f"Path to brownie/foundry project directory",
)
generate_brownie_parser.add_argument(
"--foundry",
action="store_true",
help="Project is using Foundry (if not specified, the assumption is that the project uses brownie)",
)
generate_brownie_parser.add_argument(
"--sol-filename",
required=False,
help="Name of solidity file containing your contract; required if --foundry, moonworm will look for build artifacts in out/<this filename>, defaults to the contract name if not provided",
)
generate_brownie_parser.add_argument(
"--prod",
action="store_true",
help="Generate shippable python interface, in which abi and bytecode will be included inside the generated file",
help="Generate self-contained python interface, in which ABI and bytecode will be included inside the generated file",
)
generate_brownie_parser.set_defaults(func=handle_brownie_generate)

Expand Down
22 changes: 21 additions & 1 deletion moonworm/generators/brownie.py
Original file line number Diff line number Diff line change
Expand Up @@ -21,14 +21,19 @@
BROWNIE_INTERFACE_PROD_TEMPLATE_PATH = os.path.join(
os.path.dirname(__file__), "brownie_contract_prod.py.template"
)
BROWNIE_INTERFACE_FOUNDRY_TEMPLATE_PATH = os.path.join(
os.path.dirname(__file__), "brownie_contract_foundry.py.template"
)
try:
with open(BROWNIE_INTERFACE_TEMPLATE_PATH, "r") as ifp:
BROWNIE_INTERFACE_TEMPLATE = ifp.read()
with open(BROWNIE_INTERFACE_PROD_TEMPLATE_PATH, "r") as ifp:
BROWNIE_INTERFACE_PROD_TEMPLATE = ifp.read()
with open(BROWNIE_INTERFACE_FOUNDRY_TEMPLATE_PATH, "r") as ifp:
BROWNIE_INTERFACE_FOUNDRY_TEMPLATE = ifp.read()
except Exception as e:
logging.warn(
f"WARNING: Could not load cli template from ({BROWNIE_INTERFACE_TEMPLATE_PATH})/({BROWNIE_INTERFACE_PROD_TEMPLATE_PATH}):"
f"WARNING: Could not load cli template from ({BROWNIE_INTERFACE_TEMPLATE_PATH})/({BROWNIE_INTERFACE_PROD_TEMPLATE_PATH})/({BROWNIE_INTERFACE_FOUNDRY_TEMPLATE_PATH}):"
)
logging.warn(e)

Expand Down Expand Up @@ -962,6 +967,8 @@ def generate_brownie_interface(
cli: bool = True,
format: bool = True,
prod: bool = False,
foundry: bool = True,
intermediate_dirs: Optional[List[str]] = None,
) -> str:
"""
Generates Python code which allows you to interact with a smart contract with a given ABI, build data, and a given name.
Expand All @@ -988,6 +995,11 @@ def generate_brownie_interface(
7. `prod`: If True, creates a self-contained file. Generated code will not require reference to an
existing brownie project at its runtime.
8. `foundry`: If True, assumes a Foundry project structure.
9. intermediate_dirs: Currently only used for Foundry projects. Path to build file via intermediate
build subdirectory which takes the name of the Solidity file that the contract is implemented in.
## Outputs
The generated code as a string.
Expand All @@ -1011,6 +1023,14 @@ def generate_brownie_interface(
contract_body=contract_body,
moonworm_version=MOONWORM_VERSION,
)
elif foundry:
content = BROWNIE_INTERFACE_FOUNDRY_TEMPLATE.format(
contract_body=contract_body,
moonworm_version=MOONWORM_VERSION,
relative_path=relative_path,
build_subdir=intermediate_dirs[0],
)

else:
content = BROWNIE_INTERFACE_TEMPLATE.format(
contract_body=contract_body,
Expand Down
85 changes: 85 additions & 0 deletions moonworm/generators/brownie_contract_foundry.py.template
Original file line number Diff line number Diff line change
@@ -0,0 +1,85 @@
# Code generated by moonworm : https://github.com/moonstream-to/moonworm
# Moonworm version : {moonworm_version}

import argparse
import json
import os
from pathlib import Path
from typing import Any, Dict, List, Optional, Union

from brownie import Contract, network, project
from brownie.network.contract import ContractContainer
from eth_typing.evm import ChecksumAddress


PROJECT_DIRECTORY = os.path.abspath(os.path.join(os.path.dirname(__file__), {relative_path}))
BUILD_DIRECTORY = os.path.join(PROJECT_DIRECTORY, "out", "{build_subdir}")

def boolean_argument_type(raw_value: str) -> bool:
TRUE_VALUES = ["1", "t", "y", "true", "yes"]
FALSE_VALUES = ["0", "f", "n", "false", "no"]

if raw_value.lower() in TRUE_VALUES:
return True
elif raw_value.lower() in FALSE_VALUES:
return False

raise ValueError(
f"Invalid boolean argument: {{raw_value}}. Value must be one of: {{','.join(TRUE_VALUES + FALSE_VALUES)}}"
)

def bytes_argument_type(raw_value: str) -> str:
return raw_value

def get_abi_json(abi_name: str) -> List[Dict[str, Any]]:
abi_full_path = os.path.join(BUILD_DIRECTORY, f"{{abi_name}}.json")
if not os.path.isfile(abi_full_path):
raise IOError(
f"File does not exist: {{abi_full_path}}. Maybe you have to compile the smart contracts?"
)

with open(abi_full_path, "r") as ifp:
build = json.load(ifp)

abi_json = build.get("abi")
if abi_json is None:
raise ValueError(f"Could not find ABI definition in: {{abi_full_path}}")

return abi_json


def contract_from_build(abi_name: str) -> ContractContainer:
# This is workaround because brownie currently doesn't support loading the same project multiple
# times. This causes problems when using multiple contracts from the same project in the same
# python project.
PROJECT = project.main.Project("moonworm", Path(PROJECT_DIRECTORY))

abi_full_path = os.path.join(BUILD_DIRECTORY, f"{{abi_name}}.json")
if not os.path.isfile(abi_full_path):
raise IOError(
f"File does not exist: {{abi_full_path}}. Maybe you have to compile the smart contracts?"
)

with open(abi_full_path, "r") as ifp:
foundry_build = json.load(ifp)

build = {{
"type": "contract",
"ast": foundry_build["ast"],
"abi": foundry_build["abi"],
"contractName": abi_name,
"compiler": {{
"version": foundry_build["metadata"]["compiler"]["version"],
}},
"language": foundry_build["metadata"]["language"],
"bytecode": foundry_build["bytecode"]["object"],
"sourceMap": foundry_build["bytecode"]["sourceMap"],
"deployedBytecode": foundry_build["deployedBytecode"]["object"],
"deployedSourceMap": foundry_build["deployedBytecode"]["sourceMap"],
"pcMap": {{}},
}}

return ContractContainer(PROJECT, build)


{contract_body}
2 changes: 1 addition & 1 deletion moonworm/version.py
Original file line number Diff line number Diff line change
@@ -1 +1 @@
MOONWORM_VERSION = "0.7.2"
MOONWORM_VERSION = "0.8.0"

0 comments on commit 05274f3

Please sign in to comment.