diff --git a/CHANGELOG.md b/CHANGELOG.md index 80fa0e6c30..041ff645d8 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -15,6 +15,7 @@ See [Conventional Commits](https://conventionalcommits.org) for commit guideline * bump snapshot version to 4.13.1 [#2160](https://github.com/hyperledger-web3j/web3j/pull/2160) * Upgrade to https://github.com/Consensys/tuweni/releases/tag/v2.7.0 [#2170](https://github.com/LFDT-web3j/web3j/pull/2170) +* Add support for code generation of custom error type introduced with `solidity v0.8.4` [#2173](https://github.com/LFDT-web3j/web3j/pull/2173) ### BREAKING CHANGES diff --git a/abi/src/main/java/org/web3j/abi/CustomErrorEncoder.java b/abi/src/main/java/org/web3j/abi/CustomErrorEncoder.java new file mode 100644 index 0000000000..91356072a3 --- /dev/null +++ b/abi/src/main/java/org/web3j/abi/CustomErrorEncoder.java @@ -0,0 +1,53 @@ +/* + * Copyright 2019 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; + +import java.util.List; +import java.util.stream.Collectors; + +import org.web3j.abi.datatypes.CustomError; +import org.web3j.abi.datatypes.Type; +import org.web3j.crypto.Hash; +import org.web3j.utils.Numeric; + +/** + * Ethereum custom error encoding. Further limited details are available here. + */ +public class CustomErrorEncoder { + + private CustomErrorEncoder() {} + + public static String encode(CustomError error) { + return calculateSignatureHash(buildErrorSignature(error.getName(), error.getParameters())); + } + + static String buildErrorSignature( + String errorName, List> parameters) { + + StringBuilder result = new StringBuilder(); + result.append(errorName); + result.append("("); + String params = + parameters.stream().map(Utils::getTypeName).collect(Collectors.joining(",")); + result.append(params); + result.append(")"); + return result.toString(); + } + + public static String calculateSignatureHash(String errorSignature) { + byte[] input = errorSignature.getBytes(); + byte[] hash = Hash.sha3(input); + return Numeric.toHexString(hash); + } +} 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..fa2dbfd764 --- /dev/null +++ b/abi/src/main/java/org/web3j/abi/datatypes/CustomError.java @@ -0,0 +1,38 @@ +/* + * Copyright 2019 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.List; + +import org.web3j.abi.TypeReference; + +import static org.web3j.abi.Utils.convert; + +/** CustomError wrapper type. */ +public class CustomError { + private String name; + private List> parameters; + + public CustomError(String name, List> parameters) { + this.name = name; + this.parameters = convert(parameters); + } + + public String getName() { + return name; + } + + public List> getParameters() { + return parameters; + } +} diff --git a/abi/src/test/java/org/web3j/abi/CustomErrorEncoderTest.java b/abi/src/test/java/org/web3j/abi/CustomErrorEncoderTest.java new file mode 100644 index 0000000000..65e2e9fbfb --- /dev/null +++ b/abi/src/test/java/org/web3j/abi/CustomErrorEncoderTest.java @@ -0,0 +1,90 @@ +/* + * Copyright 2019 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; + +import java.util.Arrays; +import java.util.List; + +import org.junit.jupiter.api.Test; + +import org.web3j.abi.datatypes.Address; +import org.web3j.abi.datatypes.CustomError; +import org.web3j.abi.datatypes.DynamicArray; +import org.web3j.abi.datatypes.Utf8String; +import org.web3j.abi.datatypes.generated.Uint256; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.web3j.abi.Utils.convert; + +public class CustomErrorEncoderTest { + @Test + public void testCalculateSignatureHash() { + assertEquals( + CustomErrorEncoder.calculateSignatureHash("InvalidAccess(address,string,uint256)"), + ("0xcb5157bf1b439b9573ea7a95f7c00cc33f832ed728345c2bd29146ce58bbab57")); + + assertEquals( + CustomErrorEncoder.calculateSignatureHash("RandomError(address[],bytes)"), + ("0xbf37b77ddf0fbbf29ee6a3ebda3d177c2d438123b10571806c57958230d9f905")); + } + + @Test + public void testEncode() { + CustomError error = + new CustomError( + "InvalidAccess", + Arrays.>asList( + new TypeReference
() {}, + new TypeReference() {}, + new TypeReference() {})); + + assertEquals( + CustomErrorEncoder.encode(error), + "0xcb5157bf1b439b9573ea7a95f7c00cc33f832ed728345c2bd29146ce58bbab57"); + } + + @Test + public void testBuildErrorSignature() { + List> parameters = + Arrays.>asList( + new TypeReference
() {}, + new TypeReference() {}, + new TypeReference() {}); + + assertEquals( + "InvalidAccess(address,string,uint256)", + CustomErrorEncoder.buildErrorSignature("InvalidAccess", convert(parameters))); + } + + @Test + void testBuildErrorSignatureWithDynamicStructs() { + List> parameters = + Arrays.asList( + new TypeReference() {}, + new TypeReference() {}); + + assertEquals( + "DynamicStructError((((string,string)[])[],uint256),(string,string))", + CustomErrorEncoder.buildErrorSignature("DynamicStructError", convert(parameters))); + } + + @Test + void testBuildErrorSignatureWithDynamicArrays() { + List> parameters = + Arrays.asList(new TypeReference>() {}); + + assertEquals( + "DynamicArrayError((((string,string)[])[],uint256)[])", + EventEncoder.buildMethodSignature("DynamicArrayError", convert(parameters))); + } +} 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..daad67d9e9 --- /dev/null +++ b/abi/src/test/java/org/web3j/abi/datatypes/CustomErrorTest.java @@ -0,0 +1,43 @@ +/* + * Copyright 2020 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.Iterator; +import java.util.List; + +import org.junit.jupiter.api.Test; + +import org.web3j.abi.TypeReference; +import org.web3j.abi.datatypes.generated.Uint256; + +import static org.junit.jupiter.api.Assertions.assertEquals; + +public class CustomErrorTest { + + @Test + public void testCreation() { + + List> parameters = + Arrays.>asList( + new TypeReference
() {}, new TypeReference() {}); + CustomError event = new CustomError("MyError", parameters); + + assertEquals(event.getName(), "MyError"); + + Iterator> expectedParameter = parameters.iterator(); + for (TypeReference actualParameter : event.getParameters()) { + assertEquals(expectedParameter.next(), actualParameter); + } + } +} diff --git a/codegen/src/main/java/org/web3j/codegen/SolidityFunctionWrapper.java b/codegen/src/main/java/org/web3j/codegen/SolidityFunctionWrapper.java index a27b2a8918..d95fb7c9b8 100644 --- a/codegen/src/main/java/org/web3j/codegen/SolidityFunctionWrapper.java +++ b/codegen/src/main/java/org/web3j/codegen/SolidityFunctionWrapper.java @@ -51,6 +51,7 @@ import org.web3j.abi.TypeReference; import org.web3j.abi.datatypes.Address; import org.web3j.abi.datatypes.Array; +import org.web3j.abi.datatypes.CustomError; import org.web3j.abi.datatypes.DynamicArray; import org.web3j.abi.datatypes.DynamicStruct; import org.web3j.abi.datatypes.Event; @@ -106,6 +107,7 @@ public class SolidityFunctionWrapper extends Generator { private static final String FUNC_NAME_PREFIX = "FUNC_"; private static final String TYPE_FUNCTION = "function"; private static final String TYPE_EVENT = "event"; + private static final String TYPE_ERROR = "error"; private static final String TYPE_CONSTRUCTOR = "constructor"; private static final ClassName LOG = ClassName.get(Log.class); @@ -439,6 +441,143 @@ private FieldSpec createEventDefinition( .build(); } + void buildCustomErrorDefinitions( + @NotNull AbiDefinition abiDefinition, + TypeSpec.Builder classBuilder, + Map customErrorsOccurrences) + throws ClassNotFoundException { + if (abiDefinition.getName().isEmpty()) { + System.out.println( + "\nWarning: Blank name field found in custom error abi definition. " + + "No code will be generated for this abi definition."); + return; + } + + List parameters = new ArrayList<>(); + for (AbiDefinition.NamedType namedType : abiDefinition.getInputs()) { + final TypeName typeName; + if (namedType.getType().equals("tuple")) { + typeName = structClassNameMap.get(namedType.structIdentifier()); + } else if (namedType.getType().startsWith("tuple") + && namedType.getType().contains("[")) { + typeName = buildStructArrayTypeName(namedType, false); + } else { + typeName = buildTypeName(namedType.getType(), useJavaPrimitiveTypes); + } + parameters.add(new NamedTypeName(namedType, typeName)); + } + + classBuilder.addField( + createCustomErrorDefinition( + abiDefinition.getName(), parameters, customErrorsOccurrences)); + } + + /** + * Generates code for CustomError instance definition. For example, with name of {@code + * InvalidAddress}, generates: + * + *
{@code
+     * public static final Event INVALIDADDRESS_ERROR = new CustomError(...);
+     * ;
+     * }
+ */ + @NotNull + private FieldSpec createCustomErrorDefinition( + String customErrorName, + List parameters, + Map customErrorsOccurrences) { + + return FieldSpec.builder( + CustomError.class, + buildCustomErrorDefinitionName(customErrorName, customErrorsOccurrences)) + .addModifiers(Modifier.PUBLIC, Modifier.STATIC, Modifier.FINAL) + .initializer(buildVariableLengthCustomErrorInitializer(customErrorName, parameters)) + .build(); + } + + /** + * Generates code for CustomError instance. For example, with abi definition of: + * + *
{@code
+     * {
+     *   "inputs":[
+     *     {"internalType":"address","type":"address"},
+     *     {"internalType":"string","type":"string"}
+     *   ],
+     *   "name":"InvalidAddress",
+     *   "type":"error"
+     * }
+     * }
+ * + * generates: + * + *
{@code
+     * new CustomError("InvalidAddress",
+     *       Arrays.>asList(new TypeReference
() {}, new TypeReference() {})); + * }
+ */ + @NotNull + private static CodeBlock buildVariableLengthCustomErrorInitializer( + String errorName, @NotNull List parameterTypes) { + + List objects = new ArrayList<>(); + objects.add(CustomError.class); + objects.add(errorName); + + objects.add(Arrays.class); + objects.add(TypeReference.class); + for (NamedTypeName parameterType : parameterTypes) { + objects.add(TypeReference.class); + objects.add(parameterType.getTypeName()); + } + + String asListParams = + parameterTypes.stream() + .map(type -> "new $T<$T>() {}") + .collect(Collectors.joining(", ")); + + return CodeBlock.builder() + .addStatement( + "new $T($S, \n" + "$T.<$T>asList(" + asListParams + "))", + objects.toArray()) + .build(); + } + + /** + * Creates variable names for CustomError definitions. Duplicate names will be suffixed with + * number. + * + *
{@code
+     * Given error abi definitions;
+     * [
+     *   {"name": "aa", "type": "error", "inputs": []},
+     *   {"name": "aA", "type": "error", "inputs": []},
+     *   {"name": "bbb", "type": "error", "inputs": []},
+     *   {"name": "bBB", "type": "error", "inputs": [{"type": "string"}]},
+     *   {"name": "bbB", "type": "error", "inputs": []}
+     * ]
+     *
+     * Then variables with names;
+     * public static final CustomError AA_ERROR = ...
+     * public static final CustomError AA1_ERROR = ...
+     * public static final CustomError BBB_ERROR = ...
+     * public static final CustomError BBB1_ERROR = ...
+     * public static final CustomError BBB2_ERROR = ...
+     * }
+ */ + @NotNull + private String buildCustomErrorDefinitionName( + String customErrorName, @NotNull Map customErrorsOccurrences) { + + customErrorName = customErrorName.toUpperCase(); + Integer occurrences = customErrorsOccurrences.get(customErrorName); + if (occurrences > 1) { + customErrorsOccurrences.replace(customErrorName, occurrences - 1); + customErrorName = customErrorName + (occurrences - 1); + } + return customErrorName + "_ERROR"; + } + private String buildEventDefinitionName(String eventName) { return eventName.toUpperCase() + "_EVENT"; } @@ -451,6 +590,8 @@ List buildFunctionDefinitions( Set duplicateFunctionNames = getDuplicateFunctionNames(functionDefinitions); Map eventsCount = getDuplicatedEventNames(functionDefinitions); + Map customErrorsOccurrences = + getDuplicateCustomErrorNames(functionDefinitions); List methodSpecs = new ArrayList<>(); for (AbiDefinition functionDefinition : functionDefinitions) { if (functionDefinition.getType().equals(TYPE_FUNCTION)) { @@ -460,11 +601,34 @@ List buildFunctionDefinitions( } else if (functionDefinition.getType().equals(TYPE_EVENT)) { methodSpecs.addAll( buildEventFunctions(functionDefinition, classBuilder, eventsCount)); + } else if (functionDefinition.getType().equals(TYPE_ERROR)) { + buildCustomErrorDefinitions( + functionDefinition, classBuilder, customErrorsOccurrences); } } return methodSpecs; } + // TODO: Refactor to use for types of function, event and error + Map getDuplicateCustomErrorNames(@NotNull List abiDefinitions) { + Map countMap = new HashMap<>(); + + abiDefinitions.stream() + .filter(abi -> TYPE_ERROR.equals(abi.getType()) && abi.getName() != null) + .forEach( + abi -> { + String functionName = abi.getName().toUpperCase(); + if (countMap.containsKey(functionName)) { + int count = countMap.get(functionName); + countMap.put(functionName, count + 1); + } else { + countMap.put(functionName, 1); + } + }); + + return countMap; + } + Map getDuplicatedEventNames(List functionDefinitions) { Map countMap = new HashMap<>(); diff --git a/codegen/src/test/java/org/web3j/codegen/SolidityFunctionWrapperGeneratorTest.java b/codegen/src/test/java/org/web3j/codegen/SolidityFunctionWrapperGeneratorTest.java index efab656f84..374af63658 100644 --- a/codegen/src/test/java/org/web3j/codegen/SolidityFunctionWrapperGeneratorTest.java +++ b/codegen/src/test/java/org/web3j/codegen/SolidityFunctionWrapperGeneratorTest.java @@ -269,6 +269,12 @@ public void testStaticArrayOfStructsInStructGenerationCompareJavaFile() throws E compareJavaFile("StaticArrayOfStructsInStruct", true, false); } + @Test + public void testCustomErrorGeneration() throws Exception { + testCodeGeneration("customerror", "CustomError", JAVA_TYPES_ARG, false); + testCodeGeneration("customerror", "CustomError", SOLIDITY_TYPES_ARG, false); + } + private void compareJavaFile(String inputFileName, boolean useBin, boolean abiFuncs) throws Exception { String contract = inputFileName.toLowerCase(); diff --git a/codegen/src/test/java/org/web3j/codegen/SolidityFunctionWrapperTest.java b/codegen/src/test/java/org/web3j/codegen/SolidityFunctionWrapperTest.java index b907ab2d72..5b883a55eb 100644 --- a/codegen/src/test/java/org/web3j/codegen/SolidityFunctionWrapperTest.java +++ b/codegen/src/test/java/org/web3j/codegen/SolidityFunctionWrapperTest.java @@ -1168,4 +1168,96 @@ public void testBinaryWithUnlinkedLibraryLengthOver65534() throws Exception { + "__$927c5a12e2f339676f56d42ec1c0537964$__" + "a".repeat(40000)); } + + @Test + public void testBuildCustomErrorDefinitionsWithEmptyNameField() throws Exception { + List abiDefinitions = + List.of( + new AbiDefinition( + false, + List.of(new NamedType("reason", "string")), + "Error", + Collections.emptyList(), + "error", + false), + new AbiDefinition( + false, + List.of(new NamedType("account", "address")), + "", + Collections.emptyList(), + "error", + false), + new AbiDefinition( + false, + Collections.emptyList(), + "", + Collections.emptyList(), + "error", + false), + new AbiDefinition( + false, + Collections.emptyList(), + "Empty", + Collections.emptyList(), + "error", + false)); + String expectedJavaCode = + "class MyContract {\n" + + " public static final org.web3j.abi.datatypes.CustomError ERROR_ERROR = new org.web3j.abi.datatypes.CustomError(\"Error\", \n" + + " java.util.Arrays.>asList(new org.web3j.abi.TypeReference() {}));\n" + + " ;\n\n" + + " public static final org.web3j.abi.datatypes.CustomError EMPTY_ERROR = new org.web3j.abi.datatypes.CustomError(\"Empty\", \n" + + " java.util.Arrays.>asList());\n" + + " ;\n}\n"; + + TypeSpec.Builder builder = TypeSpec.classBuilder("MyContract"); + solidityFunctionWrapper.buildFunctionDefinitions("MyContract", builder, abiDefinitions); + + assertEquals(expectedJavaCode, builder.build().toString()); + } + + @Test + public void testBuildCustomErrorDefinitionsWithDuplicateNames() throws Exception { + List abiDefinitions = + List.of( + new AbiDefinition( + false, + List.of(new NamedType("account", "address")), + "invalidAccess", + Collections.emptyList(), + "error", + false), + new AbiDefinition( + false, + List.of(new NamedType("account", "address")), + "InvalidAccesS", + Collections.emptyList(), + "error", + false), + new AbiDefinition( + false, + Arrays.asList( + new NamedType("account", "address"), + new NamedType("reason", "string")), + "InvalidAccess", + Collections.emptyList(), + "error", + false)); + String expectedJavaCode = + "class MyContract {\n" + + " public static final org.web3j.abi.datatypes.CustomError INVALIDACCESS2_ERROR = new org.web3j.abi.datatypes.CustomError(\"invalidAccess\", \n" + + " java.util.Arrays.>asList(new org.web3j.abi.TypeReference() {}));\n" + + " ;\n\n" + + " public static final org.web3j.abi.datatypes.CustomError INVALIDACCESS1_ERROR = new org.web3j.abi.datatypes.CustomError(\"InvalidAccesS\", \n" + + " java.util.Arrays.>asList(new org.web3j.abi.TypeReference() {}));\n" + + " ;\n\n" + + " public static final org.web3j.abi.datatypes.CustomError INVALIDACCESS_ERROR = new org.web3j.abi.datatypes.CustomError(\"InvalidAccess\", \n" + + " java.util.Arrays.>asList(new org.web3j.abi.TypeReference() {}, new org.web3j.abi.TypeReference() {}));\n" + + " ;\n}\n"; + + TypeSpec.Builder builder = TypeSpec.classBuilder("MyContract"); + solidityFunctionWrapper.buildFunctionDefinitions("MyContract", builder, abiDefinitions); + + assertEquals(expectedJavaCode, builder.build().toString()); + } } diff --git a/codegen/src/test/resources/solidity/customerror/CustomError.sol b/codegen/src/test/resources/solidity/customerror/CustomError.sol new file mode 100644 index 0000000000..7e6e558c7c --- /dev/null +++ b/codegen/src/test/resources/solidity/customerror/CustomError.sol @@ -0,0 +1,11 @@ +pragma solidity ^0.8.4; + +contract CustomError { + address public _testAddress = 0x9A734f85fE7676096979503f8CEd26EA387138b4; + + error InvalidAccess(address, string reason); + + function customError() public { + revert InvalidAccess(_testAddress, "wrong address"); + } +} diff --git a/codegen/src/test/resources/solidity/customerror/build/CustomError.abi b/codegen/src/test/resources/solidity/customerror/build/CustomError.abi new file mode 100644 index 0000000000..8f55fc788e --- /dev/null +++ b/codegen/src/test/resources/solidity/customerror/build/CustomError.abi @@ -0,0 +1 @@ +[{"inputs":[{"internalType":"address","name":"","type":"address"},{"internalType":"string","name":"reason","type":"string"}],"name":"InvalidAccess","type":"error"},{"inputs":[],"name":"_testAddress","outputs":[{"internalType":"address","name":"","type":"address"}],"stateMutability":"view","type":"function"},{"inputs":[],"name":"customError","outputs":[],"stateMutability":"nonpayable","type":"function"}] \ No newline at end of file diff --git a/codegen/src/test/resources/solidity/customerror/build/CustomError.bin b/codegen/src/test/resources/solidity/customerror/build/CustomError.bin new file mode 100644 index 0000000000..9695f70f75 --- /dev/null +++ b/codegen/src/test/resources/solidity/customerror/build/CustomError.bin @@ -0,0 +1 @@ +6080604052739a734f85fe7676096979503f8ced26ea387138b46000806101000a81548173ffffffffffffffffffffffffffffffffffffffff021916908373ffffffffffffffffffffffffffffffffffffffff16021790555034801561006457600080fd5b50610201806100746000396000f3fe608060405234801561001057600080fd5b50600436106100365760003560e01c8063d693f59d1461003b578063dda3a7bd14610059575b600080fd5b610043610063565b6040516100509190610125565b60405180910390f35b610061610087565b005b60008054906101000a900473ffffffffffffffffffffffffffffffffffffffff1681565b60008054906101000a900473ffffffffffffffffffffffffffffffffffffffff166040517fccc96a260000000000000000000000000000000000000000000000000000000081526004016100db919061019d565b60405180910390fd5b600073ffffffffffffffffffffffffffffffffffffffff82169050919050565b600061010f826100e4565b9050919050565b61011f81610104565b82525050565b600060208201905061013a6000830184610116565b92915050565b600082825260208201905092915050565b7f77726f6e67206164647265737300000000000000000000000000000000000000600082015250565b6000610187600d83610140565b915061019282610151565b602082019050919050565b60006040820190506101b26000830184610116565b81810360208301526101c38161017a565b90509291505056fea2646970667358221220d0cc181a8c50b7b62161d498f20c16acfc67c1bb58a62342e98001508839122964736f6c63430008140033 \ No newline at end of file