diff --git a/nucypher/blockchain/eth/actors.py b/nucypher/blockchain/eth/actors.py index a328dbbaa..f12291f38 100644 --- a/nucypher/blockchain/eth/actors.py +++ b/nucypher/blockchain/eth/actors.py @@ -36,7 +36,7 @@ from constant_sorrow.constants import ( from eth_tester.exceptions import TransactionFailed from eth_utils import keccak, is_checksum_address, to_checksum_address from twisted.logger import Logger -from web3 import Web3 +from web3 import Web3, IPCProvider from web3.contract import ContractFunction from nucypher.blockchain.economics import StandardTokenEconomics, EconomicsFactory, BaseEconomics @@ -48,8 +48,11 @@ from nucypher.blockchain.eth.agents import ( ContractAgency, PreallocationEscrowAgent, MultiSigAgent, - WorkLockAgent) -from nucypher.blockchain.eth.decorators import validate_checksum_address, only_me, save_receipt + WorkLockAgent +) +from nucypher.blockchain.eth.clients import ClefClient +from nucypher.blockchain.eth.decorators import only_me, save_receipt +from nucypher.blockchain.eth.decorators import validate_checksum_address from nucypher.blockchain.eth.deployers import ( NucypherTokenDeployer, StakingEscrowDeployer, @@ -1478,10 +1481,16 @@ class StakeHolder(Staker): def __init__(self, registry: BaseContractRegistry, client_addresses: set = None, - keyfiles: List[str] = None): + keyfiles: List[str] = None, + signer=None): - # Wallet - self.__keyfiles = keyfiles + if signer: + provider = IPCProvider(signer) + w3 = Web3(provider=provider) + signer = ClefClient(w3=w3) + + self.__signer = signer + self.__keyfiles = keyfiles or list() self.__local_accounts = dict() self.__client_accounts = set() # Note: Account index is meaningless here self.__transacting_powers = dict() @@ -1504,10 +1513,9 @@ class StakeHolder(Staker): return self.blockchain.transacting_power.account def __get_accounts(self) -> None: - # chain_name = self.blockchain.client.chain_name.lower() - # keystore = f'{os.path.expanduser("~")}/.ethereum/{chain_name}/keystore' - # keyfiles = os.listdir(keystore) - # for keyfile in keyfiles: + if self.__signer: + signer_accounts = self.__signer.accounts() + self.__client_accounts.update(signer_accounts) for keyfile in self.__keyfiles: try: account = to_checksum_address(keyfile.split('--')[-1]) @@ -1531,7 +1539,10 @@ class StakeHolder(Staker): transacting_power = self.__transacting_powers[checksum_address] except KeyError: keyfile = self.__local_accounts.get(checksum_address) - transacting_power = TransactingPower(password=password, account=checksum_address, keyfile=keyfile) + transacting_power = TransactingPower(password=password, + account=checksum_address, + client=self.__signer, + keyfile=keyfile) self.__transacting_powers[checksum_address] = transacting_power transacting_power.activate(password=password) @@ -1553,6 +1564,7 @@ class StakeHolder(Staker): initial_address: str = None, checksum_addresses: set = None, keyfiles: List[str] = None, + signer: str = None, password: str = None, *args, **kwargs): @@ -1564,7 +1576,8 @@ class StakeHolder(Staker): # Wallet self.wallet = self.StakingWallet(registry=self.registry, client_addresses=checksum_addresses, - keyfiles=keyfiles) + keyfiles=keyfiles, + signer=signer) if initial_address: # If an initial address was passed, # it is safe to understand that it has already been used at a higher level. diff --git a/nucypher/blockchain/eth/clients.py b/nucypher/blockchain/eth/clients.py index 94accf447..3f156434c 100644 --- a/nucypher/blockchain/eth/clients.py +++ b/nucypher/blockchain/eth/clients.py @@ -1,15 +1,15 @@ import json import os import shutil -import time -from typing import Union +from typing import Union, List import maya +import time from constant_sorrow.constants import NOT_RUNNING, UNKNOWN_DEVELOPMENT_CHAIN_ID from cytoolz.dicttoolz import dissoc from eth_account import Account from eth_account.messages import encode_defunct -from eth_utils import to_canonical_address +from eth_utils import to_canonical_address, to_normalized_address from eth_utils import to_checksum_address from geth import LoggingMixin from geth.accounts import get_accounts, create_new_account @@ -25,7 +25,6 @@ from web3 import Web3 from nucypher.config.constants import DEFAULT_CONFIG_ROOT, DEPLOY_DIR, USER_LOG_DIR - UNKNOWN_DEVELOPMENT_CHAIN_ID.bool_value(True) @@ -71,10 +70,13 @@ class Web3Client: PARITY = 'Parity' ALT_PARITY = 'Parity-Ethereum' GANACHE = 'EthereumJS TestRPC' + ETHEREUM_TESTER = 'EthereumTester' # (PyEVM) + CLEF = 'Clef' # Signer-only + + PEERING_TIMEOUT = 30 # seconds SYNC_TIMEOUT_DURATION = 60 # seconds to wait for various blockchain syncing endeavors - PEERING_TIMEOUT = 30 - SYNC_SLEEP_DURATION = 5 + SYNC_SLEEP_DURATION = 5 # seconds class ConnectionNotEstablished(RuntimeError): pass @@ -124,6 +126,9 @@ class Web3Client: # Test Clients cls.GANACHE: GanacheClient, cls.ETHEREUM_TESTER: EthereumTesterClient, + + # Singers + cls.CLEF: ClefClient } try: @@ -132,6 +137,9 @@ class Web3Client: ClientSubclass = clients[node_technology] except (ValueError, IndexError): + # check if this is a clef signer TODO: move this? + if 'clef' in getattr(w3.provider, 'ipc_path', ''): + return ClefClient(w3=w3) raise ValueError(f"Invalid client version string. Got '{w3.clientVersion}'") except KeyError: @@ -355,6 +363,41 @@ class GethClient(Web3Client): return self.w3.manager.request_blocking("personal_listWallets", []) +class ClefClient: + + def __init__(self, w3): + self.w3 = w3 + self.log = Logger(self.__class__.__name__) + + def is_connected(self): + return True + + def accounts(self) -> List[str]: + normalized_addresses = self.w3.manager.request_blocking("account_list", []) + checksum_addresses = [to_checksum_address(addr) for addr in normalized_addresses] + return checksum_addresses + + def sign_transaction(self, transaction: dict): + # FIXME: SO LAME + transaction['value'] = self.w3.toHex(transaction['value']) + transaction['gas'] = self.w3.toHex(transaction['gas']) + transaction['gasPrice'] = self.w3.toHex(transaction['gasPrice']) + transaction['chainId'] = self.w3.toHex(transaction['chainId']) + transaction['nonce'] = self.w3.toHex(transaction['nonce']) + transaction['from'] = to_normalized_address(transaction['from']) + signed = self.w3.manager.request_blocking("account_signTransaction", [transaction]) + return signed.raw + + def sign_message(self, account: str, message: bytes) -> str: + return self.w3.manager.request_blocking("account_signData", [message]) + + def unlock_account(self) -> bool: + return True + + def lock_account(self): + return True + + class ParityClient(Web3Client): @property diff --git a/nucypher/cli/commands/stake.py b/nucypher/cli/commands/stake.py index 2c0fcb7e6..06ca76021 100644 --- a/nucypher/cli/commands/stake.py +++ b/nucypher/cli/commands/stake.py @@ -150,7 +150,7 @@ class StakerOptions: self.config_options = config_options self.staking_address = staking_address - def create_character(self, emitter, config_file, individual_allocation=None, initial_address=None, keyfiles=None): + def create_character(self, emitter, config_file, initial_address=None, *args, **kwargs): stakeholder_config = self.config_options.create_config(emitter, config_file) if initial_address is None: @@ -158,8 +158,7 @@ class StakerOptions: return stakeholder_config.produce( initial_address=initial_address, - individual_allocation=individual_allocation, - keyfiles=keyfiles + *args, **kwargs ) def get_blockchain(self): @@ -177,12 +176,13 @@ class TransactingStakerOptions: __option_name__ = 'transacting_staker_options' - def __init__(self, staker_options, hw_wallet, beneficiary_address, allocation_filepath, keyfile=None): + def __init__(self, staker_options, hw_wallet, beneficiary_address, allocation_filepath, keyfile, signer): self.staker_options = staker_options self.hw_wallet = hw_wallet self.beneficiary_address = beneficiary_address self.allocation_filepath = allocation_filepath self.keyfile = keyfile + self.signer = signer def create_character(self, emitter, config_file): @@ -223,7 +223,8 @@ class TransactingStakerOptions: config_file, individual_allocation=individual_allocation, initial_address=initial_address, - keyfiles=[self.keyfile] # TODO: Accept multiple? + keyfiles=[self.keyfile] if self.keyfile else None, # TODO: Accept multiple?, + signer=self.signer, ) def get_blockchain(self): @@ -242,8 +243,10 @@ group_transacting_staker_options = group_options( hw_wallet=option_hw_wallet, beneficiary_address=click.option('--beneficiary-address', help="Address of a pre-allocation beneficiary", type=EIP55_CHECKSUM_ADDRESS), allocation_filepath=click.option('--allocation-filepath', help="Path to individual allocation file", type=EXISTING_READABLE_FILE), - keyfile=click.option('--keyfile', default=None, type=EXISTING_READABLE_FILE) - ) + keyfile=click.option('--keyfile', default=None, type=EXISTING_READABLE_FILE), + signer=click.option('--signer', default=None, type=EXISTING_READABLE_FILE) + +) @click.group() diff --git a/nucypher/crypto/powers.py b/nucypher/crypto/powers.py index 6cf1775ba..7d916f951 100644 --- a/nucypher/crypto/powers.py +++ b/nucypher/crypto/powers.py @@ -116,14 +116,15 @@ class TransactingPower(CryptoPowerUp): def __init__(self, account: str, - provider_uri: str = None, + client=None, password: str = None, cache: bool = False, keyfile: str = None): """ Instantiates a TransactingPower for the given checksum_address. """ - self.blockchain = BlockchainInterfaceFactory.get_or_create_interface(provider_uri=provider_uri) + self.blockchain = BlockchainInterfaceFactory.get_interface() + self.__client = client or self.blockchain.client # injectable signer self.__account = account self.__password = password self.__unlocked = False @@ -138,7 +139,7 @@ class TransactingPower(CryptoPowerUp): return False try: # TODO: Temporary fix for #1128 and #1385. It's ugly af, but it works. Move somewhere else? - wallets = self.blockchain.client.wallets + wallets = self.__client.wallets except AttributeError: return False else: @@ -176,7 +177,7 @@ class TransactingPower(CryptoPowerUp): try: with open(self.__keyfile) as keyfile: encrypted_key = keyfile.read() - private_key = self.blockchain.client.w3.eth.account.decrypt(encrypted_key, password) + private_key = self.__client.w3.eth.account.decrypt(encrypted_key, password) except FileNotFoundError: raise # TODO except Exception: @@ -195,7 +196,7 @@ class TransactingPower(CryptoPowerUp): elif self.is_local: self.__key = None else: - self.blockchain.client.lock_account(address=self.account) + self.__client.lock_account(address=self.account) self.__unlocked = False return self.__unlocked @@ -206,7 +207,7 @@ class TransactingPower(CryptoPowerUp): elif self.is_local: unlocked = self.__import_keyfile(password=password) else: - if self.blockchain.client is NO_BLOCKCHAIN_CONNECTION: + if self.__client is NO_BLOCKCHAIN_CONNECTION: raise self.NoBlockchainConnection unlocked = self.blockchain.client.unlock_account(address=self.account, password=password, duration=duration) self.__unlocked = unlocked @@ -232,7 +233,7 @@ class TransactingPower(CryptoPowerUp): signed_transaction = w3.eth.account.sign_transaction(transaction_dict=unsigned_transaction, private_key=self.__key) signed_raw_transaction = signed_transaction['rawTransaction'] else: - signed_raw_transaction = self.blockchain.client.sign_transaction(transaction=unsigned_transaction) + signed_raw_transaction = self.__client.sign_transaction(transaction=unsigned_transaction) return signed_raw_transaction def __enter__(self):