diff --git a/abi/src/main/java/org/web3j/abi/Utils.java b/abi/src/main/java/org/web3j/abi/Utils.java index d2fe345cdd..7610fed063 100644 --- a/abi/src/main/java/org/web3j/abi/Utils.java +++ b/abi/src/main/java/org/web3j/abi/Utils.java @@ -331,4 +331,62 @@ private static String getClassName(Class type) { return type.getName(); } + + /** + * Gets the Solidity type name for a given TypeReference. + * This method handles both simple types and complex types (arrays, structs). + * + * @param typeReference the TypeReference to get the Solidity type name for + * @return the Solidity type name (e.g. "uint256", "address", "string[]") + */ + public static String getSolidityTypeName(TypeReference typeReference) { + try { + java.lang.reflect.Type reflectedType = typeReference.getType(); + Class type; + + if (reflectedType instanceof ParameterizedType) { + type = (Class) ((ParameterizedType) reflectedType).getRawType(); + if (DynamicArray.class.isAssignableFrom(type)) { + Class componentType = getParameterizedTypeFromArray(typeReference); + return getSoliditySimpleTypeName(componentType) + "[]"; + } else if (StaticArray.class.isAssignableFrom(type)) { + Class componentType = getParameterizedTypeFromArray(typeReference); + int length = ((TypeReference.StaticArrayTypeReference) typeReference).getSize(); + return getSoliditySimpleTypeName(componentType) + "[" + length + "]"; + } + } + + type = typeReference.getClassType(); + return getSoliditySimpleTypeName(type); + } catch (ClassNotFoundException e) { + throw new UnsupportedOperationException("Invalid class reference provided", e); + } + } + + private static String getSoliditySimpleTypeName(Class type) { + if (Uint.class.isAssignableFrom(type)) { + return "uint256"; + } else if (Int.class.isAssignableFrom(type)) { + return "int256"; + } else if (Ufixed.class.isAssignableFrom(type)) { + return "ufixed256"; + } else if (Fixed.class.isAssignableFrom(type)) { + return "fixed256"; + } else if (Utf8String.class.isAssignableFrom(type)) { + return "string"; + } else if (DynamicBytes.class.isAssignableFrom(type)) { + return "bytes"; + } else if (org.web3j.abi.datatypes.Address.class.isAssignableFrom(type)) { + return "address"; + } else if (org.web3j.abi.datatypes.Bool.class.isAssignableFrom(type)) { + return "bool"; + } else if (org.web3j.abi.datatypes.Bytes.class.isAssignableFrom(type)) { + String typeName = type.getSimpleName(); + return typeName.toLowerCase(); + } else if (StructType.class.isAssignableFrom(type)) { + return getStructType(type); + } else { + return type.getSimpleName().toLowerCase(); + } + } } diff --git a/abi/src/main/java/org/web3j/abi/datatypes/CustomError.java b/abi/src/main/java/org/web3j/abi/datatypes/CustomError.java new file mode 100644 index 0000000000..82ce89f5f3 --- /dev/null +++ b/abi/src/main/java/org/web3j/abi/datatypes/CustomError.java @@ -0,0 +1,90 @@ +/* + * Copyright 2024 Web3 Labs Ltd. + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on + * an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the + * specific language governing permissions and limitations under the License. + */ +package org.web3j.abi.datatypes; + +import java.util.ArrayList; +import java.util.List; +import java.util.Objects; + +import org.web3j.abi.TypeReference; +import org.web3j.abi.Utils; + +/** + * Represents a Solidity custom error definition. + * This class defines the structure of a custom error, not its values. + * The parameters list contains TypeReferences that define what types of values + * the error can contain, not the actual values themselves. + */ +public class CustomError { + private final String name; + private final List> parameters; + + public CustomError(String name, List> parameters) { + this.name = name; + this.parameters = parameters; + } + + public CustomError(String name) { + this(name, new ArrayList<>()); + } + + public String getName() { + return name; + } + + public List> getParameters() { + return parameters; + } + + /** + * Returns the error signature in the format "ErrorName(type1,type2,...)" + */ + public String getSignature() { + StringBuilder signature = new StringBuilder(); + signature.append(name).append("("); + for (int i = 0; i < parameters.size(); i++) { + signature.append(Utils.getSolidityTypeName(parameters.get(i))); + if (i < parameters.size() - 1) { + signature.append(","); + } + } + signature.append(")"); + return signature.toString(); + } + + /** + * Returns the first 4 bytes of the Keccak-256 hash of the error signature. + * This is used to identify the error in transaction receipts. + */ + public String getSelector() { + return org.web3j.crypto.Hash.sha3String(getSignature()).substring(0, 10); + } + + @Override + public boolean equals(Object o) { + if (this == o) return true; + if (o == null || getClass() != o.getClass()) return false; + CustomError that = (CustomError) o; + return this.getSignature().equals(that.getSignature()); + } + + @Override + public int hashCode() { + return getSignature().hashCode(); + } + + @Override + public String toString() { + return getSignature(); + } +} \ No newline at end of file diff --git a/abi/src/test/java/org/web3j/abi/datatypes/CustomErrorTest.java b/abi/src/test/java/org/web3j/abi/datatypes/CustomErrorTest.java new file mode 100644 index 0000000000..f13123c1ae --- /dev/null +++ b/abi/src/test/java/org/web3j/abi/datatypes/CustomErrorTest.java @@ -0,0 +1,142 @@ +/* + * Copyright 2024 Web3 Labs Ltd. + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on + * an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the + * specific language governing permissions and limitations under the License. + */ +package org.web3j.abi.datatypes; + +import java.util.Arrays; +import java.util.Collections; +import java.util.List; + +import org.junit.jupiter.api.Test; + +import org.web3j.abi.TypeReference; +import org.web3j.abi.datatypes.AbiTypes; +import org.web3j.abi.datatypes.generated.Uint256; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertNotEquals; + +public class CustomErrorTest { + + @Test + public void testEmptyCustomError() { + CustomError error = new CustomError("SimpleError"); + assertEquals( + "SimpleError()", + error.getSignature(), + "Empty error should have signature 'SimpleError()'" + ); + assertEquals( + "0xc2bb947c", + error.getSelector(), + "Selector should match keccak256('SimpleError()')" + ); + } + + @Test + public void testCustomErrorWithParameters() { + List> parameters = Arrays.asList( + TypeReference.create(Address.class), + TypeReference.create(Uint256.class), + TypeReference.create(Utf8String.class) + ); + + CustomError error = new CustomError("ComplexError", parameters); + + String expectedSignature = "ComplexError(address,uint256,string)"; + assertEquals( + expectedSignature, + error.getSignature(), + "Error signature should match '" + expectedSignature + "'" + ); + assertEquals( + parameters, + error.getParameters(), + "Parameters list should match the input parameters" + ); + assertEquals( + "0xcca85a17", + error.getSelector(), + "Selector should match keccak256('" + expectedSignature + "')" + ); + } + + @Test + public void testCustomErrorWithArrayParameter() { + List> parameters = Collections.singletonList( + new TypeReference>() {} + ); + + CustomError error = new CustomError("ArrayError", parameters); + + String expectedSignature = "ArrayError(uint256[])"; + assertEquals( + expectedSignature, + error.getSignature(), + "Error signature should match '" + expectedSignature + "'" + ); + assertEquals( + parameters, + error.getParameters(), + "Parameters list should match the input parameters" + ); + assertEquals( + "0x6300af57", + error.getSelector(), + "Selector should match keccak256('" + expectedSignature + "')" + ); + } + + @Test + public void testEquality() { + List> parameters1 = Arrays.asList( + TypeReference.create(Address.class), + TypeReference.create(Uint256.class) + ); + + List> parameters2 = Arrays.asList( + TypeReference.create(Address.class), + TypeReference.create(Uint256.class) + ); + + CustomError error1 = new CustomError("TestError", parameters1); + CustomError error2 = new CustomError("TestError", parameters2); + CustomError error3 = new CustomError("DifferentError", parameters1); + + assertEquals( + error1, + error2, + "Errors with same name and parameters should be equal" + ); + assertNotEquals( + error1, + error3, + "Errors with different names should not be equal" + ); + } + + @Test + public void testToString() { + List> parameters = Arrays.asList( + TypeReference.create(Address.class), + TypeReference.create(Uint256.class) + ); + + CustomError error = new CustomError("TestError", parameters); + String expectedString = "TestError(address,uint256)"; + assertEquals( + expectedString, + error.toString(), + "toString() should return the error signature '" + expectedString + "'" + ); + } +} \ No newline at end of file diff --git a/core/src/main/java/org/web3j/protocol/core/methods/response/EthCall.java b/core/src/main/java/org/web3j/protocol/core/methods/response/EthCall.java index 503f861fab..2bb21aa55f 100644 --- a/core/src/main/java/org/web3j/protocol/core/methods/response/EthCall.java +++ b/core/src/main/java/org/web3j/protocol/core/methods/response/EthCall.java @@ -14,12 +14,16 @@ import java.util.Collections; import java.util.List; +import java.util.ArrayList; +import java.util.stream.Collectors; import org.web3j.abi.FunctionReturnDecoder; +import org.web3j.abi.FunctionEncoder; import org.web3j.abi.TypeReference; import org.web3j.abi.datatypes.AbiTypes; import org.web3j.abi.datatypes.Type; import org.web3j.abi.datatypes.Utf8String; +import org.web3j.abi.datatypes.CustomError; import org.web3j.protocol.core.Response; import org.web3j.utils.EnsUtils; @@ -66,4 +70,53 @@ public String getRevertReason() { } return null; } + + /** + * Gets the revert reason as a string for a custom error. + * If the revert data matches the supplied custom error definition, returns a formatted string + * with the error name and decoded parameters. Otherwise, returns null. + * + * @param customError The custom error definition. + * @return the custom error revert reason as a string, or null if not a custom error. + */ + @SuppressWarnings("unchecked") + public String getRevertReason(CustomError customError) { + if (!hasError() && !isReverted()) { + return null; + } + + String data = getValue(); + if (data == null || data.length() < 10) { // At least 4 bytes for selector + return null; + } + + // Calculate error selector and compare with the supplied custom error + String customErrorSelector = customError.getSelector(); + String actualSelector = data.substring(0, 10); + + if (!customErrorSelector.equals(actualSelector)) { + return null; + } + + // Get the encoded parameters after the selector + String encodedParameters = data.substring(10); + if (encodedParameters.isEmpty()) { + return customError.getName() + "()"; + } + + // Convert TypeReference to TypeReference + List> typeReferences = customError.getParameters().stream() + .map(param -> (TypeReference) param) + .collect(Collectors.toList()); + + // Decode parameters using the custom error's parameter types + List decodedParameters = FunctionReturnDecoder.decode(encodedParameters, typeReferences); + + // Convert parameters into a string representation. + String parametersString = decodedParameters.stream() + .map(t -> t.getValue() == null ? "null" : t.getValue().toString()) + .collect(Collectors.joining(", ")); + + return customError.getName() + "(" + parametersString + ")"; + } }