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/ .venv/
.moonworm/ .moonworm/
.vscode/ .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 .contracts import CU, ERC20, ERC721
from .crawler.networks import Network from .crawler.networks import Network
from .generator import ( from .generator import (
generate_brownie_interface,
generate_contract_cli_content, generate_contract_cli_content,
generate_contract_interface_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}") 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: def handle_watch(args: argparse.Namespace) -> None:
if args.abi == "erc20": if args.abi == "erc20":
contract_abi = ERC20.abi() contract_abi = ERC20.abi()
@ -268,6 +290,29 @@ def generate_argument_parser() -> argparse.ArgumentParser:
watch_cu_parser.set_defaults(func=handle_watch_cu) 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_parser = subcommands.add_parser(
"generate", description="Moonworm code generator" "generate", description="Moonworm code generator"
) )

Wyświetl plik

@ -1,7 +1,7 @@
import keyword import keyword
import logging import logging
import os import os
from typing import Any, Dict, List, Union, cast from typing import Any, Dict, List, Sequence, Union, cast
import libcst as cst import libcst as cst
from libcst._nodes.statement import BaseCompoundStatement 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(f"WARNING: Could not load cli template from {CLI_TEMPLATE_PATH}:")
logging.warn(e) 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: if len(types) == 1:
return cst.Annotation(annotation=cst.Name(types[0])) return cst.Annotation(annotation=cst.Name(types[0]))
union_slice = [] 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) 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: def normalize_abi_name(name: str) -> str:
if keyword.iskeyword(name): if keyword.iskeyword(name):
@ -66,7 +86,56 @@ def python_type(evm_type: str) -> List[str]:
elif evm_type == "tuple[]": elif evm_type == "tuple[]":
return ["list"] return ["list"]
else: 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( 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_argument_parser_function(abi: List[Dict[str, Any]]) -> cst.FunctionDef:
def generate_function_subparser( def generate_function_subparser(
function_abi: Dict[str, Any], 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 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