From 41c3c329a1ac5f5946e2fd7239f2186e85f482fa Mon Sep 17 00:00:00 2001 From: Alfonso Bribiesca Date: Tue, 28 Apr 2026 13:20:27 -0600 Subject: [PATCH 1/2] refactor: identify tx types via abi method ids --- .../crypto/enums/ContractAbiType.java | 10 ++ .../crypto/transactions/Deserializer.java | 32 ++--- .../builder/TokenApproveBuilder.java | 3 +- .../builder/TokenTransferBuilder.java | 3 +- .../arkecosystem/crypto/utils/AbiBase.java | 79 ++++++++++++- .../arkecosystem/crypto/utils/AbiDecoder.java | 9 ++ .../arkecosystem/crypto/utils/AbiEncoder.java | 9 +- .../utils/TransactionTypeIdentifier.java | 100 ++++++++++++++++ .../utils/TransactionTypeIdentifierTest.java | 111 ++++++++++++++++++ 9 files changed, 321 insertions(+), 35 deletions(-) create mode 100644 src/main/java/org/arkecosystem/crypto/enums/ContractAbiType.java create mode 100644 src/main/java/org/arkecosystem/crypto/utils/TransactionTypeIdentifier.java create mode 100644 src/test/java/org/arkecosystem/crypto/utils/TransactionTypeIdentifierTest.java diff --git a/src/main/java/org/arkecosystem/crypto/enums/ContractAbiType.java b/src/main/java/org/arkecosystem/crypto/enums/ContractAbiType.java new file mode 100644 index 0000000..f275aac --- /dev/null +++ b/src/main/java/org/arkecosystem/crypto/enums/ContractAbiType.java @@ -0,0 +1,10 @@ +package org.arkecosystem.crypto.enums; + +public enum ContractAbiType { + CONSENSUS, + MULTIPAYMENT, + USERNAMES, + ERC20BATCH_TRANSFER, + TOKEN, + CUSTOM +} diff --git a/src/main/java/org/arkecosystem/crypto/transactions/Deserializer.java b/src/main/java/org/arkecosystem/crypto/transactions/Deserializer.java index 7d26290..fedfa1e 100644 --- a/src/main/java/org/arkecosystem/crypto/transactions/Deserializer.java +++ b/src/main/java/org/arkecosystem/crypto/transactions/Deserializer.java @@ -2,13 +2,11 @@ import java.math.BigInteger; import java.util.List; -import java.util.Map; import org.arkecosystem.crypto.configuration.Network; import org.arkecosystem.crypto.encoding.Hex; -import org.arkecosystem.crypto.enums.AbiFunction; import org.arkecosystem.crypto.transactions.types.*; -import org.arkecosystem.crypto.utils.AbiDecoder; import org.arkecosystem.crypto.utils.RlpDecoder; +import org.arkecosystem.crypto.utils.TransactionTypeIdentifier; public class Deserializer { @@ -90,40 +88,24 @@ private AbstractTransaction guessTransactionFromTransactionData( return new Transfer(); } - Map payloadData = decodePayload(transactionData); - if (payloadData == null) { + String payload = transactionData.data != null ? transactionData.data : ""; + if (payload.isEmpty()) { return new EvmCall(); } - String functionName = (String) payloadData.get("functionName"); - - if (functionName.equals(AbiFunction.VOTE.toString())) { + if (TransactionTypeIdentifier.isVote(payload)) { return new Vote(transactionData.toHashMap()); - } else if (functionName.equals(AbiFunction.UNVOTE.toString())) { + } else if (TransactionTypeIdentifier.isUnvote(payload)) { return new Unvote(transactionData.toHashMap()); - } else if (functionName.equals(AbiFunction.VALIDATOR_REGISTRATION.toString())) { + } else if (TransactionTypeIdentifier.isValidatorRegistration(payload)) { return new ValidatorRegistration(transactionData.toHashMap()); - } else if (functionName.equals(AbiFunction.VALIDATOR_RESIGNATION.toString())) { + } else if (TransactionTypeIdentifier.isValidatorResignation(payload)) { return new ValidatorResignation(transactionData.toHashMap()); } return new EvmCall(); } - private Map decodePayload(AbstractTransaction transaction) { - String payload = transaction.data != null ? transaction.data : ""; - if (payload.isEmpty()) { - return null; - } - - try { - AbiDecoder abiDecoder = new AbiDecoder(); - return abiDecoder.decodeFunctionData(payload); - } catch (Exception e) { - return null; - } - } - private static long bytesToLong(byte[] bytes) { if (bytes.length == 0) return 0; return new BigInteger(1, bytes).longValue(); diff --git a/src/main/java/org/arkecosystem/crypto/transactions/builder/TokenApproveBuilder.java b/src/main/java/org/arkecosystem/crypto/transactions/builder/TokenApproveBuilder.java index 071e1f3..39a3e9a 100644 --- a/src/main/java/org/arkecosystem/crypto/transactions/builder/TokenApproveBuilder.java +++ b/src/main/java/org/arkecosystem/crypto/transactions/builder/TokenApproveBuilder.java @@ -4,6 +4,7 @@ import java.util.ArrayList; import java.util.List; import org.arkecosystem.crypto.enums.AbiFunction; +import org.arkecosystem.crypto.enums.ContractAbiType; import org.arkecosystem.crypto.transactions.types.AbstractTransaction; import org.arkecosystem.crypto.transactions.types.EvmCall; import org.arkecosystem.crypto.utils.AbiEncoder; @@ -21,7 +22,7 @@ public TokenApproveBuilder spender(String address, BigInteger amount) { try { String payload = - new AbiEncoder("Abi.Token.json") + new AbiEncoder(ContractAbiType.TOKEN) .encodeFunctionCall(AbiFunction.APPROVE.toString(), args); this.transaction.data = payload.replaceFirst("^0x", ""); diff --git a/src/main/java/org/arkecosystem/crypto/transactions/builder/TokenTransferBuilder.java b/src/main/java/org/arkecosystem/crypto/transactions/builder/TokenTransferBuilder.java index 386e1f7..ec12591 100644 --- a/src/main/java/org/arkecosystem/crypto/transactions/builder/TokenTransferBuilder.java +++ b/src/main/java/org/arkecosystem/crypto/transactions/builder/TokenTransferBuilder.java @@ -4,6 +4,7 @@ import java.util.ArrayList; import java.util.List; import org.arkecosystem.crypto.enums.AbiFunction; +import org.arkecosystem.crypto.enums.ContractAbiType; import org.arkecosystem.crypto.transactions.types.AbstractTransaction; import org.arkecosystem.crypto.transactions.types.EvmCall; import org.arkecosystem.crypto.utils.AbiEncoder; @@ -21,7 +22,7 @@ public TokenTransferBuilder recipient(String address, BigInteger amount) { try { String payload = - new AbiEncoder("Abi.Token.json") + new AbiEncoder(ContractAbiType.TOKEN) .encodeFunctionCall(AbiFunction.TRANSFER.toString(), args); this.transaction.data = payload.replaceFirst("^0x", ""); diff --git a/src/main/java/org/arkecosystem/crypto/utils/AbiBase.java b/src/main/java/org/arkecosystem/crypto/utils/AbiBase.java index 275cb5a..865669b 100644 --- a/src/main/java/org/arkecosystem/crypto/utils/AbiBase.java +++ b/src/main/java/org/arkecosystem/crypto/utils/AbiBase.java @@ -3,9 +3,12 @@ import com.fasterxml.jackson.databind.ObjectMapper; import java.io.IOException; import java.io.InputStream; +import java.nio.file.Files; +import java.nio.file.Paths; import java.util.*; import java.util.regex.Matcher; import java.util.regex.Pattern; +import org.arkecosystem.crypto.enums.ContractAbiType; import org.web3j.crypto.Hash; import org.web3j.utils.Numeric; @@ -14,15 +17,35 @@ public abstract class AbiBase { protected List> abi; public AbiBase() throws IOException { - this("Abi.Consensus.json"); + this(ContractAbiType.CONSENSUS, null); } - public AbiBase(String abiFilePath) throws IOException { - InputStream abiInputStream = getClass().getClassLoader().getResourceAsStream(abiFilePath); + public AbiBase(ContractAbiType type) throws IOException { + this(type, null); + } - ObjectMapper mapper = new ObjectMapper(); - Map abiJson = mapper.readValue(abiInputStream, Map.class); - this.abi = (List>) abiJson.get("abi"); + public AbiBase(ContractAbiType type, String path) throws IOException { + String abiFilePath = contractAbiPath(type, path); + Map decodedAbi = loadAbiJson(abiFilePath); + this.abi = (List>) decodedAbi.get("abi"); + } + + public static Map methodIdentifiers(ContractAbiType type) throws IOException { + return methodIdentifiers(type, null); + } + + public static Map methodIdentifiers(ContractAbiType type, String path) + throws IOException { + String abiFilePath = contractAbiPath(type, path); + Map decodedAbi = loadAbiJson(abiFilePath); + + Object methodIdentifiers = decodedAbi.get("methodIdentifiers"); + if (!(methodIdentifiers instanceof Map)) { + throw new RuntimeException( + "ABI JSON does not contain methodIdentifiers: " + abiFilePath); + } + + return (Map) methodIdentifiers; } protected static String[] getArrayComponents(String type) { @@ -77,4 +100,48 @@ protected String concatHex(List hexes) { } return result.toString(); } + + protected static String contractAbiPath(ContractAbiType type, String path) { + switch (type) { + case CONSENSUS: + return "Abi.Consensus.json"; + case MULTIPAYMENT: + return "Abi.Multipayment.json"; + case USERNAMES: + return "Abi.Usernames.json"; + case ERC20BATCH_TRANSFER: + return "Abi.ERC20BatchTransfer.json"; + case TOKEN: + return "Abi.Token.json"; + case CUSTOM: + if (path == null || path.isEmpty()) { + throw new IllegalArgumentException( + "A non-empty path must be provided when using ContractAbiType.CUSTOM."); + } + return path; + default: + throw new IllegalArgumentException("Unhandled ContractAbiType: " + type.name()); + } + } + + private static Map loadAbiJson(String path) throws IOException { + InputStream stream = AbiBase.class.getClassLoader().getResourceAsStream(path); + if (stream == null) { + if (Files.exists(Paths.get(path))) { + stream = Files.newInputStream(Paths.get(path)); + } else { + throw new RuntimeException("Unable to load ABI JSON: " + path); + } + } + + ObjectMapper mapper = new ObjectMapper(); + Map decoded = mapper.readValue(stream, Map.class); + + Object abi = decoded.get("abi"); + if (!(abi instanceof List)) { + throw new RuntimeException("ABI JSON does not contain a valid abi array: " + path); + } + + return decoded; + } } diff --git a/src/main/java/org/arkecosystem/crypto/utils/AbiDecoder.java b/src/main/java/org/arkecosystem/crypto/utils/AbiDecoder.java index 14363ba..b06ae00 100644 --- a/src/main/java/org/arkecosystem/crypto/utils/AbiDecoder.java +++ b/src/main/java/org/arkecosystem/crypto/utils/AbiDecoder.java @@ -3,6 +3,7 @@ import java.io.IOException; import java.math.BigInteger; import java.util.*; +import org.arkecosystem.crypto.enums.ContractAbiType; import org.web3j.utils.Numeric; public class AbiDecoder extends AbiBase { @@ -11,6 +12,14 @@ public AbiDecoder() throws IOException { super(); } + public AbiDecoder(ContractAbiType type) throws IOException { + super(type); + } + + public AbiDecoder(ContractAbiType type, String path) throws IOException { + super(type, path); + } + public Map decodeFunctionData(String data) throws Exception { data = stripHexPrefix(data); diff --git a/src/main/java/org/arkecosystem/crypto/utils/AbiEncoder.java b/src/main/java/org/arkecosystem/crypto/utils/AbiEncoder.java index fa4e79a..0ac9c95 100644 --- a/src/main/java/org/arkecosystem/crypto/utils/AbiEncoder.java +++ b/src/main/java/org/arkecosystem/crypto/utils/AbiEncoder.java @@ -3,6 +3,7 @@ import java.io.IOException; import java.math.BigInteger; import java.util.*; +import org.arkecosystem.crypto.enums.ContractAbiType; import org.web3j.utils.Numeric; public class AbiEncoder extends AbiBase { @@ -11,8 +12,12 @@ public AbiEncoder() throws IOException { super(); } - public AbiEncoder(String abiFilePath) throws IOException { - super(abiFilePath); + public AbiEncoder(ContractAbiType type) throws IOException { + super(type); + } + + public AbiEncoder(ContractAbiType type, String path) throws IOException { + super(type, path); } public String encodeFunctionCall(String functionName) throws Exception { diff --git a/src/main/java/org/arkecosystem/crypto/utils/TransactionTypeIdentifier.java b/src/main/java/org/arkecosystem/crypto/utils/TransactionTypeIdentifier.java new file mode 100644 index 0000000..a3ea069 --- /dev/null +++ b/src/main/java/org/arkecosystem/crypto/utils/TransactionTypeIdentifier.java @@ -0,0 +1,100 @@ +package org.arkecosystem.crypto.utils; + +import java.util.HashMap; +import java.util.Map; +import org.arkecosystem.crypto.enums.ContractAbiType; +import org.web3j.utils.Numeric; + +public class TransactionTypeIdentifier { + + private static final String TRANSFER_SIGNATURE = ""; + + private static Map signatures; + + public static boolean isTransfer(String data) { + return TRANSFER_SIGNATURE.equals(data); + } + + public static boolean isVote(String data) { + return startsWithSignature(data, signatures().get("vote")); + } + + public static boolean isUnvote(String data) { + return startsWithSignature(data, signatures().get("unvote")); + } + + public static boolean isMultiPayment(String data) { + return startsWithSignature(data, signatures().get("multiPayment")); + } + + public static boolean isUsernameRegistration(String data) { + return startsWithSignature(data, signatures().get("registerUsername")); + } + + public static boolean isUsernameResignation(String data) { + return startsWithSignature(data, signatures().get("resignUsername")); + } + + public static boolean isValidatorRegistration(String data) { + return startsWithSignature(data, signatures().get("registerValidator")); + } + + public static boolean isValidatorResignation(String data) { + return startsWithSignature(data, signatures().get("resignValidator")); + } + + public static boolean isUpdateValidator(String data) { + return startsWithSignature(data, signatures().get("updateValidator")); + } + + public static boolean isTokenTransfer(String data) { + Map decodedData = decodeTokenFunction(data); + return decodedData != null && "transfer".equals(decodedData.get("functionName")); + } + + private static boolean startsWithSignature(String data, String signature) { + if (data == null || signature == null) { + return false; + } + return Numeric.cleanHexPrefix(data).toLowerCase().startsWith(signature.toLowerCase()); + } + + private static synchronized Map signatures() { + if (signatures != null) { + return signatures; + } + + try { + Map consensusMethods = + AbiBase.methodIdentifiers(ContractAbiType.CONSENSUS); + Map multipaymentMethods = + AbiBase.methodIdentifiers(ContractAbiType.MULTIPAYMENT); + Map usernamesMethods = + AbiBase.methodIdentifiers(ContractAbiType.USERNAMES); + + Map map = new HashMap<>(); + map.put("multiPayment", multipaymentMethods.get("pay(address[],uint256[])")); + map.put("registerUsername", usernamesMethods.get("registerUsername(string)")); + map.put("resignUsername", usernamesMethods.get("resignUsername()")); + map.put("registerValidator", consensusMethods.get("registerValidator(bytes)")); + map.put("resignValidator", consensusMethods.get("resignValidator()")); + map.put("vote", consensusMethods.get("vote(address)")); + map.put("unvote", consensusMethods.get("unvote()")); + map.put("updateValidator", consensusMethods.get("updateValidator(bytes)")); + map.put("transfer", "transfer"); + + signatures = map; + return signatures; + } catch (Exception e) { + throw new RuntimeException("Unable to load method identifiers", e); + } + } + + private static Map decodeTokenFunction(String data) { + try { + return new AbiDecoder(ContractAbiType.TOKEN).decodeFunctionData(data); + } catch (Exception e) { + return null; + } + } +} diff --git a/src/test/java/org/arkecosystem/crypto/utils/TransactionTypeIdentifierTest.java b/src/test/java/org/arkecosystem/crypto/utils/TransactionTypeIdentifierTest.java new file mode 100644 index 0000000..276d2f7 --- /dev/null +++ b/src/test/java/org/arkecosystem/crypto/utils/TransactionTypeIdentifierTest.java @@ -0,0 +1,111 @@ +package org.arkecosystem.crypto.utils; + +import static org.junit.jupiter.api.Assertions.assertFalse; +import static org.junit.jupiter.api.Assertions.assertTrue; + +import java.util.Map; +import org.arkecosystem.crypto.enums.ContractAbiType; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; + +class TransactionTypeIdentifierTest { + + private Map consensusMethods; + private Map multipaymentMethods; + private Map usernamesMethods; + + @BeforeEach + void setUp() throws Exception { + consensusMethods = AbiBase.methodIdentifiers(ContractAbiType.CONSENSUS); + multipaymentMethods = AbiBase.methodIdentifiers(ContractAbiType.MULTIPAYMENT); + usernamesMethods = AbiBase.methodIdentifiers(ContractAbiType.USERNAMES); + } + + @Test + void identifies_transfer_by_empty_payload() { + assertTrue(TransactionTypeIdentifier.isTransfer("")); + assertFalse(TransactionTypeIdentifier.isTransfer("0x")); + assertFalse(TransactionTypeIdentifier.isTransfer("12345678")); + } + + @Test + void identifies_vote_signature() { + String signature = consensusMethods.get("vote(address)"); + + assertTrue(TransactionTypeIdentifier.isVote(signature)); + assertTrue(TransactionTypeIdentifier.isVote("0x" + signature)); + assertFalse(TransactionTypeIdentifier.isVote("1234567")); + } + + @Test + void identifies_unvote_signature() { + String signature = consensusMethods.get("unvote()"); + + assertTrue(TransactionTypeIdentifier.isUnvote(signature)); + assertTrue(TransactionTypeIdentifier.isUnvote("0x" + signature)); + assertFalse(TransactionTypeIdentifier.isUnvote("1234567")); + } + + @Test + void identifies_multipayment_signature() { + String signature = multipaymentMethods.get("pay(address[],uint256[])"); + + assertTrue(TransactionTypeIdentifier.isMultiPayment(signature)); + assertTrue(TransactionTypeIdentifier.isMultiPayment("0x" + signature)); + assertFalse(TransactionTypeIdentifier.isMultiPayment("1234567")); + } + + @Test + void identifies_username_registration_signature() { + String signature = usernamesMethods.get("registerUsername(string)"); + + assertTrue(TransactionTypeIdentifier.isUsernameRegistration(signature)); + assertTrue(TransactionTypeIdentifier.isUsernameRegistration("0x" + signature)); + assertFalse(TransactionTypeIdentifier.isUsernameRegistration("1234567")); + } + + @Test + void identifies_username_resignation_signature() { + String signature = usernamesMethods.get("resignUsername()"); + + assertTrue(TransactionTypeIdentifier.isUsernameResignation(signature)); + assertTrue(TransactionTypeIdentifier.isUsernameResignation("0x" + signature)); + assertFalse(TransactionTypeIdentifier.isUsernameResignation("1234567")); + } + + @Test + void identifies_validator_registration_signature() { + String signature = consensusMethods.get("registerValidator(bytes)"); + + assertTrue(TransactionTypeIdentifier.isValidatorRegistration(signature)); + assertTrue(TransactionTypeIdentifier.isValidatorRegistration("0x" + signature)); + assertFalse(TransactionTypeIdentifier.isValidatorRegistration("1234567")); + } + + @Test + void identifies_validator_resignation_signature() { + String signature = consensusMethods.get("resignValidator()"); + + assertTrue(TransactionTypeIdentifier.isValidatorResignation(signature)); + assertTrue(TransactionTypeIdentifier.isValidatorResignation("0x" + signature)); + assertFalse(TransactionTypeIdentifier.isValidatorResignation("1234567")); + } + + @Test + void identifies_update_validator_signature() { + String signature = consensusMethods.get("updateValidator(bytes)"); + + assertTrue(TransactionTypeIdentifier.isUpdateValidator(signature)); + assertTrue(TransactionTypeIdentifier.isUpdateValidator("0x" + signature)); + assertFalse(TransactionTypeIdentifier.isUpdateValidator("1234567")); + } + + @Test + void identifies_token_transfer_payloads() { + assertTrue( + TransactionTypeIdentifier.isTokenTransfer( + "0xa9059cbb000000000000000000000000a5cc0bfeb09742c5e4c610f2ebaab82eb142ca10000000000000000000000000000000000000009bd2ffdd71438a49e803314000")); + assertFalse(TransactionTypeIdentifier.isTokenTransfer("0x" + "0".repeat(64))); + assertFalse(TransactionTypeIdentifier.isTokenTransfer("1234567")); + } +} From 7efd55ec88d1346c176326d0bf991ae09881ff87 Mon Sep 17 00:00:00 2001 From: Alfonso Bribiesca Date: Tue, 28 Apr 2026 13:30:25 -0600 Subject: [PATCH 2/2] test: cover identifier and abi base validations --- .../crypto/utils/AbiBaseTest.java | 101 ++++++++++++ .../crypto/utils/AbiJsonFilesTest.java | 10 +- .../utils/TransactionTypeIdentifierTest.java | 156 +++++++++++------- 3 files changed, 201 insertions(+), 66 deletions(-) create mode 100644 src/test/java/org/arkecosystem/crypto/utils/AbiBaseTest.java diff --git a/src/test/java/org/arkecosystem/crypto/utils/AbiBaseTest.java b/src/test/java/org/arkecosystem/crypto/utils/AbiBaseTest.java new file mode 100644 index 0000000..74d6e54 --- /dev/null +++ b/src/test/java/org/arkecosystem/crypto/utils/AbiBaseTest.java @@ -0,0 +1,101 @@ +package org.arkecosystem.crypto.utils; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertNotNull; +import static org.junit.jupiter.api.Assertions.assertThrows; + +import java.io.IOException; +import java.nio.file.Files; +import java.nio.file.Path; +import java.util.Map; +import org.arkecosystem.crypto.enums.ContractAbiType; +import org.junit.jupiter.api.Test; + +class AbiBaseTest { + + @Test + void method_identifiers_loads_consensus_signatures() throws Exception { + Map identifiers = AbiBase.methodIdentifiers(ContractAbiType.CONSENSUS); + + assertEquals("6dd7d8ea", identifiers.get("vote(address)")); + assertEquals("3174b689", identifiers.get("unvote()")); + assertEquals("602a9eee", identifiers.get("registerValidator(bytes)")); + } + + @Test + void method_identifiers_loads_multipayment_signatures() throws Exception { + Map identifiers = AbiBase.methodIdentifiers(ContractAbiType.MULTIPAYMENT); + + assertEquals("084ce708", identifiers.get("pay(address[],uint256[])")); + } + + @Test + void method_identifiers_loads_usernames_signatures() throws Exception { + Map identifiers = AbiBase.methodIdentifiers(ContractAbiType.USERNAMES); + + assertEquals("36a94134", identifiers.get("registerUsername(string)")); + assertEquals("ebed6dab", identifiers.get("resignUsername()")); + } + + @Test + void custom_type_requires_a_non_empty_path() { + assertThrows(IllegalArgumentException.class, () -> new AbiEncoder(ContractAbiType.CUSTOM)); + assertThrows( + IllegalArgumentException.class, () -> new AbiEncoder(ContractAbiType.CUSTOM, "")); + assertThrows( + IllegalArgumentException.class, + () -> AbiBase.methodIdentifiers(ContractAbiType.CUSTOM, null)); + } + + @Test + void custom_type_loads_external_abi_path(@org.junit.jupiter.api.io.TempDir Path tempDir) + throws Exception { + Path file = tempDir.resolve("custom.json"); + Files.writeString( + file, + "{\n" + + " \"abi\": [{\"type\":\"function\",\"name\":\"vote\",\"inputs\":[{\"type\":\"address\",\"name\":\"validator\"}]}],\n" + + " \"methodIdentifiers\": {\"vote(address)\":\"6dd7d8ea\"}\n" + + "}"); + + AbiEncoder encoder = new AbiEncoder(ContractAbiType.CUSTOM, file.toString()); + assertNotNull(encoder); + + Map identifiers = + AbiBase.methodIdentifiers(ContractAbiType.CUSTOM, file.toString()); + assertEquals("6dd7d8ea", identifiers.get("vote(address)")); + } + + @Test + void load_throws_when_file_is_missing() { + assertThrows( + RuntimeException.class, + () -> new AbiEncoder(ContractAbiType.CUSTOM, "does-not-exist.json")); + } + + @Test + void load_throws_when_abi_array_is_missing(@org.junit.jupiter.api.io.TempDir Path tempDir) + throws Exception { + Path file = tempDir.resolve("invalid.json"); + Files.writeString(file, "{\"contractName\":\"X\"}"); + + RuntimeException error = + assertThrows( + RuntimeException.class, + () -> new AbiEncoder(ContractAbiType.CUSTOM, file.toString())); + assertEquals("ABI JSON does not contain a valid abi array: " + file, error.getMessage()); + } + + @Test + void method_identifiers_throws_when_field_is_missing( + @org.junit.jupiter.api.io.TempDir Path tempDir) throws IOException { + Path file = tempDir.resolve("no-mi.json"); + Files.writeString(file, "{\"abi\":[]}"); + + RuntimeException error = + assertThrows( + RuntimeException.class, + () -> AbiBase.methodIdentifiers(ContractAbiType.CUSTOM, file.toString())); + assertEquals("ABI JSON does not contain methodIdentifiers: " + file, error.getMessage()); + } +} diff --git a/src/test/java/org/arkecosystem/crypto/utils/AbiJsonFilesTest.java b/src/test/java/org/arkecosystem/crypto/utils/AbiJsonFilesTest.java index 152cad5..6d7737a 100644 --- a/src/test/java/org/arkecosystem/crypto/utils/AbiJsonFilesTest.java +++ b/src/test/java/org/arkecosystem/crypto/utils/AbiJsonFilesTest.java @@ -64,9 +64,13 @@ void method_identifiers_match_keccak256_of_signature() throws Exception { } @Test - void abi_encoder_loads_every_abi_file() throws Exception { - for (String file : ABI_FILES) { - new AbiEncoder(file); + void abi_encoder_loads_every_contract_abi_type() throws Exception { + for (org.arkecosystem.crypto.enums.ContractAbiType type : + org.arkecosystem.crypto.enums.ContractAbiType.values()) { + if (type == org.arkecosystem.crypto.enums.ContractAbiType.CUSTOM) { + continue; + } + new AbiEncoder(type); } } diff --git a/src/test/java/org/arkecosystem/crypto/utils/TransactionTypeIdentifierTest.java b/src/test/java/org/arkecosystem/crypto/utils/TransactionTypeIdentifierTest.java index 276d2f7..fc2f663 100644 --- a/src/test/java/org/arkecosystem/crypto/utils/TransactionTypeIdentifierTest.java +++ b/src/test/java/org/arkecosystem/crypto/utils/TransactionTypeIdentifierTest.java @@ -1,111 +1,141 @@ package org.arkecosystem.crypto.utils; +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 java.math.BigInteger; +import java.util.Arrays; +import java.util.List; import java.util.Map; import org.arkecosystem.crypto.enums.ContractAbiType; -import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Test; class TransactionTypeIdentifierTest { - private Map consensusMethods; - private Map multipaymentMethods; - private Map usernamesMethods; - - @BeforeEach - void setUp() throws Exception { - consensusMethods = AbiBase.methodIdentifiers(ContractAbiType.CONSENSUS); - multipaymentMethods = AbiBase.methodIdentifiers(ContractAbiType.MULTIPAYMENT); - usernamesMethods = AbiBase.methodIdentifiers(ContractAbiType.USERNAMES); - } + private static final String VOTE = "6dd7d8ea"; + private static final String UNVOTE = "3174b689"; + private static final String REGISTER_VALIDATOR = "602a9eee"; + private static final String RESIGN_VALIDATOR = "b85f5da2"; + private static final String UPDATE_VALIDATOR = "5a8eed73"; + private static final String PAY = "084ce708"; + private static final String REGISTER_USERNAME = "36a94134"; + private static final String RESIGN_USERNAME = "ebed6dab"; + private static final String ERC20_TRANSFER = "a9059cbb"; @Test - void identifies_transfer_by_empty_payload() { + void identifies_transfer_only_for_empty_payload() { assertTrue(TransactionTypeIdentifier.isTransfer("")); assertFalse(TransactionTypeIdentifier.isTransfer("0x")); - assertFalse(TransactionTypeIdentifier.isTransfer("12345678")); + assertFalse(TransactionTypeIdentifier.isTransfer(VOTE)); } @Test - void identifies_vote_signature() { - String signature = consensusMethods.get("vote(address)"); - - assertTrue(TransactionTypeIdentifier.isVote(signature)); - assertTrue(TransactionTypeIdentifier.isVote("0x" + signature)); + void identifies_vote_via_canonical_selector() { + assertTrue(TransactionTypeIdentifier.isVote(VOTE)); + assertTrue(TransactionTypeIdentifier.isVote("0x" + VOTE)); + assertTrue( + TransactionTypeIdentifier.isVote( + "0x6dd7d8ea000000000000000000000000512f366d524157bcf734546eb29a6d687b762255")); + assertFalse(TransactionTypeIdentifier.isVote(UNVOTE)); + assertFalse(TransactionTypeIdentifier.isVote("")); assertFalse(TransactionTypeIdentifier.isVote("1234567")); } @Test - void identifies_unvote_signature() { - String signature = consensusMethods.get("unvote()"); - - assertTrue(TransactionTypeIdentifier.isUnvote(signature)); - assertTrue(TransactionTypeIdentifier.isUnvote("0x" + signature)); - assertFalse(TransactionTypeIdentifier.isUnvote("1234567")); + void identifies_unvote_via_canonical_selector() { + assertTrue(TransactionTypeIdentifier.isUnvote(UNVOTE)); + assertTrue(TransactionTypeIdentifier.isUnvote("0x" + UNVOTE)); + assertFalse(TransactionTypeIdentifier.isUnvote(VOTE)); } @Test - void identifies_multipayment_signature() { - String signature = multipaymentMethods.get("pay(address[],uint256[])"); - - assertTrue(TransactionTypeIdentifier.isMultiPayment(signature)); - assertTrue(TransactionTypeIdentifier.isMultiPayment("0x" + signature)); - assertFalse(TransactionTypeIdentifier.isMultiPayment("1234567")); + void identifies_multipayment_via_canonical_selector() { + assertTrue(TransactionTypeIdentifier.isMultiPayment(PAY)); + assertTrue(TransactionTypeIdentifier.isMultiPayment("0x" + PAY)); + assertFalse(TransactionTypeIdentifier.isMultiPayment(VOTE)); } @Test - void identifies_username_registration_signature() { - String signature = usernamesMethods.get("registerUsername(string)"); - - assertTrue(TransactionTypeIdentifier.isUsernameRegistration(signature)); - assertTrue(TransactionTypeIdentifier.isUsernameRegistration("0x" + signature)); - assertFalse(TransactionTypeIdentifier.isUsernameRegistration("1234567")); + void identifies_username_registration_via_canonical_selector() { + assertTrue(TransactionTypeIdentifier.isUsernameRegistration(REGISTER_USERNAME)); + assertTrue(TransactionTypeIdentifier.isUsernameRegistration("0x" + REGISTER_USERNAME)); + assertFalse(TransactionTypeIdentifier.isUsernameRegistration(RESIGN_USERNAME)); } @Test - void identifies_username_resignation_signature() { - String signature = usernamesMethods.get("resignUsername()"); - - assertTrue(TransactionTypeIdentifier.isUsernameResignation(signature)); - assertTrue(TransactionTypeIdentifier.isUsernameResignation("0x" + signature)); - assertFalse(TransactionTypeIdentifier.isUsernameResignation("1234567")); + void identifies_username_resignation_via_canonical_selector() { + assertTrue(TransactionTypeIdentifier.isUsernameResignation(RESIGN_USERNAME)); + assertTrue(TransactionTypeIdentifier.isUsernameResignation("0x" + RESIGN_USERNAME)); + assertFalse(TransactionTypeIdentifier.isUsernameResignation(REGISTER_USERNAME)); } @Test - void identifies_validator_registration_signature() { - String signature = consensusMethods.get("registerValidator(bytes)"); - - assertTrue(TransactionTypeIdentifier.isValidatorRegistration(signature)); - assertTrue(TransactionTypeIdentifier.isValidatorRegistration("0x" + signature)); - assertFalse(TransactionTypeIdentifier.isValidatorRegistration("1234567")); + void identifies_validator_registration_via_canonical_selector() { + assertTrue(TransactionTypeIdentifier.isValidatorRegistration(REGISTER_VALIDATOR)); + assertTrue(TransactionTypeIdentifier.isValidatorRegistration("0x" + REGISTER_VALIDATOR)); + assertFalse(TransactionTypeIdentifier.isValidatorRegistration(RESIGN_VALIDATOR)); } @Test - void identifies_validator_resignation_signature() { - String signature = consensusMethods.get("resignValidator()"); - - assertTrue(TransactionTypeIdentifier.isValidatorResignation(signature)); - assertTrue(TransactionTypeIdentifier.isValidatorResignation("0x" + signature)); - assertFalse(TransactionTypeIdentifier.isValidatorResignation("1234567")); + void identifies_validator_resignation_via_canonical_selector() { + assertTrue(TransactionTypeIdentifier.isValidatorResignation(RESIGN_VALIDATOR)); + assertTrue(TransactionTypeIdentifier.isValidatorResignation("0x" + RESIGN_VALIDATOR)); + assertFalse(TransactionTypeIdentifier.isValidatorResignation(REGISTER_VALIDATOR)); } @Test - void identifies_update_validator_signature() { - String signature = consensusMethods.get("updateValidator(bytes)"); + void identifies_update_validator_via_canonical_selector() { + assertTrue(TransactionTypeIdentifier.isUpdateValidator(UPDATE_VALIDATOR)); + assertTrue(TransactionTypeIdentifier.isUpdateValidator("0x" + UPDATE_VALIDATOR)); + assertFalse(TransactionTypeIdentifier.isUpdateValidator(REGISTER_VALIDATOR)); + } - assertTrue(TransactionTypeIdentifier.isUpdateValidator(signature)); - assertTrue(TransactionTypeIdentifier.isUpdateValidator("0x" + signature)); - assertFalse(TransactionTypeIdentifier.isUpdateValidator("1234567")); + @Test + @SuppressWarnings("unchecked") + void identifies_token_transfer_and_decodes_canonical_arguments() throws Exception { + String address = "0xA5cc0BfeB09742C5e4C610F2EBaab82Eb142Ca10"; + BigInteger amount = new BigInteger("47000000000000000000000000000000"); + + String payload = + new AbiEncoder(ContractAbiType.TOKEN) + .encodeFunctionCall("transfer", Arrays.asList(address, amount)); + + assertTrue(payload.toLowerCase().startsWith("0x" + ERC20_TRANSFER)); + assertTrue(TransactionTypeIdentifier.isTokenTransfer(payload)); + + Map decoded = + new AbiDecoder(ContractAbiType.TOKEN).decodeFunctionData(payload); + List args = (List) decoded.get("args"); + assertEquals("transfer", decoded.get("functionName")); + assertEquals(address.toLowerCase(), ((String) args.get(0)).toLowerCase()); + assertEquals(amount.toString(), args.get(1)); } @Test - void identifies_token_transfer_payloads() { - assertTrue( - TransactionTypeIdentifier.isTokenTransfer( - "0xa9059cbb000000000000000000000000a5cc0bfeb09742c5e4c610f2ebaab82eb142ca10000000000000000000000000000000000000009bd2ffdd71438a49e803314000")); + void rejects_token_transfer_for_non_transfer_payloads() { assertFalse(TransactionTypeIdentifier.isTokenTransfer("0x" + "0".repeat(64))); assertFalse(TransactionTypeIdentifier.isTokenTransfer("1234567")); + assertFalse( + TransactionTypeIdentifier.isTokenTransfer( + "0x6dd7d8ea000000000000000000000000512f366d524157bcf734546eb29a6d687b762255")); + } + + @Test + void selectors_are_mutually_exclusive() { + assertFalse(TransactionTypeIdentifier.isVote(UNVOTE)); + assertFalse(TransactionTypeIdentifier.isUnvote(VOTE)); + assertFalse(TransactionTypeIdentifier.isMultiPayment(VOTE)); + assertFalse(TransactionTypeIdentifier.isValidatorRegistration(VOTE)); + assertFalse(TransactionTypeIdentifier.isValidatorResignation(VOTE)); + assertFalse(TransactionTypeIdentifier.isUpdateValidator(VOTE)); + assertFalse(TransactionTypeIdentifier.isUsernameRegistration(VOTE)); + assertFalse(TransactionTypeIdentifier.isUsernameResignation(VOTE)); + } + + @Test + void matching_is_case_insensitive() { + assertTrue(TransactionTypeIdentifier.isVote(VOTE.toUpperCase())); + assertTrue(TransactionTypeIdentifier.isVote("0x" + VOTE.toUpperCase())); } }