def _call_and_transact( contract_function: ContractFunction, transaction_params: Optional[Dict] = None ) -> str: """ Executes contract_function.{call, transaction}(transaction_params) and returns txhash """ # First 'call' might raise an exception contract_function.call(transaction_params) return contract_function.transact(transaction_params)
def call_and_transact( contract_function: ContractFunction, transaction_params: Optional[TxParams] = None, ) -> HexBytes: """ Executes contract_function.{call, transaction}(transaction_params) and returns txhash """ # First 'call' might raise an exception contract_function.call(transaction_params) return contract_function.transact(transaction_params)
def build_contract_transaction( self, contract_function: ContractFunction, sender_address: str, payload: dict = None, transaction_gas_limit: Optional[int] = None, gas_estimation_multiplier: Optional[float] = None, use_pending_nonce: Optional[bool] = None, ) -> dict: if transaction_gas_limit is not None: self.log.warn( "The transaction gas limit of {transaction_gas_limit} will override gas estimation attempts" ) # Sanity checks for the gas estimation multiplier if gas_estimation_multiplier is not None: if not 1 <= gas_estimation_multiplier <= 3: # Arbitrary upper bound. raise ValueError( f"The gas estimation multiplier should be a float between 1 and 3, " f"but we received {gas_estimation_multiplier}.") payload = self.build_payload( sender_address=sender_address, payload=payload, transaction_gas_limit=transaction_gas_limit, use_pending_nonce=use_pending_nonce) self.__log_transaction(transaction_dict=payload, contract_function=contract_function) try: if 'gas' not in payload: # i.e., transaction_gas_limit is not None # As web3 buildTransaction() will estimate gas with block identifier "pending" by default, # explicitly estimate gas here with block identifier 'latest' if not otherwise specified # as a pending transaction can cause gas estimation to fail, notably in case of worklock refunds. payload['gas'] = contract_function.estimateGas( payload, block_identifier='latest') transaction_dict = contract_function.buildTransaction(payload) except (TestTransactionFailed, ValidationError, ValueError) as error: # Note: Geth (1.9.15) raises ValueError in the same condition that pyevm raises ValidationError here. # Treat this condition as "Transaction Failed" during gas estimation. raise self._handle_failed_transaction( exception=error, transaction_dict=payload, contract_function=contract_function, logger=self.log) # Increase the estimated gas limit according to the gas estimation multiplier, if any. if gas_estimation_multiplier and not transaction_gas_limit: gas_estimation = transaction_dict['gas'] overestimation = int( math.ceil(gas_estimation * gas_estimation_multiplier)) self.log.debug( f"Gas limit for this TX was increased from {gas_estimation} to {overestimation}, " f"using a multiplier of {gas_estimation_multiplier}.") transaction_dict['gas'] = overestimation # TODO: What if we're going over the block limit? Not likely, but perhaps worth checking (NRN) return transaction_dict
def submit_update(network: str, web3_client: Web3, function_call: ContractFunction) -> None: tx_params = get_transaction_params(network, web3_client) estimated_gas = function_call.estimateGas(tx_params) # add 10% margin to the estimated gas tx_params["gas"] = int(estimated_gas * 0.1) + estimated_gas # execute transaction tx_hash = function_call.transact(tx_params) logger.info(f"[{network}] Submitted transaction: {Web3.toHex(tx_hash)}") wait_for_transaction(network, web3_client, tx_hash)
def execute_transaction(self, contract_function: ContractFunction, **kwargs) -> str: gas_price: int = self.gas_price transaction_args: Dict[str, Any] = { "from": self.address, "nonce": self.nonce, "chainId": self.chain.value, "gasPrice": gas_price, } transaction_args.update(kwargs) transaction: Dict[str, Any] = contract_function.buildTransaction( transaction_args) if "gas" not in transaction: estimate_gas: int = 1000000 try: estimate_gas = self._w3.eth.estimateGas(transaction) except ValueError: self.logger().error( f"Failed to estimate gas. Using default of 1000000.") transaction["gas"] = estimate_gas signed_transaction: AttributeDict = self._account.signTransaction( transaction) tx_hash: str = signed_transaction.hash.hex() self.schedule_eth_transaction(signed_transaction, gas_price) return tx_hash
def build_contract_transaction( self, contract_function: ContractFunction, sender_address: str, payload: dict = None, transaction_gas_limit: int = None, ) -> dict: payload = self.build_payload( sender_address=sender_address, payload=payload, transaction_gas_limit=transaction_gas_limit) self.__log_transaction(transaction_dict=payload, contract_function=contract_function) try: transaction_dict = contract_function.buildTransaction( payload) # Gas estimation occurs here except (TestTransactionFailed, ValidationError, ValueError) as error: # Note: Geth raises ValueError in the same condition that pyevm raises ValidationError here. # Treat this condition as "Transaction Failed" during gas estimation. raise self._handle_failed_transaction( exception=error, transaction_dict=payload, contract_function=contract_function, logger=self.log) return transaction_dict
def execute_transaction(self, contract_function: ContractFunction, **kwargs) -> str: """ This function WILL result in immediate network calls (e.g. to get the gas price, nonce and gas cost), even though it is written in sync manner. :param contract_function: :param kwargs: :return: """ if self._network_status is not NetworkStatus.CONNECTED: raise EnvironmentError("Cannot send transactions when network status is not connected.") gas_price: int = self.gas_price transaction_args: Dict[str, Any] = { "from": self.address, "nonce": self.nonce, "chainId": self.chain.value, "gasPrice": gas_price, } transaction_args.update(kwargs) transaction: Dict[str, Any] = contract_function.buildTransaction(transaction_args) if "gas" not in transaction: estimate_gas: int = 1000000 try: estimate_gas = self._w3.eth.estimateGas(transaction) except ValueError: self.logger().error(f"Failed to estimate gas. Using default of 1000000.") transaction["gas"] = estimate_gas signed_transaction: AttributeDict = self._account.signTransaction(transaction) tx_hash: str = signed_transaction.hash.hex() self.schedule_eth_transaction(signed_transaction, gas_price) return tx_hash
def transact(self, contract_method: ContractFunction) -> TxReceipt: """A wrapper around to_be_called.transact() that waits until the transaction succeeds.""" txhash = contract_method.transact(self.transaction) LOG.debug(f"Sending txHash={encode_hex(txhash)}") receipt, _ = check_successful_tx(web3=self.web3, txid=txhash, timeout=self.wait) return receipt
def build_transaction( self, contract_function: ContractFunction, sender_address: str, payload: dict = None, transaction_gas_limit: int = None, ) -> dict: # # Build # if not payload: payload = {} nonce = self.client.w3.eth.getTransactionCount(sender_address, 'pending') payload.update({ 'chainId': int(self.client.chain_id), 'nonce': nonce, 'from': sender_address, 'gasPrice': self.client.gas_price }) if transaction_gas_limit: payload['gas'] = int(transaction_gas_limit) # Get transaction type deployment = isinstance(contract_function, ContractConstructor) try: transaction_name = contract_function.fn_name.upper() except AttributeError: transaction_name = 'DEPLOY' if deployment else 'UNKNOWN' payload_pprint = dict(payload) payload_pprint['from'] = to_checksum_address(payload['from']) payload_pprint = ', '.join("{}: {}".format(k, v) for k, v in payload_pprint.items()) self.log.debug(f"[TX-{transaction_name}] | {payload_pprint}") # Build transaction payload try: unsigned_transaction = contract_function.buildTransaction(payload) except (ValidationError, ValueError) as e: # TODO: #1504 - Handle validation failures for gas limits, invalid fields, etc. # Note: Geth raises ValueError in the same condition that pyevm raises ValidationError here. # Treat this condition as "Transaction Failed". error = str(e).replace("{", "").replace("}", "") # See #724 self.log.critical(f"Validation error: {error}") raise else: if deployment: self.log.info( f"Deploying contract: {len(unsigned_transaction['data'])} bytes" ) return unsigned_transaction
def build_and_send_tx(self, func: ContractFunction, tx_params: Optional[TxParams] = None) -> HexBytes: if not tx_params: tx_params = self.get_tx_params() transaction = func.buildTransaction(tx_params) signed_tx = self.w3.eth.account.sign_transaction( transaction, private_key=self.secrets._pk) try: return self.w3.eth.send_raw_transaction(signed_tx.rawTransaction) finally: self.last_nonce = Nonce(tx_params["nonce"] + 1)
def build_contract_transaction( self, contract_function: ContractFunction, sender_address: str, payload: dict = None, transaction_gas_limit: Optional[int] = None, gas_estimation_multiplier: Optional[float] = None, use_pending_nonce: Optional[bool] = None, ) -> dict: # Sanity checks for the gas estimation multiplier if gas_estimation_multiplier is not None: if not 1 <= gas_estimation_multiplier <= 3: # TODO: Arbitrary upper bound. raise ValueError( f"The gas estimation multiplier should be a float between 1 and 3, " f"but we received {gas_estimation_multiplier}.") elif transaction_gas_limit is not None: raise ValueError( "'transaction_gas_limit' and 'gas_estimation_multiplier' can't be used together." ) payload = self.build_payload( sender_address=sender_address, payload=payload, transaction_gas_limit=transaction_gas_limit, use_pending_nonce=use_pending_nonce) self.__log_transaction(transaction_dict=payload, contract_function=contract_function) try: transaction_dict = contract_function.buildTransaction( payload) # Gas estimation occurs here except (TestTransactionFailed, ValidationError, ValueError) as error: # Note: Geth raises ValueError in the same condition that pyevm raises ValidationError here. # Treat this condition as "Transaction Failed" during gas estimation. raise self._handle_failed_transaction( exception=error, transaction_dict=payload, contract_function=contract_function, logger=self.log) # Overestimate the transaction gas limit according to the gas estimation multiplier, if any if gas_estimation_multiplier: gas_estimation = transaction_dict['gas'] overestimation = int( math.ceil(gas_estimation * gas_estimation_multiplier)) self.log.debug( f"Gas limit for this TX was increased from {gas_estimation} to {overestimation}, " f"using a multiplier of {gas_estimation_multiplier}.") transaction_dict['gas'] = overestimation # TODO: What if we're going over the block limit? Not likely, but perhaps worth checking (NRN) return transaction_dict
def __append_contract_functions(self, contract_address, contract_abi): self.__contract_functions = self.__filter_by_type( 'function', contract_abi) for funcName in self.__contract_functions.keys(): setattr( self, funcName, self.__decorateContractFunctions( ContractFunction.factory(funcName, web3=self.web3, contract_abi=contract_abi, address=contract_address, function_identifier=funcName)))
def estimate_transaction_cost(self, contract_function: ContractFunction, **kwargs) -> int: transaction_args: Dict[str, Any] = { "from": self.address, "nonce": self.nonce, "chainId": self.chain.value, "gasPrice": self.gas_price, } transaction_args.update(kwargs) transaction: Dict[str, Any] = contract_function.buildTransaction( transaction_args) gas_estimate = self._w3.eth.estimateGas(transaction) return gas_estimate * self.gas_price
def checked_transact( web3: Web3, sender_address: Address, function_call: ContractFunction, task_name: str, wait_confirmation_interval: bool = True, ) -> TxReceipt: log.info(f"Starting: {task_name}") transaction_hash = function_call.transact({"from": sender_address}) confirmation_msg = "" if wait_confirmation_interval: confirmation_msg = " and waiting for confirmation" click.secho( f"\nSending transaction{confirmation_msg}: {task_name}" f"\n\tSee {etherscan_url_for_txhash(web3.eth.chain_id, transaction_hash)}" ) timeout = 60 * 10 # 10mins transaction_receipt = web3.eth.wait_for_transaction_receipt(transaction_hash, timeout, 1.0) if wait_confirmation_interval: while ( "blockNumber" not in transaction_receipt or web3.eth.block_number < transaction_receipt["blockNumber"] + DEFAULT_NUMBER_OF_BLOCK_CONFIRMATIONS ): transaction_receipt = web3.eth.wait_for_transaction_receipt( transaction_hash, timeout, 1.0 ) gevent.sleep(1) was_successful = transaction_receipt["status"] == 1 if not was_successful: log.error( f"Failed: {task_name}\nPlease check that the account " f"{to_checksum_address(sender_address)} has sufficient funds.", receipt=transaction_receipt, ) sys.exit(1) log.info( f"Finished: {task_name}", successful=was_successful, transaction_hash=to_hex(transaction_hash), ) return transaction_receipt
def _build_payload_same_function( self, contract_function: ContractFunction, contract_addresses: Sequence[ChecksumAddress], ) -> Tuple[List[Tuple[ChecksumAddress, bytes]], List[List[Any]]]: targets_with_data = [] output_types = [] tx_data = HexBytes(contract_function._encode_transaction_data()) for contract_address in contract_addresses: targets_with_data.append((contract_address, tx_data)) output_types.append( [output["type"] for output in contract_function.abi["outputs"]] ) return targets_with_data, output_types
def build_transaction( self, contract_function: ContractFunction, sender_address: str, payload: dict = None, transaction_gas_limit: int = None, ) -> dict: # # Build Payload # base_payload = { 'chainId': int(self.client.chain_id), 'nonce': self.client.w3.eth.getTransactionCount(sender_address, 'pending'), 'from': sender_address, 'gasPrice': self.client.gas_price } # Aggregate if not payload: payload = {} payload.update(base_payload) # Explicit gas override - will skip gas estimation in next operation. if transaction_gas_limit: payload['gas'] = int(transaction_gas_limit) # # Build Transaction # self.__log_transaction(transaction_dict=payload, contract_function=contract_function) try: transaction_dict = contract_function.buildTransaction( payload) # Gas estimation occurs here except (TestTransactionFailed, ValidationError, ValueError) as error: # Note: Geth raises ValueError in the same condition that pyevm raises ValidationError here. # Treat this condition as "Transaction Failed" during gas estimation. raise self.__transaction_failed( exception=error, transaction_dict=payload, contract_function=contract_function) return transaction_dict
def _build_and_send_tx(self, function: ContractFunction, tx_params: Optional[TxParams] = None) -> HexBytes: """Build and send a transaction.""" if not tx_params: tx_params = self._get_tx_params() transaction = function.buildTransaction(tx_params) signed_txn = self.w3.eth.account.sign_transaction( transaction, private_key=self.private_key) # TODO: This needs to get more complicated if we want to support replacing a transaction # FIXME: This does not play nice if transactions are sent from other places using the same wallet. try: return self.w3.eth.sendRawTransaction(signed_txn.rawTransaction) finally: logger.debug(f"nonce: {tx_params['nonce']}") self.last_nonce = Nonce(tx_params["nonce"] + 1)
def build_and_send_tx(function: ContractFunction, tx_params: Optional[TxParams] = None) -> HexBytes: """ Build and send a transaction. """ if not tx_params: tx_params = get_tx_params() transaction = function.buildTransaction(tx_params) signed_txn = w3.eth.account.sign_transaction(transaction, private_key=PRIVATE_KEY) logging.info(signed_txn.rawTransaction) # !--- Commented for test. # return w3.eth.sendRawTransaction(signed_txn.rawTransaction) return w3.eth.send_raw_transaction(signed_txn.rawTransaction)
def _build_and_send_tx(self, function: ContractFunction, tx_params: Optional[TxParams] = None) -> HexBytes: """Build and send a transaction.""" if not tx_params: tx_params = self._get_tx_params() transaction = function.buildTransaction(tx_params) signed_txn = self.w3.eth.account.sign_transaction( transaction, private_key=self.private_key) try: return self.w3.eth.sendRawTransaction(signed_txn.rawTransaction) finally: print("Print TX = ", self.w3.toHex(self.w3.keccak(signed_txn.rawTransaction))) logger.debug(f"nonce: {tx_params['nonce']}") self.last_nonce = Nonce(tx_params["nonce"] + 1) #LOGG TX TO JSON with open('transactions.json', 'r') as fp: data = json.load(fp) tx_input = tx_params data.append(tx_input) with open('transactions.json', 'w') as fp: json.dump(data, fp, indent=2) fp.close() last_tx = self.w3.toHex(self.w3.keccak(signed_txn.rawTransaction)) hash = [{'hash': last_tx}] with open('hash.json', 'w') as fp: json.dump(hash, fp, indent=2) fp.close() print("Waiting for TX to Confirm") while True: try: if self.w3.eth.getTransactionReceipt(last_tx) != None: print("Transaction Confirmed!") break else: print("waiting for tx to confirm") except Exception: pass time.sleep(5)
def checked_transact( web3: Web3, service_address: Address, function_call: ContractFunction, task_name: str ) -> None: log.info(f"Starting: {task_name}") transaction_hash = function_call.transact({"from": service_address}) transaction_receipt = web3.eth.waitForTransactionReceipt(transaction_hash) was_successful = transaction_receipt["status"] == 1 if not was_successful: log.error( f"Failed: {task_name}\nPlease check that the account " f"{to_checksum_address(service_address)} has sufficient funds.", receipt=transaction_receipt, ) sys.exit(1) log.info( f"Finished: {task_name}", successful=was_successful, transaction_hash=to_hex(transaction_hash), )
def send_transaction(self, contract_function: ContractFunction, sender_address: str, payload: dict = None, ) -> dict: if self.transacting_power is READ_ONLY_INTERFACE: raise self.InterfaceError(str(READ_ONLY_INTERFACE)) # # Build # if not payload: payload = {} nonce = self.client.w3.eth.getTransactionCount(sender_address) payload.update({'chainId': int(self.client.chain_id), 'nonce': nonce, 'from': sender_address, 'gasPrice': self.client.w3.eth.gasPrice}) # Get interface name try: transaction_name = contract_function.fn_name.upper() except AttributeError: if isinstance(contract_function, ContractConstructor): transaction_name = 'DEPLOY' else: transaction_name = 'UNKNOWN' # Build transaction payload try: unsigned_transaction = contract_function.buildTransaction(payload) except ValidationError: # TODO: Handle validation failures for gas limits, invalid fields, etc. raise # # Broadcast # signed_raw_transaction = self.transacting_power.sign_transaction(unsigned_transaction) txhash = self.client.send_raw_transaction(signed_raw_transaction) self.log.debug(f"[TX-{transaction_name}] | {to_checksum_address(payload['from'])}") try: receipt = self.client.wait_for_receipt(txhash, timeout=self.TIMEOUT) except TimeExhausted: # TODO: Handle transaction timeout raise else: self.log.debug(f"[RECEIPT-{transaction_name}] | {receipt['transactionHash'].hex()}") # # Confirm # # Primary check deployment_status = receipt.get('status', UNKNOWN_TX_STATUS) if deployment_status is 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: self.log.info(f"Unknown transaction status for {txhash} (receipt did not contain a status field)") # Secondary check TODO: Is this a sensible check? tx = self.client.get_transaction(txhash) if tx["gas"] == receipt["gasUsed"]: raise self.InterfaceError(f"Transaction consumed 100% of transaction gas." f"Full receipt: \n {pprint.pformat(receipt, indent=2)}") return receipt
def send_transaction( self, transaction_function: ContractFunction, sender_address: str, payload: dict = None, ) -> dict: if self.transacting_power is READ_ONLY_INTERFACE: raise self.InterfaceError(str(READ_ONLY_INTERFACE)) # # Build # if not payload: payload = {} nonce = self.client.w3.eth.getTransactionCount(sender_address) payload.update({ 'chainId': int(self.client.chain_id), 'nonce': nonce, 'from': sender_address, 'gasPrice': self.client.w3.eth.gasPrice }) unsigned_transaction = transaction_function.buildTransaction(payload) # # Broadcast # signed_raw_transaction = self.transacting_power.sign_transaction( unsigned_transaction) txhash = self.client.send_raw_transaction(signed_raw_transaction) try: receipt = self.client.wait_for_receipt(txhash, timeout=self.TIMEOUT) except TimeExhausted: raise # # Confirm # # Primary check deployment_status = receipt.get('status', UNKNOWN_TX_STATUS) if deployment_status is 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: self.log.info( f"Unknown transaction status for {txhash} (receipt did not contain a status field)" ) # Secondary check TODO: Is this a sensible check? tx = self.client.get_transaction(txhash) if tx["gas"] == receipt["gasUsed"]: raise self.InterfaceError( f"Transaction consumed 100% of transaction gas." f"Full receipt: \n {pprint.pformat(receipt, indent=2)}") return receipt
def send_transaction( self, contract_function: ContractFunction, sender_address: str, payload: dict = None, transaction_gas_limit: int = None, ) -> dict: if self.transacting_power is READ_ONLY_INTERFACE: raise self.InterfaceError(str(READ_ONLY_INTERFACE)) # # Build # if not payload: payload = {} nonce = self.client.w3.eth.getTransactionCount(sender_address, 'pending') payload.update({ 'chainId': int(self.client.chain_id), 'nonce': nonce, 'from': sender_address, 'gasPrice': self.client.gas_price }) if transaction_gas_limit: payload['gas'] = int(transaction_gas_limit) # Get interface name deployment = True if isinstance(contract_function, ContractConstructor) else False try: transaction_name = contract_function.fn_name.upper() except AttributeError: if deployment: transaction_name = 'DEPLOY' else: transaction_name = 'UNKNOWN' payload_pprint = dict(payload) payload_pprint['from'] = to_checksum_address(payload['from']) payload_pprint = ', '.join("{}: {}".format(k, v) for k, v in payload_pprint.items()) self.log.debug(f"[TX-{transaction_name}] | {payload_pprint}") # Build transaction payload try: unsigned_transaction = contract_function.buildTransaction(payload) except (ValidationError, ValueError) as e: # TODO: #1504 - Handle validation failures for gas limits, invalid fields, etc. # Note: Geth raises ValueError in the same condition that pyevm raises ValidationError here. # Treat this condition as "Transaction Failed". error = str(e).replace("{", "{{").replace("}", "}}") # See #724 self.log.critical(f"Validation error: {error}") raise else: if deployment: self.log.info( f"Deploying contract: {len(unsigned_transaction['data'])} bytes" ) # # Broadcast # signed_raw_transaction = self.transacting_power.sign_transaction( unsigned_transaction) txhash = self.client.send_raw_transaction(signed_raw_transaction) try: receipt = self.client.wait_for_receipt(txhash, timeout=self.TIMEOUT) except TimeExhausted: # TODO: #1504 - Handle transaction timeout raise else: self.log.debug( f"[RECEIPT-{transaction_name}] | txhash: {receipt['transactionHash'].hex()}" ) # # Confirm # # Primary check deployment_status = receipt.get('status', UNKNOWN_TX_STATUS) if deployment_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: self.log.info( f"Unknown transaction status for {txhash} (receipt did not contain a status field)" ) # Secondary check tx = self.client.get_transaction(txhash) if tx["gas"] == receipt["gasUsed"]: raise self.InterfaceError( f"Transaction consumed 100% of transaction gas." f"Full receipt: \n {pprint.pformat(receipt, indent=2)}") return receipt