kopia lustrzana https://github.com/bugout-dev/dao
Added CLI functionality for basic diamond operations
rodzic
db1437bafb
commit
65fdaa166e
|
@ -0,0 +1,78 @@
|
||||||
|
"""
|
||||||
|
ABI utilities, because web3 doesn't do selectors well.
|
||||||
|
"""
|
||||||
|
|
||||||
|
import glob
|
||||||
|
import json
|
||||||
|
import os
|
||||||
|
from typing import Any, Dict, List, Optional
|
||||||
|
|
||||||
|
from web3 import Web3
|
||||||
|
|
||||||
|
|
||||||
|
def abi_input_signature(input_abi: Dict[str, Any]) -> str:
|
||||||
|
"""
|
||||||
|
Stringifies a function ABI input object according to the ABI specification:
|
||||||
|
https://docs.soliditylang.org/en/v0.5.3/abi-spec.html
|
||||||
|
"""
|
||||||
|
input_type = input_abi["type"]
|
||||||
|
if input_type.startswith("tuple"):
|
||||||
|
component_types = [
|
||||||
|
abi_input_signature(component) for component in input_abi["components"]
|
||||||
|
]
|
||||||
|
input_type = f"({','.join(component_types)}){input_type[len('tuple'):]}"
|
||||||
|
return input_type
|
||||||
|
|
||||||
|
|
||||||
|
def abi_function_signature(function_abi: Dict[str, Any]) -> str:
|
||||||
|
"""
|
||||||
|
Stringifies a function ABI according to the ABI specification:
|
||||||
|
https://docs.soliditylang.org/en/v0.5.3/abi-spec.html
|
||||||
|
"""
|
||||||
|
function_name = function_abi["name"]
|
||||||
|
function_arg_types = [
|
||||||
|
abi_input_signature(input_item) for input_item in function_abi["inputs"]
|
||||||
|
]
|
||||||
|
function_signature = f"{function_name}({','.join(function_arg_types)})"
|
||||||
|
return function_signature
|
||||||
|
|
||||||
|
|
||||||
|
def encode_function_signature(function_abi: Dict[str, Any]) -> Optional[str]:
|
||||||
|
"""
|
||||||
|
Encodes the given function (from ABI) with arguments arg_1, ..., arg_n into its 4 byte signature
|
||||||
|
by calculating:
|
||||||
|
keccak256("<function_name>(<arg_1_type>,...,<arg_n_type>")
|
||||||
|
|
||||||
|
If function_abi is not actually a function ABI (detected by checking if function_abi["type"] == "function),
|
||||||
|
returns None.
|
||||||
|
"""
|
||||||
|
if function_abi["type"] != "function":
|
||||||
|
return None
|
||||||
|
function_signature = abi_function_signature(function_abi)
|
||||||
|
encoded_signature = Web3.keccak(text=function_signature)[:4]
|
||||||
|
return encoded_signature.hex()
|
||||||
|
|
||||||
|
|
||||||
|
def project_abis(project_dir: str) -> Dict[str, List[Dict[str, Any]]]:
|
||||||
|
"""
|
||||||
|
Load all ABIs for project contracts and return then in a dictionary keyed by contract name.
|
||||||
|
|
||||||
|
Inputs:
|
||||||
|
- project_dir
|
||||||
|
Path to brownie project
|
||||||
|
"""
|
||||||
|
build_dir = os.path.join(project_dir, "build", "contracts")
|
||||||
|
build_files = glob.glob(os.path.join(build_dir, "*.json"))
|
||||||
|
|
||||||
|
abis: Dict[str, List[Dict[str, Any]]] = {}
|
||||||
|
|
||||||
|
for filepath in build_files:
|
||||||
|
contract_name, _ = os.path.splitext(os.path.basename(filepath))
|
||||||
|
with open(filepath, "r") as ifp:
|
||||||
|
contract_artifact = json.load(ifp)
|
||||||
|
|
||||||
|
contract_abi = contract_artifact.get("abi", [])
|
||||||
|
|
||||||
|
abis[contract_name] = contract_abi
|
||||||
|
|
||||||
|
return abis
|
18
dao/cli.py
18
dao/cli.py
|
@ -1,5 +1,21 @@
|
||||||
|
import argparse
|
||||||
|
|
||||||
|
from . import diamond
|
||||||
|
|
||||||
|
|
||||||
def main():
|
def main():
|
||||||
print("Hello")
|
parser = argparse.ArgumentParser(
|
||||||
|
description="dao: The command line interface to Moonstream DAO"
|
||||||
|
)
|
||||||
|
parser.set_defaults(func=lambda _: parser.print_help())
|
||||||
|
dao_subparsers = parser.add_subparsers()
|
||||||
|
|
||||||
|
diamond_parser = diamond.generate_cli()
|
||||||
|
dao_subparsers.add_parser("diamond", parents=[diamond_parser], add_help=False)
|
||||||
|
|
||||||
|
args = parser.parse_args()
|
||||||
|
args.func(args)
|
||||||
|
|
||||||
|
|
||||||
if __name__ == "__main__":
|
if __name__ == "__main__":
|
||||||
main()
|
main()
|
||||||
|
|
|
@ -0,0 +1,159 @@
|
||||||
|
"""
|
||||||
|
Generic diamond functionality for Moonstream contracts.
|
||||||
|
"""
|
||||||
|
|
||||||
|
import argparse
|
||||||
|
import os
|
||||||
|
from typing import Any, Dict, List, Set
|
||||||
|
|
||||||
|
from brownie import network
|
||||||
|
|
||||||
|
from . import (
|
||||||
|
abi,
|
||||||
|
Diamond,
|
||||||
|
DiamondCutFacet,
|
||||||
|
DiamondLoupeFacet,
|
||||||
|
OwnershipFacet,
|
||||||
|
)
|
||||||
|
|
||||||
|
FACETS: Dict[str, Any] = {
|
||||||
|
"DiamondCutFacet": DiamondCutFacet,
|
||||||
|
"DiamondLoupeFacet": DiamondLoupeFacet,
|
||||||
|
"OwnershipFacet": OwnershipFacet,
|
||||||
|
}
|
||||||
|
|
||||||
|
FACET_PRECEDENCE: List[str] = [
|
||||||
|
"DiamondCutFacet",
|
||||||
|
"OwnershipFacet",
|
||||||
|
"DiamondLoupeFacet",
|
||||||
|
]
|
||||||
|
|
||||||
|
FACET_ACTIONS: Dict[str, int] = {"add": 0, "replace": 1, "remove": 2}
|
||||||
|
|
||||||
|
ZERO_ADDRESS = "0x0000000000000000000000000000000000000000"
|
||||||
|
|
||||||
|
|
||||||
|
def facet_cut(
|
||||||
|
diamond_address: str,
|
||||||
|
facet_name: str,
|
||||||
|
facet_address: str,
|
||||||
|
action: str,
|
||||||
|
transaction_config: Dict[str, Any],
|
||||||
|
) -> Any:
|
||||||
|
"""
|
||||||
|
Cuts the given facet onto the given Diamond contract.
|
||||||
|
|
||||||
|
Resolves selectors in the precedence order defined by FACET_PRECEDENCE (highest precedence first).
|
||||||
|
"""
|
||||||
|
assert (
|
||||||
|
facet_name in FACETS
|
||||||
|
), f"Invalid facet: {facet_name}. Choices: {','.join(FACETS)}."
|
||||||
|
|
||||||
|
assert (
|
||||||
|
action in FACET_ACTIONS
|
||||||
|
), f"Invalid cut action: {action}. Choices: {','.join(FACET_ACTIONS)}."
|
||||||
|
|
||||||
|
project_dir = os.path.abspath(os.path.dirname(os.path.dirname(__file__)))
|
||||||
|
abis = abi.project_abis(project_dir)
|
||||||
|
|
||||||
|
reserved_selectors: Set[str] = set()
|
||||||
|
for facet in FACET_PRECEDENCE:
|
||||||
|
if facet == facet_name:
|
||||||
|
break
|
||||||
|
|
||||||
|
facet_abi = abis.get(facet, [])
|
||||||
|
for item in facet_abi:
|
||||||
|
if item["type"] == "function":
|
||||||
|
reserved_selectors.add(abi.encode_function_signature(item))
|
||||||
|
|
||||||
|
facet_function_selectors: List[str] = []
|
||||||
|
facet_abi = abis.get(facet_name, [])
|
||||||
|
for item in facet_abi:
|
||||||
|
if item["type"] == "function":
|
||||||
|
function_selector = abi.encode_function_signature(item)
|
||||||
|
if function_selector not in reserved_selectors:
|
||||||
|
facet_function_selectors.append(function_selector)
|
||||||
|
|
||||||
|
target_address = facet_address
|
||||||
|
if FACET_ACTIONS[action] == 2:
|
||||||
|
target_address = ZERO_ADDRESS
|
||||||
|
|
||||||
|
diamond_cut_action = [
|
||||||
|
target_address,
|
||||||
|
FACET_ACTIONS[action],
|
||||||
|
facet_function_selectors,
|
||||||
|
]
|
||||||
|
|
||||||
|
diamond = DiamondCutFacet.DiamondCutFacet(diamond_address)
|
||||||
|
transaction = diamond.diamond_cut(
|
||||||
|
[diamond_cut_action], ZERO_ADDRESS, b"", transaction_config
|
||||||
|
)
|
||||||
|
return transaction
|
||||||
|
|
||||||
|
|
||||||
|
def handle_facet_cut(args: argparse.Namespace) -> None:
|
||||||
|
network.connect(args.network)
|
||||||
|
diamond_address = args.address
|
||||||
|
action = args.action
|
||||||
|
facet_name = args.facet_name
|
||||||
|
facet_address = args.facet_address
|
||||||
|
transaction_config = Diamond.get_transaction_config(args)
|
||||||
|
facet_cut(diamond_address, facet_name, facet_address, action, transaction_config)
|
||||||
|
|
||||||
|
|
||||||
|
def generate_cli() -> argparse.ArgumentParser:
|
||||||
|
parser = argparse.ArgumentParser(
|
||||||
|
description="CLI to manage Moonstream DAO diamond contracts",
|
||||||
|
)
|
||||||
|
parser.set_defaults(func=lambda _: parser.print_help())
|
||||||
|
subcommands = parser.add_subparsers()
|
||||||
|
|
||||||
|
Diamond_parser = Diamond.generate_cli()
|
||||||
|
subcommands.add_parser("diamond", parents=[Diamond_parser], add_help=False)
|
||||||
|
|
||||||
|
facet_cut_parser = subcommands.add_parser("facet-cut")
|
||||||
|
Diamond.add_default_arguments(facet_cut_parser, transact=True)
|
||||||
|
facet_cut_parser.add_argument(
|
||||||
|
"--facet-name",
|
||||||
|
required=True,
|
||||||
|
choices=FACETS,
|
||||||
|
help="Name of facet to cut into or out of diamond",
|
||||||
|
)
|
||||||
|
facet_cut_parser.add_argument(
|
||||||
|
"--facet-address",
|
||||||
|
required=False,
|
||||||
|
default=ZERO_ADDRESS,
|
||||||
|
help=f"Address of deployed facet (default: {ZERO_ADDRESS})",
|
||||||
|
)
|
||||||
|
facet_cut_parser.add_argument(
|
||||||
|
"--action",
|
||||||
|
required=True,
|
||||||
|
choices=FACET_ACTIONS,
|
||||||
|
help="Diamond cut action to take on entire facet",
|
||||||
|
)
|
||||||
|
facet_cut_parser.set_defaults(func=handle_facet_cut)
|
||||||
|
|
||||||
|
DiamondCutFacet_parser = DiamondCutFacet.generate_cli()
|
||||||
|
subcommands.add_parser(
|
||||||
|
"diamond-cut", parents=[DiamondCutFacet_parser], add_help=False
|
||||||
|
)
|
||||||
|
|
||||||
|
DiamondLoupeFacet_parser = DiamondLoupeFacet.generate_cli()
|
||||||
|
subcommands.add_parser(
|
||||||
|
"diamond-loupe", parents=[DiamondLoupeFacet_parser], add_help=False
|
||||||
|
)
|
||||||
|
|
||||||
|
OwnershipFacet_parser = OwnershipFacet.generate_cli()
|
||||||
|
subcommands.add_parser("ownership", parents=[OwnershipFacet_parser], add_help=False)
|
||||||
|
|
||||||
|
return parser
|
||||||
|
|
||||||
|
|
||||||
|
def main() -> None:
|
||||||
|
parser = generate_cli()
|
||||||
|
args = parser.parse_args()
|
||||||
|
args.func(args)
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
main()
|
Ładowanie…
Reference in New Issue