Skip to content

Commit

Permalink
feat: add support for parsing ABIs (#49)
Browse files Browse the repository at this point in the history
* refactor: locate manifest tests in locally, clean up manifests

* refactor: add keep and skip functionality to serializable types

* feat: add support for parsing ABIs

* refactor: remove unncessary `to_dict()` (which doesn't work anyways)
  • Loading branch information
fubuloubu authored May 26, 2021
1 parent f4525c9 commit 6bdf9b7
Show file tree
Hide file tree
Showing 12 changed files with 144 additions and 48 deletions.
45 changes: 33 additions & 12 deletions src/ape/types/abstract.py
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
import json
from copy import deepcopy
from pathlib import Path
from typing import Dict
from typing import Any, Dict, Optional, Set, Union

import dataclassy as dc

Expand All @@ -22,28 +22,49 @@ def update_dict_params(params, param_name, param_type):
params[param_name][key] = param_type.from_dict(params[param_name][key])


def remove_none_fields(data):
def remove_empty_fields(data, keep_fields: Optional[Set[str]] = None):
if isinstance(data, dict):
return {
k: remove_none_fields(v)
for k, v in data.items()
if v is not None and remove_none_fields(v) is not None
k: v
for k, v in zip(data.keys(), map(remove_empty_fields, data.values()))
if isinstance(v, bool) or (keep_fields and k in keep_fields) or v
}

elif isinstance(data, list):
return [
remove_none_fields(v)
for v in data
if v is not None and remove_none_fields(v) is not None
]
return [v for v in map(remove_empty_fields, data) if isinstance(v, bool) or v]

return data


@dc.dataclass(slots=True, kwargs=True)
def to_dict(v: Any) -> Optional[Union[list, dict, str, int, bool]]:
if isinstance(v, SerializableType):
return v.to_dict()

elif isinstance(v, list):
return [to_dict(i) for i in v] # type: ignore

elif isinstance(v, dict):
return {k: to_dict(i) for k, i in v.items()}

elif isinstance(v, (str, int, bool)) or v is None:
return v

else:
raise # Unhandled type


@dc.dataclass(slots=True, kwargs=True, repr=True)
class SerializableType:
_keep_fields_: Set[str] = set()
_skip_fields_: Set[str] = set()

def to_dict(self) -> Dict:
return remove_none_fields({k: v for k, v in dc.asdict(self).items() if v})
data = {
k: to_dict(v)
for k, v in dc.values(self).items()
if not (k.startswith("_") or k in self._skip_fields_)
}
return remove_empty_fields(data, keep_fields=self._keep_fields_)

@classmethod
def from_dict(cls, params: Dict):
Expand Down
109 changes: 99 additions & 10 deletions src/ape/types/contract.py
Original file line number Diff line number Diff line change
@@ -1,14 +1,14 @@
import urllib.request
from copy import deepcopy
from typing import Dict, List, Optional
from typing import Dict, List, Optional, Set, Union

from ape.utils import compute_checksum

from .abstract import SerializableType, update_list_params, update_params
from .abstract import FileMixin, SerializableType, update_list_params, update_params


# TODO link references & link values are for solidity, not used with Vyper
# Offsets are for dynamic links, e.g. doggie's proxy forwarder
# Offsets are for dynamic links, e.g. EIP1167 proxy forwarder
class LinkDependency(SerializableType):
offsets: List[int]
type: str
Expand All @@ -26,6 +26,17 @@ class Bytecode(SerializableType):
linkReferences: Optional[List[LinkReference]] = None
linkDependencies: Optional[List[LinkDependency]] = None

def __repr__(self) -> str:
self_str = super().__repr__()

# Truncate bytecode for display
if self.bytecode:
self_str = self_str.replace(
self.bytecode, self.bytecode[:5] + "..." + self.bytecode[-3:]
)

return self_str

@classmethod
def from_dict(cls, params: Dict):
params = deepcopy(params)
Expand Down Expand Up @@ -55,27 +66,105 @@ class Compiler(SerializableType):
contractTypes: Optional[List[str]] = None


class ContractType(SerializableType):
class ABIType(SerializableType):
name: str = "" # NOTE: Tuples don't have names by default
indexed: Optional[bool] = None
type: Union[str, "ABIType"]
internalType: Optional[str] = None


class ABI(SerializableType):
name: str = ""
inputs: List[ABIType] = []
outputs: List[ABIType] = []
# ABI v2 Field
# NOTE: Only functions have this field
stateMutability: Optional[str] = None
# NOTE: Only events have this field
anonymous: Optional[bool] = None
# TODO: Handle events and functions separately (maybe also default and constructor)
# Would parse based on value of type here, so some indirection required
# Might make most sense to add to `ContractType` as a serde extension
type: str

@property
def is_event(self) -> bool:
return self.anonymous is not None

@property
def is_stateful(self) -> bool:
return self.stateMutability not in ("view", "pure")

@classmethod
def from_dict(cls, params: Dict):
params = deepcopy(params)

# Handle ABI v1 fields (convert to ABI v2)
if "anonymous" not in params and "stateMutability" not in params:
if params.get("constant", False):
params["stateMutability"] = "view"

elif params.get("payable", False):
params["stateMutability"] = "payable"

else:
params["stateMutability"] = "nonpayable"

if "constant" in params:
params.pop("constant")

elif "payable" in params:
params.pop("payable")

update_list_params(params, "inputs", ABIType)
update_list_params(params, "outputs", ABIType)
return cls(**params) # type: ignore


class ContractType(FileMixin, SerializableType):
_keep_fields_: Set[str] = {"abi"}
_skip_fields_: Set[str] = {"contractName"}
contractName: str
sourceId: Optional[str] = None
deploymentBytecode: Optional[Bytecode] = None
runtimeBytecode: Optional[Bytecode] = None
# abi, userdoc and devdoc must conform to spec
abi: Optional[str] = None
abi: List[ABI] = []
userdoc: Optional[str] = None
devdoc: Optional[str] = None

def to_dict(self):
data = super().to_dict()
@property
def constructor(self) -> Optional[ABI]:
for abi in self.abi:
if abi.type == "constructor":
return abi

return None

@property
def fallback(self) -> Optional[ABI]:
for abi in self.abi:
if abi.type == "fallback":
return abi

return None

@property
def events(self) -> List[ABI]:
return [abi for abi in self.abi if abi.type == "event"]

if "abi" in data:
data["abi"] = self.abi # NOTE: Don't prune this one of empty lists
@property
def calls(self) -> List[ABI]:
return [abi for abi in self.abi if abi.type == "function" and abi.is_stateful]

return data
@property
def transactions(self) -> List[ABI]:
return [abi for abi in self.abi if abi.type == "function" and not abi.is_stateful]

@classmethod
def from_dict(cls, params: Dict):
params = deepcopy(params)
update_list_params(params, "abi", ABI)
update_params(params, "deploymentBytecode", Bytecode)
update_params(params, "runtimeBytecode", Bytecode)
return cls(**params) # type: ignore
Expand Down
19 changes: 1 addition & 18 deletions src/ape/types/manifest.py
Original file line number Diff line number Diff line change
Expand Up @@ -54,24 +54,6 @@ def __getattr__(self, attr_name: str):
else:
raise AttributeError(f"{self.__class__.__name__} has no attribute '{attr_name}'")

def to_dict(self):
data = super().to_dict()

if "contractTypes" in data and data["contractTypes"]:
for name in data["contractTypes"]:
# NOTE: This was inserted by us, remove it
del data["contractTypes"][name]["contractName"]
# convert Path to str, or else we can't serialize this as JSON
data["contractTypes"][name]["sourceId"] = str(
data["contractTypes"][name]["sourceId"]
)
if "sourcePath" in data["contractTypes"][name]:
data["contractTypes"][name]["sourcePath"] = str(
data["contractTypes"][name]["sourcePath"]
)

return data

@classmethod
def from_dict(cls, params: Dict):
params = deepcopy(params)
Expand All @@ -83,6 +65,7 @@ def from_dict(cls, params: Dict):
for name in params["contractTypes"]:
params["contractTypes"][name] = ContractType.from_dict( # type: ignore
{
# NOTE: We inject this parameter ourselves, remove it when serializing
"contractName": name,
**params["contractTypes"][name],
}
Expand Down
11 changes: 3 additions & 8 deletions tests/conftest.py
Original file line number Diff line number Diff line change
@@ -1,8 +1,8 @@
import json
from pathlib import Path
from tempfile import mkdtemp

import pytest # type: ignore
import requests
from click.testing import CliRunner

from ape import Project, config
Expand Down Expand Up @@ -42,7 +42,7 @@ def project(project_folder):
@pytest.fixture(
scope="session",
params=[
# From https://github.com/ethpm/ethpm-spec/tree/master/examples
# Copied from https://github.com/ethpm/ethpm-spec/tree/master/examples
"escrow",
"owned",
"piper-coin",
Expand All @@ -54,9 +54,4 @@ def project(project_folder):
],
)
def manifest(request):
# NOTE: `v3-pretty.json` exists for each, and can be used for debugging
manifest_uri = (
"https://raw.githubusercontent.com/ethpm/ethpm-spec/master/examples/"
f"{request.param}/v3.json"
)
yield requests.get(manifest_uri).json()
yield json.loads((Path(__file__).parent / "manifests" / f"{request.param}.json").read_text())
1 change: 1 addition & 0 deletions tests/manifests/escrow.json
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
{"compilers": [{"contractTypes": ["Escrow", "SafeSendLib"], "name": "solc", "settings": {"optimize": false}, "version": "0.6.8+commit.0bbfe453"}], "contractTypes": {"Escrow": {"abi": [{"inputs": [{"internalType": "address", "name": "_recipient", "type": "address"}], "stateMutability": "nonpayable", "type": "constructor"}, { "name": "recipient", "outputs": [{"internalType": "address", "type": "address"}], "stateMutability": "view", "type": "function"}, { "name": "releaseFunds", "stateMutability": "nonpayable", "type": "function"}, { "name": "sender", "outputs": [{"internalType": "address", "type": "address"}], "stateMutability": "view", "type": "function"}], "deploymentBytecode": {"bytecode": "0x608060405234801561001057600080fd5b506040516104e83803806104e88339818101604052602081101561003357600080fd5b8101908080519060200190929190505050336000806101000a81548173ffffffffffffffffffffffffffffffffffffffff021916908373ffffffffffffffffffffffffffffffffffffffff16021790555080600160006101000a81548173ffffffffffffffffffffffffffffffffffffffff021916908373ffffffffffffffffffffffffffffffffffffffff16021790555050610413806100d56000396000f3fe608060405234801561001057600080fd5b50600436106100415760003560e01c806366d003ac1461004657806367e404ce1461009057806369d89575146100da575b600080fd5b61004e6100e4565b604051808273ffffffffffffffffffffffffffffffffffffffff1673ffffffffffffffffffffffffffffffffffffffff16815260200191505060405180910390f35b61009861010a565b604051808273ffffffffffffffffffffffffffffffffffffffff1673ffffffffffffffffffffffffffffffffffffffff16815260200191505060405180910390f35b6100e261012f565b005b600160009054906101000a900473ffffffffffffffffffffffffffffffffffffffff1681565b6000809054906101000a900473ffffffffffffffffffffffffffffffffffffffff1681565b6000809054906101000a900473ffffffffffffffffffffffffffffffffffffffff1673ffffffffffffffffffffffffffffffffffffffff163373ffffffffffffffffffffffffffffffffffffffff16141561028257600160009054906101000a900473ffffffffffffffffffffffffffffffffffffffff1673ffffffffffffffffffffffffffffffffffffffff16730000000000000000000000000000000000000000639341231c9091476040518363ffffffff1660e01b8152600401808373ffffffffffffffffffffffffffffffffffffffff1673ffffffffffffffffffffffffffffffffffffffff1681526020018281526020019250505060206040518083038186803b15801561024157600080fd5b505af4158015610255573d6000803e3d6000fd5b505050506040513d602081101561026b57600080fd5b8101908080519060200190929190505050506103db565b600160009054906101000a900473ffffffffffffffffffffffffffffffffffffffff1673ffffffffffffffffffffffffffffffffffffffff163373ffffffffffffffffffffffffffffffffffffffff1614156103d5576000809054906101000a900473ffffffffffffffffffffffffffffffffffffffff1673ffffffffffffffffffffffffffffffffffffffff16730000000000000000000000000000000000000000639341231c9091476040518363ffffffff1660e01b8152600401808373ffffffffffffffffffffffffffffffffffffffff1673ffffffffffffffffffffffffffffffffffffffff1681526020018281526020019250505060206040518083038186803b15801561039457600080fd5b505af41580156103a8573d6000803e3d6000fd5b505050506040513d60208110156103be57600080fd5b8101908080519060200190929190505050506103da565b600080fd5b5b56fea2646970667358221220c0256c8fdbb9d70e72b54b5dcae800eb0d0723c366103209d2e40e60b6f352e564736f6c63430006080033", "linkReferences": [{"length": 20, "name": "SafeSendLib", "offsets": [660, 999]}]}, "devdoc": {"author": "Piper Merriam <[email protected]>", "methods": {"releaseFunds()": {"details": "Releases the escrowed funds to the other party."}}, "title": "Contract for holding funds in escrow between two semi trusted parties."}, "runtimeBytecode": {"bytecode": "0x608060405234801561001057600080fd5b50600436106100415760003560e01c806366d003ac1461004657806367e404ce1461009057806369d89575146100da575b600080fd5b61004e6100e4565b604051808273ffffffffffffffffffffffffffffffffffffffff1673ffffffffffffffffffffffffffffffffffffffff16815260200191505060405180910390f35b61009861010a565b604051808273ffffffffffffffffffffffffffffffffffffffff1673ffffffffffffffffffffffffffffffffffffffff16815260200191505060405180910390f35b6100e261012f565b005b600160009054906101000a900473ffffffffffffffffffffffffffffffffffffffff1681565b6000809054906101000a900473ffffffffffffffffffffffffffffffffffffffff1681565b6000809054906101000a900473ffffffffffffffffffffffffffffffffffffffff1673ffffffffffffffffffffffffffffffffffffffff163373ffffffffffffffffffffffffffffffffffffffff16141561028257600160009054906101000a900473ffffffffffffffffffffffffffffffffffffffff1673ffffffffffffffffffffffffffffffffffffffff16730000000000000000000000000000000000000000639341231c9091476040518363ffffffff1660e01b8152600401808373ffffffffffffffffffffffffffffffffffffffff1673ffffffffffffffffffffffffffffffffffffffff1681526020018281526020019250505060206040518083038186803b15801561024157600080fd5b505af4158015610255573d6000803e3d6000fd5b505050506040513d602081101561026b57600080fd5b8101908080519060200190929190505050506103db565b600160009054906101000a900473ffffffffffffffffffffffffffffffffffffffff1673ffffffffffffffffffffffffffffffffffffffff163373ffffffffffffffffffffffffffffffffffffffff1614156103d5576000809054906101000a900473ffffffffffffffffffffffffffffffffffffffff1673ffffffffffffffffffffffffffffffffffffffff16730000000000000000000000000000000000000000639341231c9091476040518363ffffffff1660e01b8152600401808373ffffffffffffffffffffffffffffffffffffffff1673ffffffffffffffffffffffffffffffffffffffff1681526020018281526020019250505060206040518083038186803b15801561039457600080fd5b505af41580156103a8573d6000803e3d6000fd5b505050506040513d60208110156103be57600080fd5b8101908080519060200190929190505050506103da565b600080fd5b5b56fea2646970667358221220c0256c8fdbb9d70e72b54b5dcae800eb0d0723c366103209d2e40e60b6f352e564736f6c63430006080033", "linkReferences": [{"length": 20, "name": "SafeSendLib", "offsets": [447, 786]}]}, "sourceId": "Escrow.sol"}, "SafeSendLib": {"deploymentBytecode": {"bytecode": "0x610132610026600b82828239805160001a60731461001957fe5b30600052607381538281f3fe730000000000000000000000000000000000000000301460806040526004361060335760003560e01c80639341231c146038575b600080fd5b818015604357600080fd5b50608d60048036036040811015605857600080fd5b81019080803573ffffffffffffffffffffffffffffffffffffffff1690602001909291908035906020019092919050505060a7565b604051808215151515815260200191505060405180910390f35b60004782111560b557600080fd5b8273ffffffffffffffffffffffffffffffffffffffff166108fc839081150290604051600060405180830381858888f1935050505060f257600080fd5b600190509291505056fea26469706673582212203471502f8b953b5622ae380d01fe3a6c574e1b7ae3ba80ebae95d88771f6714564736f6c63430006080033"}, "devdoc": {"author": "Piper Merriam <[email protected]>", "methods": {"sendOrThrow(address,uint256)": {"details": "Attempts to send the specified amount to the recipient throwing an error if it fails", "params": {"recipient": "The address that the funds should be to.", "value": "The amount in wei that should be sent."}}}, "title": "Library for safe sending of ether."}, "runtimeBytecode": {"bytecode": "0x730000000000000000000000000000000000000000301460806040526004361060335760003560e01c80639341231c146038575b600080fd5b818015604357600080fd5b50608d60048036036040811015605857600080fd5b81019080803573ffffffffffffffffffffffffffffffffffffffff1690602001909291908035906020019092919050505060a7565b604051808215151515815260200191505060405180910390f35b60004782111560b557600080fd5b8273ffffffffffffffffffffffffffffffffffffffff166108fc839081150290604051600060405180830381858888f1935050505060f257600080fd5b600190509291505056fea26469706673582212203471502f8b953b5622ae380d01fe3a6c574e1b7ae3ba80ebae95d88771f6714564736f6c63430006080033"}, "sourceId": "SafeSendLib.sol"}}, "deployments": {"blockchain://d4e56740f876aef8c010b86a40d5f56745a118d0906a34e69aec8c0db1cb8fa3/block/752820c0ad7abc1200f9ad42c4adc6fbb4bd44b5bed4667990e64565102c1ba6": {"Escrow": {"address": "0x41B8E7F94F92aE75266054f7029b2f5C30D19171", "block": "0xe29b6d17dc4da99bbd985dd62c69cf70f3437c2c104eee24fa7a41d73e4a6524", "contractType": "Escrow", "runtimeBytecode": {"linkDependencies": [{"offsets": [447, 786], "type": "reference", "value": "SafeSendLib"}]}, "transaction": "0x6f1bdf9e303c866dc74452cb418e05737c9b8c3a5ddfcd1c09509d5ec1fc23e9"}, "SafeSendLib": {"address": "0x379EdD01a8c6E56649C092D2699eA877CC89414B", "block": "0xb05e03a8ba4b744d5754f93fcb9c48e836fa624d9c2942c7081661a0e9e3134b", "contractType": "SafeSendLib", "transaction": "0x32a498696bd9924f34ac01f755c126d5628947f9935188098dc8eb6f7a30c418"}}}, "manifest": "ethpm/3", "name": "escrow", "sources": {"./Escrow.sol": {"installPath": "./Escrow.sol", "type": "solidity", "urls": ["ipfs://QmNLpdCi4UakwJ9rBoL7rDnEzNeA6f8uvKbiMhZVqTucu1"]}, "./SafeSendLib.sol": {"installPath": "./SafeSendLib.sol", "type": "solidity", "urls": ["ipfs://QmbEnqvCSAAYwQ474S1vCSBdMgdiRZ4gZWEmSmdXepXQJq"]}}, "version": "1.0.0"}
1 change: 1 addition & 0 deletions tests/manifests/owned.json
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
{"manifest": "ethpm/3", "meta": {"authors": ["Piper Merriam <[email protected]>"], "description": "Reusable contracts which implement a privileged 'owner' model for authorization.", "keywords": ["authorization"], "license": "MIT", "links": {"documentation": "ipfs://QmUYcVzTfSwJoigggMxeo2g5STWAgJdisQsqcXHws7b1FW"}}, "name": "owned", "sources": {"Owned.sol": {"installPath": "./Owned.sol", "type": "solidity", "urls": ["ipfs://QmU8QUSt56ZoBDJgjjXvAZEPro9LmK1m2gjVG5Q4s9x29W"]}}, "version": "1.0.0"}
Loading

0 comments on commit 6bdf9b7

Please sign in to comment.