Skip to content

Commit 548700e

Browse files
authored
Merge pull request #17 from unicitynetwork/aggregator-subscription-support
Add support for aggregator API keys
2 parents 9215a13 + 2258d56 commit 548700e

5 files changed

Lines changed: 338 additions & 6 deletions

File tree

build.gradle.kts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -45,12 +45,14 @@ dependencies {
4545
// Testing
4646
testImplementation(platform("org.junit:junit-bom:5.10.2"))
4747
testImplementation("org.junit.jupiter:junit-jupiter")
48+
testRuntimeOnly("org.junit.platform:junit-platform-launcher")
4849
testImplementation("org.testcontainers:testcontainers:1.19.8")
4950
testImplementation("org.testcontainers:junit-jupiter:1.19.8")
5051
testImplementation("org.testcontainers:mongodb:1.19.8")
5152
testImplementation("org.awaitility:awaitility:4.2.0")
5253
testImplementation("org.slf4j:slf4j-simple:2.0.13")
5354
testImplementation("com.google.guava:guava:33.0.0-jre")
55+
testImplementation("com.squareup.okhttp3:mockwebserver:4.12.0")
5456

5557
// ✅ Cucumber for BDD
5658
testImplementation("io.cucumber:cucumber-java:7.27.2")

src/main/java/org/unicitylabs/sdk/api/AggregatorClient.java

Lines changed: 16 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,16 +1,27 @@
11
package org.unicitylabs.sdk.api;
22

33
import java.util.Collections;
4+
import java.util.LinkedHashMap;
5+
import java.util.List;
6+
import java.util.Map;
47
import java.util.concurrent.CompletableFuture;
58
import org.unicitylabs.sdk.hash.DataHash;
69
import org.unicitylabs.sdk.jsonrpc.JsonRpcHttpTransport;
710

11+
import static com.google.common.net.HttpHeaders.AUTHORIZATION;
12+
813
public class AggregatorClient implements IAggregatorClient {
914

1015
private final JsonRpcHttpTransport transport;
16+
private final String apiKey;
1117

1218
public AggregatorClient(String url) {
19+
this(url, null);
20+
}
21+
22+
public AggregatorClient(String url, String apiKey) {
1323
this.transport = new JsonRpcHttpTransport(url);
24+
this.apiKey = apiKey;
1425
}
1526

1627
public CompletableFuture<SubmitCommitmentResponse> submitCommitment(
@@ -20,7 +31,11 @@ public CompletableFuture<SubmitCommitmentResponse> submitCommitment(
2031

2132
SubmitCommitmentRequest request = new SubmitCommitmentRequest(requestId, transactionHash,
2233
authenticator, false);
23-
return this.transport.request("submit_commitment", request, SubmitCommitmentResponse.class);
34+
Map<String, List<String>> headers = new LinkedHashMap<>();
35+
if (apiKey != null) {
36+
headers.put(AUTHORIZATION, Collections.singletonList("Bearer " + apiKey));
37+
}
38+
return this.transport.request("submit_commitment", request, SubmitCommitmentResponse.class, headers);
2439
}
2540

2641
public CompletableFuture<InclusionProofResponse> getInclusionProof(RequestId requestId) {

src/main/java/org/unicitylabs/sdk/jsonrpc/JsonRpcHttpTransport.java

Lines changed: 19 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,8 @@
22
package org.unicitylabs.sdk.jsonrpc;
33

44
import java.io.IOException;
5+
import java.util.List;
6+
import java.util.Map;
57
import java.util.concurrent.CompletableFuture;
68
import okhttp3.Call;
79
import okhttp3.Callback;
@@ -18,9 +20,9 @@
1820
*/
1921
public class JsonRpcHttpTransport {
2022

21-
private static final MediaType MEDIA_TYPE_JSON = MediaType.get("application/json; charset=utf-8");
23+
private static final MediaType MEDIA_TYPE_JSON = MediaType.get("application/json; charset=utf-8");
2224

23-
private final String url;
25+
private final String url;
2426
private final OkHttpClient httpClient;
2527

2628
/**
@@ -35,17 +37,29 @@ public JsonRpcHttpTransport(String url) {
3537
* Send a JSON-RPC request.
3638
*/
3739
public <T> CompletableFuture<T> request(String method, Object params, Class<T> resultType) {
40+
return request(method, params, resultType, Map.of());
41+
}
42+
43+
/**
44+
* Send a JSON-RPC request with optional API key.
45+
*/
46+
public <T> CompletableFuture<T> request(String method, Object params, Class<T> resultType, Map<String, List<String>> headers) {
3847
CompletableFuture<T> future = new CompletableFuture<>();
3948

4049
try {
41-
Request request = new Request.Builder()
50+
Request.Builder requestBuilder = new Request.Builder()
4251
.url(this.url)
4352
.post(
4453
RequestBody.create(
4554
UnicityObjectMapper.JSON.writeValueAsString(new JsonRpcRequest(method, params)),
4655
JsonRpcHttpTransport.MEDIA_TYPE_JSON)
47-
)
48-
.build();
56+
);
57+
58+
headers.forEach((header, values) ->
59+
values.forEach(value ->
60+
requestBuilder.addHeader(header, value)));
61+
62+
Request request = requestBuilder.build();
4963

5064
this.httpClient.newCall(request).enqueue(new Callback() {
5165
@Override
Lines changed: 157 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,157 @@
1+
package org.unicitylabs.sdk;
2+
3+
import com.fasterxml.jackson.core.JsonProcessingException;
4+
import okhttp3.mockwebserver.Dispatcher;
5+
import okhttp3.mockwebserver.MockResponse;
6+
import okhttp3.mockwebserver.MockWebServer;
7+
import okhttp3.mockwebserver.RecordedRequest;
8+
import com.fasterxml.jackson.databind.ObjectMapper;
9+
import com.fasterxml.jackson.databind.JsonNode;
10+
import org.jetbrains.annotations.Nullable;
11+
12+
import java.io.IOException;
13+
import java.util.Set;
14+
import java.util.HashSet;
15+
import java.util.UUID;
16+
17+
public class MockAggregatorServer {
18+
19+
private final MockWebServer server;
20+
private final ObjectMapper objectMapper;
21+
private final Set<String> protectedMethods;
22+
private volatile boolean simulateRateLimit = false;
23+
private volatile int rateLimitRetryAfter = 0;
24+
private volatile String expectedApiKey = null;
25+
26+
public MockAggregatorServer() {
27+
this.server = new MockWebServer();
28+
this.objectMapper = new ObjectMapper();
29+
this.protectedMethods = new HashSet<>();
30+
this.protectedMethods.add("submit_commitment");
31+
32+
server.setDispatcher(new Dispatcher() {
33+
@Override
34+
public MockResponse dispatch(RecordedRequest request) {
35+
return handleRequest(request);
36+
}
37+
});
38+
}
39+
40+
public void start() throws IOException {
41+
server.start();
42+
}
43+
44+
public void shutdown() throws IOException {
45+
server.shutdown();
46+
}
47+
48+
public String getUrl() {
49+
return server.url("/").toString();
50+
}
51+
52+
public RecordedRequest takeRequest() throws InterruptedException {
53+
return server.takeRequest();
54+
}
55+
56+
public void simulateRateLimitForNextRequest(int retryAfterSeconds) {
57+
this.simulateRateLimit = true;
58+
this.rateLimitRetryAfter = retryAfterSeconds;
59+
}
60+
61+
public void setExpectedApiKey(String apiKey) {
62+
this.expectedApiKey = apiKey;
63+
}
64+
65+
private MockResponse handleRequest(RecordedRequest request) {
66+
try {
67+
if (simulateRateLimit) {
68+
try {
69+
return new MockResponse()
70+
.setResponseCode(429)
71+
.setHeader("Retry-After", String.valueOf(rateLimitRetryAfter))
72+
.setBody("Too Many Requests");
73+
} finally {
74+
// Reset for next request
75+
simulateRateLimit = false;
76+
rateLimitRetryAfter = 0;
77+
}
78+
}
79+
80+
String method = extractJsonRpcMethod(request);
81+
82+
if (protectedMethods.contains(method) && expectedApiKey != null && !hasValidApiKey(request)) {
83+
return new MockResponse()
84+
.setResponseCode(401)
85+
.setHeader("WWW-Authenticate", "Bearer")
86+
.setBody("Unauthorized");
87+
}
88+
89+
return generateSuccessResponse(method);
90+
91+
} catch (Exception e) {
92+
return new MockResponse()
93+
.setResponseCode(400)
94+
.setBody("Bad Request");
95+
}
96+
}
97+
98+
private boolean hasValidApiKey(RecordedRequest request) {
99+
String authHeader = request.getHeader("Authorization");
100+
if (authHeader != null && authHeader.startsWith("Bearer ")) {
101+
String providedKey = authHeader.substring(7);
102+
return expectedApiKey.equals(providedKey);
103+
}
104+
return false;
105+
}
106+
107+
private @Nullable String extractJsonRpcMethod(RecordedRequest request) throws JsonProcessingException {
108+
if (!"POST".equals(request.getMethod())) {
109+
return null;
110+
}
111+
JsonNode jsonRequest = objectMapper.readTree(request.getBody().readUtf8());
112+
return jsonRequest.has("method") ? jsonRequest.get("method").asText() : null;
113+
}
114+
115+
private MockResponse generateSuccessResponse(String method) {
116+
String responseBody;
117+
String id = UUID.randomUUID().toString();
118+
119+
switch (method != null ? method : "") {
120+
case "submit_commitment":
121+
responseBody = String.format(
122+
"{\n" +
123+
" \"jsonrpc\": \"2.0\",\n" +
124+
" \"result\": {\n" +
125+
" \"status\": \"SUCCESS\"\n" +
126+
" },\n" +
127+
" \"id\": \"%s\"\n" +
128+
"}", id);
129+
break;
130+
131+
case "get_block_height":
132+
responseBody = String.format(
133+
"{\n" +
134+
" \"jsonrpc\": \"2.0\",\n" +
135+
" \"result\": {\n" +
136+
" \"blockNumber\": \"67890\"\n" +
137+
" },\n" +
138+
" \"id\": \"%s\"\n" +
139+
"}", id);
140+
break;
141+
142+
default:
143+
responseBody = String.format(
144+
"{\n" +
145+
" \"jsonrpc\": \"2.0\",\n" +
146+
" \"result\": \"OK\",\n" +
147+
" \"id\": \"%s\"\n" +
148+
"}", id);
149+
break;
150+
}
151+
152+
return new MockResponse()
153+
.setResponseCode(200)
154+
.setHeader("Content-Type", "application/json")
155+
.setBody(responseBody);
156+
}
157+
}

0 commit comments

Comments
 (0)