From 564ab7ed71d3c4b4c63adab2b793a1d42a9cf6a7 Mon Sep 17 00:00:00 2001 From: Ben Hauser Date: Sun, 11 Oct 2020 00:09:21 +0300 Subject: [PATCH 1/9] refactor: add tx to history before awaiting confirmation --- brownie/network/account.py | 12 ++++++++++-- brownie/network/transaction.py | 2 ++ 2 files changed, 12 insertions(+), 2 deletions(-) diff --git a/brownie/network/account.py b/brownie/network/account.py index 12f995c84..aa07f7d08 100644 --- a/brownie/network/account.py +++ b/brownie/network/account.py @@ -450,11 +450,15 @@ def deploy( txid, self, silent=silent, - required_confs=required_confs, + required_confs=0, name=contract._name + ".constructor", revert_data=revert_data, ) + # add the TxHistory before waiting for confirmation, this way the tx + # object is available if the user CTRL-C to stop waiting in the console history._add_tx(receipt) + receipt.wait(required_confs) + add_thread = threading.Thread(target=contract._add_from_tx, args=(receipt,), daemon=True) add_thread.start() @@ -585,9 +589,13 @@ def transfer( revert_data = (exc.revert_msg, exc.pc, exc.revert_type) receipt = TransactionReceipt( - txid, self, required_confs=required_confs, silent=silent, revert_data=revert_data + txid, self, required_confs=0, silent=silent, revert_data=revert_data ) + # add the TxHistory before waiting for confirmation, this way the tx + # object is available if the user CTRL-C to stop waiting in the console history._add_tx(receipt) + receipt.wait(required_confs) + if rpc.is_active(): undo_thread = threading.Thread( target=Chain()._add_to_undo_buffer, diff --git a/brownie/network/transaction.py b/brownie/network/transaction.py index 58955b273..74b1fcbf2 100644 --- a/brownie/network/transaction.py +++ b/brownie/network/transaction.py @@ -254,6 +254,8 @@ def confirmations(self) -> int: return web3.eth.blockNumber - self.block_number + 1 def wait(self, required_confs: int) -> None: + if required_confs < 1: + return if self.confirmations > required_confs: print(f"This transaction already has {self.confirmations} confirmations.") return From a6b426f9610e3bfba65cf118ffc175f6a7af894e Mon Sep 17 00:00:00 2001 From: Ben Hauser Date: Sun, 11 Oct 2020 02:32:21 +0300 Subject: [PATCH 2/9] feat: replace transactions --- brownie/network/state.py | 13 +++++- brownie/network/transaction.py | 72 +++++++++++++++++++++++++++++++--- 2 files changed, 78 insertions(+), 7 deletions(-) diff --git a/brownie/network/state.py b/brownie/network/state.py index 868af1ec5..5364fc3e4 100644 --- a/brownie/network/state.py +++ b/brownie/network/state.py @@ -68,6 +68,15 @@ def _revert(self, height: int) -> None: def _add_tx(self, tx: TransactionReceipt) -> None: self._list.append(tx) + confirm_thread = threading.Thread(target=self._await_confirm, args=(tx,), daemon=True) + confirm_thread.start() + + def _await_confirm(self, tx: TransactionReceipt) -> None: + # in case of multiple tx's with the same nonce, remove the dropped tx's upon confirmation + tx._confirmed.wait() + for dropped_tx in self.filter(sender=tx.sender, nonce=tx.nonce, key=lambda k: k != tx): + dropped_tx._set_replaced_by(tx) + self._list.remove(dropped_tx) def clear(self) -> None: self._list.clear() @@ -76,7 +85,9 @@ def copy(self) -> List: """Returns a shallow copy of the object as a list""" return self._list.copy() - def filter(self, key: Optional[Callable] = None, **kwargs: Dict) -> List: + def filter( + self, key: Optional[Callable] = None, **kwargs: Optional[Any] + ) -> List[TransactionReceipt]: """ Return a filtered list of transactions. diff --git a/brownie/network/transaction.py b/brownie/network/transaction.py index 74b1fcbf2..2b10c11cd 100644 --- a/brownie/network/transaction.py +++ b/brownie/network/transaction.py @@ -13,7 +13,7 @@ import requests from eth_abi import decode_abi from hexbytes import HexBytes -from web3.exceptions import TransactionNotFound +from web3.exceptions import TimeExhausted, TransactionNotFound from brownie._config import CONFIG from brownie.convert import EthAddress, Wei @@ -148,6 +148,7 @@ def __init__( self._internal_transfers = None self._subcalls: Optional[List[Dict]] = None self._confirmed = threading.Event() + self._replaced_by = None # attributes that can be set immediately self.sender = sender @@ -175,8 +176,11 @@ def __init__( self._expand_trace() def __repr__(self) -> str: - c = {-1: "bright yellow", 0: "bright red", 1: None} - return f"" + if self._replaced_by: + color_str: Optional[str] = color("dark white") + else: + color_str = {-1: "bright yellow", 0: "bright red", 1: None}[self.status] + return f"" def __hash__(self) -> int: return hash(self.txid) @@ -253,6 +257,48 @@ def confirmations(self) -> int: return 0 return web3.eth.blockNumber - self.block_number + 1 + def replace( + self, increment: Optional[float] = None, gas_price: Optional[Wei] = None, + ) -> "TransactionReceipt": + """ + Rebroadcast this transaction with a higher gas price. + + Exactly one of `increment` and `gas_price` must be given. + + Arguments + --------- + increment : float, optional + Multiplier applied to the gas price of this transaction in order + to determine the new gas price + gas_price: Wei, optional + Absolute gas price to use in the replacement transaction + + Returns + ------- + TransactionReceipt + New transaction object + """ + if increment is None and gas_price is None: + raise ValueError("Must give one of `increment` or `gas_price`") + if gas_price is not None and increment is not None: + raise ValueError("Cannot set `increment` and `gas_price` together") + if self.status > -1: + raise ValueError("Transaction has already confirmed") + + if increment is not None: + gas_price = Wei(self.gas_price * 1.1) + + return self.sender.transfer( # type: ignore + self.receiver, + self.value, + gas_limit=self.gas_limit, + gas_price=Wei(gas_price), + data=self.input, + nonce=self.nonce, + required_confs=0, + silent=self._silent, + ) + def wait(self, required_confs: int) -> None: if required_confs < 1: return @@ -265,10 +311,16 @@ def wait(self, required_confs: int) -> None: tx: Dict = web3.eth.getTransaction(self.txid) break except TransactionNotFound: - time.sleep(0.5) + if self._replaced_by: + print(f"This transaction was replaced by {self._replaced_by.txid}.") + return + time.sleep(1) self._await_confirmation(tx, required_confs) + def _set_replaced_by(self, tx: "TransactionReceipt") -> None: + self._replaced_by = tx + def _raise_if_reverted(self, exc: Any) -> None: if self.status or CONFIG.mode == "console": return @@ -294,7 +346,9 @@ def _await_transaction(self, required_confs: int = 1) -> None: # if sender was not explicitly set, this transaction was # not broadcasted locally and so likely doesn't exist raise - time.sleep(0.5) + if self._replaced_by: + return + time.sleep(1) self._set_from_tx(tx) if not self._silent: @@ -324,7 +378,13 @@ def _await_confirmation(self, tx: Dict, required_confs: int = 1) -> None: sys.stdout.flush() # await first confirmation - receipt = web3.eth.waitForTransactionReceipt(self.txid, timeout=None, poll_latency=0.5) + while True: + try: + receipt = web3.eth.waitForTransactionReceipt(self.txid, timeout=30, poll_latency=1) + break + except TimeExhausted: + if self._replaced_by: + return self.block_number = receipt["blockNumber"] # wait for more confirmations if required and handle uncle blocks From 9e0fcfbaa0a42b62cd62f1881915e2fc27303345 Mon Sep 17 00:00:00 2001 From: Ben Hauser Date: Sun, 11 Oct 2020 13:35:12 +0300 Subject: [PATCH 3/9] fix: return empty EventDict instead of list --- brownie/network/event.py | 13 ++++++++----- 1 file changed, 8 insertions(+), 5 deletions(-) diff --git a/brownie/network/event.py b/brownie/network/event.py index bfee75b58..81b5e1568 100644 --- a/brownie/network/event.py +++ b/brownie/network/event.py @@ -19,11 +19,14 @@ class EventDict: Dict/list hybrid container, base class for all events fired in a transaction. """ - def __init__(self, events: List) -> None: + def __init__(self, events: Optional[List] = None) -> None: """Instantiates the class. Args: events: event data as supplied by eth_event.decode_logs or eth_event.decode_trace""" + if events is None: + events = [] + self._ordered = [ _EventItem( i["name"], @@ -208,9 +211,9 @@ def _add_deployment_topics(address: str, abi: List) -> None: _deployment_topics[address] = eth_event.get_topic_map(abi) -def _decode_logs(logs: List) -> Union["EventDict", List[None]]: +def _decode_logs(logs: List) -> EventDict: if not logs: - return [] + return EventDict() idx = 0 events: List = [] @@ -237,9 +240,9 @@ def _decode_logs(logs: List) -> Union["EventDict", List[None]]: return EventDict(events) -def _decode_trace(trace: Sequence, initial_address: str) -> Union["EventDict", List[None]]: +def _decode_trace(trace: Sequence, initial_address: str) -> EventDict: if not trace: - return [] + return EventDict() events = eth_event.decode_traceTransaction( trace, _topics, allow_undecoded=True, initial_address=initial_address From 07d440043e8958243901f9bc86378c3fff87afe8 Mon Sep 17 00:00:00 2001 From: Ben Hauser Date: Sun, 11 Oct 2020 13:58:59 +0300 Subject: [PATCH 4/9] feat: add `is_blocking` and fix tx blocking logic --- brownie/network/account.py | 16 +++++++--- brownie/network/transaction.py | 54 ++++++++++++++++++---------------- 2 files changed, 40 insertions(+), 30 deletions(-) diff --git a/brownie/network/account.py b/brownie/network/account.py index aa07f7d08..1e1bf69f2 100644 --- a/brownie/network/account.py +++ b/brownie/network/account.py @@ -450,14 +450,16 @@ def deploy( txid, self, silent=silent, - required_confs=0, + required_confs=required_confs, + is_blocking=False, name=contract._name + ".constructor", revert_data=revert_data, ) # add the TxHistory before waiting for confirmation, this way the tx # object is available if the user CTRL-C to stop waiting in the console history._add_tx(receipt) - receipt.wait(required_confs) + if required_confs > 0: + receipt._confirmed.wait() add_thread = threading.Thread(target=contract._add_from_tx, args=(receipt,), daemon=True) add_thread.start() @@ -589,12 +591,18 @@ def transfer( revert_data = (exc.revert_msg, exc.pc, exc.revert_type) receipt = TransactionReceipt( - txid, self, required_confs=0, silent=silent, revert_data=revert_data + txid, + self, + required_confs=required_confs, + is_blocking=False, + silent=silent, + revert_data=revert_data, ) # add the TxHistory before waiting for confirmation, this way the tx # object is available if the user CTRL-C to stop waiting in the console history._add_tx(receipt) - receipt.wait(required_confs) + if required_confs > 0: + receipt._confirmed.wait() if rpc.is_active(): undo_thread = threading.Thread( diff --git a/brownie/network/transaction.py b/brownie/network/transaction.py index 2b10c11cd..1039a67e8 100644 --- a/brownie/network/transaction.py +++ b/brownie/network/transaction.py @@ -26,7 +26,7 @@ from brownie.utils.output import build_tree from . import state -from .event import _decode_logs, _decode_trace +from .event import EventDict, _decode_logs, _decode_trace from .web3 import web3 @@ -114,6 +114,7 @@ def __init__( sender: Any = None, silent: bool = True, required_confs: int = 1, + is_blocking: bool = True, name: str = "", revert_data: Optional[Tuple] = None, ) -> None: @@ -123,6 +124,8 @@ def __init__( txid: hexstring transaction ID sender: sender as a hex string or Account object required_confs: the number of required confirmations before processing the receipt + is_blocking: if True, creating the object is a blocking action until the required + confirmations are received silent: toggles console verbosity (default True) name: contract function being called revert_data: (revert string, program counter, revert type) @@ -135,20 +138,20 @@ def __init__( print(f"Transaction sent: {color('bright blue')}{txid}{color}") # internal attributes - self._trace_exc = None - self._trace_origin = None - self._raw_trace = None - self._trace = None self._call_cost = 0 - self._events = None - self._return_value = None - self._revert_msg = None - self._modified_state = None - self._new_contracts = None - self._internal_transfers = None - self._subcalls: Optional[List[Dict]] = None self._confirmed = threading.Event() - self._replaced_by = None + self._trace_exc: Optional[Exception] = None + self._trace_origin: Optional[str] = None + self._raw_trace: Optional[List] = None + self._trace: Optional[List] = None + self._events: Optional[EventDict] = None + self._return_value: Any = None + self._revert_msg: Optional[str] = None + self._modified_state: Optional[bool] = None + self._new_contracts: Optional[List] = None + self._internal_transfers: Optional[List[Dict]] = None + self._subcalls: Optional[List[Dict]] = None + self._replaced_by: Optional["TransactionReceipt"] = None # attributes that can be set immediately self.sender = sender @@ -165,15 +168,7 @@ def __init__( if self._revert_msg is None and revert_type not in ("revert", "invalid_opcode"): self._revert_msg = revert_type - self._await_transaction(required_confs) - - # if coverage evaluation is active, evaluate the trace - if ( - CONFIG.argv["coverage"] - and not coverage._check_cached(self.coverage_hash) - and self.trace - ): - self._expand_trace() + self._await_transaction(required_confs, is_blocking) def __repr__(self) -> str: if self._replaced_by: @@ -186,7 +181,7 @@ def __hash__(self) -> int: return hash(self.txid) @trace_property - def events(self) -> Optional[List]: + def events(self) -> Optional[EventDict]: if not self.status: self._get_trace() return self._events @@ -335,7 +330,7 @@ def _raise_if_reverted(self, exc: Any) -> None: source = self._error_string(1) raise exc._with_attr(source=source, revert_msg=self._revert_msg) - def _await_transaction(self, required_confs: int = 1) -> None: + def _await_transaction(self, required_confs: int, is_blocking: bool) -> None: # await tx showing in mempool while True: try: @@ -363,7 +358,7 @@ def _await_transaction(self, required_confs: int = 1) -> None: target=self._await_confirmation, args=(tx, required_confs), daemon=True ) confirm_thread.start() - if required_confs > 0: + if is_blocking and required_confs > 0: confirm_thread.join() def _await_confirmation(self, tx: Dict, required_confs: int = 1) -> None: @@ -415,9 +410,16 @@ def _await_confirmation(self, tx: Dict, required_confs: int = 1) -> None: time.sleep(1) self._set_from_receipt(receipt) - self._confirmed.set() + # if coverage evaluation is active, evaluate the trace + if ( + CONFIG.argv["coverage"] + and not coverage._check_cached(self.coverage_hash) + and self.trace + ): + self._expand_trace() if not self._silent and required_confs > 0: print(self._confirm_output()) + self._confirmed.set() def _set_from_tx(self, tx: Dict) -> None: if not self.sender: From a96056a73b72ebc1cea204734e03376517b79d80 Mon Sep 17 00:00:00 2001 From: Ben Hauser Date: Sun, 11 Oct 2020 13:59:35 +0300 Subject: [PATCH 5/9] test: fix failing tests --- tests/network/transaction/test_confirmation.py | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/tests/network/transaction/test_confirmation.py b/tests/network/transaction/test_confirmation.py index 6fde6c9ae..83f8769c9 100644 --- a/tests/network/transaction/test_confirmation.py +++ b/tests/network/transaction/test_confirmation.py @@ -4,31 +4,31 @@ def test_await_conf_simple_xfer(accounts): tx = accounts[0].transfer(accounts[1], "1 ether") assert tx.status == 1 - tx._await_transaction() + tx._await_transaction(1, True) def test_await_conf_successful_contract_call(accounts, tester): tx = tester.revertStrings(6, {"from": accounts[1]}) assert tx.status == 1 - tx._await_transaction() + tx._await_transaction(1, True) def test_await_conf_failed_contract_call(accounts, tester, console_mode): tx = tester.revertStrings(1, {"from": accounts[1]}) assert tx.status == 0 - tx._await_transaction() + tx._await_transaction(1, True) def test_await_conf_successful_contract_deploy(accounts, BrownieTester): tx = BrownieTester.deploy(True, {"from": accounts[0]}).tx assert tx.status == 1 - tx._await_transaction() + tx._await_transaction(1, True) def test_await_conf_failed_contract_deploy(accounts, BrownieTester, console_mode): tx = BrownieTester.deploy(False, {"from": accounts[0]}) assert tx.status == 0 - tx._await_transaction() + tx._await_transaction(1, True) def test_transaction_confirmations(accounts, chain): From 8b743b9456d272c79af35c370fd2f60359f30135 Mon Sep 17 00:00:00 2001 From: Ben Hauser Date: Sun, 11 Oct 2020 14:17:49 +0300 Subject: [PATCH 6/9] style: use IntEnum for transaction status --- brownie/network/transaction.py | 18 +++++++++++++----- 1 file changed, 13 insertions(+), 5 deletions(-) diff --git a/brownie/network/transaction.py b/brownie/network/transaction.py index 1039a67e8..6890c25b7 100644 --- a/brownie/network/transaction.py +++ b/brownie/network/transaction.py @@ -5,6 +5,7 @@ import threading import time from collections import OrderedDict +from enum import IntEnum from hashlib import sha1 from pathlib import Path from typing import Any, Callable, Dict, List, Optional, Sequence, Tuple, Union @@ -35,7 +36,7 @@ def trace_property(fn: Callable) -> Any: @property # type: ignore def wrapper(self: "TransactionReceipt") -> Any: - if self.status == -1: + if self.status < 0: return None if self._trace_exc is not None: raise self._trace_exc @@ -58,6 +59,13 @@ def wrapper(self: "TransactionReceipt", *args: Any, **kwargs: Any) -> Any: return wrapper +class Status(IntEnum): + Dropped = -2 + Pending = -1 + Reverted = 0 + Confirmed = 1 + + class TransactionReceipt: """Attributes and methods relating to a broadcasted transaction. @@ -155,7 +163,7 @@ def __init__( # attributes that can be set immediately self.sender = sender - self.status = -1 + self.status = Status(-1) self.txid = txid self.contract_name = None self.fn_name = name @@ -242,7 +250,7 @@ def trace(self) -> Optional[List]: @property def timestamp(self) -> Optional[int]: - if self.status == -1: + if self.status < 0: return None return web3.eth.getBlock(self.block_number)["timestamp"] @@ -443,7 +451,7 @@ def _set_from_receipt(self, receipt: Dict) -> None: self.txindex = receipt["transactionIndex"] self.gas_used = receipt["gasUsed"] self.logs = receipt["logs"] - self.status = receipt["status"] + self.status = Status(receipt["status"]) self.contract_address = receipt["contractAddress"] if self.contract_address and not self.contract_name: @@ -451,7 +459,7 @@ def _set_from_receipt(self, receipt: Dict) -> None: base = ( f"{self.nonce}{self.block_number}{self.sender}{self.receiver}" - f"{self.value}{self.input}{self.status}{self.gas_used}{self.txindex}" + f"{self.value}{self.input}{int(self.status)}{self.gas_used}{self.txindex}" ) self.coverage_hash = sha1(base.encode()).hexdigest() From ef409e0b6584ccadf6006c9fb34dd71fcb67485d Mon Sep 17 00:00:00 2001 From: Ben Hauser Date: Sun, 11 Oct 2020 14:40:05 +0300 Subject: [PATCH 7/9] refactor: use nonce to detect dropped transactions --- brownie/network/state.py | 8 +++++++- brownie/network/transaction.py | 25 ++++++++++++------------- 2 files changed, 19 insertions(+), 14 deletions(-) diff --git a/brownie/network/state.py b/brownie/network/state.py index 5364fc3e4..75e7c2602 100644 --- a/brownie/network/state.py +++ b/brownie/network/state.py @@ -45,6 +45,13 @@ def __repr__(self) -> str: return str(self._list) return super().__repr__() + def __getattribute__(self, name: str) -> Any: + # filter dropped transactions prior to attribute access + items = super().__getattribute__("_list") + items = [i for i in items if i.status != -2] + setattr(self, "_list", items) + return super().__getattribute__(name) + def __bool__(self) -> bool: return bool(self._list) @@ -75,7 +82,6 @@ def _await_confirm(self, tx: TransactionReceipt) -> None: # in case of multiple tx's with the same nonce, remove the dropped tx's upon confirmation tx._confirmed.wait() for dropped_tx in self.filter(sender=tx.sender, nonce=tx.nonce, key=lambda k: k != tx): - dropped_tx._set_replaced_by(tx) self._list.remove(dropped_tx) def clear(self) -> None: diff --git a/brownie/network/transaction.py b/brownie/network/transaction.py index 6890c25b7..06183eee2 100644 --- a/brownie/network/transaction.py +++ b/brownie/network/transaction.py @@ -159,7 +159,6 @@ def __init__( self._new_contracts: Optional[List] = None self._internal_transfers: Optional[List[Dict]] = None self._subcalls: Optional[List[Dict]] = None - self._replaced_by: Optional["TransactionReceipt"] = None # attributes that can be set immediately self.sender = sender @@ -179,11 +178,8 @@ def __init__( self._await_transaction(required_confs, is_blocking) def __repr__(self) -> str: - if self._replaced_by: - color_str: Optional[str] = color("dark white") - else: - color_str = {-1: "bright yellow", 0: "bright red", 1: None}[self.status] - return f"" + color_str = {-2: "dark white", -1: "bright yellow", 0: "bright red", 1: ""}[self.status] + return f"" def __hash__(self) -> int: return hash(self.txid) @@ -314,16 +310,14 @@ def wait(self, required_confs: int) -> None: tx: Dict = web3.eth.getTransaction(self.txid) break except TransactionNotFound: - if self._replaced_by: - print(f"This transaction was replaced by {self._replaced_by.txid}.") + if self.sender.nonce > self.nonce: # type: ignore + self.status = Status(-2) + print("This transaction was replaced.") return time.sleep(1) self._await_confirmation(tx, required_confs) - def _set_replaced_by(self, tx: "TransactionReceipt") -> None: - self._replaced_by = tx - def _raise_if_reverted(self, exc: Any) -> None: if self.status or CONFIG.mode == "console": return @@ -349,7 +343,8 @@ def _await_transaction(self, required_confs: int, is_blocking: bool) -> None: # if sender was not explicitly set, this transaction was # not broadcasted locally and so likely doesn't exist raise - if self._replaced_by: + if self.sender.nonce > self.nonce: + self.status = Status(-2) return time.sleep(1) self._set_from_tx(tx) @@ -382,11 +377,15 @@ def _await_confirmation(self, tx: Dict, required_confs: int = 1) -> None: # await first confirmation while True: + # if sender nonce is greater than tx nonce, the tx should be confirmed + expect_confirmed = bool(self.sender.nonce > self.nonce) # type: ignore try: receipt = web3.eth.waitForTransactionReceipt(self.txid, timeout=30, poll_latency=1) break except TimeExhausted: - if self._replaced_by: + if expect_confirmed: + # if we expected confirmation based on the nonce, tx likely dropped + self.status = Status(-2) return self.block_number = receipt["blockNumber"] From 7f866dbf0de041a47260ca201133de5b38f0100b Mon Sep 17 00:00:00 2001 From: Ben Hauser Date: Mon, 16 Nov 2020 22:11:26 +0400 Subject: [PATCH 8/9] fix: minor bugfixes --- brownie/network/transaction.py | 14 ++++++++------ 1 file changed, 8 insertions(+), 6 deletions(-) diff --git a/brownie/network/transaction.py b/brownie/network/transaction.py index 06183eee2..1644144e4 100644 --- a/brownie/network/transaction.py +++ b/brownie/network/transaction.py @@ -113,7 +113,7 @@ class TransactionReceipt: logs = None nonce = None sender = None - txid = None + txid: str txindex = None def __init__( @@ -163,7 +163,7 @@ def __init__( # attributes that can be set immediately self.sender = sender self.status = Status(-1) - self.txid = txid + self.txid = str(txid) self.contract_name = None self.fn_name = name @@ -336,14 +336,14 @@ def _await_transaction(self, required_confs: int, is_blocking: bool) -> None: # await tx showing in mempool while True: try: - tx: Dict = web3.eth.getTransaction(self.txid) + tx: Dict = web3.eth.getTransaction(HexBytes(self.txid)) break - except TransactionNotFound: + except (TransactionNotFound, ValueError): if self.sender is None: # if sender was not explicitly set, this transaction was # not broadcasted locally and so likely doesn't exist raise - if self.sender.nonce > self.nonce: + if self.nonce is not None and self.sender.nonce > self.nonce: self.status = Status(-2) return time.sleep(1) @@ -380,7 +380,9 @@ def _await_confirmation(self, tx: Dict, required_confs: int = 1) -> None: # if sender nonce is greater than tx nonce, the tx should be confirmed expect_confirmed = bool(self.sender.nonce > self.nonce) # type: ignore try: - receipt = web3.eth.waitForTransactionReceipt(self.txid, timeout=30, poll_latency=1) + receipt = web3.eth.waitForTransactionReceipt( + HexBytes(self.txid), timeout=30, poll_latency=1 + ) break except TimeExhausted: if expect_confirmed: From f8439f0a00501b9c428644d2af6b4236a85786c9 Mon Sep 17 00:00:00 2001 From: Ben Hauser Date: Mon, 16 Nov 2020 23:13:36 +0400 Subject: [PATCH 9/9] docs: add tx.replace to documentation --- docs/api-network.rst | 30 +++++++++++++++++++++++++++++- docs/core-accounts.rst | 19 +++++++++++++++++++ docs/core-chain.rst | 2 ++ 3 files changed, 50 insertions(+), 1 deletion(-) diff --git a/docs/api-network.rst b/docs/api-network.rst index ceda31ff2..5d82d8a41 100644 --- a/docs/api-network.rst +++ b/docs/api-network.rst @@ -2219,7 +2219,12 @@ TransactionReceipt Attributes .. py:attribute:: TransactionReceipt.status - The status of the transaction: -1 for pending, 0 for failed, 1 for success. + An :class:`IntEnum ` object representing the status of the transaction: + + * ``1``: Successful + * ``0``: Reverted + * ``-1``: Pending + * ``-2``: Dropped .. code-block:: python @@ -2315,6 +2320,29 @@ TransactionReceipt Attributes TransactionReceipt Methods ************************** +.. py:method:: TransactionReceipt.replace(increment=None, gas_price=None) + + Broadcast an identical transaction with the same nonce and a higher gas price. + + Exactly one of the following arguments must be provided: + + * ``increment``: Multiplier applied to the gas price of the current transaction in order to determine a new gas price + * ``gas_price``: Absolute gas price to use in the replacement transaction + + Returns a :func:`TransactionReceipt ` object. + + .. code-block:: python + + >>> tx = accounts[0].transfer(accounts[1], 100, required_confs=0, gas_price="1 gwei") + Transaction sent: 0xc1aab54599d7875fc1fe8d3e375abb0f490cbb80d5b7f48cedaa95fa726f29be + Gas price: 13.0 gwei Gas limit: 21000 Nonce: 3 + + + >>> tx.replace(1.1) + Transaction sent: 0x9a525e42b326c3cd57e889ad8c5b29c88108227a35f9763af33dccd522375212 + Gas price: 14.3 gwei Gas limit: 21000 Nonce: 3 + + .. py:classmethod:: TransactionReceipt.info() Displays verbose information about the transaction, including event logs and the error string if a transaction reverts. diff --git a/docs/core-accounts.rst b/docs/core-accounts.rst index fccc2c69c..f8eb03bca 100644 --- a/docs/core-accounts.rst +++ b/docs/core-accounts.rst @@ -89,3 +89,22 @@ Additionally, setting ``silent = True`` suppresses the console output. [1, -1, -1] These transactions are initially pending (``status == -1``) and appear yellow in the console. + +Replacing Transactions +====================== + +The :func:`TransactionReceipt.replace ` method can be used to replace underpriced transactions while they are still pending: + +.. code-block:: python + + >>> tx = accounts[0].transfer(accounts[1], 100, required_confs=0, gas_price="1 gwei") + Transaction sent: 0xc1aab54599d7875fc1fe8d3e375abb0f490cbb80d5b7f48cedaa95fa726f29be + Gas price: 13.0 gwei Gas limit: 21000 Nonce: 3 + + + >>> tx.replace(1.1) + Transaction sent: 0x9a525e42b326c3cd57e889ad8c5b29c88108227a35f9763af33dccd522375212 + Gas price: 14.3 gwei Gas limit: 21000 Nonce: 3 + + +All pending transactions are available within the :func:`history ` object. As soon as one transaction confirms, the remaining dropped transactions are removed. See the documentation on :ref:`accessing transaction history ` for more info. diff --git a/docs/core-chain.rst b/docs/core-chain.rst index 3cd204be1..b89808b63 100644 --- a/docs/core-chain.rst +++ b/docs/core-chain.rst @@ -53,6 +53,8 @@ The :func:`Chain ` object, available as ``chain``, Accessing Transaction Data ========================== +.. _core-chain-history: + Local Transaction History -------------------------