-
Notifications
You must be signed in to change notification settings - Fork 554
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
Merge pull request #847 from eth-brownie/feat-replace-tx-auto
Automatic transaction repricing / replacement
- Loading branch information
Showing
14 changed files
with
846 additions
and
44 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Empty file.
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,164 @@ | ||
import inspect | ||
import threading | ||
import time | ||
from abc import ABC, abstractmethod | ||
from typing import Any, Generator, Iterator, Union | ||
|
||
from brownie.network.web3 import web3 | ||
|
||
|
||
class GasABC(ABC): | ||
""" | ||
Base ABC for all gas strategies. | ||
This class should not be directly subclassed from. Instead, use | ||
`SimpleGasStrategy`, `BlockGasStrategy` or `TimeGasStrategy`. | ||
""" | ||
|
||
@abstractmethod | ||
def get_gas_price(self) -> Union[Generator[int, None, None], int]: | ||
raise NotImplementedError | ||
|
||
|
||
class ScalingGasABC(GasABC): | ||
""" | ||
Base ABC for scaling gas strategies. | ||
This class should not be directly subclassed from. | ||
Instead, use `BlockGasStrategy` or `TimeGasStrategy`. | ||
""" | ||
|
||
duration: int | ||
|
||
def __new__(cls, *args: Any, **kwargs: Any) -> object: | ||
obj = super().__new__(cls) | ||
if not inspect.isgeneratorfunction(cls.get_gas_price): | ||
raise TypeError("Scaling strategy must implement get_gas_price as a generator function") | ||
return obj | ||
|
||
@abstractmethod | ||
def interval(self) -> int: | ||
""" | ||
Return "now" as it relates to the scaling strategy. | ||
This can be e.g. the current time or block height. It is used in combination | ||
with `duration` to determine when to rebroadcast a transaction. | ||
""" | ||
raise NotImplementedError | ||
|
||
def _loop(self, receipt: Any, gas_iter: Iterator) -> None: | ||
while web3.eth.getTransactionCount(str(receipt.sender)) < receipt.nonce: | ||
# do not run scaling strategy while prior tx's are still pending | ||
time.sleep(5) | ||
|
||
latest_interval = self.interval() | ||
while True: | ||
if web3.eth.getTransactionCount(str(receipt.sender)) > receipt.nonce: | ||
break | ||
|
||
if self.interval() - latest_interval >= self.duration: | ||
gas_price = next(gas_iter) | ||
if gas_price >= int(receipt.gas_price * 1.1): | ||
try: | ||
receipt = receipt.replace(gas_price=gas_price) | ||
latest_interval = self.interval() | ||
except ValueError: | ||
pass | ||
time.sleep(2) | ||
|
||
def run(self, receipt: Any, gas_iter: Iterator) -> None: | ||
thread = threading.Thread( | ||
target=self._loop, | ||
args=(receipt, gas_iter), | ||
daemon=True, | ||
name=f"Gas strategy {receipt.txid}", | ||
) | ||
thread.start() | ||
|
||
|
||
class SimpleGasStrategy(GasABC): | ||
""" | ||
Abstract base class for simple gas strategies. | ||
Simple gas strategies are called once to provide a gas price | ||
at the time a transaction is broadcasted. Transactions using simple | ||
gas strategies are not automatically rebroadcasted. | ||
Subclass from this ABC to implement your own simple gas strategy. | ||
""" | ||
|
||
@abstractmethod | ||
def get_gas_price(self) -> int: | ||
""" | ||
Return the initial gas price for a transaction. | ||
Returns | ||
------- | ||
int | ||
Gas price, given as an integer in wei. | ||
""" | ||
raise NotImplementedError | ||
|
||
|
||
class BlockGasStrategy(ScalingGasABC): | ||
""" | ||
Abstract base class for block gas strategies. | ||
Block gas strategies are called every `block_duration` blocks and | ||
can be used to automatically rebroadcast a pending transaction with | ||
a higher gas price. | ||
Subclass from this ABC to implement your own block gas strategy. | ||
""" | ||
|
||
duration = 2 | ||
|
||
def __init__(self, block_duration: int = 2) -> None: | ||
self.duration = block_duration | ||
|
||
def interval(self) -> int: | ||
return web3.eth.blockNumber | ||
|
||
@abstractmethod | ||
def get_gas_price(self) -> Generator[int, None, None]: | ||
""" | ||
Generator function to yield gas prices for a transaction. | ||
Returns | ||
------- | ||
int | ||
Gas price, given as an integer in wei. | ||
""" | ||
raise NotImplementedError | ||
|
||
|
||
class TimeGasStrategy(ScalingGasABC): | ||
""" | ||
Abstract base class for time gas strategies. | ||
Time gas strategies are called every `time_duration` seconds and | ||
can be used to automatically rebroadcast a pending transaction with | ||
a higher gas price. | ||
Subclass from this ABC to implement your own time gas strategy. | ||
""" | ||
|
||
duration = 30 | ||
|
||
def __init__(self, time_duration: int = 30) -> None: | ||
self.duration = time_duration | ||
|
||
def interval(self) -> int: | ||
return int(time.time()) | ||
|
||
@abstractmethod | ||
def get_gas_price(self) -> Generator[int, None, None]: | ||
""" | ||
Generator function to yield gas prices for a transaction. | ||
Returns | ||
------- | ||
int | ||
Gas price, given as an integer in wei. | ||
""" | ||
raise NotImplementedError |
Oops, something went wrong.