Skip to content

Commit 44bac5a

Browse files
nkramer44sappenin
andauthored
Price Oracle Support (#537)
* Add XLS-47d Price Oracle objects & transactions * Add unit tests * Cleanup ITs to operate on all rippled & clio nodes and networks. * Update and cleanup Checkstyle --------- Co-authored-by: David Fuelling <sappenin@gmail.com>
1 parent 6658182 commit 44bac5a

File tree

67 files changed

+3908
-468
lines changed

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

67 files changed

+3908
-468
lines changed

checkstyle.xml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -205,7 +205,7 @@
205205
<property name="target" value="CLASS_DEF, INTERFACE_DEF, ENUM_DEF, METHOD_DEF, CTOR_DEF, VARIABLE_DEF"/>
206206
</module>
207207
<module name="JavadocMethod">
208-
<property name="scope" value="public"/>
208+
<property name="accessModifiers" value="public, protected"/>
209209
<property name="allowMissingParamTags" value="true"/>
210210
<property name="allowMissingReturnTag" value="true"/>
211211
<property name="allowedAnnotations" value="Override, Test"/>

pom.xml

Lines changed: 6 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -367,6 +367,9 @@
367367
</goals>
368368
</execution>
369369
</executions>
370+
<configuration>
371+
<rerunFailingTestsCount>2</rerunFailingTestsCount>
372+
</configuration>
370373
</plugin>
371374

372375
<!-- org.apache.maven.plugins:maven-source-plugin -->
@@ -432,7 +435,7 @@
432435
<dependency>
433436
<groupId>com.puppycrawl.tools</groupId>
434437
<artifactId>checkstyle</artifactId>
435-
<version>8.36.1</version>
438+
<version>9.3</version>
436439
</dependency>
437440
</dependencies>
438441
<executions>
@@ -650,7 +653,8 @@
650653
<plugin>
651654
<artifactId>maven-checkstyle-plugin</artifactId>
652655
<configuration>
653-
<encoding>UTF-8</encoding>
656+
<inputEncoding>UTF-8</inputEncoding>
657+
<outputEncoding>UTF-8</outputEncoding>
654658
<consoleOutput>true</consoleOutput>
655659
<linkXRef>false</linkXRef>
656660
<excludes>**/generated-sources/**/*,**/generated-test-sources/**/*</excludes>

xrpl4j-client/src/main/java/org/xrpl/xrpl4j/client/JsonRpcClient.java

Lines changed: 27 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -61,6 +61,13 @@ public interface JsonRpcClient {
6161
int SERVICE_UNAVAILABLE_STATUS = 503;
6262
Duration RETRY_INTERVAL = Duration.ofSeconds(1);
6363

64+
String RESULT = "result";
65+
String STATUS = "status";
66+
String ERROR = "error";
67+
String ERROR_EXCEPTION = "error_exception";
68+
String ERROR_MESSAGE = "error_message";
69+
String N_A = "n/a";
70+
6471
/**
6572
* Constructs a new client for the given url.
6673
*
@@ -134,7 +141,7 @@ default <T extends XrplResult> T send(
134141
JavaType resultType
135142
) throws JsonRpcClientErrorException {
136143
JsonNode response = postRpcRequest(request);
137-
JsonNode result = response.get("result");
144+
JsonNode result = response.get(RESULT);
138145
checkForError(response);
139146
try {
140147
return objectMapper.readValue(result.toString(), resultType);
@@ -151,13 +158,25 @@ default <T extends XrplResult> T send(
151158
* @throws JsonRpcClientErrorException If rippled returns an error message.
152159
*/
153160
default void checkForError(JsonNode response) throws JsonRpcClientErrorException {
154-
if (response.has("result")) {
155-
JsonNode result = response.get("result");
156-
if (result.has("error")) {
157-
String errorMessage = Optional.ofNullable(result.get("error_exception"))
158-
.map(JsonNode::asText)
159-
.orElseGet(() -> result.get("error_message").asText());
160-
throw new JsonRpcClientErrorException(errorMessage);
161+
if (response.has(RESULT)) {
162+
JsonNode result = response.get(RESULT);
163+
if (result.has(STATUS)) {
164+
String status = result.get(STATUS).asText();
165+
if (status.equals(ERROR)) { // <-- Only an error if result.status == "error"
166+
if (result.has(ERROR)) {
167+
String errorCode = result.get(ERROR).asText();
168+
169+
final String errorMessage;
170+
if (result.hasNonNull(ERROR_EXCEPTION)) {
171+
errorMessage = result.get(ERROR_EXCEPTION).asText();
172+
} else if (result.hasNonNull(ERROR_MESSAGE)) {
173+
errorMessage = result.get(ERROR_MESSAGE).asText();
174+
} else {
175+
errorMessage = N_A;
176+
}
177+
throw new JsonRpcClientErrorException(String.format("%s (%s)", errorCode, errorMessage));
178+
}
179+
}
161180
}
162181
}
163182
}

xrpl4j-client/src/main/java/org/xrpl/xrpl4j/client/XrplClient.java

Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -72,6 +72,8 @@
7272
import org.xrpl.xrpl4j.model.client.nft.NftInfoResult;
7373
import org.xrpl.xrpl4j.model.client.nft.NftSellOffersRequestParams;
7474
import org.xrpl.xrpl4j.model.client.nft.NftSellOffersResult;
75+
import org.xrpl.xrpl4j.model.client.oracle.GetAggregatePriceRequestParams;
76+
import org.xrpl.xrpl4j.model.client.oracle.GetAggregatePriceResult;
7577
import org.xrpl.xrpl4j.model.client.path.BookOffersRequestParams;
7678
import org.xrpl.xrpl4j.model.client.path.BookOffersResult;
7779
import org.xrpl.xrpl4j.model.client.path.DepositAuthorizedRequestParams;
@@ -810,6 +812,28 @@ public AmmInfoResult ammInfo(
810812
return jsonRpcClient.send(request, AmmInfoResult.class);
811813
}
812814

815+
/**
816+
* Retreive the aggregate price of specified oracle objects, returning three price statistics: mean, median, and
817+
* trimmed mean.
818+
*
819+
* @param params A {@link GetAggregatePriceRequestParams}.
820+
*
821+
* @return A {@link GetAggregatePriceResult}.
822+
*
823+
* @throws JsonRpcClientErrorException if {@code jsonRpcClient} throws an error.
824+
*/
825+
@Beta
826+
public GetAggregatePriceResult getAggregatePrice(
827+
GetAggregatePriceRequestParams params
828+
) throws JsonRpcClientErrorException {
829+
JsonRpcRequest request = JsonRpcRequest.builder()
830+
.method(XrplMethods.GET_AGGREGATE_PRICE)
831+
.addParams(params)
832+
.build();
833+
834+
return jsonRpcClient.send(request, GetAggregatePriceResult.class);
835+
}
836+
813837
public JsonRpcClient getJsonRpcClient() {
814838
return jsonRpcClient;
815839
}
Lines changed: 164 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,164 @@
1+
package org.xrpl.xrpl4j.client;
2+
3+
import static org.assertj.core.api.AssertionsForClassTypes.assertThat;
4+
import static org.junit.jupiter.api.Assertions.assertThrows;
5+
import static org.mockito.Mockito.mock;
6+
import static org.mockito.Mockito.when;
7+
8+
import com.fasterxml.jackson.databind.JsonNode;
9+
import org.junit.jupiter.api.BeforeEach;
10+
import org.junit.jupiter.api.Test;
11+
import org.mockito.Mock;
12+
import org.mockito.MockitoAnnotations;
13+
14+
/**
15+
* Unit test for {@link JsonRpcClient}.
16+
*/
17+
class JsonRpcClientTest {
18+
19+
private JsonRpcClient jsonRpcClient;
20+
21+
@Mock
22+
JsonNode jsonResponseNodeMock; // <-- The main response
23+
24+
@Mock
25+
JsonNode jsonResultNodeMock; // <-- resp.result
26+
27+
@Mock
28+
JsonNode jsonStatusNodeMock; // <-- result.status
29+
30+
@Mock
31+
JsonNode jsonErrorNodeMock; // <-- result.error
32+
33+
@Mock
34+
JsonNode jsonErrorMessageNodeMock; // <-- result.error_message
35+
36+
@Mock
37+
JsonNode jsonErrorExceptionNodeMock; // <-- result.error_exception
38+
39+
@BeforeEach
40+
void setUp() {
41+
MockitoAnnotations.openMocks(this);
42+
jsonRpcClient = rpcRequest -> jsonResponseNodeMock;
43+
44+
// See https://xrpl.org/docs/references/http-websocket-apis/api-conventions/error-formatting/#json-rpc-format
45+
when(jsonStatusNodeMock.asText()).thenReturn(JsonRpcClient.ERROR);
46+
47+
when(jsonErrorNodeMock.asText()).thenReturn("error_foo");
48+
when(jsonErrorMessageNodeMock.asText()).thenReturn("error_message_foo");
49+
when(jsonErrorExceptionNodeMock.asText()).thenReturn("error_exception_foo");
50+
51+
// By default, there's a result.
52+
when(jsonResponseNodeMock.has("result")).thenReturn(true);
53+
when(jsonResponseNodeMock.get("result")).thenReturn(jsonResultNodeMock);
54+
55+
// By default, there's an error.
56+
when(jsonResultNodeMock.has("status")).thenReturn(true);
57+
when(jsonResultNodeMock.get("status")).thenReturn(jsonStatusNodeMock);
58+
59+
hasError(true); // <-- By default, there's a `result.error`
60+
hasErrorMessage(false); // <-- By default, there's no `result.error_message`
61+
hasErrorException(false); // <-- By default, there's no `result.error_exception`
62+
}
63+
64+
//////////////////
65+
// checkForError()
66+
//////////////////
67+
68+
@Test
69+
void testCheckForErrorWhenResponseHasNoResultField() throws JsonRpcClientErrorException {
70+
// Do nothing if no "result" field
71+
when(jsonResponseNodeMock.has("result")).thenReturn(false);
72+
jsonRpcClient.checkForError(jsonResponseNodeMock); // <-- No error should be thrown.
73+
}
74+
75+
@Test
76+
void testCheckForErrorWhenResponseHasNoStatusFields() throws JsonRpcClientErrorException {
77+
when(jsonResultNodeMock.has("status")).thenReturn(false);
78+
jsonRpcClient.checkForError(jsonResponseNodeMock);
79+
}
80+
81+
@Test
82+
void testCheckForErrorWhenResponseHasNoErrorFields() throws JsonRpcClientErrorException {
83+
hasError(false);
84+
jsonRpcClient.checkForError(jsonResponseNodeMock);
85+
}
86+
87+
@Test
88+
void testCheckForErrorWhenResponseHasResultErrorException() {
89+
hasErrorException(true);
90+
91+
JsonRpcClientErrorException error = assertThrows(
92+
JsonRpcClientErrorException.class,
93+
() -> jsonRpcClient.checkForError(jsonResponseNodeMock)
94+
);
95+
assertThat(error.getMessage()).isEqualTo("error_foo (error_exception_foo)");
96+
}
97+
98+
@Test
99+
void testCheckForErrorWhenResponseHasResultErrorMessage() {
100+
hasErrorMessage(true);
101+
102+
JsonRpcClientErrorException error = assertThrows(
103+
JsonRpcClientErrorException.class,
104+
() -> jsonRpcClient.checkForError(jsonResponseNodeMock)
105+
);
106+
assertThat(error.getMessage()).isEqualTo("error_foo (error_message_foo)");
107+
}
108+
109+
@Test
110+
void testCheckForErrorWhenResponseHasResultError() {
111+
hasError(true);
112+
113+
JsonRpcClientErrorException error = assertThrows(JsonRpcClientErrorException.class,
114+
() -> jsonRpcClient.checkForError(jsonResponseNodeMock));
115+
assertThat(error.getMessage()).isEqualTo("error_foo (n/a)");
116+
}
117+
118+
@Test
119+
void testCheckForErrorWhenResponseHasAll() {
120+
hasError(true);
121+
hasErrorMessage(true);
122+
hasErrorMessage(true);
123+
124+
JsonRpcClientErrorException error = assertThrows(
125+
JsonRpcClientErrorException.class,
126+
() -> jsonRpcClient.checkForError(jsonResponseNodeMock)
127+
);
128+
assertThat(error.getMessage()).isEqualTo("error_foo (error_message_foo)");
129+
}
130+
131+
//////////////////
132+
// Private Helpers
133+
//////////////////
134+
135+
private void hasError(boolean hasError) {
136+
when(jsonResultNodeMock.has(JsonRpcClient.ERROR)).thenReturn(hasError);
137+
if (hasError) {
138+
when(jsonResultNodeMock.get(JsonRpcClient.ERROR)).thenReturn(jsonErrorNodeMock);
139+
when(jsonResultNodeMock.hasNonNull(JsonRpcClient.ERROR)).thenReturn(true);
140+
} else {
141+
when(jsonResultNodeMock.get(JsonRpcClient.ERROR)).thenReturn(null);
142+
}
143+
}
144+
145+
private void hasErrorMessage(boolean hasErrorMessage) {
146+
when(jsonResultNodeMock.has(JsonRpcClient.ERROR_MESSAGE)).thenReturn(hasErrorMessage);
147+
if (hasErrorMessage) {
148+
when(jsonResultNodeMock.get(JsonRpcClient.ERROR_MESSAGE)).thenReturn(jsonErrorMessageNodeMock);
149+
when(jsonResultNodeMock.hasNonNull(JsonRpcClient.ERROR_MESSAGE)).thenReturn(true);
150+
} else {
151+
when(jsonResultNodeMock.get(JsonRpcClient.ERROR_MESSAGE)).thenReturn(null);
152+
}
153+
}
154+
155+
private void hasErrorException(boolean hasErrorException) {
156+
when(jsonResultNodeMock.has(JsonRpcClient.ERROR_EXCEPTION)).thenReturn(hasErrorException);
157+
if (hasErrorException) {
158+
when(jsonResultNodeMock.get(JsonRpcClient.ERROR_EXCEPTION)).thenReturn(jsonErrorExceptionNodeMock);
159+
when(jsonResultNodeMock.hasNonNull(JsonRpcClient.ERROR_EXCEPTION)).thenReturn(true);
160+
} else {
161+
when(jsonResultNodeMock.get(JsonRpcClient.ERROR_EXCEPTION)).thenReturn(null);
162+
}
163+
}
164+
}

xrpl4j-client/src/test/java/org/xrpl/xrpl4j/client/XrplClientTest.java

Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -87,6 +87,8 @@
8787
import org.xrpl.xrpl4j.model.client.nft.NftInfoResult;
8888
import org.xrpl.xrpl4j.model.client.nft.NftSellOffersRequestParams;
8989
import org.xrpl.xrpl4j.model.client.nft.NftSellOffersResult;
90+
import org.xrpl.xrpl4j.model.client.oracle.GetAggregatePriceRequestParams;
91+
import org.xrpl.xrpl4j.model.client.oracle.GetAggregatePriceResult;
9092
import org.xrpl.xrpl4j.model.client.path.BookOffersRequestParams;
9193
import org.xrpl.xrpl4j.model.client.path.BookOffersResult;
9294
import org.xrpl.xrpl4j.model.client.path.DepositAuthorizedRequestParams;
@@ -1087,4 +1089,22 @@ void nftInfo() throws JsonRpcClientErrorException {
10871089

10881090
assertThat(result).isEqualTo(mockResult);
10891091
}
1092+
1093+
@Test
1094+
void getAggregatePrice() throws JsonRpcClientErrorException {
1095+
GetAggregatePriceRequestParams params = mock(GetAggregatePriceRequestParams.class);
1096+
GetAggregatePriceResult expectedResult = mock(GetAggregatePriceResult.class);
1097+
1098+
when(jsonRpcClientMock.send(
1099+
JsonRpcRequest.builder()
1100+
.method(XrplMethods.GET_AGGREGATE_PRICE)
1101+
.addParams(params)
1102+
.build(),
1103+
GetAggregatePriceResult.class
1104+
)).thenReturn(expectedResult);
1105+
1106+
GetAggregatePriceResult result = xrplClient.getAggregatePrice(params);
1107+
1108+
assertThat(result).isEqualTo(expectedResult);
1109+
}
10901110
}

xrpl4j-core/src/main/java/org/xrpl/xrpl4j/codec/binary/types/CurrencyType.java

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -9,9 +9,9 @@
99
* Licensed under the Apache License, Version 2.0 (the "License");
1010
* you may not use this file except in compliance with the License.
1111
* You may obtain a copy of the License at
12-
*
12+
*
1313
* http://www.apache.org/licenses/LICENSE-2.0
14-
*
14+
*
1515
* Unless required by applicable law or agreed to in writing, software
1616
* distributed under the License is distributed on an "AS IS" BASIS,
1717
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
@@ -93,9 +93,9 @@ private boolean onlyIso(UnsignedByteArray byteList) {
9393
/**
9494
* Convert {@code list} to a {@link String} of raw ISO codes.
9595
*
96-
* @param list
96+
* @param list An {@link UnsignedByteArray} representing raw ISO codes.
9797
*
98-
* @return
98+
* @return A {@link String}.
9999
*/
100100
private String rawISO(UnsignedByteArray list) {
101101
return new String(list.slice(12, 15).toByteArray());

xrpl4j-core/src/main/java/org/xrpl/xrpl4j/crypto/signing/SignatureUtils.java

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -56,6 +56,8 @@
5656
import org.xrpl.xrpl4j.model.transactions.NfTokenMint;
5757
import org.xrpl.xrpl4j.model.transactions.OfferCancel;
5858
import org.xrpl.xrpl4j.model.transactions.OfferCreate;
59+
import org.xrpl.xrpl4j.model.transactions.OracleDelete;
60+
import org.xrpl.xrpl4j.model.transactions.OracleSet;
5961
import org.xrpl.xrpl4j.model.transactions.Payment;
6062
import org.xrpl.xrpl4j.model.transactions.PaymentChannelClaim;
6163
import org.xrpl.xrpl4j.model.transactions.PaymentChannelCreate;
@@ -381,6 +383,14 @@ public <T extends Transaction> SingleSignedTransaction<T> addSignatureToTransact
381383
transactionWithSignature = DidDelete.builder().from((DidDelete) transaction)
382384
.transactionSignature(signature)
383385
.build();
386+
} else if (OracleSet.class.isAssignableFrom(transaction.getClass())) {
387+
transactionWithSignature = OracleSet.builder().from((OracleSet) transaction)
388+
.transactionSignature(signature)
389+
.build();
390+
} else if (OracleDelete.class.isAssignableFrom(transaction.getClass())) {
391+
transactionWithSignature = OracleDelete.builder().from((OracleDelete) transaction)
392+
.transactionSignature(signature)
393+
.build();
384394
} else {
385395
// Should never happen, but will in a unit test if we miss one.
386396
throw new IllegalArgumentException("Signing fields could not be added to the transaction.");
@@ -584,6 +594,14 @@ public <T extends Transaction> T addMultiSignaturesToTransaction(T transaction,
584594
transactionWithSignatures = DidDelete.builder().from((DidDelete) transaction)
585595
.signers(signers)
586596
.build();
597+
} else if (OracleSet.class.isAssignableFrom(transaction.getClass())) {
598+
transactionWithSignatures = OracleSet.builder().from((OracleSet) transaction)
599+
.signers(signers)
600+
.build();
601+
} else if (OracleDelete.class.isAssignableFrom(transaction.getClass())) {
602+
transactionWithSignatures = OracleDelete.builder().from((OracleDelete) transaction)
603+
.signers(signers)
604+
.build();
587605
} else {
588606
// Should never happen, but will in a unit test if we miss one.
589607
throw new IllegalArgumentException("Signing fields could not be added to the transaction.");

xrpl4j-core/src/main/java/org/xrpl/xrpl4j/model/client/XrplMethods.java

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -233,4 +233,5 @@ public class XrplMethods {
233233
*/
234234
public static final String PING = "ping";
235235

236+
public static final String GET_AGGREGATE_PRICE = "get_aggregate_price";
236237
}

0 commit comments

Comments
 (0)