Skip to content
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
14 changes: 14 additions & 0 deletions core/src/main/java/org/web3j/dto/EnsGatewayRequestDTO.java
Original file line number Diff line number Diff line change
Expand Up @@ -16,18 +16,32 @@
public class EnsGatewayRequestDTO {

private String data;
private String sender;

public EnsGatewayRequestDTO() {}

public EnsGatewayRequestDTO(String data) {
this.data = data;
}

public EnsGatewayRequestDTO(String data, String sender) {
this.data = data;
this.sender = sender;
}

public String getData() {
return data;
}

public void setData(String data) {
this.data = data;
}

public String getSender() {
return sender;
}

public void setSender(String sender) {
this.sender = sender;
}
}
35 changes: 25 additions & 10 deletions core/src/main/java/org/web3j/ens/EnsResolver.java
Original file line number Diff line number Diff line change
Expand Up @@ -30,7 +30,10 @@
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

import org.web3j.abi.DefaultFunctionEncoder;
import org.web3j.abi.DefaultFunctionReturnDecoder;
import org.web3j.abi.datatypes.DynamicBytes;
import org.web3j.abi.datatypes.Type;
import org.web3j.abi.datatypes.ens.OffchainLookup;
import org.web3j.crypto.Credentials;
import org.web3j.crypto.Keys;
Expand Down Expand Up @@ -259,12 +262,16 @@ protected String resolveOffchain(
ObjectMapper objectMapper = ObjectMapperFactory.getObjectMapper();
EnsGatewayResponseDTO gatewayResponseDTO =
objectMapper.readValue(gatewayResult, EnsGatewayResponseDTO.class);
String callbackSelector = Numeric.toHexString(offchainLookup.getCallbackFunction());
List<Type> parameters =
Arrays.asList(
new DynamicBytes(
Numeric.hexStringToByteArray(gatewayResponseDTO.getData())),
new DynamicBytes(offchainLookup.getExtraData()));

String resolvedNameHex =
resolver.resolveWithProof(
Numeric.hexStringToByteArray(gatewayResponseDTO.getData()),
offchainLookup.getExtraData())
.send();
String encodedParams = new DefaultFunctionEncoder().encodeParameters(parameters);
String encodedFunction = callbackSelector + encodedParams;
String resolvedNameHex = resolver.executeCallWithoutDecoding(encodedFunction);

// This protocol can result in multiple lookups being requested by the same contract.
if (EnsUtils.isEIP3668(resolvedNameHex)) {
Expand Down Expand Up @@ -344,19 +351,27 @@ protected Request buildRequest(String url, String sender, String data)
if (data == null) {
throw new EnsResolutionException("Data is null");
}
if (!url.contains("{sender}")) {
throw new EnsResolutionException("Url is not valid, sender parameter is not exist");
}

// URL expansion
String href = url.replace("{sender}", sender).replace("{data}", data);
String href = url;

if (url.contains("{sender}")) {
href = href.replace("{sender}", sender);
}

if (url.contains("{data}")) {
href = href.replace("{data}", data);
}

Request.Builder builder = new Request.Builder().url(href);

// According to ERC-3668:
// - If URL contains {data}, use GET
// - Otherwise, use POST with JSON payload containing data and sender
if (url.contains("{data}")) {
return builder.get().build();
} else {
EnsGatewayRequestDTO requestDTO = new EnsGatewayRequestDTO(data);
EnsGatewayRequestDTO requestDTO = new EnsGatewayRequestDTO(data, sender);
ObjectMapper om = ObjectMapperFactory.getObjectMapper();

return builder.post(RequestBody.create(om.writeValueAsString(requestDTO), JSON))
Expand Down
3 changes: 2 additions & 1 deletion core/src/main/java/org/web3j/utils/EnsUtils.java
Original file line number Diff line number Diff line change
Expand Up @@ -29,7 +29,8 @@ public static boolean isEIP3668(String data) {
return false;
}

return EnsUtils.EIP_3668_CCIP_INTERFACE_ID.equals(data.substring(0, 10));
return EnsUtils.EIP_3668_CCIP_INTERFACE_ID.equals(
Numeric.removeDoubleQuotes(data).substring(0, 10));
}

public static String getParent(String url) {
Expand Down
136 changes: 126 additions & 10 deletions core/src/test/java/org/web3j/ens/EnsResolverTest.java
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,7 @@
import org.junit.jupiter.api.BeforeAll;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;
import org.mockito.ArgumentCaptor;

import org.web3j.abi.TypeEncoder;
import org.web3j.abi.datatypes.Utf8String;
Expand Down Expand Up @@ -281,13 +282,60 @@ void buildRequestWhenNotValidSenderTest() {
}

@Test
void buildRequestWhenNotValidUrl() {
void buildRequestWithDataParameterOnly() throws Exception {
String url = "https://example.com/gateway/{data}.json";
sender = "0x226159d592E2b063810a10Ebf6dcbADA94Ed68b8";
data = "0xd5fa2b00";

assertThrows(
EnsResolutionException.class, () -> ensResolver.buildRequest(url, sender, data));
okhttp3.Request request = ensResolver.buildRequest(url, sender, data);

assertEquals("https://example.com/gateway/0xd5fa2b00.json", request.url().toString());
assertEquals("GET", request.method());
assertNull(request.body());
}

@Test
void buildRequestWithSenderParameterOnly() throws Exception {
String url = "https://example.com/gateway/{sender}/lookup";
sender = "0x226159d592E2b063810a10Ebf6dcbADA94Ed68b8";
data = "0xd5fa2b00";

okhttp3.Request request = ensResolver.buildRequest(url, sender, data);

assertEquals(
"https://example.com/gateway/0x226159d592E2b063810a10Ebf6dcbADA94Ed68b8/lookup",
request.url().toString());
assertEquals("POST", request.method());
assertNotNull(request.body());
assertEquals("application/json", request.header("Content-Type"));
}

@Test
void verifyPostRequestBodyContainsSenderAndData() throws Exception {
String url = "https://example.com/gateway/lookup";
sender = "0x226159d592E2b063810a10Ebf6dcbADA94Ed68b8";
data = "0xd5fa2b00";

okhttp3.Request request = ensResolver.buildRequest(url, sender, data);
assertNotNull(request.body());
okhttp3.MediaType contentType = request.body().contentType();
assertNotNull(contentType);
assertTrue(contentType.toString().startsWith("application/json"));
}

@Test
void buildRequestWithBothParameters() throws Exception {
String url = "https://example.com/gateway/{sender}/{data}";
sender = "0x226159d592E2b063810a10Ebf6dcbADA94Ed68b8";
data = "0xd5fa2b00";

okhttp3.Request request = ensResolver.buildRequest(url, sender, data);

assertEquals(
"https://example.com/gateway/0x226159d592E2b063810a10Ebf6dcbADA94Ed68b8/0xd5fa2b00",
request.url().toString());
assertEquals("GET", request.method());
assertNull(request.body());
}

@Test
Expand Down Expand Up @@ -420,9 +468,7 @@ void resolveOffchainSuccess() throws Exception {
when(httpClientMock.newCall(any())).thenReturn(call);
when(call.execute()).thenReturn(responseObj);

RemoteFunctionCall respWithProof = mock(RemoteFunctionCall.class);
when(resolver.resolveWithProof(any(), any())).thenReturn(respWithProof);
when(respWithProof.send()).thenReturn(RESOLVED_NAME_HEX);
when(resolver.executeCallWithoutDecoding(any())).thenReturn(RESOLVED_NAME_HEX);

String result = ensResolver.resolveOffchain(LOOKUP_HEX, resolver, 4);

Expand All @@ -447,17 +493,87 @@ void resolveOffchainWhenLookUpCallsOutOfLimit() throws Exception {
buildResponse(200, urls.get(0), sender, data),
buildResponse(200, urls.get(0), sender, data));

RemoteFunctionCall respWithProof = mock(RemoteFunctionCall.class);
when(resolver.resolveWithProof(any(), any()))
.thenReturn(respWithProof, respWithProof, respWithProof);
String eip3668Data = EnsUtils.EIP_3668_CCIP_INTERFACE_ID + "data";
when(respWithProof.send()).thenReturn(eip3668Data, eip3668Data, eip3668Data);
when(resolver.executeCallWithoutDecoding(any()))
.thenReturn(eip3668Data, eip3668Data, eip3668Data);

assertThrows(
EnsResolutionException.class,
() -> ensResolver.resolveOffchain(LOOKUP_HEX, resolver, 2));
}

@Test
void resolveOffchainWithDynamicCallback() throws Exception {
OffchainResolverContract resolver = mock(OffchainResolverContract.class);
when(resolver.getContractAddress())
.thenReturn("0xc1735677a60884abbcf72295e88d47764beda282");

OkHttpClient httpClientMock = mock(OkHttpClient.class);
Call call = mock(Call.class);
okhttp3.Response responseObj = buildResponse(200, urls.get(0), sender, data);
ensResolver.setHttpClient(httpClientMock);
when(httpClientMock.newCall(any())).thenReturn(call);
when(call.execute()).thenReturn(responseObj);

// Create a custom LOOKUP_HEX with a different callback function
String customCallbackSelector = "aabbccdd";
String customLookupHex =
LOOKUP_HEX.substring(0, 202) + customCallbackSelector + LOOKUP_HEX.substring(210);

// Capture the function call to verify the correct callback selector is used
ArgumentCaptor<String> functionCallCaptor = ArgumentCaptor.forClass(String.class);
when(resolver.executeCallWithoutDecoding(functionCallCaptor.capture()))
.thenReturn(RESOLVED_NAME_HEX);

String result = ensResolver.resolveOffchain(customLookupHex, resolver, 4);
assertEquals("0x41563129cdbbd0c5d3e1c86cf9563926b243834d", result);

// Verify that the captured function call starts with our custom callback selector
String capturedFunctionCall = functionCallCaptor.getValue();
assertTrue(
capturedFunctionCall.startsWith("0x" + customCallbackSelector),
"Function call should start with the custom callback selector");
}

@Test
void resolveOffchainParameterEncoding() throws Exception {
OffchainResolverContract resolver = mock(OffchainResolverContract.class);
when(resolver.getContractAddress())
.thenReturn("0xc1735677a60884abbcf72295e88d47764beda282");

OkHttpClient httpClientMock = mock(OkHttpClient.class);
Call call = mock(Call.class);
String testData = "0xabcdef";

EnsGatewayResponseDTO responseDTO = new EnsGatewayResponseDTO(testData);
String responseJson = om.writeValueAsString(responseDTO);

okhttp3.Response responseObj =
new okhttp3.Response.Builder()
.request(new okhttp3.Request.Builder().url(urls.get(0)).build())
.protocol(Protocol.HTTP_2)
.code(200)
.body(ResponseBody.create(responseJson, JSON_MEDIA_TYPE))
.message("OK")
.build();

ensResolver.setHttpClient(httpClientMock);
when(httpClientMock.newCall(any())).thenReturn(call);
when(call.execute()).thenReturn(responseObj);

ArgumentCaptor<String> functionCallCaptor = ArgumentCaptor.forClass(String.class);
when(resolver.executeCallWithoutDecoding(functionCallCaptor.capture()))
.thenReturn(RESOLVED_NAME_HEX);

String result = ensResolver.resolveOffchain(LOOKUP_HEX, resolver, 4);
assertEquals("0x41563129cdbbd0c5d3e1c86cf9563926b243834d", result);

String capturedFunctionCall = functionCallCaptor.getValue();
assertTrue(
capturedFunctionCall.contains(testData.substring(2)),
"Function call should contain the encoded test data");
}

class EnsResolverForTest extends EnsResolver {
private OffchainResolverContract resolverMock;

Expand Down
22 changes: 22 additions & 0 deletions core/src/test/java/org/web3j/utils/EnsUtilsTest.java
Original file line number Diff line number Diff line change
Expand Up @@ -56,4 +56,26 @@ void getParentWhenUrlNullOrEmpty() {
void getParentWhenUrlWithoutParent() {
assertNull(EnsUtils.getParent("parent"));
}

@Test
public void testIsEIP3668() {
// Valid EIP3668 data
String validData = "0x556f1830abcdef1234567890";
assertTrue(EnsUtils.isEIP3668(validData));

String validDataWithQuotes = "\"0x556f1830abcdef1234567890\"";
assertTrue(EnsUtils.isEIP3668(validDataWithQuotes));

// Invalid EIP3668 data - different interface ID
String invalidData = "0x123456789abcdef1234567890";
assertFalse(EnsUtils.isEIP3668(invalidData));

String invalidDataWithQuotes = "\"0x123456789abcdef1234567890\"";
assertFalse(EnsUtils.isEIP3668(invalidDataWithQuotes));

// Edge cases
assertFalse(EnsUtils.isEIP3668(null));
assertFalse(EnsUtils.isEIP3668(""));
assertFalse(EnsUtils.isEIP3668("0x123"));
}
}