Skip to content

Commit

Permalink
Merge pull request #169 from ulope/refactor_contractmgr
Browse files Browse the repository at this point in the history
Simplify contract manager
  • Loading branch information
loredanacirstea committed Jul 6, 2018
2 parents 2c4b466 + 30dbfee commit c49924a
Show file tree
Hide file tree
Showing 6 changed files with 96 additions and 117 deletions.
3 changes: 2 additions & 1 deletion .travis.yml
Original file line number Diff line number Diff line change
Expand Up @@ -26,10 +26,11 @@ jobs:
- pip install -U pip wheel coveralls "coverage<4.4"
- pip install pytest-travis-fold
- pip install -r requirements-dev.txt
- pip install pytest-xdist pytest-sugar
- python setup.py compile_contracts

before_script:
- flake8 raiden_contracts/

script:
- coverage run --source raiden_contracts/ -m py.test -Wd --travis-fold=always $TEST_TYPE
- coverage run --source raiden_contracts/ -m py.test -Wd --travis-fold=always -n auto -v $TEST_TYPE
10 changes: 6 additions & 4 deletions raiden_contracts/cm_test/test_contract_manager.py
Original file line number Diff line number Diff line change
@@ -1,3 +1,5 @@
from pathlib import Path

import pytest

from raiden_contracts.contract_manager import (
Expand All @@ -9,8 +11,6 @@
EVENT_CHANNEL_CLOSED,
)

PRECOMPILED_CONTRACTS_PATH = 'raiden_contracts/data/contracts.json'


def contract_manager_meta(contracts_path):
manager = ContractManager(contracts_path)
Expand All @@ -30,6 +30,8 @@ def test_contract_manager_compile():
contract_manager_meta(CONTRACTS_SOURCE_DIRS)


def test_contract_manager_json():
def test_contract_manager_json(tmpdir):
precompiled_path = Path(str(tmpdir)).joinpath('contracts.json')
ContractManager(CONTRACTS_SOURCE_DIRS).store_compiled_contracts(precompiled_path)
# try to load contracts from a precompiled file
contract_manager_meta(PRECOMPILED_CONTRACTS_PATH)
contract_manager_meta(precompiled_path)
165 changes: 70 additions & 95 deletions raiden_contracts/contract_manager.py
Original file line number Diff line number Diff line change
@@ -1,131 +1,106 @@
import os
import json
import logging
from typing import Union, List, Dict
from pathlib import Path
from typing import Dict, Union

from solc import compile_files
from web3.utils.contracts import find_matching_event_abi

log = logging.getLogger(__name__)
CONTRACTS_DIR = os.path.join(os.path.dirname(__file__), 'data/contracts.json')
CONTRACTS_SOURCE_DIRS = {
'raiden': os.path.join(os.path.dirname(__file__), 'contracts/'),
'test': os.path.join(os.path.dirname(__file__), 'contracts/test'),
}
CONTRACTS_SOURCE_DIRS = {
k: os.path.normpath(v) for k, v in CONTRACTS_SOURCE_DIRS.items()
}

log = logging.getLogger(__name__)

def fix_contract_key_names(input: Dict) -> Dict:
result = {}
_BASE = Path(__file__).parent

for k, v in input.items():
name = k.split(':')[1]
result[name] = v

return result
CONTRACTS_PRECOMPILED_PATH = _BASE.joinpath('data', 'contracts.json')
CONTRACTS_SOURCE_DIRS = {
'raiden': _BASE.joinpath('contracts'),
'test': _BASE.joinpath('contracts/test'),
}


class ContractManager:
def __init__(self, path: Union[str, List[str]]) -> None:
def __init__(self, path: Union[Path, Dict[str, Path]]) -> None:
"""Params:
path: either path to a precompiled contract JSON file, or a list of
directories which contain solidity files to compile
"""
self.contracts_source_dirs = None
self.abi = dict()
self._contracts_source_dirs = None
self._contracts = dict()
if isinstance(path, dict):
self.contracts_source_dirs = path
for dir_path in path.values():
self.abi.update(
ContractManager.precompile_contracts(dir_path, self.get_mappings()),
)
elif os.path.isdir(path):
ContractManager.__init__(self, {'smart_contracts': path})
self._contracts_source_dirs = path
elif isinstance(path, Path):
if path.is_dir():
ContractManager.__init__(self, {'smart_contracts': path})
else:
self._contracts = json.loads(path.read_text())
else:
with open(path, 'r') as json_file:
self.abi = json.load(json_file)

def compile_contract(self, contract_name: str, libs=None, *args):
"""Compile contract and return JSON containing abi and bytecode"""
contract_json = compile_files(
[self.get_contract_path(contract_name)[0]],
output_values=('abi', 'bin', 'ast'),
import_remappings=self.get_mappings(),
optimize=False,
)
contract_json = {
os.path.basename(key).split('.', 1)[0]: value
for key, value in contract_json.items()
}
return contract_json.get(contract_name, None)

def get_contract_path(self, contract_name: str):
return sum(
(self.list_contract_path(contract_name, x)
for x in self.contracts_source_dirs.values()),
[],
)

@staticmethod
def list_contract_path(contract_name: str, directory: str):
"""Get contract source file for a specified contract"""
return [
os.path.join(directory, x)
for x in os.listdir(directory)
if os.path.basename(x).split('.', 1)[0] == contract_name
]

def get_mappings(self) -> List[str]:
"""Return dict of mappings to use as solc argument."""
return ['%s=%s' % (k, v) for k, v in self.contracts_source_dirs.items()]

@staticmethod
def precompile_contracts(contracts_dir: str, map_dirs: List) -> Dict:
raise TypeError('`path` must be either `Path` or `dict`')

def _compile_all_contracts(self) -> None:
"""
Compile solidity contracts into ABI. This requires solc somewhere in the $PATH
and also ethereum.tools python library.
Parameters:
contracts_dir: directory where the contracts are stored.
All files with .sol suffix will be compiled.
The method won't recurse into subdirectories.
Return:
map (contract_name => ABI)
Compile solidity contracts into ABI and BIN. This requires solc somewhere in the $PATH
and also the :ref:`ethereum.tools` python library.
"""
files = []
for contract in os.listdir(contracts_dir):
contract_path = os.path.join(contracts_dir, contract)
if not os.path.isfile(contract_path) or not contract_path.endswith('.sol'):
continue
files.append(contract_path)
if self._contracts_source_dirs is None:
raise TypeError("Can't compile contracts when using precompiled archive.")

import_dir_map = ['%s=%s' % (k, v) for k, v in self._contracts_source_dirs.items()]
try:
res = compile_files(
files,
output_values=('abi', 'bin', 'ast'),
import_remappings=map_dirs,
optimize=False,
)
return fix_contract_key_names(res)
except FileNotFoundError:
raise Exception('Could not compile the contract. Check that solc is available.')
for contracts_dir in self._contracts_source_dirs.values():
res = compile_files(
[str(file) for file in contracts_dir.glob('*.sol')],
output_values=('abi', 'bin', 'ast'),
import_remappings=import_dir_map,
optimize=False,
)
self._contracts.update(_fix_contract_key_names(res))
except FileNotFoundError as ex:
raise RuntimeError(
'Could not compile the contract. Check that solc is available.',
) from ex

def store_compiled_contracts(self, target_path: Path) -> None:
""" Store compiled contracts JSON at `target_path`. """
if self._contracts_source_dirs is None:
raise TypeError("Already using stored contracts.")

if not self._contracts:
self._compile_all_contracts()

target_path.parent.mkdir(parents=True, exist_ok=True)
target_path.write_text(json.dumps(self._contracts))

def get_contract(self, contract_name: str) -> Dict:
"""Return bin+abi of the contract"""
return self.abi[contract_name]
""" Return ABI, BIN of the given contract. """
if not self._contracts:
self._compile_all_contracts()
return self._contracts[contract_name]

def get_contract_abi(self, contract_name: str) -> Dict:
""" Returns the ABI for a given contract. """
return self.abi[contract_name]['abi']
if not self._contracts:
self._compile_all_contracts()
return self._contracts[contract_name]['abi']

def get_event_abi(self, contract_name: str, event_name: str) -> Dict:
""" Returns the ABI for a given event. """
if not self._contracts:
self._compile_all_contracts()
contract_abi = self.get_contract_abi(contract_name)
return find_matching_event_abi(contract_abi, event_name)


if os.path.isfile(CONTRACTS_DIR):
CONTRACT_MANAGER = ContractManager(CONTRACTS_DIR)
def _fix_contract_key_names(input: Dict) -> Dict:
result = {}

for k, v in input.items():
name = k.split(':')[1]
result[name] = v

return result


if CONTRACTS_PRECOMPILED_PATH.is_file():
CONTRACT_MANAGER = ContractManager(CONTRACTS_PRECOMPILED_PATH)
else:
CONTRACT_MANAGER = ContractManager(CONTRACTS_SOURCE_DIRS)
Empty file removed raiden_contracts/data/__init__.py
Empty file.
4 changes: 2 additions & 2 deletions raiden_contracts/tests/fixtures/contracts.py
Original file line number Diff line number Diff line change
Expand Up @@ -33,7 +33,7 @@ def deploy_tester_contract(
"""Returns a function that can be used to deploy a named contract,
using conract manager to compile the bytecode and get the ABI"""
def f(contract_name, libs=None, args=None):
json_contract = contracts_manager.compile_contract(contract_name, libs)
json_contract = contracts_manager.get_contract(contract_name)
contract = deploy_contract(
web3,
contract_deployer_address,
Expand All @@ -57,7 +57,7 @@ def deploy_tester_contract_txhash(
"""Returns a function that can be used to deploy a named contract,
but returning txhash only"""
def f(contract_name, libs=None, args=None):
json_contract = contracts_manager.compile_contract(contract_name, libs)
json_contract = contracts_manager.get_contract(contract_name)
txhash = deploy_contract_txhash(
web3,
contract_deployer_address,
Expand Down
31 changes: 16 additions & 15 deletions setup.py
Original file line number Diff line number Diff line change
@@ -1,5 +1,4 @@
#!/usr/bin/env python3

try:
from setuptools import setup, find_packages
except ImportError:
Expand All @@ -13,7 +12,6 @@

DESCRIPTION = 'Raiden contracts library and utilities'
VERSION = '0.0.1'
COMPILED_CONTRACTS = 'raiden_contracts/data/contracts.json'


def read_requirements(path: str):
Expand All @@ -33,7 +31,9 @@ def run(self):

class SdistCommand(sdist):
def run(self):
if os.path.isfile(COMPILED_CONTRACTS) is False:
from raiden_contracts.contract_manager import CONTRACTS_PRECOMPILED_PATH

if not CONTRACTS_PRECOMPILED_PATH.is_file():
try:
self.run_command('build')
except SystemExit:
Expand All @@ -52,23 +52,24 @@ def finalize_options(self):
pass

def run(self):
from raiden_contracts.contract_manager import (
ContractManager,
CONTRACTS_PRECOMPILED_PATH,
CONTRACTS_SOURCE_DIRS,
)
try:
from solc import compile_files # noqa
except ModuleNotFoundError:
print('py-solc is not installed, skipping contracts compilation')
return
from raiden_contracts.contract_manager import CONTRACT_MANAGER
if CONTRACT_MANAGER.contracts_source_dirs is None:
print(
'skipping compilation - contract manager is using precompiled contracts',
)
return
compiled = CONTRACT_MANAGER.precompile_contracts(
'raiden_contracts/contracts/',
CONTRACT_MANAGER.get_mappings(),
)
with open(COMPILED_CONTRACTS, 'w') as compiled_json:
compiled_json.write(json.dumps(compiled))

try:
contract_manager = ContractManager(CONTRACTS_SOURCE_DIRS)
contract_manager.store_compiled_contracts(CONTRACTS_PRECOMPILED_PATH)
except RuntimeError:
import traceback
print("Couldn't compile the contracts!")
traceback.print_exc()


config = {
Expand Down

0 comments on commit c49924a

Please sign in to comment.