diff --git a/CHANGELOG.md b/CHANGELOG.md index 02af045..ee3f9fd 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,11 +2,12 @@ All notable changes to this project will be documented in this file. -## [0.0.5] - 2024-07-14 +## [0.0.6] - 2024-07-21 ### 🚀 Features - Functions with multiple parameters can be called using the call method +- Added option to read contract storage from slot using the read method ### 🐛 Bug Fixes @@ -15,6 +16,7 @@ All notable changes to this project will be documented in this file. ### ⚙️ Miscellaneous Tasks - Pyblish workflow updated +- Demo.tape & gitignore updated ## [0.0.4] - 2024-07-13 diff --git a/README.md b/README.md index 98c2f0a..9375a7c 100644 --- a/README.md +++ b/README.md @@ -56,6 +56,8 @@ To call a view function with the signature `call_this_view_function(uint256)(str ```bash # function which takes a single input parameter ape_utils call --function-sig "call_this_view_function(uint256)(string)" --address "0x80E097a70cacA11EB71B6401FB12D48A1A61Ef54" --args '[6147190]' --network :sepolia:infura +# or +ape utils call -s "call_this_view_function(uint256)(string)" -a "0x80E097a70cacA11EB71B6401FB12D48A1A61Ef54" -ag '[6147190]' --network :sepolia:infura ``` #### Calling a view function with multiple parameter @@ -101,6 +103,20 @@ ape_utils encode --signature "call_this_view_function(uint256 arg1)" 1234 ape_utils decode --signature "call_this_view_function(uint256 arg1)" "0x1e4f420d00000000000000000000000000000000000000000000000000000000000004d2" ``` +#### Don't want beautiful print statements ? + +You can now pass the `--raw` flag to each option to print the output data only + +```sh +ape_utils encode --signature 'isLastFloor(uint256 arg1)' 1234 --raw +``` + +#### Read storage slots of a contract + +```sh +ape utils read --address "0xDbB18e367E4A2A36A9F2AF7af8b3c743938deCF2" --slot 1 --network :sepolia +``` + ![working](media/working.png) ## Development diff --git a/demo.tape b/demo.tape index 43e78d8..aa7672e 100644 --- a/demo.tape +++ b/demo.tape @@ -28,4 +28,10 @@ Enter 1 Sleep 3s Type "ape_utils decode --signature 'call_this_view_function(uint256 arg1)' '0x1e4f420d00000000000000000000000000000000000000000000000000000000000004d2'" Enter 1 +Sleep 2s +Type "ape utils read --address '0xDbB18e367E4A2A36A9F2AF7af8b3c743938deCF2' --slot 1 --network :sepolia" +Enter 1 +Sleep 8s +Type "ape_utils encode --signature 'call_this_view_function(uint256 arg1)' 1234 --raw" +Enter 1 Sleep 10s diff --git a/media/demo.gif b/media/demo.gif index 685a1ab..ff660fc 100644 Binary files a/media/demo.gif and b/media/demo.gif differ diff --git a/media/help.png b/media/help.png index a22b4f6..5f1707e 100644 Binary files a/media/help.png and b/media/help.png differ diff --git a/src/ape_utils/__version__.py b/src/ape_utils/__version__.py index 2b13487..37d324c 100644 --- a/src/ape_utils/__version__.py +++ b/src/ape_utils/__version__.py @@ -1 +1 @@ -version = "0.0.5" +version = "0.0.6" diff --git a/src/ape_utils/_cli.py b/src/ape_utils/_cli.py index d064b24..91c98b3 100644 --- a/src/ape_utils/_cli.py +++ b/src/ape_utils/_cli.py @@ -19,6 +19,7 @@ call_view_function, decode_calldata, encode_calldata, + read_storage, ) install() @@ -76,115 +77,169 @@ def cli() -> None: @click.command(cls=ConnectedProviderCommand) @click.option( "--function-sig", + "-s", required=True, help="The function signature (e.g., function_name(input param type)(output param type)).", ) -@click.option("--address", required=True, help="The address of the smart contract.") -@click.option("--args", required=True, help="The arguments for the function call.", cls=PythonLiteralOption) +@click.option("--address", "-a", required=True, help="The address of the smart contract.") +@click.option("--args", "-ag", required=True, help="The arguments for the function call.", cls=PythonLiteralOption) +@click.option("--raw", "-r", is_flag=True, help="Print raw data without colorful output or additional text.") @network_option(default="ethereum:local:node", required=True) -def call_view_function_from_cli(function_sig: str, address: str, args: str, provider: Node) -> None: +def call_view_function_from_cli(function_sig: str, address: str, args: str, provider: Node, raw: bool) -> None: # noqa: FBT001 """ Calls a view function on the blockchain given a function signature and address. Using ape's native network parsing. """ try: - # console.print(provider.web3.provider) - # console.print(dir(provider.web3)) parsed_args = list(args) - # console.print(f"{parsed_args=}") output = call_view_function(function_sig, address, parsed_args, provider) - console.print(f"[blue bold]Output: [green]{output}") + if raw: + console.print(output) + else: + console.print(f"[blue bold]Output: [green]{output}") except Exception as e: - console.print(f"Error: [red]{e!s}") + if raw: + console.print(e) + else: + console.print(f"Error: [red]{e!s}") + raise e + + +@click.command(cls=ConnectedProviderCommand) +@click.option("--address", "-a", required=True, help="The address of the smart contract.") +@click.option("--slot", required=True, type=int, help="The storage slot to read from the contract.") +@click.option("--raw", "-r", is_flag=True, help="Print raw data without colorful output or additional text.") +@network_option(default="ethereum:local:node", required=True) +def read_storage_from_cli(address: str, slot: int, provider: Node, raw: bool) -> None: # noqa: ARG001, FBT001 + """ + Reads storage from a given address and storage slot on the blockchain. + """ + try: + data = read_storage(address, slot) + if raw: + console.print(data) + else: + console.print(f"[blue bold]Storage Data: [green]{data}") + except Exception as e: + if raw: + console.print(e) + else: + console.print(f"Error: [red]{e!s}") raise e @click.command(cls=rclick.RichCommand) @click.option( "--signature", + "-s", help="The function signature (e.g., function_name(input param type)).", required=True, ) @click.argument("args", nargs=-1, type=str) -def abi_encode(signature: str, args: Any) -> None: +@click.option("--raw", "-r", is_flag=True, help="Print raw data without colorful output or additional text.") +def abi_encode(signature: str, args: Any, raw: bool) -> None: # noqa: FBT001 """ Encodes calldata for a function given its signature and arguments excluding the selector. """ try: calldata = abi_encode_calldata(signature, *args) - console.print(f"[blue bold]Encoded Calldata: [green]{calldata.hex()}") + if raw: + console.print(calldata.hex()) + else: + console.print(f"[blue bold]Encoded Calldata: [green]{calldata.hex()}") except Exception as e: - console.print(f"Error: [red]{e!s}") - # TODO: Raise if debug mode is enabled - # raise e + if raw: + console.print(e) + else: + console.print(f"Error: [red]{e!s}") @click.command(cls=rclick.RichCommand) @click.option( "--signature", + "-s", help="The function signature (e.g., function_name(input param type)).", required=True, ) @click.argument("calldata", type=str) -def abi_decode(signature: str, calldata: str) -> None: +@click.option("--raw", "-r", is_flag=True, help="Print raw data without colorful output or additional text.") +def abi_decode(signature: str, calldata: str, raw: bool) -> None: # noqa: FBT001 """ Decodes calldata for a function given its signature and calldata string. """ try: decoded_data = abi_decode_calldata(signature, calldata) - # * print the ouput in a single line - console.print("[blue bold]Decoded Data: ", end="") - pprint(decoded_data) + if raw: + console.print(decoded_data) + else: + console.print("[blue bold]Decoded Data: ", end="") + pprint(decoded_data) except Exception as e: - console.print(f"Error: [red]{e!s}") + if raw: + console.print(e) + else: + console.print(f"Error: [red]{e!s}") @click.command(cls=rclick.RichCommand) @click.option( "--signature", + "-s", help="The function signature (e.g., function_name(input param type)).", required=True, ) @click.argument("args", nargs=-1, type=str) -def encode(signature: str, args: Any) -> None: +@click.option("--raw", "-r", is_flag=True, help="Print raw data without colorful output or additional text.") +def encode(signature: str, args: Any, raw: bool) -> None: # noqa: FBT001 """ Encodes calldata for a function given its signature and arguments Including the selector. """ try: calldata = encode_calldata(signature, *args) - console.print(f"[blue bold]Encoded Calldata: [green]{calldata.hex()}") + if raw: + console.print(calldata.hex()) + else: + console.print(f"[blue bold]Encoded Calldata: [green]{calldata.hex()}") except Exception as e: - console.print(f"Error: [red]{e!s}") - # TODO: Raise if debug mode is enabled - # raise e + if raw: + console.print(e) + else: + console.print(f"Error: [red]{e!s}") @click.command(cls=rclick.RichCommand) @click.option( "--signature", + "-s", help="The function signature (e.g., function_name(input param type)).", required=True, ) @click.argument("calldata", type=str) -def decode(signature: str, calldata: str) -> None: +@click.option("--raw", "-r", is_flag=True, help="Print raw data without colorful output or additional text.") +def decode(signature: str, calldata: str, raw: bool) -> None: # noqa: FBT001 """ Decodes calldata for a function given its signature and calldata string. """ try: decoded_data = decode_calldata(signature, calldata) - # * print the ouput in a single line - console.print("[blue bold]Decoded Data: ", end="") - pprint(decoded_data) + if raw: + console.print(decoded_data) + else: + console.print("[blue bold]Decoded Data: ", end="") + pprint(decoded_data) except Exception as e: - console.print(f"Error: [red]{e!s}") + if raw: + console.print(e) + else: + console.print(f"Error: [red]{e!s}") -# * Add commands to the CLI group cli.add_command(call_view_function_from_cli, name="call") cli.add_command(abi_encode, name="abi_encode") cli.add_command(abi_decode, name="abi_decode") cli.add_command(encode, name="encode") cli.add_command(decode, name="decode") +cli.add_command(read_storage_from_cli, name="read") if __name__ == "__main__": call_view_function_from_cli() diff --git a/src/ape_utils/utils.py b/src/ape_utils/utils.py index 3472174..93efd8a 100644 --- a/src/ape_utils/utils.py +++ b/src/ape_utils/utils.py @@ -2,7 +2,8 @@ import ape import ethpm_types -from ape.types import HexBytes +from ape import networks +from ape.types import AddressType, HexBytes from ape_node.provider import Node from eth_utils import keccak from ethpm_types import MethodABI @@ -182,6 +183,47 @@ def decode_calldata(signature: str, encoded_data: str) -> Union[dict, Any]: return ape.networks.ethereum.decode_calldata(method_abi, encoded_data_bytes) +def read_storage(address: Union[AddressType, str], slot: int) -> Any: + """ + Gets the raw value of a storage slot of a contract. + + This function interacts with the blockchain to read the storage data of a + specified contract address at a given storage slot. It uses the provider + from the Ape framework to fetch the storage data. + + Args: + address (Union[AddressType, str]): The address of the smart contract whose storage + data is to be read. This should be a valid Ethereum address. + slot (int): The storage slot number from which to read the data. This + should be a non-negative integer representing the position in the + contract's storage. + + Returns: + Any: The data stored at the specified storage slot of the given address. + The return type can vary depending on the data stored at the slot. + + Example: + >>> address = "0x1234567890abcdef1234567890abcdef12345678" + >>> slot = 5 + >>> data = read_storage(address, slot) + >>> print(data) + + Notes: + - This function requires a connection to an Ethereum network through + the Ape framework. + - Ensure that the address and slot provided are valid and exist in the + context of the smart contract's storage. + + Raises: + ValueError: If the provided address or slot is invalid. + ConnectionError: If there is an issue connecting to the Ethereum network. + + """ + data = networks.provider.get_storage(address, slot) + + return data + + if __name__ == "__main__": function_sig: str = "call_this_view_function(uint256)(string)" address: str = "0x80E097a70cacA11EB71B6401FB12D48A1A61Ef54"