Skip to content

Commit

Permalink
✨ Contract controller
Browse files Browse the repository at this point in the history
  • Loading branch information
Guido W. Pettinari committed Dec 16, 2022
1 parent 65a4712 commit 9b01327
Show file tree
Hide file tree
Showing 12 changed files with 167 additions and 8 deletions.
1 change: 1 addition & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -244,6 +244,7 @@ Thank you very much to the [web3.py](https://github.com/ethereum/web3.py) and [`


# TODO
- Contract: Controller tests
- Fix setting boolean variables via env, e.g. `WEB3CLI_POPULATE_DB=0 w3 db chain list` or `WEB3CLI_POPULATE_DB=false w3 db chain list` should work as intended
- Define command shortcuts using argparse aliases, e.g. `w3 add-chain` instead of `w3 db chain add`
- Use different db file for dev environment
Expand Down
2 changes: 1 addition & 1 deletion pyproject.toml
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
[project]
name = "web3cli"
version = "0.10.0"
version = "0.11.0"
description = "Interact with blockchains and smart contracts using the command line"
authors = [
{name = "coccoinomane", email = "[email protected]"},
Expand Down
101 changes: 101 additions & 0 deletions src/web3cli/controllers/db/contract_controller.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,101 @@
from cement import ex
from web3cli.controllers.controller import Controller
from web3cli.core.helpers.os import read_json
from web3cli.core.models.contract import Contract
from web3cli.core.exceptions import Web3CliError
from web3cli.helpers.render import render_table
from playhouse.shortcuts import model_to_dict


class ContractController(Controller):
"""Handler of the `w3 db contract` commands"""

class Meta:
label = "contract"
help = "add, list or delete contracts"
stacked_type = "nested"
stacked_on = "db"

@ex(help="list contracts")
def list(self) -> None:
render_table(
self.app,
data=[
[c.name, c.chain, c.address] for c in Contract.get_all(Contract.name)
],
headers=["NAME", "CHAIN", "ADDRESS"],
wrap=42,
)

@ex(
help="show details of the given contract",
arguments=[
(["name"], {"help": "name of the contract"}),
],
)
def get(self) -> None:
contract = Contract.get_by_name_or_raise(self.app.pargs.name)
self.app.render(model_to_dict(contract), indent=4, handler="json")

@ex(
help="add a new contract to the database",
arguments=[
(["name"], {"help": "name of the contract, for reference"}),
(
["address"],
{"help": "address of the contract on the blockchain (0x...)"},
),
(["-d", "--desc"], {"action": "store"}),
(
["-u", "--update"],
{
"help": "if a contract with the same name is present, overwrite it",
"action": "store_true",
},
),
(
["--abi"],
{
"help": "json file containing the contract's ABI",
},
),
],
)
def add(self) -> None:

# Parse ABI file
abi = None
if self.app.pargs.abi:
try:
abi = read_json(self.app.pargs.abi)
except:
raise Web3CliError(f"Could not read ABI from file {self.app.pargs.abi}")

# Add or update contract
contract = Contract.get_by_name(self.app.pargs.name)
if not contract or self.app.pargs.update:
Contract.upsert(
{
"name": self.app.pargs.name,
"desc": self.app.pargs.desc,
"address": self.app.pargs.address,
"chain": self.app.chain_name,
"abi": abi,
},
logger=self.app.log.info,
)
else:
raise Web3CliError(
f"Contract '{self.app.pargs.name}' already exists. Use `--update` or `-u` to update it."
)

@ex(
help="delete a contract",
arguments=[
(["name"], {"help": "hash of the contract to delete"}),
],
)
def delete(self) -> None:
contract = Contract.get_by_name_or_raise(self.app.pargs.name)
contract.delete_instance()
self.app.log.info(f"Contract '{self.app.pargs.name}' deleted correctly")
3 changes: 2 additions & 1 deletion src/web3cli/core/helpers/database.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,12 +3,13 @@
from web3cli.core.models.base_model import BaseModel, db
from web3cli.core.models.address import Address
from web3cli.core.models.chain import Chain, ChainRpc, Rpc
from web3cli.core.models.contract import Contract
from web3cli.core.models.signer import Signer
from web3cli.core.models.tx import Tx
from web3cli.core.helpers.os import create_folder
import os

tables: List[Type[BaseModel]] = [Signer, Address, Chain, Rpc, ChainRpc, Tx]
tables: List[Type[BaseModel]] = [Signer, Address, Chain, Rpc, ChainRpc, Tx, Contract]


def init_db(db_path: str) -> SqliteExtDatabase:
Expand Down
8 changes: 8 additions & 0 deletions src/web3cli/core/helpers/os.py
Original file line number Diff line number Diff line change
@@ -1,4 +1,6 @@
import json
import os
from typing import Any


def create_folder(dir: str, mode: int = 0o777) -> None:
Expand All @@ -8,3 +10,9 @@ def create_folder(dir: str, mode: int = 0o777) -> None:
if not os.path.isdir(expanded_dir):
os.makedirs(expanded_dir, mode)
os.chmod(expanded_dir, mode) # for good measure


def read_json(file_path: str) -> Any:
"""Parse a JSON file in a python object"""
with open(file_path, encoding="utf-8") as file:
return json.load(file)
1 change: 0 additions & 1 deletion src/web3cli/core/models/address.py
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,6 @@
)
import web3
from web3cli.core.models.types import AddressFields
from playhouse.shortcuts import update_model_from_dict
from playhouse.signals import pre_save
from web3cli.core.types import Logger

Expand Down
3 changes: 1 addition & 2 deletions src/web3cli/core/models/chain.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,6 @@
from typing import List
from peewee import TextField, IntegerField, ForeignKeyField
from web3cli.core.exceptions import (
ChainNotFound,
ChainNotResolved,
RpcIsInvalid,
RpcNotFound,
Expand All @@ -14,7 +13,7 @@
from web3.middleware import geth_poa_middleware
from web3cli.core.models.types import ChainFields
from web3cli.core.types import Logger
from playhouse.shortcuts import update_model_from_dict, dict_to_model
from playhouse.shortcuts import dict_to_model
from web3cli.core.seeds import chain_seeds


Expand Down
38 changes: 38 additions & 0 deletions src/web3cli/core/models/contract.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,38 @@
from __future__ import annotations
from typing import Type
from peewee import TextField
from playhouse.signals import pre_save
from web3cli.core.exceptions import ContractIsInvalid, ContractNotFound
from web3cli.core.models.address import Address
from web3cli.core.models.base_model import BaseModel
from playhouse.sqlite_ext import JSONField
from web3._utils.validation import validate_abi
from web3cli.core.models.types import ContractFields
from web3cli.core.types import Logger


class Contract(BaseModel):
class Meta:
table_name = "contracts"

name = TextField(unique=True)
desc = TextField(null=True)
address = TextField()
chain = TextField()
abi = JSONField(null=True)

@classmethod
def upsert(cls, fields: ContractFields, logger: Logger = None) -> Contract:
"""Create contract or update it if one with the same name already exists"""
return cls.upsert_by_field(cls.name, fields["name"], fields, logger, True)


@pre_save(sender=Contract)
def validate(model_class: Contract, instance: Type[Contract], created: bool) -> None:
"""Validate the contract which is about to be saved"""
if not Address.is_valid_address(instance.address):
raise ContractIsInvalid(
f"Invalid address given for contract: {instance.address}"
)
if instance.abi:
validate_abi(instance.abi)
1 change: 0 additions & 1 deletion src/web3cli/core/models/tx.py
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,6 @@
from web3cli.core.models.address import Address
from web3cli.core.models.timestamps_model import TimestampsModel
from playhouse.signals import pre_save
from playhouse.shortcuts import update_model_from_dict
from web3cli.core.models.types import TxFields
from web3cli.core.types import Logger
import re
Expand Down
13 changes: 12 additions & 1 deletion src/web3cli/core/models/types.py
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
from typing import List, TypedDict
from typing import Any, Dict, List, TypedDict
from typing_extensions import NotRequired
from web3.types import ABI


class RpcFields(TypedDict):
Expand Down Expand Up @@ -51,3 +52,13 @@ class TxFields(TypedDict):
receipt: NotRequired[str]
created_at: NotRequired[str]
updated_at: NotRequired[str]


class ContractFields(TypedDict):
"""Typing for Contract model creation and update"""

name: str
desc: str
address: str
chain: str
abi: ABI
2 changes: 1 addition & 1 deletion src/web3cli/helpers/version.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@
from cement.utils.version import get_version_banner
from cement.utils.version import get_version as cement_get_version

VERSION = (0, 10, 0, "alpha", 0)
VERSION = (0, 11, 0, "alpha", 0)


def get_version_number(version: Tuple[int, int, int, str, int] = VERSION) -> str:
Expand Down
2 changes: 2 additions & 0 deletions src/web3cli/main.py
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@
from web3cli.controllers.db.address_controller import AddressController
from web3cli.controllers.db.signer_controller import SignerController
from web3cli.controllers.db.tx_controller import TxController
from web3cli.controllers.db.contract_controller import ContractController
from web3cli.core.exceptions import Web3CliError
from web3cli import hooks
import os
Expand Down Expand Up @@ -97,6 +98,7 @@ class Meta:
SignerController,
AddressController,
TxController,
ContractController,
]

# extend the app with cement hook system
Expand Down

0 comments on commit 9b01327

Please sign in to comment.