diff --git a/brownie/network/account.py b/brownie/network/account.py index f8d5bae2c..7277a56a9 100644 --- a/brownie/network/account.py +++ b/brownie/network/account.py @@ -1,6 +1,7 @@ #!/usr/bin/python3 import json +import sys import threading import time from collections.abc import Iterator @@ -12,7 +13,9 @@ import eth_keys import rlp from eth_utils import keccak +from eth_utils.applicators import apply_formatters_to_dict from hexbytes import HexBytes +from web3 import HTTPProvider, IPCProvider from brownie._config import CONFIG, _get_data_folder from brownie._singleton import _Singleton @@ -267,6 +270,54 @@ def clear(self) -> None: """ self._accounts.clear() + def connect_to_clef(self, uri: str = None, timeout: int = 120) -> None: + """ + Connect to Clef and import open accounts. + + Clef is an account signing utility packaged with Geth, which can be + used to interact with HW wallets in Brownie. Before calling this + function, Clef must be running in another command prompt. + + Arguments + --------- + uri : str + IPC path or http url to use to connect to clef. If None is given, + uses the default IPC path on Unix systems or localhost on Windows. + timeout : int + The number of seconds to wait on a clef request before raising a + timeout exception. + """ + provider = None + if uri is None: + if sys.platform == "win32": + uri = "http://localhost:8550/" + else: + uri = Path.home().joinpath(".clef/clef.ipc").as_posix() + try: + if Path(uri).exists(): + provider = IPCProvider(uri, timeout=timeout) + except OSError: + if uri is not None and uri.startswith("http"): + provider = HTTPProvider(uri, {"timeout": timeout}) + if provider is None: + raise ValueError("Unknown URI, must be IPC socket path or URL starting with 'http'") + + response = provider.make_request("account_list", []) + if "error" in response: + raise ValueError(response["error"]["message"]) + + for address in response["result"]: + if to_address(address) not in self._accounts: + self._accounts.append(ClefAccount(address, provider)) + + def disconnect_from_clef(self) -> None: + """ + Disconnect from Clef. + + Removes all `ClefAccount` objects from the container. + """ + self._accounts = [i for i in self._accounts if not isinstance(i, ClefAccount)] + class PublicKeyAccount: """Class for interacting with an Ethereum account where you do not control @@ -769,3 +820,40 @@ def _transact(self, tx: Dict, allow_revert: bool) -> None: tx["chainId"] = web3.chain_id signed_tx = self._acct.sign_transaction(tx).rawTransaction # type: ignore return web3.eth.send_raw_transaction(signed_tx) + + +class ClefAccount(_PrivateKeyAccount): + + """ + Class for interacting with an Ethereum account where signing is handled in Clef. + """ + + def __init__(self, address: str, provider: Union[HTTPProvider, IPCProvider]) -> None: + self._provider = provider + super().__init__(address) + + def _transact(self, tx: Dict, allow_revert: bool) -> None: + if allow_revert is None: + allow_revert = bool(CONFIG.network_type == "development") + if not allow_revert: + self._check_for_revert(tx) + + formatters = { + "nonce": web3.toHex, + "gasPrice": web3.toHex, + "gas": web3.toHex, + "value": web3.toHex, + "chainId": web3.toHex, + "data": web3.toHex, + "from": to_address, + } + if "to" in tx: + formatters["to"] = to_address + + tx["chainId"] = web3.chain_id + tx = apply_formatters_to_dict(formatters, tx) + + response = self._provider.make_request("account_signTransaction", [tx]) + if "error" in response: + raise ValueError(response["error"]["message"]) + return web3.eth.send_raw_transaction(response["result"]["raw"]) diff --git a/docs/account-management.rst b/docs/account-management.rst index 7d6832a4f..c5560fcc9 100644 --- a/docs/account-management.rst +++ b/docs/account-management.rst @@ -10,18 +10,18 @@ When we use the term `local` it implies that the account exists locally on your You can manage your locally available accounts via the commandline: -:: + :: - $ brownie accounts + $ brownie accounts Generating a New Account ======================== To generate a new account using the command line: -:: + :: - $ brownie accounts generate + $ brownie accounts generate You will be asked to choose a password for the account. Brownie will then generate a random private key, and make the account available as ````. @@ -30,9 +30,9 @@ Importing from a Private Key To add a new account via private key: -:: + :: - $ brownie accounts new + $ brownie accounts new You will be asked to input the private key, and to choose a password. The account will then be available as ````. @@ -41,9 +41,9 @@ Importing from a Keystore You can import an existing JSON keystore into Brownie using the commandline: -:: + :: - $ brownie accounts import + $ brownie accounts import Once imported the account is available as ````. @@ -52,9 +52,9 @@ Exporting a Keystore To export an existing account as a JSON keystore file: -:: + :: - $ brownie accounts export + $ brownie accounts export The exported account will be saved at ````. @@ -63,16 +63,16 @@ Unlocking Accounts In order to access a local account from a script or console, you must first unlock it. This is done via the :func:`Accounts.load ` method: -.. code-block:: python + .. code-block:: python - >>> accounts - [] - >>> accounts.load(id) - >>> accounts.load('my_account') - Enter the password for this account: - - >>> accounts - [] + >>> accounts + [] + >>> accounts.load(id) + >>> accounts.load('my_account') + Enter the password for this account: + + >>> accounts + [] Once the account is unlocked it will be available for use within the :func:`Accounts ` container. @@ -82,14 +82,33 @@ Unlocking Accounts on Development Networks On a local or forked development network you can unlock and use any account, even if you don't have the corresponding private key. To do so, add the account to the ``unlock`` setting in a project's :ref:`configuration file`: -.. code-block:: yaml + .. code-block:: yaml - networks: - development: - cmd_settings: - unlock: - - 0x7E1E3334130355799F833ffec2D731BCa3E68aF6 - - 0x0063046686E46Dc6F15918b61AE2B121458534a5 + networks: + development: + cmd_settings: + unlock: + - 0x7E1E3334130355799F833ffec2D731BCa3E68aF6 + - 0x0063046686E46Dc6F15918b61AE2B121458534a5 The unlocked accounts are automatically added to the :func:`Accounts ` container. Note that you might need to fund the unlocked accounts manually. + +Using a Hardware Wallet +======================= + +Brownie allows the use of hardware wallets via `Clef `_, an account management tool included within `Geth `_. + +To use a hardware wallet in Brownie, start by `installing Geth `_. Once finished, type the following command and follow the on-screen prompts to set of Clef: + + :: + + clef init + +Once Clef is configured, run Brownie in one command prompt and Clef in another. From within Brownie: + + .. code-block:: python + + >>> accounts.connect_to_clef() + +Again, follow the prompts in Clef to unlock the accounts in Brownie. You can now use the unlocked accounts as you would any other account. Note that you will have to authorize each transaction made with a :func:`ClefAccount ` from within clef. diff --git a/docs/api-network.rst b/docs/api-network.rst index eb163bb41..e514ffac2 100644 --- a/docs/api-network.rst +++ b/docs/api-network.rst @@ -228,6 +228,30 @@ Accounts Methods >>> accounts.remove('0xc1826925377b4103cC92DeeCDF6F96A03142F37a') +.. py:classmethod:: Accounts.connect_to_clef(uri=None, timeout=120) + + Connect to clef and add unlocked accounts to the container as :func:`ClefAccount ` objects. + + `Clef `_ is an account signing utility packaged with Geth, which can be used to interact with hardware wallets in Brownie. Before calling this function, Clef must be running and unlocked in another command prompt. + + * ``uri``: IPC path or http url to use to connect to clef. If ``None``, uses clef's default IPC path on Unix systems or ``http://localhost:8550/`` on Windows. + * ``timeout``: The number of seconds to wait for clef to respond to a request before raising a ``TimeoutError``. + + .. code-block:: python + + >>> accounts + [] + >>> accounts.connect_to_clef() + >>> accounts + [] + +.. py:classmethod:: Accounts.disconnect_from_clef() + + Disconnect from Clef. + + Removes all :func:`ClefAccount ` objects from the container. + + Accounts Internal Methods ************************* @@ -455,6 +479,21 @@ LocalAccount Methods Enter the password to encrypt this account with: /home/computer/my_account.json +ClefAccount +------------ + +.. py:class:: brownie.network.account.ClefAccount + + Functionally identical to :func:`Account `. A ``ClefAccount`` object is used for accounts that have been unlocked via `clef `_, and where signing of transactions is handled externally from brownie. This is useful for hardware wallets. + + .. code-block:: python + + >>> accounts + [] + >>> accounts.connect_to_clef() + >>> accounts + [] + PublicKeyAccount ----------------