Skip to content

Commit

Permalink
feat: support dynamic fee transactions (#237)
Browse files Browse the repository at this point in the history
* feat: dynamic txn types

* fix: set gas limit after priority fee

* refactor: rename to transaction_types

Co-authored-by: El De-dog-lo <[email protected]>

* doc: add note indicating EIP-1559

Co-authored-by: El De-dog-lo <[email protected]>

* fix: finish rename

* doc: just use dict type

Co-authored-by: El De-dog-lo <[email protected]>
  • Loading branch information
antazoey and fubuloubu authored Nov 18, 2021
1 parent 76b0673 commit e7cca21
Show file tree
Hide file tree
Showing 16 changed files with 223 additions and 88 deletions.
4 changes: 2 additions & 2 deletions setup.py
Original file line number Diff line number Diff line change
Expand Up @@ -75,7 +75,7 @@
"backports.cached_property ; python_version<'3.8'",
"click>=8.0.0",
"dataclassy==0.10.4", # NOTE: Pinned due to issue with `Type[<nothing>]`
"eth-account>=0.5.5,<0.6.0",
"eth-account>=0.5.6,<0.6.0",
"pluggy>=0.13.1,<1.0",
"PyGithub>=1.54,<2.0",
"pyyaml>=0.2.5",
Expand All @@ -85,7 +85,7 @@
"singledispatchmethod ; python_version<'3.8'",
"IPython>=7.25",
"pytest>=6.0,<7.0",
"web3[tester]>=5.18.0,<6.0.0",
"web3[tester]>=5.24.0,<6.0.0",
],
entry_points={
"console_scripts": ["ape=ape._cli:cli"],
Expand Down
2 changes: 2 additions & 0 deletions src/ape/api/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@
TestProviderAPI,
TransactionAPI,
TransactionStatusEnum,
TransactionType,
Web3Provider,
)

Expand All @@ -33,5 +34,6 @@
"TestProviderAPI",
"TransactionAPI",
"TransactionStatusEnum",
"TransactionType",
"Web3Provider",
]
23 changes: 14 additions & 9 deletions src/ape/api/accounts.py
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
from pathlib import Path
from typing import TYPE_CHECKING, Callable, Iterator, List, Optional, Type, Union

from ape.exceptions import AccountsError, AliasAlreadyInUseError, SignatureError
from ape.types import (
AddressType,
ContractType,
Expand All @@ -10,11 +11,10 @@
)
from ape.utils import cached_property

from ..exceptions import AccountsError, AliasAlreadyInUseError, SignatureError
from .address import AddressAPI
from .base import abstractdataclass, abstractmethod
from .contracts import ContractContainer, ContractInstance
from .providers import ReceiptAPI, TransactionAPI
from .providers import ReceiptAPI, TransactionAPI, TransactionType

if TYPE_CHECKING:
from ape.managers.config import ConfigManager
Expand Down Expand Up @@ -76,18 +76,23 @@ def call(self, txn: TransactionAPI, send_everything: bool = False) -> ReceiptAPI
elif txn.nonce < self.nonce:
raise AccountsError("Invalid nonce, will not publish.")

# TODO: Add `GasEstimationAPI`
if txn.gas_price is None:
txn.gas_price = self.provider.gas_price
# else: assume user specified a correct price, or will take too much time to confirm
txn_type = TransactionType(txn.type)
if txn_type == TransactionType.STATIC and txn.gas_price is None: # type: ignore
txn.gas_price = self.provider.gas_price # type: ignore
elif txn_type == TransactionType.DYNAMIC:
if txn.max_priority_fee is None: # type: ignore
txn.max_priority_fee = self.provider.priority_fee # type: ignore

if txn.max_fee is None:
txn.max_fee = self.provider.base_fee + txn.max_priority_fee
# else: Assume user specified the correct amount or txn will fail and waste gas

# NOTE: Allow overriding gas limit
if txn.gas_limit is None:
txn.gas_limit = self.provider.estimate_gas_cost(txn)
# else: assume user specified the correct amount or txn will fail and waste gas
# else: Assume user specified the correct amount or txn will fail and waste gas

if send_everything:
txn.value = self.balance - txn.gas_limit * txn.gas_price
txn.value = self.balance - txn.max_fee

if txn.total_transfer_value > self.balance:
raise AccountsError(
Expand Down
14 changes: 3 additions & 11 deletions src/ape/api/address.py
Original file line number Diff line number Diff line change
@@ -1,10 +1,10 @@
from typing import List, Optional, Type
from typing import List, Optional

from ape.exceptions import AddressError
from ape.types import AddressType

from ..exceptions import AddressError
from .base import abstractdataclass, abstractmethod
from .providers import ProviderAPI, ReceiptAPI, TransactionAPI
from .providers import ProviderAPI


@abstractdataclass
Expand All @@ -24,14 +24,6 @@ def provider(self) -> ProviderAPI:
def provider(self, value: ProviderAPI):
self._provider = value

@property
def _receipt_class(self) -> Type[ReceiptAPI]:
return self.provider.network.ecosystem.receipt_class

@property
def _transaction_class(self) -> Type[TransactionAPI]:
return self.provider.network.ecosystem.transaction_class

@property
@abstractmethod
def address(self) -> AddressType:
Expand Down
2 changes: 1 addition & 1 deletion src/ape/api/contracts.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,10 +2,10 @@

from eth_utils import to_bytes

from ape.exceptions import ArgumentsLengthError, ContractDeployError, TransactionError
from ape.logging import logger
from ape.types import ABI, AddressType, ContractType

from ..exceptions import ArgumentsLengthError, ContractDeployError, TransactionError
from .address import Address, AddressAPI
from .base import dataclass
from .providers import ProviderAPI, ReceiptAPI, TransactionAPI
Expand Down
6 changes: 3 additions & 3 deletions src/ape/api/networks.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,10 +4,10 @@

from pluggy import PluginManager # type: ignore

from ape.exceptions import NetworkError, NetworkNotFoundError
from ape.types import ABI, AddressType
from ape.utils import cached_property

from ..exceptions import NetworkError, NetworkNotFoundError
from .base import abstractdataclass, abstractmethod, dataclass
from .config import ConfigItem

Expand All @@ -17,7 +17,7 @@

from .contracts import ContractLog
from .explorers import ExplorerAPI
from .providers import ProviderAPI, ReceiptAPI, TransactionAPI
from .providers import ProviderAPI, ReceiptAPI, TransactionAPI, TransactionType


@abstractdataclass
Expand All @@ -33,7 +33,7 @@ class EcosystemAPI:
data_folder: Path
request_header: str

transaction_class: Type["TransactionAPI"]
transaction_types: Dict["TransactionType", Type["TransactionAPI"]]
receipt_class: Type["ReceiptAPI"]

_default_network: str = "development"
Expand Down
57 changes: 52 additions & 5 deletions src/ape/api/providers.py
Original file line number Diff line number Diff line change
@@ -1,20 +1,25 @@
from enum import IntEnum
from enum import Enum, IntEnum
from pathlib import Path
from typing import Iterator, List, Optional

from dataclassy import as_dict
from hexbytes import HexBytes
from web3 import Web3

from ape.exceptions import ProviderError
from ape.logging import logger
from ape.types import TransactionSignature

from ..exceptions import ProviderError
from . import networks
from .base import abstractdataclass, abstractmethod
from .config import ConfigItem


class TransactionType(Enum):
STATIC = "0x0"
DYNAMIC = "0x2" # EIP-1559


@abstractdataclass
class TransactionAPI:
chain_id: int = 0
Expand All @@ -23,24 +28,38 @@ class TransactionAPI:
nonce: Optional[int] = None # NOTE: `Optional` only to denote using default behavior
value: int = 0
gas_limit: Optional[int] = None # NOTE: `Optional` only to denote using default behavior
gas_price: Optional[int] = None # NOTE: `Optional` only to denote using default behavior
data: bytes = b""
type: TransactionType = TransactionType.STATIC

signature: Optional[TransactionSignature] = None

def __post_init__(self):
if not self.is_valid:
raise ProviderError("Transaction is not valid.")

@property
def max_fee(self) -> int:
"""
The total amount in fees willing to be spent on a transaction.
Override this property as needed, such as for EIP-1559 differences.
See :class:`~ape_ethereum.ecosystem.StaticFeeTransaction` and
:class`~ape_ethereum.ecosystem.DynamicFeeTransaction` as examples.
"""
return 0

@max_fee.setter
def max_fee(self, value):
raise NotImplementedError("Max fee is not settable by default.")

@property
def total_transfer_value(self) -> int:
"""
The total amount of WEI that a transaction could use.
Useful for determining if an account balance can afford
to submit the transaction.
"""
# TODO Support EIP-1559
return (self.gas_limit or 0) * (self.gas_price or 0) + self.value
return self.value + self.max_fee

@property
@abstractmethod
Expand Down Expand Up @@ -95,6 +114,12 @@ def __post_init__(self):
def __str__(self) -> str:
return f"<{self.__class__.__name__} {self.txn_hash}>"

def raise_for_status(self, txn: TransactionAPI):
"""
Handle provider-specific errors regarding a non-successful
:class:`~api.providers.TransactionStatusEnum`.
"""

def ran_out_of_gas(self, gas_limit: int) -> bool:
"""
Returns ``True`` when the transaction failed and used the
Expand Down Expand Up @@ -159,6 +184,16 @@ def estimate_gas_cost(self, txn: TransactionAPI) -> int:
def gas_price(self) -> int:
...

@property
@abstractmethod
def priority_fee(self) -> int:
raise NotImplementedError("priority_fee is not implemented by this provider")

@property
@abstractmethod
def base_fee(self) -> int:
raise NotImplementedError("base_fee is not implemented by this provider")

@abstractmethod
def send_call(self, txn: TransactionAPI) -> bytes: # Return value of function
...
Expand Down Expand Up @@ -229,6 +264,18 @@ def gas_price(self) -> int:
"""
return self._web3.eth.generate_gas_price() # type: ignore

@property
def priority_fee(self) -> int:
"""
Returns the current max priority fee per gas in wei.
"""
return self._web3.eth.max_priority_fee

@property
def base_fee(self) -> int:
block = self._web3.eth.get_block("latest")
return block.baseFeePerGas # type: ignore

def get_nonce(self, address: str) -> int:
"""
Returns the number of transactions sent from an address.
Expand Down
2 changes: 1 addition & 1 deletion src/ape/managers/compilers.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,12 +4,12 @@
from dataclassy import dataclass

from ape.api.compiler import CompilerAPI
from ape.exceptions import CompilerError
from ape.logging import logger
from ape.plugins import PluginManager
from ape.types import ContractType
from ape.utils import cached_property

from ..exceptions import CompilerError
from .config import ConfigManager


Expand Down
2 changes: 1 addition & 1 deletion src/ape/managers/project.py
Original file line number Diff line number Diff line change
Expand Up @@ -5,10 +5,10 @@
import requests
from dataclassy import dataclass

from ape.exceptions import ProjectError
from ape.types import Checksum, Compiler, ContractType, PackageManifest, Source
from ape.utils import compute_checksum

from ..exceptions import ProjectError
from .compilers import CompilerManager
from .config import ConfigManager

Expand Down
2 changes: 1 addition & 1 deletion src/ape/utils.py
Original file line number Diff line number Diff line change
Expand Up @@ -186,7 +186,7 @@ def extract_nested_value(root: Mapping, *args: str) -> Optional[Dict]:
"""
current_value: Any = root
for arg in args:
if not isinstance(current_value, dict):
if not hasattr(current_value, "get"):
return None

current_value = current_value.get(arg)
Expand Down
Loading

0 comments on commit e7cca21

Please sign in to comment.