From 162ed9e1c90fe8062a2e5f8dc996c5e28a5110c1 Mon Sep 17 00:00:00 2001 From: Alfonso Bribiesca Date: Tue, 28 Apr 2026 10:48:44 -0600 Subject: [PATCH 1/3] refactor: expose decoder methods as public static --- .../arkecosystem/crypto/utils/AbiBase.java | 2 +- .../arkecosystem/crypto/utils/AbiDecoder.java | 20 ++++----- .../crypto/utils/AbiDecoderTest.java | 44 +++++++++++++++++++ 3 files changed, 55 insertions(+), 11 deletions(-) diff --git a/src/main/java/org/arkecosystem/crypto/utils/AbiBase.java b/src/main/java/org/arkecosystem/crypto/utils/AbiBase.java index 039ad0d..275cb5a 100644 --- a/src/main/java/org/arkecosystem/crypto/utils/AbiBase.java +++ b/src/main/java/org/arkecosystem/crypto/utils/AbiBase.java @@ -25,7 +25,7 @@ public AbiBase(String abiFilePath) throws IOException { this.abi = (List>) abiJson.get("abi"); } - protected String[] getArrayComponents(String type) { + protected static String[] getArrayComponents(String type) { Pattern pattern = Pattern.compile("^(.*)\\[(\\d*)\\]$"); Matcher matcher = pattern.matcher(type); if (matcher.find()) { diff --git a/src/main/java/org/arkecosystem/crypto/utils/AbiDecoder.java b/src/main/java/org/arkecosystem/crypto/utils/AbiDecoder.java index 49dc7d1..14363ba 100644 --- a/src/main/java/org/arkecosystem/crypto/utils/AbiDecoder.java +++ b/src/main/java/org/arkecosystem/crypto/utils/AbiDecoder.java @@ -68,7 +68,7 @@ private List decodeAbiParameters(List> params, Strin return values; } - private Object[] decodeParameter(byte[] bytes, int offset, Map param) + private static Object[] decodeParameter(byte[] bytes, int offset, Map param) throws Exception { String type = (String) param.get("type"); String[] arrayComponents = getArrayComponents(type); @@ -108,7 +108,7 @@ private Object[] decodeParameter(byte[] bytes, int offset, Map p } } - private Object[] decodeAddress(byte[] bytes, int offset) { + public static Object[] decodeAddress(byte[] bytes, int offset) { byte[] data = Arrays.copyOfRange(bytes, offset, offset + 32); byte[] addressBytes = Arrays.copyOfRange(data, 12, 32); String address = "0x" + Numeric.toHexStringNoPrefix(addressBytes); @@ -117,14 +117,14 @@ private Object[] decodeAddress(byte[] bytes, int offset) { return new Object[] {address, 32}; } - private Object[] decodeBool(byte[] bytes, int offset) { + public static Object[] decodeBool(byte[] bytes, int offset) { byte[] data = Arrays.copyOfRange(bytes, offset, offset + 32); boolean value = new BigInteger(data).compareTo(BigInteger.ZERO) != 0; return new Object[] {value, 32}; } - private Object[] decodeNumber(byte[] bytes, int offset, int bits, boolean signed) { + public static Object[] decodeNumber(byte[] bytes, int offset, int bits, boolean signed) { byte[] data = Arrays.copyOfRange(bytes, offset, offset + 32); BigInteger value = new BigInteger(1, data); if (signed && value.testBit(bits - 1)) { @@ -134,7 +134,7 @@ private Object[] decodeNumber(byte[] bytes, int offset, int bits, boolean signed return new Object[] {value.toString(), 32}; } - private Object[] decodeString(byte[] bytes, int offset) { + public static Object[] decodeString(byte[] bytes, int offset) { int dataOffset = readUInt(bytes, offset).intValue(); int stringOffset = offset + dataOffset; int length = readUInt(bytes, stringOffset).intValue(); @@ -145,7 +145,7 @@ private Object[] decodeString(byte[] bytes, int offset) { return new Object[] {value, 32}; } - private Object[] decodeDynamicBytes(byte[] bytes, int offset) { + public static Object[] decodeDynamicBytes(byte[] bytes, int offset) { int dataOffset = readUInt(bytes, offset).intValue(); int bytesOffset = offset + dataOffset; int length = readUInt(bytes, bytesOffset).intValue(); @@ -155,14 +155,14 @@ private Object[] decodeDynamicBytes(byte[] bytes, int offset) { return new Object[] {value, 32}; } - private Object[] decodeFixedBytes(byte[] bytes, int offset, int size) { + public static Object[] decodeFixedBytes(byte[] bytes, int offset, int size) { byte[] data = Arrays.copyOfRange(bytes, offset, offset + 32); String value = "0x" + Numeric.toHexStringNoPrefix(Arrays.copyOfRange(data, 0, size)); return new Object[] {value, 32}; } - private Object[] decodeArray( + public static Object[] decodeArray( byte[] bytes, int offset, Map param, Integer length) throws Exception { String baseType = (String) param.get("type"); Map elementType = new HashMap<>(param); @@ -192,7 +192,7 @@ private Object[] decodeArray( return new Object[] {values, 32}; } - private Object[] decodeTuple(byte[] bytes, int offset, Map param) + public static Object[] decodeTuple(byte[] bytes, int offset, Map param) throws Exception { List> components = (List>) param.get("components"); Map values = new LinkedHashMap<>(); @@ -210,7 +210,7 @@ private Object[] decodeTuple(byte[] bytes, int offset, Map param return new Object[] {values, 32}; } - private BigInteger readUInt(byte[] bytes, int offset) { + public static BigInteger readUInt(byte[] bytes, int offset) { byte[] data = Arrays.copyOfRange(bytes, offset, offset + 32); return new BigInteger(1, data); } diff --git a/src/test/java/org/arkecosystem/crypto/utils/AbiDecoderTest.java b/src/test/java/org/arkecosystem/crypto/utils/AbiDecoderTest.java index 2e47471..4bebd8f 100644 --- a/src/test/java/org/arkecosystem/crypto/utils/AbiDecoderTest.java +++ b/src/test/java/org/arkecosystem/crypto/utils/AbiDecoderTest.java @@ -2,12 +2,14 @@ import static org.junit.jupiter.api.Assertions.assertEquals; +import java.math.BigInteger; import java.util.Arrays; import java.util.HashMap; import java.util.List; import java.util.Map; import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Test; +import org.web3j.utils.Numeric; class AbiDecoderTest { @@ -18,6 +20,48 @@ void setUp() throws Exception { decoder = new AbiDecoder(); } + @Test + void it_should_expose_decode_address_as_static() { + byte[] bytes = + Numeric.hexStringToByteArray( + "000000000000000000000000512f366d524157bcf734546eb29a6d687b762255"); + Object[] result = AbiDecoder.decodeAddress(bytes, 0); + + assertEquals("0x512F366D524157BcF734546eB29a6d687B762255", result[0]); + assertEquals(32, result[1]); + } + + @Test + void it_should_expose_decode_bool_as_static() { + byte[] truthy = + Numeric.hexStringToByteArray( + "0000000000000000000000000000000000000000000000000000000000000001"); + byte[] falsy = + Numeric.hexStringToByteArray( + "0000000000000000000000000000000000000000000000000000000000000000"); + + assertEquals(true, AbiDecoder.decodeBool(truthy, 0)[0]); + assertEquals(false, AbiDecoder.decodeBool(falsy, 0)[0]); + } + + @Test + void it_should_expose_decode_number_as_static() { + byte[] bytes = + Numeric.hexStringToByteArray( + "00000000000000000000000000000000000000000000000000000000000000ff"); + + assertEquals("255", AbiDecoder.decodeNumber(bytes, 0, 256, false)[0]); + } + + @Test + void it_should_expose_read_uint_as_static() { + byte[] bytes = + Numeric.hexStringToByteArray( + "0000000000000000000000000000000000000000000000000000000000000020"); + + assertEquals(BigInteger.valueOf(32), AbiDecoder.readUInt(bytes, 0)); + } + @Test void it_should_decode_vote_payload() throws Exception { String functionName = "vote"; From b8552771e621a053cdd3005eed7a20c2206686af Mon Sep 17 00:00:00 2001 From: Alfonso Bribiesca Date: Tue, 28 Apr 2026 13:24:57 -0600 Subject: [PATCH 2/3] test: cover all exposed static decoders --- .../crypto/utils/AbiDecoderTest.java | 115 ++++++++++++++++++ 1 file changed, 115 insertions(+) diff --git a/src/test/java/org/arkecosystem/crypto/utils/AbiDecoderTest.java b/src/test/java/org/arkecosystem/crypto/utils/AbiDecoderTest.java index 4bebd8f..aaf4bae 100644 --- a/src/test/java/org/arkecosystem/crypto/utils/AbiDecoderTest.java +++ b/src/test/java/org/arkecosystem/crypto/utils/AbiDecoderTest.java @@ -1,6 +1,7 @@ package org.arkecosystem.crypto.utils; import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertThrows; import java.math.BigInteger; import java.util.Arrays; @@ -62,6 +63,120 @@ void it_should_expose_read_uint_as_static() { assertEquals(BigInteger.valueOf(32), AbiDecoder.readUInt(bytes, 0)); } + @Test + void it_should_expose_decode_signed_negative_number_as_static() { + byte[] bytes = + Numeric.hexStringToByteArray( + "ffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff"); + + assertEquals("-1", AbiDecoder.decodeNumber(bytes, 0, 256, true)[0]); + } + + @Test + void it_should_expose_decode_string_as_static() { + byte[] bytes = + Numeric.hexStringToByteArray( + "0000000000000000000000000000000000000000000000000000000000000020" + + "000000000000000000000000000000000000000000000000000000000000000b" + + "68656c6c6f20776f726c64000000000000000000000000000000000000000000"); + + Object[] result = AbiDecoder.decodeString(bytes, 0); + + assertEquals("hello world", result[0]); + assertEquals(32, result[1]); + } + + @Test + void it_should_expose_decode_dynamic_bytes_as_static() { + byte[] bytes = + Numeric.hexStringToByteArray( + "0000000000000000000000000000000000000000000000000000000000000020" + + "0000000000000000000000000000000000000000000000000000000000000004" + + "deadbeef00000000000000000000000000000000000000000000000000000000"); + + Object[] result = AbiDecoder.decodeDynamicBytes(bytes, 0); + + assertEquals("0xdeadbeef", result[0]); + assertEquals(32, result[1]); + } + + @Test + void it_should_expose_decode_fixed_bytes_as_static() { + byte[] bytes = + Numeric.hexStringToByteArray( + "deadbeef00000000000000000000000000000000000000000000000000000000"); + + Object[] result = AbiDecoder.decodeFixedBytes(bytes, 0, 4); + + assertEquals("0xdeadbeef", result[0]); + assertEquals(32, result[1]); + } + + @Test + void it_should_expose_decode_fixed_array_as_static() throws Exception { + byte[] bytes = + Numeric.hexStringToByteArray( + "0000000000000000000000000000000000000000000000000000000000000001" + + "0000000000000000000000000000000000000000000000000000000000000002" + + "0000000000000000000000000000000000000000000000000000000000000003"); + Map param = new HashMap<>(); + param.put("type", "uint256"); + + Object[] result = AbiDecoder.decodeArray(bytes, 0, param, 3); + + assertEquals(Arrays.asList("1", "2", "3"), result[0]); + } + + @Test + void it_should_expose_decode_dynamic_array_as_static() throws Exception { + byte[] bytes = + Numeric.hexStringToByteArray( + "0000000000000000000000000000000000000000000000000000000000000020" + + "0000000000000000000000000000000000000000000000000000000000000003" + + "000000000000000000000000000000000000000000000000000000000000000a" + + "0000000000000000000000000000000000000000000000000000000000000014" + + "000000000000000000000000000000000000000000000000000000000000001e"); + Map param = new HashMap<>(); + param.put("type", "uint256"); + + Object[] result = AbiDecoder.decodeArray(bytes, 0, param, null); + + assertEquals(Arrays.asList("10", "20", "30"), result[0]); + } + + @Test + void it_should_expose_decode_static_tuple_as_static() throws Exception { + byte[] bytes = + Numeric.hexStringToByteArray( + "000000000000000000000000000000000000000000000000000000000000002a" + + "000000000000000000000000512f366d524157bcf734546eb29a6d687b762255"); + Map uintComponent = new HashMap<>(); + uintComponent.put("type", "uint256"); + uintComponent.put("name", "amount"); + Map addrComponent = new HashMap<>(); + addrComponent.put("type", "address"); + addrComponent.put("name", "recipient"); + Map param = new HashMap<>(); + param.put("components", Arrays.asList(uintComponent, addrComponent)); + + Object[] result = AbiDecoder.decodeTuple(bytes, 0, param); + + Map tuple = (Map) result[0]; + assertEquals("42", tuple.get("amount")); + assertEquals("0x512F366D524157BcF734546eB29a6d687B762255", tuple.get("recipient")); + } + + @Test + void it_should_throw_when_function_selector_is_not_found() { + String unknownSelector = "deadbeef"; + + Exception thrown = + assertThrows( + Exception.class, () -> decoder.decodeFunctionData("0x" + unknownSelector)); + + assertEquals("Function selector not found in ABI: " + unknownSelector, thrown.getMessage()); + } + @Test void it_should_decode_vote_payload() throws Exception { String functionName = "vote"; From 2f49f0d56ced910d94a35cc35acccfd1a67ac048 Mon Sep 17 00:00:00 2001 From: Alfonso Bribiesca Date: Tue, 28 Apr 2026 13:33:04 -0600 Subject: [PATCH 3/3] feat: add argument decoder --- .../crypto/utils/abi/ArgumentDecoder.java | 33 ++++++++ .../crypto/utils/abi/ArgumentDecoderTest.java | 76 +++++++++++++++++++ 2 files changed, 109 insertions(+) create mode 100644 src/main/java/org/arkecosystem/crypto/utils/abi/ArgumentDecoder.java create mode 100644 src/test/java/org/arkecosystem/crypto/utils/abi/ArgumentDecoderTest.java diff --git a/src/main/java/org/arkecosystem/crypto/utils/abi/ArgumentDecoder.java b/src/main/java/org/arkecosystem/crypto/utils/abi/ArgumentDecoder.java new file mode 100644 index 0000000..9e19727 --- /dev/null +++ b/src/main/java/org/arkecosystem/crypto/utils/abi/ArgumentDecoder.java @@ -0,0 +1,33 @@ +package org.arkecosystem.crypto.utils.abi; + +import org.arkecosystem.crypto.utils.AbiDecoder; +import org.web3j.utils.Numeric; + +public class ArgumentDecoder { + + private final byte[] bytes; + + public ArgumentDecoder(String hex) { + this.bytes = Numeric.hexStringToByteArray(hex); + } + + public String decodeString() { + return (String) AbiDecoder.decodeString(bytes, 0)[0]; + } + + public String decodeAddress() { + return (String) AbiDecoder.decodeAddress(bytes, 0)[0]; + } + + public String decodeUnsignedInt() { + return (String) AbiDecoder.decodeNumber(bytes, 0, 256, false)[0]; + } + + public String decodeSignedInt() { + return (String) AbiDecoder.decodeNumber(bytes, 0, 256, true)[0]; + } + + public boolean decodeBool() { + return (boolean) AbiDecoder.decodeBool(bytes, 0)[0]; + } +} diff --git a/src/test/java/org/arkecosystem/crypto/utils/abi/ArgumentDecoderTest.java b/src/test/java/org/arkecosystem/crypto/utils/abi/ArgumentDecoderTest.java new file mode 100644 index 0000000..9768404 --- /dev/null +++ b/src/test/java/org/arkecosystem/crypto/utils/abi/ArgumentDecoderTest.java @@ -0,0 +1,76 @@ +package org.arkecosystem.crypto.utils.abi; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertFalse; +import static org.junit.jupiter.api.Assertions.assertTrue; + +import org.junit.jupiter.api.Test; + +class ArgumentDecoderTest { + + @Test + void it_should_decode_string() { + String payload = + "0000000000000000000000000000000000000000000000000000000000000020" + + "000000000000000000000000000000000000000000000000000000000000000D" + + "48656C6C6F2C20776F726C642100000000000000000000000000000000000000"; + + ArgumentDecoder decoder = new ArgumentDecoder(payload); + + assertEquals("Hello, world!", decoder.decodeString()); + } + + @Test + void it_should_decode_address() { + String payload = "000000000000000000000000512F366D524157BcF734546eB29a6d687B762255"; + + ArgumentDecoder decoder = new ArgumentDecoder(payload); + + assertEquals("0x512F366D524157BcF734546eB29a6d687B762255", decoder.decodeAddress()); + } + + @Test + void it_should_decode_unsigned_int() { + String payload = "000000000000000000000000000000000000000000000000016345785d8a0000"; + + ArgumentDecoder decoder = new ArgumentDecoder(payload); + + assertEquals("100000000000000000", decoder.decodeUnsignedInt()); + } + + @Test + void it_should_decode_signed_int() { + String payload = "000000000000000000000000000000000000000000000000016345785d8a0000"; + + ArgumentDecoder decoder = new ArgumentDecoder(payload); + + assertEquals("100000000000000000", decoder.decodeSignedInt()); + } + + @Test + void it_should_decode_negative_signed_int() { + String payload = "ffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff"; + + ArgumentDecoder decoder = new ArgumentDecoder(payload); + + assertEquals("-1", decoder.decodeSignedInt()); + } + + @Test + void it_should_decode_bool_as_true() { + String payload = "0000000000000000000000000000000000000000000000000000000000000001"; + + ArgumentDecoder decoder = new ArgumentDecoder(payload); + + assertTrue(decoder.decodeBool()); + } + + @Test + void it_should_decode_bool_as_false() { + String payload = "0000000000000000000000000000000000000000000000000000000000000000"; + + ArgumentDecoder decoder = new ArgumentDecoder(payload); + + assertFalse(decoder.decodeBool()); + } +}