Skip to content

Commit 9530cf0

Browse files
authored
feat(abi): support ABI v2 structs and nested arrays (#212)
1 parent 83ad0b6 commit 9530cf0

54 files changed

Lines changed: 9101 additions & 333 deletions

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

abi/build.gradle

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,4 +6,5 @@ description 'TRON Application Binary Interface (ABI) for working with smart cont
66

77
dependencies {
88
implementation project(':utils')
9+
testImplementation "com.fasterxml.jackson.core:jackson-databind:2.18.6"
910
}
Lines changed: 43 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,43 @@
1+
package org.tron.trident.abi;
2+
3+
import java.nio.charset.StandardCharsets;
4+
import java.util.List;
5+
import java.util.stream.Collectors;
6+
import org.tron.trident.abi.datatypes.CustomError;
7+
import org.tron.trident.abi.datatypes.Type;
8+
import org.tron.trident.crypto.Hash;
9+
import org.tron.trident.utils.Numeric;
10+
11+
/**
12+
* Ethereum custom error encoding. Further limited details are available <a
13+
* href="https://docs.soliditylang.org/en/develop/abi-spec.html#errors">here</a>.
14+
*/
15+
public class CustomErrorEncoder {
16+
17+
private CustomErrorEncoder() {
18+
}
19+
20+
public static String encode(CustomError error) {
21+
return calculateSignatureHash(
22+
buildErrorSignature(error.getName(), error.getParameters()));
23+
}
24+
25+
static <T extends Type> String buildErrorSignature(
26+
String errorName, List<TypeReference<T>> parameters) {
27+
28+
StringBuilder result = new StringBuilder();
29+
result.append(errorName);
30+
result.append("(");
31+
String params =
32+
parameters.stream().map(Utils::getTypeName).collect(Collectors.joining(","));
33+
result.append(params);
34+
result.append(")");
35+
return result.toString();
36+
}
37+
38+
public static String calculateSignatureHash(String errorSignature) {
39+
byte[] input = errorSignature.getBytes(StandardCharsets.UTF_8);
40+
byte[] hash = Hash.sha3(input);
41+
return Numeric.toHexString(hash).substring(2);
42+
}
43+
}

abi/src/main/java/org/tron/trident/abi/DefaultFunctionEncoder.java

Lines changed: 45 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,7 @@
1717
import java.util.List;
1818
import org.tron.trident.abi.datatypes.Function;
1919
import org.tron.trident.abi.datatypes.StaticArray;
20+
import org.tron.trident.abi.datatypes.StaticStruct;
2021
import org.tron.trident.abi.datatypes.Type;
2122
import org.tron.trident.abi.datatypes.Uint;
2223

@@ -64,11 +65,53 @@ private static String encodeParameters(
6465
return result.toString();
6566
}
6667

68+
/**
69+
* Encodes a function call including its method signature (selector) and its parameters.
70+
*
71+
* @param methodId the 4-byte selector (8 hex chars)
72+
* @param parameters the list of parameters to encode
73+
* @return the complete ABI-encoded hex string representing the function call
74+
*/
75+
public String encodeWithSelector(String methodId, List<Type> parameters) {
76+
final StringBuilder result = new StringBuilder(methodId);
77+
78+
return encodeParameters(parameters, result);
79+
}
80+
81+
/**
82+
* Encodes parameters using tight packing (abi.encodePacked).
83+
* This is a non-standard ABI encoding used primarily for computing hashes,
84+
* where padding is omitted and dynamic types are concatenated without length prefixes.
85+
*
86+
* @param parameters the list of parameters to pack
87+
* @return the packed ABI-encoded hex string
88+
*/
89+
@Override
90+
protected String encodePackedParameters(List<Type> parameters) {
91+
final StringBuilder result = new StringBuilder();
92+
for (Type parameter : parameters) {
93+
result.append(TypeEncoder.encodePacked(parameter));
94+
}
95+
return result.toString();
96+
}
97+
98+
/**
99+
* Calculates the length of the tuple head (in 32-byte slots) required for the given parameters.
100+
* Crucially, all dynamic types (including StaticArrays containing dynamic elements) always occupy exactly
101+
* 1 slot in the head (for the offset pointer). Pure static arrays and structs are flattened to calculate
102+
* their total inline slot requirement.
103+
*
104+
* @param parameters the list of types to be encoded.
105+
* @return the total number of 32-byte slots required for the tuple head.
106+
*/
107+
@SuppressWarnings("unchecked")
67108
private static int getLength(final List<Type> parameters) {
68109
int count = 0;
69110
for (final Type type : parameters) {
70-
if (type instanceof StaticArray) {
71-
count += ((StaticArray) type).getValue().size();
111+
if (TypeEncoder.isDynamic(type)) {
112+
count++;
113+
} else if (type instanceof StaticArray || type instanceof StaticStruct) {
114+
count += type.bytes32PaddedLength() / Type.MAX_BYTE_LENGTH;
72115
} else {
73116
count++;
74117
}

abi/src/main/java/org/tron/trident/abi/DefaultFunctionReturnDecoder.java

Lines changed: 44 additions & 39 deletions
Original file line numberDiff line numberDiff line change
@@ -13,14 +13,16 @@
1313

1414
package org.tron.trident.abi;
1515

16+
import static org.tron.trident.abi.TypeDecoder.MAX_BYTE_LENGTH_FOR_HEX_STRING;
17+
import static org.tron.trident.abi.TypeDecoder.isDynamic;
18+
1619
import java.util.ArrayList;
1720
import java.util.Collections;
1821
import java.util.List;
1922
import org.tron.trident.abi.datatypes.Array;
2023
import org.tron.trident.abi.datatypes.Bytes;
2124
import org.tron.trident.abi.datatypes.BytesType;
2225
import org.tron.trident.abi.datatypes.DynamicArray;
23-
import org.tron.trident.abi.datatypes.DynamicBytes;
2426
import org.tron.trident.abi.datatypes.DynamicStruct;
2527
import org.tron.trident.abi.datatypes.StaticArray;
2628
import org.tron.trident.abi.datatypes.StaticStruct;
@@ -31,7 +33,7 @@
3133
import org.tron.trident.utils.Strings;
3234

3335
/**
34-
* Ethereum Contract Application Binary Interface (ABI) encoding for functions. Further details are
36+
* Ethereum Contract Application Binary Interface (ABI) decoding for functions. Further details are
3537
* available <a href="https://github.com/ethereum/wiki/wiki/Ethereum-Contract-ABI">here</a>.
3638
*/
3739
public class DefaultFunctionReturnDecoder extends FunctionReturnDecoder {
@@ -73,60 +75,63 @@ public <T extends Type> Type decodeEventParameter(
7375
}
7476

7577
private static List<Type> build(String input, List<TypeReference<Type>> outputParameters) {
78+
// Reject cyclic or excessively nested TypeReference graphs up front; any
79+
// downstream recursion through subTypeReference / innerTypes is then bounded
80+
// by what passed validation here.
81+
for (TypeReference<?> typeReference : outputParameters) {
82+
Utils.validateTypeReferenceDepth(typeReference);
83+
}
84+
7685
List<Type> results = new ArrayList<>(outputParameters.size());
7786

7887
int offset = 0;
7988
for (TypeReference<?> typeReference : outputParameters) {
8089
try {
90+
int hexStringDataOffset = getDataOffset(input, offset, typeReference);
91+
8192
@SuppressWarnings("unchecked")
8293
Class<Type> classType = (Class<Type>) typeReference.getClassType();
8394

84-
int hexStringDataOffset = getDataOffset(input, offset, classType);
85-
8695
Type result;
8796
if (DynamicStruct.class.isAssignableFrom(classType)) {
88-
if (outputParameters.size() != 1) {
89-
throw new UnsupportedOperationException(
90-
"Multiple return objects containing a struct is not supported");
91-
}
9297
result =
93-
TypeDecoder.decodeDynamicStruct(
94-
input, hexStringDataOffset, typeReference);
95-
offset += TypeDecoder.MAX_BYTE_LENGTH_FOR_HEX_STRING;
98+
TypeDecoder.decodeDynamicStruct(
99+
input, hexStringDataOffset, typeReference);
100+
offset += MAX_BYTE_LENGTH_FOR_HEX_STRING;
96101

97102
} else if (DynamicArray.class.isAssignableFrom(classType)) {
98103
result =
99-
TypeDecoder.decodeDynamicArray(
100-
input, hexStringDataOffset, typeReference);
101-
offset += TypeDecoder.MAX_BYTE_LENGTH_FOR_HEX_STRING;
102-
103-
} else if (typeReference instanceof TypeReference.StaticArrayTypeReference) {
104-
int length = ((TypeReference.StaticArrayTypeReference) typeReference).getSize();
105-
result =
106-
TypeDecoder.decodeStaticArray(
107-
input, hexStringDataOffset, typeReference, length);
108-
offset += length * TypeDecoder.MAX_BYTE_LENGTH_FOR_HEX_STRING;
104+
TypeDecoder.decodeDynamicArray(
105+
input, hexStringDataOffset, typeReference);
106+
offset += MAX_BYTE_LENGTH_FOR_HEX_STRING;
109107

110108
} else if (StaticStruct.class.isAssignableFrom(classType)) {
111109
result =
112-
TypeDecoder.decodeStaticStruct(
113-
input, hexStringDataOffset, typeReference);
114-
offset +=
115-
classType.getDeclaredFields().length * TypeDecoder.MAX_BYTE_LENGTH_FOR_HEX_STRING;
110+
TypeDecoder.decodeStaticStruct(
111+
input, hexStringDataOffset, typeReference);
112+
offset += (result.bytes32PaddedLength() / Type.MAX_BYTE_LENGTH)
113+
* MAX_BYTE_LENGTH_FOR_HEX_STRING;
114+
116115
} else if (StaticArray.class.isAssignableFrom(classType)) {
117-
int length =
118-
Integer.parseInt(
119-
classType
120-
.getSimpleName()
121-
.substring(StaticArray.class.getSimpleName().length()));
116+
int length;
117+
if (typeReference instanceof TypeReference.StaticArrayTypeReference) {
118+
length = ((TypeReference.StaticArrayTypeReference) typeReference).getSize();
119+
} else {
120+
length = Utils.extractStaticArraySize(classType);
121+
}
122122
result =
123-
TypeDecoder.decodeStaticArray(
124-
input, hexStringDataOffset, typeReference, length);
125-
offset += length * TypeDecoder.MAX_BYTE_LENGTH_FOR_HEX_STRING;
126-
123+
TypeDecoder.decodeStaticArray(
124+
input, hexStringDataOffset, typeReference, length);
125+
if (isDynamic(typeReference)) {
126+
offset += MAX_BYTE_LENGTH_FOR_HEX_STRING;
127+
} else {
128+
offset +=
129+
(result.bytes32PaddedLength() / Type.MAX_BYTE_LENGTH)
130+
* MAX_BYTE_LENGTH_FOR_HEX_STRING;
131+
}
127132
} else {
128133
result = TypeDecoder.decode(input, hexStringDataOffset, classType);
129-
offset += TypeDecoder.MAX_BYTE_LENGTH_FOR_HEX_STRING;
134+
offset += MAX_BYTE_LENGTH_FOR_HEX_STRING;
130135
}
131136
results.add(result);
132137

@@ -137,10 +142,10 @@ private static List<Type> build(String input, List<TypeReference<Type>> outputPa
137142
return results;
138143
}
139144

140-
private static <T extends Type> int getDataOffset(String input, int offset, Class<T> type) {
141-
if (DynamicBytes.class.isAssignableFrom(type)
142-
|| Utf8String.class.isAssignableFrom(type)
143-
|| DynamicArray.class.isAssignableFrom(type)) {
145+
public static <T extends Type> int getDataOffset(
146+
String input, int offset, TypeReference<?> typeReference)
147+
throws ClassNotFoundException {
148+
if (isDynamic(typeReference)) {
144149
return TypeDecoder.decodeUintAsInt(input, offset) << 1;
145150
} else {
146151
return offset;

abi/src/main/java/org/tron/trident/abi/FunctionEncoder.java

Lines changed: 57 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,9 @@
1313

1414
package org.tron.trident.abi;
1515

16+
import static org.tron.trident.abi.TypeDecoder.instantiateType;
17+
import static org.tron.trident.abi.TypeReference.makeTypeReference;
18+
1619
import java.lang.reflect.InvocationTargetException;
1720
import java.util.ArrayList;
1821
import java.util.Iterator;
@@ -32,36 +35,55 @@
3235
* @see DefaultFunctionEncoder
3336
* @see FunctionEncoderProvider
3437
*/
38+
3539
public abstract class FunctionEncoder {
3640

37-
private static FunctionEncoder DEFAULT_ENCODER;
41+
private static final FunctionEncoder FUNCTION_ENCODER;
3842

39-
private static final ServiceLoader<FunctionEncoderProvider> loader =
40-
ServiceLoader.load(FunctionEncoderProvider.class);
43+
static {
44+
ServiceLoader<FunctionEncoderProvider> loader =
45+
ServiceLoader.load(FunctionEncoderProvider.class);
46+
final Iterator<FunctionEncoderProvider> iterator = loader.iterator();
47+
48+
FUNCTION_ENCODER =
49+
iterator.hasNext() ? iterator.next().get() : new DefaultFunctionEncoder();
50+
}
4151

4252
public static String encode(final Function function) {
43-
return encoder().encodeFunction(function);
53+
return FUNCTION_ENCODER.encodeFunction(function);
54+
}
55+
56+
/** Encode function when we know function method Id / Selector. */
57+
public static String encode(final String methodId, final List<Type> parameters) {
58+
return FUNCTION_ENCODER.encodeWithSelector(methodId, parameters);
4459
}
4560

4661
public static String encodeConstructor(final List<Type> parameters) {
47-
return encoder().encodeParameters(parameters);
62+
return FUNCTION_ENCODER.encodeParameters(parameters);
63+
}
64+
65+
public static String encodeConstructorPacked(final List<Type> parameters) {
66+
return FUNCTION_ENCODER.encodePackedParameters(parameters);
4867
}
4968

5069
public static Function makeFunction(
5170
String fnname,
5271
List<String> solidityInputTypes,
5372
List<Object> arguments,
5473
List<String> solidityOutputTypes)
55-
throws ClassNotFoundException, NoSuchMethodException, InstantiationException,
56-
IllegalAccessException, InvocationTargetException {
74+
throws ClassNotFoundException,
75+
NoSuchMethodException,
76+
InstantiationException,
77+
IllegalAccessException,
78+
InvocationTargetException {
5779
List<Type> encodedInput = new ArrayList<>();
5880
Iterator argit = arguments.iterator();
5981
for (String st : solidityInputTypes) {
60-
encodedInput.add(TypeDecoder.instantiateType(st, argit.next()));
82+
encodedInput.add(instantiateType(st, argit.next()));
6183
}
6284
List<TypeReference<?>> encodedOutput = new ArrayList<>();
6385
for (String st : solidityOutputTypes) {
64-
encodedOutput.add(TypeReference.makeTypeReference(st));
86+
encodedOutput.add(makeTypeReference(st));
6587
}
6688
return new Function(fnname, encodedInput, encodedOutput);
6789
}
@@ -70,6 +92,32 @@ public static Function makeFunction(
7092

7193
protected abstract String encodeParameters(List<Type> parameters);
7294

95+
/**
96+
* Encodes parameters prefixed with the given selector. Not part of the original
97+
* subclass contract: implementations predating it inherit this throwing default,
98+
* so the pre-existing entry points keep working and only the newer
99+
* {@link #encode(String, List)} fails, with a clear message.
100+
*
101+
* @param methodId Callback selector / Abi method Id (Hex format)
102+
*/
103+
protected String encodeWithSelector(
104+
final String methodId, final List<Type> parameters) {
105+
throw new UnsupportedOperationException(
106+
"encodeWithSelector is not implemented by " + getClass().getName()
107+
+ "; override it to support FunctionEncoder.encode(methodId, parameters)");
108+
}
109+
110+
/**
111+
* Encodes parameters using tight packing (abi.encodePacked). Not part of the
112+
* original subclass contract; see {@link #encodeWithSelector(String, List)}.
113+
*/
114+
protected String encodePackedParameters(List<Type> parameters) {
115+
throw new UnsupportedOperationException(
116+
"encodePackedParameters is not implemented by " + getClass().getName()
117+
+ "; override it to support "
118+
+ "FunctionEncoder.encodeConstructorPacked(parameters)");
119+
}
120+
73121
protected static String buildMethodSignature(
74122
final String methodName, final List<Type> parameters) {
75123

@@ -88,16 +136,4 @@ protected static String buildMethodId(final String methodSignature) {
88136
final byte[] hash = Hash.sha3(input);
89137
return Numeric.toHexString(hash).substring(2, 10);
90138
}
91-
92-
private static FunctionEncoder encoder() {
93-
final Iterator<FunctionEncoderProvider> iterator = loader.iterator();
94-
return iterator.hasNext() ? iterator.next().get() : defaultEncoder();
95-
}
96-
97-
private static FunctionEncoder defaultEncoder() {
98-
if (DEFAULT_ENCODER == null) {
99-
DEFAULT_ENCODER = new DefaultFunctionEncoder();
100-
}
101-
return DEFAULT_ENCODER;
102-
}
103139
}

0 commit comments

Comments
 (0)