Skip to content
Closed
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
160 changes: 102 additions & 58 deletions crypto/src/main/java/org/web3j/crypto/StructuredDataEncoder.java
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,6 @@
import java.io.ByteArrayOutputStream;
import java.io.IOException;
import java.lang.reflect.Constructor;
import java.lang.reflect.InvocationTargetException;
import java.math.BigInteger;
import java.util.ArrayList;
import java.util.Collections;
Expand Down Expand Up @@ -307,11 +306,25 @@ public byte[] encodeData(String primaryType, HashMap<String, Object> data)

List<String> encTypes = new ArrayList<>();
List<Object> encValues = new ArrayList<>();
List<byte[]> dynamicData = new ArrayList<>(); // Store dynamic data

// Add typehash
encTypes.add("bytes32");
encValues.add(typeHash(primaryType));

// Calculate header size (static part)
int headSize = 32; // typehash
for (StructuredData.Entry field : types.get(primaryType)) {
if (field.getType().contains("[]")) {
headSize += 32; // Dynamic array offset takes 32 bytes
} else {
headSize += 32; // Static type takes 32 bytes
}
Comment on lines +318 to +322
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

the check here makes no sense as the same line of code is executed every time

}

// Track total size of dynamic data
int dynamicDataSize = 0;

// Add field contents
for (StructuredData.Entry field : types.get(primaryType)) {
Object value = data.get(field.getName());
Expand All @@ -337,86 +350,117 @@ public byte[] encodeData(String primaryType, HashMap<String, Object> data)
} else if (arrayTypePattern.matcher(field.getType()).find()) {
String baseTypeName = field.getType().substring(0, field.getType().indexOf('['));
List<Object> arrayItems = getArrayItems(field, value);
ByteArrayOutputStream concatenatedArrayEncodingBuffer = new ByteArrayOutputStream();

for (Object arrayItem : arrayItems) {
byte[] arrayItemEncoding;
if (types.containsKey(baseTypeName)) {
arrayItemEncoding =
sha3(
encodeData(
baseTypeName,
(HashMap<String, Object>)
arrayItem)); // need to hash each user type
// before adding
} else {
arrayItemEncoding =
convertToEncodedItem(
baseTypeName,
arrayItem); // add raw item, packed to 32 bytes

if (baseTypeName.startsWith("uint")
|| baseTypeName.startsWith("int")
|| baseTypeName.equals("address")
|| baseTypeName.equals("bool")) {
// Handle dynamic array
encTypes.add(baseTypeName); // Use base type instead of array type
// Add offset position, considering actual size of all previous dynamic data
encValues.add(BigInteger.valueOf(headSize + dynamicDataSize));

// Prepare dynamic data
ByteArrayOutputStream dynamicBuffer = new ByteArrayOutputStream();
// Write array length
byte[] lengthBytes =
Numeric.toBytesPadded(BigInteger.valueOf(arrayItems.size()), 32);
dynamicBuffer.write(lengthBytes, 0, lengthBytes.length);

// Write array elements
for (Object arrayItem : arrayItems) {
BigInteger itemValue = convertToBigInt(arrayItem);
byte[] itemBytes = Numeric.toBytesPadded(itemValue, 32);
dynamicBuffer.write(itemBytes, 0, itemBytes.length);
}

concatenatedArrayEncodingBuffer.write(
arrayItemEncoding, 0, arrayItemEncoding.length);
byte[] dynamicBytes = dynamicBuffer.toByteArray();
dynamicData.add(dynamicBytes);
// Update total size of dynamic data
dynamicDataSize += dynamicBytes.length;
} else {
// Handle other types of arrays
ByteArrayOutputStream concatenatedArrayEncodingBuffer =
new ByteArrayOutputStream();
for (Object arrayItem : arrayItems) {
byte[] arrayItemEncoding;
if (types.containsKey(baseTypeName)) {
arrayItemEncoding =
sha3(
encodeData(
baseTypeName,
(HashMap<String, Object>) arrayItem));
} else {
arrayItemEncoding = convertToEncodedItem(baseTypeName, arrayItem);
}
concatenatedArrayEncodingBuffer.write(
arrayItemEncoding, 0, arrayItemEncoding.length);
}
byte[] concatenatedArrayEncodings =
concatenatedArrayEncodingBuffer.toByteArray();
byte[] hashedValue = sha3(concatenatedArrayEncodings);
encTypes.add("bytes32");
encValues.add(hashedValue);
}

byte[] concatenatedArrayEncodings = concatenatedArrayEncodingBuffer.toByteArray();
byte[] hashedValue = sha3(concatenatedArrayEncodings);
encTypes.add("bytes32");
encValues.add(hashedValue);
} else if (field.getType().startsWith("uint") || field.getType().startsWith("int")) {
encTypes.add(field.getType());
// convert to BigInteger for ABI constructor compatibility
try {
encValues.add(convertToBigInt(value));
} catch (NumberFormatException | NullPointerException e) {
encValues.add(
value); // value null or failed to convert, fallback to add string as
// before
encValues.add(value);
}
} else {
encTypes.add(field.getType());
encValues.add(value);
}
}

// Write all data
ByteArrayOutputStream baos = new ByteArrayOutputStream();

// Write header (static data and offsets)
for (int i = 0; i < encTypes.size(); i++) {
Class<Type> typeClazz = (Class<Type>) AbiTypes.getType(encTypes.get(i));
String type = encTypes.get(i);
Object value = encValues.get(i);

boolean atleastOneConstructorExistsForGivenParametersType = false;
// Using the Reflection API to get the types of the parameters
Constructor[] constructors = typeClazz.getConstructors();
for (Constructor constructor : constructors) {
// Check which constructor matches
try {
Class[] parameterTypes = constructor.getParameterTypes();
byte[] temp =
Numeric.hexStringToByteArray(
TypeEncoder.encode(
typeClazz
.getDeclaredConstructor(parameterTypes)
.newInstance(encValues.get(i))));
baos.write(temp, 0, temp.length);
atleastOneConstructorExistsForGivenParametersType = true;
break;
} catch (IllegalArgumentException
| NoSuchMethodException
| InstantiationException
| IllegalAccessException
| InvocationTargetException ignored) {
if (type.equals("bytes32")) {
if (value instanceof byte[]) {
baos.write((byte[]) value, 0, ((byte[]) value).length);
} else {
throw new RuntimeException("Expected byte[] for bytes32 type");
}
} else {
Class<Type> typeClazz = (Class<Type>) AbiTypes.getType(type);
Constructor[] constructors = typeClazz.getConstructors();
boolean encoded = false;

for (Constructor constructor : constructors) {
try {
Class[] parameterTypes = constructor.getParameterTypes();
byte[] temp =
Numeric.hexStringToByteArray(
TypeEncoder.encode(
typeClazz
.getDeclaredConstructor(parameterTypes)
.newInstance(value)));
baos.write(temp, 0, temp.length);
encoded = true;
break;
} catch (Exception ignored) {
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

this should be handled

}
}
}

if (!atleastOneConstructorExistsForGivenParametersType) {
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

please keep the same old check, it gives more context than "decoded"

throw new RuntimeException(
String.format(
"Received an invalid argument for which no constructor"
+ " exists for the ABI Class %s",
typeClazz.getSimpleName()));
if (!encoded) {
throw new RuntimeException("Failed to encode parameter");
}
}
}

// Write dynamic data
for (byte[] dynamicBytes : dynamicData) {
baos.write(dynamicBytes, 0, dynamicBytes.length);
}

return baos.toByteArray();
}

Expand Down
100 changes: 100 additions & 0 deletions crypto/src/test/java/org/web3j/crypto/StructuredDataEncoderTest.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,100 @@
/*
* Copyright 2025 Web3 Labs Ltd.
*
* Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with
* the License. You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on
* an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the
* specific language governing permissions and limitations under the License.
*/
package org.web3j.crypto;

import java.io.ByteArrayOutputStream;
import java.util.HashMap;

import org.junit.jupiter.api.Test;

import org.web3j.utils.Numeric;

import static org.junit.jupiter.api.Assertions.*;

public class StructuredDataEncoderTest {

@Test
public void testCustomEncodeData() throws Exception {

// json

String jsonMessage =
"""
{
"domain" : {
"chainId" : "84532",
"name" : "Test",
"verifyingContract" : "0xe483dea6aa7d3831173379d81e5c08874f1042e7",
"version" : "1.0.0"
},
"message" : {
"amounts" : [ 0, 0 ],
"to" : "0xe483dea6aa7d3831173379d81e5c08874f1042e7",
"tokenIds" : [ 0, 1 ],
"validityEndTimestamp" : 1743005854,
"validityStartTimestamp" : 1742919454,
"salt" : "82"
},
"primaryType" : "ClaimRequest",
"types" : {
"ClaimRequest" : [ {
"name" : "to",
"type" : "address"
}, {
"name" : "tokenIds",
"type" : "uint256[]"
}, {
"name" : "amounts",
"type" : "uint256[]"
}, {
"name" : "validityStartTimestamp",
"type" : "uint128"
}, {
"name" : "validityEndTimestamp",
"type" : "uint128"
}, {
"name" : "salt",
"type" : "uint256"
} ],
"EIP712Domain" : [ {
"name" : "name",
"type" : "string"
}, {
"name" : "version",
"type" : "string"
}, {
"name" : "chainId",
"type" : "uint256"
}, {
"name" : "verifyingContract",
"type" : "address"
} ]
}
}
""";

System.out.println("JSON Message: " + jsonMessage);

// struct instance
StructuredDataEncoder encoder = new StructuredDataEncoder(jsonMessage);
encoder.hashStructuredData();
ByteArrayOutputStream baos = new ByteArrayOutputStream();

byte[] dataHash =
encoder.encodeData(
encoder.jsonMessageObject.getPrimaryType(),
(HashMap<String, Object>) encoder.jsonMessageObject.getMessage());
System.out.println("################ hash is " + Numeric.toHexString(dataHash));
baos.write(dataHash, 0, dataHash.length);
Comment on lines +86 to +98
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This has nothing to do with a unit test. Please create a proper test smart contract abi and assert the expected output of the encode result

}
}