mirror of https://github.com/nucypher/nucypher.git
Merge pull request #2043 from cygnusv/block
Improve block confirmation logic and testspull/2087/head
commit
60043adffb
|
@ -25,6 +25,7 @@ from constant_sorrow.constants import NOT_RUNNING, UNKNOWN_DEVELOPMENT_CHAIN_ID
|
|||
from cytoolz.dicttoolz import dissoc
|
||||
from eth_account import Account
|
||||
from eth_account.messages import encode_defunct
|
||||
from eth_typing import HexStr
|
||||
from eth_typing.evm import BlockNumber, ChecksumAddress
|
||||
from eth_utils import to_canonical_address, to_checksum_address
|
||||
from geth import LoggingMixin
|
||||
|
@ -41,7 +42,10 @@ from typing import Union
|
|||
from web3 import Web3
|
||||
from web3.contract import Contract
|
||||
from web3.types import Wei, TxReceipt
|
||||
from web3._utils.threads import Timeout
|
||||
from web3.exceptions import TimeExhausted, TransactionNotFound
|
||||
|
||||
from nucypher.blockchain.eth.constants import AVERAGE_BLOCK_TIME_IN_SECONDS
|
||||
from nucypher.config.constants import DEFAULT_CONFIG_ROOT, DEPLOY_DIR, USER_LOG_DIR
|
||||
|
||||
UNKNOWN_DEVELOPMENT_CHAIN_ID.bool_value(True)
|
||||
|
@ -58,8 +62,10 @@ class Web3ClientConnectionFailed(Web3ClientError):
|
|||
class Web3ClientUnexpectedVersionString(Web3ClientError):
|
||||
pass
|
||||
|
||||
|
||||
# TODO: Consider creating a ChainInventory class and/or moving this to a separate module
|
||||
|
||||
|
||||
PUBLIC_CHAINS = {0: "Olympic",
|
||||
1: "Mainnet",
|
||||
2: "Morden",
|
||||
|
@ -92,7 +98,6 @@ POA_CHAINS = { # TODO: This list is incomplete, but it suffices for the moment
|
|||
|
||||
|
||||
class EthereumClient:
|
||||
|
||||
is_local = False
|
||||
|
||||
GETH = 'Geth'
|
||||
|
@ -103,9 +108,13 @@ class EthereumClient:
|
|||
ETHEREUM_TESTER = 'EthereumTester' # (PyEVM)
|
||||
CLEF = 'Clef' # Signer-only
|
||||
|
||||
PEERING_TIMEOUT = 30 # seconds
|
||||
PEERING_TIMEOUT = 30 # seconds
|
||||
SYNC_TIMEOUT_DURATION = 60 # seconds to wait for various blockchain syncing endeavors
|
||||
SYNC_SLEEP_DURATION = 5 # seconds
|
||||
SYNC_SLEEP_DURATION = 5 # seconds
|
||||
BLOCK_CONFIRMATIONS_POLLING_TIME = 3 # seconds
|
||||
TRANSACTION_POLLING_TIME = 0.5 # seconds
|
||||
COOLING_TIME = 5 # seconds
|
||||
STALECHECK_ALLOWABLE_DELAY = 30 # seconds
|
||||
|
||||
class ConnectionNotEstablished(RuntimeError):
|
||||
pass
|
||||
|
@ -116,6 +125,27 @@ class EthereumClient:
|
|||
class UnknownAccount(ValueError):
|
||||
pass
|
||||
|
||||
class TransactionBroadcastError(RuntimeError):
|
||||
pass
|
||||
|
||||
class NotEnoughConfirmations(TransactionBroadcastError):
|
||||
pass
|
||||
|
||||
class TransactionTimeout(TransactionBroadcastError):
|
||||
pass
|
||||
|
||||
class ChainReorganizationDetected(TransactionBroadcastError):
|
||||
"""Raised when block confirmations logic detects that a TX was lost due to a chain reorganization"""
|
||||
|
||||
error_message = ("Chain re-organization detected: Transaction {transaction_hash} was reported to be in "
|
||||
"block {block_hash}, but it's not there anymore")
|
||||
|
||||
def __init__(self, receipt):
|
||||
self.receipt = receipt
|
||||
self.message = self.error_message.format(transaction_hash=Web3.toHex(receipt['transactionHash']),
|
||||
block_hash=Web3.toHex(receipt['blockHash']))
|
||||
super().__init__(self.message)
|
||||
|
||||
def __init__(self,
|
||||
w3,
|
||||
node_technology: str,
|
||||
|
@ -254,14 +284,84 @@ class EthereumClient:
|
|||
def coinbase(self) -> ChecksumAddress:
|
||||
return self.w3.eth.coinbase
|
||||
|
||||
def wait_for_receipt(self, transaction_hash: str, timeout: int) -> TxReceipt:
|
||||
receipt = self.w3.eth.waitForTransactionReceipt(transaction_hash=transaction_hash, timeout=timeout)
|
||||
def wait_for_receipt(self,
|
||||
transaction_hash: str,
|
||||
timeout: float,
|
||||
confirmations: int = 0) -> TxReceipt:
|
||||
receipt: TxReceipt = None
|
||||
if confirmations:
|
||||
# If we're waiting for confirmations, we may as well let pass some time initially to make everything easier
|
||||
time.sleep(self.COOLING_TIME)
|
||||
|
||||
# We'll keep trying to get receipts until there are enough confirmations or the timeout happens
|
||||
with Timeout(seconds=timeout, exception=self.TransactionTimeout) as timeout_context:
|
||||
while not receipt:
|
||||
try:
|
||||
receipt = self.block_until_enough_confirmations(transaction_hash=transaction_hash,
|
||||
timeout=timeout,
|
||||
confirmations=confirmations)
|
||||
except (self.ChainReorganizationDetected, self.NotEnoughConfirmations, TimeExhausted):
|
||||
timeout_context.sleep(self.BLOCK_CONFIRMATIONS_POLLING_TIME)
|
||||
continue
|
||||
|
||||
else:
|
||||
# If not asking for confirmations, just use web3 and assume the returned receipt is final
|
||||
try:
|
||||
receipt = self.w3.eth.waitForTransactionReceipt(transaction_hash=transaction_hash,
|
||||
timeout=timeout,
|
||||
poll_latency=self.TRANSACTION_POLLING_TIME)
|
||||
except TimeExhausted:
|
||||
raise # TODO: #1504 - Handle transaction timeout
|
||||
|
||||
return receipt
|
||||
|
||||
def block_until_enough_confirmations(self, transaction_hash: str, timeout: float, confirmations: int) -> dict:
|
||||
|
||||
receipt: TxReceipt = self.w3.eth.waitForTransactionReceipt(transaction_hash=transaction_hash,
|
||||
timeout=timeout,
|
||||
poll_latency=self.TRANSACTION_POLLING_TIME)
|
||||
|
||||
preliminary_block_hash = Web3.toHex(receipt['blockHash'])
|
||||
tx_block_number = Web3.toInt(receipt['blockNumber'])
|
||||
self.log.info(f"Transaction {Web3.toHex(transaction_hash)} is preliminarily included in "
|
||||
f"block {preliminary_block_hash}")
|
||||
|
||||
confirmations_timeout = self._calculate_confirmations_timeout(confirmations)
|
||||
confirmations_so_far = 0
|
||||
with Timeout(seconds=confirmations_timeout, exception=self.NotEnoughConfirmations) as timeout_context:
|
||||
while confirmations_so_far < confirmations:
|
||||
timeout_context.sleep(self.BLOCK_CONFIRMATIONS_POLLING_TIME)
|
||||
self.check_transaction_is_on_chain(receipt=receipt)
|
||||
confirmations_so_far = self.block_number - tx_block_number
|
||||
self.log.info(f"We have {confirmations_so_far} confirmations. "
|
||||
f"Waiting for {confirmations - confirmations_so_far} more.")
|
||||
return receipt
|
||||
|
||||
@staticmethod
|
||||
def _calculate_confirmations_timeout(confirmations):
|
||||
confirmations_timeout = 3 * AVERAGE_BLOCK_TIME_IN_SECONDS * confirmations
|
||||
return confirmations_timeout
|
||||
|
||||
def check_transaction_is_on_chain(self, receipt: TxReceipt) -> bool:
|
||||
transaction_hash = Web3.toHex(receipt['transactionHash'])
|
||||
try:
|
||||
new_receipt = self.w3.eth.getTransactionReceipt(transaction_hash=transaction_hash)
|
||||
except TransactionNotFound:
|
||||
reorg_detected = True
|
||||
else:
|
||||
reorg_detected = receipt['blockHash'] != new_receipt['blockHash']
|
||||
|
||||
if reorg_detected:
|
||||
exception = self.ChainReorganizationDetected(receipt=receipt)
|
||||
self.log.info(exception.message)
|
||||
raise exception
|
||||
# TODO: Consider adding an optional param in this exception to include extra info (e.g. new block)
|
||||
return True
|
||||
|
||||
def sign_transaction(self, transaction_dict: dict) -> bytes:
|
||||
raise NotImplementedError
|
||||
|
||||
def get_transaction(self, transaction_hash) -> str:
|
||||
def get_transaction(self, transaction_hash) -> dict:
|
||||
return self.w3.eth.getTransaction(transaction_hash=transaction_hash)
|
||||
|
||||
def send_transaction(self, transaction_dict: dict) -> str:
|
||||
|
@ -278,12 +378,15 @@ class EthereumClient:
|
|||
"""
|
||||
return self.w3.eth.sign(account, data=message)
|
||||
|
||||
def get_blocktime(self):
|
||||
highest_block = self.w3.eth.getBlock('latest')
|
||||
now = highest_block['timestamp']
|
||||
return now
|
||||
|
||||
def _has_latest_block(self) -> bool:
|
||||
# TODO: Investigate using `web3.middleware.make_stalecheck_middleware` #2060
|
||||
# check that our local chain data is up to date
|
||||
return (
|
||||
time.time() -
|
||||
self.w3.eth.getBlock(self.w3.eth.blockNumber)['timestamp']
|
||||
) < 30
|
||||
return (time.time() - self.get_blocktime()) < self.STALECHECK_ALLOWABLE_DELAY
|
||||
|
||||
def sync(self, timeout: int = 120, quiet: bool = False):
|
||||
|
||||
|
@ -312,7 +415,7 @@ class EthereumClient:
|
|||
self.log.info(f"Waiting for {self.chain_name.capitalize()} chain synchronization to begin")
|
||||
while not self.syncing:
|
||||
time.sleep(0)
|
||||
check_for_timeout(t=self.SYNC_TIMEOUT_DURATION*2)
|
||||
check_for_timeout(t=self.SYNC_TIMEOUT_DURATION * 2)
|
||||
|
||||
while True:
|
||||
syncdata = self.syncing
|
||||
|
@ -344,7 +447,7 @@ class GethClient(EthereumClient):
|
|||
return self.w3.geth.admin.peers()
|
||||
|
||||
def new_account(self, password: str) -> str:
|
||||
new_account = self.w3.geth.personal.newAccount(password)
|
||||
new_account = self.w3.geth.personal.new_account(password)
|
||||
return to_checksum_address(new_account) # cast and validate
|
||||
|
||||
def unlock_account(self, account: str, password: str, duration: int = None):
|
||||
|
@ -363,10 +466,10 @@ class GethClient(EthereumClient):
|
|||
debug_message += " with no password."
|
||||
|
||||
self.log.debug(debug_message)
|
||||
return self.w3.geth.personal.unlockAccount(account, password, duration)
|
||||
return self.w3.geth.personal.unlock_account(account, password, duration)
|
||||
|
||||
def lock_account(self, account):
|
||||
return self.w3.geth.personal.lockAccount(account)
|
||||
return self.w3.geth.personal.lock_account(account)
|
||||
|
||||
def sign_transaction(self, transaction_dict: dict) -> bytes:
|
||||
|
||||
|
@ -383,7 +486,7 @@ class GethClient(EthereumClient):
|
|||
|
||||
@property
|
||||
def wallets(self):
|
||||
return self.w3.manager.request_blocking("personal_listWallets", [])
|
||||
return self.w3.geth.personal.list_wallets()
|
||||
|
||||
|
||||
class ParityClient(EthereumClient):
|
||||
|
@ -396,18 +499,17 @@ class ParityClient(EthereumClient):
|
|||
return self.w3.manager.request_blocking("parity_netPeers", [])
|
||||
|
||||
def new_account(self, password: str) -> str:
|
||||
new_account = self.w3.parity.personal.newAccount(password)
|
||||
new_account = self.w3.parity.personal.new_account(password)
|
||||
return to_checksum_address(new_account) # cast and validate
|
||||
|
||||
def unlock_account(self, account, password, duration: int = None) -> bool:
|
||||
return self.w3.parity.personal.unlockAccount(account, password, duration)
|
||||
return self.w3.parity.personal.unlock_account(account, password, duration)
|
||||
|
||||
def lock_account(self, account):
|
||||
return self.w3.parity.personal.lockAccount(account)
|
||||
return self.w3.parity.personal.lock_account(account)
|
||||
|
||||
|
||||
class GanacheClient(EthereumClient):
|
||||
|
||||
is_local = True
|
||||
|
||||
def unlock_account(self, *args, **kwargs) -> bool:
|
||||
|
@ -418,8 +520,8 @@ class GanacheClient(EthereumClient):
|
|||
|
||||
|
||||
class InfuraClient(EthereumClient):
|
||||
|
||||
is_local = False
|
||||
TRANSACTION_POLLING_TIME = 2 # seconds
|
||||
|
||||
def unlock_account(self, *args, **kwargs) -> bool:
|
||||
return True
|
||||
|
@ -429,7 +531,6 @@ class InfuraClient(EthereumClient):
|
|||
|
||||
|
||||
class EthereumTesterClient(EthereumClient):
|
||||
|
||||
is_local = True
|
||||
|
||||
def unlock_account(self, account, password, duration: int = None) -> bool:
|
||||
|
@ -486,7 +587,6 @@ class EthereumTesterClient(EthereumClient):
|
|||
|
||||
|
||||
class NuCypherGethProcess(LoggingMixin, BaseGethProcess):
|
||||
|
||||
IPC_PROTOCOL = 'http'
|
||||
IPC_FILENAME = 'geth.ipc'
|
||||
VERBOSITY = 5
|
||||
|
@ -540,7 +640,6 @@ class NuCypherGethProcess(LoggingMixin, BaseGethProcess):
|
|||
|
||||
|
||||
class NuCypherGethDevProcess(NuCypherGethProcess):
|
||||
|
||||
_CHAIN_NAME = 'poa-development'
|
||||
|
||||
def __init__(self, config_root: str = None, *args, **kwargs):
|
||||
|
@ -567,7 +666,6 @@ class NuCypherGethDevProcess(NuCypherGethProcess):
|
|||
|
||||
|
||||
class NuCypherGethDevnetProcess(NuCypherGethProcess):
|
||||
|
||||
IPC_PROTOCOL = 'file'
|
||||
GENESIS_FILENAME = 'testnet_genesis.json'
|
||||
GENESIS_SOURCE_FILEPATH = os.path.join(DEPLOY_DIR, GENESIS_FILENAME)
|
||||
|
@ -646,7 +744,6 @@ class NuCypherGethDevnetProcess(NuCypherGethProcess):
|
|||
|
||||
|
||||
class NuCypherGethGoerliProcess(NuCypherGethProcess):
|
||||
|
||||
IPC_PROTOCOL = 'file'
|
||||
GENESIS_FILENAME = 'testnet_genesis.json'
|
||||
GENESIS_SOURCE_FILEPATH = os.path.join(DEPLOY_DIR, GENESIS_FILENAME)
|
||||
|
|
|
@ -591,7 +591,7 @@ class StakingEscrowDeployer(BaseContractDeployer, UpgradeableContractMixin, Owna
|
|||
target_contract=the_escrow_contract,
|
||||
deployer_address=self.deployer_address)
|
||||
|
||||
dispatcher_receipts = dispatcher_deployer.deploy(gas_limit=gas_limit)
|
||||
dispatcher_receipts = dispatcher_deployer.deploy(gas_limit=gas_limit, confirmations=confirmations)
|
||||
dispatcher_deploy_receipt = dispatcher_receipts[dispatcher_deployer.deployment_steps[0]]
|
||||
if progress:
|
||||
progress.update(1)
|
||||
|
@ -615,11 +615,14 @@ class StakingEscrowDeployer(BaseContractDeployer, UpgradeableContractMixin, Owna
|
|||
# This is the end of deployment without activation: the contract is now idle, waiting for activation
|
||||
return preparation_receipts
|
||||
else: # deployment_mode is FULL
|
||||
activation_receipts = self.activate(gas_limit=gas_limit, progress=progress, emitter=emitter)
|
||||
activation_receipts = self.activate(gas_limit=gas_limit,
|
||||
progress=progress,
|
||||
confirmations=confirmations,
|
||||
emitter=emitter)
|
||||
self.deployment_receipts.update(activation_receipts)
|
||||
return self.deployment_receipts
|
||||
|
||||
def activate(self, gas_limit: int = None, progress=None, emitter=None):
|
||||
def activate(self, gas_limit: int = None, progress=None, emitter=None, confirmations: int = 0):
|
||||
|
||||
self._contract = self._get_deployed_contract()
|
||||
if not self.ready_to_activate:
|
||||
|
@ -638,6 +641,7 @@ class StakingEscrowDeployer(BaseContractDeployer, UpgradeableContractMixin, Owna
|
|||
# TODO: Confirmations / Successful Transaction Indicator / Events ?? - #1193, #1194
|
||||
approve_reward_receipt = self.blockchain.send_transaction(contract_function=approve_reward_function,
|
||||
sender_address=self.deployer_address,
|
||||
confirmations=confirmations,
|
||||
payload=origin_args)
|
||||
if progress:
|
||||
progress.update(1)
|
||||
|
@ -648,6 +652,7 @@ class StakingEscrowDeployer(BaseContractDeployer, UpgradeableContractMixin, Owna
|
|||
init_function = self._contract.functions.initialize(self.economics.erc20_reward_supply)
|
||||
init_receipt = self.blockchain.send_transaction(contract_function=init_function,
|
||||
sender_address=self.deployer_address,
|
||||
confirmations=confirmations,
|
||||
payload=origin_args)
|
||||
if progress:
|
||||
progress.update(1)
|
||||
|
@ -754,7 +759,7 @@ class PolicyManagerDeployer(BaseContractDeployer, UpgradeableContractMixin, Owna
|
|||
target_contract=policy_manager_contract,
|
||||
deployer_address=self.deployer_address)
|
||||
|
||||
proxy_deploy_receipt = proxy_deployer.deploy(gas_limit=gas_limit)
|
||||
proxy_deploy_receipt = proxy_deployer.deploy(gas_limit=gas_limit, confirmations=confirmations)
|
||||
proxy_deploy_receipt = proxy_deploy_receipt[proxy_deployer.deployment_steps[0]]
|
||||
if progress:
|
||||
progress.update(1)
|
||||
|
@ -777,6 +782,7 @@ class PolicyManagerDeployer(BaseContractDeployer, UpgradeableContractMixin, Owna
|
|||
set_policy_manager_function = self.staking_contract.functions.setPolicyManager(wrapped_contract.address)
|
||||
set_policy_manager_receipt = self.blockchain.send_transaction(contract_function=set_policy_manager_function,
|
||||
sender_address=self.deployer_address,
|
||||
confirmations=confirmations,
|
||||
payload=tx_args)
|
||||
if progress:
|
||||
progress.update(1)
|
||||
|
@ -1060,7 +1066,7 @@ class AdjudicatorDeployer(BaseContractDeployer, UpgradeableContractMixin, Ownabl
|
|||
]
|
||||
return super().check_deployment_readiness(additional_rules=adjudicator_deployment_rules, *args, **kwargs)
|
||||
|
||||
def _deploy_essential(self, contract_version: str, gas_limit: int = None, **overrides):
|
||||
def _deploy_essential(self, contract_version: str, gas_limit: int = None, confirmations: int = 0, **overrides):
|
||||
args = self.economics.slashing_deployment_parameters
|
||||
constructor_kwargs = {
|
||||
"_hashAlgorithm": args[0],
|
||||
|
@ -1077,6 +1083,7 @@ class AdjudicatorDeployer(BaseContractDeployer, UpgradeableContractMixin, Ownabl
|
|||
self.registry,
|
||||
self.contract_name,
|
||||
gas_limit=gas_limit,
|
||||
confirmations=confirmations,
|
||||
contract_version=contract_version,
|
||||
**constructor_kwargs)
|
||||
return adjudicator_contract, deploy_receipt
|
||||
|
@ -1088,6 +1095,7 @@ class AdjudicatorDeployer(BaseContractDeployer, UpgradeableContractMixin, Ownabl
|
|||
contract_version: str = "latest",
|
||||
ignore_deployed: bool = False,
|
||||
emitter=None,
|
||||
confirmations: int = 0,
|
||||
**overrides) -> Dict[str, str]:
|
||||
|
||||
if deployment_mode not in (BARE, IDLE, FULL):
|
||||
|
@ -1100,6 +1108,7 @@ class AdjudicatorDeployer(BaseContractDeployer, UpgradeableContractMixin, Ownabl
|
|||
emitter.message(f"\nNext Transaction: {self.contract_name} Contract Creation", color='blue', bold=True)
|
||||
adjudicator_contract, deploy_receipt = self._deploy_essential(contract_version=contract_version,
|
||||
gas_limit=gas_limit,
|
||||
confirmations=confirmations,
|
||||
**overrides)
|
||||
|
||||
# This is the end of bare deployment.
|
||||
|
@ -1118,7 +1127,7 @@ class AdjudicatorDeployer(BaseContractDeployer, UpgradeableContractMixin, Ownabl
|
|||
target_contract=adjudicator_contract,
|
||||
deployer_address=self.deployer_address)
|
||||
|
||||
proxy_deploy_receipts = proxy_deployer.deploy(gas_limit=gas_limit)
|
||||
proxy_deploy_receipts = proxy_deployer.deploy(gas_limit=gas_limit, confirmations=confirmations)
|
||||
proxy_deploy_receipt = proxy_deploy_receipts[proxy_deployer.deployment_steps[0]]
|
||||
if progress:
|
||||
progress.update(1)
|
||||
|
@ -1139,6 +1148,7 @@ class AdjudicatorDeployer(BaseContractDeployer, UpgradeableContractMixin, Ownabl
|
|||
set_adjudicator_function = self.staking_contract.functions.setAdjudicator(adjudicator_contract.address)
|
||||
set_adjudicator_receipt = self.blockchain.send_transaction(contract_function=set_adjudicator_function,
|
||||
sender_address=self.deployer_address,
|
||||
confirmations=confirmations,
|
||||
transaction_gas_limit=gas_limit)
|
||||
if progress:
|
||||
progress.update(1)
|
||||
|
|
|
@ -19,13 +19,18 @@ along with nucypher. If not, see <https://www.gnu.org/licenses/>.
|
|||
import collections
|
||||
|
||||
import click
|
||||
import maya
|
||||
import os
|
||||
import pprint
|
||||
import requests
|
||||
import time
|
||||
from constant_sorrow.constants import (INSUFFICIENT_ETH, NO_BLOCKCHAIN_CONNECTION, NO_COMPILATION_PERFORMED,
|
||||
NO_PROVIDER_PROCESS, READ_ONLY_INTERFACE, UNKNOWN_TX_STATUS)
|
||||
from constant_sorrow.constants import (
|
||||
INSUFFICIENT_ETH,
|
||||
NO_BLOCKCHAIN_CONNECTION,
|
||||
NO_COMPILATION_PERFORMED,
|
||||
NO_PROVIDER_PROCESS,
|
||||
READ_ONLY_INTERFACE,
|
||||
UNKNOWN_TX_STATUS
|
||||
)
|
||||
from eth_tester import EthereumTester
|
||||
from eth_tester.exceptions import TransactionFailed as TestTransactionFailed
|
||||
from eth_utils import to_checksum_address
|
||||
|
@ -40,9 +45,16 @@ from web3.middleware import geth_poa_middleware
|
|||
|
||||
from nucypher.blockchain.eth.clients import EthereumClient, POA_CHAINS
|
||||
from nucypher.blockchain.eth.decorators import validate_checksum_address
|
||||
from nucypher.blockchain.eth.providers import (_get_HTTP_provider, _get_IPC_provider, _get_auto_provider,
|
||||
_get_infura_provider, _get_mock_test_provider, _get_pyevm_test_provider,
|
||||
_get_test_geth_parity_provider, _get_websocket_provider)
|
||||
from nucypher.blockchain.eth.providers import (
|
||||
_get_auto_provider,
|
||||
_get_HTTP_provider,
|
||||
_get_infura_provider,
|
||||
_get_IPC_provider,
|
||||
_get_mock_test_provider,
|
||||
_get_pyevm_test_provider,
|
||||
_get_test_geth_parity_provider,
|
||||
_get_websocket_provider
|
||||
)
|
||||
from nucypher.blockchain.eth.registry import BaseContractRegistry
|
||||
from nucypher.blockchain.eth.sol.compile import SolidityCompiler
|
||||
from nucypher.blockchain.eth.utils import get_transaction_name, prettify_eth_amount
|
||||
|
@ -62,7 +74,7 @@ class BlockchainInterface:
|
|||
ethereum contracts with the given web3 provider backend.
|
||||
"""
|
||||
|
||||
TIMEOUT = 600 # seconds
|
||||
TIMEOUT = 600 # seconds # TODO: Correlate with the gas strategy - #2070
|
||||
|
||||
DEFAULT_GAS_STRATEGY = 'medium'
|
||||
GAS_STRATEGIES = {'glacial': time_based.glacial_gas_price_strategy, # 24h
|
||||
|
@ -91,9 +103,6 @@ class BlockchainInterface:
|
|||
class UnknownContract(InterfaceError):
|
||||
pass
|
||||
|
||||
class NotEnoughConfirmations(InterfaceError):
|
||||
pass
|
||||
|
||||
REASONS = {
|
||||
INSUFFICIENT_ETH: 'insufficient funds for gas * price + value',
|
||||
}
|
||||
|
@ -143,7 +152,7 @@ class BlockchainInterface:
|
|||
gas_strategy: Union[str, Callable] = DEFAULT_GAS_STRATEGY):
|
||||
|
||||
"""
|
||||
A blockchain "network interface"; The circumflex wraps entirely around the bounds of
|
||||
A blockchain "network interface"; the circumflex wraps entirely around the bounds of
|
||||
contract operations including compilation, deployment, and execution.
|
||||
|
||||
TODO: #1502 - Move to API docs.
|
||||
|
@ -537,8 +546,8 @@ class BlockchainInterface:
|
|||
# Receipt
|
||||
#
|
||||
|
||||
try:
|
||||
receipt = self.client.wait_for_receipt(txhash, timeout=self.TIMEOUT)
|
||||
try: # TODO: Handle block confirmation exceptions
|
||||
receipt = self.client.wait_for_receipt(txhash, timeout=self.TIMEOUT, confirmations=confirmations)
|
||||
except TimeExhausted:
|
||||
# TODO: #1504 - Handle transaction timeout
|
||||
raise
|
||||
|
@ -550,13 +559,13 @@ class BlockchainInterface:
|
|||
#
|
||||
|
||||
# Primary check
|
||||
deployment_status = receipt.get('status', UNKNOWN_TX_STATUS)
|
||||
if deployment_status == 0:
|
||||
transaction_status = receipt.get('status', UNKNOWN_TX_STATUS)
|
||||
if transaction_status == 0:
|
||||
failure = f"Transaction transmitted, but receipt returned status code 0. " \
|
||||
f"Full receipt: \n {pprint.pformat(receipt, indent=2)}"
|
||||
raise self.InterfaceError(failure)
|
||||
|
||||
if deployment_status is UNKNOWN_TX_STATUS:
|
||||
if transaction_status is UNKNOWN_TX_STATUS:
|
||||
self.log.info(f"Unknown transaction status for {txhash} (receipt did not contain a status field)")
|
||||
|
||||
# Secondary check
|
||||
|
@ -565,33 +574,10 @@ class BlockchainInterface:
|
|||
raise self.InterfaceError(f"Transaction consumed 100% of transaction gas."
|
||||
f"Full receipt: \n {pprint.pformat(receipt, indent=2)}")
|
||||
|
||||
# Block confirmations
|
||||
if confirmations:
|
||||
start = maya.now()
|
||||
confirmations_so_far = self.get_confirmations(receipt)
|
||||
while confirmations_so_far < confirmations:
|
||||
self.log.info(f"So far, we've received {confirmations_so_far} confirmations. "
|
||||
f"Waiting for {confirmations - confirmations_so_far} more.")
|
||||
time.sleep(3)
|
||||
confirmations_so_far = self.get_confirmations(receipt)
|
||||
if (maya.now() - start).seconds > self.TIMEOUT:
|
||||
raise self.NotEnoughConfirmations
|
||||
|
||||
return receipt
|
||||
|
||||
def get_confirmations(self, receipt: dict) -> int:
|
||||
tx_block_number = receipt.get('blockNumber')
|
||||
latest_block_number = self.w3.eth.blockNumber
|
||||
confirmations = latest_block_number - tx_block_number
|
||||
if confirmations < 0:
|
||||
raise ValueError(f"Can't get number of confirmations for transaction {receipt['transactionHash'].hex()}, "
|
||||
f"as it seems to come from {-confirmations} blocks in the future...")
|
||||
return confirmations
|
||||
|
||||
def get_blocktime(self):
|
||||
highest_block = self.w3.eth.getBlock('latest')
|
||||
now = highest_block['timestamp']
|
||||
return now
|
||||
return self.client.get_blocktime()
|
||||
|
||||
@validate_checksum_address
|
||||
def send_transaction(self,
|
||||
|
|
|
@ -312,7 +312,7 @@ class Stake:
|
|||
result = self.unlock_datetime.slang_date()
|
||||
else:
|
||||
# TODO - #1509 EthAgent?
|
||||
blocktime_epoch = self.staking_agent.blockchain.client.w3.eth.getBlock('latest').timestamp
|
||||
blocktime_epoch = self.staking_agent.blockchain.client.get_blocktime()
|
||||
delta = self.unlock_datetime.epoch - blocktime_epoch
|
||||
result = delta
|
||||
return result
|
||||
|
|
|
@ -439,7 +439,7 @@ def contracts(general_config, actor_options, mode, activate, gas, ignore_deploye
|
|||
staking_escrow_address=escrow_address)
|
||||
click.confirm(prompt, abort=True)
|
||||
|
||||
receipts = staking_escrow_deployer.activate()
|
||||
receipts = staking_escrow_deployer.activate(gas_limit=gas, confirmations=confirmations)
|
||||
for tx_name, receipt in receipts.items():
|
||||
paint_receipt_summary(emitter=emitter,
|
||||
receipt=receipt,
|
||||
|
|
|
@ -136,7 +136,7 @@ def events(general_config, registry_options, contract_name, from_block, to_block
|
|||
if from_block is None:
|
||||
# Sketch of logic for getting the approximate block height of current period start,
|
||||
# so by default, this command only shows events of the current period
|
||||
last_block = blockchain.client.w3.eth.blockNumber
|
||||
last_block = blockchain.client.block_number
|
||||
staking_agent = ContractAgency.get_agent(StakingEscrowAgent, registry=registry)
|
||||
current_period = staking_agent.get_current_period()
|
||||
current_period_start = datetime_at_period(period=current_period,
|
||||
|
|
|
@ -14,16 +14,24 @@ GNU Affero General Public License for more details.
|
|||
You should have received a copy of the GNU Affero General Public License
|
||||
along with nucypher. If not, see <https://www.gnu.org/licenses/>.
|
||||
"""
|
||||
import types
|
||||
from os.path import abspath, dirname
|
||||
|
||||
import os
|
||||
import pytest
|
||||
from unittest.mock import PropertyMock
|
||||
|
||||
from nucypher.blockchain.eth.interfaces import BlockchainDeployerInterface
|
||||
import maya
|
||||
import pytest
|
||||
from hexbytes import HexBytes
|
||||
|
||||
from nucypher.blockchain.eth.clients import EthereumClient
|
||||
from nucypher.blockchain.eth.interfaces import BlockchainDeployerInterface, BlockchainInterfaceFactory
|
||||
from nucypher.blockchain.eth.registry import InMemoryContractRegistry
|
||||
from nucypher.blockchain.eth.sol.compile import SolidityCompiler, SourceDirs
|
||||
from nucypher.crypto.powers import TransactingPower
|
||||
# Prevents TesterBlockchain to be picked up by py.test as a test class
|
||||
from tests.fixtures import _make_testerchain
|
||||
from tests.mock.interfaces import MockBlockchain
|
||||
from tests.utils.blockchain import TesterBlockchain as _TesterBlockchain
|
||||
from tests.constants import (DEVELOPMENT_ETH_AIRDROP_AMOUNT, INSECURE_DEVELOPMENT_PASSWORD,
|
||||
NUMBER_OF_ETH_TEST_ACCOUNTS, NUMBER_OF_STAKERS_IN_BLOCKCHAIN_TESTS,
|
||||
|
@ -155,72 +163,28 @@ def test_multiversion_contract():
|
|||
assert contract.functions.VERSION().call() == 2
|
||||
|
||||
|
||||
def test_block_confirmations(testerchain, test_registry):
|
||||
|
||||
testerchain.TIMEOUT = 5 # Reduce timeout for tests, for the moment
|
||||
def test_block_confirmations(testerchain, test_registry, mocker):
|
||||
origin = testerchain.etherbase_account
|
||||
|
||||
# Mocks and test adjustments
|
||||
testerchain.TIMEOUT = 5 # Reduce timeout for tests, for the moment
|
||||
mocker.patch.object(testerchain.client, '_calculate_confirmations_timeout', return_value=1)
|
||||
EthereumClient.BLOCK_CONFIRMATIONS_POLLING_TIME = 0.1
|
||||
EthereumClient.COOLING_TIME = 0
|
||||
|
||||
# Let's try to deploy a simple contract (ReceiveApprovalMethodMock) with 1 confirmation.
|
||||
# Since the testerchain doesn't automine, this fails.
|
||||
with pytest.raises(testerchain.NotEnoughConfirmations):
|
||||
_ = testerchain.deploy_contract(origin,
|
||||
test_registry,
|
||||
'ReceiveApprovalMethodMock',
|
||||
confirmations=10)
|
||||
# Since the testerchain doesn't mine new blocks automatically, this fails.
|
||||
with pytest.raises(EthereumClient.TransactionTimeout):
|
||||
_ = testerchain.deploy_contract(origin, test_registry, 'ReceiveApprovalMethodMock', confirmations=1)
|
||||
|
||||
# Trying again with no confirmation succeeds.
|
||||
contract, _ = testerchain.deploy_contract(origin,
|
||||
test_registry,
|
||||
'ReceiveApprovalMethodMock')
|
||||
contract, _ = testerchain.deploy_contract(origin, test_registry, 'ReceiveApprovalMethodMock')
|
||||
|
||||
# Trying a simple function of the contract with 1 confirmations fails too, for the same reason
|
||||
tx_function = contract.functions.receiveApproval(origin, 0, origin, b'')
|
||||
with pytest.raises(testerchain.NotEnoughConfirmations):
|
||||
_ = testerchain.send_transaction(contract_function=tx_function,
|
||||
sender_address=origin,
|
||||
confirmations=1)
|
||||
with pytest.raises(EthereumClient.TransactionTimeout):
|
||||
_ = testerchain.send_transaction(contract_function=tx_function, sender_address=origin, confirmations=1)
|
||||
|
||||
# Trying again with no confirmation succeeds.
|
||||
tx_receipt = testerchain.send_transaction(contract_function=tx_function,
|
||||
sender_address=origin,
|
||||
confirmations=0)
|
||||
|
||||
assert testerchain.get_confirmations(tx_receipt) == 0
|
||||
testerchain.w3.eth.web3.testing.mine(1)
|
||||
assert testerchain.get_confirmations(tx_receipt) == 1
|
||||
|
||||
# TODO: Find a way to test block confirmations. The following approach fails sometimes. Perhaps using a background threat that mines blocks?
|
||||
# # Ok, I admit that the tests so far weren't very exciting, since we cannot directly test confirmations
|
||||
# # as new blocks are not mined continuously in our test framework.
|
||||
# # Let's do something hacky and monkey-patch the method that checks the number of confirmations to
|
||||
# # mine a new block, say, each 5 seconds.
|
||||
#
|
||||
# get_confirmations = testerchain.get_confirmations
|
||||
#
|
||||
# def patched_get_confirmations(self, receipt):
|
||||
# now = maya.now().second
|
||||
# elapsed = now - patched_get_confirmations.timestamp
|
||||
# blocks = elapsed // 5
|
||||
# if blocks > 0:
|
||||
# testerchain.w3.eth.web3.testing.mine(blocks)
|
||||
# patched_get_confirmations.timestamp = now
|
||||
# return get_confirmations(receipt)
|
||||
#
|
||||
# patched_get_confirmations.timestamp = maya.now().second
|
||||
# testerchain.get_confirmations = types.MethodType(patched_get_confirmations, testerchain)
|
||||
#
|
||||
# # With a timeout of 30, now we can ask for 1 or 2 confirmations...
|
||||
# testerchain.TIMEOUT = 30
|
||||
# _ = testerchain.send_transaction(contract_function=tx_function,
|
||||
# sender_address=origin,
|
||||
# confirmations=1)
|
||||
#
|
||||
# _ = testerchain.send_transaction(contract_function=tx_function,
|
||||
# sender_address=origin,
|
||||
# confirmations=2)
|
||||
#
|
||||
# # ... but not 10, that's too much.
|
||||
# with pytest.raises(testerchain.NotEnoughConfirmations):
|
||||
# _ = testerchain.send_transaction(contract_function=tx_function,
|
||||
# sender_address=origin,
|
||||
# confirmations=10)
|
||||
receipt = testerchain.send_transaction(contract_function=tx_function, sender_address=origin, confirmations=0)
|
||||
assert receipt['status'] == 1
|
||||
|
|
|
@ -441,8 +441,8 @@ def _make_testerchain(mock_backend: bool = False) -> TesterBlockchain:
|
|||
if mock_backend:
|
||||
testerchain = MockBlockchain()
|
||||
else:
|
||||
testerchain = TesterBlockchain(eth_airdrop=not mock_backend,
|
||||
free_transactions=True)
|
||||
testerchain = TesterBlockchain(eth_airdrop=True, free_transactions=True)
|
||||
|
||||
return testerchain
|
||||
|
||||
|
||||
|
@ -475,7 +475,6 @@ def testerchain(_testerchain) -> TesterBlockchain:
|
|||
eth_amount = Web3().fromWei(spent, 'ether')
|
||||
testerchain.log.info("Airdropped {} ETH {} -> {}".format(eth_amount, tx['from'], tx['to']))
|
||||
|
||||
# if not BlockchainInterfaceFactory.is_interface_initialized(provider_uri=TEST_PROVIDER_URI):
|
||||
BlockchainInterfaceFactory.register_interface(interface=testerchain, force=True)
|
||||
# Mock TransactingPower Consumption (Deployer)
|
||||
testerchain.transacting_power = TransactingPower(password=INSECURE_DEVELOPMENT_PASSWORD,
|
||||
|
|
|
@ -20,6 +20,7 @@ from contextlib import contextmanager
|
|||
|
||||
from typing import Union
|
||||
|
||||
from nucypher.blockchain.eth.clients import EthereumClient
|
||||
from nucypher.blockchain.eth.constants import PREALLOCATION_ESCROW_CONTRACT_NAME
|
||||
from nucypher.blockchain.eth.networks import NetworksInventory
|
||||
from nucypher.blockchain.eth.registry import (BaseContractRegistry, CanonicalRegistrySource,
|
||||
|
@ -77,3 +78,9 @@ class MockBlockchain(TesterBlockchain):
|
|||
|
||||
def __init__(self):
|
||||
super().__init__(mock_backend=True)
|
||||
|
||||
|
||||
class MockEthereumClient(EthereumClient):
|
||||
|
||||
def __init__(self, w3):
|
||||
super().__init__(w3, None, None, None, None)
|
||||
|
|
|
@ -0,0 +1,179 @@
|
|||
"""
|
||||
This file is part of nucypher.
|
||||
|
||||
nucypher is free software: you can redistribute it and/or modify
|
||||
it under the terms of the GNU Affero General Public License as published by
|
||||
the Free Software Foundation, either version 3 of the License, or
|
||||
(at your option) any later version.
|
||||
|
||||
nucypher is distributed in the hope that it will be useful,
|
||||
but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
GNU Affero General Public License for more details.
|
||||
|
||||
You should have received a copy of the GNU Affero General Public License
|
||||
along with nucypher. If not, see <https://www.gnu.org/licenses/>.
|
||||
"""
|
||||
import time
|
||||
from unittest.mock import PropertyMock
|
||||
|
||||
import pytest
|
||||
from hexbytes import HexBytes
|
||||
from web3.exceptions import TransactionNotFound, TimeExhausted
|
||||
|
||||
from tests.mock.interfaces import MockEthereumClient
|
||||
|
||||
|
||||
@pytest.fixture(scope='function')
|
||||
def mock_ethereum_client(mocker):
|
||||
web3_mock = mocker.Mock()
|
||||
mock_client = MockEthereumClient(w3=web3_mock)
|
||||
return mock_client
|
||||
|
||||
|
||||
@pytest.fixture()
|
||||
def receipt():
|
||||
block_number_of_my_tx = 42
|
||||
my_tx_hash = HexBytes('0xFabadaAcabada')
|
||||
receipt = {
|
||||
'transactionHash': my_tx_hash,
|
||||
'blockNumber': block_number_of_my_tx,
|
||||
'blockHash': HexBytes('0xBebeCafe')
|
||||
}
|
||||
return receipt
|
||||
|
||||
|
||||
def test_check_transaction_is_on_chain(mocker, mock_ethereum_client, receipt):
|
||||
# Mocking Web3 and EthereumClient
|
||||
web3_mock = mock_ethereum_client.w3
|
||||
web3_mock.eth.getTransactionReceipt = mocker.Mock(return_value=receipt)
|
||||
|
||||
# Test with no chain reorganizations:
|
||||
|
||||
# While web3 keeps returning the same receipt that we initially had, all good
|
||||
assert mock_ethereum_client.check_transaction_is_on_chain(receipt=receipt)
|
||||
|
||||
# Test with chain re-organizations:
|
||||
|
||||
# Let's assume that our TX ends up mined in a different block, and we receive a new receipt
|
||||
new_receipt = dict(receipt)
|
||||
new_receipt.update({'blockHash': HexBytes('0xBebeCebada')})
|
||||
|
||||
web3_mock.eth.getTransactionReceipt = mocker.Mock(return_value=new_receipt)
|
||||
|
||||
exception = mock_ethereum_client.ChainReorganizationDetected
|
||||
message = exception(receipt=receipt).message
|
||||
with pytest.raises(exception, match=message):
|
||||
_ = mock_ethereum_client.check_transaction_is_on_chain(receipt=receipt)
|
||||
|
||||
# Another example: there has been a chain reorganization and our beloved TX is gone for good:
|
||||
web3_mock.eth.getTransactionReceipt = mocker.Mock(side_effect=TransactionNotFound)
|
||||
with pytest.raises(exception, match=message):
|
||||
_ = mock_ethereum_client.check_transaction_is_on_chain(receipt=receipt)
|
||||
|
||||
|
||||
def test_block_until_enough_confirmations(mocker, mock_ethereum_client, receipt):
|
||||
my_tx_hash = receipt['transactionHash']
|
||||
block_number_of_my_tx = receipt['blockNumber']
|
||||
|
||||
# Test that web3's TimeExhausted is propagated:
|
||||
web3_mock = mock_ethereum_client.w3
|
||||
web3_mock.eth.waitForTransactionReceipt = mocker.Mock(side_effect=TimeExhausted)
|
||||
|
||||
with pytest.raises(TimeExhausted):
|
||||
mock_ethereum_client.block_until_enough_confirmations(transaction_hash=my_tx_hash,
|
||||
timeout=1,
|
||||
confirmations=1)
|
||||
|
||||
# Test that NotEnoughConfirmations is raised when there are not enough confirmations.
|
||||
# In this case, we're going to mock eth.blockNumber to be stuck
|
||||
web3_mock.eth.waitForTransactionReceipt = mocker.Mock(return_value=receipt)
|
||||
web3_mock.eth.getTransactionReceipt = mocker.Mock(return_value=receipt)
|
||||
|
||||
type(web3_mock.eth).blockNumber = PropertyMock(return_value=block_number_of_my_tx) # See docs of PropertyMock
|
||||
|
||||
# Additional adjustments to make the test faster
|
||||
mocker.patch.object(mock_ethereum_client, '_calculate_confirmations_timeout', return_value=0.1)
|
||||
mock_ethereum_client.BLOCK_CONFIRMATIONS_POLLING_TIME = 0
|
||||
|
||||
with pytest.raises(mock_ethereum_client.NotEnoughConfirmations):
|
||||
mock_ethereum_client.block_until_enough_confirmations(transaction_hash=my_tx_hash,
|
||||
timeout=1,
|
||||
confirmations=1)
|
||||
|
||||
# Test that block_until_enough_confirmations keeps iterating until the required confirmations are obtained
|
||||
required_confirmations = 3
|
||||
new_blocks_sequence = range(block_number_of_my_tx, block_number_of_my_tx + required_confirmations + 1)
|
||||
type(web3_mock.eth).blockNumber = PropertyMock(side_effect=new_blocks_sequence) # See docs of PropertyMock
|
||||
spy_check_transaction = mocker.spy(mock_ethereum_client, 'check_transaction_is_on_chain')
|
||||
|
||||
returned_receipt = mock_ethereum_client.block_until_enough_confirmations(transaction_hash=my_tx_hash,
|
||||
timeout=1,
|
||||
confirmations=required_confirmations)
|
||||
assert receipt == returned_receipt
|
||||
assert required_confirmations + 1 == spy_check_transaction.call_count
|
||||
|
||||
|
||||
def test_wait_for_receipt_no_confirmations(mocker, mock_ethereum_client, receipt):
|
||||
my_tx_hash = receipt['transactionHash']
|
||||
|
||||
# Test that web3's TimeExhausted is propagated:
|
||||
web3_mock = mock_ethereum_client.w3
|
||||
web3_mock.eth.waitForTransactionReceipt = mocker.Mock(side_effect=TimeExhausted)
|
||||
with pytest.raises(TimeExhausted):
|
||||
_ = mock_ethereum_client.wait_for_receipt(transaction_hash=my_tx_hash, timeout=1, confirmations=0)
|
||||
web3_mock.eth.waitForTransactionReceipt.assert_called_once_with(transaction_hash=my_tx_hash,
|
||||
timeout=1,
|
||||
poll_latency=MockEthereumClient.TRANSACTION_POLLING_TIME)
|
||||
|
||||
# Test that when web3's layer returns the receipt, we get that receipt
|
||||
web3_mock.eth.waitForTransactionReceipt = mocker.Mock(return_value=receipt)
|
||||
returned_receipt = mock_ethereum_client.wait_for_receipt(transaction_hash=my_tx_hash, timeout=1, confirmations=0)
|
||||
assert receipt == returned_receipt
|
||||
web3_mock.eth.waitForTransactionReceipt.assert_called_once_with(transaction_hash=my_tx_hash,
|
||||
timeout=1,
|
||||
poll_latency=MockEthereumClient.TRANSACTION_POLLING_TIME)
|
||||
|
||||
|
||||
def test_wait_for_receipt_with_confirmations(mocker, mock_ethereum_client, receipt):
|
||||
my_tx_hash = receipt['transactionHash']
|
||||
|
||||
mock_ethereum_client.COOLING_TIME = 0 # Don't make test unnecessarily slow
|
||||
|
||||
time_spy = mocker.spy(time, 'sleep')
|
||||
# timeout_spy = mocker.spy(Timeout, 'check') # FIXME
|
||||
|
||||
# First, let's make a simple, successful call to check that:
|
||||
# - The same receipt goes through
|
||||
# - The cooling time is respected
|
||||
mock_ethereum_client.block_until_enough_confirmations = mocker.Mock(return_value=receipt)
|
||||
returned_receipt = mock_ethereum_client.wait_for_receipt(transaction_hash=my_tx_hash, timeout=1, confirmations=1)
|
||||
assert receipt == returned_receipt
|
||||
time_spy.assert_called_once_with(mock_ethereum_client.COOLING_TIME)
|
||||
# timeout_spy.assert_not_called() # FIXME
|
||||
|
||||
# Test that wait_for_receipt finishes when a receipt is returned by block_until_enough_confirmations
|
||||
sequence_of_events = (
|
||||
TimeExhausted,
|
||||
mock_ethereum_client.ChainReorganizationDetected(receipt),
|
||||
mock_ethereum_client.NotEnoughConfirmations,
|
||||
receipt
|
||||
)
|
||||
timeout = None
|
||||
|
||||
mock_ethereum_client.BLOCK_CONFIRMATIONS_POLLING_TIME = 0
|
||||
mock_ethereum_client.block_until_enough_confirmations = mocker.Mock(side_effect=sequence_of_events)
|
||||
|
||||
returned_receipt = mock_ethereum_client.wait_for_receipt(transaction_hash=my_tx_hash, timeout=timeout, confirmations=1)
|
||||
assert receipt == returned_receipt
|
||||
# assert timeout_spy.call_count == 3 # FIXME
|
||||
|
||||
# Test that a TransactionTimeout is thrown when no receipt is found during the given time
|
||||
timeout = 0.1
|
||||
sequence_of_events = [TimeExhausted] * 10
|
||||
mock_ethereum_client.BLOCK_CONFIRMATIONS_POLLING_TIME = 0.015
|
||||
mock_ethereum_client.block_until_enough_confirmations = mocker.Mock(side_effect=sequence_of_events)
|
||||
with pytest.raises(mock_ethereum_client.TransactionTimeout):
|
||||
_ = mock_ethereum_client.wait_for_receipt(transaction_hash=my_tx_hash,
|
||||
timeout=timeout,
|
||||
confirmations=1)
|
|
@ -20,8 +20,9 @@ import maya
|
|||
import os
|
||||
from eth_tester.exceptions import TransactionFailed
|
||||
from eth_utils import to_canonical_address
|
||||
from hexbytes import HexBytes
|
||||
from twisted.logger import Logger
|
||||
from typing import List, Tuple
|
||||
from typing import List, Tuple, Union
|
||||
from web3 import Web3
|
||||
|
||||
from nucypher.blockchain.economics import BaseEconomics, StandardTokenEconomics
|
||||
|
@ -266,16 +267,16 @@ class TesterBlockchain(BlockchainDeployerInterface):
|
|||
accounts = set(self.client.accounts)
|
||||
return list(accounts.difference(assigned_accounts))
|
||||
|
||||
def wait_for_receipt(self, txhash: bytes, timeout: int = None) -> dict:
|
||||
def wait_for_receipt(self, txhash: Union[bytes, str, HexBytes], timeout: int = None) -> dict:
|
||||
"""Wait for a transaction receipt and return it"""
|
||||
timeout = timeout or self.TIMEOUT
|
||||
result = self.w3.eth.waitForTransactionReceipt(txhash, timeout=timeout)
|
||||
result = self.client.wait_for_receipt(transaction_hash=txhash, timeout=timeout)
|
||||
if result.status == 0:
|
||||
raise TransactionFailed()
|
||||
return result
|
||||
|
||||
def get_block_number(self) -> int:
|
||||
return self.client.w3.eth.blockNumber
|
||||
return self.client.block_number
|
||||
|
||||
def read_storage_slot(self, address, slot):
|
||||
# https://github.com/ethereum/web3.py/issues/1490
|
||||
|
|
Loading…
Reference in New Issue