Skip to content

Commit

Permalink
MEGACOMMIT, but all related.
Browse files Browse the repository at this point in the history
- adds ability to set a newly created account as default
- adds `use_default_account` setting to networks.yml
- accounts with this setting will autodeploy with the default network(if autodeploy_allowed is set, anyway)
- standardized `sb test` exit codes
- `sb test` autocompiles now
- account attribute is now not required for an instance of deployer, only for certain uses
- adds "stateless" test and deploy tests without using ganache

Issue #77
  • Loading branch information
mikeshultz committed Mar 23, 2019
1 parent 7764c37 commit a9ec020
Show file tree
Hide file tree
Showing 13 changed files with 284 additions and 48 deletions.
28 changes: 21 additions & 7 deletions solidbyte/cli/accounts.py
Original file line number Diff line number Diff line change
Expand Up @@ -51,6 +51,9 @@ def add_parser_arguments(parser):
dest='passphrase',
help='The passphrase to use to encrypt the keyfile. Leave empty for prompt.'
)
create_parser.add_argument('-e', '--default', action='store_true',
dest="create_default", default=False,
help='Set the newly created account as default')

# Set default account
default_parser = subparsers.add_parser('default', help="Set the default account")
Expand All @@ -68,26 +71,37 @@ def main(parser_args):
network_name = parser_args.network
else:
network_name = None

web3 = web3c.get_web3(network_name)
accts = Accounts(network_name=network_name,
keystore_dir=parser_args.keystore, web3=web3)
mfile = MetaFile()

if parser_args.account_command == 'create':
print("creating account...")

log.debug("creating account...")

if parser_args.passphrase:
password = parser_args.passphrase
else:
password = getpass('Encryption password:')

addr = accts.create_account(password)
print("Created new account: {}".format(addr))

log.info("Created new account: {}".format(addr))

if parser_args.create_default is True:
mfile.set_default_account(addr)
log.info("New account set as default.")

elif parser_args.account_command == 'default':
print("Setting default account to: {}".format(parser_args.default_address))

metafile = MetaFile()
metafile.set_default_account(parser_args.default_address)
log.info("Setting default account to: {}".format(parser_args.default_address))

mfile.set_default_account(parser_args.default_address)

else:
metafile = MetaFile()
default_account = metafile.get_default_account()
default_account = mfile.get_default_account()
default_string = lambda a: "*" if a.address == default_account else "" # noqa: E731
print("Accounts")
print("========")
Expand Down
6 changes: 4 additions & 2 deletions solidbyte/cli/deploy.py
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,7 @@ def add_parser_arguments(parser):
""" Add additional subcommands onto this command """
parser.add_argument('network', metavar="NETWORK", type=str, nargs=1,
help='Ethereum network to connect the console to')
parser.add_argument('-a', '--address', type=str, required=True,
parser.add_argument('-a', '--address', type=str, dest="address",
help='Address of the Ethereum account to use for deployment')
parser.add_argument(
'-p',
Expand Down Expand Up @@ -47,7 +47,7 @@ def main(parser_args):
# Make sure we actually need to deploy
if not deployer.check_needs_deploy():
log.info("No changes, deployment unnecessary")
sys.exit()
sys.exit(0)

deployer.deploy()

Expand All @@ -57,3 +57,5 @@ def main(parser_args):
contract = deployer.contracts[name]
log.info("{}: {}".format(contract.name, contract.address))
log.info("--------------------------------------")

sys.exit(0)
4 changes: 3 additions & 1 deletion solidbyte/cli/script.py
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,8 @@ def add_parser_arguments(parser):
help='Ethereum network to connect the console to')
parser.add_argument('script', metavar="FILE", type=str, nargs='+',
help='Script to run')
parser.add_argument('-a', '--address', type=str, dest="address",
help='Address of the Ethereum account used for deployment')
return parser


Expand All @@ -27,7 +29,7 @@ def main(parser_args):
', '.join(parser_args.script))
)

res = run_scripts(collapse_oel(parser_args.network), parser_args.script)
res = run_scripts(collapse_oel(parser_args.network), parser_args.script, parser_args.address)

if not res:
log.error("Script{} returned error".format(scripts_plural))
Expand Down
26 changes: 22 additions & 4 deletions solidbyte/cli/test.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,11 +2,12 @@
"""
import sys
from pathlib import Path
from enum import IntEnum
from tabulate import tabulate
from ..testing import run_tests
from ..compile.artifacts import artifacts
from ..common import collapse_oel
from ..common.exceptions import DeploymentValidationError
from ..common.exceptions import AccountError, DeploymentValidationError
from ..common import store
from ..common.web3 import web3c, remove_0x
from ..common.logging import ConsoleStyle, getLogger
Expand All @@ -21,6 +22,17 @@
GAS_ERROR = 47e5 # Ropsten is at 4.7m


class TestReturnCodes(IntEnum):
SUCCESS = 0 # Pytest "success"
TESTS_FAILED = 1 # pytest "some failed"
INTERRUPTED = 2 # pytest user interrupt
INTERNAL = 3 # pytest internal error
CLI_ERROR = 4 # pytest CLI usage error
NO_TESTS = 5 # pytest no tests found
ERROR = 10 # solidbyte error
NOT_ALLOWED = 11 # solidbyte not allowed


def highlight_gas(gas):
""" Uses console color highlights to indicate high gas usage """

Expand Down Expand Up @@ -98,14 +110,20 @@ def main(parser_args):
return_code = run_tests(network_name, web3=web3, args=args,
account_address=parser_args.address,
keystore_dir=parser_args.keystore, gas_report_storage=report)
except AccountError as err:
if 'use_default_account' in str(err):
log.exception("Use of a default account dissallowed.")
sys.exit(TestReturnCodes.NOT_ALLOWED)
else:
raise err
except DeploymentValidationError as err:
if 'autodeployment' in str(err):
log.error("The -a/--address option or --default must be provided for autodeployment")
sys.exit(1)
log.error("The -a/--address option be provided for autodeployment")
sys.exit(TestReturnCodes.NOT_ALLOWED)
else:
raise err
else:
if return_code != 0:
if return_code != TestReturnCodes.SUCCESS:
log.error("Tests have failed. Return code: {}".format(return_code))
else:
if parser_args.gas:
Expand Down
3 changes: 3 additions & 0 deletions solidbyte/common/metafile.py
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,7 @@
from typing import Union, Any, Optional, Callable, List, Tuple
from pathlib import Path
from datetime import datetime
from functools import wraps
from shutil import copyfile
from attrdict import AttrDict
from .logging import getLogger
Expand All @@ -48,6 +49,7 @@

def autoload(f: Callable) -> Callable:
""" Automatically load the metafile before method execution """
@wraps(f)
def wrapper(*args, **kwargs):
# A bit defensive, but make sure this is a decorator of a MetaFile method
if len(args) > 0 and isinstance(args[0], MetaFile):
Expand All @@ -58,6 +60,7 @@ def wrapper(*args, **kwargs):

def autosave(f):
""" Automatically save the metafile after method execution """
@wraps(f)
def wrapper(*args, **kwargs):
retval = f(*args, **kwargs)
# A bit defensive, but make sure this is a decorator of a MetaFile method
Expand Down
58 changes: 51 additions & 7 deletions solidbyte/deploy/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -10,11 +10,11 @@
builddir,
to_path_or_cwd,
)
from ..common.exceptions import DeploymentError
from ..common.exceptions import AccountError, DeploymentError
from ..common.logging import getLogger
from ..common.web3 import web3c
from ..common.metafile import MetaFile
# from ..common.networks import NetworksYML
from ..common.networks import NetworksYML
from .objects import Contract, ContractDependencyTree

log = getLogger(__name__)
Expand Down Expand Up @@ -78,10 +78,8 @@ def __init__(self, network_name: str, account: str = None, project_dir: PS = Non
# else:
self.metafile: MetaFile = MetaFile(project_dir=project_dir)

if account:
self.account = self.web3.toChecksumAddress(account)
else:
self.account = self.metafile.get_default_account()
self.account = None
self._init_account(account, fail_on_error=False)

if not self.contracts_dir.is_dir():
raise FileNotFoundError("contracts directory does not exist")
Expand Down Expand Up @@ -123,6 +121,9 @@ def get_contracts(self, force: bool = False):
if force is False and len(self._contracts) > 0:
return self._contracts

if not self.account:
self._init_account(fail_on_error=False)

self._contracts = AttrDict()
for key in self.artifacts.keys():
self._contracts[key] = Contract(
Expand Down Expand Up @@ -208,9 +209,13 @@ def deploy(self) -> bool:
"""

if not self.account:
raise DeploymentError("Account needs to be set for deployment")
self._init_account()
if not self.account:
raise DeploymentError("No account available.")

if self.account and self.web3.eth.getBalance(self.account) == 0:
log.warning("Account has zero balance ({})".format(self.account))

if self.network_id != (self.web3.net.chainId or self.web3.net.version):
raise DeploymentError("Connected node is does not match the provided chain ID")

Expand All @@ -219,6 +224,39 @@ def deploy(self) -> bool:

return True

def _init_account(self, account=None, fail_on_error=True):
""" Try and figure out what account to use for deployment """

if account is not None:
account = self.web3.toChecksumAddress(account)
self.account = account
return

if self.account is not None:
return

yml = NetworksYML(project_dir=self.project_dir)

if yml.use_default_account(self.network_name):

self.account = self.metafile.get_default_account()

if self.account is not None:
return self.account
elif fail_on_error:
raise DeploymentError(
"Account needs to be set for deployment. No default account found."
)

return None

elif fail_on_error:

raise AccountError(
"Use of default account on this network is not allowed and no account was"
"provided. You may want to set 'use_default_account: true' for this network."
)

def _load_user_scripts(self) -> None:
""" Load the user deploy scripts from deploy folder as python modules and stash 'em away for
later execution.
Expand Down Expand Up @@ -275,6 +313,12 @@ def _get_script_kwargs(self) -> Dict[str, T]:
:returns: dict of the kwargs to provide to deployer scripts
"""

if not self.account:
self._init_account()
if not self.account:
raise DeploymentError("Account not set.")

return {
'contracts': self.contracts,
'web3': self.web3,
Expand Down
16 changes: 8 additions & 8 deletions solidbyte/script/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -15,12 +15,12 @@
deployer: Optional[Deployer] = None


def get_contracts(network: str) -> AttrDict:
def get_contracts(network: str, account: str = None) -> AttrDict:
""" Get a list of web3 contract instances. """
global deployer

if not deployer:
deployer = Deployer(network_name=network)
deployer = Deployer(network_name=network, account=account)

contracts: AttrDict = AttrDict({})

Expand All @@ -36,20 +36,20 @@ def get_contracts(network: str) -> AttrDict:
return contracts


def get_availble_script_kwargs(network) -> Dict[str, Any]:
def get_availble_script_kwargs(network, account: str = None) -> Dict[str, Any]:
""" Get a dict of the kwargs available for user scripts """
return {
'web3': web3c.get_web3(network),
'contracts': get_contracts(network),
'contracts': get_contracts(network, account),
'network': network,
}


def run_script(network: str, script: str) -> bool:
def run_script(network: str, script: str, account: str = None) -> bool:
""" Runs a user script """

scriptPath: Path = to_path(script)
availible_script_kwargs: Dict[str, Any] = get_availble_script_kwargs(network)
availible_script_kwargs: Dict[str, Any] = get_availble_script_kwargs(network, account)

spec = spec_from_file_location(scriptPath.name[:-3], str(scriptPath))
mod: Optional[ModuleType] = module_from_spec(spec)
Expand All @@ -74,11 +74,11 @@ def run_script(network: str, script: str) -> bool:
return True


def run_scripts(network: str, scripts: List[str]) -> bool:
def run_scripts(network: str, scripts: List[str], account: str = None) -> bool:
""" Run multiple user scripts """

if len(scripts) < 1:
log.warning("No scripts provided")
return True

return all([run_script(network, script) for script in scripts])
return all([run_script(network, script, account) for script in scripts])
1 change: 1 addition & 0 deletions solidbyte/templates/templates/bare/networks.yml
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ dev:
test:
type: eth_tester
autodeploy_allowed: true
use_default_account: true

infura-mainnet:
type: websocket
Expand Down
1 change: 1 addition & 0 deletions solidbyte/templates/templates/erc20/networks.yml
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ dev:
test:
type: eth_tester
autodeploy_allowed: true
use_default_account: true

infura-mainnet:
type: websocket
Expand Down
8 changes: 8 additions & 0 deletions solidbyte/testing/__init__.py
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
import pytest
from attrdict import AttrDict
from ..accounts import Accounts
from ..compile import compile_all
from ..deploy import Deployer, get_latest_from_deployed
from ..common.utils import to_path, to_path_or_cwd
from ..common.web3 import web3c
Expand Down Expand Up @@ -89,6 +90,11 @@ def run_tests(network_name, args=[], web3=None, project_dir=None, account_addres

log.debug("Using account {} for deployer.".format(account_address))

log.info("Compiling contracts for testing...")
compile_all()

log.info("Checking if deployment is necessary...")

# First, see if we're allowed to deploy, and whether we need to
deployer = Deployer(
network_name=network_name,
Expand All @@ -103,6 +109,8 @@ def run_tests(network_name, args=[], web3=None, project_dir=None, account_addres
if not account_address:
raise DeploymentValidationError("Account needs to be provided for autodeployment")

log.info("Deploying contracts...")

deployer.deploy()

elif deployer.check_needs_deploy() and not (
Expand Down
Loading

0 comments on commit a9ec020

Please sign in to comment.