WIP brownie interface

pull/25/head
yhtiyar 2021-12-03 01:22:31 +03:00
rodzic 234d44985e
commit 76ac5cac51
4 zmienionych plików z 253 dodań i 4 usunięć

2
.gitignore vendored
Wyświetl plik

@ -133,3 +133,5 @@ venv/
.venv/
.moonworm/
.vscode/
.secrets/
generated/

Wyświetl plik

@ -0,0 +1,32 @@
# Code generated by moonworm : https://github.com/bugout-dev/moonworm
# Moonworm version : {moonworm_version}
import argparse
from typing import Any, List, Optional
from eth_typing.evm import ChecksumAddress
import os
import json
from brownie import Contract, network, project
from brownie.network.contract import ContractContainer
PROJECT_DIRECTORY = os.path.abspath(os.path.join(os.path.dirname(__file__), ".."))
BUILD_DIRECTORY = os.path.join(PROJECT_DIRECTORY, "build", "contracts")
PROJECT = project.load(PROJECT_DIRECTORY)
def contract_from_build(abi_name: str) -> ContractContainer:
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)
return ContractContainer(PROJECT, build)
{contract_body}

Wyświetl plik

@ -13,6 +13,7 @@ from moonworm.watch import watch_contract
from .contracts import CU, ERC20, ERC721
from .crawler.networks import Network
from .generator import (
generate_brownie_interface,
generate_contract_cli_content,
generate_contract_interface_content,
)
@ -80,6 +81,27 @@ def handle_generate(args: argparse.Namespace) -> None:
print(f"Files are successfully generated to:{args.outdir}")
def handle_brownie_generate(args: argparse.Namespace):
Path(args.outdir).mkdir(exist_ok=True)
project_directory = args.project
build_directory = os.path.join(project_directory, "build", "contracts")
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?"
)
with open(build_file_path, "r") as ifp:
build = json.load(ifp)
abi = build["abi"]
interface = generate_brownie_interface(abi, args.name)
write_file(interface, os.path.join(args.outdir, args.name + ".py"))
def handle_watch(args: argparse.Namespace) -> None:
if args.abi == "erc20":
contract_abi = ERC20.abi()
@ -268,6 +290,29 @@ def generate_argument_parser() -> argparse.ArgumentParser:
watch_cu_parser.set_defaults(func=handle_watch_cu)
generate_brownie_parser = subcommands.add_parser(
"generate-brownie", description="Moonworm code generator for brownie projects"
)
generate_brownie_parser.add_argument(
"-o",
"--outdir",
required=True,
help=f"Output directory where files will be generated.",
)
generate_brownie_parser.add_argument(
"--name",
"-n",
required=True,
help="Prefix name for generated files",
)
generate_brownie_parser.add_argument(
"-p",
"--project",
required=True,
help=f"Path to brownie project directory",
)
generate_brownie_parser.set_defaults(func=handle_brownie_generate)
generate_parser = subcommands.add_parser(
"generate", description="Moonworm code generator"
)

Wyświetl plik

@ -1,7 +1,7 @@
import keyword
import logging
import os
from typing import Any, Dict, List, Union, cast
from typing import Any, Dict, List, Sequence, Union, cast
import libcst as cst
from libcst._nodes.statement import BaseCompoundStatement
@ -27,8 +27,20 @@ except Exception as e:
logging.warn(f"WARNING: Could not load cli template from {CLI_TEMPLATE_PATH}:")
logging.warn(e)
BROWNIE_INTERFACE_TEMPLATE_PATH = os.path.join(
os.path.dirname(__file__), "brownie_contract.py.template"
)
try:
with open(BROWNIE_INTERFACE_TEMPLATE_PATH, "r") as ifp:
BROWNIE_INTERFACE_TEMPLATE = ifp.read()
except Exception as e:
logging.warn(
f"WARNING: Could not load cli template from {BROWNIE_INTERFACE_TEMPLATE_PATH}:"
)
logging.warn(e)
def make_annotation(types: list):
def make_annotation(types: list, optional: bool = False):
if len(types) == 1:
return cst.Annotation(annotation=cst.Name(types[0]))
union_slice = []
@ -40,10 +52,18 @@ def make_annotation(types: list):
)
),
)
return cst.Annotation(
annotation = cst.Annotation(
annotation=cst.Subscript(value=cst.Name("Union"), slice=union_slice)
)
if optional:
annotation = cst.Annotation(
annotation=cst.Subscript(
value=cst.Name("Optional"),
slice=cast(Sequence[cst.SubscriptElement], annotation),
)
)
def normalize_abi_name(name: str) -> str:
if keyword.iskeyword(name):
@ -66,7 +86,56 @@ def python_type(evm_type: str) -> List[str]:
elif evm_type == "tuple[]":
return ["list"]
else:
raise ValueError(f"Cannot convert to python type {evm_type}")
return ["Any"]
def generate_brownie_contract_class(
abi: List[Dict[str, Any]],
contract_name: str,
) -> cst.ClassDef:
class_name = contract_name
class_constructor = cst.FunctionDef(
name=cst.Name("__init__"),
body=cst.IndentedBlock(
body=[
cst.parse_statement("self.address = contract_address"),
cst.parse_statement(
f"self.contract = contract_from_build({contract_name})"
),
]
),
params=cst.Parameters(
params=[
cst.Param(name=cst.Name("self")),
cst.Param(
name=cst.Name("contract_address"),
annotation=make_annotation(["ChecksumAddress"], optional=True),
),
]
),
)
contract_constructors = [c for c in abi if c["type"] == "constructor"]
if len(contract_constructors) == 1:
contract_constructor = contract_constructors[0]
elif len(contract_constructors) == 0:
contract_constructor = {"inputs": []}
else:
raise ValueError("Multiple constructors found in ABI")
contract_constructor["name"] = "constructor"
class_functions = (
[class_constructor]
+ [generate_brownie_constructor_function(contract_constructor)]
+ [
generate_brownie_contract_function(function)
for function in abi
if function["type"] == "function"
]
)
return cst.ClassDef(
name=cst.Name(class_name), body=cst.IndentedBlock(body=class_functions)
)
def generate_contract_class(
@ -196,6 +265,95 @@ def generate_contract_function(func_object: Dict[str, Any]) -> cst.FunctionDef:
)
def generate_brownie_constructor_function(
func_object: Dict[str, Any]
) -> cst.FunctionDef:
default_param_name = "arg"
default_counter = 1
func_params = []
func_params.append(cst.Param(name=cst.Name("self")))
param_names = []
for param in func_object["inputs"]:
param_name = normalize_abi_name(param["name"])
if param_name == "":
param_name = f"{default_param_name}{default_counter}"
default_counter += 1
param_type = make_annotation(python_type(param["type"]))
param_names.append(param_name)
func_params.append(
cst.Param(
name=cst.Name(value=param_name),
annotation=param_type,
)
)
func_params.append(cst.Param(name=cst.Name("signer")))
func_name = "deploy"
param_names.append("{'from': signer}")
proxy_call_code = (
f"deployed_contract = self.contract.deploy({','.join(param_names)})"
)
func_body = cst.IndentedBlock(
body=[
cst.parse_statement(proxy_call_code),
cst.parse_statement("self.address=deployed_contract.address"),
]
)
return cst.FunctionDef(
name=cst.Name(func_name),
params=cst.Parameters(params=func_params),
body=func_body,
)
def generate_brownie_contract_function(func_object: Dict[str, Any]) -> cst.FunctionDef:
default_param_name = "arg"
default_counter = 1
func_params = []
func_params.append(cst.Param(name=cst.Name("self")))
param_names = []
for param in func_object["inputs"]:
param_name = normalize_abi_name(param["name"])
if param_name == "":
param_name = f"{default_param_name}{default_counter}"
default_counter += 1
param_type = make_annotation(python_type(param["type"]))
param_names.append(param_name)
func_params.append(
cst.Param(
name=cst.Name(value=param_name),
annotation=param_type,
)
)
func_raw_name = normalize_abi_name(func_object["name"])
func_name = cst.Name(func_raw_name)
if func_object["stateMutability"] == "view":
proxy_call_code = (
f"return self.contract.{func_raw_name}.call({','.join(param_names)})"
)
else:
func_params.append(cst.Param(name=cst.Name(value="signer")))
param_names.append(f"{{'from': signer}}")
proxy_call_code = (
f"return self.contract.{func_raw_name}({','.join(param_names)})"
)
func_body = cst.IndentedBlock(body=[cst.parse_statement(proxy_call_code)])
func_returns = cst.Annotation(annotation=cst.Name(value="Any"))
return cst.FunctionDef(
name=func_name,
params=cst.Parameters(params=func_params),
body=func_body,
returns=func_returns,
)
def generate_argument_parser_function(abi: List[Dict[str, Any]]) -> cst.FunctionDef:
def generate_function_subparser(
function_abi: Dict[str, Any],
@ -340,3 +498,15 @@ def generate_contract_cli_content(abi: List[Dict[str, Any]], abi_file_name: str)
)
return content
def generate_brownie_interface(abi: List[Dict[str, Any]], contract_name: str) -> str:
contract_body = cst.Module(
body=[generate_brownie_contract_class(abi, contract_name)]
).code
content = BROWNIE_INTERFACE_TEMPLATE.format(
contract_body=contract_body,
moonworm_version=MOONWORM_VERSION,
)
return content