Skip to content

Commit

Permalink
Asyncify eth.get_transaction_receipt and eth.wait_for_transaction_rec…
Browse files Browse the repository at this point in the history
…eipt (ethereum#2265)

* asyncify get_transaction_receipt and tests

* asyncify wait_for_transaction_receipt and tests

* remove fixme waiting on json-rpc spec
  • Loading branch information
Paul Robinson authored Dec 17, 2021
1 parent 9413e0c commit c70f7fb
Show file tree
Hide file tree
Showing 5 changed files with 184 additions and 33 deletions.
2 changes: 2 additions & 0 deletions docs/providers.rst
Original file line number Diff line number Diff line change
Expand Up @@ -415,8 +415,10 @@ Eth
- :meth:`web3.eth.get_raw_transaction_by_block() <web3.eth.Eth.get_raw_transaction_by_block>`
- :meth:`web3.eth.get_transaction() <web3.eth.Eth.get_transaction>`
- :meth:`web3.eth.get_transaction_count() <web3.eth.Eth.get_transaction_count>`
- :meth:`web3.eth.get_transaction_receipt() <web3.eth.Eth.get_transaction_receipt>`
- :meth:`web3.eth.send_transaction() <web3.eth.Eth.send_transaction>`
- :meth:`web3.eth.send_raw_transaction() <web3.eth.Eth.send_raw_transaction>`
- :meth:`web3.eth.wait_for_transaction_receipt() <web3.eth.Eth.wait_for_transaction_receipt>`

Net
***
Expand Down
1 change: 1 addition & 0 deletions newsfragments/2265.feature.rst
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
Add async ``eth.get_transaction_receipt`` and ``eth.wait_for_transaction_receipt`` methods
123 changes: 123 additions & 0 deletions web3/_utils/module_testing/eth_module.py
Original file line number Diff line number Diff line change
Expand Up @@ -47,6 +47,7 @@
InvalidAddress,
InvalidTransaction,
NameNotFound,
TimeExhausted,
TransactionNotFound,
TransactionTypeMismatch,
)
Expand Down Expand Up @@ -705,6 +706,128 @@ async def test_async_eth_mining(
mining = await async_w3.eth.mining # type: ignore
assert is_boolean(mining)

@pytest.mark.asyncio
async def test_async_eth_get_transaction_receipt_mined(
self,
async_w3: "Web3",
block_with_txn: BlockData,
mined_txn_hash: HexStr
) -> None:
receipt = await async_w3.eth.get_transaction_receipt(mined_txn_hash) # type: ignore
assert is_dict(receipt)
assert receipt['blockNumber'] == block_with_txn['number']
assert receipt['blockHash'] == block_with_txn['hash']
assert receipt['transactionIndex'] == 0
assert receipt['transactionHash'] == HexBytes(mined_txn_hash)
assert is_checksum_address(receipt['to'])
assert receipt['from'] is not None
assert is_checksum_address(receipt['from'])

effective_gas_price = receipt['effectiveGasPrice']
assert isinstance(effective_gas_price, int)
assert effective_gas_price > 0

@pytest.mark.asyncio
async def test_async_eth_get_transaction_receipt_unmined(
self, async_w3: "Web3", unlocked_account_dual_type: ChecksumAddress
) -> None:
txn_hash = await async_w3.eth.send_transaction({ # type: ignore
'from': unlocked_account_dual_type,
'to': unlocked_account_dual_type,
'value': Wei(1),
'gas': Wei(21000),
'maxFeePerGas': async_w3.toWei(3, 'gwei'),
'maxPriorityFeePerGas': async_w3.toWei(1, 'gwei')
})
with pytest.raises(TransactionNotFound):
await async_w3.eth.get_transaction_receipt(txn_hash) # type: ignore

@pytest.mark.asyncio
async def test_async_eth_get_transaction_receipt_with_log_entry(
self,
async_w3: "Web3",
block_with_txn_with_log: BlockData,
emitter_contract: "Contract",
txn_hash_with_log: HexStr,
) -> None:
receipt = await async_w3.eth.wait_for_transaction_receipt(txn_hash_with_log) # type: ignore
assert is_dict(receipt)
assert receipt['blockNumber'] == block_with_txn_with_log['number']
assert receipt['blockHash'] == block_with_txn_with_log['hash']
assert receipt['transactionIndex'] == 0
assert receipt['transactionHash'] == HexBytes(txn_hash_with_log)

assert len(receipt['logs']) == 1
log_entry = receipt['logs'][0]

assert log_entry['blockNumber'] == block_with_txn_with_log['number']
assert log_entry['blockHash'] == block_with_txn_with_log['hash']
assert log_entry['logIndex'] == 0
assert is_same_address(log_entry['address'], emitter_contract.address)
assert log_entry['transactionIndex'] == 0
assert log_entry['transactionHash'] == HexBytes(txn_hash_with_log)

@pytest.mark.asyncio
async def test_async_eth_wait_for_transaction_receipt_mined(
self,
async_w3: "Web3",
block_with_txn: BlockData,
mined_txn_hash: HexStr
) -> None:
receipt = await async_w3.eth.wait_for_transaction_receipt(mined_txn_hash) # type: ignore
assert is_dict(receipt)
assert receipt['blockNumber'] == block_with_txn['number']
assert receipt['blockHash'] == block_with_txn['hash']
assert receipt['transactionIndex'] == 0
assert receipt['transactionHash'] == HexBytes(mined_txn_hash)
assert is_checksum_address(receipt['to'])
assert receipt['from'] is not None
assert is_checksum_address(receipt['from'])

effective_gas_price = receipt['effectiveGasPrice']
assert isinstance(effective_gas_price, int)
assert effective_gas_price > 0

@pytest.mark.asyncio
async def test_async_eth_wait_for_transaction_receipt_unmined(
self, async_w3: "Web3", unlocked_account_dual_type: ChecksumAddress
) -> None:
txn_hash = await async_w3.eth.send_transaction({ # type: ignore
'from': unlocked_account_dual_type,
'to': unlocked_account_dual_type,
'value': Wei(1),
'gas': Wei(21000),
'maxFeePerGas': async_w3.toWei(3, 'gwei'),
'maxPriorityFeePerGas': async_w3.toWei(1, 'gwei')
})
with pytest.raises(TimeExhausted):
await async_w3.eth.wait_for_transaction_receipt(txn_hash, timeout=2) # type: ignore

@pytest.mark.asyncio
async def test_async_eth_wait_for_transaction_receipt_with_log_entry(
self,
async_w3: "Web3",
block_with_txn_with_log: BlockData,
emitter_contract: "Contract",
txn_hash_with_log: HexStr,
) -> None:
receipt = await async_w3.eth.wait_for_transaction_receipt(txn_hash_with_log) # type: ignore
assert is_dict(receipt)
assert receipt['blockNumber'] == block_with_txn_with_log['number']
assert receipt['blockHash'] == block_with_txn_with_log['hash']
assert receipt['transactionIndex'] == 0
assert receipt['transactionHash'] == HexBytes(txn_hash_with_log)

assert len(receipt['logs']) == 1
log_entry = receipt['logs'][0]

assert log_entry['blockNumber'] == block_with_txn_with_log['number']
assert log_entry['blockHash'] == block_with_txn_with_log['hash']
assert log_entry['logIndex'] == 0
assert is_same_address(log_entry['address'], emitter_contract.address)
assert log_entry['transactionIndex'] == 0
assert log_entry['transactionHash'] == HexBytes(txn_hash_with_log)


class EthModuleTest:
def test_eth_protocol_version(self, web3: "Web3") -> None:
Expand Down
26 changes: 0 additions & 26 deletions web3/_utils/transactions.py
Original file line number Diff line number Diff line change
Expand Up @@ -21,24 +21,17 @@
from web3._utils.compat import (
Literal,
)
from web3._utils.threads import (
Timeout,
)
from web3._utils.utility_methods import (
all_in_dict,
any_in_dict,
)
from web3.constants import (
DYNAMIC_FEE_TXN_PARAMS,
)
from web3.exceptions import (
TransactionNotFound,
)
from web3.types import (
BlockIdentifier,
TxData,
TxParams,
TxReceipt,
Wei,
_Hash32,
)
Expand Down Expand Up @@ -126,25 +119,6 @@ def fill_transaction_defaults(web3: "Web3", transaction: TxParams) -> TxParams:
return merge(defaults, transaction)


def wait_for_transaction_receipt(
web3: "Web3", txn_hash: _Hash32, timeout: float, poll_latency: float
) -> TxReceipt:
with Timeout(timeout) as _timeout:
while True:
try:
txn_receipt = web3.eth.get_transaction_receipt(txn_hash)
except TransactionNotFound:
txn_receipt = None
# FIXME: The check for a null `blockHash` is due to parity's
# non-standard implementation of the JSON-RPC API and should
# be removed once the formal spec for the JSON-RPC endpoints
# has been finalized.
if txn_receipt is not None and txn_receipt['blockHash'] is not None:
break
_timeout.sleep(poll_latency)
return txn_receipt


def get_block_gas_limit(web3: "Web3", block_identifier: Optional[BlockIdentifier] = None) -> Wei:
if block_identifier is None:
block_identifier = web3.eth.block_number
Expand Down
65 changes: 58 additions & 7 deletions web3/eth.py
Original file line number Diff line number Diff line change
Expand Up @@ -62,7 +62,6 @@
extract_valid_transaction_params,
get_required_transaction,
replace_transaction,
wait_for_transaction_receipt,
)
from web3.contract import (
ConciseContract,
Expand All @@ -71,6 +70,7 @@
)
from web3.exceptions import (
TimeExhausted,
TransactionNotFound,
)
from web3.iban import (
Iban,
Expand Down Expand Up @@ -277,6 +277,17 @@ def call_munger(
mungers=None,
)

_get_transaction_receipt: Method[Callable[[_Hash32], TxReceipt]] = Method(
RPC.eth_getTransactionReceipt,
mungers=[default_root_munger]
)

# _get_transaction_receipt_awaitable: Method[Callable[[_Hash32],
# Awaitable[TxReceipt]]] = Method(
# RPC.eth_getTransactionReceipt,
# mungers=[default_root_munger]
# )


class AsyncEth(BaseEth):
is_async = True
Expand Down Expand Up @@ -403,6 +414,36 @@ async def get_transaction_count(
mungers=[BaseEth.call_munger]
)

async def get_transaction_receipt(
self, transaction_hash: _Hash32
) -> TxReceipt:
return await self._get_transaction_receipt(transaction_hash) # type: ignore

async def wait_for_transaction_receipt(
self, transaction_hash: _Hash32, timeout: float = 120, poll_latency: float = 0.1
) -> TxReceipt:
try:
with Timeout(timeout) as _timeout:
while True:
try:
tx_receipt = await self._get_transaction_receipt( # type: ignore
transaction_hash
)
except TransactionNotFound:
tx_receipt = None
if tx_receipt is not None:
break
_timeout.sleep(poll_latency)
return tx_receipt

except Timeout:
raise TimeExhausted(
"Transaction {!r} is not in the chain, after {} seconds".format(
HexBytes(transaction_hash),
timeout,
)
)

async def call(
self,
transaction: TxParams,
Expand Down Expand Up @@ -683,7 +724,17 @@ def wait_for_transaction_receipt(
self, transaction_hash: _Hash32, timeout: float = 120, poll_latency: float = 0.1
) -> TxReceipt:
try:
return wait_for_transaction_receipt(self.web3, transaction_hash, timeout, poll_latency)
with Timeout(timeout) as _timeout:
while True:
try:
tx_receipt = self._get_transaction_receipt(transaction_hash)
except TransactionNotFound:
tx_receipt = None
if tx_receipt is not None:
break
_timeout.sleep(poll_latency)
return tx_receipt

except Timeout:
raise TimeExhausted(
"Transaction {!r} is not in the chain, after {} seconds".format(
Expand All @@ -692,10 +743,10 @@ def wait_for_transaction_receipt(
)
)

get_transaction_receipt: Method[Callable[[_Hash32], TxReceipt]] = Method(
RPC.eth_getTransactionReceipt,
mungers=[default_root_munger]
)
def get_transaction_receipt(
self, transaction_hash: _Hash32
) -> TxReceipt:
return self._get_transaction_receipt(transaction_hash)

get_transaction_count: Method[Callable[..., Nonce]] = Method(
RPC.eth_getTransactionCount,
Expand Down Expand Up @@ -924,7 +975,7 @@ def setGasPriceStrategy(self, gas_price_strategy: GasPriceStrategy) -> None:
sendRawTransaction = DeprecatedMethod(send_raw_transaction, # type: ignore
'sendRawTransaction',
'send_raw_transaction')
getTransactionReceipt = DeprecatedMethod(get_transaction_receipt,
getTransactionReceipt = DeprecatedMethod(get_transaction_receipt, # type: ignore
'getTransactionReceipt',
'get_transaction_receipt')
uninstallFilter = DeprecatedMethod(uninstall_filter, 'uninstallFilter', 'uninstall_filter')
Expand Down

0 comments on commit c70f7fb

Please sign in to comment.