Skip to content

Commit

Permalink
Merge branch 'main' into poll-methods-redux
Browse files Browse the repository at this point in the history
  • Loading branch information
antazoey authored Dec 8, 2023
2 parents 5020556 + d033f85 commit f25e637
Show file tree
Hide file tree
Showing 14 changed files with 265 additions and 42 deletions.
26 changes: 19 additions & 7 deletions src/ape/api/accounts.py
Original file line number Diff line number Diff line change
@@ -1,9 +1,12 @@
from pathlib import Path
from typing import TYPE_CHECKING, Iterator, List, Optional, Type, Union
from typing import TYPE_CHECKING, Any, Iterator, List, Optional, Type, Union

import click
from eip712.messages import EIP712Message
from eip712.messages import SignableMessage as EIP712SignableMessage
from eth_account import Account
from eth_account.messages import encode_defunct
from hexbytes import HexBytes

from ape.api.address import BaseAddress
from ape.api.transactions import ReceiptAPI, TransactionAPI
Expand Down Expand Up @@ -54,18 +57,21 @@ def alias(self) -> Optional[str]:
return None

@abstractmethod
def sign_message(self, msg: SignableMessage) -> Optional[MessageSignature]:
def sign_message(self, msg: Any, **signer_options) -> Optional[MessageSignature]:
"""
Sign a message.
Args:
msg (:class:`~ape.types.signatures.SignableMessage`): The message to sign.
msg (Any): The message to sign. Account plugins can handle various types of messages.
For example, :class:`~ape_accounts.accouns.KeyfileAccount` can handle
:class:`~ape.types.signatures.SignableMessage`, str, int, and bytes.
See these
`docs <https://eth-account.readthedocs.io/en/stable/eth_account.html#eth_account.messages.SignableMessage>`__ # noqa: E501
for more type information on this type.
for more type information on the ``SignableMessage`` type.
**signer_options: Additional kwargs given to the signer to modify the signing operation.
Returns:
:class:`~ape.types.signatures.MessageSignature` (optional): The signed message.
:class:`~ape.types.signatures.MessageSignature` (optional): The signature corresponding to the message.
"""

@abstractmethod
Expand Down Expand Up @@ -258,7 +264,7 @@ def declare(self, contract: "ContractContainer", *args, **kwargs) -> ReceiptAPI:

def check_signature(
self,
data: Union[SignableMessage, TransactionAPI],
data: Union[SignableMessage, TransactionAPI, str, EIP712Message, int],
signature: Optional[MessageSignature] = None, # TransactionAPI doesn't need it
) -> bool:
"""
Expand All @@ -273,6 +279,12 @@ def check_signature(
Returns:
bool: ``True`` if the data was signed by this account. ``False`` otherwise.
"""
if isinstance(data, str):
data = encode_defunct(text=data)
elif isinstance(data, int):
data = encode_defunct(hexstr=HexBytes(data).hex())
elif isinstance(data, EIP712Message):
data = data.signable_message
if isinstance(data, (SignableMessage, EIP712SignableMessage)):
if signature:
return self.address == Account.recover_message(data, vrs=signature)
Expand Down Expand Up @@ -494,7 +506,7 @@ class ImpersonatedAccount(AccountAPI):
def address(self) -> AddressType:
return self.raw_address

def sign_message(self, msg: SignableMessage) -> Optional[MessageSignature]:
def sign_message(self, msg: Any, **signer_options) -> Optional[MessageSignature]:
raise NotImplementedError("This account cannot sign messages")

def sign_transaction(self, txn: TransactionAPI, **kwargs) -> Optional[TransactionAPI]:
Expand Down
4 changes: 4 additions & 0 deletions src/ape/api/providers.py
Original file line number Diff line number Diff line change
Expand Up @@ -824,6 +824,10 @@ def _sanitize_web3_url(msg: str) -> str:
parts = msg.split("URI: ")
prefix = parts[0].strip()
rest = parts[1].split(" ")

# * To remove the `,` from the url http://127.0.0.1:8545,
if "," in rest[0]:
rest[0] = rest[0].rstrip(",")
sanitized_url = sanitize_url(rest[0])
return f"{prefix} URI: {sanitized_url} {' '.join(rest[1:])}"

Expand Down
3 changes: 2 additions & 1 deletion src/ape/managers/project/manager.py
Original file line number Diff line number Diff line change
Expand Up @@ -752,7 +752,8 @@ def track_deployment(self, contract: ContractInstance):

if destination.is_file():
logger.debug("Deployment already tracked. Re-tracking.")
destination.unlink()
# NOTE: missing_ok=True to handle race condition.
destination.unlink(missing_ok=True)

destination.write_text(artifact.json())

Expand Down
38 changes: 37 additions & 1 deletion src/ape/types/signatures.py
Original file line number Diff line number Diff line change
@@ -1,8 +1,9 @@
from typing import Iterator, Union
from typing import Iterator, Optional, Union

from eth_account import Account
from eth_account.messages import SignableMessage
from eth_utils import to_bytes, to_hex
from hexbytes import HexBytes
from pydantic.dataclasses import dataclass

from ape.types import AddressType
Expand All @@ -13,6 +14,41 @@
)


# Improve repr to force hexstr for body instead of raw bytes.
def signable_message_repr(msg) -> str:
name = getattr(SignableMessage, "__name__", "SignableMessage")
default_value = "<unknown!>" # Shouldn't happen
version_str = _bytes_to_human_str(msg.version) or default_value
header_str = _bytes_to_human_str(msg.header) or default_value
body_str = _bytes_to_human_str(msg.body) or default_value
return f"{name}(" f'version="{version_str}", header="{header_str}", body="{body_str}")'


SignableMessage.__repr__ = signable_message_repr # type: ignore[method-assign]


def _bytes_to_human_str(bytes_value: bytes) -> Optional[str]:
try:
# Try as text
return bytes_value.decode("utf8")
except Exception:
pass

try:
# Try as hex
return HexBytes(bytes_value).hex()
except Exception:
pass

try:
# Try normal str
return str(bytes_value)
except Exception:
pass

return None


def _left_pad_bytes(val: bytes, num_bytes: int) -> bytes:
return b"\x00" * (num_bytes - len(val)) + val if len(val) < num_bytes else val

Expand Down
58 changes: 54 additions & 4 deletions src/ape_accounts/accounts.py
Original file line number Diff line number Diff line change
@@ -1,10 +1,12 @@
import json
from os import environ
from pathlib import Path
from typing import Iterator, Optional
from typing import Any, Dict, Iterator, Optional

import click
from eip712.messages import EIP712Message
from eth_account import Account as EthAccount
from eth_account.messages import encode_defunct
from eth_keys import keys # type: ignore
from eth_utils import to_bytes
from ethpm_types import HexBytes
Expand All @@ -25,6 +27,8 @@ def __init__(self):


class AccountContainer(AccountContainerAPI):
loaded_accounts: Dict[str, "KeyfileAccount"] = {}

@property
def _keyfiles(self) -> Iterator[Path]:
return self.data_folder.glob("*.json")
Expand All @@ -37,7 +41,11 @@ def aliases(self) -> Iterator[str]:
@property
def accounts(self) -> Iterator[AccountAPI]:
for keyfile in self._keyfiles:
yield KeyfileAccount(keyfile_path=keyfile)
if keyfile.stem not in self.loaded_accounts:
keyfile_account = KeyfileAccount(keyfile_path=keyfile)
self.loaded_accounts[keyfile.stem] = keyfile_account

yield self.loaded_accounts[keyfile.stem]

def __len__(self) -> int:
return len([*self._keyfiles])
Expand Down Expand Up @@ -149,8 +157,50 @@ def delete(self):
self.__decrypt_keyfile(passphrase)
self.keyfile_path.unlink()

def sign_message(self, msg: SignableMessage) -> Optional[MessageSignature]:
user_approves = self.__autosign or click.confirm(f"{msg}\n\nSign: ")
def sign_message(self, msg: Any, **signer_options) -> Optional[MessageSignature]:
user_approves = False

if isinstance(msg, str):
user_approves = self.__autosign or click.confirm(f"Message: {msg}\n\nSign: ")
msg = encode_defunct(text=msg)
elif isinstance(msg, int):
user_approves = self.__autosign or click.confirm(f"Message: {msg}\n\nSign: ")
msg = encode_defunct(hexstr=HexBytes(msg).hex())
elif isinstance(msg, bytes):
user_approves = self.__autosign or click.confirm(f"Message: {msg.hex()}\n\nSign: ")
msg = encode_defunct(primitive=msg)
elif isinstance(msg, EIP712Message):
# Display message data to user
display_msg = "Signing EIP712 Message\n"

# Domain Data
display_msg += "Domain\n"
if msg._name_:
display_msg += f"\tName: {msg._name_}\n"
if msg._version_:
display_msg += f"\tVersion: {msg._version_}\n"
if msg._chainId_:
display_msg += f"\tChain ID: {msg._chainId_}\n"
if msg._verifyingContract_:
display_msg += f"\tContract: {msg._verifyingContract_}\n"
if msg._salt_:
display_msg += f"\tSalt: 0x{msg._salt_.hex()}\n"

# Message Data
display_msg += "Message\n"
for field, value in msg._body_["message"].items():
display_msg += f"\t{field}: {value}\n"

user_approves = self.__autosign or click.confirm(f"{display_msg}\nSign: ")

# Convert EIP712Message to SignableMessage for handling below
msg = msg.signable_message
elif isinstance(msg, SignableMessage):
user_approves = self.__autosign or click.confirm(f"{msg}\n\nSign: ")
else:
logger.warning("Unsupported message type, (type=%r, msg=%r)", type(msg), msg)
return None

if not user_approves:
return None

Expand Down
15 changes: 13 additions & 2 deletions src/ape_ethereum/ecosystem.py
Original file line number Diff line number Diff line change
Expand Up @@ -639,7 +639,9 @@ def create_transaction(self, **kwargs) -> TransactionAPI:
if isinstance(kwargs.get("chainId"), str):
kwargs["chainId"] = int(kwargs["chainId"], 16)

elif "chainId" not in kwargs and self.network_manager.active_provider is not None:
elif (
"chainId" not in kwargs or kwargs["chainId"] is None
) and self.network_manager.active_provider is not None:
kwargs["chainId"] = self.provider.chain_id

if "input" in kwargs:
Expand All @@ -660,7 +662,16 @@ def create_transaction(self, **kwargs) -> TransactionAPI:
kwargs["gas"] = kwargs.pop("gas_limit", kwargs.get("gas"))

if "value" in kwargs and not isinstance(kwargs["value"], int):
kwargs["value"] = self.conversion_manager.convert(kwargs["value"], int)
value = kwargs["value"] or 0 # Convert None to 0.
kwargs["value"] = self.conversion_manager.convert(value, int)

# This causes problems in pydantic for some reason.
if "gas_price" in kwargs and kwargs["gas_price"] is None:
del kwargs["gas_price"]

# None is not allowed, the user likely means `b""`.
if "data" in kwargs and kwargs["data"] is None:
kwargs["data"] = b""

return txn_class(**kwargs)

Expand Down
5 changes: 4 additions & 1 deletion src/ape_pm/compiler.py
Original file line number Diff line number Diff line change
Expand Up @@ -31,12 +31,15 @@ def compile(
get_relative_path(path, base_path) if base_path and path.is_absolute() else path
)
source_id = str(source_path)
code = path.read_text()
if not code:
continue

try:
# NOTE: Always set the source ID to the source of the JSON file
# to avoid manifest corruptions later on.
contract_type = self.compile_code(
path.read_text(),
code,
base_path=base_path,
sourceId=source_id,
)
Expand Down
29 changes: 20 additions & 9 deletions src/ape_test/accounts.py
Original file line number Diff line number Diff line change
@@ -1,8 +1,9 @@
from typing import Iterator, List, Optional
from typing import Any, Iterator, List, Optional

from eth_account import Account as EthAccount
from eth_account.messages import SignableMessage
from eth_account.messages import SignableMessage, encode_defunct
from eth_utils import to_bytes
from hexbytes import HexBytes

from ape.api import TestAccountAPI, TestAccountContainerAPI, TransactionAPI
from ape.types import AddressType, MessageSignature, TransactionSignature
Expand Down Expand Up @@ -101,13 +102,23 @@ def alias(self) -> str:
def address(self) -> AddressType:
return self.network_manager.ethereum.decode_address(self.address_str)

def sign_message(self, msg: SignableMessage) -> Optional[MessageSignature]:
signed_msg = EthAccount.sign_message(msg, self.private_key)
return MessageSignature(
v=signed_msg.v,
r=to_bytes(signed_msg.r),
s=to_bytes(signed_msg.s),
)
def sign_message(self, msg: Any, **signer_options) -> Optional[MessageSignature]:
# Convert str and int to SignableMessage if needed
if isinstance(msg, str):
msg = encode_defunct(text=msg)
elif isinstance(msg, int):
msg = HexBytes(msg).hex()
msg = encode_defunct(hexstr=msg)

# Process SignableMessage
if isinstance(msg, SignableMessage):
signed_msg = EthAccount.sign_message(msg, self.private_key)
return MessageSignature(
v=signed_msg.v,
r=to_bytes(signed_msg.r),
s=to_bytes(signed_msg.s),
)
return None

def sign_transaction(self, txn: TransactionAPI, **kwargs) -> Optional[TransactionAPI]:
# Signs anything that's given to it
Expand Down
2 changes: 2 additions & 0 deletions tests/conftest.py
Original file line number Diff line number Diff line change
Expand Up @@ -309,6 +309,8 @@ def empty_data_folder():
@pytest.fixture
def keyfile_account(owner, keyparams, temp_accounts_path, temp_keyfile_account_ctx):
with temp_keyfile_account_ctx(temp_accounts_path, ALIAS, keyparams, owner) as account:
# Ensure starts off locked.
account.lock()
yield account


Expand Down
2 changes: 2 additions & 0 deletions tests/functional/conftest.py
Original file line number Diff line number Diff line change
Expand Up @@ -173,6 +173,8 @@ def address():
@pytest.fixture
def second_keyfile_account(sender, keyparams, temp_accounts_path, temp_keyfile_account_ctx):
with temp_keyfile_account_ctx(temp_accounts_path, ALIAS_2, keyparams, sender) as account:
# Ensure starts off locked.
account.lock()
yield account


Expand Down
Loading

0 comments on commit f25e637

Please sign in to comment.