Published on

How to Get Require Error Messages with Web3j

Authors

Web3j is a web3 Ethereum client for Java. So far using it has been a great experience. Web3j has quite a large following and seems to be the web3 client of choice for Android apps that want to interact with Ethereum (Trustwallet uses it).

One issue that was not obvious was how to get the reason for a require error in web3j instead of the generic: Transaction has failed with status: 0x0. Gas used: 25668. (not-enough gas?).

I found an article that gives a solution but does not give a full example making it difficult to work out how the solution fits together.

Below I include a full solution to make it easier for others in future to work solve this.

Firstly lets say I have the following ballot contract (taken from the official examples here)

pragma solidity >=0.4.22 <0.6.0;

/// @title Voting with delegation.
contract Ballot {
    // ...


    /// Give your vote (including votes delegated to you)
    /// to proposal `proposals[proposal].name`.
    function vote(uint proposal) public {
        Voter storage sender = voters[msg.sender];
        require(sender.weight != 0, "Has no right to vote");
        require(!sender.voted, "Already voted.");
        sender.voted = true;
        sender.vote = proposal;

        // If `proposal` is out of the range of the array,
        // this will throw automatically and revert all
        // changes.
        proposals[proposal].voteCount += sender.weight;
    }
    // ...

}

Now I get an error when calling the vote method via web3j. My web3j call off of the generated contracts looks like this:

Web3j web3j = Web3j.build(new HttpService("http://localhost:8545"));
Credentials credentials = WalletUtils.loadCredentials(
                "password you used to encrypt your account", // when you called: `geth --datadir path/to/your/data account new`
                "path/to/your/data/keystore/UTC--2019-10-31T11-07-09.786733000Z--741aaaaaaaaaaaaaaaaaaaaa8541c92b823e26a7"
        );
ContractGasProvider contractGasProvider = new DefaultGasProvider();

Ballot ballotContract = Ballot.load(
    "0xc78c10d681b68d634969e5043a855bf7c2b4e174", // this will be the address of your deployed ballot contract
    web3j,
    credentials,
    contractGasProvider
);

ballotContract.vote(BigInteger.valueOf(2)).send(); //the error happens here as this account has already voted

To see the error returned you will need to change your code to the below:

Web3j web3j = Web3j.build(new HttpService("http://localhost:8545"));
Credentials credentials = WalletUtils.loadCredentials(
                "password you used to encrypt your account", // when you called: `geth --datadir path/to/your/data account new`
                "path/to/your/data/keystore/UTC--2019-10-31T11-07-09.786733000Z--741aaaaaaaaaaaaaaaaaaaaa8541c92b823e26a7"
        );
ContractGasProvider contractGasProvider = new DefaultGasProvider();

Ballot ballotContract = Ballot.load(
    "0xc78c10d681b68d634969e5043a855bf7c2b4e174", // this will be the address of your deployed ballot contract
    web3j,
    credentials,
    contractGasProvider
);

BigInteger voteChoice = BigInteger.valueOf(2);
try {
    ballotContract.vote(voteChoice).send(); //the error happens here as this account has already voted
}
catch(Error e) {
    //the error is still the generic: Transaction has failed with status: 0x0. Gas used: 25668. (not-enough gas?)
    final org.web3j.abi.datatypes.Function voteFunction = new org.web3j.abi.datatypes.Function(
                "vote",
                Arrays.<Type>asList(new org.web3j.abi.datatypes.Uint8(voteChoice)),  // this must match all the method input types you have in your function, make it an empty list otherwise
                Collections.<TypeReference<?>>emptyList() //this must match your function return types. For example if your function returns a uint8 use: `Arrays.<TypeReference<?>>asList(new TypeReference<Uint8>() {}));`
    );

    String encodedFunction = FunctionEncoder.encode(voteFunction);
    EthCall ethCall = web3j.ethCall(
                    Transaction.createEthCallTransaction(
                            callerAddress, // this is your wallet's address. Use `credentials.getAddress();` if you do not know what yours is
                            ballotContract.getContractAddress(), // this should be the same as what is in the load function above
                            encodedFunction
                    ),
                    DefaultBlockParameterName.LATEST
    ).send();

    Optional<String> revertReason = getRevertReason(ethCall); // this is the same function from the blog post mentioned
    System.out.println(revertReason.get()); // outputs: 'Already voted.'
}

I made a little Kotlin class that handles this whole process and uses the nice error handling pattern described in this post. Also, note how this handler can be used for solidity functions that return nothing (i.e. they return a transaction receipt only) and those that do (only tried this on functions that return a single input).

package com.example

import org.slf4j.LoggerFactory
import org.web3j.abi.FunctionEncoder
import org.web3j.abi.FunctionReturnDecoder
import org.web3j.abi.TypeReference
import org.web3j.abi.datatypes.AbiTypes
import org.web3j.abi.datatypes.Function
import org.web3j.abi.datatypes.Type
import org.web3j.abi.datatypes.Utf8String
import org.web3j.protocol.Web3j
import org.web3j.protocol.core.DefaultBlockParameterName
import org.web3j.protocol.core.methods.request.Transaction
import org.web3j.protocol.core.methods.response.EthCall
import java.util.*

sealed class ChainResponse {
    data class Success<R>(val result: R) : ChainResponse()
    data class Error(val revertReason: String) : ChainResponse()

    companion object {

        private val logger = LoggerFactory.getLogger(ChainResponse::class.java)

        fun <R> processChainCall(
                web3j: Web3j,
                requestToChain: () -> R,
                toAddress: String,
                contractFunction: Function,
                fromAddress: String,
                optionalOutputLogCallback: (transactionReceipt: TransactionReceipt) -> Unit = {}
        ): ChainResponse {
            return try {
                val result = requestToChain()
                if (result is TransactionReceipt) {
                    optionalOutputLogCallback(result)
                }
                Success(result)
            } catch (cce: ClientConnectionException) {
                logger.error("Failed to connect to the ethereum client. Error stacktrace: ", cce)
                Error("Failed to connect to the ethereum client")
            } catch (te: TransactionException) {
                handleTransactionException(te, web3j, fromAddress, toAddress, contractFunction)
            } catch (e: Exception) {
                logger.error("Some other exception occured when trying to call a contract method", e)
                Error("Some other exception occured when trying to call a contract method")
            }
        }

        private fun handleTransactionException(te: TransactionException, web3j: Web3j, fromAddress: String, toAddress: String, contractFunction: Function): ChainResponse {
            return when {
                te.message!!.contains("Transaction has failed with status: 0x0. Gas used: 61722. (not-enough gas?)") ->
                    getChainResponseError(
                            web3j,
                            fromAddress,
                            toAddress,
                            contractFunction
                    )
                else -> {
                    logger.error("A general transaction exception occurred", te)
                    Error("A general transaction exception occurred")
                }
            }
        }

        private fun getChainResponseError(web3j: Web3j, caller: String, contractAddress: String, function: Function): Error {
            val ethCallResponse = callSmartContractFunction(web3j, function, contractAddress, caller)
            val revertReason = getRevertReason(ethCallResponse)
            return Error(revertReason.orElse("General revert (no revert reason given)"))
        }

        private fun callSmartContractFunction(web3j: Web3j, function: Function, contractAddress: String, callerAddress: String): EthCall {
            val encodedFunction = FunctionEncoder.encode(function)

            return web3j.ethCall(
                    Transaction.createEthCallTransaction(
                            callerAddress,
                            contractAddress,
                            encodedFunction
                    ),
                    DefaultBlockParameterName.LATEST
            ).send()
        }

        private fun getRevertReason(ethCall: EthCall): Optional<String> {
            // Numeric.toHexString(Hash.sha3("Error(string)".getBytes())).substring(0, 10)
            val errorMethodId = "0x08c379a0"
            val revertReasonTypes = listOf<TypeReference<Type<*>>>(TypeReference.create<Type<*>>(AbiTypes.getType("string") as Class<Type<*>>))

            if (!ethCall.hasError() && ethCall.value != null && ethCall.value.startsWith(errorMethodId)) {
                val encodedRevertReason = ethCall.value.substring(errorMethodId.length)
                val decoded = FunctionReturnDecoder.decode(encodedRevertReason, revertReasonTypes)
                val decodedRevertReason = decoded[0] as Utf8String
                return Optional.of(decodedRevertReason.value)
            }
            return Optional.empty()
        }
    }
}

In the above note how ethcall is used which:

Executes a new message call immediately without creating a transaction on the block chain.

This can make this process slow as it needs to hit the chain again but will not result in state being changed. Unfortunately I cannot see another way to get the revert reason. There is more documentation on using ethcall with web3j to check the status of transactions.

Example usage of the above to call the ballot function (also in Kotlin) is below:

val chainResponse = processChainCall(
    web3j = web3j,
    toAddress = ballot.contractAddress,
    fromAddress = nodeAddress,
    requestToChain = ballot.vote(voteChoice)::send,
    contractFunction = createVoteFunction(voteChoice)
)

// ...

fun createVoteFunction(voteChoice): Function {
return Function(
                "vote",
                listOf<Type<*>>(Uint8(voteChoice)),
                emptyList()
    )
}

The easiest way to get the functions to build is to cmd/ctrl+click into the generated method function. For example, if I control clicked to the source of ballot.vote(voteChoice); I am taken to the generated Java code for that method which has the web3j Function they use which you can copy-paste for the contractFunction parameter in the above util.

Working with ChainResponse in Kotlin is really easy when using when:

when (val response = blockchainAdapter.castVote(voteChoice)) {
    is ChainResponse.Success<*> -> println("+++++++++ success: [${response.result as TransactionReceipt}]") // or as whatever the expected return type is if it is for a function that returns
    is ChainResponse.Error -> println("--------- error : [${response.revertReason}]")
}