diff --git a/abi-parser-simple/src/main/java/com/openelements/hiero/smartcontract/abi/simple/GsonAbiParser.java b/abi-parser-simple/src/main/java/com/openelements/hiero/smartcontract/abi/simple/GsonAbiParser.java index b07d4fd..e042e5e 100644 --- a/abi-parser-simple/src/main/java/com/openelements/hiero/smartcontract/abi/simple/GsonAbiParser.java +++ b/abi-parser-simple/src/main/java/com/openelements/hiero/smartcontract/abi/simple/GsonAbiParser.java @@ -18,7 +18,6 @@ public GsonAbiParser() { this.gson = new GsonBuilder() .registerTypeAdapterFactory(new AbiTypeAdapterFactory()) .create(); - } public @NonNull AbiModel parse(@NonNull Reader abiReader) throws AbiParserException { diff --git a/abi-parser-simple/src/main/java/com/openelements/hiero/smartcontract/abi/simple/implementation/AbiEntryTypeAdapter.java b/abi-parser-simple/src/main/java/com/openelements/hiero/smartcontract/abi/simple/implementation/AbiEntryTypeAdapter.java index 8196991..4144d6f 100644 --- a/abi-parser-simple/src/main/java/com/openelements/hiero/smartcontract/abi/simple/implementation/AbiEntryTypeAdapter.java +++ b/abi-parser-simple/src/main/java/com/openelements/hiero/smartcontract/abi/simple/implementation/AbiEntryTypeAdapter.java @@ -12,6 +12,7 @@ import java.util.ArrayList; import java.util.Collections; import java.util.List; +import org.jspecify.annotations.NonNull; public class AbiEntryTypeAdapter extends BasicTypeAdapter { @@ -20,7 +21,7 @@ public AbiEntryTypeAdapter(Gson gson) { } @Override - public AbiEntry read(JsonReader in) throws IOException { + public AbiEntry read(@NonNull final JsonReader in) throws IOException { final JsonObject abiEntryObject = readObject(in); final AbiEntryType type = getStringValue(abiEntryObject, "type") .map(AbiEntryType::of) diff --git a/abi-parser-simple/src/main/java/com/openelements/hiero/smartcontract/abi/simple/implementation/AbiModelTypeAdapter.java b/abi-parser-simple/src/main/java/com/openelements/hiero/smartcontract/abi/simple/implementation/AbiModelTypeAdapter.java index 5c8811f..28da97b 100644 --- a/abi-parser-simple/src/main/java/com/openelements/hiero/smartcontract/abi/simple/implementation/AbiModelTypeAdapter.java +++ b/abi-parser-simple/src/main/java/com/openelements/hiero/smartcontract/abi/simple/implementation/AbiModelTypeAdapter.java @@ -18,7 +18,7 @@ public AbiModelTypeAdapter(@NonNull Gson gson) { } @Override - public AbiModel read(JsonReader in) throws IOException { + public AbiModel read(@NonNull final JsonReader in) throws IOException { JsonArray array = readArray(in); final List entries = new ArrayList<>(); array.forEach(jsonElement -> { diff --git a/abi-parser-simple/src/main/java/com/openelements/hiero/smartcontract/abi/simple/implementation/AbiParameterTypeAdapter.java b/abi-parser-simple/src/main/java/com/openelements/hiero/smartcontract/abi/simple/implementation/AbiParameterTypeAdapter.java index dcd73c4..5028fd3 100644 --- a/abi-parser-simple/src/main/java/com/openelements/hiero/smartcontract/abi/simple/implementation/AbiParameterTypeAdapter.java +++ b/abi-parser-simple/src/main/java/com/openelements/hiero/smartcontract/abi/simple/implementation/AbiParameterTypeAdapter.java @@ -9,6 +9,7 @@ import java.util.ArrayList; import java.util.Collections; import java.util.List; +import org.jspecify.annotations.NonNull; public class AbiParameterTypeAdapter extends BasicTypeAdapter { @@ -17,7 +18,7 @@ public AbiParameterTypeAdapter(Gson gson) { } @Override - public AbiParameter read(JsonReader in) throws IOException { + public AbiParameter read(@NonNull final JsonReader in) throws IOException { final JsonObject parameterObject = readObject(in); final String name = getStringValue(parameterObject, "name") diff --git a/abi-parser-simple/src/main/java/com/openelements/hiero/smartcontract/abi/simple/implementation/AbiTypeAdapterFactory.java b/abi-parser-simple/src/main/java/com/openelements/hiero/smartcontract/abi/simple/implementation/AbiTypeAdapterFactory.java index 2b029c6..f1b600a 100644 --- a/abi-parser-simple/src/main/java/com/openelements/hiero/smartcontract/abi/simple/implementation/AbiTypeAdapterFactory.java +++ b/abi-parser-simple/src/main/java/com/openelements/hiero/smartcontract/abi/simple/implementation/AbiTypeAdapterFactory.java @@ -7,11 +7,14 @@ import com.openelements.hiero.smartcontract.abi.model.AbiEntry; import com.openelements.hiero.smartcontract.abi.model.AbiModel; import com.openelements.hiero.smartcontract.abi.model.AbiParameter; +import java.util.Objects; +import org.jspecify.annotations.NonNull; public class AbiTypeAdapterFactory implements TypeAdapterFactory { @Override - public TypeAdapter create(Gson gson, TypeToken type) { + public TypeAdapter create(@NonNull final Gson gson, @NonNull final TypeToken type) { + Objects.requireNonNull(type, "type must not be null"); if (type.getRawType().equals(AbiModel.class)) { return (TypeAdapter) new AbiModelTypeAdapter(gson); } diff --git a/abi-parser-simple/src/main/java/com/openelements/hiero/smartcontract/abi/simple/implementation/BasicTypeAdapter.java b/abi-parser-simple/src/main/java/com/openelements/hiero/smartcontract/abi/simple/implementation/BasicTypeAdapter.java index b25273e..6c87c30 100644 --- a/abi-parser-simple/src/main/java/com/openelements/hiero/smartcontract/abi/simple/implementation/BasicTypeAdapter.java +++ b/abi-parser-simple/src/main/java/com/openelements/hiero/smartcontract/abi/simple/implementation/BasicTypeAdapter.java @@ -21,31 +21,30 @@ public abstract class BasicTypeAdapter extends TypeAdapter { private final TypeAdapter arrayTypeAdapter; - - public BasicTypeAdapter(final Gson gson) { + public BasicTypeAdapter(@NonNull final Gson gson) { this.gson = Objects.requireNonNull(gson, "gson"); objectTypeAdapter = gson.getAdapter(JsonObject.class); arrayTypeAdapter = gson.getAdapter(JsonArray.class); } - protected JsonObject readObject(JsonReader in) throws IOException { + protected JsonObject readObject(@NonNull final JsonReader in) throws IOException { return objectTypeAdapter.read(in); } - protected JsonArray readArray(JsonReader in) throws IOException { + protected JsonArray readArray(@NonNull final JsonReader in) throws IOException { return arrayTypeAdapter.read(in); } - - protected T fromJson(JsonElement json, Class classOfT) throws JsonSyntaxException { + protected T fromJson(@NonNull final JsonElement json, @NonNull final Class classOfT) throws JsonSyntaxException { return gson.fromJson(json, classOfT); } - + @NonNull protected Gson getGson() { return gson; } + @NonNull protected TypeAdapter getObjectTypeAdapter() { return objectTypeAdapter; } @@ -55,6 +54,7 @@ public void write(JsonWriter out, T value) throws IOException { throw new UnsupportedOperationException("Not supported yet."); } + @NonNull protected JsonArray getArray(@NonNull final JsonObject jsonObject, @NonNull final String key) { Objects.requireNonNull(jsonObject, "jsonObject"); Objects.requireNonNull(key, "key"); @@ -69,6 +69,7 @@ protected JsonArray getArray(@NonNull final JsonObject jsonObject, @NonNull fina return new JsonArray(); } + @NonNull protected Optional getStringValue(@NonNull final JsonObject jsonObject, @NonNull final String key) { Objects.requireNonNull(jsonObject, "jsonObject"); Objects.requireNonNull(key, "key"); @@ -82,6 +83,7 @@ protected Optional getStringValue(@NonNull final JsonObject jsonObject, return Optional.empty(); } + @NonNull protected Optional getBooleanValue(@NonNull final JsonObject jsonObject, @NonNull final String key) { Objects.requireNonNull(jsonObject, "jsonObject"); Objects.requireNonNull(key, "key"); diff --git a/abi-parser/pom.xml b/abi-parser/pom.xml index b75bef4..6ed2c3b 100644 --- a/abi-parser/pom.xml +++ b/abi-parser/pom.xml @@ -23,6 +23,15 @@ jspecify compile + + org.bouncycastle + bcprov-jdk15to18 + + + org.junit.jupiter + junit-jupiter + test + diff --git a/abi-parser/src/main/java/com/openelements/hiero/smartcontract/abi/model/AbiEvent.java b/abi-parser/src/main/java/com/openelements/hiero/smartcontract/abi/model/AbiEvent.java index a0d793a..38a90c4 100644 --- a/abi-parser/src/main/java/com/openelements/hiero/smartcontract/abi/model/AbiEvent.java +++ b/abi-parser/src/main/java/com/openelements/hiero/smartcontract/abi/model/AbiEvent.java @@ -1,9 +1,11 @@ package com.openelements.hiero.smartcontract.abi.model; +import com.openelements.hiero.smartcontract.abi.util.HexConverter; +import com.openelements.hiero.smartcontract.abi.util.KeccakUtil; +import java.nio.charset.StandardCharsets; import java.util.List; import java.util.Objects; import org.jspecify.annotations.NonNull; -import org.jspecify.annotations.Nullable; public record AbiEvent(@NonNull String name, @NonNull List inputs, boolean anonymous) implements AbiEntry{ @@ -16,4 +18,32 @@ public record AbiEvent(@NonNull String name, @NonNull List inputs, public AbiEntryType type() { return AbiEntryType.EVENT; } + + @NonNull + public List getIndexedInputParameters() { + return inputs.stream().filter(AbiParameter::indexed).toList(); + } + + @NonNull + public List getNonIndexedInputParameters() { + return inputs.stream().filter(parameter -> !parameter.indexed()).toList(); + } + + @NonNull + private String createEventSignature() { + final List canonicalParameterTypes = inputs.stream().map(AbiParameter::getCanonicalType).toList(); + return name + "(" + String.join(",", canonicalParameterTypes) + ")"; + } + + @NonNull + private byte[] createEventSignatureHash() { + final String eventSignature = createEventSignature(); + return KeccakUtil.keccak256(eventSignature.getBytes(StandardCharsets.UTF_8)); + } + + @NonNull + public String createEventSignatureHashAsHex() { + final byte[] eventSignatureHash = createEventSignatureHash(); + return "0x" + HexConverter.bytesToHex(eventSignatureHash); + } } diff --git a/abi-parser/src/main/java/com/openelements/hiero/smartcontract/abi/model/AbiFunction.java b/abi-parser/src/main/java/com/openelements/hiero/smartcontract/abi/model/AbiFunction.java index 8887f96..1ab1da0 100644 --- a/abi-parser/src/main/java/com/openelements/hiero/smartcontract/abi/model/AbiFunction.java +++ b/abi-parser/src/main/java/com/openelements/hiero/smartcontract/abi/model/AbiFunction.java @@ -36,6 +36,5 @@ public record AbiFunction(@NonNull AbiEntryType type, @Nullable String name, @No Objects.requireNonNull(inputs, "inputs"); Objects.requireNonNull(outputs, "outputs"); Objects.requireNonNull(stateMutability, "stateMutability"); - } } diff --git a/abi-parser/src/main/java/com/openelements/hiero/smartcontract/abi/model/AbiModel.java b/abi-parser/src/main/java/com/openelements/hiero/smartcontract/abi/model/AbiModel.java index e027b79..35ea604 100644 --- a/abi-parser/src/main/java/com/openelements/hiero/smartcontract/abi/model/AbiModel.java +++ b/abi-parser/src/main/java/com/openelements/hiero/smartcontract/abi/model/AbiModel.java @@ -10,19 +10,24 @@ public record AbiModel(@NonNull List entries) { Objects.requireNonNull(entries, "entries"); } + @NonNull public List getFunctions() { return getEntriesOfType(AbiFunction.class); } + @NonNull public List getEvents() { return getEntriesOfType(AbiEvent.class); } + @NonNull public List getErrors() { return getEntriesOfType(AbiError.class); } - private List getEntriesOfType(Class type) { + @NonNull + private List getEntriesOfType(@NonNull final Class type) { + Objects.requireNonNull(type, "type must not be null"); return entries.stream() .filter(type::isInstance) .map(type::cast) diff --git a/abi-parser/src/main/java/com/openelements/hiero/smartcontract/abi/model/AbiParameter.java b/abi-parser/src/main/java/com/openelements/hiero/smartcontract/abi/model/AbiParameter.java index 3323a21..4ff104c 100644 --- a/abi-parser/src/main/java/com/openelements/hiero/smartcontract/abi/model/AbiParameter.java +++ b/abi-parser/src/main/java/com/openelements/hiero/smartcontract/abi/model/AbiParameter.java @@ -18,4 +18,8 @@ public record AbiParameter(@NonNull String name, @NonNull AbiParameterType type, } } + public String getCanonicalType() { + return type.getCanonicalType(); + } + } diff --git a/abi-parser/src/main/java/com/openelements/hiero/smartcontract/abi/model/AbiParameterType.java b/abi-parser/src/main/java/com/openelements/hiero/smartcontract/abi/model/AbiParameterType.java index 8e2dbb1..2215d8d 100644 --- a/abi-parser/src/main/java/com/openelements/hiero/smartcontract/abi/model/AbiParameterType.java +++ b/abi-parser/src/main/java/com/openelements/hiero/smartcontract/abi/model/AbiParameterType.java @@ -6,6 +6,7 @@ public enum AbiParameterType { BYTE32, BOOL, UINT256, + UINT, TUPLE; public static AbiParameterType of(final String name) { @@ -15,7 +16,39 @@ public static AbiParameterType of(final String name) { case "bytes32" -> BYTE32; case "bool" -> BOOL; case "uint256" -> UINT256; + case "uint" -> UINT; + case "tuple" -> TUPLE; default -> throw new IllegalArgumentException("Unknown value type: " + name); }; } + + public String getCanonicalType() { + return switch (this) { + case ADDRESS -> "address"; + case STRING -> "string"; + case BYTE32 -> "bytes32"; + case BOOL -> "bool"; + case UINT256 -> "uint256"; + case TUPLE -> "tuple"; + case UINT -> "uint256"; + }; + } + + public boolean isDynamic() { + return switch (this) { + case STRING, BYTE32 -> true; + default -> false; + }; + } + + public int getFixedSize() { + return switch (this) { + case ADDRESS -> 20; + case BOOL -> 1; + case UINT256 -> 32; + case UINT -> 32; + case BYTE32 -> 32; + default -> throw new IllegalStateException("No fixed size for type: " + this); + }; + } } diff --git a/abi-parser/src/main/java/com/openelements/hiero/smartcontract/abi/util/HexConverter.java b/abi-parser/src/main/java/com/openelements/hiero/smartcontract/abi/util/HexConverter.java new file mode 100644 index 0000000..909b0f6 --- /dev/null +++ b/abi-parser/src/main/java/com/openelements/hiero/smartcontract/abi/util/HexConverter.java @@ -0,0 +1,35 @@ +package com.openelements.hiero.smartcontract.abi.util; + +import java.nio.charset.StandardCharsets; +import java.util.Objects; +import org.jspecify.annotations.NonNull; + +public class HexConverter { + + private static final byte[] HEX_ARRAY = "0123456789ABCDEF".getBytes(StandardCharsets.US_ASCII); + + @NonNull + public static String bytesToHex(@NonNull final byte[] bytes) { + //see https://stackoverflow.com/questions/9655181/java-convert-a-byte-array-to-a-hex-string + Objects.requireNonNull(bytes, "bytes must not be null"); + final byte[] hexChars = new byte[bytes.length * 2]; + for (int j = 0; j < bytes.length; j++) { + int v = bytes[j] & 0xFF; + hexChars[j * 2] = HEX_ARRAY[v >>> 4]; + hexChars[j * 2 + 1] = HEX_ARRAY[v & 0x0F]; + } + return new String(hexChars, StandardCharsets.UTF_8).toLowerCase(); + } + + @NonNull + public static byte[] hexToBytes(@NonNull final String hex) { + Objects.requireNonNull(hex, "hex must not be null"); + int len = hex.length(); + byte[] data = new byte[len / 2]; + for (int i = 0; i < len; i += 2) { + data[i / 2] = (byte) ((Character.digit(hex.charAt(i), 16) << 4) + + Character.digit(hex.charAt(i+1), 16)); + } + return data; + } +} diff --git a/abi-parser/src/main/java/com/openelements/hiero/smartcontract/abi/util/KeccakUtil.java b/abi-parser/src/main/java/com/openelements/hiero/smartcontract/abi/util/KeccakUtil.java new file mode 100644 index 0000000..9a87a53 --- /dev/null +++ b/abi-parser/src/main/java/com/openelements/hiero/smartcontract/abi/util/KeccakUtil.java @@ -0,0 +1,13 @@ +package com.openelements.hiero.smartcontract.abi.util; + +import org.bouncycastle.jcajce.provider.digest.Keccak; +import org.jspecify.annotations.NonNull; + +public class KeccakUtil { + + public static byte[] keccak256(@NonNull final byte[] data) { + final Keccak.DigestKeccak kecc = new Keccak.Digest256(); + kecc.update(data); + return kecc.digest(); + } +} diff --git a/abi-parser/src/main/java/module-info.java b/abi-parser/src/main/java/module-info.java index 84eab7f..9116606 100644 --- a/abi-parser/src/main/java/module-info.java +++ b/abi-parser/src/main/java/module-info.java @@ -1,5 +1,7 @@ module com.openelements.hiero.smartcontract.abi { exports com.openelements.hiero.smartcontract.abi; exports com.openelements.hiero.smartcontract.abi.model; + exports com.openelements.hiero.smartcontract.abi.util; requires org.jspecify; + requires org.bouncycastle.provider; } \ No newline at end of file diff --git a/abi-parser/src/test/java/com/openelements/hiero/smartcontract/abi/test/AbiEventTests.java b/abi-parser/src/test/java/com/openelements/hiero/smartcontract/abi/test/AbiEventTests.java new file mode 100644 index 0000000..4db2c54 --- /dev/null +++ b/abi-parser/src/test/java/com/openelements/hiero/smartcontract/abi/test/AbiEventTests.java @@ -0,0 +1,44 @@ +package com.openelements.hiero.smartcontract.abi.test; + +import com.openelements.hiero.smartcontract.abi.model.AbiEvent; +import com.openelements.hiero.smartcontract.abi.model.AbiParameter; +import com.openelements.hiero.smartcontract.abi.model.AbiParameterType; +import java.util.List; +import org.junit.jupiter.api.Assertions; +import org.junit.jupiter.api.Test; + +public class AbiEventTests { + + @Test + void testEventSignatureCreation() { + // Given + final List inputs = List.of( + new AbiParameter("identifier_", AbiParameterType.STRING, List.of(), false) + ); + final AbiEvent event = new AbiEvent("HashAdded", inputs, false); + + + // When + String hex = event.createEventSignatureHashAsHex(); + + // Then + Assertions.assertEquals("0xe0c9f5e6f5abddac86dac0e02afc9f3fda7b7fc6d9454a13c51fcb28621e1e5f", hex); + } + + @Test + void testEventSignatureCreation2() { + // Given + final List inputs = List.of( + new AbiParameter("count", AbiParameterType.UINT, List.of(), false) + ); + final AbiEvent event = new AbiEvent("MissingVerificationCountUpdated", inputs, false); + + + // When + String hex = event.createEventSignatureHashAsHex(); + + // Then + Assertions.assertEquals("0x271219bdbb9b91472a5df68ef7a9d3f8de02f3c27b93a35306f888acf081ea60", hex); + } + +} diff --git a/abi-parser/src/test/java/com/openelements/hiero/smartcontract/abi/test/HexConverterTests.java b/abi-parser/src/test/java/com/openelements/hiero/smartcontract/abi/test/HexConverterTests.java new file mode 100644 index 0000000..107652d --- /dev/null +++ b/abi-parser/src/test/java/com/openelements/hiero/smartcontract/abi/test/HexConverterTests.java @@ -0,0 +1,32 @@ +package com.openelements.hiero.smartcontract.abi.test; + +import com.openelements.hiero.smartcontract.abi.util.HexConverter; +import java.util.stream.Stream; +import org.junit.jupiter.api.Assertions; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.params.ParameterizedTest; +import org.junit.jupiter.params.provider.Arguments; +import org.junit.jupiter.params.provider.MethodSource; + +public class HexConverterTests { + + @ParameterizedTest + @MethodSource("provideArgumentsForHexToBytesTest") + void hexToBytesTest(String hexInput, byte[] expectedBytes) { + //given + final String hex = "00"; + + //when + final byte[] bytes = HexConverter.hexToBytes(hex); + + //then + Assertions.assertArrayEquals(new byte[]{0x0}, bytes); + } + + private static Stream provideArgumentsForHexToBytesTest() { + return Stream.of( + Arguments.of("00", new byte[]{0x0}), + Arguments.of("01", new byte[]{0x1}) + ); + } +} diff --git a/abi-parser/src/test/java/module-info.java b/abi-parser/src/test/java/module-info.java new file mode 100644 index 0000000..27881b2 --- /dev/null +++ b/abi-parser/src/test/java/module-info.java @@ -0,0 +1,5 @@ +open module com.openelements.hiero.smartcontract.abi.test { + requires com.openelements.hiero.smartcontract.abi; + requires org.bouncycastle.provider; + requires org.junit.jupiter.params; +} \ No newline at end of file diff --git a/pom.xml b/pom.xml index 3f53737..4da8c19 100644 --- a/pom.xml +++ b/pom.xml @@ -53,6 +53,9 @@ 1.1.1 2.10.1 3.6.1.Final + 1.1.0 + 1.76 + 3.3.1 3.13.0 3.3.1 @@ -191,6 +194,11 @@ gson ${gson.version} + + org.bouncycastle + bcprov-jdk15to18 + ${bouncycastle.version} +