Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Feat tx required confs #593

Merged
merged 22 commits into from
Jun 3, 2020
Merged
Show file tree
Hide file tree
Changes from 20 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,8 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
- Project folder structure is now configurable ([#581](https://github.com/eth-brownie/brownie/pull/581))
- Deployment artifacts can now be saved via project setting `dev_deployment_artifacts: true` ([#590](https://github.com/eth-brownie/brownie/pull/590))
- All deployment artifacts are tracked in `deployments/map.json` ([#590](https://github.com/eth-brownie/brownie/pull/590))
- `required_confs = n / {'required_confs: n}` argument for transactions. Will wait for n confirmations before processing the tx receipt. `n = 0` will immediately return a pending receipt. ([#587](https://github.com/eth-brownie/brownie/pull/587))
- `tx.confirmations` shows number of confirmations, `tx.wait(n)` waits until `tx` has `n` or more confirmations. ([#587](https://github.com/eth-brownie/brownie/pull/587))

### Changed
- `tx.call_trace()` now displays internal and total gas usage ([#564](https://github.com/iamdefinitelyahuman/brownie/pull/564))
Expand Down
12 changes: 10 additions & 2 deletions brownie/network/account.py
Original file line number Diff line number Diff line change
Expand Up @@ -309,6 +309,7 @@ def deploy(
gas_limit: Optional[int] = None,
gas_price: Optional[int] = None,
nonce: Optional[int] = None,
required_confs: int = 1,
) -> Any:
"""Deploys a contract.

Expand Down Expand Up @@ -354,7 +355,11 @@ def deploy(
revert_data = (exc.revert_msg, exc.pc, exc.revert_type)

receipt = TransactionReceipt(
txid, self, name=contract._name + ".constructor", revert_data=revert_data
txid,
self,
required_confs=required_confs,
name=contract._name + ".constructor",
revert_data=revert_data,
)
add_thread = threading.Thread(target=contract._add_from_tx, args=(receipt,), daemon=True)
add_thread.start()
Expand Down Expand Up @@ -416,6 +421,7 @@ def transfer(
gas_price: Optional[int] = None,
data: str = None,
nonce: Optional[int] = None,
required_confs: int = 1,
silent: bool = False,
) -> "TransactionReceipt":
"""
Expand Down Expand Up @@ -454,7 +460,9 @@ def transfer(
txid = exc.txid
revert_data = (exc.revert_msg, exc.pc, exc.revert_type)

receipt = TransactionReceipt(txid, self, silent=silent, revert_data=revert_data)
receipt = TransactionReceipt(
txid, self, required_confs=required_confs, silent=silent, revert_data=revert_data
)
if rpc.is_active():
undo_thread = threading.Thread(
target=rpc._add_to_undo_buffer,
Expand Down
11 changes: 10 additions & 1 deletion brownie/network/contract.py
Original file line number Diff line number Diff line change
Expand Up @@ -241,6 +241,7 @@ def __call__(self, *args: Tuple) -> Union["Contract", TransactionReceiptType]:
gas_limit=tx["gas"],
gas_price=tx["gasPrice"],
nonce=tx["nonce"],
required_confs=tx["required_confs"],
)

def _autosuggest(self) -> List:
Expand Down Expand Up @@ -947,6 +948,7 @@ def transact(self, *args: Tuple) -> TransactionReceiptType:
gas_limit=tx["gas"],
gas_price=tx["gasPrice"],
nonce=tx["nonce"],
required_confs=tx["required_confs"],
data=self.encode_input(*args),
)

Expand Down Expand Up @@ -1062,7 +1064,14 @@ def _get_tx(owner: Optional[AccountsType], args: Tuple) -> Tuple:
owner = None

# seperate contract inputs from tx dict and set default tx values
tx = {"from": owner, "value": 0, "gas": None, "gasPrice": None, "nonce": None}
tx = {
"from": owner,
"value": 0,
"gas": None,
"gasPrice": None,
"nonce": None,
"required_confs": 1,
}
if args and isinstance(args[-1], dict):
tx.update(args[-1])
args = args[:-1]
Expand Down
115 changes: 88 additions & 27 deletions brownie/network/transaction.py
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
#!/usr/bin/python3

import sys
import threading
import time
from hashlib import sha1
Expand Down Expand Up @@ -72,6 +73,8 @@ class TransactionReceipt:
gas_limit: Gas limit
gas_used: Gas used
input: Hexstring input data
confirmations: The number of blocks since the transaction was confirmed
required_confs: Required confirmations before processing the receipt
nonce: Transaction nonce
block_number: Block number this transaction was included in
timestamp: Timestamp of the block this transaction was included in
Expand Down Expand Up @@ -100,6 +103,7 @@ class TransactionReceipt:
"_return_value",
"_revert_msg",
"_revert_pc",
"_silent",
"_trace",
"_trace_origin",
"block_number",
Expand All @@ -114,6 +118,7 @@ class TransactionReceipt:
"logs",
"nonce",
"receiver",
"required_confs",
"sender",
"status",
"txid",
Expand All @@ -126,6 +131,7 @@ def __init__(
txid: Union[str, bytes],
sender: Any = None,
silent: bool = False,
required_confs: int = 1,
name: str = "",
revert_data: Optional[Tuple] = None,
) -> None:
Expand All @@ -134,16 +140,18 @@ def __init__(
Args:
txid: hexstring transaction ID
sender: sender as a hex string or Account object
required_confs: the number of required confirmations before processing the receipt
silent: toggles console verbosity
name: contract function being called
revert_data: (revert string, program counter, revert type)
"""

self._silent = silent
if CONFIG.mode == "test":
silent = True
self._silent = True
if isinstance(txid, bytes):
txid = txid.hex()
if not silent:
if not self._silent:
print(f"Transaction sent: {color('bright blue')}{txid}{color}")
history._add_tx(self)

Expand All @@ -163,6 +171,7 @@ def __init__(
self.status = -1
self.txid = txid
self.fn_name = name
self.required_confs = required_confs
matnad marked this conversation as resolved.
Show resolved Hide resolved

if name and "." in name:
self.contract_name, self.fn_name = name.split(".", maxsplit=1)
Expand All @@ -172,23 +181,15 @@ def __init__(
if self._revert_msg is None and revert_type not in ("revert", "invalid_opcode"):
self._revert_msg = revert_type

# threaded to allow impatient users to ctrl-c to stop waiting in the console
confirm_thread = threading.Thread(
target=self._await_confirmation, args=(silent,), daemon=True
)
confirm_thread.start()
try:
confirm_thread.join()
# 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()
except KeyboardInterrupt:
if CONFIG.mode != "console":
raise
self._await_transaction()

# 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()

def __repr__(self) -> str:
c = {-1: "bright yellow", 0: "bright red", 1: None}
Expand Down Expand Up @@ -263,6 +264,25 @@ def timestamp(self) -> Optional[int]:
return None
return web3.eth.getBlock(self.block_number)["timestamp"]

@property
def confirmations(self) -> int:
if not self.block_number:
return 0
return web3.eth.blockNumber - self.block_number + 1

def wait(self, required_confs: int) -> None:
if self.confirmations > required_confs:
print(f"This transaction already has {self.confirmations} confirmations.")
else:
self.required_confs = required_confs
while True:
try:
tx: Dict = web3.eth.getTransaction(self.txid)
break
except TransactionNotFound:
time.sleep(0.5)
self._await_confirmation(tx)

def _raise_if_reverted(self, exc: Any) -> None:
if self.status or CONFIG.mode == "console":
return
Expand All @@ -277,29 +297,70 @@ 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_confirmation(self, silent: bool) -> None:
def _await_transaction(self) -> None:
# await tx showing in mempool
while True:
try:
tx = web3.eth.getTransaction(self.txid)
tx: Dict = web3.eth.getTransaction(self.txid)
break
except TransactionNotFound:
time.sleep(0.5)
self._set_from_tx(tx)

if not silent:
if not self._silent:
print(
f" Gas price: {color('bright blue')}{self.gas_price/10**9}{color} gwei"
f" Gas price: {color('bright blue')}{self.gas_price / 10 ** 9}{color} gwei"
f" Gas limit: {color('bright blue')}{self.gas_limit}{color}"
)
if not tx["blockNumber"] and not silent:
print("Waiting for confirmation...")

# await confirmation
# await confirmation of tx in a separate thread which is blocking if required_confs > 0
confirm_thread = threading.Thread(target=self._await_confirmation, args=(tx,), daemon=True)
confirm_thread.start()
if self.required_confs > 0:
confirm_thread.join()

def _await_confirmation(self, tx: Dict) -> None:
if not tx["blockNumber"] and not self._silent and self.required_confs > 0:
if self.required_confs == 1:
print("Waiting for confirmation...")
else:
sys.stdout.write(
f"\rRequired confirmations: {color('bright yellow')}0/"
f"{self.required_confs}{color}"
)
sys.stdout.flush()

# await first confirmation
receipt = web3.eth.waitForTransactionReceipt(self.txid, None)

self.block_number: int = receipt["blockNumber"]
# wait for more confirmations if required and handle uncle blocks
remaining_confs = self.required_confs
while remaining_confs > 0 and self.required_confs > 1:
try:
receipt = web3.eth.getTransactionReceipt(self.txid)
self.block_number = receipt["blockNumber"]
except TransactionNotFound:
if not self._silent:
sys.stdout.write(f"\r{color('red')}Transaction was lost...{color}")
matnad marked this conversation as resolved.
Show resolved Hide resolved
sys.stdout.flush()
continue
if self.required_confs - self.confirmations != remaining_confs:
remaining_confs = self.required_confs - self.confirmations
if not self._silent:
sys.stdout.write(
f"\rRequired confirmations: {color('bright yellow')}{self.confirmations}/"
f"{self.required_confs}{color}"
)
if remaining_confs == 0:
sys.stdout.write("\n")
sys.stdout.flush()
if remaining_confs > 0:
time.sleep(1)

self._set_from_receipt(receipt)
self._confirmed.set()
if not silent:
if not self._silent and self.required_confs > 0:
print(self._confirm_output())

def _set_from_tx(self, tx: Dict) -> None:
Expand Down
37 changes: 37 additions & 0 deletions docs/api-network.rst
Original file line number Diff line number Diff line change
Expand Up @@ -1798,6 +1798,17 @@ TransactionReceipt Attributes
>>> tx.block_number
2

.. py:attribute:: TransactionReceipt.confirmations

The number of blocks mined since the transaction was confirmed, including the block the transaction was mined in: ``block_height - tx.block_number + 1``.

.. code-block:: python

>>> tx
<Transaction '0x8c166b66b356ad7f5c58337973b89950f03105cdae896ac66f16cdd4fc395d05'>
>>> tx.confirmations
11

.. py:attribute:: TransactionReceipt.contract_address

The address of the contract deployed in this transaction, if the transaction was a deployment.
Expand Down Expand Up @@ -1987,6 +1998,17 @@ TransactionReceipt Attributes
>>> tx.revert_msg
None

.. py:attribute:: TransactionReceipt.required_confs

The number of :attr:`confirmations<TransactionReceipt.confirmations>` needed before the ``TransactionReceipt`` is processesd.

.. code-block:: python

>>> tx
<Transaction '0x8c166b66b356ad7f5c58337973b89950f03105cdae896ac66f16cdd4fc395d05'>
>>> tx.required_confs
4

.. py:attribute:: TransactionReceipt.return_value

The value returned from the called function, if any. Only available if the RPC client allows ``debug_traceTransaction``.
Expand Down Expand Up @@ -2224,6 +2246,21 @@ TransactionReceipt Methods
function mul(uint a, uint b) internal pure returns (uint c) {
c = a * b;


.. py:classmethod:: TransactionReceipt.wait(n)

Will wait for ``n`` :attr:`confirmations<TransactionReceipt.confirmations>` of the transaction. This has no effect if ``n`` is less than the current amount of confirmations.

.. code-block:: python

>>> tx
<Transaction '0x830b842e24efae712b67dddd97633356122c36e6cf2193fcf9f7dc635c4cbe2f'>
>>> tx.wait(2)
This transaction already has 3 confirmations.
>>> tx.wait(6)
Required confirmations: 6/6
Transaction confirmed - Block: 17 Gas used: 21000 (0.31%)

``brownie.network.web3``
========================

Expand Down
4 changes: 2 additions & 2 deletions docs/deploy.rst
Original file line number Diff line number Diff line change
Expand Up @@ -53,7 +53,7 @@ The Deployment Map

Brownie will maintain a ``map.json`` file in your ``build/deployment/`` folder that lists all deployed contracts on live networks, sorted by chain and contract name.

::
.. code-block:: json

{
"1": {
Expand Down Expand Up @@ -93,7 +93,7 @@ To restore a deleted :func:`ProjectContract <brownie.network.contract.ProjectCon
Saving Deployments on Development Networks
==========================================

If you need deployment artifacts on a development network, set :attr:`dev_deployment_artifacts` to true in the in the project's ``brownie-config.yaml`` file.
If you need deployment artifacts on a development network, set :attr:`dev_deployment_artifacts` to ``true`` in the in the project's ``brownie-config.yaml`` file.

These temporary deployment artifacts and the corresponding entries in :ref:`the deployment map<persistence>` will be removed whenever you (re-) load a project or connect, disconnect, revert or reset your local network.

Expand Down
Loading