def test_transact_throws_opcode(deploy_client: JSONRPCClient) -> None: """ The receipt status field of a transaction that hit an assert or require is 0x0 """ contract_proxy, _ = deploy_rpc_test_contract(deploy_client, "RpcTest") address = contract_proxy.address assert len(deploy_client.web3.eth.getCode(address)) > 0 # the method always fails, so the gas estimation returns 0 here, using a # hardcoded a value to circumvent gas estimation. estimated_gas = safe_gas_limit(22000) gas_price = gas_price_for_fast_transaction(deploy_client.web3) block = deploy_client.get_block(BLOCK_ID_LATEST) estimated_transaction_fail_assert = TransactionEstimated( from_address=address, data=SmartContractCall(contract_proxy, "fail_assert", (), {}, value=0), eth_node=deploy_client.eth_node, extra_log_details={}, estimated_gas=estimated_gas, gas_price=gas_price, approximate_block=(block["hash"], block["number"]), ) transaction_fail_assert_sent = deploy_client.transact( estimated_transaction_fail_assert) transaction_fail_assert_mined = deploy_client.poll_transaction( transaction_fail_assert_sent) msg = "Transaction must have failed" assert not was_transaction_successfully_mined( transaction_fail_assert_mined), msg estimated_transaction_fail_require = TransactionEstimated( from_address=address, data=SmartContractCall(contract_proxy, "fail_require", (), {}, value=0), eth_node=deploy_client.eth_node, extra_log_details={}, estimated_gas=estimated_gas, gas_price=gas_price, approximate_block=(block["hash"], block["number"]), ) transaction_fail_require_sent = deploy_client.transact( estimated_transaction_fail_require) transaction_fail_require_mined = deploy_client.poll_transaction( transaction_fail_require_sent) msg = "Transaction must have failed" assert not was_transaction_successfully_mined( transaction_fail_require_mined), msg
def mint_for(self, amount: TokenAmount, address: Address) -> TransactionHash: """Try to mint tokens by calling `mintFor`. Raises: MintFailed if anything goes wrong. Returns: TransactionHash of the successfully mined Ethereum transaction associated with the token mint. """ extra_log_details: Dict[str, Any] = {} estimated_transaction = self.client.estimate_gas( self.proxy, "mintFor", extra_log_details, amount, address) if estimated_transaction is not None: transaction_sent = self.client.transact(estimated_transaction) transaction_mined = self.client.poll_transaction(transaction_sent) if not was_transaction_successfully_mined(transaction_mined): raise MintFailed( "Call to contract method mintFor: Transaction failed.") else: return transaction_mined.transaction_hash else: raise MintFailed( "Gas estimation failed. Make sure the token has a method " "named mintFor(uint256,address).")
def set_url(self, url: str) -> TransactionHash: """Sets the url needed to access the service via HTTP for the caller""" if not url.strip(): msg = "Invalid empty URL" raise BrokenPreconditionError(msg) parsed_url = urlparse(url) if parsed_url.scheme not in ("http", "https"): msg = "URL provided to service registry must be a valid HTTP(S) endpoint." raise BrokenPreconditionError(msg) extra_log_details: Dict[str, Any] = {} estimated_transaction = self.client.estimate_gas( self.proxy, "setURL", extra_log_details, url) if estimated_transaction is None: msg = f"URL {url} is invalid" raise RaidenUnrecoverableError(msg) transaction_sent = self.client.transact(estimated_transaction) transaction_mined = self.client.poll_transaction(transaction_sent) if not was_transaction_successfully_mined(transaction_mined): msg = f"URL {url} is invalid" raise RaidenUnrecoverableError(msg) else: return transaction_mined.transaction_hash
def _withdraw_check_result( self, transaction_sent: Optional[TransactionSent], amount_to_withdraw: TokenAmount, token: Token, previous_token_balance: TokenAmount, ) -> None: if transaction_sent is None: failed_at = self.client.get_block(BLOCK_ID_LATEST) failed_at_blocknumber = failed_at["number"] self.client.check_for_insufficient_eth( transaction_name="withdraw", transaction_executed=False, required_gas=self.gas_measurements["UserDeposit.withdraw"], block_identifier=failed_at_blocknumber, ) raise RaidenRecoverableError( "Withdraw transaction failed to be sent for an unknown reason." ) transaction_mined = self.client.poll_transaction(transaction_sent) if not was_transaction_successfully_mined(transaction_mined): failed_at_blocknumber = BlockNumber(transaction_mined.receipt["blockNumber"]) withdraw_plan = self.get_withdraw_plan( withdrawer_address=self.node_address, block_identifier=failed_at_blocknumber ) whole_balance = self.whole_balance(block_identifier=failed_at_blocknumber) if amount_to_withdraw > withdraw_plan.withdraw_amount: raise RaidenRecoverableError( f"Couldn't withdraw {amount_to_withdraw}, " f"current withdraw plan only allows for {withdraw_plan.withdraw_amount}." ) if withdraw_plan.withdraw_block > failed_at_blocknumber: raise RaidenRecoverableError( f"Couldn't withdraw at block {failed_at_blocknumber}. " f"The current withdraw plan requires block number " f"{withdraw_plan.withdraw_block}." ) if whole_balance - amount_to_withdraw < 0: raise RaidenRecoverableError( f"The current whole balance is {whole_balance}. " f"The withdraw of {amount_to_withdraw} would have lead to an underflow." ) current_token_balance = TokenAmount( token.balance_of(self.node_address, block_identifier=failed_at_blocknumber) ) # FIXME: This seems fishy. The token balance could have an unexpected value for # unrelated reasons (e.g. concurrent token transfers via transferFrom). if current_token_balance != previous_token_balance + amount_to_withdraw: raise RaidenRecoverableError("Token transfer during withdraw failed.") raise RaidenRecoverableError("Withdraw failed for an unknown reason.")
def test_transact_opcode(deploy_client: JSONRPCClient) -> None: """ The receipt status field of a transaction that did not throw is 0x1 """ contract_proxy, _ = deploy_rpc_test_contract(deploy_client, "RpcTest") address = contract_proxy.address assert len(deploy_client.web3.eth.getCode(address)) > 0 estimated_transaction = deploy_client.estimate_gas(contract_proxy, "ret", {}) assert estimated_transaction estimated_transaction.estimated_gas *= 2 transaction_sent = deploy_client.transact(estimated_transaction) transaction_mined = deploy_client.poll_transaction(transaction_sent) assert was_transaction_successfully_mined( transaction_mined), "Transaction must be succesfull"
def test_transact_opcode_oog(deploy_client: JSONRPCClient) -> None: """ The receipt status field of a transaction that did NOT throw is 0x0. """ contract_proxy, _ = deploy_rpc_test_contract(deploy_client, "RpcTest") address = contract_proxy.address assert len(deploy_client.web3.eth.getCode(address)) > 0 # divide the estimate by 2 to run into out-of-gas estimated_transaction = deploy_client.estimate_gas(contract_proxy, "loop", {}, 1000) assert estimated_transaction estimated_transaction.estimated_gas //= 2 transaction_sent = deploy_client.transact(estimated_transaction) transaction_mined = deploy_client.poll_transaction(transaction_sent) msg = "Transaction must be succesfull" assert not was_transaction_successfully_mined(transaction_mined), msg
def deposit(self, block_identifier: BlockIdentifier, limit_amount: TokenAmount) -> None: """Makes a deposit to create or extend a registration""" extra_log_details = {"given_block_identifier": block_identifier} estimated_transaction = self.client.estimate_gas( self.proxy, "deposit", extra_log_details, limit_amount) if estimated_transaction is None: msg = "ServiceRegistry.deposit transaction fails" raise RaidenUnrecoverableError(msg) transaction_sent = self.client.transact(estimated_transaction) transaction_mined = self.client.poll_transaction(transaction_sent) if not was_transaction_successfully_mined(transaction_mined): msg = "ServiceRegistry.deposit transaction failed" raise RaidenUnrecoverableError(msg)
def _plan_withdraw_check_result( self, transaction_sent: Optional[TransactionSent], amount_to_plan_withdraw: TokenAmount ) -> Optional[TransactionMined]: if transaction_sent is None: failed_at = self.client.get_block(BLOCK_ID_LATEST) failed_at_blocknumber = failed_at["number"] self.client.check_for_insufficient_eth( transaction_name="planWithdraw", transaction_executed=False, required_gas=self.gas_measurements["UserDeposit.planWithdraw"], block_identifier=failed_at_blocknumber, ) raise RaidenRecoverableError( "Plan withdraw transaction failed to be sent for an unknown reason." ) transaction_mined = self.client.poll_transaction(transaction_sent) if not was_transaction_successfully_mined(transaction_mined): if amount_to_plan_withdraw <= 0: raise RaidenRecoverableError( f"Planned withdraw amount was <= 0: {amount_to_plan_withdraw}." ) failed_at_blocknumber = BlockNumber( transaction_mined.receipt["blockNumber"]) current_balance = self.get_total_deposit( address=self.node_address, block_identifier=failed_at_blocknumber) if current_balance < amount_to_plan_withdraw: raise RaidenRecoverableError( f"Couldn't plan withdraw because planned amount " f"{amount_to_plan_withdraw} exceeded current balance of {current_balance}." ) raise RaidenRecoverableError( "Plan withdraw failed for an unknown reason.") return transaction_mined
def mint(self, amount: TokenAmount) -> None: """ Try to mint tokens by calling `mint`. Raises: MintFailed if anything goes wrong. """ extra_log_details: Dict[str, Any] = {} estimated_transaction = self.client.estimate_gas( self.proxy, "mint", extra_log_details, amount) if estimated_transaction is not None: transaction_sent = self.client.transact(estimated_transaction) transaction_mined = self.client.poll_transaction(transaction_sent) if not was_transaction_successfully_mined(transaction_mined): raise MintFailed("Mint failed.") else: raise MintFailed( "Gas estimation failed. Make sure the token has a method mint(uint256)." )
def _register_secret_batch( self, secrets_to_register: List[Secret], transaction_result: AsyncResult, log_details: Dict[str, Any], ) -> None: estimated_transaction = self.client.estimate_gas( self.proxy, "registerSecretBatch", log_details, secrets_to_register ) msg = None transaction_mined = None if estimated_transaction is not None: estimated_transaction.estimated_gas = safe_gas_limit( estimated_transaction.estimated_gas, len(secrets_to_register) * GAS_REQUIRED_PER_SECRET_IN_BATCH, ) try: transaction_sent = self.client.transact(estimated_transaction) transaction_mined = self.client.poll_transaction(transaction_sent) except Exception as e: # pylint: disable=broad-except msg = f"Unexpected exception {e} at sending registerSecretBatch transaction." # Clear `open_secret_transactions` regardless of the transaction being # successfully executed or not. with self._open_secret_transactions_lock: for secret in secrets_to_register: self.open_secret_transactions.pop(secret) # As of version `0.4.0` of the contract has *no* asserts or requires. # Therefore the only reason for the transaction to fail is if there is # a bug. unrecoverable_error = transaction_mined is None or not was_transaction_successfully_mined( transaction_mined ) exception: Union[RaidenRecoverableError, RaidenUnrecoverableError] if unrecoverable_error: # If the transaction was sent it must not fail. If this happened # some of our assumptions is broken therefore the error is # unrecoverable if transaction_mined is not None: receipt = transaction_mined.receipt if receipt["gasUsed"] == transaction_mined.startgas: # The transaction failed and all gas was used. This can # happen because of: # # - A compiler bug if an invalid opcode was executed. # - A configuration bug if an assert was executed, # because version 0.4.0 of the secret registry does not have an # assert. # - An ethereum client bug if the gas_limit was # underestimated. # # Safety cannot be guaranteed under any of these cases, # this error is unrecoverable. error = ( "Secret registration failed because of a bug in either " "the solidity compiler, the running ethereum client, or " "a configuration error in Raiden." ) else: # The transaction failed and *not* all gas was used. This # can happen because of: # # - A compiler bug if a revert was introduced. # - A configuration bug, because for 0.4.0 the secret # registry does not have a revert. error = ( "Secret registration failed because of a configuration " "bug or compiler bug. Please double check the secret " "smart contract is at version 0.4.0, if it is then a " "compiler bug was hit." ) exception = RaidenUnrecoverableError(error) transaction_result.set_exception(exception) raise exception # If gas_limit is set and there is no receipt then an exception was # raised while sending the transaction. This should only happen if # the account is being used concurrently, which is not supported. # This can happen because: # # - The nonce of the transaction was already used # - The nonce was reused *and* the account didn't have enough ether # to pay for the gas # # Safety cannot be guaranteed under any of these cases, this error # is unrecoverable. *Note*: This assumes the ethereum client # takes into account the current transactions in the pool. if estimated_transaction is not None: assert msg, "Unexpected control flow, an exception should have been raised." error = ( f"Sending the the transaction for registerSecretBatch " f"failed with: `{msg}`. This happens if the same ethereum " f"account is being used by more than one program which is not " f"supported." ) exception = RaidenUnrecoverableError(error) transaction_result.set_exception(exception) raise exception # gas_limit can fail because: # # - The Ethereum client detected the transaction could not # successfully execute, this happens if an assert/revert is hit. # - The account is lacking funds to pay for the gas. # # Either of these is a bug. The contract does not use # assert/revert, and the account should always be funded self.client.check_for_insufficient_eth( transaction_name="registerSecretBatch", transaction_executed=True, required_gas=GAS_REQUIRED_PER_SECRET_IN_BATCH * len(secrets_to_register), block_identifier=self.client.get_checking_block(), ) error = "Call to registerSecretBatch couldn't be done" exception = RaidenRecoverableError(error) transaction_result.set_exception(exception) raise exception # The local **MUST** transaction_result be set before waiting for the # other results, otherwise we have a dead-lock assert transaction_mined is not None, MYPY_ANNOTATION transaction_result.set(transaction_mined.transaction_hash)
def _deposit_check_result( self, transaction_sent: Optional[TransactionSent], token: Token, beneficiary: Address, total_deposit: TokenAmount, amount_to_deposit: TokenAmount, ) -> None: if transaction_sent is None: failed_at = self.client.get_block(BLOCK_ID_LATEST) failed_at_blocknumber = failed_at["number"] self.client.check_for_insufficient_eth( transaction_name="deposit", transaction_executed=False, required_gas=self.gas_measurements["UserDeposit.deposit"], block_identifier=failed_at_blocknumber, ) latest_deposit = self.get_total_deposit( address=beneficiary, block_identifier=failed_at_blocknumber ) amount_to_deposit = TokenAmount(total_deposit - latest_deposit) allowance = token.allowance( owner=self.node_address, spender=Address(self.address), block_identifier=failed_at_blocknumber, ) whole_balance = self.whole_balance(block_identifier=failed_at_blocknumber) whole_balance_limit = self.whole_balance_limit(block_identifier=failed_at_blocknumber) if allowance < amount_to_deposit: msg = ( "The allowance is insufficient. Check concurrent deposits " "for the same user deposit but different proxies." ) raise RaidenRecoverableError(msg) if token.balance_of(self.node_address, failed_at_blocknumber) < amount_to_deposit: msg = "The address doesnt have enough tokens" raise RaidenRecoverableError(msg) if latest_deposit < total_deposit: msg = "Deposit amount did not increase after deposit transaction" raise RaidenRecoverableError(msg) if whole_balance + amount_to_deposit > UINT256_MAX: msg = ( f"Current whole balance is {whole_balance}. " f"The new deposit of {amount_to_deposit} would lead to an overflow." ) raise RaidenRecoverableError(msg) if whole_balance + amount_to_deposit > whole_balance_limit: msg = ( f"Current whole balance is {whole_balance}. " f"With the new deposit of {amount_to_deposit}, the deposit " f"limit of {whole_balance_limit} would be exceeded." ) raise RaidenRecoverableError(msg) raise RaidenRecoverableError("Deposit failed of unknown reason") else: transaction_mined = self.client.poll_transaction(transaction_sent) if not was_transaction_successfully_mined(transaction_mined): failed_at_blocknumber = BlockNumber(transaction_mined.receipt["blockNumber"]) latest_deposit = self.get_total_deposit( address=beneficiary, block_identifier=failed_at_blocknumber ) amount_to_deposit = TokenAmount(total_deposit - latest_deposit) allowance = token.allowance( owner=self.node_address, spender=Address(self.address), block_identifier=failed_at_blocknumber, ) whole_balance = self.whole_balance(block_identifier=failed_at_blocknumber) whole_balance_limit = self.whole_balance_limit( block_identifier=failed_at_blocknumber ) if latest_deposit >= total_deposit: msg = "Deposit amount already increased after another transaction" raise RaidenRecoverableError(msg) if allowance < amount_to_deposit: msg = ( "The allowance is insufficient. Check concurrent deposits " "for the same token network but different proxies." ) raise RaidenRecoverableError(msg) # Because we acquired the lock for the token, and the gas estimation succeeded, # We know that the account had enough balance for the deposit transaction. if token.balance_of(self.node_address, failed_at_blocknumber) < amount_to_deposit: msg = ( "Transaction failed and balance decreased unexpectedly. " "This could be a bug in Raiden or a mallicious " "ERC20 Token." ) raise RaidenRecoverableError(msg) if whole_balance + amount_to_deposit > UINT256_MAX: msg = ( f"Current whole balance is {whole_balance}. " f"The new deposit of {amount_to_deposit} caused an overflow." ) raise RaidenRecoverableError(msg) if whole_balance + amount_to_deposit > whole_balance_limit: msg = ( f"Current whole balance is {whole_balance}. " f"With the new deposit of {amount_to_deposit}, the deposit " f"limit of {whole_balance_limit} was exceeded." ) raise RaidenRecoverableError(msg) if latest_deposit < total_deposit: msg = "Deposit amount did not increase after deposit transaction" raise RaidenRecoverableError(msg) raise RaidenRecoverableError("Deposit failed of unknown reason")
def _init( self, monitoring_service_address: MonitoringServiceAddress, one_to_n_address: OneToNAddress ) -> None: log_details: Dict[str, Any] = {} estimated_transaction = self.client.estimate_gas( self.proxy, "init", log_details, monitoring_service_address, one_to_n_address ) if estimated_transaction is None: failed_at = self.client.get_block(BLOCK_ID_LATEST) failed_at_blocknumber = failed_at["number"] self.client.check_for_insufficient_eth( transaction_name="init", transaction_executed=False, required_gas=self.gas_measurements["UserDeposit.init"], block_identifier=failed_at_blocknumber, ) existing_monitoring_service_address = self.monitoring_service_address( block_identifier=failed_at_blocknumber ) existing_one_to_n_address = self.one_to_n_address( block_identifier=failed_at_blocknumber ) if existing_monitoring_service_address != EMPTY_ADDRESS: msg = ( f"MonitoringService contract address was set to " f"{to_checksum_address(existing_monitoring_service_address)}" ) raise RaidenRecoverableError(msg) if existing_one_to_n_address != EMPTY_ADDRESS: msg = ( f"OneToN contract address was set to " f"{to_checksum_address(existing_one_to_n_address)}" ) raise RaidenRecoverableError(msg) raise RaidenRecoverableError("Deposit failed of unknown reason") else: transaction_sent = self.client.transact(estimated_transaction) transaction_mined = self.client.poll_transaction(transaction_sent) if not was_transaction_successfully_mined(transaction_mined): failed_at_blocknumber = BlockNumber(transaction_mined.receipt["blockNumber"]) existing_monitoring_service_address = self.monitoring_service_address( block_identifier=failed_at_blocknumber ) existing_one_to_n_address = self.one_to_n_address( block_identifier=failed_at_blocknumber ) if existing_monitoring_service_address != EMPTY_ADDRESS: msg = ( f"MonitoringService contract address was set to " f"{to_checksum_address(existing_monitoring_service_address)}" ) raise RaidenRecoverableError(msg) if existing_one_to_n_address != EMPTY_ADDRESS: msg = ( f"OneToN contract address was set to " f"{to_checksum_address(existing_one_to_n_address)}" ) raise RaidenRecoverableError(msg) raise RaidenRecoverableError("Deposit failed of unknown reason")
def _add_token( self, token_address: TokenAddress, channel_participant_deposit_limit: TokenAmount, token_network_deposit_limit: TokenAmount, log_details: Dict[Any, Any], ) -> Tuple[TransactionHash, TokenNetworkAddress]: token_network_address = None kwargs = { "_token_address": token_address, "_channel_participant_deposit_limit": channel_participant_deposit_limit, "_token_network_deposit_limit": token_network_deposit_limit, } estimated_transaction = self.rpc_client.estimate_gas( self.proxy, "createERC20TokenNetwork", log_details, **kwargs) if estimated_transaction is not None: estimated_transaction.estimated_gas = safe_gas_limit( estimated_transaction.estimated_gas, self.gas_measurements[ "TokenNetworkRegistry createERC20TokenNetwork"], ) transaction_sent = self.rpc_client.transact(estimated_transaction) transaction_mined = self.rpc_client.poll_transaction( transaction_sent) receipt = transaction_mined.receipt if not was_transaction_successfully_mined(transaction_mined): failed_at_blocknumber = BlockNumber(receipt["blockNumber"]) max_token_networks = self.get_max_token_networks( block_identifier=failed_at_blocknumber) token_networks_created = self.get_token_network_created( block_identifier=failed_at_blocknumber) already_registered = self.get_token_network( token_address=token_address, block_identifier=failed_at_blocknumber) deprecation_executor = self.get_deprecation_executor( block_identifier=failed_at_blocknumber) settlement_timeout_min = self.settlement_timeout_min( block_identifier=failed_at_blocknumber) settlement_timeout_max = self.settlement_timeout_max( block_identifier=failed_at_blocknumber) chain_id = self.get_chain_id( block_identifier=failed_at_blocknumber) secret_registry_address = self.get_secret_registry_address( block_identifier=failed_at_blocknumber) try: # Creating a new instance to run the constructor checks. token_proxy = Token( jsonrpc_client=self.rpc_client, token_address=token_address, contract_manager=self.proxy_manager.contract_manager, block_identifier=failed_at_blocknumber, ) except AddressWithoutCode: # This cannot be an unrecoverable error, since the ERC20 # code is external. raise RaidenRecoverableError( "Token disappeared! The address " f"{to_checksum_address(token_address)} did have code at " f"block {log_details['given_block_identifier']}, however " f"at block {failed_at_blocknumber} when the registration " "transaction was mined the address didn't have code " "anymore.") check_transaction_failure(transaction_mined, self.rpc_client) check_address_has_code_handle_pruned_block( client=self.rpc_client, address=Address(secret_registry_address), contract_name=CONTRACT_SECRET_REGISTRY, expected_code=decode_hex( self.proxy_manager.contract_manager. get_runtime_hexcode(CONTRACT_SECRET_REGISTRY)), given_block_identifier=failed_at_blocknumber, ) if token_networks_created >= max_token_networks: raise RaidenRecoverableError( "The number of existing token networks reached the maximum allowed" ) if already_registered: # Race condition lost, the token network was created in a different # transaction which got mined first. raise RaidenRecoverableError( "The token was already registered in the TokenNetworkRegistry." ) if deprecation_executor == NULL_ADDRESS_BYTES: raise RaidenUnrecoverableError( "The deprecation executor property for the " "TokenNetworkRegistry is invalid.") if chain_id == 0: raise RaidenUnrecoverableError( "The chain ID property for the TokenNetworkRegistry is invalid." ) if chain_id != self.rpc_client.chain_id: raise RaidenUnrecoverableError( f"The provided chain ID {chain_id} does not match the " f"network Raiden is running on: {self.rpc_client.chain_id}." ) if secret_registry_address == NULL_ADDRESS_BYTES: raise RaidenUnrecoverableError( "The secret registry address for the token network is invalid." ) if settlement_timeout_min == 0: raise RaidenUnrecoverableError( "The minimum settlement timeout for the token network " "should be larger than zero.") if settlement_timeout_max <= settlement_timeout_min: raise RaidenUnrecoverableError( "The maximum settlement timeout for the token network " "should be larger than the minimum settlement timeout." ) total_supply = token_proxy.total_supply( block_identifier=failed_at_blocknumber) if not total_supply or total_supply <= 0: raise RaidenRecoverableError( f"The given token address is not a valid ERC20 token, " f"total_supply() returned an invalid value {total_supply}." ) # At this point, the TokenNetworkRegistry fails to instantiate # a new TokenNetwork. raise RaidenUnrecoverableError( "createERC20TokenNetwork failed for an unknown reason, even " "though the gas estimation succeeded.") succeeded_at_blockhash = receipt["blockHash"] token_network_address = self.get_token_network( token_address, succeeded_at_blockhash) if token_network_address is None: msg = "createERC20TokenNetwork succeeded but token network address is Null" raise RaidenUnrecoverableError(msg) else: # `estimated_transaction` is None # The latest block can not be used reliably because of reorgs, # therefore every call using this block has to handle pruned data. failed_at_block = self.rpc_client.get_block(BLOCK_ID_LATEST) failed_at_blockhash = failed_at_block["hash"].hex() failed_at_blocknumber = failed_at_block["number"] max_token_networks = self.get_max_token_networks( block_identifier=failed_at_blocknumber) token_networks_created = self.get_token_network_created( block_identifier=failed_at_blocknumber) already_registered = self.get_token_network( token_address=token_address, block_identifier=failed_at_blocknumber) deprecation_executor = self.get_deprecation_executor( block_identifier=failed_at_blocknumber) settlement_timeout_min = self.settlement_timeout_min( block_identifier=failed_at_blocknumber) settlement_timeout_max = self.settlement_timeout_max( block_identifier=failed_at_blocknumber) chain_id = self.get_chain_id( block_identifier=failed_at_blocknumber) secret_registry_address = self.get_secret_registry_address( block_identifier=failed_at_blocknumber) try: # Creating a new instance to run the constructor checks. token_proxy = Token( jsonrpc_client=self.rpc_client, token_address=token_address, contract_manager=self.proxy_manager.contract_manager, block_identifier=failed_at_blocknumber, ) except AddressWithoutCode: # This cannot be an unrecoverable error, since the ERC20 # code is external. raise RaidenRecoverableError( "Token disappeared! The address " "{to_checksum_address(token_address)} did have code at " "block {log_details['given_block_identifier']}, however " "at block {failed_at_blocknumber} when the registration " "transaction was mined the address didn't have code " "anymore.") check_address_has_code_handle_pruned_block( client=self.rpc_client, address=Address(secret_registry_address), contract_name=CONTRACT_SECRET_REGISTRY, expected_code=decode_hex( self.proxy_manager.contract_manager.get_runtime_hexcode( CONTRACT_SECRET_REGISTRY)), given_block_identifier=failed_at_blocknumber, ) required_gas = self.gas_measurements[ "TokenNetworkRegistry createERC20TokenNetwork"] self.rpc_client.check_for_insufficient_eth( transaction_name="createERC20TokenNetwork", transaction_executed=False, required_gas=required_gas, block_identifier=failed_at_blocknumber, ) if token_networks_created >= max_token_networks: raise RaidenRecoverableError( "The number of existing token networks reached the maximum allowed" ) if already_registered: # Race condition lost, the token network was created in a different # transaction which got mined first. raise RaidenRecoverableError( "The token was already registered in the TokenNetworkRegistry." ) if deprecation_executor == NULL_ADDRESS_BYTES: raise RaidenUnrecoverableError( "The deprecation executor property for the TokenNetworkRegistry is invalid." ) if chain_id == 0: raise RaidenUnrecoverableError( "The chain ID property for the TokenNetworkRegistry is invalid." ) if chain_id != self.rpc_client.chain_id: raise RaidenUnrecoverableError( f"The provided chain ID {chain_id} does not match the " f"network Raiden is running on: {self.rpc_client.chain_id}." ) if secret_registry_address == NULL_ADDRESS_BYTES: raise RaidenUnrecoverableError( "The secret registry address for the token network is invalid." ) if settlement_timeout_min <= 0: raise RaidenUnrecoverableError( "The minimum settlement timeout for the token network " "should be larger than zero.") if settlement_timeout_max <= settlement_timeout_min: raise RaidenUnrecoverableError( "The maximum settlement timeout for the token network " "should be larger than the minimum settlement timeout.") total_supply = token_proxy.total_supply( block_identifier=failed_at_blocknumber) if not total_supply or total_supply <= 0: raise RaidenRecoverableError( f"The given token address is not a valid ERC20 token, " f"total_supply() returned an invalid value {total_supply}." ) # At this point, the TokenNetworkRegistry fails to instantiate # a new TokenNetwork. raise RaidenUnrecoverableError( f"createERC20TokenNetwork gas estimation failed for an unknown " f"reason. Reference block {failed_at_blockhash} " f"{failed_at_blocknumber}.") return ( TransactionHash(transaction_mined.transaction_hash), TokenNetworkAddress(token_network_address), )
def approve(self, allowed_address: Address, allowance: TokenAmount) -> None: """ Approve `allowed_address` to transfer up to `deposit` amount of token. Note: For channel deposit please use the channel proxy, since it does additional validations. We assume there to be sufficient balance as a precondition if this is called, so it is not checked as a precondition here. """ # Note that given_block_identifier is not used here as there # are no preconditions to check before sending the transaction # There are no direct calls to this method in any event handler, # so a precondition check would make no sense. with self.token_lock: log_details: Dict[str, Any] = {} error_prefix = "Call to approve will fail" estimated_transaction = self.client.estimate_gas( self.proxy, "approve", log_details, allowed_address, allowance) if estimated_transaction is not None: error_prefix = "Call to approve failed" transaction_sent = self.client.transact(estimated_transaction) transaction_mined = self.client.poll_transaction( transaction_sent) if not was_transaction_successfully_mined(transaction_mined): failed_receipt = transaction_mined.receipt failed_at_blockhash = encode_hex( failed_receipt["blockHash"]) check_transaction_failure(transaction_mined, self.client) balance = self.balance_of(self.client.address, failed_at_blockhash) if balance < allowance: msg = (f"{error_prefix} Your balance of {balance} is " "below the required amount of {allowance}.") if balance == 0: msg += ( " Note: The balance was 0, which may also happen " "if the contract is not a valid ERC20 token " "(balanceOf method missing).") raise RaidenRecoverableError(msg) raise RaidenRecoverableError( f"{error_prefix}. The reason is unknown, you have enough tokens for " f"the requested allowance and enough eth to pay the gas. There may " f"be a problem with the token contract.") else: failed_at = self.client.get_block(BLOCK_ID_LATEST) failed_at_blockhash = encode_hex(failed_at["hash"]) failed_at_blocknumber = failed_at["number"] self.client.check_for_insufficient_eth( transaction_name="approve", transaction_executed=False, required_gas=GAS_REQUIRED_FOR_APPROVE, block_identifier=failed_at_blocknumber, ) balance = self.balance_of(self.client.address, failed_at_blockhash) if balance < allowance: msg = (f"{error_prefix} Your balance of {balance} is " "below the required amount of {allowance}.") if balance == 0: msg += ( " Note: The balance was 0, which may also happen if the contract " "is not a valid ERC20 token (balanceOf method missing)." ) raise RaidenRecoverableError(msg) raise RaidenRecoverableError( f"{error_prefix} Gas estimation failed for unknown reason. " f"Please make sure the contract is a valid ERC20 token.")
def transfer(self, to_address: Address, amount: TokenAmount) -> None: """ Transfer `amount` tokens to `to_address`. Note: We assume there to be sufficient balance as a precondition if this is called, so that is not checked as a precondition here. """ def check_for_insufficient_token_balance( block_number: BlockNumber) -> None: failed_at_hash = encode_hex( self.client.blockhash_from_blocknumber(block_number)) self.client.check_for_insufficient_eth( transaction_name="transfer", transaction_executed=False, required_gas=GAS_LIMIT_FOR_TOKEN_CONTRACT_CALL, block_identifier=block_number, ) balance = self.balance_of(self.client.address, failed_at_hash) if balance < amount: msg = ( f"Call to transfer will fail. Your balance of {balance} is " f"below the required amount of {amount}.") if balance == 0: msg += ( " Note: The balance was 0, which may also happen if the contract " "is not a valid ERC20 token (balanceOf method missing)." ) raise RaidenRecoverableError(msg) # Note that given_block_identifier is not used here as there # are no preconditions to check before sending the transaction # There are no direct calls to this method in any event handler, # so a precondition check would make no sense. with self.token_lock: log_details: Dict[str, Any] = {} estimated_transaction = self.client.estimate_gas( self.proxy, "transfer", log_details, to_address, amount) if estimated_transaction is not None: # TODO: check Transfer event (issue: #2598) transaction_sent = self.client.transact(estimated_transaction) transaction_mined = self.client.poll_transaction( transaction_sent) if was_transaction_successfully_mined(transaction_mined): return failed_at_block_number = BlockNumber( transaction_mined.receipt["blockNumber"]) check_for_insufficient_token_balance(failed_at_block_number) raise RaidenRecoverableError( "Call to transfer failed for unknown reason. Please make sure the " "contract is a valid ERC20 token.") else: failed_at_block_number = self.client.get_block( BLOCK_ID_LATEST)["number"] check_for_insufficient_token_balance(failed_at_block_number) raise RaidenRecoverableError( "Call to transfer will fail. Gas estimation failed for unknown " "reason. Please make sure the contract is a valid ERC20 token." )