kopia lustrzana https://github.com/bugout-dev/moonworm
Merge pull request #24 from bugout-dev/brownie-support
Added brownie support in moonworm via "moonworm generate-brownie"pull/25/head v0.1.0
commit
f3a926be06
|
@ -133,3 +133,5 @@ venv/
|
|||
.venv/
|
||||
.moonworm/
|
||||
.vscode/
|
||||
.secrets/
|
||||
generated/
|
|
@ -12,10 +12,11 @@ from moonworm.watch import watch_contract
|
|||
|
||||
from .contracts import CU, ERC20, ERC721
|
||||
from .crawler.networks import Network
|
||||
from .generator import (
|
||||
from .generators.basic import (
|
||||
generate_contract_cli_content,
|
||||
generate_contract_interface_content,
|
||||
)
|
||||
from .generators.brownie import generate_brownie_interface
|
||||
|
||||
|
||||
def write_file(content: str, path: str):
|
||||
|
@ -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"
|
||||
)
|
||||
|
|
|
@ -1,13 +1,16 @@
|
|||
from collections import defaultdict
|
||||
import copy
|
||||
import keyword
|
||||
import logging
|
||||
import os
|
||||
from typing import Any, Dict, List, Union, cast
|
||||
from typing import Any, Dict, List, Sequence, Union, cast
|
||||
|
||||
import black
|
||||
import black.mode
|
||||
import inflection
|
||||
import libcst as cst
|
||||
from libcst._nodes.statement import BaseCompoundStatement
|
||||
from web3.types import ABIFunction
|
||||
|
||||
from .version import MOONWORM_VERSION
|
||||
from ..version import MOONWORM_VERSION
|
||||
|
||||
CONTRACT_TEMPLATE_PATH = os.path.join(os.path.dirname(__file__), "contract.py.template")
|
||||
try:
|
||||
|
@ -28,21 +31,38 @@ except Exception as e:
|
|||
logging.warn(e)
|
||||
|
||||
|
||||
def make_annotation(types: list):
|
||||
if len(types) == 1:
|
||||
return cst.Annotation(annotation=cst.Name(types[0]))
|
||||
union_slice = []
|
||||
for _type in types:
|
||||
union_slice.append(
|
||||
cst.SubscriptElement(
|
||||
slice=cst.Index(
|
||||
value=cst.Name(_type),
|
||||
)
|
||||
),
|
||||
def format_code(code: str) -> str:
|
||||
formatted_code = black.format_str(code, mode=black.mode.Mode())
|
||||
return formatted_code
|
||||
|
||||
|
||||
def make_annotation(types: list, optional: bool = False):
|
||||
annotation = cst.Annotation(annotation=cst.Name(types[0]))
|
||||
if len(types) > 1:
|
||||
union_slice = []
|
||||
for _type in types:
|
||||
union_slice.append(
|
||||
cst.SubscriptElement(
|
||||
slice=cst.Index(
|
||||
value=cst.Name(_type),
|
||||
)
|
||||
),
|
||||
)
|
||||
annotation = cst.Annotation(
|
||||
annotation=cst.Subscript(value=cst.Name("Union"), slice=union_slice)
|
||||
)
|
||||
return 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=[
|
||||
cst.SubscriptElement(slice=cst.Index(value=annotation.annotation))
|
||||
],
|
||||
)
|
||||
)
|
||||
|
||||
return annotation
|
||||
|
||||
|
||||
def normalize_abi_name(name: str) -> str:
|
||||
|
@ -66,7 +86,7 @@ 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_contract_class(
|
||||
|
@ -121,6 +141,104 @@ def generate_contract_class(
|
|||
)
|
||||
|
||||
|
||||
def function_spec(function_abi: Dict[str, Any]) -> Dict[str, Dict[str, Any]]:
|
||||
"""
|
||||
Accepts function interface definitions from smart contract ABIs. An example input:
|
||||
{
|
||||
"inputs": [
|
||||
{
|
||||
"internalType": "uint256",
|
||||
"name": "_tokenId",
|
||||
"type": "uint256"
|
||||
}
|
||||
],
|
||||
"name": "getDNA",
|
||||
"outputs": [
|
||||
{
|
||||
"internalType": "uint256",
|
||||
"name": "",
|
||||
"type": "uint256"
|
||||
}
|
||||
],
|
||||
"stateMutability": "view",
|
||||
"type": "function"
|
||||
}
|
||||
|
||||
Returns an dictionary of the form:
|
||||
{
|
||||
"abi": "getDNA",
|
||||
"method": "get_dna",
|
||||
"cli": "get-dna",
|
||||
"inputs": [
|
||||
{
|
||||
"abi": "_tokenId",
|
||||
"method": "_tokenId",
|
||||
"cli": "--token-id",
|
||||
"args": "token_id",
|
||||
"type": int,
|
||||
"cli_type": int,
|
||||
},
|
||||
],
|
||||
"transact": False,
|
||||
}
|
||||
"""
|
||||
abi_name = function_abi.get("name")
|
||||
if abi_name is None:
|
||||
raise ValueError('function_spec -- Valid function ABI must have a "name" field')
|
||||
|
||||
underscored_name = inflection.underscore(abi_name)
|
||||
function_name = normalize_abi_name(underscored_name)
|
||||
cli_name = inflection.dasherize(underscored_name)
|
||||
|
||||
default_input_name = "arg"
|
||||
default_counter = 1
|
||||
|
||||
inputs: List[Dict[str, Any]] = []
|
||||
for item in function_abi.get("inputs", []):
|
||||
item_abi_name = item.get("name")
|
||||
if not item_abi_name:
|
||||
item_abi_name = f"{default_input_name}{default_counter}"
|
||||
default_counter += 1
|
||||
|
||||
item_method_name = normalize_abi_name(inflection.underscore(item_abi_name))
|
||||
item_args_name = item_method_name
|
||||
if item_args_name.startswith("_") or item_args_name.endswith("_"):
|
||||
item_args_name = item_args_name.strip("_") + "_arg"
|
||||
|
||||
item_cli_name = f"--{inflection.dasherize(item_args_name)}"
|
||||
|
||||
item_type = python_type(item["type"])[0]
|
||||
|
||||
item_cli_type = None
|
||||
if item_type in {"int", "bool"}:
|
||||
item_cli_type = item_type
|
||||
|
||||
input_spec: Dict[str, Any] = {
|
||||
"abi": item_abi_name,
|
||||
"method": item_method_name,
|
||||
"cli": item_cli_name,
|
||||
"args": item_args_name,
|
||||
"type": item_type,
|
||||
"cli_type": item_cli_type,
|
||||
}
|
||||
|
||||
inputs.append(input_spec)
|
||||
|
||||
transact = True
|
||||
if function_abi.get("stateMutability") == "view":
|
||||
transact = False
|
||||
|
||||
spec = {
|
||||
"abi": abi_name,
|
||||
"method": function_name,
|
||||
"cli": cli_name,
|
||||
"inputs": inputs,
|
||||
"transact": transact,
|
||||
}
|
||||
|
||||
return spec
|
||||
|
||||
|
||||
def generate_contract_constructor_function(
|
||||
func_object: Dict[str, Any]
|
||||
) -> cst.FunctionDef:
|
||||
|
@ -317,7 +435,7 @@ def generate_argument_parser_function(abi: List[Dict[str, Any]]) -> cst.Function
|
|||
|
||||
|
||||
def generate_contract_interface_content(
|
||||
abi: List[Dict[str, Any]], abi_file_name: str
|
||||
abi: List[Dict[str, Any]], abi_file_name: str, format: bool = True
|
||||
) -> str:
|
||||
contract_body = cst.Module(body=[generate_contract_class(abi)]).code
|
||||
|
||||
|
@ -326,11 +444,16 @@ def generate_contract_interface_content(
|
|||
moonworm_version=MOONWORM_VERSION,
|
||||
abi_file_name=abi_file_name,
|
||||
)
|
||||
|
||||
if format:
|
||||
content = format_code(content)
|
||||
|
||||
return content
|
||||
|
||||
|
||||
def generate_contract_cli_content(abi: List[Dict[str, Any]], abi_file_name: str) -> str:
|
||||
|
||||
def generate_contract_cli_content(
|
||||
abi: List[Dict[str, Any]], abi_file_name: str, format: bool = True
|
||||
) -> str:
|
||||
cli_body = cst.Module(body=[generate_argument_parser_function(abi)]).code
|
||||
|
||||
content = CLI_FILE_TEMPLATE.format(
|
||||
|
@ -339,4 +462,7 @@ def generate_contract_cli_content(abi: List[Dict[str, Any]], abi_file_name: str)
|
|||
abi_file_name=abi_file_name,
|
||||
)
|
||||
|
||||
if format:
|
||||
content = format_code(content)
|
||||
|
||||
return content
|
|
@ -0,0 +1,593 @@
|
|||
import logging
|
||||
import os
|
||||
from typing import Any, Dict, List, Optional
|
||||
|
||||
import libcst as cst
|
||||
from libcst._nodes.statement import SimpleStatementLine
|
||||
|
||||
from ..version import MOONWORM_VERSION
|
||||
from .basic import (
|
||||
format_code,
|
||||
function_spec,
|
||||
make_annotation,
|
||||
)
|
||||
|
||||
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 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(f'self.contract_name = "{contract_name}"'),
|
||||
cst.parse_statement("self.address = contract_address"),
|
||||
cst.parse_statement("self.contract = None"),
|
||||
cst.parse_statement(f'self.abi = get_abi_json("{contract_name}")'),
|
||||
cst.If(
|
||||
test=cst.Comparison(
|
||||
left=cst.Attribute(
|
||||
attr=cst.Name(value="address"),
|
||||
value=cst.Name(value="self"),
|
||||
),
|
||||
comparisons=[
|
||||
cst.ComparisonTarget(
|
||||
operator=cst.IsNot(), comparator=cst.Name(value="None")
|
||||
)
|
||||
],
|
||||
),
|
||||
body=cst.parse_statement(
|
||||
"self.contract: Optional[Contract] = Contract.from_abi(self.contract_name, self.address, self.abi)"
|
||||
),
|
||||
),
|
||||
]
|
||||
),
|
||||
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_assert_contract_is_instantiated(),
|
||||
]
|
||||
+ [
|
||||
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_brownie_constructor_function(
|
||||
func_object: Dict[str, Any]
|
||||
) -> cst.FunctionDef:
|
||||
spec = function_spec(func_object)
|
||||
func_params = []
|
||||
func_params.append(cst.Param(name=cst.Name("self")))
|
||||
param_names = []
|
||||
for param in spec["inputs"]:
|
||||
param_type = make_annotation([param["type"]])
|
||||
param_names.append(param["method"])
|
||||
func_params.append(
|
||||
cst.Param(
|
||||
name=cst.Name(value=param["method"]),
|
||||
annotation=param_type,
|
||||
)
|
||||
)
|
||||
func_params.append(cst.Param(name=cst.Name("transaction_config")))
|
||||
|
||||
func_name = "deploy"
|
||||
param_names.append("transaction_config")
|
||||
|
||||
func_body = cst.IndentedBlock(
|
||||
body=[
|
||||
cst.parse_statement(
|
||||
f"contract_class = contract_from_build(self.contract_name)"
|
||||
),
|
||||
cst.parse_statement(
|
||||
f"deployed_contract = contract_class.deploy({','.join(param_names)})"
|
||||
),
|
||||
cst.parse_statement("self.address = deployed_contract.address"),
|
||||
cst.parse_statement("self.contract = deployed_contract"),
|
||||
]
|
||||
)
|
||||
|
||||
return cst.FunctionDef(
|
||||
name=cst.Name(func_name),
|
||||
params=cst.Parameters(params=func_params),
|
||||
body=func_body,
|
||||
)
|
||||
|
||||
|
||||
def generate_assert_contract_is_instantiated() -> cst.FunctionDef:
|
||||
function_body = cst.IndentedBlock(
|
||||
body=[
|
||||
cst.If(
|
||||
test=cst.Comparison(
|
||||
left=cst.Attribute(
|
||||
attr=cst.Name(value="contract"), value=cst.Name(value="self")
|
||||
),
|
||||
comparisons=[
|
||||
cst.ComparisonTarget(
|
||||
operator=cst.Is(), comparator=cst.Name(value="None")
|
||||
)
|
||||
],
|
||||
),
|
||||
body=cst.parse_statement(
|
||||
'raise Exception("contract has not been instantiated")'
|
||||
),
|
||||
),
|
||||
],
|
||||
)
|
||||
function_def = cst.FunctionDef(
|
||||
name=cst.Name(value="assert_contract_is_instantiated"),
|
||||
params=cst.Parameters(
|
||||
params=[cst.Param(name=cst.Name(value="self"))],
|
||||
),
|
||||
body=function_body,
|
||||
returns=cst.Annotation(annotation=cst.Name(value="None")),
|
||||
)
|
||||
return function_def
|
||||
|
||||
|
||||
def generate_brownie_contract_function(func_object: Dict[str, Any]) -> cst.FunctionDef:
|
||||
spec = function_spec(func_object)
|
||||
func_params = []
|
||||
func_params.append(cst.Param(name=cst.Name("self")))
|
||||
|
||||
param_names = []
|
||||
for param in spec["inputs"]:
|
||||
param_type = make_annotation([param["type"]])
|
||||
param_name = param["method"]
|
||||
param_names.append(param_name)
|
||||
func_params.append(
|
||||
cst.Param(
|
||||
name=cst.Name(value=param_name),
|
||||
annotation=param_type,
|
||||
)
|
||||
)
|
||||
|
||||
func_raw_name = spec["abi"]
|
||||
func_python_name = spec["method"]
|
||||
func_name = cst.Name(value=func_python_name)
|
||||
if spec["transact"]:
|
||||
func_params.append(cst.Param(name=cst.Name(value="transaction_config")))
|
||||
param_names.append("transaction_config")
|
||||
proxy_call_code = (
|
||||
f"return self.contract.{func_raw_name}({','.join(param_names)})"
|
||||
)
|
||||
else:
|
||||
proxy_call_code = (
|
||||
f"return self.contract.{func_raw_name}.call({','.join(param_names)})"
|
||||
)
|
||||
|
||||
func_body = cst.IndentedBlock(
|
||||
body=[
|
||||
cst.parse_statement("self.assert_contract_is_instantiated()"),
|
||||
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_get_transaction_config() -> cst.FunctionDef:
|
||||
function_body = cst.IndentedBlock(
|
||||
body=[
|
||||
cst.parse_statement(
|
||||
"signer = network.accounts.load(args.sender, args.password)"
|
||||
),
|
||||
cst.parse_statement(
|
||||
'transaction_config: Dict[str, Any] = {"from": signer}'
|
||||
),
|
||||
cst.If(
|
||||
test=cst.Comparison(
|
||||
left=cst.Attribute(
|
||||
attr=cst.Name(value="gas_price"), value=cst.Name(value="args")
|
||||
),
|
||||
comparisons=[
|
||||
cst.ComparisonTarget(
|
||||
operator=cst.IsNot(), comparator=cst.Name(value="None")
|
||||
)
|
||||
],
|
||||
),
|
||||
body=cst.parse_statement(
|
||||
'transaction_config["gas_price"] = args.gas_price'
|
||||
),
|
||||
),
|
||||
cst.If(
|
||||
test=cst.Comparison(
|
||||
left=cst.Attribute(
|
||||
attr=cst.Name(value="confirmations"),
|
||||
value=cst.Name(value="args"),
|
||||
),
|
||||
comparisons=[
|
||||
cst.ComparisonTarget(
|
||||
operator=cst.IsNot(), comparator=cst.Name(value="None")
|
||||
)
|
||||
],
|
||||
),
|
||||
body=cst.parse_statement(
|
||||
'transaction_config["required_confs"] = args.confirmations'
|
||||
),
|
||||
),
|
||||
cst.parse_statement("return transaction_config"),
|
||||
],
|
||||
)
|
||||
function_def = cst.FunctionDef(
|
||||
name=cst.Name(value="get_transaction_config"),
|
||||
params=cst.Parameters(
|
||||
params=[
|
||||
cst.Param(
|
||||
name=cst.Name(value="args"),
|
||||
annotation=cst.Annotation(
|
||||
annotation=cst.Attribute(
|
||||
attr=cst.Name(value="Namespace"),
|
||||
value=cst.Name(value="argparse"),
|
||||
)
|
||||
),
|
||||
)
|
||||
],
|
||||
),
|
||||
body=function_body,
|
||||
returns=cst.Annotation(
|
||||
annotation=cst.Subscript(
|
||||
value=cst.Name(value="Dict"),
|
||||
slice=[
|
||||
cst.SubscriptElement(slice=cst.Index(value=cst.Name(value="str"))),
|
||||
cst.SubscriptElement(slice=cst.Index(value=cst.Name(value="Any"))),
|
||||
],
|
||||
)
|
||||
),
|
||||
)
|
||||
return function_def
|
||||
|
||||
|
||||
def generate_cli_handler(
|
||||
function_abi: Dict[str, Any], contract_name: str
|
||||
) -> Optional[cst.FunctionDef]:
|
||||
"""
|
||||
Generates a handler which translates parsed command line arguments to method calls on the generated
|
||||
smart contract interface.
|
||||
|
||||
Returns None if it is not appropriate for the given function to have a handler (e.g. fallback or
|
||||
receive). constructor is handled separately with a deploy handler.
|
||||
"""
|
||||
spec = function_spec(function_abi)
|
||||
function_name = spec["method"]
|
||||
|
||||
function_body_raw: List[cst.CSTNode] = []
|
||||
|
||||
# Instantiate the contract
|
||||
function_body_raw.extend(
|
||||
[
|
||||
cst.parse_statement("network.connect(args.network)"),
|
||||
cst.parse_statement(f"contract = {contract_name}(args.address)"),
|
||||
]
|
||||
)
|
||||
|
||||
# If a transaction is required, extract transaction parameters from CLI
|
||||
requires_transaction = True
|
||||
if function_abi["stateMutability"] == "view":
|
||||
requires_transaction = False
|
||||
|
||||
if requires_transaction:
|
||||
function_body_raw.append(
|
||||
cst.parse_statement("transaction_config = get_transaction_config(args)")
|
||||
)
|
||||
|
||||
# Call contract method
|
||||
call_args: List[cst.Arg] = []
|
||||
for param in spec["inputs"]:
|
||||
call_args.append(
|
||||
cst.Arg(
|
||||
keyword=cst.Name(value=param["method"]),
|
||||
value=cst.Attribute(
|
||||
attr=cst.Name(value=param["args"]), value=cst.Name(value="args")
|
||||
),
|
||||
)
|
||||
)
|
||||
if requires_transaction:
|
||||
call_args.append(
|
||||
cst.Arg(
|
||||
keyword=cst.Name(value="transaction_config"),
|
||||
value=cst.Name(value="transaction_config"),
|
||||
)
|
||||
)
|
||||
method_call = cst.Call(
|
||||
func=cst.Attribute(
|
||||
attr=cst.Name(value=spec["method"]),
|
||||
value=cst.Name(value="contract"),
|
||||
),
|
||||
args=call_args,
|
||||
)
|
||||
method_call_result_statement = cst.SimpleStatementLine(
|
||||
body=[
|
||||
cst.Assign(
|
||||
targets=[cst.AssignTarget(target=cst.Name(value="result"))],
|
||||
value=method_call,
|
||||
)
|
||||
]
|
||||
)
|
||||
function_body_raw.append(method_call_result_statement)
|
||||
|
||||
function_body_raw.append(cst.parse_statement("print(result)"))
|
||||
|
||||
function_body = cst.IndentedBlock(body=function_body_raw)
|
||||
|
||||
function_def = cst.FunctionDef(
|
||||
name=cst.Name(value=f"handle_{function_name}"),
|
||||
params=cst.Parameters(
|
||||
params=[
|
||||
cst.Param(
|
||||
name=cst.Name(value="args"),
|
||||
annotation=cst.Annotation(
|
||||
annotation=cst.Attribute(
|
||||
attr=cst.Name(value="Namespace"),
|
||||
value=cst.Name(value="argparse"),
|
||||
)
|
||||
),
|
||||
)
|
||||
],
|
||||
),
|
||||
body=function_body,
|
||||
returns=cst.Annotation(annotation=cst.Name(value="None")),
|
||||
)
|
||||
return function_def
|
||||
|
||||
|
||||
def generate_add_default_arguments() -> cst.FunctionDef:
|
||||
function_body = cst.IndentedBlock(
|
||||
body=[
|
||||
cst.parse_statement(
|
||||
'parser.add_argument("--network", required=True, help="Name of brownie network to connect to")'
|
||||
),
|
||||
cst.parse_statement(
|
||||
'parser.add_argument("--address", required=False, help="Address of deployed contract to connect to")'
|
||||
),
|
||||
# TODO(zomglings): The generated code could be confusing for users. Fix this so that it adds additional arguments as part of the "if" statement
|
||||
cst.If(
|
||||
test=cst.UnaryOperation(
|
||||
operator=cst.Not(), expression=cst.Name(value="transact")
|
||||
),
|
||||
body=cst.parse_statement("return"),
|
||||
),
|
||||
cst.parse_statement(
|
||||
'parser.add_argument("--sender", required=True, help="Path to keystore file for transaction sender")'
|
||||
),
|
||||
cst.parse_statement(
|
||||
'parser.add_argument("--password", required=False, help="Password to keystore file (if you do not provide it, you will be prompted for it)")'
|
||||
),
|
||||
cst.parse_statement(
|
||||
'parser.add_argument("--gas-price", default=None, help="Gas price at which to submit transaction")'
|
||||
),
|
||||
cst.parse_statement(
|
||||
'parser.add_argument("--confirmations", type=int, default=None, help="Number of confirmations to await before considering a transaction completed")'
|
||||
),
|
||||
],
|
||||
)
|
||||
function_def = cst.FunctionDef(
|
||||
name=cst.Name(value="add_default_arguments"),
|
||||
params=cst.Parameters(
|
||||
params=[
|
||||
cst.Param(
|
||||
name=cst.Name(value="parser"),
|
||||
annotation=cst.Annotation(
|
||||
annotation=cst.Attribute(
|
||||
attr=cst.Name(value="ArgumentParser"),
|
||||
value=cst.Name(value="argparse"),
|
||||
)
|
||||
),
|
||||
),
|
||||
cst.Param(
|
||||
name=cst.Name(value="transact"),
|
||||
annotation=cst.Annotation(
|
||||
annotation=cst.Name(value="bool"),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
body=function_body,
|
||||
returns=cst.Annotation(annotation=cst.Name(value="None")),
|
||||
)
|
||||
return function_def
|
||||
|
||||
|
||||
def generate_cli_generator(
|
||||
abi: List[Dict[str, Any]], contract_name: Optional[str] = None
|
||||
) -> cst.FunctionDef:
|
||||
"""
|
||||
Generates a generate_cli function that creates a CLI for the generated contract.
|
||||
"""
|
||||
if contract_name is None:
|
||||
contract_name = "generated contract"
|
||||
statements: List[cst.SimpleStatementLine] = [
|
||||
cst.parse_statement(
|
||||
f'parser = argparse.ArgumentParser(description="CLI for {contract_name}")'
|
||||
),
|
||||
cst.parse_statement("parser.set_defaults(func=lambda _: parser.print_help())"),
|
||||
cst.parse_statement("subcommands = parser.add_subparsers()"),
|
||||
]
|
||||
for item in abi:
|
||||
if item["type"] != "function":
|
||||
continue
|
||||
spec = function_spec(item)
|
||||
subparser_statements: List[SimpleStatementLine] = [cst.Newline()]
|
||||
|
||||
subparser_name = f'{spec["method"]}_parser'
|
||||
|
||||
subparser_statements.append(
|
||||
cst.parse_statement(
|
||||
f'{subparser_name} = subcommands.add_parser("{spec["cli"]}")'
|
||||
)
|
||||
)
|
||||
subparser_statements.append(
|
||||
cst.parse_statement(
|
||||
f'add_default_arguments({subparser_name}, {spec["transact"]})'
|
||||
)
|
||||
)
|
||||
|
||||
for param in spec["inputs"]:
|
||||
call_args = [
|
||||
cst.Arg(
|
||||
value=cst.SimpleString(value=f'u"{param["cli"]}"'),
|
||||
),
|
||||
cst.Arg(
|
||||
keyword=cst.Name(value="required"),
|
||||
value=cst.Name(value="True"),
|
||||
),
|
||||
]
|
||||
if param["type"] is not None:
|
||||
cst.Arg(
|
||||
keyword=cst.Name(value="type"),
|
||||
value=cst.Name(param["type"]),
|
||||
),
|
||||
|
||||
add_argument_call = cst.Call(
|
||||
func=cst.Attribute(
|
||||
attr=cst.Name(value="add_argument"),
|
||||
value=cst.Name(value=subparser_name),
|
||||
),
|
||||
args=call_args,
|
||||
)
|
||||
add_argument_statement = cst.SimpleStatementLine(
|
||||
body=[cst.Expr(value=add_argument_call)]
|
||||
)
|
||||
subparser_statements.append(add_argument_statement)
|
||||
|
||||
subparser_statements.append(
|
||||
cst.parse_statement(
|
||||
f"{subparser_name}.set_defaults(func=handle_{spec['method']})"
|
||||
)
|
||||
)
|
||||
subparser_statements.append(cst.Newline())
|
||||
statements.extend(subparser_statements)
|
||||
|
||||
statements.append(cst.parse_statement("return parser"))
|
||||
|
||||
function_body = cst.IndentedBlock(body=statements)
|
||||
function_def = cst.FunctionDef(
|
||||
name=cst.Name(value="generate_cli"),
|
||||
params=cst.Parameters(params=[]),
|
||||
body=function_body,
|
||||
returns=cst.Annotation(
|
||||
annotation=cst.Attribute(
|
||||
attr=cst.Name(value="ArgumentParser"), value=cst.Name(value="argparse")
|
||||
)
|
||||
),
|
||||
)
|
||||
return function_def
|
||||
|
||||
|
||||
def generate_main() -> cst.FunctionDef:
|
||||
statements: List[cst.SimpleStatementLine] = [
|
||||
cst.parse_statement("parser = generate_cli()"),
|
||||
cst.parse_statement("args = parser.parse_args()"),
|
||||
cst.parse_statement("args.func(args)"),
|
||||
]
|
||||
function_body = cst.IndentedBlock(body=statements)
|
||||
function_def = cst.FunctionDef(
|
||||
name=cst.Name(value="main"),
|
||||
params=cst.Parameters(params=[]),
|
||||
body=function_body,
|
||||
returns=cst.Annotation(annotation=cst.Name(value="None")),
|
||||
)
|
||||
return function_def
|
||||
|
||||
|
||||
def generate_runner() -> cst.If:
|
||||
module = cst.parse_module(
|
||||
"""
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
"""
|
||||
)
|
||||
return module.body[0]
|
||||
|
||||
|
||||
def generate_brownie_cli(
|
||||
abi: List[Dict[str, Any]], contract_name: str
|
||||
) -> List[cst.FunctionDef]:
|
||||
"""
|
||||
Generates an argparse CLI to a brownie smart contract using the generated smart contract interface.
|
||||
"""
|
||||
get_transaction_config_function = generate_get_transaction_config()
|
||||
add_default_arguments_function = generate_add_default_arguments()
|
||||
handlers = [get_transaction_config_function, add_default_arguments_function]
|
||||
handlers.extend(
|
||||
[
|
||||
generate_cli_handler(function_abi, contract_name)
|
||||
for function_abi in abi
|
||||
if function_abi.get("type") == "function"
|
||||
and function_abi.get("name") is not None
|
||||
]
|
||||
)
|
||||
nodes: List[cst.CSTNode] = [handler for handler in handlers if handler is not None]
|
||||
nodes.append(generate_cli_generator(abi, contract_name))
|
||||
nodes.append(generate_main())
|
||||
nodes.append(generate_runner())
|
||||
return nodes
|
||||
|
||||
|
||||
def generate_brownie_interface(
|
||||
abi: List[Dict[str, Any]], contract_name: str, cli: bool = True, format: bool = True
|
||||
) -> str:
|
||||
contract_class = generate_brownie_contract_class(abi, contract_name)
|
||||
module_body = [contract_class]
|
||||
|
||||
if cli:
|
||||
contract_cli_functions = generate_brownie_cli(abi, contract_name)
|
||||
module_body.extend(contract_cli_functions)
|
||||
|
||||
contract_body = cst.Module(body=module_body).code
|
||||
|
||||
content = BROWNIE_INTERFACE_TEMPLATE.format(
|
||||
contract_body=contract_body,
|
||||
moonworm_version=MOONWORM_VERSION,
|
||||
)
|
||||
|
||||
if format:
|
||||
content = format_code(content)
|
||||
|
||||
return content
|
|
@ -0,0 +1,50 @@
|
|||
# Code generated by moonworm : https://github.com/bugout-dev/moonworm
|
||||
# Moonworm version : {moonworm_version}
|
||||
|
||||
import argparse
|
||||
from typing import Any, Dict, List, Optional, Union
|
||||
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")
|
||||
|
||||
|
||||
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:
|
||||
PROJECT = project.load(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:
|
||||
build = json.load(ifp)
|
||||
|
||||
return ContractContainer(PROJECT, build)
|
||||
|
||||
|
||||
{contract_body}
|
|
@ -0,0 +1,76 @@
|
|||
import unittest
|
||||
|
||||
from moonworm.generators.basic import function_spec
|
||||
|
||||
|
||||
class TestFunctionSpec(unittest.TestCase):
|
||||
def test_function_spec_single_input(self):
|
||||
function_abi = {
|
||||
"inputs": [
|
||||
{"internalType": "uint256", "name": "_tokenId", "type": "uint256"}
|
||||
],
|
||||
"name": "getDNA",
|
||||
"outputs": [{"internalType": "uint256", "name": "", "type": "uint256"}],
|
||||
"stateMutability": "view",
|
||||
"type": "function",
|
||||
}
|
||||
expected_spec = {
|
||||
"abi": "getDNA",
|
||||
"method": "get_dna",
|
||||
"cli": "get-dna",
|
||||
"inputs": [
|
||||
{
|
||||
"abi": "_tokenId",
|
||||
"method": "_token_id",
|
||||
"cli": "--token-id-arg",
|
||||
"args": "token_id_arg",
|
||||
"type": "int",
|
||||
"cli_type": "int",
|
||||
},
|
||||
],
|
||||
"transact": False,
|
||||
}
|
||||
spec = function_spec(function_abi)
|
||||
self.assertDictEqual(spec, expected_spec)
|
||||
|
||||
def test_function_spec_multiple_inputs(self):
|
||||
function_abi = {
|
||||
"inputs": [
|
||||
{"internalType": "address", "name": "owner", "type": "address"},
|
||||
{"internalType": "uint256", "name": "index", "type": "uint256"},
|
||||
],
|
||||
"name": "tokenOfOwnerByIndex",
|
||||
"outputs": [{"internalType": "uint256", "name": "", "type": "uint256"}],
|
||||
"stateMutability": "view",
|
||||
"type": "function",
|
||||
}
|
||||
expected_spec = {
|
||||
"abi": "tokenOfOwnerByIndex",
|
||||
"method": "token_of_owner_by_index",
|
||||
"cli": "token-of-owner-by-index",
|
||||
"inputs": [
|
||||
{
|
||||
"abi": "owner",
|
||||
"method": "owner",
|
||||
"cli": "--owner",
|
||||
"args": "owner",
|
||||
"type": "ChecksumAddress",
|
||||
"cli_type": None,
|
||||
},
|
||||
{
|
||||
"abi": "index",
|
||||
"method": "index",
|
||||
"cli": "--index",
|
||||
"args": "index",
|
||||
"type": "int",
|
||||
"cli_type": "int",
|
||||
},
|
||||
],
|
||||
"transact": False,
|
||||
}
|
||||
spec = function_spec(function_abi)
|
||||
self.assertDictEqual(spec, expected_spec)
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
unittest.main()
|
|
@ -1 +1 @@
|
|||
MOONWORM_VERSION = "0.0.5"
|
||||
MOONWORM_VERSION = "0.1.0"
|
||||
|
|
2
mypy.ini
2
mypy.ini
|
@ -1,4 +1,4 @@
|
|||
[mypy]
|
||||
python_version = 3.8
|
||||
ignore_missing_imports = True
|
||||
exclude = tests|crawler
|
||||
exclude = tests|crawler|moonworm.generators.brownie
|
||||
|
|
14
setup.py
14
setup.py
|
@ -11,12 +11,20 @@ setup(
|
|||
version=MOONWORM_VERSION,
|
||||
packages=find_packages(),
|
||||
package_data={"moonworm": ["py.typed"]},
|
||||
install_requires=["web3[tester]", "tqdm", "libcst", "pysha3<2.0.0,>=1.0.0", "moonstreamdb", "typing-extensions<4,>=3.7.4"],
|
||||
install_requires=[
|
||||
"black",
|
||||
"inflection",
|
||||
"libcst",
|
||||
"moonstreamdb",
|
||||
"pysha3<2.0.0,>=1.0.0",
|
||||
"tqdm",
|
||||
"typing-extensions<4,>=3.7.4",
|
||||
"web3[tester]",
|
||||
],
|
||||
extras_require={
|
||||
"dev": [
|
||||
"black",
|
||||
"mypy",
|
||||
"isort",
|
||||
"mypy",
|
||||
"wheel",
|
||||
],
|
||||
"distribute": ["setuptools", "twine", "wheel"],
|
||||
|
|
Ładowanie…
Reference in New Issue