From 9febf7205f2c2ddf0c6a40046a99dc023928fc9a Mon Sep 17 00:00:00 2001 From: antazoey Date: Wed, 23 Aug 2023 07:37:46 -0500 Subject: [PATCH] feat: support internal tx [APE-1308] (#19) --- ape_arbitrum/ecosystem.py | 136 ++++++++++++++++++++++++++++++++------ tests/test_ecosystem.py | 52 ++++++++++++++- 2 files changed, 166 insertions(+), 22 deletions(-) diff --git a/ape_arbitrum/ecosystem.py b/ape_arbitrum/ecosystem.py index 093c8c8..821db2a 100644 --- a/ape_arbitrum/ecosystem.py +++ b/ape_arbitrum/ecosystem.py @@ -1,20 +1,35 @@ -from typing import Optional, Type, Union, cast +import time +from typing import Optional, Type, cast -from ape.api import TransactionAPI from ape.api.config import PluginConfig from ape.api.networks import LOCAL_NETWORK_NAME -from ape.exceptions import ApeException +from ape.api.transactions import ConfirmationsProgressBar, ReceiptAPI, TransactionAPI +from ape.exceptions import ApeException, TransactionError +from ape.logging import logger from ape.types import TransactionSignature -from ape.utils import DEFAULT_LOCAL_TRANSACTION_ACCEPTANCE_TIMEOUT +from ape.utils import DEFAULT_LOCAL_TRANSACTION_ACCEPTANCE_TIMEOUT, to_int from ape_ethereum.ecosystem import Ethereum, NetworkConfig -from ape_ethereum.transactions import DynamicFeeTransaction, StaticFeeTransaction, TransactionType +from ape_ethereum.transactions import ( + DynamicFeeTransaction, + Receipt, + StaticFeeTransaction, + TransactionStatusEnum, +) +from ape_ethereum.transactions import TransactionType as EthTransactionType from eth_utils import decode_hex +from ethpm_types import HexBytes +from pydantic.fields import Field NETWORKS = { # chain_id, network_id "mainnet": (42161, 42161), "goerli": (421613, 421613), } +INTERNAL_TRANSACTION_TYPE = 106 + + +class InternalTransaction(StaticFeeTransaction): + type: int = Field(INTERNAL_TRANSACTION_TYPE, exclude=True) class ApeArbitrumError(ApeException): @@ -23,6 +38,56 @@ class ApeArbitrumError(ApeException): """ +class ArbitrumReceipt(Receipt): + def await_confirmations(self) -> "ReceiptAPI": + """ + Overridden to handle skipping nonce-check for internal txns. + """ + + if self.type != INTERNAL_TRANSACTION_TYPE: + return super().await_confirmations() + + # This logic is copied from ape-ethereum but removes the nonce-increase + # waiting, as internal transactions don't increase a nonce (apparently). + + try: + self.raise_for_status() + except TransactionError: + # Skip waiting for confirmations when the transaction has failed. + return self + + if self.required_confirmations == 0: + # The transaction might not yet be confirmed but + # the user is aware of this. Or, this is a development environment. + return self + + confirmations_occurred = self._confirmations_occurred + if self.required_confirmations and confirmations_occurred >= self.required_confirmations: + return self + + # If we get here, that means the transaction has been recently submitted. + if explorer_url := self._explorer and self._explorer.get_transaction_url(self.txn_hash): + log_message = f"Submitted {explorer_url}" + else: + log_message = f"Submitted {self.txn_hash}" + + logger.info(log_message) + + if self.required_confirmations: + with ConfirmationsProgressBar(self.required_confirmations) as progress_bar: + while confirmations_occurred < self.required_confirmations: + confirmations_occurred = self._confirmations_occurred + progress_bar.confs = confirmations_occurred + + if confirmations_occurred == self.required_confirmations: + break + + time_to_sleep = int(self._block_time / 2) + time.sleep(time_to_sleep) + + return self + + def _create_network_config( required_confirmations: int = 1, block_time: int = 1, **kwargs ) -> NetworkConfig: @@ -66,8 +131,8 @@ def create_transaction(self, **kwargs) -> TransactionAPI: :class:`~ape.api.transactions.TransactionAPI` """ - transaction_type = self.get_transaction_type(kwargs.get("type")) - kwargs["type"] = transaction_type.value + transaction_type = to_int(kwargs.get("type", EthTransactionType.STATIC.value)) + kwargs["type"] = transaction_type txn_class = _get_transaction_cls(transaction_type) if "required_confirmations" not in kwargs or kwargs["required_confirmations"] is None: @@ -94,20 +159,51 @@ def create_transaction(self, **kwargs) -> TransactionAPI: return txn_class.parse_obj(kwargs) - def get_transaction_type(self, _type: Optional[Union[int, str, bytes]]) -> TransactionType: - if _type is None: - version = TransactionType.STATIC - elif not isinstance(_type, int): - version = TransactionType(self.conversion_manager.convert(_type, int)) - else: - version = TransactionType(_type) - return version - - -def _get_transaction_cls(transaction_type: TransactionType) -> Type[TransactionAPI]: + def decode_receipt(self, data: dict) -> ReceiptAPI: + """ + NOTE: Overridden to use custom receipt class. + """ + status = data.get("status") + if status: + status = self.conversion_manager.convert(status, int) + status = TransactionStatusEnum(status) + + txn_hash = None + hash_key_choices = ("hash", "txHash", "txnHash", "transactionHash", "transaction_hash") + for choice in hash_key_choices: + if choice in data: + txn_hash = data[choice] + break + + if txn_hash: + txn_hash = txn_hash.hex() if isinstance(txn_hash, HexBytes) else txn_hash + + data_bytes = data.get("data", b"") + if data_bytes and isinstance(data_bytes, str): + data["data"] = HexBytes(data_bytes) + + elif "input" in data and isinstance(data["input"], str): + data["input"] = HexBytes(data["input"]) + + receipt = ArbitrumReceipt( + block_number=data.get("block_number") or data.get("blockNumber"), + contract_address=data.get("contract_address") or data.get("contractAddress"), + gas_limit=data.get("gas", data.get("gas_limit", data.get("gasLimit"))) or 0, + gas_price=data.get("gas_price", data.get("gasPrice")) or 0, + gas_used=data.get("gas_used", data.get("gasUsed")) or 0, + logs=data.get("logs", []), + status=status, + txn_hash=txn_hash, + transaction=self.create_transaction(**data), + ) + return receipt + + +def _get_transaction_cls(transaction_type: int) -> Type[TransactionAPI]: transaction_types = { - TransactionType.STATIC: StaticFeeTransaction, - TransactionType.DYNAMIC: DynamicFeeTransaction, + EthTransactionType.STATIC.value: StaticFeeTransaction, + EthTransactionType.DYNAMIC.value: DynamicFeeTransaction, + INTERNAL_TRANSACTION_TYPE: InternalTransaction, } if transaction_type not in transaction_types: raise ApeArbitrumError(f"Transaction type '{transaction_type}' not supported.") diff --git a/tests/test_ecosystem.py b/tests/test_ecosystem.py index 45fa959..20db923 100644 --- a/tests/test_ecosystem.py +++ b/tests/test_ecosystem.py @@ -1,5 +1,8 @@ import pytest from ape_ethereum.transactions import TransactionType +from ethpm_types import HexBytes + +from ape_arbitrum.ecosystem import INTERNAL_TRANSACTION_TYPE, ArbitrumReceipt def test_gas_limit(arbitrum): @@ -8,5 +11,50 @@ def test_gas_limit(arbitrum): @pytest.mark.parametrize("type", (0, "0x0")) def test_create_transaction(arbitrum, type): - txn = arbitrum.create_transaction(type=type) - assert txn.type == TransactionType.STATIC.value + tx = arbitrum.create_transaction(type=type) + assert tx.type == TransactionType.STATIC.value + + +def test_internal_tx(arbitrum): + tx = arbitrum.create_transaction(type=INTERNAL_TRANSACTION_TYPE) + assert tx.type == INTERNAL_TRANSACTION_TYPE + + +def test_decode_receipt(arbitrum): + data = { + "required_confirmations": 0, + "blockHash": HexBytes("0x01b9030516454bbb3d846bfa31fca8bf5cbdfb735879dcee27fd61f2ae3776b3"), + "blockNumber": 121166619, + "hash": HexBytes("0x8b8c74711aa2e117a307f8a96a93350e5ca7e01a7bf39dbb7a824e6a6fc3736f"), + "chainId": 42161, + "from": "0x00000000000000000000000000000000000A4B05", + "gas": 0, + "gasPrice": 0, + "input": HexBytes( + "0x6bf6a42d000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000011148fc000000000000000000000000000000000000000000000000000000000738db1b0000000000000000000000000000000000000000000000000000000000000000" # noqa: E501 + ), + "nonce": 0, + "r": HexBytes("0x00"), + "s": HexBytes("0x00"), + "to": "0x00000000000000000000000000000000000A4B05", + "transactionIndex": 0, + "type": 106, + "v": 0, + "value": 0, + "transactionHash": HexBytes( + "0x8b8c74711aa2e117a307f8a96a93350e5ca7e01a7bf39dbb7a824e6a6fc3736f" + ), + "logs": [], + "contractAddress": None, + "effectiveGasPrice": 100000000, + "cumulativeGasUsed": 0, + "gasUsed": 0, + "logsBloom": HexBytes( + "0x00000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000" # noqa: E501 + ), + "status": 1, + "l1BlockNumber": "0x11148fc", + "gasUsedForL1": "0x0", + } + actual = arbitrum.decode_receipt(data) + assert isinstance(actual, ArbitrumReceipt)