Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: refactor for zksync abstraction #195

Merged
merged 50 commits into from
Jun 12, 2024
Merged
Show file tree
Hide file tree
Changes from 43 commits
Commits
Show all changes
50 commits
Select commit Hold shift + click to select a range
3be0754
Constructor has no method ID
DanielSchiavini Apr 10, 2024
ac35cda
Docstring fix and more memory
DanielSchiavini Apr 12, 2024
cc5746c
Merge branch '104/refactor-pyevm' into zksync
DanielSchiavini Apr 12, 2024
3921a08
Merge branch '104/refactor-pyevm' into zksync
DanielSchiavini Apr 15, 2024
8f4f660
Sign the full typed data
DanielSchiavini Apr 15, 2024
1ff3c18
generate_bytecode function
DanielSchiavini Apr 16, 2024
16de424
Move get_chain_id to NetworkEnv
DanielSchiavini Apr 16, 2024
32deb48
fix eth_signTypedData_v4 in Colab
DanielSchiavini Apr 16, 2024
80b3e20
Revert "generate_bytecode function"
DanielSchiavini Apr 17, 2024
ab90622
Self-review
DanielSchiavini Apr 17, 2024
a29bd5c
Update tests
DanielSchiavini Apr 17, 2024
20ab1f5
Move compile to environment
DanielSchiavini Apr 17, 2024
2c85b4d
Merge branch 'master' of github.com:vyperlang/titanoboa into zksync
DanielSchiavini Apr 17, 2024
ddf553c
Unused function
DanielSchiavini Apr 17, 2024
ce99613
Fix tests
DanielSchiavini Apr 17, 2024
37e0997
Add optional compiler_data to _BaseEvmContract and ABI contract
DanielSchiavini Apr 22, 2024
9565ce0
fix error check
DanielSchiavini Apr 22, 2024
6035c0c
Patch chain_id
DanielSchiavini Apr 24, 2024
3d335d4
Extra some utilities, stacktrace improvements
DanielSchiavini Apr 29, 2024
853d550
Improve stacktrace
DanielSchiavini Apr 29, 2024
debb3c8
Allow customizing the deployer
DanielSchiavini Apr 29, 2024
ae7e6ad
Merge branch 'master' of github.com:vyperlang/titanoboa into zksync
DanielSchiavini Apr 29, 2024
1ff4e28
Review comment
DanielSchiavini Apr 29, 2024
b43481b
Fix tests
DanielSchiavini Apr 30, 2024
309a688
js fix
DanielSchiavini May 1, 2024
08fe81d
Improve error parsing
DanielSchiavini May 3, 2024
2dfdf53
tests
DanielSchiavini May 6, 2024
ee260b4
Merge branch 'master' of github.com:vyperlang/titanoboa into zksync
DanielSchiavini May 6, 2024
1c83d70
Merge branch 'master' of github.com:vyperlang/titanoboa into zksync
DanielSchiavini May 13, 2024
f09af27
Keep last computation in contract
DanielSchiavini May 14, 2024
79ff1b7
Another issue with str frames
DanielSchiavini May 14, 2024
517e7df
Merge branch 'master' of github.com:vyperlang/titanoboa into zksync
DanielSchiavini May 21, 2024
571889e
Update eth-account
DanielSchiavini May 21, 2024
f0a2539
Merge branch 'master' of github.com:vyperlang/titanoboa into zksync
DanielSchiavini May 27, 2024
d788747
Merge branch 'master' of github.com:vyperlang/titanoboa into zksync
DanielSchiavini May 31, 2024
821d856
Review comments
DanielSchiavini May 31, 2024
0631a5d
Proper branch
DanielSchiavini May 31, 2024
16da97a
Merge branch 'master' of github.com:vyperlang/titanoboa into zksync
DanielSchiavini Jun 3, 2024
f96fff3
Make filename public
DanielSchiavini Jun 3, 2024
cf54096
Get rid of `set_deployer_class`, add optional `deployer_class` to the…
DanielSchiavini Jun 4, 2024
e0a8644
Proper branch
DanielSchiavini Jun 5, 2024
330dda0
Merge branch 'master' of github.com:vyperlang/titanoboa into zksync
DanielSchiavini Jun 10, 2024
bb840ca
Review comments
DanielSchiavini Jun 10, 2024
ac57b4d
Make name optional
DanielSchiavini Jun 12, 2024
2c4e55b
Move functions back to the factory method
DanielSchiavini Jun 12, 2024
9ec7a09
Remove compiler data
DanielSchiavini Jun 12, 2024
e7909cd
functions cached_property
DanielSchiavini Jun 12, 2024
92e29aa
Handle constructor name in errors
DanielSchiavini Jun 12, 2024
433e76c
Get rid of __init__ str
DanielSchiavini Jun 12, 2024
5f48854
Fix abi test
DanielSchiavini Jun 12, 2024
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
58 changes: 41 additions & 17 deletions boa/contracts/abi/abi_contract.py
Original file line number Diff line number Diff line change
@@ -1,7 +1,6 @@
from collections import defaultdict
from copy import deepcopy
from functools import cached_property
from os.path import basename
from typing import Any, Optional, Union
from warnings import warn

Expand Down Expand Up @@ -34,7 +33,8 @@ def __init__(self, abi: dict, contract_name: str):

@property
def name(self) -> str:
return self._abi["name"]
# note: the `constructor` definition does not have a name
return self._abi.get("name", "")
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

yea i don't think we should use "" as a default here, i think we should return Optional and just handle the None case at the call sites. like we will get stuff like "" as a key in the overloads dictionary, which seems funky.

or if we really want a name, we can use "__init__" (or "constructor" as i think you originally had).

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

yeah constructor was the first workaround I thought of, because it's just clear and simple.
handling it at the call sites is fine, but it creates a lot of boiler plate code


@cached_property
def argument_types(self) -> list:
Expand Down Expand Up @@ -62,6 +62,8 @@ def pretty_signature(self) -> str:

@cached_property
def method_id(self) -> bytes:
if self._abi["type"] == "constructor":
return b"" # constructors don't have method IDs
Copy link
Member

@charles-cooper charles-cooper Jun 11, 2024

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

i think this should be handled at the caller really (as is done in vyper_contract.py)

return method_id(self.name + self.signature)

def __repr__(self) -> str:
Expand Down Expand Up @@ -215,13 +217,16 @@ class ABIContract(_BaseEVMContract):
def __init__(
self,
name: str,
abi: dict,
abi: list[dict],
functions: list[ABIFunction],
address: Address,
filename: Optional[str] = None,
env=None,
compiler_data: Optional[Any] = None,
):
super().__init__(env, filename=filename, address=address)
super().__init__(
env, filename=filename, address=address, compiler_data=compiler_data
)
self._name = name
self._abi = abi
self._functions = functions
Expand All @@ -241,6 +246,7 @@ def __init__(
setattr(self, name, ABIOverload.create(group, self))

self._address = Address(address)
self._computation: Optional[ComputationAPI] = None

@property
def abi(self):
Expand All @@ -260,6 +266,7 @@ def marshal_to_python(self, computation, abi_type: list[str]) -> tuple[Any, ...]
:param computation: the computation object returned by `execute_code`
:param abi_type: the ABI type of the return value.
"""
self._computation = computation
# when there's no contract in the address, the computation output is empty
if computation.is_error:
return self.handle_error(computation)
Expand All @@ -274,13 +281,18 @@ def stack_trace(self, computation: ComputationAPI) -> StackTrace:
"""
Create a stack trace for a failed contract call.
"""
reason = ""
# revert without reason has args=[None], so we don't want to include that
if computation.is_error and any(computation.error.args):
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

what does any(computation.error.args) do?

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

In case of a revert from an assert without reason, computation.error.args will be [None]. In that case it's better to leave reason=""

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

would it be better to filter those on the next line?

reason = " ".join(str(arg) for arg in computation.error.args if arg is not None)

reason = " ".join(str(arg) for arg in computation.error.args)

calldata_method_id = bytes(computation.msg.data[:4])
if calldata_method_id in self.method_id_map:
function = self.method_id_map[calldata_method_id]
msg = f" ({self}.{function.pretty_signature})"
msg = f" {reason}({self}.{function.pretty_signature})"
else:
# Method might not be specified in the ABI
msg = f" (unknown method id {self}.0x{calldata_method_id.hex()})"
msg = f" {reason}(unknown method id {self}.0x{calldata_method_id.hex()})"

return_trace = StackTrace([msg])
return _handle_child_trace(computation, self.env, return_trace)
Expand All @@ -290,7 +302,12 @@ def deployer(self) -> "ABIContractFactory":
"""
Returns a factory that can be used to retrieve another deployed contract.
"""
return ABIContractFactory(self._name, self._abi, self._functions)
return ABIContractFactory(
self._name,
self._abi,
filename=self.filename,
compiler_data=self.compiler_data,
)

def __repr__(self):
file_str = f" (file {self.filename})" if self.filename else ""
Expand All @@ -308,33 +325,40 @@ class ABIContractFactory:
def __init__(
self,
name: str,
abi: dict,
functions: list[ABIFunction],
abi: list[dict],
filename: Optional[str] = None,
compiler_data: Optional[Any] = None,
):
self._name = name
self._abi = abi
self._functions = functions
self._filename = filename
self._functions = [
ABIFunction(item, name) for item in abi if item.get("type") == "function"
]
self.filename = filename
self.compiler_data = compiler_data

@cached_property
def abi(self):
return deepcopy(self._abi)

@classmethod
def from_abi_dict(cls, abi, name="<anonymous contract>"):
functions = [
ABIFunction(item, name) for item in abi if item.get("type") == "function"
]
return cls(basename(name), abi, functions, filename=name)
def from_abi_dict(
cls, abi, name="<anonymous contract>", filename=None, compiler_data=None
):
return cls(name, abi, filename, compiler_data)

def at(self, address: Address | str) -> ABIContract:
"""
Create an ABI contract object for a deployed contract at `address`.
"""
address = Address(address)
contract = ABIContract(
self._name, self._abi, self._functions, address, self._filename
self._name,
self._abi,
self._functions,
address,
self.filename,
compiler_data=self.compiler_data,
)
contract.env.register_contract(address, contract)
return contract
Expand Down
11 changes: 6 additions & 5 deletions boa/contracts/base_evm_contract.py
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
from dataclasses import dataclass
from typing import Optional
from typing import Any, Optional

from eth.abc import ComputationAPI

Expand All @@ -19,10 +19,12 @@ def __init__(
env: Optional[Env] = None,
filename: Optional[str] = None,
address: Optional[Address] = None,
compiler_data: Optional[Any] = None,
):
self.env = env or Env.get_singleton()
self._address = address # this can be overridden by subclasses
self.filename = filename
self.compiler_data = compiler_data

def stack_trace(self, computation: ComputationAPI):
raise NotImplementedError
Expand Down Expand Up @@ -61,10 +63,9 @@ def last_frame(self):


def _trace_for_unknown_contract(computation, env):
ret = StackTrace(
[f"<Unknown location in unknown contract {computation.msg.code_address.hex()}>"]
)
return _handle_child_trace(computation, env, ret)
err = f" <Unknown contract 0x{computation.msg.code_address.hex()}>"
trace = StackTrace([err])
return _handle_child_trace(computation, env, trace)


def _handle_child_trace(computation, env, return_trace):
Expand Down
12 changes: 12 additions & 0 deletions boa/contracts/vyper/vyper_contract.py
Original file line number Diff line number Diff line change
Expand Up @@ -74,6 +74,8 @@


class VyperDeployer:
create_compiler_data = CompilerData # this may be a different class in plugins

def __init__(self, compiler_data, filename=None):
self.compiler_data = compiler_data

Expand Down Expand Up @@ -303,6 +305,11 @@ def _check(cond, msg=""):
assert len(args) == 1, "multiple args!"
assert len(kwargs) == 0, "can't mix args and kwargs!"
err = args[0]
if isinstance(frame, str):
# frame for unknown contracts is a string
_check(err in frame, f"{frame} does not match {args}")
return

# try to match anything
_check(
err == frame.pretty_vm_reason
Expand All @@ -315,6 +322,10 @@ def _check(cond, msg=""):
# try to match a specific kwarg
assert len(kwargs) == 1 and len(args) == 0

if isinstance(frame, str):
charles-cooper marked this conversation as resolved.
Show resolved Hide resolved
# frame for unknown contracts is a string
raise ValueError(f"expected {kwargs} but got {frame}")

# don't accept magic
if frame.dev_reason:
assert frame.dev_reason.reason_type not in ("vm_error", "compiler")
Expand Down Expand Up @@ -547,6 +558,7 @@ def _set_bytecode(self, bytecode: bytes) -> None:
to_check = bytecode
if self.data_section_size != 0:
to_check = bytecode[: -self.data_section_size]
assert isinstance(self.compiler_data, CompilerData)
if to_check != self.compiler_data.bytecode_runtime:
warnings.warn(
f"casted bytecode does not match compiled bytecode at {self}",
Expand Down
53 changes: 31 additions & 22 deletions boa/integrations/jupyter/browser.py
Original file line number Diff line number Diff line change
Expand Up @@ -74,21 +74,16 @@ def send_transaction(self, tx_data: dict) -> dict:
)
return convert_frontend_dict(sign_data)

def sign_typed_data(
self, domain: dict[str, Any], types: dict[str, list], value: dict[str, Any]
) -> str:
def sign_typed_data(self, full_message: dict[str, Any]) -> str:
"""
Sign typed data value with types data structure for domain using the EIP-712 specification.
:param domain: The domain data structure.
:param types: The types data structure.
:param value: The value to sign.
:param full_message: The full message to sign.
:return: The signature.
"""
return _javascript_call(
"signTypedData",
domain,
types,
value,
"rpc",
"eth_signTypedData_v4",
[self.address, full_message],
timeout_message=TRANSACTION_TIMEOUT_MESSAGE,
)

Expand Down Expand Up @@ -141,18 +136,10 @@ def __init__(self, address=None, **kwargs):
self.signer = BrowserSigner(address)
self.set_eoa(self.signer)

def get_chain_id(self) -> int:
chain_id = _javascript_call(
"rpc", "eth_chainId", timeout_message=RPC_TIMEOUT_MESSAGE
)
return int.from_bytes(bytes.fromhex(chain_id[2:]), "big")

def set_chain_id(self, chain_id: int | str):
_javascript_call(
"rpc",
self._rpc.fetch(
"wallet_switchEthereumChain",
[{"chainId": chain_id if isinstance(chain_id, str) else hex(chain_id)}],
timeout_message=RPC_TIMEOUT_MESSAGE,
)
self._reset_fork()

Expand All @@ -169,7 +156,7 @@ def _javascript_call(js_func: str, *args, timeout_message: str) -> Any:
:return: The result of the Javascript snippet sent to the API.
"""
token = _generate_token()
args_str = ", ".join(json.dumps(p) for p in chain([token], args))
args_str = ", ".join(json.dumps(p, cls=_BytesEncoder) for p in chain([token], args))
js_code = f"window._titanoboa.{js_func}({args_str});"
if BrowserRPC._debug_mode:
logging.warning(f"Calling {js_func} with {args_str}")
Expand Down Expand Up @@ -224,9 +211,31 @@ def _parse_js_result(result: dict) -> Any:
if "data" in result:
return result["data"]

def _find_key(input_dict, target_key, typ) -> Any:
for key, value in input_dict.items():
if isinstance(value, dict):
found = _find_key(value, target_key, typ)
if found is not None:
return found
if key == target_key and isinstance(value, typ) and value != "error":
charles-cooper marked this conversation as resolved.
Show resolved Hide resolved
return value
return None

# raise the error in the Jupyter cell so that the user can see it
error = result["error"]
error = error.get("info", error).get("error", error)
error = error.get("data", error)
raise RPCError(
message=error.get("message", error), code=error.get("code", "CALLBACK_ERROR")
message=_find_key(error, "message", str) or _find_key(error, "error", str),
code=_find_key(error, "code", int) or -1,
)


class _BytesEncoder(json.JSONEncoder):
"""
A JSONEncoder that converts bytes to hex strings to be passed to JavaScript.
"""

def default(self, o):
if isinstance(o, bytes):
return "0x" + o.hex()
return super().default(o)
2 changes: 1 addition & 1 deletion boa/integrations/jupyter/constants.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@

NUL = b"\0"
CALLBACK_TOKEN_TIMEOUT = timedelta(minutes=3)
SHARED_MEMORY_LENGTH = 50 * 1024 + len(NUL) # Size of the shared memory object
SHARED_MEMORY_LENGTH = 100 * 1024 + len(NUL) # Size of the shared memory object
CALLBACK_TOKEN_CHARS = 30 # OSx limits this to 31 characters
PLUGIN_NAME = "titanoboa_jupyterlab"
TOKEN_REGEX = rf"[0-9a-fA-F]{{{CALLBACK_TOKEN_CHARS}}}"
Expand Down
11 changes: 1 addition & 10 deletions boa/integrations/jupyter/jupyter.js
Original file line number Diff line number Diff line change
Expand Up @@ -44,22 +44,14 @@
return response.text();
}

let from;
const loadSigner = async (address) => {
const accounts = await rpc('eth_requestAccounts');
from = accounts.includes(address) ? address : accounts[0];
return from;
return accounts.includes(address) ? address : accounts[0];
};

/** Sign a transaction via ethers */
const sendTransaction = async transaction => ({"hash": await rpc('eth_sendTransaction', [transaction])});

/** Sign a typed data via ethers */
const signTypedData = (domain, types, value) => rpc(
'eth_signTypedData_v4',
[from, JSON.stringify({domain, types, value})]
);

/** Wait until the transaction is mined */
const waitForTransactionReceipt = async (tx_hash, timeout, poll_latency) => {
while (true) {
Expand Down Expand Up @@ -120,7 +112,6 @@
window._titanoboa = {
loadSigner: handleCallback(loadSigner),
sendTransaction: handleCallback(sendTransaction),
signTypedData: handleCallback(signTypedData),
waitForTransactionReceipt: handleCallback(waitForTransactionReceipt),
rpc: handleCallback(rpc),
multiRpc: handleCallback(multiRpc),
Expand Down
Loading
Loading