feat(abi): support ABI v2 structs and nested arrays#212
Merged
kuny0707 merged 26 commits intoJul 21, 2026
Merged
Conversation
…odeWithOutPrefix/buildEventSignatureWithOutPrefix
…r Encoding, add unitest
…ray elements When encoding DynamicArray<StaticArray<DynamicBytes>> (e.g. bytes[3][]),the encoder was missing offset pointer table for inner elements.StaticArray with dynamic sub-elements was not recognized as needing offsets in encodeArrayValuesOffsets()
- Utils.getStructType: @parameterized on StaticArrayN struct fields now correctly emits "foo[N]" instead of collapsing to "foo[]". Throws RuntimeException on ClassNotFoundException instead of silent fallback; extractStaticArraySize now takes Class<?> and validates that the class is a generated StaticArrayN subclass (rejects raw StaticArray.class with a guidance message). - Utils.getParameterizedTypeName: build canonical ABI names for nested arrays via the new getArrayElementTypeName helper, which recurses on TypeReference rather than collapsing the inner type to a Class. The old path produced non-canonical strings like "staticarray2[]". - DefaultFunctionReturnDecoder.getDataOffset: include DynamicStruct in the dynamic-offset detection. The hand-rolled OR-chain was missing it, causing DynamicStruct return values to be decoded from the wrong offset. - DynamicArray.getTypeAsString: throw when an empty array's component type is the generic DynamicStruct/StaticStruct base or a raw Array subclass — Java type erasure cannot recover the inner element type in those cases. Concrete StructType subclasses still resolve through Utils.getStructType. - Utf8String.bytes32PaddedLength: guard against null value, matching the null-tolerance already in equals()/hashCode(). - Array: remove the unused getNativeValueCopy() method. It was dead code in this PR (zero callers) and only unwrapped one level, so nested arrays/structs still contained ABI Type objects rather than native values. - UtilsTest: positive/negative cases for issue tronprotocol#20, DynamicArray regression guard, and canonical nested array type names (uint256[2][], uint256[2][2]). - EventEncoderTest: nested static array method signature. - DynamicArrayTest: empty concrete struct array (allowed), empty raw Array subclass (throws), empty generic struct array (throws). - Utf8StringTest: bytes32PaddedLength with null value. - CustomErrorTest: assert parameter count before per-element comparison; the previous loop could pass silently if getParameters() returned fewer elements than expected. - TridentAbiEncodeDecodeCompatibilityTest.isSignedIntOutOfRange: treat bare `int` as ABI alias for int256.
jj5419952-stack
approved these changes
Jun 7, 2026
…plify DynamicStruct type-string branches
- decode trcToken[N] static arrays: instantiateStaticArray used the deprecated List-only StaticArrayN constructor, which re-derives the element type via AbiTypes.getType(getTypeAsString()); TrcToken's type string is "trcToken256" while its AbiTypes key is "trcToken", so decoding trcToken[N] failed on every path. Switch to the Class-carrying constructor and take the element type from the decoded elements. - fail fast on truncated input in decodeNumeric: Arrays.copyOfRange silently zero-pads past the end of input, and the padding lands in the low-order bytes — a truncated Uint256 word decodes to value << (8 * missing bytes) with no error. Restore the fail-fast contract with a JDK 8-compatible explicit bounds check. - keep T[0] on the static-array branch in instantiateArrayType: arraySize is -1 only for true dynamic-array references, but the dispatch tested <= 0, so uint256[0] silently instantiated as DynamicArray with type string "uint256[]" — a wrong function selector. With < 0, T[0] constructs a proper StaticArray0 and rejects non-empty values loudly. - surface type-resolution failures in isDynamic(TypeReference): the method swallowed ClassNotFoundException/ClassCastException and answered "static", silently mis-placing every subsequent field's offset. Exclude StructType from the array branch explicitly (StaticStruct extends StaticArray), then rethrow resolution failures as UnsupportedOperationException.
Three defects in the constructor-reflection struct decoding path: 1. StaticArrayN struct fields were undecodable: they fell through to decode(String, int, Class), which rejects every Array subclass. New decodeStaticArrayStructField reads the element type from @parameterized (the same contract getStructType uses for signatures), builds the array reference, and decodes via decodeStaticArray. Static arrays of dynamic elements get a clear error pointing at innerTypes instead of garbage. 2. Nested-struct spans were guessed from reflection metadata — getDeclaredFields()[i]/getConstructors()[0] parameter counts in decodeStaticStructElement (wrong for non-public or extra java fields, arbitrary constructor pick, one-word-per-parameter assumption breaking doubly-nested structs), and the public-fields flat list in decodeDynamicStructElements (undercounts private fields). Both now advance by the decoded value's real span (bytes32PaddedLength), the pattern decodeArrayElements already used. 3. getTypeReferenceForParameterizedField round-tripped StaticArrayN element types through a lossy Solidity-name string (getSimpleTypeName lowercases, AbiTypes' lookup is case-sensitive), so TrcToken/Fixed/struct elements threw "Unsupported type encountered". It now builds the StaticArrayTypeReference directly from the classes at hand; getSimpleTypeName gains the camelCase trcToken token for signatures.
…lection path string[2]-style struct fields are ABI-dynamic (head offset + tail) and already encode correctly, but decodeDynamicStructElements misclassified them as static and threw. Classify fields via isDynamicStructField (declared class OR @parameterized element type) at all three decision points, and decode the tail with the same machinery the innerTypes path uses. Add regression tests; assert zero-length static array rejection at the construction layer per review. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
…ed nested arrays Address three code-review findings on the runtime-tuple and nested-array paths: - Utils.getTypeName: serialize runtime tuple TypeReferences (innerTypes form) as "(type1,type2,...)" so EventEncoder and CustomErrorEncoder can build signatures like E((uint256,string)) without a generated struct class. Previously this form hit constructor reflection on the DynamicStruct or StaticStruct base class and threw. Composes with arrays via getSubTypeReference, e.g. E((uint256,string)[]). - Utils.getTypeReferenceForParameterizedField: fail fast when the @parameterized element type is itself a (non-struct) array class. The annotation carries a single element-type level, so nested array fields (e.g. uint256[2][2] as StaticArray2<StaticArray2<Uint256>>) previously produced invalid signatures like "(staticarray2[2])", threw ClassCastException, or silently misread tail data during decode. Documented as a known limitation of the constructor-reflection path; runtime innerTypes references (makeTypeReference) still support nesting. TypeDecoder.decodeDynamicParameterFromStruct now routes its DynamicArray branch through the same guard. - TypeEncoder.isSupportingEncodedPacked: reject StaticArray component types. Solidity forbids nested arrays in abi.encodePacked; previously the encoder silently fell back to standard ABI encoding, emitting offset/length words that hash to wrong signatures. Checking the direct component type covers all nesting shapes since a nested array's component is itself an array. Adds regression tests across EventEncoderTest, CustomErrorEncoderTest, UtilsTest and TypeEncoderPackedTest.
jj5419952-stack
approved these changes
Jul 14, 2026
- encodePacked: reject structs up front. DynamicStruct/StaticStruct extend the array classes and fell into arrayEncodePacked, silently producing corrupt packed bytes; Solidity's packed mode does not support structs. - trcToken: emit the canonical camelCase "trcToken" from both AbiTypes.getTypeAString and instance-level TrcToken.getTypeAsString (previously "trctoken" / "trcToken256"), so selectors and the getType round-trip agree on every path; the override mirrors Address over its underlying Uint160. - decoding: bound decode()'s numeric/address dispatch slices to one 32-byte word and pass offsets instead of whole-tail substrings in the dynamic-struct paths. Decoding uint256[n] copied O(n^2) characters: uint256[16000] (1 MiB hex) took ~8.4s and now ~28ms. Chain responses are untrusted input, so this was a DoS amplification vector. - StaticArray.getTypeAsString: handle empty struct components the way DynamicArray already does — tuple string via Utils.getStructType, descriptive error for generic struct / nested array / null component — instead of silently hashing lowercased Java class names into selectors. - TypeReference depth validation: allow exactly MAX_TYPEREF_DEPTH nesting (off-by-one) and replace the iterative walker plus cycle set with bounded recursion; cycles necessarily trip the depth cap. - consolidate StaticArrayN size parsing onto Utils.extractStaticArraySize, preferring StaticArrayTypeReference.getSize() where available; decoding uint256[33][2] now fails naming the missing generated StaticArray33 instead of a misleading zero-length-array error. - TypeReference.getSubTypeReference: make the base declaration public (matching current upstream web3j) so subclasses outside the package can actually override it — package-private meant external same-signature methods only shadowed it, so wrapping a runtime-tuple innerTypes reference in a caller-built array reference decoded against a null subtype and threw.
- decode(String, int, TypeReference): every call throws. getRawType().getClass() always yields java.lang.Class, so both isAssignableFrom branches are unreachable. Inherited from web3j and never called inside the SDK; document the defect, the working alternatives (decodeStaticArray / decodeDynamicArray) and the intended fix instead of changing behavior. - Address(Uint) packed width: encodePacked slices Address by the stored Uint's bit size, so only the 160-bit-normalizing constructors (Address(BigInteger), Address(String)) produce the canonical 20 packed bytes, while Address(Uint) packs to 32. Document the pitfall at the constructor and at the removePadding slice.
- route StaticArrayN size parsing through Utils.extractStaticArraySize - collapse getDataOffset's dynamic check onto TypeDecoder.isDynamic - state decodeNumeric's length check in word terms - extract shared element-type-string logic into Array - dedupe component-type inference in deprecated array constructors - reuse TypeDecoder.isDynamic for TypeEncoder's empty static-array probe - wrap two over-length lines in TypeDecoder.decode
… semantics Return the length word plus the value padded to 32-byte words, matching current web3j (null value still tolerated as empty). Port web3j's Utf8String encoding and padded-length tests.
…ce); harden Utf8String padded length - declare ClassNotFoundException on TypeDecoder.isDynamic(TypeReference) and drop the CNFE-to-UnsupportedOperationException wrapper so getDataOffset honors its checked throws contract; add regression tests covering unresolvable type references on both the getDataOffset and FunctionReturnDecoder.decode paths - guard Utf8String.bytes32PaddedLength arithmetic with Math.addExact/multiplyExact - re-enable testDecodeMultiDimDynamicArrayFunction, previously commented out when multi-dimensional dynamic arrays were unsupported - test cleanups: reuse TypeEncoder.isDynamic in the compatibility test, drop a redundant anonymous DynamicArray subclass, remove unused imports, fix a javadoc typo, construct the test mapper via JsonMapper
- getStructType: array-typed struct fields missing @parameterized now throw a named exception instead of silently emitting invalid signature tokens (e.g. "dynamicarray") that hash to wrong selectors; conversely, @parameterized on a non-array field is rejected at the getTypeReferenceForParameterizedField entry, covering the signature path and all three decode paths. - Pin every case conversion in main sources to Locale.ROOT (abi token builders, core AbiUtils switches, utils helpers): under a Turkish default locale 'I' lowercases to dotless 'ı', corrupting "Int256" into "ınt256" and silently changing selectors. Turkish-locale regression test added. - encodePacked: the array gate validates actual element types in addition to the declared componentType — arrays declared with a supertype component (e.g. Type.class) could smuggle strings, structs and nested arrays past the gate and produce corrupt packed bytes. - AbiTypes.getTypeAString: canonicalize bare Uint/Int/Ufixed/Fixed to their 256-suffixed tokens, aligning with Utils.getSimpleTypeName; the instance path used to emit "uint[]", whose selector matches no on-chain contract (solc normalizes the alias before hashing). - TrcToken now extends Uint: decodeNumeric takes the unsigned branch, so a word with the top bit set decodes to the positive token id instead of a two's-complement negative, and encoding values >= 2^255 no longer throws; the canonical "trcToken" signature token is unchanged. - Deprecate Address(Uint), which stores the Uint without 160-bit normalization and packs 32 bytes instead of the canonical 20. - Remove the misspelled EventEncoder.encodeWithOutPrefix / buildEventSignatureWithOutPrefix (new on this branch, no production callers, absent from web3j) before 1.0.0 freezes the API. - FunctionEncoder: demote the two newly added abstract methods (encodeWithSelector, encodePackedParameters) to throwing defaults. The abstract+ServiceLoader subclass contract has been published since 0.9.2; new abstract methods would break every existing third-party encoder (compile error, or AbstractMethodError via the SPI). Legacy subclasses keep serving the original entry points; only the new selector/packed entry points fail, with a clear UnsupportedOperationException.
317787106
approved these changes
Jul 21, 2026
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
What does this PR do?
Extends ABI v2 (
tuple/struct) support in theabimodule across encoding and decoding, ported from and enhanced over web3j v6.0.0:StaticStructandDynamicStruct, including nested structs, arrays of structs, and nested array layouts represented through runtimeTypeReference/innerTypes. Constructor-reflection decoding supports one array element-type level through the new@Parameterizedannotation, which restores the erased element class for array-typed constructor parameters (the static-array size is derived from the concreteStaticArrayNclass); nested array fields must use runtime type references.StaticArrayN, plus the newly generatedStaticArray0) and dynamic arrays with nested static/dynamic elements. Fixes missing offset pointers when aDynamicArraycontains dynamicStaticArrayelements.TypeEncoder.encodePackedandFunctionEncoder.encodeConstructorPackedfor supported scalar and array shapes, with explicit rejection of directly supplied structs, declared nested-array component types, and arrays declared with a supertype component (e.g.Type.class) whose actual elements are unsupported (both component-type and per-element validation).CustomErrordatatype andCustomErrorEncoderfor encoding Solidityerrordefinitions and computing their signature hashes.Bug fixes and hardening included along the way:
Utf8Stringpadded-length calculation now uses the UTF-8 byte length instead of the character count, so non-ASCII strings encode correctly; the arithmetic is overflow-checked and length-word semantics match web3j.TypeReferencenesting depth at the decoder entry. Reflective loading of externally supplied ABI type names avoids class initialization and validates that resolved classes implement the ABITypeinterface.StaticArrayinstead of a generatedStaticArrayN; when an array-typed struct field lacks@Parameterized(previously emitted invalid signature tokens likedynamicarray, yielding wrong selectors); and when@Parameterizedis placed on a non-array field.Uint/Int/Ufixed/Fixedused as array/struct components now canonicalize to their sized tokens (e.g.uint[]→uint256[]), matching solc's alias expansion before selector hashing and removing the inconsistency between the two class-to-token mappings.Locale.ROOT, so signature tokens survive locales like Turkish (whereInt256used to corrupt intoınt256).TrcTokensignature token is corrected from the wrongtrcToken256(instance path) /trctoken(TypeReference path) to the canonicaltrcToken, so selectors of trcToken-parameter functions and events change accordingly — the previous selectors matched no real contract.TrcTokenalso now extendsUint: decoding takes the unsigned branch, so a word with the top bit set yields the positive token id (previously a two's-complement negative), and encoding values >= 2^255 no longer throws.Why are these changes required?
Contracts compiled with
pragma abicoder v2(the default since Solidity 0.8) commonly use structs and nested arrays in function parameters, return values, and events. Trident already supported basic struct and array cases, but several ABI v2 combinations — such as runtime tuples, arrays of structs, nested array layouts, and array-typed struct fields — were incomplete or inconsistent across encoding, return decoding, and event-signature generation. This PR completes those paths.This PR has been tested by:
abi— encoder/decoder tests for structs and nested arrays (DefaultFunctionEncoderTest,FunctionReturnDecoderTest,TypeDecoderTest,TypeEncoderTest), packed encoding (TypeEncoderPackedTest), constructor-reflection decoding (StructFieldReflectionDecodeTest,StaticArrayInStructDecodeTest), custom errors, and datatype-level tests (Utf8StringTest,DynamicArrayTest,StaticStructTest, etc.).TridentAbiEncodeDecodeCompatibilityTestreplays large fixture datasets (contract-interface*.json.gz) to verify encode/decode output stays consistent for real-world ABI inputs.AbiV2ContractTestincorecalls a pre-deployed ABI v2 contract on Nile to verify static/dynamic structs, arrays of structs, multidimensional arrays, and packed encoding. The deployment case and the signed nested-struct transaction case remain disabled/manual.Follow up
Known limitations, deliberately kept aligned with the web3j implementation (documented in code comments) so they can be fixed upstream first and then synced back:
TypeReference/innerTypes.Address(Uint)is not normalized to 160 bits and can produce a non-canonical packed address; the constructor is now marked@Deprecated.Address(BigInteger)/Address(String)produce the canonical 20 bytes.Extra details
FunctionEncodermethodsencodeWithSelectorandencodePackedParametersare optional overrides with throwing defaults (UnsupportedOperationException): existing third-party subclasses keep compiling and serving the original entry points unchanged; only calling the new entry points without an override fails, with a clear message. Encoder/decoder providers are now resolved once during class initialization instead of per call, so providers registered afterwards are not picked up.jackson-databindis added as a test-only dependency of theabimodule (for reading JSON fixtures); no new runtime dependencies.@Deprecatedwith Javadoc pointing to the type-safe replacements.