Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
150 changes: 150 additions & 0 deletions .circleci/config.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,150 @@
version: 2.0

workflows:
version: 2
build_test_deploy:
jobs:
- bundle_dependencies-35:
filters:
tags:
only: /.*/
- bundle_dependencies-36:
filters:
tags:
only: /.*/
- bundle_dependencies-37:
filters:
tags:
only: /.*/
- run_tests-35:
filters:
tags:
only: /.*/
requires:
- bundle_dependencies-35
- run_tests-36:
filters:
tags:
only: /.*/
requires:
- bundle_dependencies-36
- run_tests-37:
filters:
tags:
only: /.*/
requires:
- bundle_dependencies-37

python_35_base: &python_35_base
working_directory: ~/numerology-35
docker:
- image: circleci/python:3.5

python_36_base: &python_36_base
working_directory: ~/numerology-36
docker:
- image: circleci/python:3.6

python_37_base: &python_37_base
working_directory: ~/numerology-37
docker:
- image: circleci/python:3.7

jobs:
bundle_dependencies-35:
<<: *python_35_base
steps:
- checkout
- run:
name: Install Python dependencies with Pipenv
command: pipenv sync --three --dev
- run:
name: Check PEP 508 Requirements
command: pipenv check
- save_cache:
paths:
- "~/.local/share/virtualenvs/"
key: v2-deps-{{ .Environment.CIRCLE_WORKFLOW_ID }}-{{ checksum "Pipfile.lock" }}-py35

bundle_dependencies-36:
<<: *python_36_base
steps:
- checkout
- run:
name: Install Python dependencies with Pipenv
command: pipenv sync --three --dev
- run:
name: Check PEP 508 Requirements
command: pipenv check
- save_cache:
paths:
- "~/.local/share/virtualenvs/"
key: v2-deps-{{ .Environment.CIRCLE_WORKFLOW_ID }}-{{ checksum "Pipfile.lock" }}-py36

bundle_dependencies-37:
<<: *python_37_base
steps:
- checkout
- run:
name: Install Python dependencies with Pipenv
command: pipenv sync --three --dev
- run:
name: Check PEP 508 Requirements
command: pipenv check
- save_cache:
paths:
- "~/.local/share/virtualenvs/"
key: v2-deps-{{ .Environment.CIRCLE_WORKFLOW_ID }}-{{ checksum "Pipfile.lock" }}-py37

run_tests-35:
<<: *python_35_base
steps:
- checkout
# - restore_cache:
# key: v2-deps-{{ .Environment.CIRCLE_WORKFLOW_ID }}-{{ checksum "Pipfile.lock" }}-py35
- run:
name: Install dependencies
command: |
pipenv sync --three --dev
pipenv install --dev pytest
- run:
name: numerology Tests (Python 3.5)
command: pipenv run pytest --junitxml=./reports/pytest/python35-results.xml
- store_test_results:
path: /reports/pytest
- store_artifacts:
path: ./htmlcov

run_tests-36:
<<: *python_36_base
steps:
- checkout
# - restore_cache:
# key: v2-deps-{{ .Environment.CIRCLE_WORKFLOW_ID }}-{{ checksum "Pipfile.lock" }}-py36
- run:
name: Install dependencies
command: pipenv sync --three --dev
- run:
name: numerology Tests (Python 3.6)
command: pipenv run pytest --junitxml=./reports/pytest/python36-results.xml
- store_test_results:
path: /reports/pytest
- store_artifacts:
path: ./htmlcov

run_tests-37:
<<: *python_37_base
steps:
- checkout
# - restore_cache:
# key: v2-deps-{{ .Environment.CIRCLE_WORKFLOW_ID }}-{{ checksum "Pipfile.lock" }}-py37
- run:
name: Install dependencies
command: pipenv sync --three --dev
- run:
name: numerology Tests (Python 3.7)
command: pipenv run pytest --junitxml=./reports/pytest/python37-results.xml
- store_test_results:
path: /reports/pytest
- store_artifacts:
path: ./htmlcov
20 changes: 20 additions & 0 deletions Pipfile
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
[[source]]
url = "https://pypi.python.org/simple"
verify_ssl = true
name = "pypi"

[packages]

[dev-packages]
pytest = "*"
web3 = "*"
py-solc = "*"
#eth-tester = "*"
eth-tester = {git = "https://github.com/KPrasch/eth-tester.git", ref = "ef4bb2fa793af8aa964b83536b20f525aa74d4e4"}
py-evm = ">=0.2.0a31"

[pipenv]
allow_prereleases = true

[scripts]
install-solc = "./scripts/install_solc.sh"
Empty file added blockchain/__init__.py
Empty file.
102 changes: 102 additions & 0 deletions blockchain/chains.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,102 @@
from logging import getLogger
from typing import List

from web3.contract import Contract

from blockchain.constants import (DEVELOPMENT_ETH_AIRDROP_AMOUNT)
from blockchain.interfaces import BlockchainDeployerInterface


class TesterBlockchain:
"""A view of a blockchain through a provided interface"""

_instance = None
__default_interface_class = BlockchainDeployerInterface

class ConnectionNotEstablished(RuntimeError):
pass

def __init__(self,
interface: BlockchainDeployerInterface = None,
airdrop=True) -> None:

self.log = getLogger("test-blockchain") # type: Logger

# Default interface
if interface is None:
interface = self.__default_interface_class()
self.__interface = interface

# Singleton
if self._instance is None:
TesterBlockchain._instance = self
else:
raise RuntimeError("Connection already established - Use .connect()")

if airdrop is True: # ETH for everyone!
self.ether_airdrop(amount=DEVELOPMENT_ETH_AIRDROP_AMOUNT)

def __repr__(self):
class_name = self.__class__.__name__
r = "{}(interface={})"
return r.format(class_name, self.__interface)

@property
def interface(self) -> BlockchainDeployerInterface:
return self.__interface

def get_contract(self, name: str) -> Contract:
"""
Gets an existing contract from the registry, or raises UnknownContract
if there is no contract data available for the name/identifier.
"""
return self.__interface.get_contract_by_name(name)

def wait_for_receipt(self, txhash: str, timeout: int = None) -> dict:
"""Wait for a transaction receipt and return it"""
timeout = timeout if timeout is not None else self.interface.timeout
result = self.__interface.w3.eth.waitForTransactionReceipt(txhash, timeout=timeout)
return result

def ether_airdrop(self, amount: int) -> List[str]:
"""Airdrops ether from creator address to all other addresses!"""

coinbase, *addresses = self.interface.w3.eth.accounts

tx_hashes = list()
for address in addresses:

tx = {'to': address, 'from': coinbase, 'value': amount}
txhash = self.interface.w3.eth.sendTransaction(tx)

_receipt = self.wait_for_receipt(txhash)
tx_hashes.append(txhash)
self.log.info("Airdropped {} ETH {} -> {}".format(amount, tx['from'], tx['to']))

return tx_hashes

def time_travel(self, hours: int=None, seconds: int=None):
"""
Wait the specified number of wait_hours by comparing
block timestamps and mines a single block.
"""

more_than_one_arg = sum(map(bool, (hours, seconds))) > 1
if more_than_one_arg:
raise ValueError("Specify hours or seconds, not a combination")

if hours:
duration = hours * (60*60)
base = 60 * 60
elif seconds:
duration = seconds
base = 1
else:
raise ValueError("Specify either hours, seconds, or lock_periods.")

now = self.interface.w3.eth.getBlock(block_identifier='latest').timestamp
end_timestamp = ((now+duration)//base) * base

self.interface.w3.eth.web3.testing.timeTravel(timestamp=end_timestamp)
self.interface.w3.eth.web3.testing.mine(1)
self.log.info("Time traveled to {}".format(end_timestamp))
96 changes: 96 additions & 0 deletions blockchain/compile.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,96 @@
import os
from logging import getLogger
from os.path import dirname

import itertools
import shutil
from solc import install_solc, compile_files
from solc.exceptions import SolcError
from blockchain.constants import CONTRACTS_DIR


class SolidityCompiler:

__default_version = 'v0.4.25'

__default_sol_binary_path = shutil.which('solc')
if __default_sol_binary_path is None:
__bin_path = os.path.dirname(shutil.which('python')) # type: str
__default_sol_binary_path = os.path.join(__bin_path, 'solc') # type: str

__default_contract_dir = CONTRACTS_DIR
__default_chain_name = 'tester'

def __init__(self,
solc_binary_path: str = None,
source_dir: str = None,
test_contract_dir: str= None
) -> None:

self.log = getLogger('solidity-compiler')
# Compiler binary and root solidity source code directory
self.__sol_binary_path = solc_binary_path if solc_binary_path is not None else self.__default_sol_binary_path
self.source_dir = source_dir if source_dir is not None else self.__default_contract_dir
self._test_solidity_source_dir = test_contract_dir

# Set the local env's solidity compiler binary
os.environ['SOLC_BINARY'] = self.__sol_binary_path

def install_compiler(self, version: str=None):
"""
Installs the specified solidity compiler version.
https://github.com/ethereum/py-solc#installing-the-solc-binary
"""
version = version if version is not None else self.__default_version
return install_solc(version, platform=None) # TODO: fix path

def compile(self) -> dict:
"""Executes the compiler with parameters specified in the json config"""

self.log.info("Using solidity compiler binary at {}".format(self.__sol_binary_path))
self.log.info("Compiling solidity source files at {}".format(self.source_dir))

source_paths = set()
source_walker = os.walk(top=self.source_dir, topdown=True)
if self._test_solidity_source_dir:
test_source_walker = os.walk(top=self._test_solidity_source_dir, topdown=True)
source_walker = itertools.chain(source_walker, test_source_walker)

for root, dirs, files in source_walker:
for filename in files:
if filename.endswith('.sol'):
path = os.path.join(root, filename)
source_paths.add(path)
self.log.debug("Collecting solidity source {}".format(path))

# Compile with remappings: https://github.com/ethereum/py-solc
project_root = dirname(self.source_dir)

remappings = ("contracts={}".format(self.source_dir),
)

self.log.info("Compiling with import remappings {}".format(", ".join(remappings)))

optimization_runs = 10 # TODO: Move..?
try:
compiled_sol = compile_files(source_files=source_paths,
import_remappings=remappings,
allow_paths=project_root,
optimize=optimization_runs)

self.log.info("Successfully compiled {} contracts with {} optimization runs".format(len(compiled_sol),
optimization_runs))

except FileNotFoundError:
raise RuntimeError("The solidity compiler is not at the specified path. "
"Check that the file exists and is executable.")
except PermissionError:
raise RuntimeError("The solidity compiler binary at {} is not executable. "
"Check the file's permissions.".format(self.__sol_binary_path))

except SolcError:
raise

# Cleanup the compiled data keys
interfaces = {name.split(':')[-1]: compiled_sol[name] for name in compiled_sol}
return interfaces
17 changes: 17 additions & 0 deletions blockchain/constants.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
import os
from os.path import abspath, dirname

import blockchain

NUCYPHER_GAS_LIMIT = 5000000

DEVELOPMENT_ETH_AIRDROP_AMOUNT = 10 ** 18 # wei -> ether

DEFAULT_NUMBER_OF_URSULAS_IN_DEVELOPMENT_NETWORK = 10

# Base Filepaths
BASE_DIR = abspath(dirname(dirname(blockchain.__file__)))
CONTRACTS_DIR = os.path.join(BASE_DIR, 'numerology', 'contracts')

# Test Constants
TEST_CONTRACTS_DIR = os.path.join(BASE_DIR, 'tests', 'contracts')
Loading