Skip to content

Commit

Permalink
Merge pull request #1104 from eth-brownie/feat/clef
Browse files Browse the repository at this point in the history
Support for clef as an account signer
  • Loading branch information
iamdefinitelyahuman authored Jun 4, 2021
2 parents 981f19b + 413db3e commit d221fcc
Show file tree
Hide file tree
Showing 3 changed files with 172 additions and 26 deletions.
88 changes: 88 additions & 0 deletions brownie/network/account.py
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
#!/usr/bin/python3

import json
import sys
import threading
import time
from collections.abc import Iterator
Expand All @@ -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
Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -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"])
71 changes: 45 additions & 26 deletions docs/account-management.rst
Original file line number Diff line number Diff line change
Expand Up @@ -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 <id>
$ brownie accounts generate <id>

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 ``<id>``.

Expand All @@ -30,9 +30,9 @@ Importing from a Private Key

To add a new account via private key:

::
::

$ brownie accounts new <id>
$ brownie accounts new <id>

You will be asked to input the private key, and to choose a password. The account will then be available as ``<id>``.

Expand All @@ -41,9 +41,9 @@ Importing from a Keystore

You can import an existing JSON keystore into Brownie using the commandline:

::
::

$ brownie accounts import <id> <path>
$ brownie accounts import <id> <path>

Once imported the account is available as ``<id>``.

Expand All @@ -52,9 +52,9 @@ Exporting a Keystore

To export an existing account as a JSON keystore file:

::
::

$ brownie accounts export <id> <path>
$ brownie accounts export <id> <path>

The exported account will be saved at ``<path>``.

Expand All @@ -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 <Accounts.load>` method:

.. code-block:: python
.. code-block:: python
>>> accounts
[]
>>> accounts.load(id)
>>> accounts.load('my_account')
Enter the password for this account:
<LocalAccount object '0xa9c2DD830DfFE8934fEb0A93BAbcb6e823e1FF05'>
>>> accounts
[<LocalAccount object '0xa9c2DD830DfFE8934fEb0A93BAbcb6e823e1FF05'>]
>>> accounts
[]
>>> accounts.load(id)
>>> accounts.load('my_account')
Enter the password for this account:
<LocalAccount object '0xa9c2DD830DfFE8934fEb0A93BAbcb6e823e1FF05'>
>>> accounts
[<LocalAccount object '0xa9c2DD830DfFE8934fEb0A93BAbcb6e823e1FF05'>]
Once the account is unlocked it will be available for use within the :func:`Accounts <brownie.network.account.Accounts>` container.
Expand All @@ -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<config>`:
.. 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 <brownie.network.account.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 <https://geth.ethereum.org/docs/clef/tutorial>`_, an account management tool included within `Geth <https://geth.ethereum.org/>`_.
To use a hardware wallet in Brownie, start by `installing Geth <https://geth.ethereum.org/docs/install-and-build/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 <brownie.network.account.ClefAccount>` from within clef.
39 changes: 39 additions & 0 deletions docs/api-network.rst
Original file line number Diff line number Diff line change
Expand Up @@ -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 <brownie.network.account.ClefAccount>` objects.
`Clef <https://geth.ethereum.org/docs/clef/tutorial>`_ 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
[<ClefAccount object '0x716E8419F2926d6AcE07442675F476ace972C580'>]
.. py:classmethod:: Accounts.disconnect_from_clef()
Disconnect from Clef.

Removes all :func:`ClefAccount <brownie.network.account.ClefAccount>` objects from the container.


Accounts Internal Methods
*************************

Expand Down Expand Up @@ -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 <brownie.network.account.Account>`. A ``ClefAccount`` object is used for accounts that have been unlocked via `clef <https://geth.ethereum.org/docs/clef/tutorial>`_, 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
[<ClefAccount object '0x716E8419F2926d6AcE07442675F476ace972C580'>]
PublicKeyAccount
----------------

Expand Down

0 comments on commit d221fcc

Please sign in to comment.