Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add decodeArgs() method for Event #33

Merged
merged 5 commits into from
Feb 14, 2022
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
77 changes: 75 additions & 2 deletions src/main/java/com/esaulpaugh/headlong/abi/Event.java
Original file line number Diff line number Diff line change
Expand Up @@ -16,18 +16,27 @@
package com.esaulpaugh.headlong.abi;

import com.esaulpaugh.headlong.abi.util.JsonUtils;
import com.esaulpaugh.headlong.util.FastHex;
import com.esaulpaugh.headlong.util.Strings;
import com.google.gson.JsonObject;
import com.joemelsha.crypto.hash.Keccak;

import java.util.Arrays;
import java.util.Objects;

/** Represents an event in Ethereum. */
public final class Event implements ABIObject {

private static final ArrayType<ByteType, byte[]> BYTES_32 = TypeFactory.create("bytes32");

private final String name;
private final boolean anonymous;
private final TupleType inputs;
private final TupleType indexedParams;
private final TupleType nonIndexedParams;
private final boolean[] indexManifest;
private final byte[] signatureHash;


public static Event create(String name, TupleType inputs, boolean... indexed) {
return new Event(name, false, inputs, indexed);
Expand All @@ -44,7 +53,10 @@ public Event(String name, boolean anonymous, TupleType inputs, boolean... indexe
throw new IllegalArgumentException("indexed.length doesn't match number of inputs");
}
this.indexManifest = Arrays.copyOf(indexed, indexed.length);
this.indexedParams = inputs.select(indexManifest);
this.nonIndexedParams = inputs.exclude(indexManifest);
this.anonymous = anonymous;
this.signatureHash = new Keccak(256).digest(Strings.decode(getCanonicalSignature(), Strings.ASCII));
}

@Override
Expand Down Expand Up @@ -76,11 +88,11 @@ public String getCanonicalSignature() {
}

public TupleType getIndexedParams() {
return inputs.select(indexManifest);
return indexedParams;
}

public TupleType getNonIndexedParams() {
return inputs.exclude(indexManifest);
return nonIndexedParams;
}

@Override
Expand Down Expand Up @@ -116,4 +128,65 @@ public String toString() {
public boolean isEvent() {
return true;
}

/**
* Decodes Event arguments
* @param topics indexed parameters to decode. If the event is anonymous, the first element is a Keccak hash of the
* canonical signature of the event (see https://docs.soliditylang.org/en/v0.8.11/abi-spec.html#events)
* @param data non-indexed parameters to decode
* @return
*/
public Tuple decodeArgs(byte[][] topics, byte[] data) {
Objects.requireNonNull(topics, "topics must not be null");
Objects.requireNonNull(data, "data must not be null");

if (!isAnonymous() && topics.length >= 1) {
checkSignatureHash(topics);
}

TupleType indexedParams = getIndexedParams();
Object[] decodedTopics = decodeTopics(topics, indexedParams);
TupleType nonIndexedParams = getNonIndexedParams();
Tuple decodedData = nonIndexedParams.decode(data);
Object[] mergedArgs = mergeDecodedArgs(decodedTopics, decodedData);
return Tuple.of(mergedArgs);
}

private Object[] decodeTopics(byte[][] topics, TupleType indexedParams) {
int offsetIsAnonymous = isAnonymous() ? 0 : 1;
Object[] decodedTopics = new Object[indexedParams.size()];
for (int i = 0; i < indexedParams.size(); i++) {
ABIType<?> abiType = indexedParams.get(i);
byte[] topic = topics[i + offsetIsAnonymous];
if (abiType.isDynamic()) {
// Dynamic indexed types are not decodable in Events. Only a special hash is stored for fast querying of records
// See https://docs.soliditylang.org/en/v0.8.11/abi-spec.html#indexed-event-encoding
decodedTopics[i] = BYTES_32.decode(topic);
} else {
decodedTopics[i] = abiType.decode(topic);
}
}
return decodedTopics;
}

private Object[] mergeDecodedArgs(Object[] decodedTopics, Tuple decodedData) {
Object[] result = new Object[inputs.size()];
for (int i = 0, topicIndex = 0, dataIndex = 0; i < indexManifest.length; i++) {
if (indexManifest[i]) {
result[i] = decodedTopics[topicIndex++];
} else {
result[i] = decodedData.get(dataIndex++);
}
}
return result;
}

private void checkSignatureHash(byte[][] topics) {
byte[] decodedSignatureHash = BYTES_32.decode(topics[0]);
if (!Arrays.equals(decodedSignatureHash, signatureHash)) {
String message = String.format("Decoded Event signature hash %s does not match the one from ABI %s",
FastHex.encodeToString(decodedSignatureHash), FastHex.encodeToString(signatureHash));
throw new IllegalArgumentException(message);
}
}
}
256 changes: 251 additions & 5 deletions src/test/java/com/esaulpaugh/headlong/abi/DecodeTest.java
Original file line number Diff line number Diff line change
Expand Up @@ -19,21 +19,20 @@
import com.esaulpaugh.headlong.util.FastHex;
import com.esaulpaugh.headlong.util.Integers;
import com.esaulpaugh.headlong.util.Strings;
import com.sun.tools.javac.util.List;
import org.junit.jupiter.api.Test;

import java.math.BigDecimal;
import java.math.BigInteger;
import java.nio.BufferUnderflowException;
import java.nio.ByteBuffer;
import java.util.Arrays;
import java.util.Collections;
import java.util.NoSuchElementException;

import static com.esaulpaugh.headlong.TestUtils.assertThrown;
import static org.junit.jupiter.api.Assertions.assertArrayEquals;
import static org.junit.jupiter.api.Assertions.assertEquals;
import static org.junit.jupiter.api.Assertions.assertFalse;
import static org.junit.jupiter.api.Assertions.assertNotEquals;
import static org.junit.jupiter.api.Assertions.assertTrue;
import static com.esaulpaugh.headlong.TestUtils.assertThrownMessageMatch;
import static org.junit.jupiter.api.Assertions.*;

public class DecodeTest {

Expand Down Expand Up @@ -468,4 +467,251 @@ public void testSingletonReturn() throws Throwable {
boolean b = bar.decodeSingletonReturn(Strings.decode("0000000000000000000000000000000000000000000000000000000000000001"));
assertTrue(b);
}

@Test
public void testDecodeEvent() {
Event event = Event.fromJson("{\n" +
" \"anonymous\": false,\n" +
" \"inputs\": [\n" +
" {\n" +
" \"indexed\": false,\n" +
" \"name\": \"buyHash\",\n" +
" \"type\": \"bytes32\"\n" +
" },\n" +
" {\n" +
" \"indexed\": false,\n" +
" \"name\": \"sellHash\",\n" +
" \"type\": \"bytes32\"\n" +
" },\n" +
" {\n" +
" \"indexed\": true,\n" +
" \"name\": \"maker\",\n" +
" \"type\": \"address\"\n" +
" },\n" +
" {\n" +
" \"indexed\": true,\n" +
" \"name\": \"taker\",\n" +
" \"type\": \"address\"\n" +
" },\n" +
" {\n" +
" \"indexed\": false,\n" +
" \"name\": \"price\",\n" +
" \"type\": \"uint256\"\n" +
" },\n" +
" {\n" +
" \"indexed\": true,\n" +
" \"name\": \"metadata\",\n" +
" \"type\": \"bytes32\"\n" +
" }\n" +
" ],\n" +
" \"name\": \"OrdersMatched\",\n" +
" \"type\": \"event\"\n" +
" }");
byte[][] topics = {
FastHex.decode("c4109843e0b7d514e4c093114b863f8e7d8d9a458c372cd51bfe526b588006c9"),
FastHex.decode("000000000000000000000000bbb677a94eda9660832e9944353dd6e814a45705"),
FastHex.decode("000000000000000000000000bcead8896acb7a045c38287e433d896eefb40f6c"),
FastHex.decode("0000000000000000000000000000000000000000000000000000000000000000")
};
byte[] data = FastHex.decode("00000000000000000000000000000000000000000000000000000000000000009b5de4f892fe73b139777ff15eb165f359a0ea9ea1c687f8e8dc5748249ca5f200000000000000000000000000000000000000000000000002386f26fc100000");
Tuple result = event.decodeArgs(topics, data);
assertEquals("0000000000000000000000000000000000000000000000000000000000000000",
Strings.encode((byte[]) result.get(0)));
assertEquals("9b5de4f892fe73b139777ff15eb165f359a0ea9ea1c687f8e8dc5748249ca5f2",
Strings.encode((byte[]) result.get(1)));
assertEquals("0xbbb677a94eda9660832e9944353dd6e814a45705", result.get(2).toString().toLowerCase());
assertEquals("0xbcead8896acb7a045c38287e433d896eefb40f6c", result.get(3).toString().toLowerCase());
assertEquals(new BigInteger("160000000000000000"), result.get(4));
assertEquals("0000000000000000000000000000000000000000000000000000000000000000",
Strings.encode((byte[]) result.get(5)));
}

@Test
public void testDecodeEventWithWrongSignatureHashShouldFail() throws Throwable {
Event event = Event.fromJson("{\n" +
" \"anonymous\": false,\n" +
" \"inputs\": [\n" +
" {\n" +
" \"indexed\": false,\n" +
" \"name\": \"buyHash\",\n" +
" \"type\": \"bytes32\"\n" +
" },\n" +
" {\n" +
" \"indexed\": false,\n" +
" \"name\": \"sellHash\",\n" +
" \"type\": \"bytes32\"\n" +
" },\n" +
" {\n" +
" \"indexed\": true,\n" +
" \"name\": \"maker\",\n" +
" \"type\": \"address\"\n" +
" },\n" +
" {\n" +
" \"indexed\": true,\n" +
" \"name\": \"taker\",\n" +
" \"type\": \"address\"\n" +
" },\n" +
" {\n" +
" \"indexed\": false,\n" +
" \"name\": \"price\",\n" +
" \"type\": \"uint256\"\n" +
" },\n" +
" {\n" +
" \"indexed\": true,\n" +
" \"name\": \"metadata\",\n" +
" \"type\": \"bytes32\"\n" +
" }\n" +
" ],\n" +
" \"name\": \"OrdersMatched\",\n" +
" \"type\": \"event\"\n" +
" }");
byte[][] topics = {
FastHex.decode("a4109843e0b7d514e4c093114b863f8e7d8d9a458c372cd51bfe526b588006d9"),
FastHex.decode("000000000000000000000000bbb677a94eda9660832e9944353dd6e814a45705"),
FastHex.decode("000000000000000000000000bcead8896acb7a045c38287e433d896eefb40f6c"),
FastHex.decode("0000000000000000000000000000000000000000000000000000000000000000")
};
byte[] data = FastHex.decode("00000000000000000000000000000000000000000000000000000000000000009b5de4f892fe73b139777ff15eb165f359a0ea9ea1c687f8e8dc5748249ca5f200000000000000000000000000000000000000000000000002386f26fc100000");
assertThrownMessageMatch(RuntimeException.class, Collections.singletonList("Decoded Event signature hash " +
"a4109843e0b7d514e4c093114b863f8e7d8d9a458c372cd51bfe526b588006d9 does not match the one from ABI " +
"c4109843e0b7d514e4c093114b863f8e7d8d9a458c372cd51bfe526b588006c9"),
() -> event.decodeArgs(topics, data));
}

@Test
public void testDecodeAnonymousEvent() {
Event event = Event.fromJson("{\n" +
" \"anonymous\": true,\n" +
" \"inputs\": [\n" +
" {\n" +
" \"indexed\": true,\n" +
" \"name\": \"maker\",\n" +
" \"type\": \"address\"\n" +
" },\n" +
" {\n" +
" \"indexed\": true,\n" +
" \"name\": \"taker\",\n" +
" \"type\": \"address\"\n" +
" }\n" +
" ],\n" +
" \"name\": \"TestEvent\",\n" +
" \"type\": \"event\"\n" +
" }");
byte[][] topics = {
FastHex.decode("000000000000000000000000bbb677a94eda9660832e9944353dd6e814a45705"),
FastHex.decode("000000000000000000000000bcead8896acb7a045c38287e433d896eefb40f6c")
};
Tuple result = event.decodeArgs(topics, new byte[0]);
assertEquals("0xbbb677a94eda9660832e9944353dd6e814a45705", result.get(0).toString().toLowerCase());
assertEquals("0xbcead8896acb7a045c38287e433d896eefb40f6c", result.get(1).toString().toLowerCase());
}

@Test
public void testDecodeEmptyTopicsEvent() {
Event event = Event.fromJson("{\n" +
" \"anonymous\": true,\n" +
" \"inputs\": [\n" +
" {\n" +
" \"indexed\": false,\n" +
" \"name\": \"maker\",\n" +
" \"type\": \"address\"\n" +
" },\n" +
" {\n" +
" \"indexed\": false,\n" +
" \"name\": \"taker\",\n" +
" \"type\": \"address\"\n" +
" }\n" +
" ],\n" +
" \"name\": \"TestEvent\",\n" +
" \"type\": \"event\"\n" +
" }");
byte[] data = FastHex.decode("000000000000000000000000bbb677a94eda9660832e9944353dd6e814a45705000000000000000000000000bcead8896acb7a045c38287e433d896eefb40f6c");
Tuple result = event.decodeArgs(new byte[0][0], data);
assertEquals("0xbbb677a94eda9660832e9944353dd6e814a45705", result.get(0).toString().toLowerCase());
assertEquals("0xbcead8896acb7a045c38287e433d896eefb40f6c", result.get(1).toString().toLowerCase());
}

@Test
public void testDecodeIndexedDynamicType() {
Event event = Event.fromJson("{\n" +
" \"anonymous\": false,\n" +
" \"inputs\": [\n" +
" {\n" +
" \"indexed\": true,\n" +
" \"internalType\": \"uint256[]\",\n" +
" \"name\": \"nums\",\n" +
" \"type\": \"uint256[]\"\n" +
" },\n" +
" {\n" +
" \"indexed\": true,\n" +
" \"internalType\": \"uint8\",\n" +
" \"name\": \"random\",\n" +
" \"type\": \"uint8\"\n" +
" }\n" +
" ],\n" +
" \"name\": \"Stored\",\n" +
" \"type\": \"event\"\n" +
" }");
IntType int8 = TypeFactory.create("int8");
byte[][] topics = {
FastHex.decode("d78fe195906f002940f4b32985f1daa40764f8481c05447b6751db32e70d744b"),
FastHex.decode("392791df626408017a264f53fde61065d5a93a32b60171df9d8a46afdf82992d"),
int8.encode(12).array()
};
Tuple result = event.decodeArgs(topics, new byte[0]);
assertEquals("392791df626408017a264f53fde61065d5a93a32b60171df9d8a46afdf82992d", Strings.encode((byte[]) result.get(0)));
assertEquals(12, (Integer) result.get(1));
}

@Test
public void testDecodeArgsNullTopicShouldFail() {
Event event = Event.fromJson("{\n" +
" \"anonymous\": false,\n" +
" \"inputs\": [\n" +
" {\n" +
" \"indexed\": true,\n" +
" \"internalType\": \"uint256[]\",\n" +
" \"name\": \"nums\",\n" +
" \"type\": \"uint256[]\"\n" +
" },\n" +
" {\n" +
" \"indexed\": true,\n" +
" \"internalType\": \"uint8\",\n" +
" \"name\": \"random\",\n" +
" \"type\": \"uint8\"\n" +
" }\n" +
" ],\n" +
" \"name\": \"Stored\",\n" +
" \"type\": \"event\"\n" +
" }");
assertThrows(NullPointerException.class, () -> {
event.decodeArgs(null, new byte[0]);
});
}

@Test
public void testDecodeArgsNullDataShouldFail() {
Event event = Event.fromJson("{\n" +
" \"anonymous\": false,\n" +
" \"inputs\": [\n" +
" {\n" +
" \"indexed\": true,\n" +
" \"internalType\": \"uint256[]\",\n" +
" \"name\": \"nums\",\n" +
" \"type\": \"uint256[]\"\n" +
" },\n" +
" {\n" +
" \"indexed\": true,\n" +
" \"internalType\": \"uint8\",\n" +
" \"name\": \"random\",\n" +
" \"type\": \"uint8\"\n" +
" }\n" +
" ],\n" +
" \"name\": \"Stored\",\n" +
" \"type\": \"event\"\n" +
" }");
assertThrows(NullPointerException.class, () -> {
event.decodeArgs(new byte[0][0], null);
});
}
}