Skip to content

Support for Generation of Custom Error type#2173

Merged
gtebrean merged 8 commits intoLFDT-web3j:mainfrom
psychoplasma:custom-error-type-generation
Apr 9, 2025
Merged

Support for Generation of Custom Error type#2173
gtebrean merged 8 commits intoLFDT-web3j:mainfrom
psychoplasma:custom-error-type-generation

Conversation

@psychoplasma
Copy link
Copy Markdown
Contributor

@psychoplasma psychoplasma commented Mar 25, 2025

What does this PR do?

  • Adds CustomError class to abi package
  • Adds CustomErrorEncoder class to abi package
  • Adds code generation support to codegen package for Custom Error(error) type introduced in solidity >=0.8.4

With this PR, codegen will be able to generate variables for Custom Errors to corresponding Contract java class.

codegen will generate two things

  • variable for each error type defined in smart contract's abi with org.web3j.abi.datatypes.CustomError java class
  • a list with List.<org.web3j.abi.datatypes.CustomError> Java class which contains all org.web3j.abi.datatypes.CustomError variables defined in the contract class

The former is straight-forward, it's just the Java type corresponding to error type in Solidity.
The latter is for later use(on the client side) to iterate over contract's error types such as resolving revert reason returned by eth_call.

Example: let's say we have the following smart contract

pragma solidity ^0.8.4;

contract MyContract {
    error InvalidAccess(address, string reason);

    function unauthorized() public {
        revert InvalidAccess(msg.sender, "unauthorized address");
    }
}

Then will generate the following code

public class MyContract extends Contract {
    // previous code block

    public static final org.web3j.abi.datatypes.CustomError INVALIDACCESS_ERROR = new org.web3j.abi.datatypes.CustomError("InvalidAccess", 
            Arrays.<TypeReference<?>>asList(new TypeReference<Address>() {}, new TypeReference<Utf8String>() {}));
    ;

    // rest of the code
}

How to use

import org.web3j.abi.CustomErrorEncoder;
import org.web3j.abi.FunctionReturnDecoder;
import org.web3j.abi.TypeReference;
import org.web3j.abi.datatypes.CustomError;
import org.web3j.abi.datatypes.Type;
import org.web3j.abi.datatypes.Utf8String;
import org.web3j.utils.Numeric;

import java.util.HashMap;
import java.util.List;
import java.util.Map;


public class RevertReasonResolver {
    // Standard error definition for reverts with string, like `require(<condition>, "reason")`
    private static final CustomError STANDARD_ERROR = new CustomError("Error", List.of(new TypeReference<Utf8String>() {}));

    // Mapping from error signatures to custom errors of the contract in interest.
    // Could be a collection of contracts either
    private final Map<String, CustomError> contractErrors = new HashMap<>();

    public RevertReasonResolver() {
        // Add standard error
        this.contractErrors.put(
            CustomErrorEncoder.encode(STANDARD_ERROR).substring(0, 10),
            STANDARD_ERROR
        );
    }

    // Add custom errors from the contracts in interest
    public void addCustomError(CustomError error) {
        this.contractErrors.put(
            CustomErrorEncoder.encode(error).substring(0, 10),
            error
        );
    }

    public String resolve(String encodedRevertReason) {
        if (encodedRevertReason.length() < 10) {
            return encodedRevertReason;
        }

        String errorSignature = encodedRevertReason.substring(0, 10);

        if (contractErrors.containsKey(errorSignature)) {
            CustomError error = contractErrors.get(errorSignature);
            return decode(encodedRevertReason, error);
        }

        return encodedRevertReason;
    }

    // Converts an Error object to string representation
    // MyError(string) with input of "wrong input" => MyError(string: wrong input)
    public static String decode(String encodedRevertReason, CustomError error) {
        List<Type> values = FunctionReturnDecoder.decode(
                encodedRevertReason.substring(10),
                error.getParameters()
        );

        String formattedValues = values.stream()
                .map(value -> value.getTypeAsString() + ": " + RevertReasonResolver.getValueAsString(value))
                .reduce((acc, s) -> acc + ", " + s)
                .orElse("");

        return error.getName() + "(" + formattedValues + ")";
    }

    public static String getValueAsString(Type<?> type) {
        if (type.getValue() instanceof byte[]) {
            return Numeric.toHexString((byte[]) type.getValue());
        } else {
            return type.getValue().toString();
        }
    }
}

public static void main() {
    MyContract myContract = new MyContract();
    RevertReasonResolver resolver = new RevertReasonResolver();
    resolver.addCustomError(myContract.INVALIDACCESS_ERROR);
    // May add some other errors from the same contract or other contracts

    System.out.println(resolver.resolve("0xabcdef01....."));
    // Would return "InvalidAccess(address: 0x..., string: unauthorized address)"
}

Some notes:

  • If there is no error type defined in contract's abi, no variables are generated for error except CUSTOM_ERRORS list which will effectively be an empty list. I thought, at first, not to generate the list if there is no error type defined in abi, but if the client implementations depend on CUSTOM_ERRORS list, then the client implementation would not compile in case of no error type defined in abi.

  • This support will not break backward compatibility for previous solidity versions.

Where should the reviewer start?

abi and codegen packages

Why is it needed?

Currently there is no support for custom errors, and it requires some effort to resolve revert reasons on the client side.

Checklist

  • I've read the contribution guidelines.
  • I've added tests (if applicable).
  • I've added a changelog entry if necessary.

Signed-off-by: Mustafa Morca <psychoplasma@gmail.com>
Signed-off-by: Mustafa Morca <psychoplasma@gmail.com>
…pe to codegen

Signed-off-by: Mustafa Morca <psychoplasma@gmail.com>
Signed-off-by: Mustafa Morca <psychoplasma@gmail.com>
…eneration

Signed-off-by: Mustafa Morca <psychoplasma@gmail.com>
@innovus-yaroslav-glukhov
Copy link
Copy Markdown

Hello, is there any possibility that this PR will be merged?


assertEquals(
CustomErrorEncoder.calculateSignatureHash("RandomError(address[],bytes)"),
("0xbf37b77ddf0fbbf29ee6a3ebda3d177c2d438123b10571806c57958230d9f905"));
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

All unit tests are on point, but I'm interested how did you generate the expected results for all the tests?

Copy link
Copy Markdown
Contributor Author

@psychoplasma psychoplasma Apr 5, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I generated the expected signature hashes used in test cases with ethers.js.

Actually, CustomErrorEncoder is totally identical to EventEncoder, and EventEncoder could be used instead, which makes CustomErrorEncoder looking like a code duplication. But, I think, keeping separate classes for Solidity abi types would be a better option in case of any changes on Solidity side in the future.

Comment thread codegen/src/main/java/org/web3j/codegen/SolidityFunctionWrapper.java Outdated
Comment thread codegen/src/test/java/org/web3j/codegen/SolidityFunctionWrapperTest.java Outdated
…dency on client implementations

Signed-off-by: Mustafa Morca <psychoplasma@gmail.com>
@gtebrean
Copy link
Copy Markdown
Contributor

gtebrean commented Apr 7, 2025

@psychoplasma LGTM, please update also the changelog and I will merge it

Signed-off-by: Mustafa Morca <psychoplasma@gmail.com>
@psychoplasma
Copy link
Copy Markdown
Contributor Author

Updated

Signed-off-by: gtebrean <99179176+gtebrean@users.noreply.github.com>
@gtebrean gtebrean merged commit e13f7ed into LFDT-web3j:main Apr 9, 2025
5 checks passed
@dgimenez27
Copy link
Copy Markdown

dgimenez27 commented Apr 18, 2025

Hi team,

Great work on this new feature!

The perfect complement to this functionality would be to add the custom error information returned by the contract to the TransactionException.java class.

What I mean is that in a call like this:

try {
    contractInstance.function(...params).send();
} catch (TransactionException ex) {

}

the raw response from the node could be:

{
"jsonrpc":"2.0",
"id":4,
"error": {
             "code": 3,
             "message": "execution reverted",
             "data": "0xe2517d3f000000000000000000000000 ... "
             }
}

The data field contains the hexadecimal signature of the custom error and its parameters, which is essential to implement this improvement and correctly capture custom errors. But the information in this field is not included in the TransactionException.java class, so it cannot be obtained.

I think it's not possible to get this information even in a simulation using eth_call because it is not included in the EthCall class either.

How can I access the data field information or the custom error selector?

I appreciate your comments and attention to this matter.
Best regards.

@psychoplasma
Copy link
Copy Markdown
Contributor Author

psychoplasma commented Apr 21, 2025

Hi @dgimenez27

That's a good point indeed.
Why don't you create an issue (maybe with enhancement tag) about this, so everyone can see it easily and discuss about the improvement?

Regarding your question,

How can I access the data field information or the custom error selector?

You can check out the How to use section above this PR description. For the time being(with this PR), you have to create a list of the custom errors you need, then you should compare the revert reason data against these custom errors' signatures.

@dgimenez27
Copy link
Copy Markdown

💡 I created a related issue to propose officially exposing the error.data field from the JSON-RPC response in TransactionException (and possibly in EthCall as well):

➡️ Issue #2180: Expose revert data field in TransactionException for decoding CustomError

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

4 participants