Skip to content

Commit

Permalink
Merge branch 'develop'
Browse files Browse the repository at this point in the history
  • Loading branch information
mikeshultz committed Jan 5, 2019
2 parents 7142fe6 + 0d20a07 commit 8f42eb6
Show file tree
Hide file tree
Showing 30 changed files with 765 additions and 286 deletions.
91 changes: 66 additions & 25 deletions solidbyte/accounts/__init__.py
@@ -1,31 +1,58 @@
""" Objects and utility functions for account operations """
import os
import json
from typing import TypeVar, List
from typing import TypeVar, List, Callable
from getpass import getpass
from pathlib import Path
from datetime import datetime
from attrdict import AttrDict
from eth_account import Account
from web3 import Web3
from ..common import to_path
from ..common.store import Store, StoreKeys
from ..common.exceptions import ValidationError
from ..common.logging import getLogger

log = getLogger(__name__)

T = TypeVar('T')


def autoload(f: Callable) -> Callable:
""" Automatically load the metafile before method execution """
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], Accounts):
args[0]._load_accounts()
return f(*args, **kwargs)
return wrapper


class Accounts(object):
def __init__(self, network_name: str = None,
keystore_dir: str = '~/.ethereum/keystore',
keystore_dir: str = None,
web3: object = None) -> None:

self.eth_account = Account()
self.accounts = []
self.keystore_dir = Path(keystore_dir).expanduser().resolve()

if keystore_dir:
self.keystore_dir = to_path(keystore_dir)
else:
# Try the session store first.
if Store.defined(StoreKeys.KEYSTORE_DIR):
self.keystore_dir = to_path(Store.get(StoreKeys.KEYSTORE_DIR))
else:
# Default to the standard loc
self.keystore_dir = to_path('~/.ethereum/keystore')
log.debug("Keystore directory: {}".format(self.keystore_dir))

if web3:
self.web3 = web3
else:
self.web3 = Web3()
log.warning("Accounts initialized without a web3 instance. Some things like gas price "
"estimation might be off or not working.")
self.web3 = None

if not self.keystore_dir.is_dir():
if self.keystore_dir.exists():
Expand Down Expand Up @@ -54,7 +81,7 @@ def _write_json_file(self, json_object: object, filename: str = None) -> None:
filename = self.keystore_dir.joinpath(
'UTC--{}--{}'.format(
datetime.now().isoformat(),
json_object.get('address')
Web3.toChecksumAddress(json_object.get('address'))
)
)
with open(filename, 'w') as json_file:
Expand All @@ -78,63 +105,68 @@ def _load_accounts(self, force: bool = False) -> None:
if not jason:
log.warning("Unable to read JSON from {}".format(file))
else:
addr = self.web3.toChecksumAddress(jason.get('address'))
addr = Web3.toChecksumAddress(jason.get('address'))
bal = -1
if self.web3:
bal = self.web3.fromWei(
self.web3.eth.getBalance(self.web3.toChecksumAddress(addr)),
'ether'
)
self.accounts.append(AttrDict({
'address': addr,
'filename': file,
'balance': self.web3.fromWei(
self.web3.eth.getBalance(self.web3.toChecksumAddress(addr)),
'ether'
),
'balance': bal,
'privkey': None
}))

def refresh(self) -> None:
self._load_accounts(True)

@autoload
def _get_account_index(self, address: str) -> int:
""" Return the list index for the account """
idx = 0
for a in self.accounts:
if a.address == address:
if Web3.toChecksumAddress(a.address) == Web3.toChecksumAddress(address):
return idx
idx += 1
raise IndexError("account does not exist")

@autoload
def get_account(self, address: str) -> AttrDict:
""" Return all the known account addresses """

self._load_accounts()

for a in self.accounts:
if a.address == address:
if Web3.toChecksumAddress(a.address) == Web3.toChecksumAddress(address):
return a

raise FileNotFoundError("Unable to find requested account")

@autoload
def account_known(self, address: str) -> bool:
""" Check if an account is known """

try:
self._get_account_index(address)
return True
except IndexError:
log.debug("Account {} is not locally managed in keystore {}".format(
address,
self.keystore_dir
))
return False

@autoload
def get_accounts(self) -> List[AttrDict]:
""" Return all the known account addresses """

self._load_accounts()

return self.accounts

def set_account_attribute(self, address: str, key: str, val: T) -> None:
""" Set an attribute of an account """
idx = 0
for a in self.accounts:
if a.address == address:
setattr(self.accounts[idx], key, val)
return
idx += 1
idx = self._get_account_index(address)
if idx < 0:
raise IndexError("{} not found. Unable to set attribute.".format(address))
return setattr(self.accounts[idx], key, val)

def create_account(self, password: str) -> str:
""" Create a new account and encrypt it with password """
Expand All @@ -144,8 +176,9 @@ def create_account(self, password: str) -> str:

self._write_json_file(encrypted_account)

return new_account.address
return Web3.toChecksumAddress(new_account.address)

@autoload
def unlock(self, account_address: str, password: str = None) -> bytes:
""" Unlock an account keystore file and return the private key """

Expand All @@ -157,7 +190,11 @@ def unlock(self, account_address: str, password: str = None) -> bytes:
return account.privkey

if not password:
password = getpass("Enter password to decrypt account ({}):".format(account_address))
password = Store.get(StoreKeys.DECRYPT_PASSPHRASE)
if not password:
password = getpass("Enter password to decrypt account ({}):".format(
account_address
))

jason = self._read_json_file(account.filename)
privkey = self.eth_account.decrypt(jason, password)
Expand All @@ -169,6 +206,10 @@ def sign_tx(self, account_address: str, tx: dict, password: str = None) -> str:

log.debug("Signing tx with account {}".format(account_address))

if not self.web3:
raise ValidationError("Unable to sign a transaction without an instantiated Web3 "
"object.")

""" Do some tx verification and substitution if necessary
"""
if tx.get('gasPrice') is None:
Expand Down
17 changes: 13 additions & 4 deletions solidbyte/cli/accounts.py
Expand Up @@ -13,9 +13,6 @@
def add_parser_arguments(parser):
""" Add additional subcommands onto this command """

parser.add_argument('-k', '--keystore', type=str,
default='~/.ethereum/keystore',
help='The Ethereum keystore directory to load accounts from')
parser.add_argument('network', metavar="NETWORK", type=str, nargs="?",
help='Ethereum network to connect the console to')

Expand Down Expand Up @@ -45,6 +42,15 @@ def add_parser_arguments(parser):

# Create account
create_parser = subparsers.add_parser('create', help="Create a new account") # noqa: F841
create_parser.add_argument(
'-p'
'--passphrase',
metavar='PASSPHRASE',
type=str,
nargs="?",
dest='passphrase',
help='The passphrase to use to encrypt the keyfile. Leave empty for prompt.'
)

# Set default account
default_parser = subparsers.add_parser('default', help="Set the default account")
Expand All @@ -69,7 +75,10 @@ def main(parser_args):

if parser_args.account_command == 'create':
print("creating account...")
password = getpass('Encryption password:')
if parser_args.passphrase:
password = parser_args.passphrase
else:
password = getpass('Encryption password:')
addr = accts.create_account(password)
print("Created new account: {}".format(addr))
elif parser_args.account_command == 'default':
Expand Down
8 changes: 8 additions & 0 deletions solidbyte/cli/handler.py
Expand Up @@ -3,6 +3,7 @@
import sys
import argparse
from importlib import import_module
from ..common.store import Store, StoreKeys
from ..common.logging import getLogger, loggingShutdown

log = getLogger(__name__)
Expand All @@ -29,6 +30,9 @@ def parse_args(argv=None):
parser = argparse.ArgumentParser(description='SolidByte Ethereum development tools')
parser.add_argument('-d', action='store_true',
help='Print debug level messages')
parser.add_argument('-k', '--keystore', type=str, dest="keystore",
default='~/.ethereum/keystore',
help='Ethereum account keystore directory to use.')

subparsers = parser.add_subparsers(title='Submcommands', dest='command',
help='do the needful')
Expand Down Expand Up @@ -68,6 +72,10 @@ def main(argv=None):
log.error('Unknown command: {}'.format(args.command))
sys.exit(2)

if args.keystore:
log.info("Using keystore at {}".format(args.keystore))
Store.set(StoreKeys.KEYSTORE_DIR, args.keystore)

IMPORTED_MODULES[args.command].main(parser_args=args)

loggingShutdown()
Expand Down
26 changes: 25 additions & 1 deletion solidbyte/cli/test.py
@@ -1,7 +1,10 @@
""" run project tests
"""
import sys
from ..testing import run_tests
from ..common import collapse_oel
from ..common.exceptions import DeploymentValidationError
from ..common.store import Store, StoreKeys
from ..common.logging import getLogger

log = getLogger(__name__)
Expand All @@ -11,12 +14,33 @@ 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=False,
help='Address of the Ethereum account to use for deployment')
parser.add_argument(
'-p',
'--passphrase',
metavar='PASSPHRASE',
type=str,
dest='passphrase',
help='The passphrase to use to decrypt the account.'
)
return parser


def main(parser_args):
""" Execute test """
log.info("Executing project tests...")

if parser_args.passphrase:
# Set this for use later
Store.set(StoreKeys.DECRYPT_PASSPHRASE, parser_args.passphrase)

network_name = collapse_oel(parser_args.network)
run_tests(network_name=network_name)
try:
run_tests(network_name=network_name, account_address=parser_args.address)
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)
else:
raise err
2 changes: 2 additions & 0 deletions solidbyte/common/__init__.py
Expand Up @@ -10,4 +10,6 @@
defs_not_in,
find_vyper,
hash_file,
to_path,
to_path_or_cwd,
)
3 changes: 3 additions & 0 deletions solidbyte/common/exceptions.py
Expand Up @@ -4,3 +4,6 @@ class DeploymentError(SolidbyteException): pass
class DeploymentValidationError(DeploymentError): pass
class CompileError(SolidbyteException): pass
class LinkError(CompileError): pass
class ConfigurationError(SolidbyteException): pass
class AccountError(SolidbyteException): pass
class ValidationError(SolidbyteException): pass
17 changes: 12 additions & 5 deletions solidbyte/common/metafile.py
Expand Up @@ -21,6 +21,10 @@
}
}
],
"seenAccounts": [
"0xdeadbeef..."
],
"defaultAccount": "0xdeadbeef..."
}
"""
import json
Expand All @@ -30,7 +34,7 @@
from shutil import copyfile
from attrdict import AttrDict
from .logging import getLogger
from .utils import hash_file
from .utils import hash_file, to_path_or_cwd
from .web3 import normalize_address, normalize_hexstring

log = getLogger(__name__)
Expand Down Expand Up @@ -68,14 +72,14 @@ class MetaFile:

def __init__(self,
filename_override: Generic[PS] = None,
project_dir: Generic[PS] = None) -> None:
project_dir: Generic[PS] = None,
read_only: bool = False) -> None:

if project_dir is not None and not isinstance(project_dir, Path):
project_dir = Path(project_dir)
self.project_dir = project_dir or Path.cwd()
self.project_dir = to_path_or_cwd(project_dir)
self.file_name = self.project_dir.joinpath(filename_override or METAFILE_FILENAME)
self._file = None
self._json = None
self._read_only = read_only

def _load(self) -> None:
""" Lazily load the metafile """
Expand All @@ -89,6 +93,9 @@ def _load(self) -> None:

def _save(self):
""" Save the metafile """
if self._read_only:
return False
log.debug("Saving metafile...")
with open(self.file_name, 'w') as openFile:
openFile.write(json.dumps(self._json, indent=2))
self._load()
Expand Down

0 comments on commit 8f42eb6

Please sign in to comment.