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
Example #2
0
    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).")
Example #3
0
    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
Example #4
0
    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
Example #7
0
    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)
Example #8
0
    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
Example #9
0
    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)."
            )
Example #10
0
    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)
Example #11
0
    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")
Example #12
0
    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")
Example #13
0
    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),
        )
Example #14
0
    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.")
Example #15
0
    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."
                )