Skip to content

Commit

Permalink
Price Oracle Support (#537)
Browse files Browse the repository at this point in the history
* 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 <[email protected]>
  • Loading branch information
nkramer44 and sappenin authored Jul 19, 2024
1 parent 6658182 commit 44bac5a
Show file tree
Hide file tree
Showing 67 changed files with 3,908 additions and 468 deletions.
2 changes: 1 addition & 1 deletion checkstyle.xml
Original file line number Diff line number Diff line change
Expand Up @@ -205,7 +205,7 @@
<property name="target" value="CLASS_DEF, INTERFACE_DEF, ENUM_DEF, METHOD_DEF, CTOR_DEF, VARIABLE_DEF"/>
</module>
<module name="JavadocMethod">
<property name="scope" value="public"/>
<property name="accessModifiers" value="public, protected"/>
<property name="allowMissingParamTags" value="true"/>
<property name="allowMissingReturnTag" value="true"/>
<property name="allowedAnnotations" value="Override, Test"/>
Expand Down
8 changes: 6 additions & 2 deletions pom.xml
Original file line number Diff line number Diff line change
Expand Up @@ -367,6 +367,9 @@
</goals>
</execution>
</executions>
<configuration>
<rerunFailingTestsCount>2</rerunFailingTestsCount>
</configuration>
</plugin>

<!-- org.apache.maven.plugins:maven-source-plugin -->
Expand Down Expand Up @@ -432,7 +435,7 @@
<dependency>
<groupId>com.puppycrawl.tools</groupId>
<artifactId>checkstyle</artifactId>
<version>8.36.1</version>
<version>9.3</version>
</dependency>
</dependencies>
<executions>
Expand Down Expand Up @@ -650,7 +653,8 @@
<plugin>
<artifactId>maven-checkstyle-plugin</artifactId>
<configuration>
<encoding>UTF-8</encoding>
<inputEncoding>UTF-8</inputEncoding>
<outputEncoding>UTF-8</outputEncoding>
<consoleOutput>true</consoleOutput>
<linkXRef>false</linkXRef>
<excludes>**/generated-sources/**/*,**/generated-test-sources/**/*</excludes>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -61,6 +61,13 @@ public interface JsonRpcClient {
int SERVICE_UNAVAILABLE_STATUS = 503;
Duration RETRY_INTERVAL = Duration.ofSeconds(1);

String RESULT = "result";
String STATUS = "status";
String ERROR = "error";
String ERROR_EXCEPTION = "error_exception";
String ERROR_MESSAGE = "error_message";
String N_A = "n/a";

/**
* Constructs a new client for the given url.
*
Expand Down Expand Up @@ -134,7 +141,7 @@ default <T extends XrplResult> T send(
JavaType resultType
) throws JsonRpcClientErrorException {
JsonNode response = postRpcRequest(request);
JsonNode result = response.get("result");
JsonNode result = response.get(RESULT);
checkForError(response);
try {
return objectMapper.readValue(result.toString(), resultType);
Expand All @@ -151,13 +158,25 @@ default <T extends XrplResult> T send(
* @throws JsonRpcClientErrorException If rippled returns an error message.
*/
default void checkForError(JsonNode response) throws JsonRpcClientErrorException {
if (response.has("result")) {
JsonNode result = response.get("result");
if (result.has("error")) {
String errorMessage = Optional.ofNullable(result.get("error_exception"))
.map(JsonNode::asText)
.orElseGet(() -> result.get("error_message").asText());
throw new JsonRpcClientErrorException(errorMessage);
if (response.has(RESULT)) {
JsonNode result = response.get(RESULT);
if (result.has(STATUS)) {
String status = result.get(STATUS).asText();
if (status.equals(ERROR)) { // <-- Only an error if result.status == "error"
if (result.has(ERROR)) {
String errorCode = result.get(ERROR).asText();

final String errorMessage;
if (result.hasNonNull(ERROR_EXCEPTION)) {
errorMessage = result.get(ERROR_EXCEPTION).asText();
} else if (result.hasNonNull(ERROR_MESSAGE)) {
errorMessage = result.get(ERROR_MESSAGE).asText();
} else {
errorMessage = N_A;
}
throw new JsonRpcClientErrorException(String.format("%s (%s)", errorCode, errorMessage));
}
}
}
}
}
Expand Down
24 changes: 24 additions & 0 deletions xrpl4j-client/src/main/java/org/xrpl/xrpl4j/client/XrplClient.java
Original file line number Diff line number Diff line change
Expand Up @@ -72,6 +72,8 @@
import org.xrpl.xrpl4j.model.client.nft.NftInfoResult;
import org.xrpl.xrpl4j.model.client.nft.NftSellOffersRequestParams;
import org.xrpl.xrpl4j.model.client.nft.NftSellOffersResult;
import org.xrpl.xrpl4j.model.client.oracle.GetAggregatePriceRequestParams;
import org.xrpl.xrpl4j.model.client.oracle.GetAggregatePriceResult;
import org.xrpl.xrpl4j.model.client.path.BookOffersRequestParams;
import org.xrpl.xrpl4j.model.client.path.BookOffersResult;
import org.xrpl.xrpl4j.model.client.path.DepositAuthorizedRequestParams;
Expand Down Expand Up @@ -810,6 +812,28 @@ public AmmInfoResult ammInfo(
return jsonRpcClient.send(request, AmmInfoResult.class);
}

/**
* Retreive the aggregate price of specified oracle objects, returning three price statistics: mean, median, and
* trimmed mean.
*
* @param params A {@link GetAggregatePriceRequestParams}.
*
* @return A {@link GetAggregatePriceResult}.
*
* @throws JsonRpcClientErrorException if {@code jsonRpcClient} throws an error.
*/
@Beta
public GetAggregatePriceResult getAggregatePrice(
GetAggregatePriceRequestParams params
) throws JsonRpcClientErrorException {
JsonRpcRequest request = JsonRpcRequest.builder()
.method(XrplMethods.GET_AGGREGATE_PRICE)
.addParams(params)
.build();

return jsonRpcClient.send(request, GetAggregatePriceResult.class);
}

public JsonRpcClient getJsonRpcClient() {
return jsonRpcClient;
}
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,164 @@
package org.xrpl.xrpl4j.client;

import static org.assertj.core.api.AssertionsForClassTypes.assertThat;
import static org.junit.jupiter.api.Assertions.assertThrows;
import static org.mockito.Mockito.mock;
import static org.mockito.Mockito.when;

import com.fasterxml.jackson.databind.JsonNode;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;
import org.mockito.Mock;
import org.mockito.MockitoAnnotations;

/**
* Unit test for {@link JsonRpcClient}.
*/
class JsonRpcClientTest {

private JsonRpcClient jsonRpcClient;

@Mock
JsonNode jsonResponseNodeMock; // <-- The main response

@Mock
JsonNode jsonResultNodeMock; // <-- resp.result

@Mock
JsonNode jsonStatusNodeMock; // <-- result.status

@Mock
JsonNode jsonErrorNodeMock; // <-- result.error

@Mock
JsonNode jsonErrorMessageNodeMock; // <-- result.error_message

@Mock
JsonNode jsonErrorExceptionNodeMock; // <-- result.error_exception

@BeforeEach
void setUp() {
MockitoAnnotations.openMocks(this);
jsonRpcClient = rpcRequest -> jsonResponseNodeMock;

// See https://xrpl.org/docs/references/http-websocket-apis/api-conventions/error-formatting/#json-rpc-format
when(jsonStatusNodeMock.asText()).thenReturn(JsonRpcClient.ERROR);

when(jsonErrorNodeMock.asText()).thenReturn("error_foo");
when(jsonErrorMessageNodeMock.asText()).thenReturn("error_message_foo");
when(jsonErrorExceptionNodeMock.asText()).thenReturn("error_exception_foo");

// By default, there's a result.
when(jsonResponseNodeMock.has("result")).thenReturn(true);
when(jsonResponseNodeMock.get("result")).thenReturn(jsonResultNodeMock);

// By default, there's an error.
when(jsonResultNodeMock.has("status")).thenReturn(true);
when(jsonResultNodeMock.get("status")).thenReturn(jsonStatusNodeMock);

hasError(true); // <-- By default, there's a `result.error`
hasErrorMessage(false); // <-- By default, there's no `result.error_message`
hasErrorException(false); // <-- By default, there's no `result.error_exception`
}

//////////////////
// checkForError()
//////////////////

@Test
void testCheckForErrorWhenResponseHasNoResultField() throws JsonRpcClientErrorException {
// Do nothing if no "result" field
when(jsonResponseNodeMock.has("result")).thenReturn(false);
jsonRpcClient.checkForError(jsonResponseNodeMock); // <-- No error should be thrown.
}

@Test
void testCheckForErrorWhenResponseHasNoStatusFields() throws JsonRpcClientErrorException {
when(jsonResultNodeMock.has("status")).thenReturn(false);
jsonRpcClient.checkForError(jsonResponseNodeMock);
}

@Test
void testCheckForErrorWhenResponseHasNoErrorFields() throws JsonRpcClientErrorException {
hasError(false);
jsonRpcClient.checkForError(jsonResponseNodeMock);
}

@Test
void testCheckForErrorWhenResponseHasResultErrorException() {
hasErrorException(true);

JsonRpcClientErrorException error = assertThrows(
JsonRpcClientErrorException.class,
() -> jsonRpcClient.checkForError(jsonResponseNodeMock)
);
assertThat(error.getMessage()).isEqualTo("error_foo (error_exception_foo)");
}

@Test
void testCheckForErrorWhenResponseHasResultErrorMessage() {
hasErrorMessage(true);

JsonRpcClientErrorException error = assertThrows(
JsonRpcClientErrorException.class,
() -> jsonRpcClient.checkForError(jsonResponseNodeMock)
);
assertThat(error.getMessage()).isEqualTo("error_foo (error_message_foo)");
}

@Test
void testCheckForErrorWhenResponseHasResultError() {
hasError(true);

JsonRpcClientErrorException error = assertThrows(JsonRpcClientErrorException.class,
() -> jsonRpcClient.checkForError(jsonResponseNodeMock));
assertThat(error.getMessage()).isEqualTo("error_foo (n/a)");
}

@Test
void testCheckForErrorWhenResponseHasAll() {
hasError(true);
hasErrorMessage(true);
hasErrorMessage(true);

JsonRpcClientErrorException error = assertThrows(
JsonRpcClientErrorException.class,
() -> jsonRpcClient.checkForError(jsonResponseNodeMock)
);
assertThat(error.getMessage()).isEqualTo("error_foo (error_message_foo)");
}

//////////////////
// Private Helpers
//////////////////

private void hasError(boolean hasError) {
when(jsonResultNodeMock.has(JsonRpcClient.ERROR)).thenReturn(hasError);
if (hasError) {
when(jsonResultNodeMock.get(JsonRpcClient.ERROR)).thenReturn(jsonErrorNodeMock);
when(jsonResultNodeMock.hasNonNull(JsonRpcClient.ERROR)).thenReturn(true);
} else {
when(jsonResultNodeMock.get(JsonRpcClient.ERROR)).thenReturn(null);
}
}

private void hasErrorMessage(boolean hasErrorMessage) {
when(jsonResultNodeMock.has(JsonRpcClient.ERROR_MESSAGE)).thenReturn(hasErrorMessage);
if (hasErrorMessage) {
when(jsonResultNodeMock.get(JsonRpcClient.ERROR_MESSAGE)).thenReturn(jsonErrorMessageNodeMock);
when(jsonResultNodeMock.hasNonNull(JsonRpcClient.ERROR_MESSAGE)).thenReturn(true);
} else {
when(jsonResultNodeMock.get(JsonRpcClient.ERROR_MESSAGE)).thenReturn(null);
}
}

private void hasErrorException(boolean hasErrorException) {
when(jsonResultNodeMock.has(JsonRpcClient.ERROR_EXCEPTION)).thenReturn(hasErrorException);
if (hasErrorException) {
when(jsonResultNodeMock.get(JsonRpcClient.ERROR_EXCEPTION)).thenReturn(jsonErrorExceptionNodeMock);
when(jsonResultNodeMock.hasNonNull(JsonRpcClient.ERROR_EXCEPTION)).thenReturn(true);
} else {
when(jsonResultNodeMock.get(JsonRpcClient.ERROR_EXCEPTION)).thenReturn(null);
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -87,6 +87,8 @@
import org.xrpl.xrpl4j.model.client.nft.NftInfoResult;
import org.xrpl.xrpl4j.model.client.nft.NftSellOffersRequestParams;
import org.xrpl.xrpl4j.model.client.nft.NftSellOffersResult;
import org.xrpl.xrpl4j.model.client.oracle.GetAggregatePriceRequestParams;
import org.xrpl.xrpl4j.model.client.oracle.GetAggregatePriceResult;
import org.xrpl.xrpl4j.model.client.path.BookOffersRequestParams;
import org.xrpl.xrpl4j.model.client.path.BookOffersResult;
import org.xrpl.xrpl4j.model.client.path.DepositAuthorizedRequestParams;
Expand Down Expand Up @@ -1087,4 +1089,22 @@ void nftInfo() throws JsonRpcClientErrorException {

assertThat(result).isEqualTo(mockResult);
}

@Test
void getAggregatePrice() throws JsonRpcClientErrorException {
GetAggregatePriceRequestParams params = mock(GetAggregatePriceRequestParams.class);
GetAggregatePriceResult expectedResult = mock(GetAggregatePriceResult.class);

when(jsonRpcClientMock.send(
JsonRpcRequest.builder()
.method(XrplMethods.GET_AGGREGATE_PRICE)
.addParams(params)
.build(),
GetAggregatePriceResult.class
)).thenReturn(expectedResult);

GetAggregatePriceResult result = xrplClient.getAggregatePrice(params);

assertThat(result).isEqualTo(expectedResult);
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -9,9 +9,9 @@
* 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.
Expand Down Expand Up @@ -93,9 +93,9 @@ private boolean onlyIso(UnsignedByteArray byteList) {
/**
* Convert {@code list} to a {@link String} of raw ISO codes.
*
* @param list
* @param list An {@link UnsignedByteArray} representing raw ISO codes.
*
* @return
* @return A {@link String}.
*/
private String rawISO(UnsignedByteArray list) {
return new String(list.slice(12, 15).toByteArray());
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -56,6 +56,8 @@
import org.xrpl.xrpl4j.model.transactions.NfTokenMint;
import org.xrpl.xrpl4j.model.transactions.OfferCancel;
import org.xrpl.xrpl4j.model.transactions.OfferCreate;
import org.xrpl.xrpl4j.model.transactions.OracleDelete;
import org.xrpl.xrpl4j.model.transactions.OracleSet;
import org.xrpl.xrpl4j.model.transactions.Payment;
import org.xrpl.xrpl4j.model.transactions.PaymentChannelClaim;
import org.xrpl.xrpl4j.model.transactions.PaymentChannelCreate;
Expand Down Expand Up @@ -381,6 +383,14 @@ public <T extends Transaction> SingleSignedTransaction<T> addSignatureToTransact
transactionWithSignature = DidDelete.builder().from((DidDelete) transaction)
.transactionSignature(signature)
.build();
} else if (OracleSet.class.isAssignableFrom(transaction.getClass())) {
transactionWithSignature = OracleSet.builder().from((OracleSet) transaction)
.transactionSignature(signature)
.build();
} else if (OracleDelete.class.isAssignableFrom(transaction.getClass())) {
transactionWithSignature = OracleDelete.builder().from((OracleDelete) transaction)
.transactionSignature(signature)
.build();
} else {
// Should never happen, but will in a unit test if we miss one.
throw new IllegalArgumentException("Signing fields could not be added to the transaction.");
Expand Down Expand Up @@ -584,6 +594,14 @@ public <T extends Transaction> T addMultiSignaturesToTransaction(T transaction,
transactionWithSignatures = DidDelete.builder().from((DidDelete) transaction)
.signers(signers)
.build();
} else if (OracleSet.class.isAssignableFrom(transaction.getClass())) {
transactionWithSignatures = OracleSet.builder().from((OracleSet) transaction)
.signers(signers)
.build();
} else if (OracleDelete.class.isAssignableFrom(transaction.getClass())) {
transactionWithSignatures = OracleDelete.builder().from((OracleDelete) transaction)
.signers(signers)
.build();
} else {
// Should never happen, but will in a unit test if we miss one.
throw new IllegalArgumentException("Signing fields could not be added to the transaction.");
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -233,4 +233,5 @@ public class XrplMethods {
*/
public static final String PING = "ping";

public static final String GET_AGGREGATE_PRICE = "get_aggregate_price";
}
Loading

0 comments on commit 44bac5a

Please sign in to comment.