Skip to content

Commit ef3dad8

Browse files
authored
Merge pull request #2164 from Syn-McJ/fix/ens-ccip-read
fix: ENS CCIP Read improvements
2 parents 693441d + b09b366 commit ef3dad8

File tree

5 files changed

+189
-21
lines changed

5 files changed

+189
-21
lines changed

core/src/main/java/org/web3j/dto/EnsGatewayRequestDTO.java

+14
Original file line numberDiff line numberDiff line change
@@ -16,18 +16,32 @@
1616
public class EnsGatewayRequestDTO {
1717

1818
private String data;
19+
private String sender;
1920

2021
public EnsGatewayRequestDTO() {}
2122

2223
public EnsGatewayRequestDTO(String data) {
2324
this.data = data;
2425
}
2526

27+
public EnsGatewayRequestDTO(String data, String sender) {
28+
this.data = data;
29+
this.sender = sender;
30+
}
31+
2632
public String getData() {
2733
return data;
2834
}
2935

3036
public void setData(String data) {
3137
this.data = data;
3238
}
39+
40+
public String getSender() {
41+
return sender;
42+
}
43+
44+
public void setSender(String sender) {
45+
this.sender = sender;
46+
}
3347
}

core/src/main/java/org/web3j/ens/EnsResolver.java

+25-10
Original file line numberDiff line numberDiff line change
@@ -30,7 +30,10 @@
3030
import org.slf4j.Logger;
3131
import org.slf4j.LoggerFactory;
3232

33+
import org.web3j.abi.DefaultFunctionEncoder;
3334
import org.web3j.abi.DefaultFunctionReturnDecoder;
35+
import org.web3j.abi.datatypes.DynamicBytes;
36+
import org.web3j.abi.datatypes.Type;
3437
import org.web3j.abi.datatypes.ens.OffchainLookup;
3538
import org.web3j.crypto.Credentials;
3639
import org.web3j.crypto.Keys;
@@ -259,12 +262,16 @@ protected String resolveOffchain(
259262
ObjectMapper objectMapper = ObjectMapperFactory.getObjectMapper();
260263
EnsGatewayResponseDTO gatewayResponseDTO =
261264
objectMapper.readValue(gatewayResult, EnsGatewayResponseDTO.class);
265+
String callbackSelector = Numeric.toHexString(offchainLookup.getCallbackFunction());
266+
List<Type> parameters =
267+
Arrays.asList(
268+
new DynamicBytes(
269+
Numeric.hexStringToByteArray(gatewayResponseDTO.getData())),
270+
new DynamicBytes(offchainLookup.getExtraData()));
262271

263-
String resolvedNameHex =
264-
resolver.resolveWithProof(
265-
Numeric.hexStringToByteArray(gatewayResponseDTO.getData()),
266-
offchainLookup.getExtraData())
267-
.send();
272+
String encodedParams = new DefaultFunctionEncoder().encodeParameters(parameters);
273+
String encodedFunction = callbackSelector + encodedParams;
274+
String resolvedNameHex = resolver.executeCallWithoutDecoding(encodedFunction);
268275

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

351355
// URL expansion
352-
String href = url.replace("{sender}", sender).replace("{data}", data);
356+
String href = url;
357+
358+
if (url.contains("{sender}")) {
359+
href = href.replace("{sender}", sender);
360+
}
361+
362+
if (url.contains("{data}")) {
363+
href = href.replace("{data}", data);
364+
}
353365

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

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

362377
return builder.post(RequestBody.create(om.writeValueAsString(requestDTO), JSON))

core/src/main/java/org/web3j/utils/EnsUtils.java

+2-1
Original file line numberDiff line numberDiff line change
@@ -29,7 +29,8 @@ public static boolean isEIP3668(String data) {
2929
return false;
3030
}
3131

32-
return EnsUtils.EIP_3668_CCIP_INTERFACE_ID.equals(data.substring(0, 10));
32+
return EnsUtils.EIP_3668_CCIP_INTERFACE_ID.equals(
33+
Numeric.removeDoubleQuotes(data).substring(0, 10));
3334
}
3435

3536
public static String getParent(String url) {

core/src/test/java/org/web3j/ens/EnsResolverTest.java

+126-10
Original file line numberDiff line numberDiff line change
@@ -28,6 +28,7 @@
2828
import org.junit.jupiter.api.BeforeAll;
2929
import org.junit.jupiter.api.BeforeEach;
3030
import org.junit.jupiter.api.Test;
31+
import org.mockito.ArgumentCaptor;
3132

3233
import org.web3j.abi.TypeEncoder;
3334
import org.web3j.abi.datatypes.Utf8String;
@@ -281,13 +282,60 @@ void buildRequestWhenNotValidSenderTest() {
281282
}
282283

283284
@Test
284-
void buildRequestWhenNotValidUrl() {
285+
void buildRequestWithDataParameterOnly() throws Exception {
285286
String url = "https://example.com/gateway/{data}.json";
286287
sender = "0x226159d592E2b063810a10Ebf6dcbADA94Ed68b8";
287288
data = "0xd5fa2b00";
288289

289-
assertThrows(
290-
EnsResolutionException.class, () -> ensResolver.buildRequest(url, sender, data));
290+
okhttp3.Request request = ensResolver.buildRequest(url, sender, data);
291+
292+
assertEquals("https://example.com/gateway/0xd5fa2b00.json", request.url().toString());
293+
assertEquals("GET", request.method());
294+
assertNull(request.body());
295+
}
296+
297+
@Test
298+
void buildRequestWithSenderParameterOnly() throws Exception {
299+
String url = "https://example.com/gateway/{sender}/lookup";
300+
sender = "0x226159d592E2b063810a10Ebf6dcbADA94Ed68b8";
301+
data = "0xd5fa2b00";
302+
303+
okhttp3.Request request = ensResolver.buildRequest(url, sender, data);
304+
305+
assertEquals(
306+
"https://example.com/gateway/0x226159d592E2b063810a10Ebf6dcbADA94Ed68b8/lookup",
307+
request.url().toString());
308+
assertEquals("POST", request.method());
309+
assertNotNull(request.body());
310+
assertEquals("application/json", request.header("Content-Type"));
311+
}
312+
313+
@Test
314+
void verifyPostRequestBodyContainsSenderAndData() throws Exception {
315+
String url = "https://example.com/gateway/lookup";
316+
sender = "0x226159d592E2b063810a10Ebf6dcbADA94Ed68b8";
317+
data = "0xd5fa2b00";
318+
319+
okhttp3.Request request = ensResolver.buildRequest(url, sender, data);
320+
assertNotNull(request.body());
321+
okhttp3.MediaType contentType = request.body().contentType();
322+
assertNotNull(contentType);
323+
assertTrue(contentType.toString().startsWith("application/json"));
324+
}
325+
326+
@Test
327+
void buildRequestWithBothParameters() throws Exception {
328+
String url = "https://example.com/gateway/{sender}/{data}";
329+
sender = "0x226159d592E2b063810a10Ebf6dcbADA94Ed68b8";
330+
data = "0xd5fa2b00";
331+
332+
okhttp3.Request request = ensResolver.buildRequest(url, sender, data);
333+
334+
assertEquals(
335+
"https://example.com/gateway/0x226159d592E2b063810a10Ebf6dcbADA94Ed68b8/0xd5fa2b00",
336+
request.url().toString());
337+
assertEquals("GET", request.method());
338+
assertNull(request.body());
291339
}
292340

293341
@Test
@@ -420,9 +468,7 @@ void resolveOffchainSuccess() throws Exception {
420468
when(httpClientMock.newCall(any())).thenReturn(call);
421469
when(call.execute()).thenReturn(responseObj);
422470

423-
RemoteFunctionCall respWithProof = mock(RemoteFunctionCall.class);
424-
when(resolver.resolveWithProof(any(), any())).thenReturn(respWithProof);
425-
when(respWithProof.send()).thenReturn(RESOLVED_NAME_HEX);
471+
when(resolver.executeCallWithoutDecoding(any())).thenReturn(RESOLVED_NAME_HEX);
426472

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

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

450-
RemoteFunctionCall respWithProof = mock(RemoteFunctionCall.class);
451-
when(resolver.resolveWithProof(any(), any()))
452-
.thenReturn(respWithProof, respWithProof, respWithProof);
453496
String eip3668Data = EnsUtils.EIP_3668_CCIP_INTERFACE_ID + "data";
454-
when(respWithProof.send()).thenReturn(eip3668Data, eip3668Data, eip3668Data);
497+
when(resolver.executeCallWithoutDecoding(any()))
498+
.thenReturn(eip3668Data, eip3668Data, eip3668Data);
455499

456500
assertThrows(
457501
EnsResolutionException.class,
458502
() -> ensResolver.resolveOffchain(LOOKUP_HEX, resolver, 2));
459503
}
460504

505+
@Test
506+
void resolveOffchainWithDynamicCallback() throws Exception {
507+
OffchainResolverContract resolver = mock(OffchainResolverContract.class);
508+
when(resolver.getContractAddress())
509+
.thenReturn("0xc1735677a60884abbcf72295e88d47764beda282");
510+
511+
OkHttpClient httpClientMock = mock(OkHttpClient.class);
512+
Call call = mock(Call.class);
513+
okhttp3.Response responseObj = buildResponse(200, urls.get(0), sender, data);
514+
ensResolver.setHttpClient(httpClientMock);
515+
when(httpClientMock.newCall(any())).thenReturn(call);
516+
when(call.execute()).thenReturn(responseObj);
517+
518+
// Create a custom LOOKUP_HEX with a different callback function
519+
String customCallbackSelector = "aabbccdd";
520+
String customLookupHex =
521+
LOOKUP_HEX.substring(0, 202) + customCallbackSelector + LOOKUP_HEX.substring(210);
522+
523+
// Capture the function call to verify the correct callback selector is used
524+
ArgumentCaptor<String> functionCallCaptor = ArgumentCaptor.forClass(String.class);
525+
when(resolver.executeCallWithoutDecoding(functionCallCaptor.capture()))
526+
.thenReturn(RESOLVED_NAME_HEX);
527+
528+
String result = ensResolver.resolveOffchain(customLookupHex, resolver, 4);
529+
assertEquals("0x41563129cdbbd0c5d3e1c86cf9563926b243834d", result);
530+
531+
// Verify that the captured function call starts with our custom callback selector
532+
String capturedFunctionCall = functionCallCaptor.getValue();
533+
assertTrue(
534+
capturedFunctionCall.startsWith("0x" + customCallbackSelector),
535+
"Function call should start with the custom callback selector");
536+
}
537+
538+
@Test
539+
void resolveOffchainParameterEncoding() throws Exception {
540+
OffchainResolverContract resolver = mock(OffchainResolverContract.class);
541+
when(resolver.getContractAddress())
542+
.thenReturn("0xc1735677a60884abbcf72295e88d47764beda282");
543+
544+
OkHttpClient httpClientMock = mock(OkHttpClient.class);
545+
Call call = mock(Call.class);
546+
String testData = "0xabcdef";
547+
548+
EnsGatewayResponseDTO responseDTO = new EnsGatewayResponseDTO(testData);
549+
String responseJson = om.writeValueAsString(responseDTO);
550+
551+
okhttp3.Response responseObj =
552+
new okhttp3.Response.Builder()
553+
.request(new okhttp3.Request.Builder().url(urls.get(0)).build())
554+
.protocol(Protocol.HTTP_2)
555+
.code(200)
556+
.body(ResponseBody.create(responseJson, JSON_MEDIA_TYPE))
557+
.message("OK")
558+
.build();
559+
560+
ensResolver.setHttpClient(httpClientMock);
561+
when(httpClientMock.newCall(any())).thenReturn(call);
562+
when(call.execute()).thenReturn(responseObj);
563+
564+
ArgumentCaptor<String> functionCallCaptor = ArgumentCaptor.forClass(String.class);
565+
when(resolver.executeCallWithoutDecoding(functionCallCaptor.capture()))
566+
.thenReturn(RESOLVED_NAME_HEX);
567+
568+
String result = ensResolver.resolveOffchain(LOOKUP_HEX, resolver, 4);
569+
assertEquals("0x41563129cdbbd0c5d3e1c86cf9563926b243834d", result);
570+
571+
String capturedFunctionCall = functionCallCaptor.getValue();
572+
assertTrue(
573+
capturedFunctionCall.contains(testData.substring(2)),
574+
"Function call should contain the encoded test data");
575+
}
576+
461577
class EnsResolverForTest extends EnsResolver {
462578
private OffchainResolverContract resolverMock;
463579

core/src/test/java/org/web3j/utils/EnsUtilsTest.java

+22
Original file line numberDiff line numberDiff line change
@@ -56,4 +56,26 @@ void getParentWhenUrlNullOrEmpty() {
5656
void getParentWhenUrlWithoutParent() {
5757
assertNull(EnsUtils.getParent("parent"));
5858
}
59+
60+
@Test
61+
public void testIsEIP3668() {
62+
// Valid EIP3668 data
63+
String validData = "0x556f1830abcdef1234567890";
64+
assertTrue(EnsUtils.isEIP3668(validData));
65+
66+
String validDataWithQuotes = "\"0x556f1830abcdef1234567890\"";
67+
assertTrue(EnsUtils.isEIP3668(validDataWithQuotes));
68+
69+
// Invalid EIP3668 data - different interface ID
70+
String invalidData = "0x123456789abcdef1234567890";
71+
assertFalse(EnsUtils.isEIP3668(invalidData));
72+
73+
String invalidDataWithQuotes = "\"0x123456789abcdef1234567890\"";
74+
assertFalse(EnsUtils.isEIP3668(invalidDataWithQuotes));
75+
76+
// Edge cases
77+
assertFalse(EnsUtils.isEIP3668(null));
78+
assertFalse(EnsUtils.isEIP3668(""));
79+
assertFalse(EnsUtils.isEIP3668("0x123"));
80+
}
5981
}

0 commit comments

Comments
 (0)