Skip to content

Commit

Permalink
feat: support internal tx [APE-1308] (#19)
Browse files Browse the repository at this point in the history
  • Loading branch information
antazoey authored Aug 23, 2023
1 parent 96c497a commit 9febf72
Show file tree
Hide file tree
Showing 2 changed files with 166 additions and 22 deletions.
136 changes: 116 additions & 20 deletions ape_arbitrum/ecosystem.py
Original file line number Diff line number Diff line change
@@ -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):
Expand All @@ -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:
Expand Down Expand Up @@ -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:
Expand All @@ -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.")
Expand Down
52 changes: 50 additions & 2 deletions tests/test_ecosystem.py
Original file line number Diff line number Diff line change
@@ -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):
Expand All @@ -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)

0 comments on commit 9febf72

Please sign in to comment.