diff --git a/README.md b/README.md index ac0819197..78ccda6e8 100644 --- a/README.md +++ b/README.md @@ -33,7 +33,7 @@ current [BOM](https://howtodoinjava.com/maven/maven-bom-bill-of-materials-depend org.xrpl xrpl4j-bom - 3.3.0 + 3.4.0 pom import diff --git a/checkstyle.xml b/checkstyle.xml index f6e990906..90182a7d0 100644 --- a/checkstyle.xml +++ b/checkstyle.xml @@ -205,7 +205,7 @@ - + diff --git a/pom.xml b/pom.xml index 812cd3586..4e89163b9 100644 --- a/pom.xml +++ b/pom.xml @@ -367,6 +367,9 @@ + + 2 + @@ -432,7 +435,7 @@ com.puppycrawl.tools checkstyle - 8.36.1 + 9.3 @@ -650,7 +653,8 @@ maven-checkstyle-plugin - UTF-8 + UTF-8 + UTF-8 true false **/generated-sources/**/*,**/generated-test-sources/**/* diff --git a/xrpl4j-client/src/main/java/org/xrpl/xrpl4j/client/JsonRpcClient.java b/xrpl4j-client/src/main/java/org/xrpl/xrpl4j/client/JsonRpcClient.java index e95c0f1bf..75a25ddd5 100644 --- a/xrpl4j-client/src/main/java/org/xrpl/xrpl4j/client/JsonRpcClient.java +++ b/xrpl4j-client/src/main/java/org/xrpl/xrpl4j/client/JsonRpcClient.java @@ -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. * @@ -134,7 +141,7 @@ default 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); @@ -151,13 +158,25 @@ default 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)); + } + } } } } diff --git a/xrpl4j-client/src/main/java/org/xrpl/xrpl4j/client/XrplClient.java b/xrpl4j-client/src/main/java/org/xrpl/xrpl4j/client/XrplClient.java index 4be150a83..bf744ea25 100644 --- a/xrpl4j-client/src/main/java/org/xrpl/xrpl4j/client/XrplClient.java +++ b/xrpl4j-client/src/main/java/org/xrpl/xrpl4j/client/XrplClient.java @@ -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; @@ -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; } diff --git a/xrpl4j-client/src/test/java/org/xrpl/xrpl4j/client/JsonRpcClientTest.java b/xrpl4j-client/src/test/java/org/xrpl/xrpl4j/client/JsonRpcClientTest.java new file mode 100644 index 000000000..cec9f0bbc --- /dev/null +++ b/xrpl4j-client/src/test/java/org/xrpl/xrpl4j/client/JsonRpcClientTest.java @@ -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); + } + } +} \ No newline at end of file diff --git a/xrpl4j-client/src/test/java/org/xrpl/xrpl4j/client/XrplClientTest.java b/xrpl4j-client/src/test/java/org/xrpl/xrpl4j/client/XrplClientTest.java index f07094164..32c4e05be 100644 --- a/xrpl4j-client/src/test/java/org/xrpl/xrpl4j/client/XrplClientTest.java +++ b/xrpl4j-client/src/test/java/org/xrpl/xrpl4j/client/XrplClientTest.java @@ -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; @@ -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); + } } diff --git a/xrpl4j-core/src/main/java/org/xrpl/xrpl4j/codec/binary/types/CurrencyType.java b/xrpl4j-core/src/main/java/org/xrpl/xrpl4j/codec/binary/types/CurrencyType.java index 35fa9e244..dffeda223 100644 --- a/xrpl4j-core/src/main/java/org/xrpl/xrpl4j/codec/binary/types/CurrencyType.java +++ b/xrpl4j-core/src/main/java/org/xrpl/xrpl4j/codec/binary/types/CurrencyType.java @@ -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. @@ -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()); diff --git a/xrpl4j-core/src/main/java/org/xrpl/xrpl4j/crypto/signing/SignatureUtils.java b/xrpl4j-core/src/main/java/org/xrpl/xrpl4j/crypto/signing/SignatureUtils.java index c8edb3b0c..8f2b39fef 100644 --- a/xrpl4j-core/src/main/java/org/xrpl/xrpl4j/crypto/signing/SignatureUtils.java +++ b/xrpl4j-core/src/main/java/org/xrpl/xrpl4j/crypto/signing/SignatureUtils.java @@ -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; @@ -381,6 +383,14 @@ public SingleSignedTransaction 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."); @@ -584,6 +594,14 @@ public 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."); diff --git a/xrpl4j-core/src/main/java/org/xrpl/xrpl4j/model/client/XrplMethods.java b/xrpl4j-core/src/main/java/org/xrpl/xrpl4j/model/client/XrplMethods.java index 13e765602..c0b112ef5 100644 --- a/xrpl4j-core/src/main/java/org/xrpl/xrpl4j/model/client/XrplMethods.java +++ b/xrpl4j-core/src/main/java/org/xrpl/xrpl4j/model/client/XrplMethods.java @@ -233,4 +233,5 @@ public class XrplMethods { */ public static final String PING = "ping"; + public static final String GET_AGGREGATE_PRICE = "get_aggregate_price"; } diff --git a/xrpl4j-core/src/main/java/org/xrpl/xrpl4j/model/client/ledger/LedgerEntryRequestParams.java b/xrpl4j-core/src/main/java/org/xrpl/xrpl4j/model/client/ledger/LedgerEntryRequestParams.java index b850bcbfb..4a5fb1692 100644 --- a/xrpl4j-core/src/main/java/org/xrpl/xrpl4j/model/client/ledger/LedgerEntryRequestParams.java +++ b/xrpl4j-core/src/main/java/org/xrpl/xrpl4j/model/client/ledger/LedgerEntryRequestParams.java @@ -19,6 +19,7 @@ import org.xrpl.xrpl4j.model.ledger.LedgerObject; import org.xrpl.xrpl4j.model.ledger.NfTokenPageObject; import org.xrpl.xrpl4j.model.ledger.OfferObject; +import org.xrpl.xrpl4j.model.ledger.OracleObject; import org.xrpl.xrpl4j.model.ledger.PayChannelObject; import org.xrpl.xrpl4j.model.ledger.RippleStateObject; import org.xrpl.xrpl4j.model.ledger.TicketObject; @@ -327,7 +328,7 @@ static LedgerEntryRequestParams bridge( /** * Construct a {@link LedgerEntryRequestParams} that requests a {@link DidObject} ledger entry. * - * @param address The address of the owner of the {@link DidObject}. + * @param address The address of the owner of the {@link DidObject}. * @param ledgerSpecifier A {@link LedgerSpecifier} indicating the ledger to query data from. * * @return A {@link LedgerEntryRequestParams} for {@link DidObject}. @@ -342,6 +343,24 @@ static LedgerEntryRequestParams did( .build(); } + /** + * Construct a {@link LedgerEntryRequestParams} that requests a {@link OracleObject} ledger entry. + * + * @param oracle The {@link OracleLedgerEntryParams} specifying the oracle. + * @param ledgerSpecifier A {@link LedgerSpecifier} indicating the ledger to query data from. + * + * @return A {@link LedgerEntryRequestParams} for {@link OracleObject}. + */ + static LedgerEntryRequestParams oracle( + OracleLedgerEntryParams oracle, + LedgerSpecifier ledgerSpecifier + ) { + return ImmutableLedgerEntryRequestParams.builder() + .oracle(oracle) + .ledgerSpecifier(ledgerSpecifier) + .build(); + } + /** * Specifies the ledger version to request. A ledger version can be specified by ledger hash, numerical ledger index, * or a shortcut value. @@ -468,6 +487,13 @@ default boolean binary() { */ Optional bridge(); + /** + * Look up a {@link org.xrpl.xrpl4j.model.ledger.OracleObject} by {@link OracleLedgerEntryParams}. + * + * @return An {@link Optional} {@link OracleLedgerEntryParams}. + */ + Optional oracle(); + /** * The {@link Class} of {@link T}. This field is helpful when telling Jackson how to deserialize rippled's response to * a {@link T}. @@ -525,6 +551,10 @@ default Class ledgerObjectClass() { return (Class) DidObject.class; } + if (oracle().isPresent()) { + return (Class) OracleObject.class; + } + return (Class) LedgerObject.class; } } diff --git a/xrpl4j-core/src/main/java/org/xrpl/xrpl4j/model/client/ledger/OracleLedgerEntryParams.java b/xrpl4j-core/src/main/java/org/xrpl/xrpl4j/model/client/ledger/OracleLedgerEntryParams.java new file mode 100644 index 000000000..f13c8ecb7 --- /dev/null +++ b/xrpl4j-core/src/main/java/org/xrpl/xrpl4j/model/client/ledger/OracleLedgerEntryParams.java @@ -0,0 +1,35 @@ +package org.xrpl.xrpl4j.model.client.ledger; + +import com.fasterxml.jackson.annotation.JsonProperty; +import com.fasterxml.jackson.databind.annotation.JsonDeserialize; +import com.fasterxml.jackson.databind.annotation.JsonSerialize; +import com.google.common.annotations.Beta; +import org.immutables.value.Value.Immutable; +import org.xrpl.xrpl4j.model.transactions.Address; +import org.xrpl.xrpl4j.model.transactions.OracleDocumentId; + +/** + * Parameters specifying an {@link org.xrpl.xrpl4j.model.ledger.OracleObject} to retrieve via a {@code ledger_entry} RPC + * call. + */ +@Beta +@Immutable +@JsonSerialize(as = ImmutableOracleLedgerEntryParams.class) +@JsonDeserialize(as = ImmutableOracleLedgerEntryParams.class) +public interface OracleLedgerEntryParams { + + /** + * Construct a {@code OracleLedgerEntryParams} builder. + * + * @return An {@link ImmutableOracleLedgerEntryParams.Builder}. + */ + static ImmutableOracleLedgerEntryParams.Builder builder() { + return ImmutableOracleLedgerEntryParams.builder(); + } + + Address account(); + + @JsonProperty("oracle_document_id") + OracleDocumentId oracleDocumentId(); + +} diff --git a/xrpl4j-core/src/main/java/org/xrpl/xrpl4j/model/client/oracle/AggregatePriceSet.java b/xrpl4j-core/src/main/java/org/xrpl/xrpl4j/model/client/oracle/AggregatePriceSet.java new file mode 100644 index 000000000..1da6b6ed5 --- /dev/null +++ b/xrpl4j-core/src/main/java/org/xrpl/xrpl4j/model/client/oracle/AggregatePriceSet.java @@ -0,0 +1,76 @@ +package org.xrpl.xrpl4j.model.client.oracle; + +import com.fasterxml.jackson.annotation.JsonIgnore; +import com.fasterxml.jackson.annotation.JsonProperty; +import com.fasterxml.jackson.databind.annotation.JsonDeserialize; +import com.fasterxml.jackson.databind.annotation.JsonSerialize; +import com.google.common.annotations.Beta; +import com.google.common.primitives.UnsignedLong; +import org.immutables.value.Value.Immutable; +import org.immutables.value.Value.Lazy; + +import java.math.BigDecimal; + +/** + * Statistics from collected oracle prices. + */ +@Beta +@Immutable +@JsonSerialize(as = ImmutableAggregatePriceSet.class) +@JsonDeserialize(as = ImmutableAggregatePriceSet.class) +public interface AggregatePriceSet { + + /** + * Construct a {@code AggregatePriceSet} builder. + * + * @return An {@link ImmutableAggregatePriceSet.Builder}. + */ + static ImmutableAggregatePriceSet.Builder builder() { + return ImmutableAggregatePriceSet.builder(); + } + + /** + * The simple mean price. + * + * @return A {@link String}. + */ + @JsonProperty("mean") + String meanString(); + + /** + * The simple mean price as a {@link BigDecimal}. + * + * @return A {@link BigDecimal}. + */ + @Lazy + @JsonIgnore + default BigDecimal mean() { + return new BigDecimal(meanString()); + } + + /** + * The size of the data set to calculate the mean. + * + * @return An {@link UnsignedLong}. + */ + UnsignedLong size(); + + /** + * The standard deviation. + * + * @return A {@link String}. + */ + @JsonProperty("standard_deviation") + String standardDeviationString(); + + /** + * The standard deviation as a {@link BigDecimal}. + * + * @return A {@link BigDecimal}. + */ + @Lazy + @JsonIgnore + default BigDecimal standardDeviation() { + return new BigDecimal(standardDeviationString()); + } +} diff --git a/xrpl4j-core/src/main/java/org/xrpl/xrpl4j/model/client/oracle/GetAggregatePriceRequestParams.java b/xrpl4j-core/src/main/java/org/xrpl/xrpl4j/model/client/oracle/GetAggregatePriceRequestParams.java new file mode 100644 index 000000000..116b676e5 --- /dev/null +++ b/xrpl4j-core/src/main/java/org/xrpl/xrpl4j/model/client/oracle/GetAggregatePriceRequestParams.java @@ -0,0 +1,84 @@ +package org.xrpl.xrpl4j.model.client.oracle; + +import com.fasterxml.jackson.annotation.JsonProperty; +import com.fasterxml.jackson.annotation.JsonUnwrapped; +import com.fasterxml.jackson.databind.annotation.JsonDeserialize; +import com.fasterxml.jackson.databind.annotation.JsonSerialize; +import com.google.common.annotations.Beta; +import com.google.common.primitives.UnsignedInteger; +import org.immutables.value.Value.Immutable; +import org.xrpl.xrpl4j.model.client.XrplRequestParams; +import org.xrpl.xrpl4j.model.client.common.LedgerSpecifier; +import org.xrpl.xrpl4j.model.client.ledger.OracleLedgerEntryParams; + +import java.util.List; +import java.util.Optional; + +/** + * Request parameters for the {@code get_aggregate_price} RPC method. + */ +@Beta +@Immutable +@JsonSerialize(as = ImmutableGetAggregatePriceRequestParams.class) +@JsonDeserialize(as = ImmutableGetAggregatePriceRequestParams.class) +public interface GetAggregatePriceRequestParams extends XrplRequestParams { + + /** + * Construct a {@code GetAggregatePriceRequestParams} builder. + * + * @return An {@link ImmutableGetAggregatePriceRequestParams.Builder}. + */ + static ImmutableGetAggregatePriceRequestParams.Builder builder() { + return ImmutableGetAggregatePriceRequestParams.builder(); + } + + /** + * Specifies the ledger version to request. A ledger version can be specified by ledger hash, + * numerical ledger index, or a shortcut value. + * + * @return A {@link LedgerSpecifier} specifying the ledger version to request. + */ + @JsonUnwrapped + LedgerSpecifier ledgerSpecifier(); + + /** + * The currency code of the asset to be priced. + * + * @return A {@link String}. + */ + @JsonProperty("base_asset") + String baseAsset(); + + /** + * The currency code of the asset to quote the price of the base asset. + * + * @return A {@link String}. + */ + @JsonProperty("quote_asset") + String quoteAsset(); + + /** + * The percentage of outliers to trim. Valid trim range is 1-25. If included, the API returns statistics for the + * trimmed mean. + * + * @return An {@link Optional} {@link UnsignedInteger}. + */ + Optional trim(); + + /** + * Defines a time range in seconds for filtering out older price data. Default value is 0, which doesn't filter any + * data. + * + * @return An {@link Optional} {@link UnsignedInteger}. + */ + @JsonProperty("trim_threshold") + Optional trimThreshold(); + + /** + * A list of oracle identifiers. + * + * @return A {@link List} of {@link OracleLedgerEntryParams}. + */ + List oracles(); + +} diff --git a/xrpl4j-core/src/main/java/org/xrpl/xrpl4j/model/client/oracle/GetAggregatePriceResult.java b/xrpl4j-core/src/main/java/org/xrpl/xrpl4j/model/client/oracle/GetAggregatePriceResult.java new file mode 100644 index 000000000..b6a6a161c --- /dev/null +++ b/xrpl4j-core/src/main/java/org/xrpl/xrpl4j/model/client/oracle/GetAggregatePriceResult.java @@ -0,0 +1,160 @@ +package org.xrpl.xrpl4j.model.client.oracle; + +import com.fasterxml.jackson.annotation.JsonIgnore; +import com.fasterxml.jackson.annotation.JsonProperty; +import com.fasterxml.jackson.databind.annotation.JsonDeserialize; +import com.fasterxml.jackson.databind.annotation.JsonSerialize; +import com.google.common.annotations.Beta; +import com.google.common.primitives.UnsignedInteger; +import org.immutables.value.Value; +import org.immutables.value.Value.Immutable; +import org.immutables.value.Value.Lazy; +import org.xrpl.xrpl4j.model.client.XrplResult; +import org.xrpl.xrpl4j.model.client.common.LedgerIndex; +import org.xrpl.xrpl4j.model.transactions.Hash256; + +import java.math.BigDecimal; +import java.util.Optional; + +/** + * Result object for {@code get_aggregate_price} RPC calls. + */ +@Beta +@Immutable +@JsonSerialize(as = ImmutableGetAggregatePriceResult.class) +@JsonDeserialize(as = ImmutableGetAggregatePriceResult.class) +public interface GetAggregatePriceResult extends XrplResult { + + /** + * Construct a {@code GetAggregatePriceResult} builder. + * + * @return An {@link ImmutableGetAggregatePriceResult.Builder}. + */ + static ImmutableGetAggregatePriceResult.Builder builder() { + return ImmutableGetAggregatePriceResult.builder(); + } + + /** + * The statistics from the collected oracle prices. + * + * @return An {@link AggregatePriceSet}. + */ + @JsonProperty("entire_set") + AggregatePriceSet entireSet(); + + /** + * The trimmed statistics from the collected oracle prices. Only appears if the trim field was specified in the + * request. + * + * @return An {@link Optional} {@link AggregatePriceSet}. + */ + @JsonProperty("trimmed_set") + Optional trimmedSet(); + + /** + * The median price, as a {@link String}. + * + * @return A {@link String}. + */ + @JsonProperty("median") + String medianString(); + + /** + * Get the median price as a {@link BigDecimal}. + * + * @return A {@link BigDecimal}. + */ + @Lazy + @JsonIgnore + default BigDecimal median() { + return new BigDecimal(medianString()); + } + + /** + * The most recent timestamp out of all LastUpdateTime values. + * + * @return An {@link UnsignedInteger}. + */ + UnsignedInteger time(); + + /** + * The identifying Hash of the ledger version used to generate this response. + * + * @return A {@link Hash256} containing the ledger hash. + */ + @JsonProperty("ledger_hash") + Optional ledgerHash(); + + /** + * Get {@link #ledgerHash()}, or throw an {@link IllegalStateException} if {@link #ledgerHash()} is empty. + * + * @return The value of {@link #ledgerHash()}. + * + * @throws IllegalStateException If {@link #ledgerHash()} is empty. + */ + @JsonIgnore + @Value.Auxiliary + default Hash256 ledgerHashSafe() { + return ledgerHash() + .orElseThrow(() -> new IllegalStateException("Result did not contain a ledgerHash.")); + } + + /** + * The Ledger Index of the ledger version used to generate this response. Only present in responses to requests with + * ledger_index = "validated" or "closed". + * + * @return A {@link LedgerIndex}. + */ + @JsonProperty("ledger_index") + Optional ledgerIndex(); + + /** + * Get {@link #ledgerIndex()}, or throw an {@link IllegalStateException} if {@link #ledgerIndex()} is empty. + * + * @return The value of {@link #ledgerIndex()}. + * + * @throws IllegalStateException If {@link #ledgerIndex()} is empty. + */ + @JsonIgnore + @Value.Auxiliary + default LedgerIndex ledgerIndexSafe() { + return ledgerIndex() + .orElseThrow(() -> new IllegalStateException("Result did not contain a ledgerIndex.")); + } + + /** + * The ledger index of the current open ledger, which was used when retrieving this information. Only present in + * responses to requests with ledger_index = "current". + * + * @return An optionally-present {@link LedgerIndex} representing the current ledger index. + */ + @JsonProperty("ledger_current_index") + Optional ledgerCurrentIndex(); + + /** + * Get {@link #ledgerCurrentIndex()}, or throw an {@link IllegalStateException} if {@link #ledgerCurrentIndex()} is + * empty. + * + * @return The value of {@link #ledgerCurrentIndex()}. + * + * @throws IllegalStateException If {@link #ledgerCurrentIndex()} is empty. + */ + @JsonIgnore + @Value.Auxiliary + default LedgerIndex ledgerCurrentIndexSafe() { + return ledgerCurrentIndex() + .orElseThrow(() -> new IllegalStateException("Result did not contain a ledgerCurrentIndex.")); + } + + /** + * If true, the information in this response comes from a validated ledger version. Otherwise, the information is + * subject to change. + * + * @return {@code true} if the information in this response comes from a validated ledger version, {@code false} if + * not. + */ + @Value.Default + default boolean validated() { + return false; + } +} diff --git a/xrpl4j-core/src/main/java/org/xrpl/xrpl4j/model/jackson/modules/AssetPriceDeserializer.java b/xrpl4j-core/src/main/java/org/xrpl/xrpl4j/model/jackson/modules/AssetPriceDeserializer.java new file mode 100644 index 000000000..f3535c94c --- /dev/null +++ b/xrpl4j-core/src/main/java/org/xrpl/xrpl4j/model/jackson/modules/AssetPriceDeserializer.java @@ -0,0 +1,28 @@ +package org.xrpl.xrpl4j.model.jackson.modules; + +import com.fasterxml.jackson.core.JsonParser; +import com.fasterxml.jackson.databind.DeserializationContext; +import com.fasterxml.jackson.databind.deser.std.StdDeserializer; +import com.google.common.primitives.UnsignedLong; +import org.xrpl.xrpl4j.model.transactions.AssetPrice; + +import java.io.IOException; + +/** + * Custom Jackson deserializer for {@link AssetPrice}s. + */ +public class AssetPriceDeserializer extends StdDeserializer { + + /** + * No-args constructor. + */ + public AssetPriceDeserializer() { + super(AssetPrice.class); + } + + @Override + public AssetPrice deserialize(JsonParser jsonParser, DeserializationContext ctxt) throws IOException { + // sfAssetPrice is an STUInt64s, which in JSON is represented as a hex-encoded String. + return AssetPrice.of(UnsignedLong.valueOf(jsonParser.getText(), 16)); + } +} diff --git a/xrpl4j-core/src/main/java/org/xrpl/xrpl4j/model/jackson/modules/AssetPriceSerializer.java b/xrpl4j-core/src/main/java/org/xrpl/xrpl4j/model/jackson/modules/AssetPriceSerializer.java new file mode 100644 index 000000000..16a2f2a54 --- /dev/null +++ b/xrpl4j-core/src/main/java/org/xrpl/xrpl4j/model/jackson/modules/AssetPriceSerializer.java @@ -0,0 +1,27 @@ +package org.xrpl.xrpl4j.model.jackson.modules; + +import com.fasterxml.jackson.core.JsonGenerator; +import com.fasterxml.jackson.databind.SerializerProvider; +import com.fasterxml.jackson.databind.ser.std.StdScalarSerializer; +import org.xrpl.xrpl4j.model.transactions.AssetPrice; + +import java.io.IOException; + +/** + * Custom Jackson serializer for {@link AssetPrice}s. + */ +public class AssetPriceSerializer extends StdScalarSerializer { + + /** + * No-args constructor. + */ + public AssetPriceSerializer() { + super(AssetPrice.class, false); + } + + @Override + public void serialize(AssetPrice count, JsonGenerator gen, SerializerProvider provider) throws IOException { + // sfAssetPrice is an STUInt64s, which in JSON is represented as a hex-encoded String. + gen.writeString(count.value().toString(16)); + } +} diff --git a/xrpl4j-core/src/main/java/org/xrpl/xrpl4j/model/jackson/modules/OracleDocumentIdDeserializer.java b/xrpl4j-core/src/main/java/org/xrpl/xrpl4j/model/jackson/modules/OracleDocumentIdDeserializer.java new file mode 100644 index 000000000..995f1399b --- /dev/null +++ b/xrpl4j-core/src/main/java/org/xrpl/xrpl4j/model/jackson/modules/OracleDocumentIdDeserializer.java @@ -0,0 +1,27 @@ +package org.xrpl.xrpl4j.model.jackson.modules; + +import com.fasterxml.jackson.core.JsonParser; +import com.fasterxml.jackson.databind.DeserializationContext; +import com.fasterxml.jackson.databind.deser.std.StdDeserializer; +import com.google.common.primitives.UnsignedInteger; +import org.xrpl.xrpl4j.model.transactions.OracleDocumentId; + +import java.io.IOException; + +/** + * Custom Jackson deserializer for {@link OracleDocumentId}s. + */ +public class OracleDocumentIdDeserializer extends StdDeserializer { + + /** + * No-args constructor. + */ + public OracleDocumentIdDeserializer() { + super(OracleDocumentId.class); + } + + @Override + public OracleDocumentId deserialize(JsonParser jsonParser, DeserializationContext ctxt) throws IOException { + return OracleDocumentId.of(UnsignedInteger.valueOf(jsonParser.getLongValue())); + } +} diff --git a/xrpl4j-core/src/main/java/org/xrpl/xrpl4j/model/jackson/modules/OracleDocumentIdSerializer.java b/xrpl4j-core/src/main/java/org/xrpl/xrpl4j/model/jackson/modules/OracleDocumentIdSerializer.java new file mode 100644 index 000000000..0be3b8249 --- /dev/null +++ b/xrpl4j-core/src/main/java/org/xrpl/xrpl4j/model/jackson/modules/OracleDocumentIdSerializer.java @@ -0,0 +1,30 @@ +package org.xrpl.xrpl4j.model.jackson.modules; + +import com.fasterxml.jackson.core.JsonGenerator; +import com.fasterxml.jackson.databind.SerializerProvider; +import com.fasterxml.jackson.databind.ser.std.StdScalarSerializer; +import org.xrpl.xrpl4j.model.transactions.OracleDocumentId; + +import java.io.IOException; + +/** + * Custom Jackson serializer for {@link OracleDocumentId}s. + */ +public class OracleDocumentIdSerializer extends StdScalarSerializer { + + /** + * No-args constructor. + */ + public OracleDocumentIdSerializer() { + super(OracleDocumentId.class, false); + } + + @Override + public void serialize( + OracleDocumentId oracleDocumentId, + JsonGenerator gen, + SerializerProvider provider + ) throws IOException { + gen.writeNumber(oracleDocumentId.value().longValue()); + } +} diff --git a/xrpl4j-core/src/main/java/org/xrpl/xrpl4j/model/jackson/modules/OracleProviderDeserializer.java b/xrpl4j-core/src/main/java/org/xrpl/xrpl4j/model/jackson/modules/OracleProviderDeserializer.java new file mode 100644 index 000000000..9aa3ddf43 --- /dev/null +++ b/xrpl4j-core/src/main/java/org/xrpl/xrpl4j/model/jackson/modules/OracleProviderDeserializer.java @@ -0,0 +1,27 @@ +package org.xrpl.xrpl4j.model.jackson.modules; + +import com.fasterxml.jackson.core.JsonParser; +import com.fasterxml.jackson.databind.DeserializationContext; +import com.fasterxml.jackson.databind.deser.std.StdDeserializer; +import org.xrpl.xrpl4j.model.transactions.OracleProvider; + +import java.io.IOException; + +/** + * A custom Jackson deserializer to deserialize {@link OracleProvider}s from a hex string in JSON. + */ +public class OracleProviderDeserializer extends StdDeserializer { + + /** + * No-args constructor. + */ + public OracleProviderDeserializer() { + super(OracleProvider.class); + } + + @Override + public OracleProvider deserialize(JsonParser jsonParser, DeserializationContext ctxt) throws IOException { + return OracleProvider.of(jsonParser.getText()); + } + +} diff --git a/xrpl4j-core/src/main/java/org/xrpl/xrpl4j/model/jackson/modules/OracleUriDeserializer.java b/xrpl4j-core/src/main/java/org/xrpl/xrpl4j/model/jackson/modules/OracleUriDeserializer.java new file mode 100644 index 000000000..8648885b9 --- /dev/null +++ b/xrpl4j-core/src/main/java/org/xrpl/xrpl4j/model/jackson/modules/OracleUriDeserializer.java @@ -0,0 +1,27 @@ +package org.xrpl.xrpl4j.model.jackson.modules; + +import com.fasterxml.jackson.core.JsonParser; +import com.fasterxml.jackson.databind.DeserializationContext; +import com.fasterxml.jackson.databind.deser.std.StdDeserializer; +import org.xrpl.xrpl4j.model.transactions.OracleUri; + +import java.io.IOException; + +/** + * A custom Jackson deserializer to deserialize {@link OracleUri}s from a hex string in JSON. + */ +public class OracleUriDeserializer extends StdDeserializer { + + /** + * No-args constructor. + */ + public OracleUriDeserializer() { + super(OracleUri.class); + } + + @Override + public OracleUri deserialize(JsonParser jsonParser, DeserializationContext ctxt) throws IOException { + return OracleUri.of(jsonParser.getText()); + } + +} diff --git a/xrpl4j-core/src/main/java/org/xrpl/xrpl4j/model/ledger/LedgerObject.java b/xrpl4j-core/src/main/java/org/xrpl/xrpl4j/model/ledger/LedgerObject.java index e463234d5..56a5b3bcb 100644 --- a/xrpl4j-core/src/main/java/org/xrpl/xrpl4j/model/ledger/LedgerObject.java +++ b/xrpl4j-core/src/main/java/org/xrpl/xrpl4j/model/ledger/LedgerObject.java @@ -58,10 +58,10 @@ @JsonSubTypes.Type(value = ImmutableBridgeObject.class, name = "Bridge"), @JsonSubTypes.Type( value = ImmutableXChainOwnedCreateAccountClaimIdObject.class, - name = "XChainOwnedCreateAccountClaimID" - ), + name = "XChainOwnedCreateAccountClaimID"), @JsonSubTypes.Type(value = ImmutableXChainOwnedClaimIdObject.class, name = "XChainOwnedClaimID"), @JsonSubTypes.Type(value = ImmutableDidObject.class, name = "DID"), + @JsonSubTypes.Type(value = ImmutableOracleObject.class, name = "Oracle"), }) // TODO: Uncomment subtypes as we implement public interface LedgerObject { @@ -154,8 +154,7 @@ enum LedgerEntryType { * The {@link LedgerEntryType} for {@code AmmObject} ledger objects. * *

This constant will be marked {@link Beta} until the AMM amendment is enabled on mainnet. Its API is subject - * to - * change.

+ * to change.

*/ @Beta AMM("AMM"), @@ -194,7 +193,16 @@ enum LedgerEntryType { * Its API is subject to change.

*/ @Beta - DID("DID"); + DID("DID"), + + /** + * The {@link LedgerEntryType} for {@code Oracle} ledger objects. + * + *

This constant will be marked {@link Beta} until the featurePriceOracle amendment is enabled on mainnet. + * Its API is subject to change.

+ */ + @Beta + ORACLE("Oracle"); private final String value; diff --git a/xrpl4j-core/src/main/java/org/xrpl/xrpl4j/model/ledger/OracleObject.java b/xrpl4j-core/src/main/java/org/xrpl/xrpl4j/model/ledger/OracleObject.java new file mode 100644 index 000000000..3d492efbf --- /dev/null +++ b/xrpl4j-core/src/main/java/org/xrpl/xrpl4j/model/ledger/OracleObject.java @@ -0,0 +1,153 @@ +package org.xrpl.xrpl4j.model.ledger; + +import com.fasterxml.jackson.annotation.JsonProperty; +import com.fasterxml.jackson.databind.annotation.JsonDeserialize; +import com.fasterxml.jackson.databind.annotation.JsonSerialize; +import com.google.common.annotations.Beta; +import com.google.common.primitives.UnsignedInteger; +import com.google.common.primitives.UnsignedLong; +import org.immutables.value.Value; +import org.immutables.value.Value.Immutable; +import org.xrpl.xrpl4j.model.flags.Flags; +import org.xrpl.xrpl4j.model.transactions.Address; +import org.xrpl.xrpl4j.model.transactions.DidData; +import org.xrpl.xrpl4j.model.transactions.DidDocument; +import org.xrpl.xrpl4j.model.transactions.DidUri; +import org.xrpl.xrpl4j.model.transactions.Hash256; +import org.xrpl.xrpl4j.model.transactions.OracleProvider; +import org.xrpl.xrpl4j.model.transactions.OracleUri; +import org.xrpl.xrpl4j.model.transactions.PriceDataWrapper; + +import java.util.List; +import java.util.Optional; + +/** + * An Oracle ledger entry holds data associated with a single price oracle object. + * + *

This interface will be marked {@link Beta} until the featurePriceOracle amendment is enabled on mainnet. Its API + * is subject to change.

+ */ +@Beta +@Immutable +@JsonSerialize(as = ImmutableOracleObject.class) +@JsonDeserialize(as = ImmutableOracleObject.class) +public interface OracleObject extends LedgerObject { + + /** + * Construct a {@code OracleObject} builder. + * + * @return An {@link ImmutableOracleObject.Builder}. + */ + static ImmutableOracleObject.Builder builder() { + return ImmutableOracleObject.builder(); + } + + /** + * The type of ledger object, which will always be "Oracle" in this case. + * + * @return Always returns {@link LedgerEntryType#ORACLE}. + */ + @JsonProperty("LedgerEntryType") + @Value.Derived + default LedgerEntryType ledgerEntryType() { + return LedgerEntryType.ORACLE; + } + + /** + * A bit-map of boolean flags. No flags are defined for {@link OracleObject}, so this value is always 0. + * + * @return Always {@link Flags#UNSET}. + */ + @JsonProperty("Flags") + @Value.Derived + default Flags flags() { + return Flags.UNSET; + } + + /** + * The XRPL account with update and delete privileges for the oracle. It's recommended to set up multi-signing on this + * account. + * + * @return An {@link Address}. + */ + @JsonProperty("Owner") + Address owner(); + + /** + * An arbitrary value that identifies an oracle provider, such as Chainlink, Band, or DIA. This field is a string, up + * to 256 ASCII hex encoded characters (0x20-0x7E). + * + * @return An {@link OracleProvider}. + */ + @JsonProperty("Provider") + OracleProvider provider(); + + /** + * An array of up to 10 PriceData objects, each representing the price information for a token pair. More than five + * PriceData objects require two owner reserves. + * + * @return A {@link List} of {@link PriceDataWrapper}. + */ + @JsonProperty("PriceDataSeries") + List priceDataSeries(); + + /** + * Describes the type of asset, such as "currency", "commodity", or "index". This field is a string, up to 16 ASCII + * hex encoded characters (0x20-0x7E). + * + * @return A {@link String}. + */ + @JsonProperty("AssetClass") + String assetClass(); + + /** + * The time the data was last updated, represented in Unix time. + * + * @return An {@link UnsignedInteger}. + */ + @JsonProperty("LastUpdateTime") + UnsignedInteger lastUpdateTime(); + + /** + * An optional Universal Resource Identifier to reference price data off-chain. This field is limited to 256 bytes. + * + * @return An {@link Optional} {@link OracleUri}. + */ + @JsonProperty("URI") + Optional uri(); + + /** + * A hint indicating which page of the sender's owner directory links to this object, in case the directory consists + * of multiple pages. + * + *

Note: The object does not contain a direct link to the owner directory containing it, since that value can be + * derived from the Account. + * + * @return A {@link String} containing the owner node hint. + */ + @JsonProperty("OwnerNode") + String ownerNode(); + + /** + * The identifying hash of the transaction that most recently modified this object. + * + * @return A {@link Hash256} containing the previous transaction hash. + */ + @JsonProperty("PreviousTxnID") + Hash256 previousTransactionId(); + + /** + * The index of the ledger that contains the transaction that most recently modified this object. + * + * @return An {@link UnsignedInteger} representing the previous transaction ledger sequence. + */ + @JsonProperty("PreviousTxnLgrSeq") + UnsignedInteger previousTransactionLedgerSequence(); + + /** + * The unique ID of the {@link OracleObject}. + * + * @return A {@link Hash256} containing the ID. + */ + Hash256 index(); +} diff --git a/xrpl4j-core/src/main/java/org/xrpl/xrpl4j/model/transactions/OracleDelete.java b/xrpl4j-core/src/main/java/org/xrpl/xrpl4j/model/transactions/OracleDelete.java new file mode 100644 index 000000000..b8e81ea7c --- /dev/null +++ b/xrpl4j-core/src/main/java/org/xrpl/xrpl4j/model/transactions/OracleDelete.java @@ -0,0 +1,53 @@ +package org.xrpl.xrpl4j.model.transactions; + +import com.fasterxml.jackson.annotation.JsonProperty; +import com.fasterxml.jackson.databind.annotation.JsonDeserialize; +import com.fasterxml.jackson.databind.annotation.JsonSerialize; +import com.google.common.annotations.Beta; +import com.google.common.primitives.UnsignedInteger; +import org.immutables.value.Value; +import org.immutables.value.Value.Immutable; +import org.xrpl.xrpl4j.model.flags.TransactionFlags; + +/** + * Delete an Oracle ledger entry. + */ +@Beta +@Immutable +@JsonSerialize(as = ImmutableOracleDelete.class) +@JsonDeserialize(as = ImmutableOracleDelete.class) +public interface OracleDelete extends Transaction { + + /** + * Construct a {@code OracleDelete} builder. + * + * @return An {@link ImmutableOracleDelete.Builder}. + */ + static ImmutableOracleDelete.Builder builder() { + return ImmutableOracleDelete.builder(); + } + + /** + * Set of {@link TransactionFlags}s for this {@link OracleDelete}, which only allows the {@code tfFullyCanonicalSig} + * flag, which is deprecated. + * + *

The value of the flags can be set manually, but exists mostly for JSON serialization/deserialization only and + * for proper signature computation in rippled. + * + * @return Always {@link TransactionFlags#EMPTY}. + */ + @JsonProperty("Flags") + @Value.Default + default TransactionFlags flags() { + return TransactionFlags.EMPTY; + } + + /** + * A unique identifier of the price oracle for the account. + * + * @return An {@link UnsignedInteger}. + */ + @JsonProperty("OracleDocumentID") + OracleDocumentId oracleDocumentId(); + +} diff --git a/xrpl4j-core/src/main/java/org/xrpl/xrpl4j/model/transactions/OracleSet.java b/xrpl4j-core/src/main/java/org/xrpl/xrpl4j/model/transactions/OracleSet.java new file mode 100644 index 000000000..7116087e8 --- /dev/null +++ b/xrpl4j-core/src/main/java/org/xrpl/xrpl4j/model/transactions/OracleSet.java @@ -0,0 +1,100 @@ +package org.xrpl.xrpl4j.model.transactions; + +import com.fasterxml.jackson.annotation.JsonProperty; +import com.fasterxml.jackson.databind.annotation.JsonDeserialize; +import com.fasterxml.jackson.databind.annotation.JsonSerialize; +import com.google.common.annotations.Beta; +import com.google.common.primitives.UnsignedInteger; +import org.immutables.value.Value; +import org.immutables.value.Value.Immutable; +import org.xrpl.xrpl4j.model.flags.TransactionFlags; + +import java.util.List; +import java.util.Optional; + +/** + * Creates a new Oracle ledger entry or updates the fields of an existing one, using the Oracle ID. + */ +@Beta +@Immutable +@JsonSerialize(as = ImmutableOracleSet.class) +@JsonDeserialize(as = ImmutableOracleSet.class) +public interface OracleSet extends Transaction { + + /** + * Construct a {@code OracleSet} builder. + * + * @return An {@link ImmutableOracleSet.Builder}. + */ + static ImmutableOracleSet.Builder builder() { + return ImmutableOracleSet.builder(); + } + + /** + * Set of {@link TransactionFlags}s for this {@link OracleSet}, which only allows the {@code tfFullyCanonicalSig} + * flag, which is deprecated. + * + *

The value of the flags can be set manually, but exists mostly for JSON serialization/deserialization only and + * for proper signature computation in rippled. + * + * @return Always {@link TransactionFlags#EMPTY}. + */ + @JsonProperty("Flags") + @Value.Default + default TransactionFlags flags() { + return TransactionFlags.EMPTY; + } + + /** + * A unique identifier of the price oracle for the account. + * + * @return An {@link UnsignedInteger}. + */ + @JsonProperty("OracleDocumentID") + OracleDocumentId oracleDocumentId(); + + /** + * An arbitrary value that identifies an oracle provider, such as Chainlink, Band, or DIA. This field is a string, up + * to 256 ASCII hex encoded characters (0x20-0x7E). This field is required when creating a new Oracle ledger entry, + * but is optional for updates. + * + * @return An {@link Optional} {@link String}. + */ + @JsonProperty("Provider") + Optional provider(); + + /** + * An optional Universal Resource Identifier to reference price data off-chain. This field is limited to 256 bytes. + * + * @return An {@link Optional} {@link String}. + */ + @JsonProperty("URI") + Optional uri(); + + /** + * The time the data was last updated, represented in Unix time. + * + * @return An {@link UnsignedInteger}. + */ + @JsonProperty("LastUpdateTime") + UnsignedInteger lastUpdateTime(); + + /** + * Describes the type of asset, such as "currency", "commodity", or "index". This field is a string, up to 16 ASCII + * hex encoded characters (0x20-0x7E). This field is required when creating a new Oracle ledger entry, but is optional + * for updates. + * + * @return An {@link Optional} {@link String}. + */ + @JsonProperty("AssetClass") + Optional assetClass(); + + /** + * An array of up to 10 PriceData objects, each representing the price information for a token pair. More than five + * PriceData objects require two owner reserves. + * + * @return A {@link List} of {@link PriceData}. + */ + @JsonProperty("PriceDataSeries") + List priceDataSeries(); +} diff --git a/xrpl4j-core/src/main/java/org/xrpl/xrpl4j/model/transactions/PriceData.java b/xrpl4j-core/src/main/java/org/xrpl/xrpl4j/model/transactions/PriceData.java new file mode 100644 index 000000000..c14e04c61 --- /dev/null +++ b/xrpl4j-core/src/main/java/org/xrpl/xrpl4j/model/transactions/PriceData.java @@ -0,0 +1,68 @@ +package org.xrpl.xrpl4j.model.transactions; + +import com.fasterxml.jackson.annotation.JsonProperty; +import com.fasterxml.jackson.databind.annotation.JsonDeserialize; +import com.fasterxml.jackson.databind.annotation.JsonSerialize; +import com.google.common.annotations.Beta; +import com.google.common.primitives.UnsignedInteger; +import com.google.common.primitives.UnsignedLong; +import org.immutables.value.Value.Immutable; + +import java.util.Optional; + +/** + * Represents price information for a token pair in an Oracle. + */ +@Beta +@Immutable +@JsonSerialize(as = ImmutablePriceData.class) +@JsonDeserialize(as = ImmutablePriceData.class) +public interface PriceData { + + /** + * Construct a {@code PriceData} builder. + * + * @return An {@link ImmutablePriceData.Builder}. + */ + static ImmutablePriceData.Builder builder() { + return ImmutablePriceData.builder(); + } + + /** + * The primary asset in a trading pair. Any valid identifier, such as a stock symbol, bond CUSIP, or currency code is + * allowed. For example, in the BTC/USD pair, BTC is the base asset; in 912810RR9/BTC, 912810RR9 is the base asset. + * + * @return A {@link String}. + */ + @JsonProperty("BaseAsset") + String baseAsset(); + + /** + * The quote asset in a trading pair. The quote asset denotes the price of one unit of the base asset. For example, in + * the BTC/USD pair, BTC is the base asset; in 912810RR9/BTC, 912810RR9 is the base asset. + * + * @return A {@link String}. + */ + @JsonProperty("QuoteAsset") + String quoteAsset(); + + /** + * The asset price after applying the {@link #scale()} precision level. It's not included if the last update + * transaction didn't include the {@link #baseAsset()}/{@link #quoteAsset()} pair. + * + * @return An {@link Optional} {@link AssetPrice}. + */ + @JsonProperty("AssetPrice") + Optional assetPrice(); + + /** + * The scaling factor to apply to an asset price. For example, if scale is 6 and original price is 0.155, then the + * scaled price is 155000. Valid scale ranges are 0-10. It's not included if the last update transaction didn't + * include the {@link #baseAsset()}/{@link #quoteAsset()} pair. + * + * @return An {@link Optional} {@link UnsignedInteger}. + */ + @JsonProperty("Scale") + Optional scale(); + +} diff --git a/xrpl4j-core/src/main/java/org/xrpl/xrpl4j/model/transactions/PriceDataWrapper.java b/xrpl4j-core/src/main/java/org/xrpl/xrpl4j/model/transactions/PriceDataWrapper.java new file mode 100644 index 000000000..46b1a1653 --- /dev/null +++ b/xrpl4j-core/src/main/java/org/xrpl/xrpl4j/model/transactions/PriceDataWrapper.java @@ -0,0 +1,29 @@ +package org.xrpl.xrpl4j.model.transactions; + +import com.fasterxml.jackson.annotation.JsonProperty; +import com.fasterxml.jackson.databind.annotation.JsonDeserialize; +import com.fasterxml.jackson.databind.annotation.JsonSerialize; +import org.immutables.value.Value.Immutable; + +/** + * Wrapper object for {@link PriceData}, because the PriceData field is an STArray which, in JSON, has elements of + * wrapped objects. + */ +@Immutable +@JsonSerialize(as = ImmutablePriceDataWrapper.class) +@JsonDeserialize(as = ImmutablePriceDataWrapper.class) +public interface PriceDataWrapper { + + static PriceDataWrapper of(PriceData priceData) { + return ImmutablePriceDataWrapper.builder().priceData(priceData).build(); + } + + /** + * The price data. + * + * @return A {@link PriceData}. + */ + @JsonProperty("PriceData") + PriceData priceData(); + +} diff --git a/xrpl4j-core/src/main/java/org/xrpl/xrpl4j/model/transactions/Transaction.java b/xrpl4j-core/src/main/java/org/xrpl/xrpl4j/model/transactions/Transaction.java index 40c3e7695..9f4f1acaf 100644 --- a/xrpl4j-core/src/main/java/org/xrpl/xrpl4j/model/transactions/Transaction.java +++ b/xrpl4j-core/src/main/java/org/xrpl/xrpl4j/model/transactions/Transaction.java @@ -88,6 +88,8 @@ public interface Transaction { .put(ImmutableXChainModifyBridge.class, TransactionType.XCHAIN_MODIFY_BRIDGE) .put(ImmutableDidSet.class, TransactionType.DID_SET) .put(ImmutableDidDelete.class, TransactionType.DID_DELETE) + .put(ImmutableOracleSet.class, TransactionType.ORACLE_SET) + .put(ImmutableOracleDelete.class, TransactionType.ORACLE_DELETE) .build(); /** diff --git a/xrpl4j-core/src/main/java/org/xrpl/xrpl4j/model/transactions/TransactionType.java b/xrpl4j-core/src/main/java/org/xrpl/xrpl4j/model/transactions/TransactionType.java index 244e00544..bc19f8dbf 100644 --- a/xrpl4j-core/src/main/java/org/xrpl/xrpl4j/model/transactions/TransactionType.java +++ b/xrpl4j-core/src/main/java/org/xrpl/xrpl4j/model/transactions/TransactionType.java @@ -318,7 +318,25 @@ public enum TransactionType { * is subject to change.

*/ @Beta - DID_DELETE("DIDDelete"); + DID_DELETE("DIDDelete"), + + /** + * The {@link TransactionType} for the {@link OracleSet} transaction. + * + *

This constant will be marked {@link Beta} until the featurePriceOracle amendment is enabled on mainnet. Its API + * is subject to change.

+ */ + @Beta + ORACLE_SET("OracleSet"), + + /** + * The {@link TransactionType} for the {@link OracleDelete} transaction. + * + *

This constant will be marked {@link Beta} until the featurePriceOracle amendment is enabled on mainnet. Its API + * is subject to change.

+ */ + @Beta + ORACLE_DELETE("OracleDelete"); private final String value; diff --git a/xrpl4j-core/src/main/java/org/xrpl/xrpl4j/model/transactions/Wrappers.java b/xrpl4j-core/src/main/java/org/xrpl/xrpl4j/model/transactions/Wrappers.java index f961875d7..b05a354ef 100644 --- a/xrpl4j-core/src/main/java/org/xrpl/xrpl4j/model/transactions/Wrappers.java +++ b/xrpl4j-core/src/main/java/org/xrpl/xrpl4j/model/transactions/Wrappers.java @@ -24,6 +24,7 @@ import com.fasterxml.jackson.annotation.JsonRawValue; import com.fasterxml.jackson.databind.annotation.JsonDeserialize; import com.fasterxml.jackson.databind.annotation.JsonSerialize; +import com.fasterxml.jackson.databind.ser.std.ToStringSerializer; import com.google.common.annotations.Beta; import com.google.common.base.Preconditions; import com.google.common.io.BaseEncoding; @@ -36,6 +37,8 @@ import org.xrpl.xrpl4j.model.immutables.Wrapper; import org.xrpl.xrpl4j.model.jackson.modules.AddressDeserializer; import org.xrpl.xrpl4j.model.jackson.modules.AddressSerializer; +import org.xrpl.xrpl4j.model.jackson.modules.AssetPriceDeserializer; +import org.xrpl.xrpl4j.model.jackson.modules.AssetPriceSerializer; import org.xrpl.xrpl4j.model.jackson.modules.DidDataDeserializer; import org.xrpl.xrpl4j.model.jackson.modules.DidDataSerializer; import org.xrpl.xrpl4j.model.jackson.modules.DidDocumentDeserializer; @@ -51,6 +54,10 @@ import org.xrpl.xrpl4j.model.jackson.modules.NfTokenIdDeserializer; import org.xrpl.xrpl4j.model.jackson.modules.NfTokenIdSerializer; import org.xrpl.xrpl4j.model.jackson.modules.NfTokenUriSerializer; +import org.xrpl.xrpl4j.model.jackson.modules.OracleDocumentIdDeserializer; +import org.xrpl.xrpl4j.model.jackson.modules.OracleDocumentIdSerializer; +import org.xrpl.xrpl4j.model.jackson.modules.OracleProviderDeserializer; +import org.xrpl.xrpl4j.model.jackson.modules.OracleUriDeserializer; import org.xrpl.xrpl4j.model.jackson.modules.TradingFeeDeserializer; import org.xrpl.xrpl4j.model.jackson.modules.TradingFeeSerializer; import org.xrpl.xrpl4j.model.jackson.modules.TransferFeeDeserializer; @@ -681,4 +688,84 @@ public String toString() { } } + + /** + * A wrapped {@link UnsignedInteger} containing an Oracle document ID. + * + *

This class will be marked {@link com.google.common.annotations.Beta} until the featurePriceOracle amendment is + * enabled on mainnet. Its API is subject to change.

+ */ + @Value.Immutable + @Wrapped + @JsonSerialize(as = OracleDocumentId.class, using = OracleDocumentIdSerializer.class) + @JsonDeserialize(as = OracleDocumentId.class, using = OracleDocumentIdDeserializer.class) + @Beta + abstract static class _OracleDocumentId extends Wrapper implements Serializable { + + @Override + public String toString() { + return this.value().toString(); + } + + } + + /** + * A wrapped {@link String} containing an Oracle provider. + * + *

This class will be marked {@link com.google.common.annotations.Beta} until the featurePriceOracle amendment is + * enabled on mainnet. Its API is subject to change.

+ */ + @Value.Immutable + @Wrapped + @JsonSerialize(as = OracleProvider.class, using = ToStringSerializer.class) + @JsonDeserialize(as = OracleProvider.class, using = OracleProviderDeserializer.class) + @Beta + abstract static class _OracleProvider extends Wrapper implements Serializable { + + @Override + public String toString() { + return this.value(); + } + + } + + /** + * A wrapped {@link String} containing an Oracle URI. + * + *

This class will be marked {@link com.google.common.annotations.Beta} until the featurePriceOracle amendment is + * enabled on mainnet. Its API is subject to change.

+ */ + @Value.Immutable + @Wrapped + @JsonSerialize(as = OracleUri.class, using = ToStringSerializer.class) + @JsonDeserialize(as = OracleUri.class, using = OracleUriDeserializer.class) + @Beta + abstract static class _OracleUri extends Wrapper implements Serializable { + + @Override + public String toString() { + return this.value(); + } + + } + + /** + * A wrapped {@link String} containing an Oracle asset price. + * + *

This class will be marked {@link com.google.common.annotations.Beta} until the featurePriceOracle amendment is + * enabled on mainnet. Its API is subject to change.

+ */ + @Value.Immutable + @Wrapped + @JsonSerialize(as = AssetPrice.class, using = AssetPriceSerializer.class) + @JsonDeserialize(as = AssetPrice.class, using = AssetPriceDeserializer.class) + @Beta + abstract static class _AssetPrice extends Wrapper implements Serializable { + + @Override + public String toString() { + return this.value().toString(); + } + + } } diff --git a/xrpl4j-core/src/main/java/org/xrpl/xrpl4j/model/transactions/metadata/MetaLedgerEntryType.java b/xrpl4j-core/src/main/java/org/xrpl/xrpl4j/model/transactions/metadata/MetaLedgerEntryType.java index b913bc70f..f38c96546 100644 --- a/xrpl4j-core/src/main/java/org/xrpl/xrpl4j/model/transactions/metadata/MetaLedgerEntryType.java +++ b/xrpl4j-core/src/main/java/org/xrpl/xrpl4j/model/transactions/metadata/MetaLedgerEntryType.java @@ -44,6 +44,9 @@ public interface MetaLedgerEntryType { @Beta MetaLedgerEntryType DID = MetaLedgerEntryType.of("DID"); + @Beta + MetaLedgerEntryType ORACLE = MetaLedgerEntryType.of("Oracle"); + /** * Construct a new {@link MetaLedgerEntryType} from a {@link String}. * @@ -98,6 +101,8 @@ default Class ledgerObjectType() { return MetaXChainOwnedCreateAccountClaimIdObject.class; case "DID": return MetaDidObject.class; + case "Oracle": + return MetaOracleObject.class; default: return MetaUnknownObject.class; } diff --git a/xrpl4j-core/src/main/java/org/xrpl/xrpl4j/model/transactions/metadata/MetaOracleObject.java b/xrpl4j-core/src/main/java/org/xrpl/xrpl4j/model/transactions/metadata/MetaOracleObject.java new file mode 100644 index 000000000..c15bbb279 --- /dev/null +++ b/xrpl4j-core/src/main/java/org/xrpl/xrpl4j/model/transactions/metadata/MetaOracleObject.java @@ -0,0 +1,114 @@ +package org.xrpl.xrpl4j.model.transactions.metadata; + +import com.fasterxml.jackson.annotation.JsonProperty; +import com.fasterxml.jackson.databind.annotation.JsonDeserialize; +import com.fasterxml.jackson.databind.annotation.JsonSerialize; +import com.google.common.annotations.Beta; +import com.google.common.primitives.UnsignedInteger; +import org.immutables.value.Value; +import org.immutables.value.Value.Immutable; +import org.xrpl.xrpl4j.model.flags.Flags; +import org.xrpl.xrpl4j.model.transactions.Address; +import org.xrpl.xrpl4j.model.transactions.Hash256; +import org.xrpl.xrpl4j.model.transactions.OracleProvider; +import org.xrpl.xrpl4j.model.transactions.OracleUri; +import org.xrpl.xrpl4j.model.transactions.PriceDataWrapper; + +import java.util.List; +import java.util.Optional; + +/** + * Transaction metadata for Oracle objects. + */ +@Beta +@Immutable +@JsonSerialize(as = ImmutableMetaOracleObject.class) +@JsonDeserialize(as = ImmutableMetaOracleObject.class) +public interface MetaOracleObject extends MetaLedgerObject { + + @JsonProperty("Flags") + @Value.Derived + default Flags flags() { + return Flags.UNSET; + } + + /** + * The XRPL account with update and delete privileges for the oracle. It's recommended to set up multi-signing on this + * account. + * + * @return An {@link Address}. + */ + @JsonProperty("Owner") + Optional
owner(); + + /** + * An arbitrary value that identifies an oracle provider, such as Chainlink, Band, or DIA. This field is a string, up + * to 256 ASCII hex encoded characters (0x20-0x7E). + * + * @return An {@link OracleProvider}. + */ + @JsonProperty("Provider") + Optional provider(); + + /** + * An array of up to 10 PriceData objects, each representing the price information for a token pair. More than five + * PriceData objects require two owner reserves. + * + * @return A {@link List} of {@link PriceDataWrapper}. + */ + @JsonProperty("PriceDataSeries") + List priceDataSeries(); + + /** + * Describes the type of asset, such as "currency", "commodity", or "index". This field is a string, up to 16 ASCII + * hex encoded characters (0x20-0x7E). + * + * @return A {@link String}. + */ + @JsonProperty("AssetClass") + Optional assetClass(); + + /** + * The time the data was last updated, represented in Unix time. + * + * @return An {@link UnsignedInteger}. + */ + @JsonProperty("LastUpdateTime") + Optional lastUpdateTime(); + + /** + * An optional Universal Resource Identifier to reference price data off-chain. This field is limited to 256 bytes. + * + * @return An {@link Optional} {@link OracleUri}. + */ + @JsonProperty("URI") + Optional uri(); + + /** + * A hint indicating which page of the sender's owner directory links to this object, in case the directory consists + * of multiple pages. + * + *

Note: The object does not contain a direct link to the owner directory containing it, since that value can be + * derived from the Account. + * + * @return A {@link String} containing the owner node hint. + */ + @JsonProperty("OwnerNode") + Optional ownerNode(); + + /** + * The identifying hash of the transaction that most recently modified this object. + * + * @return A {@link Hash256} containing the previous transaction hash. + */ + @JsonProperty("PreviousTxnID") + Optional previousTransactionId(); + + /** + * The index of the ledger that contains the transaction that most recently modified this object. + * + * @return An {@link UnsignedInteger} representing the previous transaction ledger sequence. + */ + @JsonProperty("PreviousTxnLgrSeq") + Optional previousTransactionLedgerSequence(); +} diff --git a/xrpl4j-core/src/main/resources/definitions.json b/xrpl4j-core/src/main/resources/definitions.json index b8fd9a8a1..797be9ce2 100644 --- a/xrpl4j-core/src/main/resources/definitions.json +++ b/xrpl4j-core/src/main/resources/definitions.json @@ -23,6 +23,7 @@ "UInt512": 23, "Issue": 24, "XChainBridge": 25, + "Currency": 26, "Transaction": 10001, "LedgerEntry": 10002, "Validation": 10003, @@ -51,6 +52,7 @@ "NFTokenOffer": 55, "AMM": 121, "DID": 73, + "Oracle": 128, "Any": -3, "Child": -2, "Nickname": 110, @@ -208,6 +210,16 @@ "type": "UInt8" } ], + [ + "Scale", + { + "nth": 4, + "isVLEncoded": false, + "isSerialized": true, + "isSigningField": true, + "type": "UInt8" + } + ], [ "TickSize", { @@ -498,6 +510,16 @@ "type": "UInt32" } ], + [ + "LastUpdateTime", + { + "nth": 15, + "isVLEncoded": false, + "isSerialized": true, + "isSigningField": true, + "type": "UInt32" + } + ], [ "HighQualityIn", { @@ -828,6 +850,16 @@ "type": "UInt32" } ], + [ + "OracleDocumentID", + { + "nth": 51, + "isVLEncoded": false, + "isSerialized": true, + "isSigningField": true, + "type": "UInt32" + } + ], [ "IndexNext", { @@ -1028,6 +1060,16 @@ "type": "UInt64" } ], + [ + "AssetPrice", + { + "nth": 23, + "isVLEncoded": false, + "isSerialized": true, + "isSigningField": true, + "type": "UInt64" + } + ], [ "EmailHash", { @@ -1918,6 +1960,26 @@ "type": "Blob" } ], + [ + "AssetClass", + { + "nth": 28, + "isVLEncoded": true, + "isSerialized": true, + "isSigningField": true, + "type": "Blob" + } + ], + [ + "Provider", + { + "nth": 29, + "isVLEncoded": true, + "isSerialized": true, + "isSigningField": true, + "type": "Blob" + } + ], [ "Account", { @@ -2128,6 +2190,26 @@ "type": "PathSet" } ], + [ + "BaseAsset", + { + "nth": 1, + "isVLEncoded": false, + "isSerialized": true, + "isSigningField": true, + "type": "Currency" + } + ], + [ + "QuoteAsset", + { + "nth": 2, + "isVLEncoded": false, + "isSerialized": true, + "isSigningField": true, + "type": "Currency" + } + ], [ "LockingChainIssue", { @@ -2458,6 +2540,16 @@ "type": "STObject" } ], + [ + "PriceData", + { + "nth": 32, + "isVLEncoded": false, + "isSerialized": true, + "isSigningField": true, + "type": "STObject" + } + ], [ "Signers", { @@ -2628,6 +2720,16 @@ "type": "STArray" } ], + [ + "PriceDataSeries", + { + "nth": 24, + "isVLEncoded": false, + "isSerialized": true, + "isSigningField": true, + "type": "STArray" + } + ], [ "AuthAccounts", { @@ -2656,6 +2758,7 @@ "telWRONG_NETWORK": -386, "telREQUIRES_NETWORK_ID": -385, "telNETWORK_ID_MAKES_TX_NON_CANONICAL": -384, + "telENV_RPC_FAILED": -383, "temMALFORMED": -299, "temBAD_AMOUNT": -298, @@ -2703,6 +2806,8 @@ "temXCHAIN_BRIDGE_BAD_MIN_ACCOUNT_CREATE_AMOUNT": -256, "temXCHAIN_BRIDGE_BAD_REWARD_AMOUNT": -255, "temEMPTY_DID": -254, + "temARRAY_EMPTY": -253, + "temARRAY_TOO_LARGE": -252, "tefFAILURE": -199, "tefALREADY": -198, @@ -2739,7 +2844,6 @@ "terQUEUED": -89, "terPRE_TICKET": -88, "terNO_AMM": -87, - "terSUBMITTED": -86, "tesSUCCESS": 0, @@ -2815,7 +2919,11 @@ "tecXCHAIN_SELF_COMMIT": 184, "tecXCHAIN_BAD_PUBLIC_KEY_ACCOUNT_PAIR": 185, "tecXCHAIN_CREATE_ACCOUNT_DISABLED": 186, - "tecEMPTY_DID": 187 + "tecEMPTY_DID": 187, + "tecINVALID_UPDATE_TIME": 188, + "tecTOKEN_PAIR_NOT_FOUND": 189, + "tecARRAY_EMPTY": 190, + "tecARRAY_TOO_LARGE": 191 }, "TRANSACTION_TYPES": { "Invalid": -1, @@ -2864,6 +2972,8 @@ "XChainCreateBridge": 48, "DIDSet": 49, "DIDDelete": 50, + "OracleSet": 51, + "OracleDelete": 52, "EnableAmendment": 100, "SetFee": 101, "UNLModify": 102 diff --git a/xrpl4j-core/src/test/java/org/xrpl/xrpl4j/codec/binary/BinarySerializationTests.java b/xrpl4j-core/src/test/java/org/xrpl/xrpl4j/codec/binary/BinarySerializationTests.java index d22654441..524737872 100644 --- a/xrpl4j-core/src/test/java/org/xrpl/xrpl4j/codec/binary/BinarySerializationTests.java +++ b/xrpl4j-core/src/test/java/org/xrpl/xrpl4j/codec/binary/BinarySerializationTests.java @@ -52,6 +52,7 @@ import org.xrpl.xrpl4j.model.transactions.AccountDelete; import org.xrpl.xrpl4j.model.transactions.AccountSet; import org.xrpl.xrpl4j.model.transactions.Address; +import org.xrpl.xrpl4j.model.transactions.AssetPrice; import org.xrpl.xrpl4j.model.transactions.CheckCancel; import org.xrpl.xrpl4j.model.transactions.CheckCash; import org.xrpl.xrpl4j.model.transactions.CheckCreate; @@ -68,6 +69,7 @@ import org.xrpl.xrpl4j.model.transactions.Hash256; import org.xrpl.xrpl4j.model.transactions.ImmutableDidDelete; import org.xrpl.xrpl4j.model.transactions.ImmutableDidSet; +import org.xrpl.xrpl4j.model.transactions.ImmutableOracleDelete; import org.xrpl.xrpl4j.model.transactions.ImmutableXChainAccountCreateCommit; import org.xrpl.xrpl4j.model.transactions.ImmutableXChainAddClaimAttestation; import org.xrpl.xrpl4j.model.transactions.ImmutableXChainClaim; @@ -78,10 +80,16 @@ import org.xrpl.xrpl4j.model.transactions.NetworkId; 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.OracleDocumentId; +import org.xrpl.xrpl4j.model.transactions.OracleProvider; +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; import org.xrpl.xrpl4j.model.transactions.PaymentChannelFund; +import org.xrpl.xrpl4j.model.transactions.PriceData; +import org.xrpl.xrpl4j.model.transactions.PriceDataWrapper; import org.xrpl.xrpl4j.model.transactions.SetRegularKey; import org.xrpl.xrpl4j.model.transactions.SignerListSet; import org.xrpl.xrpl4j.model.transactions.Transaction; @@ -2120,6 +2128,98 @@ void serializeDidDelete() throws JsonProcessingException { assertSerializesAndDeserializes(transaction, binary); } + @Test + void serializeOracleSet() throws JsonProcessingException { + OracleSet oracleSet = OracleSet.builder() + .account(Address.of("rMS69A6J39RmBg5yWDft5XAM8zTGbtMMZy")) + .assetClass("63757272656E6379") + .fee(XrpCurrencyAmount.ofDrops(10)) + .oracleDocumentId(OracleDocumentId.of(UnsignedInteger.valueOf(3))) + .provider(OracleProvider.of("68747470733A2F2F74687265657872702E646576")) + .sequence(UnsignedInteger.valueOf(2019238)) + .signingPublicKey(PublicKey.fromBase16EncodedPublicKey( + "EDA6501D3E53D47F10AE37A0C6B34194CF8A205DE9611FE81B63E7B62105E90EAC" + )) + .lastUpdateTime(UnsignedInteger.valueOf(1715785124)) + .addPriceDataSeries( + PriceDataWrapper.of( + PriceData.builder() + .assetPrice(AssetPrice.of(UnsignedLong.valueOf("1ff4", 16))) + .baseAsset("XRP") + .quoteAsset("IDR") + .build() + ), + PriceDataWrapper.of( + PriceData.builder() + .assetPrice(AssetPrice.of(UnsignedLong.valueOf("13652", 16))) + .baseAsset("XRP") + .quoteAsset("JPY") + .scale(UnsignedInteger.valueOf(3)) + .build() + ), + PriceDataWrapper.of( + PriceData.builder() + .assetPrice(AssetPrice.of(UnsignedLong.valueOf("117bf", 16))) + .baseAsset("XRP") + .quoteAsset("KRW") + .scale(UnsignedInteger.valueOf(2)) + .build() + ), + PriceDataWrapper.of( + PriceData.builder() + .assetPrice(AssetPrice.of(UnsignedLong.valueOf("14d6a", 16))) + .baseAsset("XRP") + .quoteAsset("MXN") + .scale(UnsignedInteger.valueOf(4)) + .build() + ), + PriceDataWrapper.of( + PriceData.builder() + .assetPrice(AssetPrice.of(UnsignedLong.valueOf("5dfe", 16))) + .baseAsset("XRP") + .quoteAsset("MYR") + .scale(UnsignedInteger.valueOf(4)) + .build() + ) + ) + .transactionSignature( + Signature.fromBase16("07205F518BC5D1D4F67E33101F140F69279670627825CDFA4BECDFE7C1D38" + + "B40B4101F62264C280C3C9A492B3FF01DCF4AE0CEF12BB6FB2E4EB681B9B81D710A") + ) + .build(); + + String expectedBinary = "12003324001ECFA62F6644CDA420330000000368400000000000000A7321EDA6501D3E53D47" + + "F10AE37A0C6B34194CF8A205DE9611FE81B63E7B62105E90EAC744007205F518BC5D1D4F67E33101F140F692796706278" + + "25CDFA4BECDFE7C1D38B40B4101F62264C280C3C9A492B3FF01DCF4AE0CEF12BB6FB2E4EB681B9B81D710A701C0863757" + + "272656E6379701D1468747470733A2F2F74687265657872702E6465768114E03E6F0A3D378C02E4CB7769053F8AA4EB3" + + "649ECF018E02030170000000000001FF4011A0000000000000000000000000000000000000000021A000000000000000" + + "0000000004944520000000000E1E02030170000000000013652041003011A00000000000000000000000000000000000" + + "00000021A0000000000000000000000004A50590000000000E1E020301700000000000117BF041002011A00000000000" + + "00000000000000000000000000000021A0000000000000000000000004B52570000000000E1E02030170000000000014" + + "D6A041004011A0000000000000000000000000000000000000000021A0000000000000000000000004D584E000000000" + + "0E1E02030170000000000005DFE041004011A0000000000000000000000000000000000000000021A000000000000000" + + "0000000004D59520000000000E1F1"; + assertSerializesAndDeserializes(oracleSet, expectedBinary); + } + + @Test + void serializeOracleDelete() throws JsonProcessingException { + OracleDelete oracleDelete = OracleDelete.builder() + .account(Address.of("rsTkARdP8evWbRcAoD1ZJX7bDtAf5BVspg")) + .fee(XrpCurrencyAmount.ofDrops(15)) + .sequence(UnsignedInteger.valueOf(10)) + .lastLedgerSequence(UnsignedInteger.valueOf(17)) + .signingPublicKey(PublicKey.fromBase16EncodedPublicKey( + "ED6884529D21E8059F689827F5BF2AD5CBC138A3427A04C12B004C9BE9BBB22A77" + )) + .oracleDocumentId(OracleDocumentId.of(UnsignedInteger.ONE)) + .build(); + String expectedBinary = "120034240000000A201B0000001120330000000168400000000000000F7321ED6884529" + + "D21E8059F689827F5BF2AD5CBC138A3427A04C12B004C9BE9BBB22A7781141AFFBE9553C7325305491B100D02E6BED8CE6BA5"; + + assertSerializesAndDeserializes(oracleDelete, expectedBinary); + } + private void assertSerializesAndDeserializes( T transaction, String expectedBinary diff --git a/xrpl4j-core/src/test/java/org/xrpl/xrpl4j/crypto/signing/SignatureUtilsTest.java b/xrpl4j-core/src/test/java/org/xrpl/xrpl4j/crypto/signing/SignatureUtilsTest.java index e23dd9dcd..592b0f5d2 100644 --- a/xrpl4j-core/src/test/java/org/xrpl/xrpl4j/crypto/signing/SignatureUtilsTest.java +++ b/xrpl4j-core/src/test/java/org/xrpl/xrpl4j/crypto/signing/SignatureUtilsTest.java @@ -93,6 +93,9 @@ 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.OracleDocumentId; +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; @@ -1109,6 +1112,33 @@ void addSignatureToDidDelete() { addSignatureToTransactionHelper(transaction); } + @Test + void addSignatureToOracleSet() { + OracleSet transaction = OracleSet.builder() + .account(sourcePublicKey.deriveAddress()) + .fee(XrpCurrencyAmount.ofDrops(10)) + .sequence(UnsignedInteger.valueOf(391)) + .signingPublicKey(sourcePublicKey) + .oracleDocumentId(OracleDocumentId.of(UnsignedInteger.ONE)) + .lastUpdateTime(UnsignedInteger.ONE) + .build(); + + addSignatureToTransactionHelper(transaction); + } + + @Test + void addSignatureToOracleDelete() { + OracleDelete transaction = OracleDelete.builder() + .account(sourcePublicKey.deriveAddress()) + .fee(XrpCurrencyAmount.ofDrops(10)) + .sequence(UnsignedInteger.valueOf(391)) + .signingPublicKey(sourcePublicKey) + .oracleDocumentId(OracleDocumentId.of(UnsignedInteger.ONE)) + .build(); + + addSignatureToTransactionHelper(transaction); + } + @Test public void addSignatureToTransactionUnsupported() { assertThrows(IllegalArgumentException.class, () -> addSignatureToTransactionHelper(transactionMock)); @@ -1724,6 +1754,31 @@ void addMultiSignatureToDidDelete() { addMultiSignatureToTransactionHelper(transaction); } + @Test + void addMultiSignatureToOracleSet() { + OracleSet transaction = OracleSet.builder() + .account(sourcePublicKey.deriveAddress()) + .fee(XrpCurrencyAmount.ofDrops(10)) + .sequence(UnsignedInteger.valueOf(391)) + .oracleDocumentId(OracleDocumentId.of(UnsignedInteger.ONE)) + .lastUpdateTime(UnsignedInteger.ONE) + .build(); + + addMultiSignatureToTransactionHelper(transaction); + } + + @Test + void addMultiSignatureToOracleDelete() { + OracleDelete transaction = OracleDelete.builder() + .account(sourcePublicKey.deriveAddress()) + .fee(XrpCurrencyAmount.ofDrops(10)) + .sequence(UnsignedInteger.valueOf(391)) + .oracleDocumentId(OracleDocumentId.of(UnsignedInteger.ONE)) + .build(); + + addMultiSignatureToTransactionHelper(transaction); + } + @Test public void addMultiSignaturesToTransactionUnsupported() { when(transactionMock.transactionSignature()).thenReturn(Optional.empty()); diff --git a/xrpl4j-core/src/test/java/org/xrpl/xrpl4j/model/client/ledger/LedgerEntryRequestParamsTest.java b/xrpl4j-core/src/test/java/org/xrpl/xrpl4j/model/client/ledger/LedgerEntryRequestParamsTest.java index 62c272658..2b7cef20c 100644 --- a/xrpl4j-core/src/test/java/org/xrpl/xrpl4j/model/client/ledger/LedgerEntryRequestParamsTest.java +++ b/xrpl4j-core/src/test/java/org/xrpl/xrpl4j/model/client/ledger/LedgerEntryRequestParamsTest.java @@ -25,11 +25,13 @@ import org.xrpl.xrpl4j.model.ledger.LedgerObject; import org.xrpl.xrpl4j.model.ledger.NfTokenPageObject; import org.xrpl.xrpl4j.model.ledger.OfferObject; +import org.xrpl.xrpl4j.model.ledger.OracleObject; import org.xrpl.xrpl4j.model.ledger.PayChannelObject; import org.xrpl.xrpl4j.model.ledger.RippleStateObject; import org.xrpl.xrpl4j.model.ledger.TicketObject; import org.xrpl.xrpl4j.model.transactions.Address; import org.xrpl.xrpl4j.model.transactions.ImmutableXChainBridge; +import org.xrpl.xrpl4j.model.transactions.OracleDocumentId; import org.xrpl.xrpl4j.model.transactions.XChainBridge; class LedgerEntryRequestParamsTest extends AbstractJsonTest { @@ -54,6 +56,7 @@ void testTypedIndexParams() throws JSONException, JsonProcessingException { assertThat(params.bridgeAccount()).isEmpty(); assertThat(params.bridge()).isEmpty(); assertThat(params.did()).isEmpty(); + assertThat(params.oracle()).isEmpty(); String json = String.format("{\n" + " \"index\": \"%s\",\n" + @@ -90,6 +93,7 @@ void testUntypedIndexParams() throws JSONException, JsonProcessingException { assertThat(params.did()).isEmpty(); assertThat(params.bridgeAccount()).isEmpty(); assertThat(params.bridge()).isEmpty(); + assertThat(params.oracle()).isEmpty(); String json = String.format("{\n" + " \"index\": \"%s\",\n" + @@ -121,6 +125,7 @@ void testAccountRootParams() throws JSONException, JsonProcessingException { assertThat(params.did()).isEmpty(); assertThat(params.bridgeAccount()).isEmpty(); assertThat(params.bridge()).isEmpty(); + assertThat(params.oracle()).isEmpty(); String json = String.format("{\n" + " \"account_root\": \"%s\",\n" + @@ -160,6 +165,7 @@ void testAmmParams() throws JSONException, JsonProcessingException { assertThat(params.did()).isEmpty(); assertThat(params.bridgeAccount()).isEmpty(); assertThat(params.bridge()).isEmpty(); + assertThat(params.oracle()).isEmpty(); String json = "{\n" + " \"amm\": {\n" + @@ -204,6 +210,7 @@ void testOfferParams() throws JSONException, JsonProcessingException { assertThat(params.did()).isEmpty(); assertThat(params.bridgeAccount()).isEmpty(); assertThat(params.bridge()).isEmpty(); + assertThat(params.oracle()).isEmpty(); String json = "{\n" + " \"offer\": {\n" + @@ -246,6 +253,7 @@ void testRippleStateParams() throws JSONException, JsonProcessingException { assertThat(params.did()).isEmpty(); assertThat(params.bridgeAccount()).isEmpty(); assertThat(params.bridge()).isEmpty(); + assertThat(params.oracle()).isEmpty(); String json = "{\n" + " \"ripple_state\": {\n" + @@ -281,6 +289,7 @@ void testCheckParams() throws JSONException, JsonProcessingException { assertThat(params.did()).isEmpty(); assertThat(params.bridgeAccount()).isEmpty(); assertThat(params.bridge()).isEmpty(); + assertThat(params.oracle()).isEmpty(); String json = String.format("{\n" + " \"check\": \"%s\",\n" + @@ -316,6 +325,7 @@ void testEscrowParams() throws JSONException, JsonProcessingException { assertThat(params.did()).isEmpty(); assertThat(params.bridgeAccount()).isEmpty(); assertThat(params.bridge()).isEmpty(); + assertThat(params.oracle()).isEmpty(); String json = "{\n" + " \"escrow\": {\n" + @@ -350,6 +360,7 @@ void testPaymentChannelParams() throws JSONException, JsonProcessingException { assertThat(params.did()).isEmpty(); assertThat(params.bridgeAccount()).isEmpty(); assertThat(params.bridge()).isEmpty(); + assertThat(params.oracle()).isEmpty(); String json = String.format("{\n" + " \"payment_channel\": \"%s\",\n" + @@ -386,6 +397,7 @@ void testDepositPreAuthParams() throws JSONException, JsonProcessingException { assertThat(params.did()).isEmpty(); assertThat(params.bridgeAccount()).isEmpty(); assertThat(params.bridge()).isEmpty(); + assertThat(params.oracle()).isEmpty(); String json = "{\n" + " \"deposit_preauth\": {\n" + @@ -425,6 +437,7 @@ void testTicketParams() throws JSONException, JsonProcessingException { assertThat(params.did()).isEmpty(); assertThat(params.bridgeAccount()).isEmpty(); assertThat(params.bridge()).isEmpty(); + assertThat(params.oracle()).isEmpty(); String json = "{\n" + " \"ticket\": {\n" + @@ -460,6 +473,7 @@ void testNftPageParams() throws JSONException, JsonProcessingException { assertThat(params.did()).isEmpty(); assertThat(params.bridgeAccount()).isEmpty(); assertThat(params.bridge()).isEmpty(); + assertThat(params.oracle()).isEmpty(); String json = String.format("{\n" + " \"nft_page\": \"%s\",\n" + @@ -492,6 +506,7 @@ void testDidParams() throws JSONException, JsonProcessingException { assertThat(params.nftPage()).isEmpty(); assertThat(params.bridgeAccount()).isEmpty(); assertThat(params.bridge()).isEmpty(); + assertThat(params.oracle()).isEmpty(); String json = String.format("{\n" + " \"did\": \"%s\",\n" + @@ -531,6 +546,7 @@ void testBridgeParams() throws JSONException, JsonProcessingException { assertThat(params.ticket()).isEmpty(); assertThat(params.nftPage()).isEmpty(); assertThat(params.did()).isEmpty(); + assertThat(params.oracle()).isEmpty(); String json = String.format("{\n" + " \"bridge_account\": \"%s\",\n" + @@ -541,4 +557,40 @@ void testBridgeParams() throws JSONException, JsonProcessingException { assertCanSerializeAndDeserialize(params, json); } + + @Test + void testOracleParams() throws JSONException, JsonProcessingException { + OracleLedgerEntryParams oracleParams = OracleLedgerEntryParams.builder() + .account(ED_ADDRESS) + .oracleDocumentId(OracleDocumentId.of(UnsignedInteger.ONE)) + .build(); + LedgerEntryRequestParams params = LedgerEntryRequestParams.oracle( + oracleParams, + LedgerSpecifier.VALIDATED + ); + assertThat(params.oracle()).isNotEmpty().get().isEqualTo(oracleParams); + assertThat(params.ledgerObjectClass()).isEqualTo(OracleObject.class); + + assertThat(params.index()).isEmpty(); + assertThat(params.accountRoot()).isEmpty(); + assertThat(params.amm()).isEmpty(); + assertThat(params.offer()).isEmpty(); + assertThat(params.rippleState()).isEmpty(); + assertThat(params.check()).isEmpty(); + assertThat(params.escrow()).isEmpty(); + assertThat(params.paymentChannel()).isEmpty(); + assertThat(params.depositPreAuth()).isEmpty(); + assertThat(params.ticket()).isEmpty(); + assertThat(params.nftPage()).isEmpty(); + assertThat(params.did()).isEmpty(); + assertThat(params.bridgeAccount()).isEmpty(); + + String json = String.format("{\n" + + " \"oracle\" : %s,\n" + + " \"binary\": false,\n" + + " \"ledger_index\": \"validated\"\n" + + " }", objectMapper.writeValueAsString(oracleParams), ED_ADDRESS); + + assertCanSerializeAndDeserialize(params, json); + } } \ No newline at end of file diff --git a/xrpl4j-core/src/test/java/org/xrpl/xrpl4j/model/client/oracle/AggregatePriceSetTest.java b/xrpl4j-core/src/test/java/org/xrpl/xrpl4j/model/client/oracle/AggregatePriceSetTest.java new file mode 100644 index 000000000..9f007697d --- /dev/null +++ b/xrpl4j-core/src/test/java/org/xrpl/xrpl4j/model/client/oracle/AggregatePriceSetTest.java @@ -0,0 +1,73 @@ +package org.xrpl.xrpl4j.model.client.oracle; + +import static org.assertj.core.api.AssertionsForClassTypes.assertThat; + +import com.fasterxml.jackson.core.JsonProcessingException; +import com.fasterxml.jackson.databind.ObjectMapper; +import com.fasterxml.jackson.databind.annotation.JsonDeserialize; +import com.fasterxml.jackson.databind.annotation.JsonSerialize; +import com.google.common.primitives.UnsignedLong; +import org.assertj.core.api.Assertions; +import org.immutables.value.Value; +import org.json.JSONException; +import org.junit.jupiter.api.Test; +import org.skyscreamer.jsonassert.JSONAssert; +import org.skyscreamer.jsonassert.JSONCompareMode; +import org.xrpl.xrpl4j.model.jackson.ObjectMapperFactory; + +import java.math.BigDecimal; + +/** + * Unit tests for {@link AggregatePriceSet}. + */ +class AggregatePriceSetTest { + + private final ObjectMapper objectMapper = ObjectMapperFactory.create(); + + @Test + void testBigDecimalFields() { + AggregatePriceSet set = AggregatePriceSet.builder() + .meanString("1234.5678") + .size(UnsignedLong.ONE) + .standardDeviationString("345678.23496") + .build(); + + assertThat(set.mean()).isEqualTo(BigDecimal.valueOf(1234.5678)); + assertThat(set.standardDeviation()).isEqualTo(BigDecimal.valueOf(345678.23496)); + } + + @Test + void testJson() throws JSONException, JsonProcessingException { + AggregatePriceSet priceSet = + AggregatePriceSet.builder() + .meanString("200") + .size(UnsignedLong.ONE) + .standardDeviationString("1.00") + .build(); + AggregatePriceSetWrapper wrapper = AggregatePriceSetWrapper.of(priceSet); + + String json = "{\"value\":{\"mean\":\"200\",\"size\":1,\"standard_deviation\":\"1.00\"}}"; + assertSerializesAndDeserializes(wrapper, json); + } + + private void assertSerializesAndDeserializes(AggregatePriceSetWrapper wrapper, String json) + throws JsonProcessingException, JSONException { + String serialized = objectMapper.writeValueAsString(wrapper); + JSONAssert.assertEquals(json, serialized, JSONCompareMode.STRICT); + AggregatePriceSetWrapper deserialized = objectMapper.readValue(serialized, AggregatePriceSetWrapper.class); + Assertions.assertThat(deserialized).isEqualTo(wrapper); + } + + @Value.Immutable + @JsonSerialize(as = ImmutableAggregatePriceSetWrapper.class) + @JsonDeserialize(as = ImmutableAggregatePriceSetWrapper.class) + interface AggregatePriceSetWrapper { + + static AggregatePriceSetWrapper of(AggregatePriceSet aggregatePriceSet) { + return ImmutableAggregatePriceSetWrapper.builder().value(aggregatePriceSet).build(); + } + + AggregatePriceSet value(); + + } +} \ No newline at end of file diff --git a/xrpl4j-core/src/test/java/org/xrpl/xrpl4j/model/client/oracle/GetAggregatePriceRequestParamsTest.java b/xrpl4j-core/src/test/java/org/xrpl/xrpl4j/model/client/oracle/GetAggregatePriceRequestParamsTest.java new file mode 100644 index 000000000..3e6cc2683 --- /dev/null +++ b/xrpl4j-core/src/test/java/org/xrpl/xrpl4j/model/client/oracle/GetAggregatePriceRequestParamsTest.java @@ -0,0 +1,45 @@ +package org.xrpl.xrpl4j.model.client.oracle; + +import com.fasterxml.jackson.core.JsonProcessingException; +import com.google.common.primitives.UnsignedInteger; +import org.json.JSONException; +import org.junit.jupiter.api.Test; +import org.xrpl.xrpl4j.model.AbstractJsonTest; +import org.xrpl.xrpl4j.model.client.common.LedgerSpecifier; +import org.xrpl.xrpl4j.model.client.ledger.OracleLedgerEntryParams; +import org.xrpl.xrpl4j.model.transactions.Address; +import org.xrpl.xrpl4j.model.transactions.OracleDocumentId; + +class GetAggregatePriceRequestParamsTest extends AbstractJsonTest { + + @Test + void testJson() throws JSONException, JsonProcessingException { + GetAggregatePriceRequestParams params = GetAggregatePriceRequestParams.builder() + .ledgerSpecifier(LedgerSpecifier.CURRENT) + .baseAsset("XRP") + .quoteAsset("USD") + .trim(UnsignedInteger.valueOf(20)) + .addOracles( + OracleLedgerEntryParams.builder() + .account(Address.of("rp047ow9WcPmnNpVHMQV5A4BF6vaL9Abm6")) + .oracleDocumentId(OracleDocumentId.of(UnsignedInteger.valueOf(34))) + .build() + ) + .build(); + + String json = "{\n" + + " \"ledger_index\": \"current\",\n" + + " \"base_asset\": \"XRP\",\n" + + " \"quote_asset\": \"USD\",\n" + + " \"trim\": 20,\n" + + " \"oracles\": [\n" + + " {\n" + + " \"account\": \"rp047ow9WcPmnNpVHMQV5A4BF6vaL9Abm6\",\n" + + " \"oracle_document_id\": 34\n" + + " }\n" + + " ]\n" + + " }"; + + assertCanSerializeAndDeserialize(params, json); + } +} \ No newline at end of file diff --git a/xrpl4j-core/src/test/java/org/xrpl/xrpl4j/model/client/oracle/GetAggregatePriceResultTest.java b/xrpl4j-core/src/test/java/org/xrpl/xrpl4j/model/client/oracle/GetAggregatePriceResultTest.java new file mode 100644 index 000000000..86cba139f --- /dev/null +++ b/xrpl4j-core/src/test/java/org/xrpl/xrpl4j/model/client/oracle/GetAggregatePriceResultTest.java @@ -0,0 +1,107 @@ +package org.xrpl.xrpl4j.model.client.oracle; + +import static org.assertj.core.api.AssertionsForClassTypes.assertThat; +import static org.assertj.core.api.AssertionsForClassTypes.assertThatThrownBy; + +import com.fasterxml.jackson.core.JsonProcessingException; +import com.google.common.primitives.UnsignedInteger; +import com.google.common.primitives.UnsignedLong; +import org.json.JSONException; +import org.junit.jupiter.api.Test; +import org.xrpl.xrpl4j.model.AbstractJsonTest; +import org.xrpl.xrpl4j.model.client.common.LedgerIndex; +import org.xrpl.xrpl4j.model.client.oracle.ImmutableGetAggregatePriceResult.Builder; +import org.xrpl.xrpl4j.model.transactions.Hash256; + +import java.math.BigDecimal; +import java.math.BigInteger; + +class GetAggregatePriceResultTest extends AbstractJsonTest { + + @Test + void testJson() throws JSONException, JsonProcessingException { + GetAggregatePriceResult result = baseBuilder() + .ledgerCurrentIndex(LedgerIndex.of(UnsignedInteger.valueOf(25))) + .build(); + + String json = "{\n" + + " \"entire_set\" : {\n" + + " \"mean\" : \"74.75\",\n" + + " \"size\" : 10,\n" + + " \"standard_deviation\" : \"0.1290994448735806\"\n" + + " },\n" + + " \"ledger_current_index\" : 25,\n" + + " \"median\" : \"74.75\",\n" + + " \"status\" : \"success\",\n" + + " \"trimmed_set\" : {\n" + + " \"mean\" : \"74.75\",\n" + + " \"size\" : 6,\n" + + " \"standard_deviation\" : \"0.1290994448735806\"\n" + + " },\n" + + " \"validated\" : false,\n" + + " \"time\" : 78937648\n" + + "}"; + + assertCanSerializeAndDeserialize(result, json); + } + + @Test + void testWithLedgerCurrentIndex() { + GetAggregatePriceResult result = baseBuilder() + .ledgerCurrentIndex(LedgerIndex.of(UnsignedInteger.valueOf(25))) + .build(); + + assertThat(result.ledgerCurrentIndex()).isNotEmpty().get().isEqualTo(result.ledgerCurrentIndexSafe()); + assertThatThrownBy(result::ledgerIndexSafe).isInstanceOf(IllegalStateException.class); + assertThatThrownBy(result::ledgerHashSafe).isInstanceOf(IllegalStateException.class); + } + + @Test + void testWithLedgerIndex() { + GetAggregatePriceResult result = baseBuilder() + .ledgerIndex(LedgerIndex.of(UnsignedInteger.valueOf(25))) + .build(); + + assertThat(result.ledgerIndex()).isNotEmpty().get().isEqualTo(result.ledgerIndexSafe()); + assertThatThrownBy(result::ledgerCurrentIndexSafe).isInstanceOf(IllegalStateException.class); + assertThatThrownBy(result::ledgerHashSafe).isInstanceOf(IllegalStateException.class); + } + + @Test + void testWithLedgerHash() { + GetAggregatePriceResult result = baseBuilder() + .ledgerHash(Hash256.of("B9D3D80EDF4083A06B2D51202E0BFB63C46FC0985E015D06767C21A62853BF6D")) + .build(); + + assertThat(result.ledgerHash()).isNotEmpty().get().isEqualTo(result.ledgerHashSafe()); + assertThatThrownBy(result::ledgerCurrentIndexSafe).isInstanceOf(IllegalStateException.class); + assertThatThrownBy(result::ledgerIndexSafe).isInstanceOf(IllegalStateException.class); + } + + @Test + void testMedian() { + GetAggregatePriceResult result = baseBuilder().build(); + assertThat(result.median()).isEqualTo(BigDecimal.valueOf(74.75)); + } + + private static Builder baseBuilder() { + return GetAggregatePriceResult.builder() + .entireSet( + AggregatePriceSet.builder() + .meanString("74.75") + .size(UnsignedLong.valueOf(10)) + .standardDeviationString("0.1290994448735806") + .build() + ) + .medianString("74.75") + .status("success") + .trimmedSet( + AggregatePriceSet.builder() + .meanString("74.75") + .size(UnsignedLong.valueOf(6)) + .standardDeviationString("0.1290994448735806") + .build() + ) + .time(UnsignedInteger.valueOf(78937648)); + } +} \ No newline at end of file diff --git a/xrpl4j-core/src/test/java/org/xrpl/xrpl4j/model/jackson/modules/AssetPriceDeserializerTest.java b/xrpl4j-core/src/test/java/org/xrpl/xrpl4j/model/jackson/modules/AssetPriceDeserializerTest.java new file mode 100644 index 000000000..ea4c3a0e9 --- /dev/null +++ b/xrpl4j-core/src/test/java/org/xrpl/xrpl4j/model/jackson/modules/AssetPriceDeserializerTest.java @@ -0,0 +1,43 @@ +package org.xrpl.xrpl4j.model.jackson.modules; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.when; + +import com.fasterxml.jackson.core.JsonParser; +import com.fasterxml.jackson.databind.DeserializationContext; +import com.google.common.primitives.UnsignedLong; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.xrpl.xrpl4j.model.transactions.AssetPrice; + +import java.io.IOException; + +/** + * Unit tests for {@link AssetPriceDeserializer}. + */ +class AssetPriceDeserializerTest { + + private AssetPriceDeserializer deserializer; + + @BeforeEach + void setUp() { + deserializer = new AssetPriceDeserializer(); + } + + @Test + void testDeserialize() throws IOException { + JsonParser mockJsonParser = mock(JsonParser.class); + + AssetPrice expected = AssetPrice.of(UnsignedLong.ZERO); + when(mockJsonParser.getText()).thenReturn("0"); + AssetPrice assetPrice = deserializer.deserialize(mockJsonParser, mock(DeserializationContext.class)); + assertThat(assetPrice).isEqualTo(expected); + + expected = AssetPrice.of(UnsignedLong.MAX_VALUE); + when(mockJsonParser.getText()).thenReturn("ffffffffffffffff"); + assetPrice = deserializer.deserialize(mockJsonParser, mock(DeserializationContext.class)); + assertThat(assetPrice).isEqualTo(expected); + } + +} \ No newline at end of file diff --git a/xrpl4j-core/src/test/java/org/xrpl/xrpl4j/model/jackson/modules/AssetPriceSerializerTest.java b/xrpl4j-core/src/test/java/org/xrpl/xrpl4j/model/jackson/modules/AssetPriceSerializerTest.java new file mode 100644 index 000000000..d0e9563ac --- /dev/null +++ b/xrpl4j-core/src/test/java/org/xrpl/xrpl4j/model/jackson/modules/AssetPriceSerializerTest.java @@ -0,0 +1,39 @@ +package org.xrpl.xrpl4j.model.jackson.modules; + +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.verify; + +import com.fasterxml.jackson.core.JsonGenerator; +import com.fasterxml.jackson.databind.SerializerProvider; +import com.google.common.primitives.UnsignedLong; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.xrpl.xrpl4j.model.transactions.AssetPrice; + +import java.io.IOException; + +/** + * Unit test for {@link AssetPriceSerializer}. + */ +class AssetPriceSerializerTest { + + private AssetPriceSerializer serializer; + + @BeforeEach + void setUp() { + serializer = new AssetPriceSerializer(); + } + + @Test + void testSerialize() throws IOException { + JsonGenerator jsonGeneratorMock = mock(JsonGenerator.class); + + AssetPrice expected = AssetPrice.of(UnsignedLong.ZERO); + serializer.serialize(expected, jsonGeneratorMock, mock(SerializerProvider.class)); + verify(jsonGeneratorMock).writeString("0"); + + expected = AssetPrice.of(UnsignedLong.MAX_VALUE); + serializer.serialize(expected, jsonGeneratorMock, mock(SerializerProvider.class)); + verify(jsonGeneratorMock).writeString("ffffffffffffffff"); + } +} \ No newline at end of file diff --git a/xrpl4j-core/src/test/java/org/xrpl/xrpl4j/model/jackson/modules/OracleDocumentIdDeserializerTest.java b/xrpl4j-core/src/test/java/org/xrpl/xrpl4j/model/jackson/modules/OracleDocumentIdDeserializerTest.java new file mode 100644 index 000000000..d1f985a86 --- /dev/null +++ b/xrpl4j-core/src/test/java/org/xrpl/xrpl4j/model/jackson/modules/OracleDocumentIdDeserializerTest.java @@ -0,0 +1,43 @@ +package org.xrpl.xrpl4j.model.jackson.modules; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.when; + +import com.fasterxml.jackson.core.JsonParser; +import com.fasterxml.jackson.databind.DeserializationContext; +import com.google.common.primitives.UnsignedInteger; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.xrpl.xrpl4j.model.transactions.OracleDocumentId; + +import java.io.IOException; + +/** + * Unit test for {@link OracleDocumentIdDeserializer}. + */ +class OracleDocumentIdDeserializerTest { + + private OracleDocumentIdDeserializer deserializer; + + @BeforeEach + void setUp() { + deserializer = new OracleDocumentIdDeserializer(); + } + + @Test + void testDeserialize() throws IOException { + JsonParser mockJsonParser = mock(JsonParser.class); + + when(mockJsonParser.getLongValue()).thenReturn(0L); + OracleDocumentId expected = OracleDocumentId.of(UnsignedInteger.ZERO); + OracleDocumentId oracleDocumentId = deserializer.deserialize(mockJsonParser, mock(DeserializationContext.class)); + assertThat(oracleDocumentId).isEqualTo(expected); + + when(mockJsonParser.getLongValue()).thenReturn(4294967295L); + expected = OracleDocumentId.of(UnsignedInteger.MAX_VALUE); + oracleDocumentId = deserializer.deserialize(mockJsonParser, mock(DeserializationContext.class)); + assertThat(oracleDocumentId).isEqualTo(expected); + } + +} \ No newline at end of file diff --git a/xrpl4j-core/src/test/java/org/xrpl/xrpl4j/model/jackson/modules/OracleDocumentIdSerializerTest.java b/xrpl4j-core/src/test/java/org/xrpl/xrpl4j/model/jackson/modules/OracleDocumentIdSerializerTest.java new file mode 100644 index 000000000..c9d64a595 --- /dev/null +++ b/xrpl4j-core/src/test/java/org/xrpl/xrpl4j/model/jackson/modules/OracleDocumentIdSerializerTest.java @@ -0,0 +1,40 @@ +package org.xrpl.xrpl4j.model.jackson.modules; + +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.verify; + +import com.fasterxml.jackson.core.JsonGenerator; +import com.fasterxml.jackson.databind.SerializerProvider; +import com.google.common.primitives.UnsignedInteger; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.xrpl.xrpl4j.model.transactions.OracleDocumentId; + +import java.io.IOException; + +/** + * Unit tests for {@link OracleDocumentIdSerializer}. + */ +class OracleDocumentIdSerializerTest { + + private OracleDocumentIdSerializer serializer; + + @BeforeEach + void setUp() { + serializer = new OracleDocumentIdSerializer(); + } + + @Test + void testSerialize() throws IOException { + JsonGenerator jsonGeneratorMock = mock(JsonGenerator.class); + + OracleDocumentId expected = OracleDocumentId.of(UnsignedInteger.ZERO); + serializer.serialize(expected, jsonGeneratorMock, mock(SerializerProvider.class)); + verify(jsonGeneratorMock).writeNumber(0L); + + expected = OracleDocumentId.of(UnsignedInteger.MAX_VALUE); + serializer.serialize(expected, jsonGeneratorMock, mock(SerializerProvider.class)); + verify(jsonGeneratorMock).writeNumber(4294967295L); + } + +} \ No newline at end of file diff --git a/xrpl4j-core/src/test/java/org/xrpl/xrpl4j/model/jackson/modules/OracleProviderDeserializerTest.java b/xrpl4j-core/src/test/java/org/xrpl/xrpl4j/model/jackson/modules/OracleProviderDeserializerTest.java new file mode 100644 index 000000000..f262b1ad6 --- /dev/null +++ b/xrpl4j-core/src/test/java/org/xrpl/xrpl4j/model/jackson/modules/OracleProviderDeserializerTest.java @@ -0,0 +1,42 @@ +package org.xrpl.xrpl4j.model.jackson.modules; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.when; + +import com.fasterxml.jackson.core.JsonParser; +import com.fasterxml.jackson.databind.DeserializationContext; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.xrpl.xrpl4j.model.transactions.OracleProvider; + +import java.io.IOException; + +/** + * Unit test for {@link OracleProviderDeserializer}. + */ +class OracleProviderDeserializerTest { + + private OracleProviderDeserializer deserializer; + + @BeforeEach + void setUp() { + deserializer = new OracleProviderDeserializer(); + } + + @Test + void testDeserialize() throws IOException { + JsonParser mockJsonParser = mock(JsonParser.class); + + OracleProvider expected = OracleProvider.of("foo"); + when(mockJsonParser.getText()).thenReturn("foo"); + OracleProvider assetPrice = deserializer.deserialize(mockJsonParser, mock(DeserializationContext.class)); + assertThat(assetPrice).isEqualTo(expected); + + expected = OracleProvider.of(""); + when(mockJsonParser.getText()).thenReturn(""); + assetPrice = deserializer.deserialize(mockJsonParser, mock(DeserializationContext.class)); + assertThat(assetPrice).isEqualTo(expected); + } + +} \ No newline at end of file diff --git a/xrpl4j-core/src/test/java/org/xrpl/xrpl4j/model/jackson/modules/OracleUriDeserializerTest.java b/xrpl4j-core/src/test/java/org/xrpl/xrpl4j/model/jackson/modules/OracleUriDeserializerTest.java new file mode 100644 index 000000000..ad2368116 --- /dev/null +++ b/xrpl4j-core/src/test/java/org/xrpl/xrpl4j/model/jackson/modules/OracleUriDeserializerTest.java @@ -0,0 +1,42 @@ +package org.xrpl.xrpl4j.model.jackson.modules; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.when; + +import com.fasterxml.jackson.core.JsonParser; +import com.fasterxml.jackson.databind.DeserializationContext; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.xrpl.xrpl4j.model.transactions.OracleUri; + +import java.io.IOException; + +/** + * Unit tests for {@link OracleUriDeserializer}. + */ +class OracleUriDeserializerTest { + + private OracleUriDeserializer deserializer; + + @BeforeEach + void setUp() { + deserializer = new OracleUriDeserializer(); + } + + @Test + void testDeserialize() throws IOException { + JsonParser mockJsonParser = mock(JsonParser.class); + + OracleUri expected = OracleUri.of("foo"); + when(mockJsonParser.getText()).thenReturn("foo"); + OracleUri oracleUri = deserializer.deserialize(mockJsonParser, mock(DeserializationContext.class)); + assertThat(oracleUri).isEqualTo(expected); + + expected = OracleUri.of(""); + when(mockJsonParser.getText()).thenReturn(""); + oracleUri = deserializer.deserialize(mockJsonParser, mock(DeserializationContext.class)); + assertThat(oracleUri).isEqualTo(expected); + } + +} \ No newline at end of file diff --git a/xrpl4j-core/src/test/java/org/xrpl/xrpl4j/model/ledger/OracleObjectTest.java b/xrpl4j-core/src/test/java/org/xrpl/xrpl4j/model/ledger/OracleObjectTest.java new file mode 100644 index 000000000..94ea51fd3 --- /dev/null +++ b/xrpl4j-core/src/test/java/org/xrpl/xrpl4j/model/ledger/OracleObjectTest.java @@ -0,0 +1,68 @@ +package org.xrpl.xrpl4j.model.ledger; + +import static org.assertj.core.api.AssertionsForClassTypes.assertThat; + +import com.fasterxml.jackson.core.JsonProcessingException; +import com.google.common.primitives.UnsignedInteger; +import com.google.common.primitives.UnsignedLong; +import org.json.JSONException; +import org.junit.jupiter.api.Test; +import org.xrpl.xrpl4j.model.AbstractJsonTest; +import org.xrpl.xrpl4j.model.flags.Flags; +import org.xrpl.xrpl4j.model.transactions.Address; +import org.xrpl.xrpl4j.model.transactions.AssetPrice; +import org.xrpl.xrpl4j.model.transactions.Hash256; +import org.xrpl.xrpl4j.model.transactions.OracleProvider; +import org.xrpl.xrpl4j.model.transactions.PriceData; +import org.xrpl.xrpl4j.model.transactions.PriceDataWrapper; + +class OracleObjectTest extends AbstractJsonTest { + + @Test + void testJson() throws JSONException, JsonProcessingException { + OracleObject oracleObject = OracleObject.builder() + .assetClass("63757272656E6379") + .lastUpdateTime(UnsignedInteger.valueOf(1715797016)) + .owner(Address.of("rMS69A6J39RmBg5yWDft5XAM8zTGbtMMZy")) + .ownerNode("0") + .previousTransactionId(Hash256.of("A5183686EF85C7D563B400C127DBEA71D1E404E419424BABB2891F4CC772E157")) + .previousTransactionLedgerSequence(UnsignedInteger.valueOf(722809)) + .provider(OracleProvider.of("68747470733A2F2F74687265657872702E646576")) + .addPriceDataSeries( + PriceDataWrapper.of( + PriceData.builder() + .assetPrice(AssetPrice.of(UnsignedLong.valueOf("2030", 16))) + .baseAsset("XRP") + .quoteAsset("IDR") + .build() + ) + ) + .index(Hash256.of("72B5901470838D8C71E392C43999DC357BF49C1DA75583872C170AD0C28A12E8")) + .build(); + + String json = "{\n" + + " \"AssetClass\": \"63757272656E6379\",\n" + + " \"Flags\": 0,\n" + + " \"LastUpdateTime\": 1715797016,\n" + + " \"LedgerEntryType\": \"Oracle\",\n" + + " \"Owner\": \"rMS69A6J39RmBg5yWDft5XAM8zTGbtMMZy\",\n" + + " \"OwnerNode\": \"0\",\n" + + " \"PreviousTxnID\": \"A5183686EF85C7D563B400C127DBEA71D1E404E419424BABB2891F4CC772E157\",\n" + + " \"PreviousTxnLgrSeq\": 722809,\n" + + " \"PriceDataSeries\": [\n" + + " {\n" + + " \"PriceData\": {\n" + + " \"AssetPrice\": \"2030\",\n" + + " \"BaseAsset\": \"XRP\",\n" + + " \"QuoteAsset\": \"IDR\"\n" + + " }\n" + + " }\n" + + " ],\n" + + " \"Provider\": \"68747470733A2F2F74687265657872702E646576\",\n" + + " \"index\": \"72B5901470838D8C71E392C43999DC357BF49C1DA75583872C170AD0C28A12E8\"\n" + + " }"; + + assertCanSerializeAndDeserialize(oracleObject, json); + assertThat(oracleObject.flags()).isEqualTo(Flags.UNSET); + } +} \ No newline at end of file diff --git a/xrpl4j-core/src/test/java/org/xrpl/xrpl4j/model/transactions/AssetPriceTest.java b/xrpl4j-core/src/test/java/org/xrpl/xrpl4j/model/transactions/AssetPriceTest.java new file mode 100644 index 000000000..274699ecc --- /dev/null +++ b/xrpl4j-core/src/test/java/org/xrpl/xrpl4j/model/transactions/AssetPriceTest.java @@ -0,0 +1,76 @@ +package org.xrpl.xrpl4j.model.transactions; + +import static org.assertj.core.api.AssertionsForClassTypes.assertThat; + +import com.fasterxml.jackson.core.JsonProcessingException; +import com.fasterxml.jackson.databind.ObjectMapper; +import com.fasterxml.jackson.databind.annotation.JsonDeserialize; +import com.fasterxml.jackson.databind.annotation.JsonSerialize; +import com.google.common.primitives.UnsignedLong; +import org.assertj.core.api.Assertions; +import org.immutables.value.Value; +import org.json.JSONException; +import org.junit.jupiter.api.Test; +import org.skyscreamer.jsonassert.JSONAssert; +import org.skyscreamer.jsonassert.JSONCompareMode; +import org.xrpl.xrpl4j.model.jackson.ObjectMapperFactory; + +/** + * Unit tests for {@link AssetPrice}. + */ +public class AssetPriceTest { + + ObjectMapper objectMapper = ObjectMapperFactory.create(); + + @Test + void testToString() { + AssetPrice count = AssetPrice.of(UnsignedLong.ZERO); + assertThat(count.toString()).isEqualTo("0"); + + AssetPrice countMax = AssetPrice.of(UnsignedLong.MAX_VALUE); + assertThat(countMax.toString()).isEqualTo("18446744073709551615"); + } + + @Test + void testJson() throws JsonProcessingException, JSONException { + AssetPrice count = AssetPrice.of(UnsignedLong.valueOf(1000)); + AssetPriceWrapper wrapper = AssetPriceWrapper.of(count); + + String json = "{\"value\": \"3e8\"}"; + assertSerializesAndDeserializes(wrapper, json); + } + + @Test + void testMaxJson() throws JSONException, JsonProcessingException { + AssetPrice count = AssetPrice.of(UnsignedLong.MAX_VALUE); + AssetPriceWrapper wrapper = AssetPriceWrapper.of(count); + + String json = "{\"value\": \"ffffffffffffffff\"}"; + assertSerializesAndDeserializes(wrapper, json); + } + + private void assertSerializesAndDeserializes( + AssetPriceWrapper wrapper, + String json + ) throws JsonProcessingException, JSONException { + String serialized = objectMapper.writeValueAsString(wrapper); + JSONAssert.assertEquals(json, serialized, JSONCompareMode.STRICT); + AssetPriceWrapper deserialized = objectMapper.readValue( + serialized, AssetPriceWrapper.class + ); + Assertions.assertThat(deserialized).isEqualTo(wrapper); + } + + @Value.Immutable + @JsonSerialize(as = ImmutableAssetPriceWrapper.class) + @JsonDeserialize(as = ImmutableAssetPriceWrapper.class) + interface AssetPriceWrapper { + + static AssetPriceWrapper of(AssetPrice count) { + return ImmutableAssetPriceWrapper.builder().value(count).build(); + } + + AssetPrice value(); + + } +} diff --git a/xrpl4j-core/src/test/java/org/xrpl/xrpl4j/model/transactions/OracleDeleteTest.java b/xrpl4j-core/src/test/java/org/xrpl/xrpl4j/model/transactions/OracleDeleteTest.java new file mode 100644 index 000000000..54e54264a --- /dev/null +++ b/xrpl4j-core/src/test/java/org/xrpl/xrpl4j/model/transactions/OracleDeleteTest.java @@ -0,0 +1,48 @@ +package org.xrpl.xrpl4j.model.transactions; + +import static org.assertj.core.api.AssertionsForClassTypes.assertThat; +import static org.xrpl.xrpl4j.crypto.TestConstants.ED_PUBLIC_KEY; +import static org.xrpl.xrpl4j.crypto.TestConstants.ED_PUBLIC_KEY_HEX; + +import com.fasterxml.jackson.core.JsonProcessingException; +import com.google.common.primitives.UnsignedInteger; +import com.google.common.primitives.UnsignedLong; +import org.json.JSONException; +import org.junit.jupiter.api.Test; +import org.xrpl.xrpl4j.model.AbstractJsonTest; +import org.xrpl.xrpl4j.model.flags.TransactionFlags; + +class OracleDeleteTest extends AbstractJsonTest { + + @Test + void testDefaultFlags() { + OracleDelete oracleDelete = baseBuilder().build(); + + assertThat(oracleDelete.flags()).isEqualTo(TransactionFlags.EMPTY); + } + + @Test + void testJson() throws JSONException, JsonProcessingException { + OracleDelete oracleDelete = baseBuilder().build(); + String json = "\n" + + "{\n" + + " \"TransactionType\": \"OracleDelete\",\n" + + " \"Account\": \"rp4pqYgrTAtdPHuZd1ZQWxrzx45jxYcZex\",\n" + + " \"OracleDocumentID\": 1,\n" + + " \"Fee\": \"12\",\n" + + " \"Sequence\": 391,\n" + + " \"SigningPubKey\": \"" + ED_PUBLIC_KEY_HEX + "\"\n" + + "}"; + + assertCanSerializeAndDeserialize(oracleDelete, json); + } + + private static ImmutableOracleDelete.Builder baseBuilder() { + return OracleDelete.builder() + .account(Address.of("rp4pqYgrTAtdPHuZd1ZQWxrzx45jxYcZex")) + .fee(XrpCurrencyAmount.ofDrops(12)) + .sequence(UnsignedInteger.valueOf(391)) + .signingPublicKey(ED_PUBLIC_KEY) + .oracleDocumentId(OracleDocumentId.of(UnsignedInteger.ONE)); + } +} \ No newline at end of file diff --git a/xrpl4j-core/src/test/java/org/xrpl/xrpl4j/model/transactions/OracleDocumentIdTest.java b/xrpl4j-core/src/test/java/org/xrpl/xrpl4j/model/transactions/OracleDocumentIdTest.java new file mode 100644 index 000000000..976ef71b2 --- /dev/null +++ b/xrpl4j-core/src/test/java/org/xrpl/xrpl4j/model/transactions/OracleDocumentIdTest.java @@ -0,0 +1,65 @@ +package org.xrpl.xrpl4j.model.transactions; + +import static org.assertj.core.api.AssertionsForClassTypes.assertThat; + +import com.fasterxml.jackson.core.JsonProcessingException; +import com.fasterxml.jackson.databind.ObjectMapper; +import com.fasterxml.jackson.databind.annotation.JsonDeserialize; +import com.fasterxml.jackson.databind.annotation.JsonSerialize; +import com.google.common.primitives.UnsignedInteger; +import org.assertj.core.api.Assertions; +import org.immutables.value.Value; +import org.immutables.value.Value.Immutable; +import org.json.JSONException; +import org.junit.jupiter.api.Test; +import org.skyscreamer.jsonassert.JSONAssert; +import org.skyscreamer.jsonassert.JSONCompareMode; +import org.xrpl.xrpl4j.model.jackson.ObjectMapperFactory; + +/** + * Unit tests for {@link OracleDocumentId}. + */ +public class OracleDocumentIdTest { + + ObjectMapper objectMapper = ObjectMapperFactory.create(); + + @Test + void testToString() { + OracleDocumentId count = OracleDocumentId.of(UnsignedInteger.ONE); + assertThat(count.toString()).isEqualTo("1"); + } + + @Test + void testJson() throws JsonProcessingException, JSONException { + OracleDocumentId count = OracleDocumentId.of(UnsignedInteger.ONE); + OracleDocumentIdWrapper wrapper = OracleDocumentIdWrapper.of(count); + + String json = "{\"value\": 1}"; + assertSerializesAndDeserializes(wrapper, json); + } + + private void assertSerializesAndDeserializes( + OracleDocumentIdWrapper wrapper, + String json + ) throws JsonProcessingException, JSONException { + String serialized = objectMapper.writeValueAsString(wrapper); + JSONAssert.assertEquals(json, serialized, JSONCompareMode.STRICT); + OracleDocumentIdWrapper deserialized = objectMapper.readValue( + serialized, OracleDocumentIdWrapper.class + ); + Assertions.assertThat(deserialized).isEqualTo(wrapper); + } + + @Immutable + @JsonSerialize(as = ImmutableOracleDocumentIdWrapper.class) + @JsonDeserialize(as = ImmutableOracleDocumentIdWrapper.class) + interface OracleDocumentIdWrapper { + + static OracleDocumentIdWrapper of(OracleDocumentId value) { + return ImmutableOracleDocumentIdWrapper.builder().value(value).build(); + } + + OracleDocumentId value(); + + } +} diff --git a/xrpl4j-core/src/test/java/org/xrpl/xrpl4j/model/transactions/OracleProviderTest.java b/xrpl4j-core/src/test/java/org/xrpl/xrpl4j/model/transactions/OracleProviderTest.java new file mode 100644 index 000000000..024c11de5 --- /dev/null +++ b/xrpl4j-core/src/test/java/org/xrpl/xrpl4j/model/transactions/OracleProviderTest.java @@ -0,0 +1,61 @@ +package org.xrpl.xrpl4j.model.transactions; + +import static org.assertj.core.api.AssertionsForClassTypes.assertThat; + +import com.fasterxml.jackson.core.JsonProcessingException; +import com.fasterxml.jackson.databind.ObjectMapper; +import com.fasterxml.jackson.databind.annotation.JsonDeserialize; +import com.fasterxml.jackson.databind.annotation.JsonSerialize; +import com.google.common.primitives.UnsignedInteger; +import org.assertj.core.api.Assertions; +import org.immutables.value.Value.Immutable; +import org.json.JSONException; +import org.junit.jupiter.api.Test; +import org.skyscreamer.jsonassert.JSONAssert; +import org.skyscreamer.jsonassert.JSONCompareMode; +import org.xrpl.xrpl4j.model.jackson.ObjectMapperFactory; + +public class OracleProviderTest { + + ObjectMapper objectMapper = ObjectMapperFactory.create(); + + @Test + void testToString() { + OracleProvider count = OracleProvider.of("ABCD"); + assertThat(count.toString()).isEqualTo("ABCD"); + } + + @Test + void testJson() throws JsonProcessingException, JSONException { + OracleProvider count = OracleProvider.of("ABCD"); + OracleProviderWrapper wrapper = OracleProviderWrapper.of(count); + + String json = "{\"value\": \"ABCD\"}"; + assertSerializesAndDeserializes(wrapper, json); + } + + private void assertSerializesAndDeserializes( + OracleProviderWrapper wrapper, + String json + ) throws JsonProcessingException, JSONException { + String serialized = objectMapper.writeValueAsString(wrapper); + JSONAssert.assertEquals(json, serialized, JSONCompareMode.STRICT); + OracleProviderWrapper deserialized = objectMapper.readValue( + serialized, OracleProviderWrapper.class + ); + Assertions.assertThat(deserialized).isEqualTo(wrapper); + } + + @Immutable + @JsonSerialize(as = ImmutableOracleProviderWrapper.class) + @JsonDeserialize(as = ImmutableOracleProviderWrapper.class) + interface OracleProviderWrapper { + + static OracleProviderWrapper of(OracleProvider value) { + return ImmutableOracleProviderWrapper.builder().value(value).build(); + } + + OracleProvider value(); + + } +} diff --git a/xrpl4j-core/src/test/java/org/xrpl/xrpl4j/model/transactions/OracleSetTest.java b/xrpl4j-core/src/test/java/org/xrpl/xrpl4j/model/transactions/OracleSetTest.java new file mode 100644 index 000000000..05bdb2733 --- /dev/null +++ b/xrpl4j-core/src/test/java/org/xrpl/xrpl4j/model/transactions/OracleSetTest.java @@ -0,0 +1,94 @@ +package org.xrpl.xrpl4j.model.transactions; + +import static org.assertj.core.api.AssertionsForClassTypes.assertThat; +import static org.xrpl.xrpl4j.crypto.TestConstants.ED_PUBLIC_KEY; +import static org.xrpl.xrpl4j.crypto.TestConstants.ED_PUBLIC_KEY_HEX; + +import com.fasterxml.jackson.core.JsonProcessingException; +import com.google.common.primitives.UnsignedInteger; +import com.google.common.primitives.UnsignedLong; +import org.json.JSONException; +import org.junit.jupiter.api.Test; +import org.xrpl.xrpl4j.model.AbstractJsonTest; +import org.xrpl.xrpl4j.model.flags.TransactionFlags; + +class OracleSetTest extends AbstractJsonTest { + + @Test + void testDefaultFlags() { + OracleSet oracleSet = baseBuilder().build(); + + assertThat(oracleSet.flags()).isEqualTo(TransactionFlags.EMPTY); + } + + @Test + void testJsonEmptyFields() throws JSONException, JsonProcessingException { + OracleSet oracleSet = baseBuilder().build(); + String json = "\n" + + "{\n" + + " \"TransactionType\": \"OracleSet\",\n" + + " \"Account\": \"rp4pqYgrTAtdPHuZd1ZQWxrzx45jxYcZex\",\n" + + " \"OracleDocumentID\": 1,\n" + + " \"Fee\": \"12\",\n" + + " \"Sequence\": 391,\n" + + " \"SigningPubKey\": \"" + ED_PUBLIC_KEY_HEX + "\",\n" + + " \"LastUpdateTime\": 1\n" + + "}"; + + assertCanSerializeAndDeserialize(oracleSet, json); + } + + @Test + void testFullJson() throws JSONException, JsonProcessingException { + OracleSet oracleSet = baseBuilder() + .provider(OracleProvider.of("70726F7669646572")) + .assetClass("63757272656E6379") + .uri(OracleUri.of("ABCD")) + .addPriceDataSeries( + PriceDataWrapper.of( + PriceData.builder() + .baseAsset("XRP") + .quoteAsset("USD") + .assetPrice(AssetPrice.of(UnsignedLong.ONE)) + .scale(UnsignedInteger.valueOf(3)) + .build() + ) + ) + .build(); + String json = "\n" + + "{\n" + + " \"TransactionType\": \"OracleSet\",\n" + + " \"Account\": \"rp4pqYgrTAtdPHuZd1ZQWxrzx45jxYcZex\",\n" + + " \"Provider\": \"70726F7669646572\"," + + " \"AssetClass\": \"63757272656E6379\"," + + " \"URI\": \"ABCD\"," + + " \"OracleDocumentID\": 1,\n" + + " \"Fee\": \"12\",\n" + + " \"Sequence\": 391,\n" + + " \"SigningPubKey\": \"" + ED_PUBLIC_KEY_HEX + "\",\n" + + " \"LastUpdateTime\": 1,\n" + + " \"PriceDataSeries\": [\n" + + " {\n" + + " \"PriceData\": {\n" + + " \"BaseAsset\": \"XRP\",\n" + + " \"QuoteAsset\": \"USD\",\n" + + " \"AssetPrice\": \"1\",\n" + + " \"Scale\": 3\n" + + " }\n" + + " }\n" + + " ]" + + "}"; + + assertCanSerializeAndDeserialize(oracleSet, json); + } + + private static ImmutableOracleSet.Builder baseBuilder() { + return OracleSet.builder() + .account(Address.of("rp4pqYgrTAtdPHuZd1ZQWxrzx45jxYcZex")) + .fee(XrpCurrencyAmount.ofDrops(12)) + .sequence(UnsignedInteger.valueOf(391)) + .signingPublicKey(ED_PUBLIC_KEY) + .oracleDocumentId(OracleDocumentId.of(UnsignedInteger.ONE)) + .lastUpdateTime(UnsignedInteger.ONE); + } +} \ No newline at end of file diff --git a/xrpl4j-core/src/test/java/org/xrpl/xrpl4j/model/transactions/OracleUriTest.java b/xrpl4j-core/src/test/java/org/xrpl/xrpl4j/model/transactions/OracleUriTest.java new file mode 100644 index 000000000..84d4acac9 --- /dev/null +++ b/xrpl4j-core/src/test/java/org/xrpl/xrpl4j/model/transactions/OracleUriTest.java @@ -0,0 +1,63 @@ +package org.xrpl.xrpl4j.model.transactions; + +import static org.assertj.core.api.AssertionsForClassTypes.assertThat; + +import com.fasterxml.jackson.core.JsonProcessingException; +import com.fasterxml.jackson.databind.ObjectMapper; +import com.fasterxml.jackson.databind.annotation.JsonDeserialize; +import com.fasterxml.jackson.databind.annotation.JsonSerialize; +import org.assertj.core.api.Assertions; +import org.immutables.value.Value.Immutable; +import org.json.JSONException; +import org.junit.jupiter.api.Test; +import org.skyscreamer.jsonassert.JSONAssert; +import org.skyscreamer.jsonassert.JSONCompareMode; +import org.xrpl.xrpl4j.model.jackson.ObjectMapperFactory; + +/** + * Unit tests for {@link OracleUri}. + */ +public class OracleUriTest { + + ObjectMapper objectMapper = ObjectMapperFactory.create(); + + @Test + void testToString() { + OracleUri count = OracleUri.of("ABCD"); + assertThat(count.toString()).isEqualTo("ABCD"); + } + + @Test + void testJson() throws JsonProcessingException, JSONException { + OracleUri count = OracleUri.of("ABCD"); + OracleUriWrapper wrapper = OracleUriWrapper.of(count); + + String json = "{\"value\": \"ABCD\"}"; + assertSerializesAndDeserializes(wrapper, json); + } + + private void assertSerializesAndDeserializes( + OracleUriWrapper wrapper, + String json + ) throws JsonProcessingException, JSONException { + String serialized = objectMapper.writeValueAsString(wrapper); + JSONAssert.assertEquals(json, serialized, JSONCompareMode.STRICT); + OracleUriWrapper deserialized = objectMapper.readValue( + serialized, OracleUriWrapper.class + ); + Assertions.assertThat(deserialized).isEqualTo(wrapper); + } + + @Immutable + @JsonSerialize(as = ImmutableOracleUriWrapper.class) + @JsonDeserialize(as = ImmutableOracleUriWrapper.class) + interface OracleUriWrapper { + + static OracleUriWrapper of(OracleUri value) { + return ImmutableOracleUriWrapper.builder().value(value).build(); + } + + OracleUri value(); + + } +} diff --git a/xrpl4j-core/src/test/java/org/xrpl/xrpl4j/model/transactions/PriceDataTest.java b/xrpl4j-core/src/test/java/org/xrpl/xrpl4j/model/transactions/PriceDataTest.java new file mode 100644 index 000000000..6dec9e070 --- /dev/null +++ b/xrpl4j-core/src/test/java/org/xrpl/xrpl4j/model/transactions/PriceDataTest.java @@ -0,0 +1,69 @@ +package org.xrpl.xrpl4j.model.transactions; + +import static org.assertj.core.api.AssertionsForClassTypes.assertThat; + +import com.fasterxml.jackson.core.JsonProcessingException; +import com.fasterxml.jackson.databind.ObjectMapper; +import com.google.common.primitives.UnsignedLong; +import org.assertj.core.api.Assertions; +import org.json.JSONException; +import org.junit.jupiter.api.Test; +import org.skyscreamer.jsonassert.JSONAssert; +import org.skyscreamer.jsonassert.JSONCompareMode; +import org.xrpl.xrpl4j.model.jackson.ObjectMapperFactory; + +/** + * Unit test for {@link PriceData}. + */ +class PriceDataTest { + + ObjectMapper objectMapper = ObjectMapperFactory.create(); + + @Test + void builder() { + PriceData priceData = PriceData.builder() + .assetPrice(AssetPrice.of(UnsignedLong.ONE)) + .baseAsset("baseAsset") + .quoteAsset("quoteAsset") + .build(); + + assertThat(priceData.assetPrice().isPresent()).isTrue(); + priceData.assetPrice().ifPresent((assetPrice) -> assertThat(assetPrice.value()).isEqualTo(UnsignedLong.ONE)); + + assertThat(priceData.baseAsset()).isEqualTo("baseAsset"); + assertThat(priceData.quoteAsset()).isEqualTo("quoteAsset"); + } + + @Test + void testToString() { + PriceData priceData = PriceData.builder() + .assetPrice(AssetPrice.of(UnsignedLong.ONE)) + .baseAsset("baseAsset") + .quoteAsset("quoteAsset") + .build(); + assertThat(priceData.toString()).isEqualTo("PriceData{baseAsset=baseAsset, quoteAsset=quoteAsset, assetPrice=1}"); + } + + @Test + void testJson() throws JsonProcessingException, JSONException { + PriceData priceData = PriceData.builder() + .assetPrice(AssetPrice.of(UnsignedLong.ONE)) + .baseAsset("baseAsset") + .quoteAsset("quoteAsset") + .build(); + PriceDataWrapper wrapper = PriceDataWrapper.of(priceData); + + String json = "{\"PriceData\":{\"BaseAsset\":\"baseAsset\",\"QuoteAsset\":\"quoteAsset\",\"AssetPrice\":\"1\"}}"; + assertSerializesAndDeserializes(wrapper, json); + } + + private void assertSerializesAndDeserializes( + PriceDataWrapper wrapper, + String json + ) throws JsonProcessingException, JSONException { + String serialized = objectMapper.writeValueAsString(wrapper); + JSONAssert.assertEquals(json, serialized, JSONCompareMode.STRICT); + PriceDataWrapper deserialized = objectMapper.readValue(serialized, PriceDataWrapper.class); + Assertions.assertThat(deserialized).isEqualTo(wrapper); + } +} \ No newline at end of file diff --git a/xrpl4j-core/src/test/java/org/xrpl/xrpl4j/model/transactions/PriceDataWrapperTest.java b/xrpl4j-core/src/test/java/org/xrpl/xrpl4j/model/transactions/PriceDataWrapperTest.java new file mode 100644 index 000000000..8fc7825d2 --- /dev/null +++ b/xrpl4j-core/src/test/java/org/xrpl/xrpl4j/model/transactions/PriceDataWrapperTest.java @@ -0,0 +1,42 @@ +package org.xrpl.xrpl4j.model.transactions; + +import com.fasterxml.jackson.core.JsonProcessingException; +import com.fasterxml.jackson.databind.ObjectMapper; +import com.google.common.primitives.UnsignedLong; +import org.assertj.core.api.Assertions; +import org.json.JSONException; +import org.junit.jupiter.api.Test; +import org.skyscreamer.jsonassert.JSONAssert; +import org.skyscreamer.jsonassert.JSONCompareMode; +import org.xrpl.xrpl4j.model.jackson.ObjectMapperFactory; + +/** + * Unit tests for {@link PriceDataWrapper}. + */ +class PriceDataWrapperTest { + + ObjectMapper objectMapper = ObjectMapperFactory.create(); + + @Test + void testJson() throws JsonProcessingException, JSONException { + PriceData priceData = PriceData.builder() + .assetPrice(AssetPrice.of(UnsignedLong.ONE)) + .baseAsset("baseAsset") + .quoteAsset("quoteAsset") + .build(); + PriceDataWrapper wrapper = PriceDataWrapper.of(priceData); + + String json = "{\"PriceData\":{\"BaseAsset\":\"baseAsset\",\"QuoteAsset\":\"quoteAsset\",\"AssetPrice\":\"1\"}}"; + assertSerializesAndDeserializes(wrapper, json); + } + + private void assertSerializesAndDeserializes( + PriceDataWrapper wrapper, + String json + ) throws JsonProcessingException, JSONException { + String serialized = objectMapper.writeValueAsString(wrapper); + JSONAssert.assertEquals(json, serialized, JSONCompareMode.STRICT); + PriceDataWrapper deserialized = objectMapper.readValue(serialized, PriceDataWrapper.class); + Assertions.assertThat(deserialized).isEqualTo(wrapper); + } +} \ No newline at end of file diff --git a/xrpl4j-core/src/test/java/org/xrpl/xrpl4j/model/transactions/metadata/MetaLedgerEntryTypeTest.java b/xrpl4j-core/src/test/java/org/xrpl/xrpl4j/model/transactions/metadata/MetaLedgerEntryTypeTest.java index 5333b30dd..20045d4e1 100644 --- a/xrpl4j-core/src/test/java/org/xrpl/xrpl4j/model/transactions/metadata/MetaLedgerEntryTypeTest.java +++ b/xrpl4j-core/src/test/java/org/xrpl/xrpl4j/model/transactions/metadata/MetaLedgerEntryTypeTest.java @@ -9,6 +9,7 @@ import org.junit.jupiter.api.Test; import org.xrpl.xrpl4j.model.AbstractJsonTest; import org.xrpl.xrpl4j.model.ledger.AccountRootObject; +import org.xrpl.xrpl4j.model.ledger.OracleObject; class MetaLedgerEntryTypeTest extends AbstractJsonTest { @@ -36,6 +37,7 @@ void testConstants() { .isEqualTo("XChainOwnedCreateAccountClaimID"); assertThat(MetaLedgerEntryType.XCHAIN_OWNED_CLAIM_ID.value()).isEqualTo("XChainOwnedClaimID"); assertThat(MetaLedgerEntryType.DID.value()).isEqualTo("DID"); + assertThat(MetaLedgerEntryType.ORACLE.value()).isEqualTo("Oracle"); } @Test @@ -64,6 +66,7 @@ void testLedgerObjectType() { MetaXChainOwnedClaimIdObject.class ); assertThat(MetaLedgerEntryType.DID.ledgerObjectType()).isEqualTo(MetaDidObject.class); + assertThat(MetaLedgerEntryType.ORACLE.ledgerObjectType()).isEqualTo(MetaOracleObject.class); } @Test diff --git a/xrpl4j-integration-tests/src/test/java/org/xrpl/xrpl4j/tests/AbstractIT.java b/xrpl4j-integration-tests/src/test/java/org/xrpl/xrpl4j/tests/AbstractIT.java index 261e938de..dcc1fdb97 100644 --- a/xrpl4j-integration-tests/src/test/java/org/xrpl/xrpl4j/tests/AbstractIT.java +++ b/xrpl4j-integration-tests/src/test/java/org/xrpl/xrpl4j/tests/AbstractIT.java @@ -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. @@ -68,6 +68,7 @@ import org.xrpl.xrpl4j.model.client.transactions.SubmitResult; import org.xrpl.xrpl4j.model.client.transactions.TransactionRequestParams; import org.xrpl.xrpl4j.model.client.transactions.TransactionResult; +import org.xrpl.xrpl4j.model.flags.TrustSetFlags; import org.xrpl.xrpl4j.model.ledger.LedgerObject; import org.xrpl.xrpl4j.model.transactions.Address; import org.xrpl.xrpl4j.model.transactions.Hash256; @@ -77,7 +78,6 @@ import org.xrpl.xrpl4j.model.transactions.TransactionResultCodes; import org.xrpl.xrpl4j.model.transactions.TransactionType; import org.xrpl.xrpl4j.model.transactions.TrustSet; -import org.xrpl.xrpl4j.model.transactions.XChainCreateBridge; import org.xrpl.xrpl4j.model.transactions.XrpCurrencyAmount; import org.xrpl.xrpl4j.tests.environment.XrplEnvironment; @@ -128,16 +128,13 @@ protected AbstractIT() { protected void logInfo(TransactionType transactionType, Hash256 hash) { String url = System.getProperty("useTestnet") != null ? "https://testnet.xrpl.org/transactions/" : (System.getProperty("useDevnet") != null ? "https://devnet.xrpl.org/transactions/" : ""); - logger.info(transactionType.value() + " transaction successful: {}{}", url, hash); + logger.info("{} transaction successful: {}{}", transactionType.value(), url, hash); } protected KeyPair createRandomAccountEd25519() { // Create the account final KeyPair randomKeyPair = Seed.ed25519Seed().deriveKeyPair(); - logger.info( - "Generated testnet wallet with ClassicAddress={})", - randomKeyPair.publicKey().deriveAddress() - ); + logAccountCreation(randomKeyPair.publicKey().deriveAddress()); fundAccount(randomKeyPair.publicKey().deriveAddress()); @@ -147,10 +144,7 @@ protected KeyPair createRandomAccountEd25519() { protected KeyPair createRandomAccountSecp256k1() { // Create the account final KeyPair randomKeyPair = Seed.secp256k1Seed().deriveKeyPair(); - logger.info( - "Generated testnet wallet with ClassicAddress={})", - randomKeyPair.publicKey().deriveAddress() - ); + logAccountCreation(randomKeyPair.publicKey().deriveAddress()); fundAccount(randomKeyPair.publicKey().deriveAddress()); @@ -171,7 +165,8 @@ public String keyIdentifier() { }; PublicKey publicKey = derivedKeySignatureService.derivePublicKey(privateKeyReference); - logger.info("Generated testnet wallet with ClassicAddress={})", publicKey.deriveAddress()); + logAccountCreation(publicKey.deriveAddress()); + fundAccount(publicKey.deriveAddress()); return privateKeyReference; @@ -191,7 +186,8 @@ public String keyIdentifier() { }; PublicKey publicKey = derivedKeySignatureService.derivePublicKey(privateKeyReference); - logger.info("Generated testnet wallet with ClassicAddress={})", publicKey.deriveAddress()); + logAccountCreation(publicKey.deriveAddress()); + fundAccount(publicKey.deriveAddress()); return privateKeyReference; @@ -425,28 +421,47 @@ protected Instant xrpTimestampToInstant(UnsignedLong xrpTimeStamp) { } /** - * Create a trustline between the given issuer and counterparty accounts for the given currency code and with the - * given limit. + * Create a trustline between the issuer of the specified {@param trustlineLimitAmount} and specified counterparty for + * the given currency code with the given limit. * - * @param currency The currency code of the trustline to create. - * @param value The trustline limit of the trustline to create. - * @param issuerKeyPair The {@link KeyPair} of the issuer account. - * @param counterpartyKeyPair The {@link KeyPair} of the counterparty account. - * @param fee The current network fee, as an {@link XrpCurrencyAmount}. + * @param counterpartyKeyPair The {@link KeyPair} of the counterparty account. + * @param trustlineLimitAmount A {@link IssuedCurrencyAmount} representing the trust limit for the counterparty. + * @param fee The current network fee, as an {@link XrpCurrencyAmount}. * * @return The {@link TrustLine} that gets created. * * @throws JsonRpcClientErrorException If anything goes wrong while communicating with rippled. */ public TrustLine createTrustLine( - String currency, - String value, - KeyPair issuerKeyPair, KeyPair counterpartyKeyPair, + IssuedCurrencyAmount trustlineLimitAmount, XrpCurrencyAmount fee + ) throws JsonRpcClientErrorException, JsonProcessingException { + return createTrustLine( + counterpartyKeyPair, trustlineLimitAmount, fee, TrustSetFlags.builder().tfSetNoRipple().build() + ); + } + + /** + * Create a trustline between the issuer of the specified {@param trustlineLimitAmount} and specified counterparty for + * the given currency code with the given limit. + * + * @param counterpartyKeyPair The {@link KeyPair} of the counterparty account. + * @param trustlineLimitAmount A {@link IssuedCurrencyAmount} representing the trust limit for the counterparty. + * @param fee The current network fee, as an {@link XrpCurrencyAmount}. + * @param trustSetFlags A {@link TrustSetFlags} to use when creating the trustline. + * + * @return The {@link TrustLine} that gets created. + * + * @throws JsonRpcClientErrorException If anything goes wrong while communicating with rippled. + */ + public TrustLine createTrustLine( + KeyPair counterpartyKeyPair, + IssuedCurrencyAmount trustlineLimitAmount, + XrpCurrencyAmount fee, + TrustSetFlags trustSetFlags ) throws JsonRpcClientErrorException, JsonProcessingException { Address counterpartyAddress = counterpartyKeyPair.publicKey().deriveAddress(); - Address issuerAddress = issuerKeyPair.publicKey().deriveAddress(); AccountInfoResult counterpartyAccountInfo = this.scanForResult( () -> this.getValidatedAccountInfo(counterpartyAddress) @@ -456,11 +471,8 @@ public TrustLine createTrustLine( .account(counterpartyAddress) .fee(fee) .sequence(counterpartyAccountInfo.accountData().sequence()) - .limitAmount(IssuedCurrencyAmount.builder() - .currency(currency) - .issuer(issuerAddress) - .value(value) - .build()) + .limitAmount(trustlineLimitAmount) + .flags(trustSetFlags) .signingPublicKey(counterpartyKeyPair.publicKey()) .build(); @@ -477,72 +489,138 @@ public TrustLine createTrustLine( ); return scanForResult( - () -> - getValidatedAccountLines(issuerAddress, counterpartyAddress), + () -> getValidatedAccountLines(trustlineLimitAmount.issuer(), counterpartyAddress), linesResult -> !linesResult.lines().isEmpty() - ) - .lines().get(0); + ).lines().get(0); } /** - * Send issued currency funds from an issuer to a counterparty. + * Send issued currency funds from a sender to a receiver. * - * @param currency The currency code to send. - * @param value The amount of currency to send. - * @param issuerKeyPair The {@link KeyPair} of the issuer account. - * @param counterpartyKeyPair The {@link KeyPair} of the counterparty account. - * @param fee The current network fee, as an {@link XrpCurrencyAmount}. + * @param senderKeyPair The {@link KeyPair} of the payment sender. + * @param receiverKeyPair The {@link KeyPair} of the payment receiver. + * @param amount An {@link IssuedCurrencyAmount} to send. + * @param fee The current network fee, as an {@link XrpCurrencyAmount}. * * @throws JsonRpcClientErrorException If anything goes wrong while communicating with rippled. */ public void sendIssuedCurrency( - String currency, - String value, - KeyPair issuerKeyPair, - KeyPair counterpartyKeyPair, + KeyPair senderKeyPair, + KeyPair receiverKeyPair, + IssuedCurrencyAmount amount, XrpCurrencyAmount fee ) throws JsonRpcClientErrorException, JsonProcessingException { - Address counterpartyAddress = counterpartyKeyPair.publicKey().deriveAddress(); - Address issuerAddress = issuerKeyPair.publicKey().deriveAddress(); + sendIssuedCurrency(senderKeyPair, receiverKeyPair, amount, fee, TransactionResultCodes.TES_SUCCESS); + } - /////////////////////////// - // Issuer sends a payment with the issued currency to the counterparty - AccountInfoResult issuerAccountInfo = this.scanForResult( - () -> getValidatedAccountInfo(issuerAddress) - ); + /** + * Send issued currency funds from a sender to a receiver. + * + * @param senderKeyPair The {@link KeyPair} of the payment sender. + * @param receiverKeyPair The {@link KeyPair} of the payment receiver. + * @param amount An {@link IssuedCurrencyAmount} to send. + * @param fee The current network fee, as an {@link XrpCurrencyAmount}. + * @param expectedEngineResult The expected engine result. + * + * @throws JsonRpcClientErrorException If anything goes wrong while communicating with rippled. + */ + public void sendIssuedCurrency( + KeyPair senderKeyPair, + KeyPair receiverKeyPair, + IssuedCurrencyAmount amount, + XrpCurrencyAmount fee, + String expectedEngineResult + ) throws JsonRpcClientErrorException, JsonProcessingException { + Objects.requireNonNull(senderKeyPair); + Objects.requireNonNull(receiverKeyPair); + Objects.requireNonNull(amount); + Objects.requireNonNull(fee); + Objects.requireNonNull(expectedEngineResult); + + final Address senderAddress = senderKeyPair.publicKey().deriveAddress(); + final Address receiverAddress = receiverKeyPair.publicKey().deriveAddress(); + + int loopGuard = 0; + String paymentEngineResult = null; + + while (!expectedEngineResult.equalsIgnoreCase(paymentEngineResult)) { + if (loopGuard++ > 30) { + throw new RuntimeException( + String.format("engineResult should have been `%s`, but was `%s` instead", + expectedEngineResult, paymentEngineResult + ) + ); + } - Payment fundCounterparty = Payment.builder() - .account(issuerAddress) - .fee(fee) - .sequence(issuerAccountInfo.accountData().sequence()) - .destination(counterpartyAddress) - .amount(IssuedCurrencyAmount.builder() - .issuer(issuerAddress) - .currency(currency) - .value(value) - .build()) - .signingPublicKey(issuerKeyPair.publicKey()) - .build(); + /////////////////////////// + // Sender sends a payment with the issued currency to the counterparty + AccountInfoResult senderAccountInfo = this.scanForResult( + () -> getValidatedAccountInfo(senderAddress) + ); + UnsignedInteger currentSenderSequence = senderAccountInfo.accountData().sequence(); + logger.info("About to send a payment on Sequence={}", currentSenderSequence); + + Payment fundCounterparty = Payment.builder() + .account(senderAddress) + .fee(fee) + .sequence(senderAccountInfo.accountData().sequence()) + .destination(receiverAddress) + .amount(amount) + .signingPublicKey(senderKeyPair.publicKey()) + .build(); - SingleSignedTransaction signedPayment = signatureService.sign( - issuerKeyPair.privateKey(), - fundCounterparty - ); - SubmitResult paymentResult = xrplClient.submit(signedPayment); - assertThat(paymentResult.engineResult()).isEqualTo(TransactionResultCodes.TES_SUCCESS); - assertThat(paymentResult.transactionResult().hash()).isEqualTo(signedPayment.hash()); + SingleSignedTransaction signedPayment = signatureService.sign( + senderKeyPair.privateKey(), fundCounterparty + ); + SubmitResult paymentResult = xrplClient.submit(signedPayment); + assertThat(paymentResult.transactionResult().hash()).isEqualTo(signedPayment.hash()); + + paymentEngineResult = paymentResult.engineResult(); + if (!paymentResult.engineResult().equals(expectedEngineResult)) { + try { + // If the code gets here, it means the transaction did not succeed. The most typical reason here is a latent + // Clio node (see description at the end of this function). This loop allows the code to retry for just a bit + // longer than the current 60s DNS TTL. + Thread.sleep(3000); // <-- Sleep for 3 seconds and try again + } catch (InterruptedException e) { + throw new RuntimeException(e.getMessage(), e); + } + logger.error( + "PaymentEngineResult `{}` did not equal expectedEngineResult `{}`", paymentEngineResult, expectedEngineResult + ); + continue; // <-- Try again, up to the loop guard above. + } - logInfo( - paymentResult.transactionResult().transaction().transactionType(), - paymentResult.transactionResult().hash() - ); + logInfo( + paymentResult.transactionResult().transaction().transactionType(), + paymentResult.transactionResult().hash() + ); - this.scanForResult( - () -> getValidatedTransaction( - paymentResult.transactionResult().hash(), - Payment.class) - ); + this.scanForResult( + () -> getValidatedTransaction( + paymentResult.transactionResult().hash(), + Payment.class) + ); + // This extra check exists for Clio servers. Occasionally, one Clio server in the cluster will report that a TX + // (e.g., with sequence 5) is `VALIDATED`. Subsequent calls to `account_info` should return an account sequence + // number of 6, but sometimes one of the servers in the cluster will return the old sequence value of 5. This + // scanner simply waits until at least one of the Clio server reports the correct account_sequence number so that + // subsequent calls to `account_info` will typically have the correct account sequence. Note that this solution + // will _mostly_ work, but not always. Consider an example with three Clio nodes in a cluster. Node A is latent, + // but B & C are not (i.e., B & C have an up-to-date account sequence). In this instance, this scanner might + // receive a result from B or C, but on the next payment, this code might get a response from the (incorrect) + // node A. In reality, this should _almost_ never happen because the current testnet DNS configuration + // pegs JSON-RPC clients to a single IP address (i.e., single clio server) for 60s. Therefore, there should + // only be very tiny windows where this solution does not fix the issue. For that, we have the loop above as + // well to retry. + this.scanForResult( + () -> getValidatedAccountInfo(senderAddress), + result -> result.accountData().sequence().equals( + senderAccountInfo.accountData().sequence().plus(UnsignedInteger.ONE) + ) + ); + } } ////////////////// @@ -603,10 +681,7 @@ protected SignatureService constructSignatureService() { protected KeyPair constructRandomAccount() { // Create the account final KeyPair randomKeyPair = Seed.ed25519Seed().deriveKeyPair(); - logger.info( - "Generated testnet wallet with ClassicAddress={})", - randomKeyPair.publicKey().deriveAddress() - ); + logAccountCreation(randomKeyPair.publicKey().deriveAddress()); fundAccount(randomKeyPair.publicKey().deriveAddress()); @@ -627,4 +702,8 @@ private KeyStore loadKeyStore() { final char[] jksPassword = "password".toCharArray(); return JavaKeystoreLoader.loadFromClasspath(jksFileName, jksPassword); } + + private void logAccountCreation(Address address) { + logger.info("Generated wallet with ClassicAddress={})", address); + } } diff --git a/xrpl4j-integration-tests/src/test/java/org/xrpl/xrpl4j/tests/ClawbackIT.java b/xrpl4j-integration-tests/src/test/java/org/xrpl/xrpl4j/tests/ClawbackIT.java index ace2e32f1..fc1ae9abc 100644 --- a/xrpl4j-integration-tests/src/test/java/org/xrpl/xrpl4j/tests/ClawbackIT.java +++ b/xrpl4j-integration-tests/src/test/java/org/xrpl/xrpl4j/tests/ClawbackIT.java @@ -45,24 +45,28 @@ void issueBalanceAndClawback() throws JsonRpcClientErrorException, JsonProcessin setAllowClawback(issuerKeyPair, issuerAccount, fee); createTrustLine( - "USD", - "10000", - issuerKeyPair, holderKeyPair, + IssuedCurrencyAmount.builder() + .issuer(issuerKeyPair.publicKey().deriveAddress()) + .currency("USD") + .value("10000") + .build(), fee ); sendIssuedCurrency( - "USD", - "100", issuerKeyPair, holderKeyPair, + IssuedCurrencyAmount.builder() + .issuer(issuerKeyPair.publicKey().deriveAddress()) + .currency("USD") + .value("100") + .build(), fee ); issuerAccount = this.getValidatedAccountInfo(issuerAccount.accountData().account()); clawback( - "USD", "10", holderKeyPair.publicKey().deriveAddress(), issuerKeyPair, @@ -82,7 +86,6 @@ void issueBalanceAndClawback() throws JsonRpcClientErrorException, JsonProcessin issuerAccount = this.getValidatedAccountInfo(issuerAccount.accountData().account()); clawback( - "USD", "90", holderKeyPair.publicKey().deriveAddress(), issuerKeyPair, @@ -101,7 +104,6 @@ void issueBalanceAndClawback() throws JsonRpcClientErrorException, JsonProcessin } private void clawback( - String currencyCode, String amount, Address holderAddress, KeyPair issuerKeyPair, @@ -115,7 +117,7 @@ private void clawback( .signingPublicKey(issuerKeyPair.publicKey()) .amount( IssuedCurrencyAmount.builder() - .currency(currencyCode) + .currency("USD") .value(amount) .issuer(holderAddress) .build() @@ -130,7 +132,7 @@ private void clawback( scanForFinality( signedClawback.hash(), issuerAccountInfo.ledgerIndexSafe(), - clawback.lastLedgerSequence().get(), + clawback.lastLedgerSequence().orElseThrow(() -> new RuntimeException("Clawback lacked lastLedgerSequence")), clawback.sequence(), clawback.account() ); @@ -159,7 +161,7 @@ private void setAllowClawback( scanForFinality( signedAccountSet.hash(), issuerAccount.ledgerIndexSafe(), - accountSet.lastLedgerSequence().get(), + accountSet.lastLedgerSequence().orElseThrow(() -> new RuntimeException("AccountSet lacked lastLedgerSequence")), accountSet.sequence(), accountSet.account() ); diff --git a/xrpl4j-integration-tests/src/test/java/org/xrpl/xrpl4j/tests/EscrowIT.java b/xrpl4j-integration-tests/src/test/java/org/xrpl/xrpl4j/tests/EscrowIT.java index 5d34919b7..c112b6009 100644 --- a/xrpl4j-integration-tests/src/test/java/org/xrpl/xrpl4j/tests/EscrowIT.java +++ b/xrpl4j-integration-tests/src/test/java/org/xrpl/xrpl4j/tests/EscrowIT.java @@ -504,18 +504,25 @@ private Instant getMinExpirationTime() { return closeTime.isBefore(now) ? now : closeTime; } - private void assertEntryEqualsObjectFromAccountObjects( Address escrowOwner, UnsignedInteger createSequence ) throws JsonRpcClientErrorException { - - EscrowObject escrowObject = (EscrowObject) xrplClient.accountObjects(AccountObjectsRequestParams.builder() - .type(AccountObjectType.ESCROW) - .account(escrowOwner) - .ledgerSpecifier(LedgerSpecifier.VALIDATED) - .build() - ).accountObjects().get(0); + EscrowObject escrowObject = (EscrowObject) this.scanForResult( + () -> { + try { + return xrplClient.accountObjects(AccountObjectsRequestParams.builder() + .type(AccountObjectType.ESCROW) + .account(escrowOwner) + .ledgerSpecifier(LedgerSpecifier.VALIDATED) + .build() + ).accountObjects(); + } catch (JsonRpcClientErrorException e) { + throw new RuntimeException(e); + } + }, + result -> result.size() == 1 + ).get(0); LedgerEntryResult escrowEntry = xrplClient.ledgerEntry( LedgerEntryRequestParams.escrow( diff --git a/xrpl4j-integration-tests/src/test/java/org/xrpl/xrpl4j/tests/FreezeIssuedCurrencyIT.java b/xrpl4j-integration-tests/src/test/java/org/xrpl/xrpl4j/tests/FreezeIssuedCurrencyIT.java index 613e0fbd5..920dbb619 100644 --- a/xrpl4j-integration-tests/src/test/java/org/xrpl/xrpl4j/tests/FreezeIssuedCurrencyIT.java +++ b/xrpl4j-integration-tests/src/test/java/org/xrpl/xrpl4j/tests/FreezeIssuedCurrencyIT.java @@ -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. @@ -21,6 +21,8 @@ */ import static org.assertj.core.api.Assertions.assertThat; +import static org.xrpl.xrpl4j.model.transactions.TransactionResultCodes.TEC_PATH_DRY; +import static org.xrpl.xrpl4j.model.transactions.TransactionResultCodes.TES_SUCCESS; import com.fasterxml.jackson.core.JsonProcessingException; import com.google.common.base.Strings; @@ -41,14 +43,12 @@ import org.xrpl.xrpl4j.model.transactions.AccountSet.AccountSetFlag; import org.xrpl.xrpl4j.model.transactions.ImmutableAccountSet; import org.xrpl.xrpl4j.model.transactions.IssuedCurrencyAmount; -import org.xrpl.xrpl4j.model.transactions.Payment; import org.xrpl.xrpl4j.model.transactions.TrustSet; import org.xrpl.xrpl4j.model.transactions.XrpCurrencyAmount; /** * An Integration Test to validate that an amount of issued currency held by a bad actor can be "frozen." */ -@SuppressWarnings("deprecation") public class FreezeIssuedCurrencyIT extends AbstractIT { private static final String TEN_THOUSAND = "10000"; @@ -85,9 +85,13 @@ public void issueAndFreezeFundsIndividual() throws JsonRpcClientErrorException, FeeResult feeResult = xrplClient.fee(); // Create a Trust Line between issuer and the bad actor. - TrustLine badActorTrustLine = this.createTrustLine( - issuerKeyPair, + TrustLine badActorTrustLine = createTrustLine( badActorKeyPair, + IssuedCurrencyAmount.builder() + .issuer(issuerKeyPair.publicKey().deriveAddress()) + .currency(ISSUED_CURRENCY_CODE) + .value(TEN_THOUSAND) + .build(), FeeUtils.computeNetworkFees(feeResult).recommendedFee() ); assertThat(badActorTrustLine.freeze()).isFalse(); @@ -98,8 +102,12 @@ public void issueAndFreezeFundsIndividual() throws JsonRpcClientErrorException, /////////////////////////// // Create a Trust Line between issuer and the good actor. TrustLine goodActorTrustLine = this.createTrustLine( - issuerKeyPair, goodActorKeyPair, + IssuedCurrencyAmount.builder() + .issuer(issuerKeyPair.publicKey().deriveAddress()) + .currency(ISSUED_CURRENCY_CODE) + .value(TEN_THOUSAND) + .build(), FeeUtils.computeNetworkFees(feeResult).recommendedFee() ); assertThat(goodActorTrustLine.freeze()).isFalse(); @@ -111,9 +119,23 @@ public void issueAndFreezeFundsIndividual() throws JsonRpcClientErrorException, // Send Funds ///////////// - // Send funds from issuer to the badActor. - sendFunds( - TEN_THOUSAND, issuerKeyPair, badActorKeyPair, FeeUtils.computeNetworkFees(feeResult).recommendedFee() + logger.info("Send ${} from issuer ({}) to the badActor ({}) and expect {}", + TEN_THOUSAND, + issuerKeyPair.publicKey().deriveAddress(), + goodActorKeyPair.publicKey().deriveAddress(), + TES_SUCCESS + ); + + sendIssuedCurrency( + issuerKeyPair, // <-- From + badActorKeyPair, // <-- To + IssuedCurrencyAmount.builder() + .currency(FreezeIssuedCurrencyIT.ISSUED_CURRENCY_CODE) + .value(TEN_THOUSAND) + .issuer(issuerKeyPair.publicKey().deriveAddress()) + .build(), + FeeUtils.computeNetworkFees(feeResult).recommendedFee(), + TES_SUCCESS ); /////////////////////////// @@ -125,9 +147,22 @@ public void issueAndFreezeFundsIndividual() throws JsonRpcClientErrorException, .anyMatch(line -> line.balance().equals("-" + TEN_THOUSAND)) ); - // Send funds from badActor to the goodActor. - sendFunds( - FIVE_THOUSAND, badActorKeyPair, goodActorKeyPair, FeeUtils.computeNetworkFees(feeResult).recommendedFee() + logger.info("Send ${} from badActor ({}) to the goodActor ({}) and expect {}", + FIVE_THOUSAND, + badActorKeyPair.publicKey().deriveAddress(), + goodActorKeyPair.publicKey().deriveAddress(), + TES_SUCCESS + ); + + sendIssuedCurrency( + badActorKeyPair, // <-- From + goodActorKeyPair, // <-- To + IssuedCurrencyAmount.builder() + .currency(FreezeIssuedCurrencyIT.ISSUED_CURRENCY_CODE) + .value(FIVE_THOUSAND) + .issuer(issuerKeyPair.publicKey().deriveAddress()) + .build(), + FeeUtils.computeNetworkFees(feeResult).recommendedFee() ); /////////////////////////// @@ -159,20 +194,62 @@ public void issueAndFreezeFundsIndividual() throws JsonRpcClientErrorException, // 2) The counterparty can only send the frozen currencies directly to the issuer (no where else) // 3) The counterparty can still receive payments from others on the frozen trust line. - // Try to send funds from badActor to goodActor should not work because the badActor is frozen. - sendFunds( - FIVE_THOUSAND, badActorKeyPair, goodActorKeyPair, FeeUtils.computeNetworkFees(feeResult).recommendedFee(), - "tecPATH_DRY" + logger.info("Send ${} from badActor ({}) to the goodActor ({}) and expect {}", + FIVE_THOUSAND, + badActorKeyPair.publicKey().deriveAddress(), + goodActorKeyPair.publicKey().deriveAddress(), + TEC_PATH_DRY + ); + + sendIssuedCurrency( + badActorKeyPair, // <-- From + goodActorKeyPair, // <-- To + IssuedCurrencyAmount.builder() + .currency(FreezeIssuedCurrencyIT.ISSUED_CURRENCY_CODE) + .value(FIVE_THOUSAND) + .issuer(issuerKeyPair.publicKey().deriveAddress()) + .build(), + FeeUtils.computeNetworkFees(feeResult).recommendedFee(), + TEC_PATH_DRY ); // Sending from the badActor to the issuer should still work - sendFunds( - FIVE_THOUSAND, badActorKeyPair, issuerKeyPair, FeeUtils.computeNetworkFees(feeResult).recommendedFee() + logger.info("Send ${} from badActor ({}) to the issuer ({}) and expect {}", + FIVE_THOUSAND, + badActorKeyPair.publicKey().deriveAddress(), + issuerKeyPair.publicKey().deriveAddress(), + TEC_PATH_DRY + ); + + sendIssuedCurrency( + badActorKeyPair, // <-- From + issuerKeyPair, // <-- To + IssuedCurrencyAmount.builder() + .currency(FreezeIssuedCurrencyIT.ISSUED_CURRENCY_CODE) + .value(FIVE_THOUSAND) + .issuer(issuerKeyPair.publicKey().deriveAddress()) + .build(), + FeeUtils.computeNetworkFees(feeResult).recommendedFee(), + TEC_PATH_DRY ); // Sending from the goodActor to the badActor should still work - sendFunds( - FIVE_THOUSAND, goodActorKeyPair, badActorKeyPair, FeeUtils.computeNetworkFees(feeResult).recommendedFee() + logger.info("Send ${} from goodActor ({}) to the badActor ({}) and expect {}", + FIVE_THOUSAND, + goodActorKeyPair.publicKey().deriveAddress(), + badActorKeyPair.publicKey().deriveAddress(), + TEC_PATH_DRY + ); + sendIssuedCurrency( + goodActorKeyPair, // <-- From + badActorKeyPair, // <-- To + IssuedCurrencyAmount.builder() + .currency(FreezeIssuedCurrencyIT.ISSUED_CURRENCY_CODE) + .value(FIVE_THOUSAND) + .issuer(issuerKeyPair.publicKey().deriveAddress()) + .build(), + FeeUtils.computeNetworkFees(feeResult).recommendedFee(), + TEC_PATH_DRY ); // Unfreeze the bad actor. @@ -201,8 +278,12 @@ public void issueAndFreezeFundsGlobal() throws JsonRpcClientErrorException, Json // Create a Trust Line between issuer and the bad actor. TrustLine badActorTrustLine = this.createTrustLine( - issuerKeyPair, badActorKeyPair, + IssuedCurrencyAmount.builder() + .issuer(issuerKeyPair.publicKey().deriveAddress()) + .currency(ISSUED_CURRENCY_CODE) + .value(TEN_THOUSAND) + .build(), FeeUtils.computeNetworkFees(feeResult).recommendedFee() ); assertThat(badActorTrustLine.freeze()).isFalse(); @@ -213,8 +294,12 @@ public void issueAndFreezeFundsGlobal() throws JsonRpcClientErrorException, Json /////////////////////////// // Create a Trust Line between issuer and the good actor. TrustLine goodActorTrustLine = this.createTrustLine( - issuerKeyPair, goodActorKeyPair, + IssuedCurrencyAmount.builder() + .issuer(issuerKeyPair.publicKey().deriveAddress()) + .currency(ISSUED_CURRENCY_CODE) + .value(TEN_THOUSAND) + .build(), FeeUtils.computeNetworkFees(feeResult).recommendedFee() ); assertThat(goodActorTrustLine.freeze()).isFalse(); @@ -226,9 +311,22 @@ public void issueAndFreezeFundsGlobal() throws JsonRpcClientErrorException, Json // Send Funds ///////////// - // Send funds from issuer to the badActor. - sendFunds( - TEN_THOUSAND, issuerKeyPair, badActorKeyPair, FeeUtils.computeNetworkFees(feeResult).recommendedFee() + logger.info("Send ${} from issuer ({}) to the badActor ({}) and expect {}", + TEN_THOUSAND, + issuerKeyPair.publicKey().deriveAddress(), + badActorKeyPair.publicKey().deriveAddress(), + TES_SUCCESS + ); + sendIssuedCurrency( + issuerKeyPair, // <-- From + badActorKeyPair, // <-- To + IssuedCurrencyAmount.builder() + .currency(FreezeIssuedCurrencyIT.ISSUED_CURRENCY_CODE) + .value(TEN_THOUSAND) + .issuer(issuerKeyPair.publicKey().deriveAddress()) + .build(), + FeeUtils.computeNetworkFees(feeResult).recommendedFee(), + TES_SUCCESS ); /////////////////////////// @@ -240,20 +338,36 @@ public void issueAndFreezeFundsGlobal() throws JsonRpcClientErrorException, Json .anyMatch(line -> line.balance().equals("-" + TEN_THOUSAND)) ); - // Send funds from badActor to the goodActor. - sendFunds( - FIVE_THOUSAND, badActorKeyPair, goodActorKeyPair, FeeUtils.computeNetworkFees(feeResult).recommendedFee() + logger.info("Send ${} from badActor ({}) to the goodActor ({}) and expect {}", + FIVE_THOUSAND, + badActorKeyPair.publicKey().deriveAddress(), + goodActorKeyPair.publicKey().deriveAddress(), + TES_SUCCESS + ); + sendIssuedCurrency( + badActorKeyPair, // <-- From + goodActorKeyPair, // <-- To + IssuedCurrencyAmount.builder() + .currency(FreezeIssuedCurrencyIT.ISSUED_CURRENCY_CODE) + .value(FIVE_THOUSAND) + .issuer(issuerKeyPair.publicKey().deriveAddress()) + .build(), + FeeUtils.computeNetworkFees(feeResult).recommendedFee(), + TES_SUCCESS ); /////////////////////////// // Validate that the TrustLine balance was updated as a result of the Payment. // The trust line returned is from the perspective of the issuer, so the balance should be negative. - this.scanForResult(() -> getValidatedAccountLines(issuerKeyPair.publicKey().deriveAddress(), - goodActorKeyPair.publicKey().deriveAddress()), + this.scanForResult(() -> getValidatedAccountLines( + issuerKeyPair.publicKey().deriveAddress(), goodActorKeyPair.publicKey().deriveAddress() + ), linesResult -> linesResult.lines().stream() .anyMatch(line -> line.balance().equals("-" + FIVE_THOUSAND)) ); + logger.info("Globally freeze trustline for issuer ({})", issuerKeyPair.publicKey().deriveAddress()); + // Global-Freeze the trustline for the issuer. AccountInfoResult issuerAccountInfo = this.adjustGlobalTrustlineFreeze( issuerKeyPair, @@ -269,170 +383,125 @@ public void issueAndFreezeFundsGlobal() throws JsonRpcClientErrorException, Json // 1) The counterparty can only send the frozen currencies directly to the issuer (no where else) // 2) The counterparty can still receive payments from others on the frozen trust line. - // Try to send funds from badActor to goodActor should not work because the badActor is frozen. - sendFunds( - "500", badActorKeyPair, goodActorKeyPair, FeeUtils.computeNetworkFees(feeResult).recommendedFee(), - "tecPATH_DRY" + logger.info("Send ${} from badActor ({}) to the goodActor ({}) and expect {}", + "500", + badActorKeyPair.publicKey().deriveAddress(), + goodActorKeyPair.publicKey().deriveAddress(), + TEC_PATH_DRY ); - // Sending from the goodActor to the badActor should not work - sendFunds( - "500", goodActorKeyPair, badActorKeyPair, FeeUtils.computeNetworkFees(feeResult).recommendedFee(), - "tecPATH_DRY" + sendIssuedCurrency( + badActorKeyPair, // <-- From + goodActorKeyPair, // <-- To + IssuedCurrencyAmount.builder() + .currency(FreezeIssuedCurrencyIT.ISSUED_CURRENCY_CODE) + .value("500") + .issuer(issuerKeyPair.publicKey().deriveAddress()) + .build(), + FeeUtils.computeNetworkFees(feeResult).recommendedFee(), + TEC_PATH_DRY ); - // Try to send funds from issuer to goodActor should work per - // https://xrpl.org/enact-global-freeze.html#intermission-while-frozen - sendFunds( - "100", issuerKeyPair, goodActorKeyPair, FeeUtils.computeNetworkFees(feeResult).recommendedFee() + logger.info("Send ${} from goodActor ({}) to the badActor ({}) and expect {}", + "500", + goodActorKeyPair.publicKey().deriveAddress(), + badActorKeyPair.publicKey().deriveAddress(), + TEC_PATH_DRY ); - - // Try to send funds from issuer to badActor should work per - // https://xrpl.org/enact-global-freeze.html#intermission-while-frozen - sendFunds( - "100", issuerKeyPair, badActorKeyPair, FeeUtils.computeNetworkFees(feeResult).recommendedFee() + sendIssuedCurrency( + goodActorKeyPair, // <-- From + badActorKeyPair, // <-- To + IssuedCurrencyAmount.builder() + .currency(FreezeIssuedCurrencyIT.ISSUED_CURRENCY_CODE) + .value("500") + .issuer(issuerKeyPair.publicKey().deriveAddress()) + .build(), + FeeUtils.computeNetworkFees(feeResult).recommendedFee(), + TEC_PATH_DRY ); - // Try to send funds from issuer to goodActor should work per - // https://xrpl.org/enact-global-freeze.html#intermission-while-frozen - sendFunds( - FIVE_THOUSAND, badActorKeyPair, issuerKeyPair, FeeUtils.computeNetworkFees(feeResult).recommendedFee() + // Note: The following should work per https://xrpl.org/enact-global-freeze.html#intermission-while-frozen). + logger.info("Send ${} from issuer ({}) to the goodActor ({}) and expect {}", + "100", + issuerKeyPair.publicKey().deriveAddress(), + goodActorKeyPair.publicKey().deriveAddress(), + TES_SUCCESS ); - - // Try to send funds from issuer to goodActor should work per - // https://xrpl.org/enact-global-freeze.html#intermission-while-frozen - sendFunds( - FIVE_THOUSAND, goodActorKeyPair, issuerKeyPair, FeeUtils.computeNetworkFees(feeResult).recommendedFee() + sendIssuedCurrency( + issuerKeyPair, // <-- From + goodActorKeyPair, // <-- To + IssuedCurrencyAmount.builder() + .currency(FreezeIssuedCurrencyIT.ISSUED_CURRENCY_CODE) + .value("100") + .issuer(issuerKeyPair.publicKey().deriveAddress()) + .build(), + FeeUtils.computeNetworkFees(feeResult).recommendedFee(), + TES_SUCCESS ); - // Unfreeze the bad actor. - issuerAccountInfo = this.adjustGlobalTrustlineFreeze( - issuerKeyPair, + // Note: The following should work per https://xrpl.org/enact-global-freeze.html#intermission-while-frozen). + logger.info("Send ${} from issuer ({}) to the badActor ({}) and expect {}", + "100", + issuerKeyPair.publicKey().deriveAddress(), + badActorKeyPair.publicKey().deriveAddress(), + TES_SUCCESS + ); + sendIssuedCurrency( + issuerKeyPair, // <-- From + badActorKeyPair, // <-- To + IssuedCurrencyAmount.builder() + .currency(FreezeIssuedCurrencyIT.ISSUED_CURRENCY_CODE) + .value("100") + .issuer(issuerKeyPair.publicKey().deriveAddress()) + .build(), FeeUtils.computeNetworkFees(feeResult).recommendedFee(), - UN_FREEZE + TES_SUCCESS ); - assertThat(issuerAccountInfo.accountData().flags().lsfGlobalFreeze()).isFalse(); - } - /** - * Send issued currency funds from an issuer to a badActor. - * - * @param value The amount of currency to send. - * @param sender The {@link KeyPair} of the sender. - * @param recipient The {@link KeyPair} of the recipient. - * @param fee The current network fee, as an {@link XrpCurrencyAmount}. - * - * @throws JsonRpcClientErrorException If anything goes wrong while communicating with rippled. - * @throws JsonProcessingException If there are any problems parsing JSON. - */ - private void sendFunds( - String value, - KeyPair sender, - KeyPair recipient, - XrpCurrencyAmount fee - ) throws JsonRpcClientErrorException, JsonProcessingException { - this.sendFunds(value, sender, recipient, fee, "tesSUCCESS"); - } - - /** - * Send issued currency funds from an issuer to a badActor. - * - * @param valueToSend The amount of currency to send. - * @param sender The {@link KeyPair} of the sender. - * @param recipient The {@link KeyPair} of the recipient. - * @param fee The current network fee, as an {@link XrpCurrencyAmount}. - * @param expectedResultCode The expected result code after submitting a payment transaction. - * - * @throws JsonRpcClientErrorException If anything goes wrong while communicating with rippled. - * @throws JsonProcessingException If there are any problems parsing JSON. - */ - private void sendFunds( - String valueToSend, - KeyPair sender, - KeyPair recipient, - XrpCurrencyAmount fee, - String expectedResultCode - ) throws JsonRpcClientErrorException, JsonProcessingException { - AccountInfoResult senderAccountInfo = this.scanForResult( - () -> getValidatedAccountInfo(sender.publicKey().deriveAddress()) + // Note: The following should work per https://xrpl.org/enact-global-freeze.html#intermission-while-frozen). + logger.info("Send ${} from issuer ({}) to the badActor ({}) and expect {}", + FIVE_THOUSAND, + badActorKeyPair.publicKey().deriveAddress(), + issuerKeyPair.publicKey().deriveAddress(), + TES_SUCCESS ); - - Payment payment = Payment.builder() - .account(sender.publicKey().deriveAddress()) - .fee(fee) - .sequence(senderAccountInfo.accountData().sequence()) - .destination(recipient.publicKey().deriveAddress()) - .amount(IssuedCurrencyAmount.builder() - .issuer(issuerKeyPair.publicKey().deriveAddress()) + sendIssuedCurrency( + badActorKeyPair, // <-- From + issuerKeyPair, // <-- To + IssuedCurrencyAmount.builder() .currency(FreezeIssuedCurrencyIT.ISSUED_CURRENCY_CODE) - .value(valueToSend) - .build()) - .signingPublicKey(sender.publicKey()) - .build(); - - SingleSignedTransaction signedPayment = signatureService.sign(sender.privateKey(), payment); - SubmitResult paymentResult = xrplClient.submit(signedPayment); - assertThat(paymentResult.engineResult()).isEqualTo(expectedResultCode); - - if (expectedResultCode.equals("tesSUCCESS")) { - logger.info( - "Payment transaction: https://testnet.xrpl.org/transactions/{}", paymentResult.transactionResult().hash() - ); - } - this.scanForResult(() -> getValidatedTransaction(paymentResult.transactionResult().hash(), Payment.class)); - } - - /** - * Create a trustline between the given issuer and badActor accounts for the given currency code and with the given - * limit. - * - * @param issuerKeyPair The {@link KeyPair} of the issuer account. - * @param counterpartKeyPair The {@link KeyPair} of the badActor account. - * @param fee The current network fee, as an {@link XrpCurrencyAmount}. - * - * @return The {@link TrustLine} that gets created. - * - * @throws JsonRpcClientErrorException If anything goes wrong while communicating with rippled. - * @throws JsonProcessingException If there are any problems parsing JSON. - */ - private TrustLine createTrustLine( - KeyPair issuerKeyPair, - KeyPair counterpartKeyPair, - XrpCurrencyAmount fee - ) throws JsonRpcClientErrorException, JsonProcessingException { - AccountInfoResult badActorAccountInfo = this.scanForResult( - () -> this.getValidatedAccountInfo(counterpartKeyPair.publicKey().deriveAddress()) + .value(FIVE_THOUSAND) + .issuer(issuerKeyPair.publicKey().deriveAddress()) + .build(), + FeeUtils.computeNetworkFees(feeResult).recommendedFee(), + TES_SUCCESS ); - TrustSet trustSet = TrustSet.builder() - .account(counterpartKeyPair.publicKey().deriveAddress()) - .fee(fee) - .sequence(badActorAccountInfo.accountData().sequence()) - .limitAmount(IssuedCurrencyAmount.builder() + // Note: The following should work per https://xrpl.org/enact-global-freeze.html#intermission-while-frozen). + logger.info("Send ${} from issuer ({}) to the goodActor ({}) and expect {}", + "FIVE_THOUSAND", + goodActorKeyPair.publicKey().deriveAddress(), + issuerKeyPair.publicKey().deriveAddress(), + TES_SUCCESS + ); + sendIssuedCurrency( + goodActorKeyPair, // <-- From + issuerKeyPair, // <-- To + IssuedCurrencyAmount.builder() .currency(FreezeIssuedCurrencyIT.ISSUED_CURRENCY_CODE) + .value(FIVE_THOUSAND) .issuer(issuerKeyPair.publicKey().deriveAddress()) - .value(FreezeIssuedCurrencyIT.TEN_THOUSAND) - .build()) - .flags(TrustSetFlags.builder() - .tfSetNoRipple() - .build()) - .signingPublicKey(counterpartKeyPair.publicKey()) - .build(); - - SingleSignedTransaction signedTrustSet = signatureService.sign(counterpartKeyPair.privateKey(), trustSet); - SubmitResult trustSetSubmitResult = xrplClient.submit(signedTrustSet); - assertThat(trustSetSubmitResult.engineResult()).isEqualTo("tesSUCCESS"); - logger.info( - "TrustSet transaction successful: https://testnet.xrpl.org/transactions/{}", - trustSetSubmitResult.transactionResult().hash() + .build(), + FeeUtils.computeNetworkFees(feeResult).recommendedFee(), + TES_SUCCESS ); - return scanForResult( - () -> - getValidatedAccountLines(issuerKeyPair.publicKey().deriveAddress(), - counterpartKeyPair.publicKey().deriveAddress()), - linesResult -> !linesResult.lines().isEmpty() - ) - .lines().get(0); + // Unfreeze the bad actor. + issuerAccountInfo = this.adjustGlobalTrustlineFreeze( + issuerKeyPair, + FeeUtils.computeNetworkFees(feeResult).recommendedFee(), + UN_FREEZE + ); + assertThat(issuerAccountInfo.accountData().flags().lsfGlobalFreeze()).isFalse(); } /** diff --git a/xrpl4j-integration-tests/src/test/java/org/xrpl/xrpl4j/tests/GatewayBalancesIT.java b/xrpl4j-integration-tests/src/test/java/org/xrpl/xrpl4j/tests/GatewayBalancesIT.java index 8fc278922..a685bdf10 100644 --- a/xrpl4j-integration-tests/src/test/java/org/xrpl/xrpl4j/tests/GatewayBalancesIT.java +++ b/xrpl4j-integration-tests/src/test/java/org/xrpl/xrpl4j/tests/GatewayBalancesIT.java @@ -36,6 +36,7 @@ import org.xrpl.xrpl4j.model.client.accounts.TrustLine; import org.xrpl.xrpl4j.model.client.common.LedgerSpecifier; import org.xrpl.xrpl4j.model.transactions.Address; +import org.xrpl.xrpl4j.model.transactions.IssuedCurrencyAmount; import org.xrpl.xrpl4j.model.transactions.XrpCurrencyAmount; import java.math.BigDecimal; @@ -55,20 +56,25 @@ public void testGatewayBalances() throws JsonRpcClientErrorException, JsonProces String xrpl4jCoin = Strings.padEnd(BaseEncoding.base16().encode("xrpl4jCoin".getBytes()), 40, '0'); TrustLine trustLine = createTrustLine( - xrpl4jCoin, - "10000", - issuerKeyPair, counterpartyKeyPair, + IssuedCurrencyAmount.builder() + .issuer(issuerKeyPair.publicKey().deriveAddress()) + .currency(xrpl4jCoin) + .value("10000") + .build(), XrpCurrencyAmount.ofXrp(BigDecimal.valueOf(1)) ); /////////////////////////// // Send some xrpl4jCoin to the counterparty account. sendIssuedCurrency( - xrpl4jCoin, - trustLine.limitPeer(), - issuerKeyPair, - counterpartyKeyPair, + issuerKeyPair, // <-- From + counterpartyKeyPair, // <-- To + IssuedCurrencyAmount.builder() + .issuer(issuerKeyPair.publicKey().deriveAddress()) + .currency(xrpl4jCoin) + .value(trustLine.limitPeer()) + .build(), XrpCurrencyAmount.ofXrp(BigDecimal.valueOf(1)) ); diff --git a/xrpl4j-integration-tests/src/test/java/org/xrpl/xrpl4j/tests/IssuedCurrencyIT.java b/xrpl4j-integration-tests/src/test/java/org/xrpl/xrpl4j/tests/IssuedCurrencyIT.java index 670135a9a..2334220cd 100644 --- a/xrpl4j-integration-tests/src/test/java/org/xrpl/xrpl4j/tests/IssuedCurrencyIT.java +++ b/xrpl4j-integration-tests/src/test/java/org/xrpl/xrpl4j/tests/IssuedCurrencyIT.java @@ -25,7 +25,6 @@ import com.fasterxml.jackson.core.JsonProcessingException; import com.google.common.base.Strings; import com.google.common.io.BaseEncoding; -import com.google.common.primitives.UnsignedInteger; import org.junit.jupiter.api.Test; import org.xrpl.xrpl4j.client.JsonRpcClientErrorException; import org.xrpl.xrpl4j.crypto.keys.KeyPair; @@ -43,7 +42,7 @@ import org.xrpl.xrpl4j.model.client.ledger.RippleStateLedgerEntryParams; import org.xrpl.xrpl4j.model.client.ledger.RippleStateLedgerEntryParams.RippleStateAccounts; import org.xrpl.xrpl4j.model.client.transactions.SubmitResult; -import org.xrpl.xrpl4j.model.ledger.EscrowObject; +import org.xrpl.xrpl4j.model.flags.TrustSetFlags; import org.xrpl.xrpl4j.model.ledger.LedgerObject; import org.xrpl.xrpl4j.model.ledger.RippleStateObject; import org.xrpl.xrpl4j.model.transactions.AccountSet; @@ -51,8 +50,6 @@ import org.xrpl.xrpl4j.model.transactions.IssuedCurrencyAmount; import org.xrpl.xrpl4j.model.transactions.PathStep; import org.xrpl.xrpl4j.model.transactions.Payment; -import org.xrpl.xrpl4j.model.transactions.TrustSet; -import org.xrpl.xrpl4j.model.transactions.XrpCurrencyAmount; import org.xrpl.xrpl4j.tests.environment.DevnetEnvironment; import org.xrpl.xrpl4j.tests.environment.TestnetEnvironment; @@ -80,10 +77,12 @@ void createTrustlineWithMaxLimit() throws JsonRpcClientErrorException, JsonProce String xrpl4jCoin = Strings.padEnd(BaseEncoding.base16().encode("xrpl4jCoin".getBytes()), 40, '0'); TrustLine trustLine = createTrustLine( - xrpl4jCoin, - IssuedCurrencyAmount.MAX_VALUE, - issuerKeyPair, counterpartyKeyPair, + IssuedCurrencyAmount.builder() + .issuer(issuerKeyPair.publicKey().deriveAddress()) + .currency(xrpl4jCoin) + .value(IssuedCurrencyAmount.MAX_VALUE) + .build(), FeeUtils.computeNetworkFees(feeResult).recommendedFee() ); @@ -111,10 +110,12 @@ void createTrustlineWithMaxLimitMinusOneExponent() throws JsonRpcClientErrorExce String xrpl4jCoin = Strings.padEnd(BaseEncoding.base16().encode("xrpl4jCoin".getBytes()), 40, '0'); TrustLine trustLine = createTrustLine( - xrpl4jCoin, - new BigDecimal(IssuedCurrencyAmount.MAX_VALUE).scaleByPowerOfTen(-1).toEngineeringString(), - issuerKeyPair, counterpartyKeyPair, + IssuedCurrencyAmount.builder() + .issuer(issuerKeyPair.publicKey().deriveAddress()) + .currency(xrpl4jCoin) + .value(new BigDecimal(IssuedCurrencyAmount.MAX_VALUE).scaleByPowerOfTen(-1).toEngineeringString()) + .build(), FeeUtils.computeNetworkFees(feeResult).recommendedFee() ); @@ -142,10 +143,12 @@ void createTrustlineWithSmallestPositiveLimit() throws JsonRpcClientErrorExcepti String xrpl4jCoin = Strings.padEnd(BaseEncoding.base16().encode("xrpl4jCoin".getBytes()), 40, '0'); TrustLine trustLine = createTrustLine( - xrpl4jCoin, - IssuedCurrencyAmount.MIN_POSITIVE_VALUE, - issuerKeyPair, counterpartyKeyPair, + IssuedCurrencyAmount.builder() + .issuer(issuerKeyPair.publicKey().deriveAddress()) + .currency(xrpl4jCoin) + .value(IssuedCurrencyAmount.MIN_POSITIVE_VALUE) + .build(), FeeUtils.computeNetworkFees(feeResult).recommendedFee() ); @@ -175,10 +178,12 @@ void createTrustlineWithSmalletPositiveLimitPlusOne() throws JsonRpcClientErrorE BigDecimal limitValue = new BigDecimal(IssuedCurrencyAmount.MIN_POSITIVE_VALUE) .add(new BigDecimal(IssuedCurrencyAmount.MIN_POSITIVE_VALUE).scaleByPowerOfTen(-1)); TrustLine trustLine = createTrustLine( - xrpl4jCoin, - limitValue.toString(), - issuerKeyPair, counterpartyKeyPair, + IssuedCurrencyAmount.builder() + .issuer(issuerKeyPair.publicKey().deriveAddress()) + .currency(xrpl4jCoin) + .value(limitValue.toString()) + .build(), FeeUtils.computeNetworkFees(feeResult).recommendedFee() ); @@ -206,10 +211,12 @@ public void issueIssuedCurrencyBalance() throws JsonRpcClientErrorException, Jso String xrpl4jCoin = Strings.padEnd(BaseEncoding.base16().encode("xrpl4jCoin".getBytes()), 40, '0'); TrustLine trustLine = createTrustLine( - xrpl4jCoin, - "10000", - issuerKeyPair, counterpartyKeyPair, + IssuedCurrencyAmount.builder() + .issuer(issuerKeyPair.publicKey().deriveAddress()) + .currency(xrpl4jCoin) + .value("10000") + .build(), FeeUtils.computeNetworkFees(feeResult).recommendedFee() ); @@ -222,7 +229,12 @@ public void issueIssuedCurrencyBalance() throws JsonRpcClientErrorException, Jso /////////////////////////// // Send some xrpl4jCoin to the counterparty account. sendIssuedCurrency( - xrpl4jCoin, trustLine.limitPeer(), issuerKeyPair, counterpartyKeyPair, + issuerKeyPair, counterpartyKeyPair, + IssuedCurrencyAmount.builder() + .issuer(issuerKeyPair.publicKey().deriveAddress()) + .currency(xrpl4jCoin) + .value(trustLine.limitPeer()) + .build(), FeeUtils.computeNetworkFees(feeResult).recommendedFee() ); @@ -230,13 +242,17 @@ public void issueIssuedCurrencyBalance() throws JsonRpcClientErrorException, Jso // Validate that the TrustLine balance was updated as a result of the Payment. // The trust line returned is from the perspective of the issuer, so the balance should be negative. TrustLine trustLineAfterPayment = this.scanForResult( - () -> getValidatedAccountLines(issuerKeyPair.publicKey().deriveAddress(), - counterpartyKeyPair.publicKey().deriveAddress()), - linesResult -> linesResult.lines().stream().anyMatch(line -> line.balance().equals("-" + trustLine.limitPeer())) + () -> getValidatedAccountLines( + issuerKeyPair.publicKey().deriveAddress(), + counterpartyKeyPair.publicKey().deriveAddress() + ), + linesResult -> linesResult.lines().stream().anyMatch(line -> line.balance().equals("-" + trustLine.limitPeer())) ).lines().stream() .filter(line -> line.balance().equals("-" + trustLine.limitPeer())) .findFirst() - .get(); + .orElseThrow(() -> new RuntimeException( + String.format("Expected a RippleStateObject for account %s, but none existed.", trustLine.account().value()) + )); assertThatEntryEqualsObjectFromAccountObjects( trustLineAfterPayment, @@ -255,8 +271,7 @@ public void issueIssuedCurrencyBalance() throws JsonRpcClientErrorException, Jso ); assertThat(counterpartyCurrencies.sendCurrencies()).asList().containsOnly(xrpl4jCoin); - assertThat(counterpartyCurrencies.ledgerCurrentIndex()).isNotEmpty().get() - .isEqualTo(counterpartyCurrencies.ledgerCurrentIndexSafe()); + assertThat(counterpartyCurrencies.ledgerCurrentIndex()).isNotEmpty(); } @Test @@ -276,40 +291,48 @@ public void sendSimpleRipplingIssuedCurrencyPayment() throws JsonRpcClientErrorE /////////////////////////// // Create a TrustLine between alice and the issuer createTrustLine( - "USD", - "10000", - issuerKeyPair, aliceKeyPair, + IssuedCurrencyAmount.builder() + .issuer(issuerKeyPair.publicKey().deriveAddress()) + .currency("USD") + .value("10000") + .build(), FeeUtils.computeNetworkFees(feeResult).recommendedFee() ); /////////////////////////// // Create a TrustLine between bob and the issuer TrustLine bobTrustLine = createTrustLine( - "USD", - "10000", - issuerKeyPair, bobKeyPair, + IssuedCurrencyAmount.builder() + .issuer(issuerKeyPair.publicKey().deriveAddress()) + .currency("USD") + .value("10000") + .build(), FeeUtils.computeNetworkFees(feeResult).recommendedFee() ); /////////////////////////// // Issuer issues 50 USD to alice sendIssuedCurrency( - "USD", - "50", - issuerKeyPair, - aliceKeyPair, + issuerKeyPair, aliceKeyPair, + IssuedCurrencyAmount.builder() + .issuer(issuerKeyPair.publicKey().deriveAddress()) + .currency("USD") + .value("50") + .build(), FeeUtils.computeNetworkFees(feeResult).recommendedFee() ); /////////////////////////// // Issuer issues 50 USD to bob sendIssuedCurrency( - "USD", - "50", - issuerKeyPair, - bobKeyPair, + issuerKeyPair, bobKeyPair, + IssuedCurrencyAmount.builder() + .issuer(issuerKeyPair.publicKey().deriveAddress()) + .currency("USD") + .value("50") + .build(), FeeUtils.computeNetworkFees(feeResult).recommendedFee() ); @@ -326,7 +349,7 @@ public void sendSimpleRipplingIssuedCurrencyPayment() throws JsonRpcClientErrorE // Note that because this path consists of all implied paths, rippled will not return any path steps scanForResult( () -> getValidatedRipplePath(aliceKeyPair, bobKeyPair, pathDestinationAmount), - path -> path.alternatives().size() > 0 // rippled will return a PathAlternative without any path steps + path -> !path.alternatives().isEmpty() // rippled will return a PathAlternative without any path steps ); /////////////////////////// @@ -382,7 +405,7 @@ public void sendSimpleRipplingIssuedCurrencyPayment() throws JsonRpcClientErrorE */ @Test public void sendMultiHopSameCurrencyPayment() throws JsonRpcClientErrorException, JsonProcessingException { - // NOTE: Only run this on non-testnet and non-devnet evironmens. + // NOTE: Only run this on non-testnet and non-devnet environments. if (TestnetEnvironment.class.isAssignableFrom(xrplEnvironment.getClass()) || DevnetEnvironment.class.isAssignableFrom(xrplEnvironment.getClass())) { return; @@ -393,8 +416,8 @@ public void sendMultiHopSameCurrencyPayment() throws JsonRpcClientErrorException final KeyPair issuerAKeyPair = createRandomAccountEd25519(); final KeyPair issuerBKeyPair = createRandomAccountEd25519(); final KeyPair charlieKeyPair = createRandomAccountEd25519(); - final KeyPair danielKeyPair = createRandomAccountEd25519(); final KeyPair emilyKeyPair = createRandomAccountEd25519(); + final KeyPair danielKeyPair = createRandomAccountEd25519(); /////////////////////////// // Set the lsfDefaultRipple AccountRoot flag so that all trustlines in this topography allow rippling @@ -402,57 +425,72 @@ public void sendMultiHopSameCurrencyPayment() throws JsonRpcClientErrorException setDefaultRipple(issuerAKeyPair, feeResult); setDefaultRipple(issuerBKeyPair, feeResult); setDefaultRipple(charlieKeyPair, feeResult); - setDefaultRipple(danielKeyPair, feeResult); setDefaultRipple(emilyKeyPair, feeResult); + setDefaultRipple(danielKeyPair, feeResult); /////////////////////////// // Create a Trustline between charlie and issuerA final TrustLine charlieTrustLineWithIssuerA = createTrustLine( - "USD", - "10000", - issuerAKeyPair, charlieKeyPair, - FeeUtils.computeNetworkFees(feeResult).recommendedFee() + IssuedCurrencyAmount.builder() + .issuer(issuerAKeyPair.publicKey().deriveAddress()) + .currency("USD") + .value("10000") + .build(), + FeeUtils.computeNetworkFees(feeResult).recommendedFee(), + TrustSetFlags.empty() ); /////////////////////////// // Create a Trustline between emily and issuerA createTrustLine( - "USD", - "10000", - issuerAKeyPair, emilyKeyPair, - FeeUtils.computeNetworkFees(feeResult).recommendedFee() + IssuedCurrencyAmount.builder() + .issuer(issuerAKeyPair.publicKey().deriveAddress()) + .currency("USD") + .value("10000") + .build(), + FeeUtils.computeNetworkFees(feeResult).recommendedFee(), + TrustSetFlags.empty() ); /////////////////////////// // Create a Trustline between emily and issuerB createTrustLine( - "USD", - "10000", - issuerBKeyPair, emilyKeyPair, - FeeUtils.computeNetworkFees(feeResult).recommendedFee() + IssuedCurrencyAmount.builder() + .issuer(issuerBKeyPair.publicKey().deriveAddress()) + .currency("USD") + .value("10000") + .build(), + FeeUtils.computeNetworkFees(feeResult).recommendedFee(), + TrustSetFlags.empty() ); /////////////////////////// // Create a Trustline between daniel and issuerB final TrustLine danielTrustLineWithIssuerB = createTrustLine( - "USD", - "10000", - issuerBKeyPair, danielKeyPair, - FeeUtils.computeNetworkFees(feeResult).recommendedFee() + IssuedCurrencyAmount.builder() + .issuer(issuerBKeyPair.publicKey().deriveAddress()) + .currency("USD") + .value("10000") + .build(), + FeeUtils.computeNetworkFees(feeResult).recommendedFee(), + TrustSetFlags.empty() ); /////////////////////////// // Issue 10 USD from issuerA to charlie. // IssuerA now owes Charlie 10 USD. sendIssuedCurrency( - "USD", - "10", - issuerAKeyPair, - charlieKeyPair, + issuerAKeyPair, // <-- From + charlieKeyPair, // <-- To + IssuedCurrencyAmount.builder() + .issuer(issuerAKeyPair.publicKey().deriveAddress()) + .currency("USD") + .value("10") + .build(), FeeUtils.computeNetworkFees(feeResult).recommendedFee() ); @@ -460,10 +498,13 @@ public void sendMultiHopSameCurrencyPayment() throws JsonRpcClientErrorException // Issue 1 USD from issuerA to emily. // IssuerA now owes Emily 1 USD sendIssuedCurrency( - "USD", - "1", - issuerAKeyPair, - emilyKeyPair, + issuerAKeyPair, // <-- From + emilyKeyPair, // <-- To + IssuedCurrencyAmount.builder() + .issuer(issuerAKeyPair.publicKey().deriveAddress()) + .currency("USD") + .value("1") + .build(), FeeUtils.computeNetworkFees(feeResult).recommendedFee() ); @@ -471,10 +512,13 @@ public void sendMultiHopSameCurrencyPayment() throws JsonRpcClientErrorException // Issue 100 USD from issuerB to emily. // IssuerB now owes Emily 100 USD sendIssuedCurrency( - "USD", - "100", - issuerBKeyPair, - emilyKeyPair, + issuerBKeyPair, // <-- From + emilyKeyPair, // <-- To + IssuedCurrencyAmount.builder() + .issuer(issuerBKeyPair.publicKey().deriveAddress()) + .currency("USD") + .value("100") + .build(), FeeUtils.computeNetworkFees(feeResult).recommendedFee() ); @@ -482,22 +526,26 @@ public void sendMultiHopSameCurrencyPayment() throws JsonRpcClientErrorException // Issue 2 USD from issuerB to daniel. // IssuerB now owes Daniel 2 USD sendIssuedCurrency( - "USD", - "2", - issuerBKeyPair, - danielKeyPair, + issuerBKeyPair, // <-- From + danielKeyPair, // <-- To + IssuedCurrencyAmount.builder() + .issuer(issuerBKeyPair.publicKey().deriveAddress()) + .currency("USD") + .value("2") + .build(), FeeUtils.computeNetworkFees(feeResult).recommendedFee() ); /////////////////////////// // Look for a payment path from charlie to daniel. List> pathSteps = scanForResult( - () -> getValidatedRipplePath(charlieKeyPair, danielKeyPair, IssuedCurrencyAmount.builder() - .issuer(issuerBKeyPair.publicKey().deriveAddress()) - .currency(charlieTrustLineWithIssuerA.currency()) - .value("10") - .build()), - path -> path.alternatives().size() > 0 + () -> getValidatedRipplePath(charlieKeyPair, danielKeyPair, + IssuedCurrencyAmount.builder() + .issuer(issuerBKeyPair.publicKey().deriveAddress()) + .currency(charlieTrustLineWithIssuerA.currency()) + .value("10") + .build()), + path -> !path.alternatives().isEmpty() ) .alternatives().stream() .filter(alt -> @@ -577,6 +625,7 @@ public void sendMultiHopSameCurrencyPayment() throws JsonRpcClientErrorException * * @param issuerKeyPair The {@link KeyPair} containing the address of the issuer account. * @param feeResult The current {@link FeeResult}. + * * @throws JsonRpcClientErrorException If anything goes wrong while communicating with rippled. */ public void setDefaultRipple(KeyPair issuerKeyPair, FeeResult feeResult) @@ -618,10 +667,11 @@ private void assertThatEntryEqualsObjectFromAccountObjects( RippleStateObject rippleStateObject = (RippleStateObject) xrplClient.accountObjects( AccountObjectsRequestParams.of(trustLine.account()) ).accountObjects().stream() - .filter(object -> RippleStateObject.class.isAssignableFrom(object.getClass()) /*&&*/ - /*((RippleStateObject) object).previousTransactionLedgerSequence().equals(lastSequence)*/) + .filter(object -> RippleStateObject.class.isAssignableFrom(object.getClass())) .findFirst() - .get(); + .orElseThrow(() -> new RuntimeException( + String.format("Expected a RippleStateObject for account %s, but none existed.", trustLine.account().value()) + )); LedgerEntryResult entry = xrplClient.ledgerEntry( LedgerEntryRequestParams.rippleState( diff --git a/xrpl4j-integration-tests/src/test/java/org/xrpl/xrpl4j/tests/NfTokenIT.java b/xrpl4j-integration-tests/src/test/java/org/xrpl/xrpl4j/tests/NfTokenIT.java index 5204f08f8..5a3c91f4e 100644 --- a/xrpl4j-integration-tests/src/test/java/org/xrpl/xrpl4j/tests/NfTokenIT.java +++ b/xrpl4j-integration-tests/src/test/java/org/xrpl/xrpl4j/tests/NfTokenIT.java @@ -491,8 +491,27 @@ void mintAndCreateThenAcceptOffer() throws JsonRpcClientErrorException, JsonProc ); logger.info("NFT Accept Offer transaction was validated successfully."); - assertThat(xrplClient.accountNfts(keypair.publicKey().deriveAddress()).accountNfts().size()).isEqualTo(0); - assertThat(xrplClient.accountNfts(wallet2.publicKey().deriveAddress()).accountNfts().size()).isEqualTo(1); + this.scanForResult( + () -> { + try { + return xrplClient.accountNfts(keypair.publicKey().deriveAddress()).accountNfts().size(); + } catch (JsonRpcClientErrorException e) { + throw new RuntimeException(e); + } + }, + size -> size == 0 + ); + + this.scanForResult( + () -> { + try { + return xrplClient.accountNfts(wallet2.publicKey().deriveAddress()).accountNfts().size(); + } catch (JsonRpcClientErrorException e) { + throw new RuntimeException(e); + } + }, + size -> size == 1 + ); logger.info("The NFT ownership was transferred."); } diff --git a/xrpl4j-integration-tests/src/test/java/org/xrpl/xrpl4j/tests/NftInfoIT.java b/xrpl4j-integration-tests/src/test/java/org/xrpl/xrpl4j/tests/NftInfoIT.java index 259a7eae0..8ffeefb61 100644 --- a/xrpl4j-integration-tests/src/test/java/org/xrpl/xrpl4j/tests/NftInfoIT.java +++ b/xrpl4j-integration-tests/src/test/java/org/xrpl/xrpl4j/tests/NftInfoIT.java @@ -50,7 +50,7 @@ void getNftInfo() throws JsonRpcClientErrorException { } @Test - void getNftInfoFromReportingModeThrows() throws JsonRpcClientErrorException { + void getNftInfoFromReportingModeThrows() { XrplClient client = new ReportingMainnetEnvironment().getXrplClient(); NftInfoRequestParams params = NftInfoRequestParams.builder() .nfTokenId(NfTokenId.of("0008138808C4E53F4F6EF5D5B2AF64F96B457F42E0ED9530FE9B131300001178")) @@ -59,7 +59,7 @@ void getNftInfoFromReportingModeThrows() throws JsonRpcClientErrorException { assertThatThrownBy( () -> client.nftInfo(params) ).isInstanceOf(JsonRpcClientErrorException.class) - .hasMessage("Unknown method."); + .hasMessage("unknownCmd (Unknown method.)"); } } diff --git a/xrpl4j-integration-tests/src/test/java/org/xrpl/xrpl4j/tests/PriceOracleIT.java b/xrpl4j-integration-tests/src/test/java/org/xrpl/xrpl4j/tests/PriceOracleIT.java new file mode 100644 index 000000000..5987f8470 --- /dev/null +++ b/xrpl4j-integration-tests/src/test/java/org/xrpl/xrpl4j/tests/PriceOracleIT.java @@ -0,0 +1,336 @@ +package org.xrpl.xrpl4j.tests; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.assertThatThrownBy; + +import com.fasterxml.jackson.core.JsonProcessingException; +import com.google.common.base.Strings; +import com.google.common.collect.Lists; +import com.google.common.io.BaseEncoding; +import com.google.common.primitives.UnsignedInteger; +import com.google.common.primitives.UnsignedLong; +import org.jetbrains.annotations.NotNull; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.condition.DisabledIf; +import org.xrpl.xrpl4j.client.JsonRpcClientErrorException; +import org.xrpl.xrpl4j.crypto.keys.KeyPair; +import org.xrpl.xrpl4j.crypto.signing.SingleSignedTransaction; +import org.xrpl.xrpl4j.model.client.Finality; +import org.xrpl.xrpl4j.model.client.FinalityStatus; +import org.xrpl.xrpl4j.model.client.accounts.AccountInfoResult; +import org.xrpl.xrpl4j.model.client.common.LedgerSpecifier; +import org.xrpl.xrpl4j.model.client.fees.FeeResult; +import org.xrpl.xrpl4j.model.client.fees.FeeUtils; +import org.xrpl.xrpl4j.model.client.ledger.LedgerEntryRequestParams; +import org.xrpl.xrpl4j.model.client.ledger.LedgerEntryResult; +import org.xrpl.xrpl4j.model.client.ledger.OracleLedgerEntryParams; +import org.xrpl.xrpl4j.model.client.oracle.GetAggregatePriceRequestParams; +import org.xrpl.xrpl4j.model.client.oracle.GetAggregatePriceResult; +import org.xrpl.xrpl4j.model.client.transactions.SubmitResult; +import org.xrpl.xrpl4j.model.ledger.OracleObject; +import org.xrpl.xrpl4j.model.transactions.AssetPrice; +import org.xrpl.xrpl4j.model.transactions.OracleDelete; +import org.xrpl.xrpl4j.model.transactions.OracleDocumentId; +import org.xrpl.xrpl4j.model.transactions.OracleProvider; +import org.xrpl.xrpl4j.model.transactions.OracleSet; +import org.xrpl.xrpl4j.model.transactions.OracleUri; +import org.xrpl.xrpl4j.model.transactions.PriceData; +import org.xrpl.xrpl4j.model.transactions.PriceDataWrapper; + +import java.math.BigDecimal; +import java.time.Instant; +import java.util.Optional; + +@DisabledIf(value = "shouldNotRun", disabledReason = "PriceOracleIT only runs on local rippled node or devnet.") +public class PriceOracleIT extends AbstractIT { + + static boolean shouldNotRun() { + return System.getProperty("useTestnet") != null || + System.getProperty("useClioTestnet") != null; + } + + String xrpl4jCoin = Strings.padEnd(BaseEncoding.base16().encode("xrpl4jCoin".getBytes()), 40, '0'); + + @Test + void createAndUpdateAndDeleteOracle() throws JsonRpcClientErrorException, JsonProcessingException { + KeyPair sourceKeyPair = createRandomAccountEd25519(); + + AccountInfoResult accountInfo = this.scanForResult( + () -> this.getValidatedAccountInfo(sourceKeyPair.publicKey().deriveAddress()) + ); + + FeeResult feeResult = xrplClient.fee(); + OracleProvider provider = OracleProvider.of(BaseEncoding.base16().encode("DIA".getBytes())); + OracleUri uri = OracleUri.of(BaseEncoding.base16().encode("http://example.com".getBytes())); + UnsignedInteger lastUpdateTime = unixTimestamp(); + String assetClass = BaseEncoding.base16().encode("currency".getBytes()); + PriceDataWrapper priceData1 = PriceDataWrapper.of( + PriceData.builder() + .baseAsset("XRP") + .quoteAsset(xrpl4jCoin) + .assetPrice(AssetPrice.of(UnsignedLong.ONE)) + .scale(UnsignedInteger.valueOf(10)) + .build() + ); + PriceDataWrapper priceData2 = PriceDataWrapper.of( + PriceData.builder() + .baseAsset("XRP") + .quoteAsset("EUR") + .assetPrice(AssetPrice.of(UnsignedLong.ONE)) + .scale(UnsignedInteger.valueOf(10)) + .build() + ); + OracleSet oracleSet = OracleSet.builder() + .account(sourceKeyPair.publicKey().deriveAddress()) + .fee(FeeUtils.computeNetworkFees(feeResult).recommendedFee()) + .sequence(accountInfo.accountData().sequence()) + .signingPublicKey(sourceKeyPair.publicKey()) + .lastLedgerSequence(accountInfo.ledgerIndexSafe().plus(UnsignedInteger.valueOf(4)).unsignedIntegerValue()) + .oracleDocumentId(OracleDocumentId.of(UnsignedInteger.ONE)) + .provider(provider) + .uri(uri) + .lastUpdateTime(lastUpdateTime) + .assetClass(assetClass) + .addPriceDataSeries(priceData1, priceData2) + .build(); + + SingleSignedTransaction signedOracleSet = signatureService.sign(sourceKeyPair.privateKey(), oracleSet); + SubmitResult oracleSetSubmitResult = xrplClient.submit(signedOracleSet); + assertThat(oracleSetSubmitResult.engineResult()).isEqualTo("tesSUCCESS"); + + Finality finality = scanForFinality( + signedOracleSet.hash(), + accountInfo.ledgerIndexSafe(), + oracleSet.lastLedgerSequence().get(), + oracleSet.sequence(), + sourceKeyPair.publicKey().deriveAddress() + ); + assertThat(finality.finalityStatus()).isEqualTo(FinalityStatus.VALIDATED_SUCCESS); + + LedgerEntryResult ledgerEntry = xrplClient.ledgerEntry( + LedgerEntryRequestParams.oracle( + OracleLedgerEntryParams.builder() + .oracleDocumentId(oracleSet.oracleDocumentId()) + .account(sourceKeyPair.publicKey().deriveAddress()) + .build(), + LedgerSpecifier.VALIDATED + ) + ); + OracleObject oracleObject = ledgerEntry.node(); + assertThat(oracleObject.owner()).isEqualTo(sourceKeyPair.publicKey().deriveAddress()); + assertThat(oracleObject.provider()).isEqualTo(provider); + assertThat(oracleObject.assetClass()).isEqualTo(assetClass); + assertThat(oracleObject.lastUpdateTime()).isEqualTo(lastUpdateTime); + assertThat(oracleObject.uri()).isNotEmpty().get().isEqualTo(uri); + assertThat(oracleObject.priceDataSeries()).containsExactlyInAnyOrder(priceData1, priceData2); + + UnsignedInteger lastUpdateTime2 = unixTimestamp(); + PriceDataWrapper newPriceData = PriceDataWrapper.of( + PriceData.builder() + .baseAsset("XRP") + .quoteAsset("USD") + .assetPrice(AssetPrice.of(UnsignedLong.ONE)) + .scale(UnsignedInteger.valueOf(10)) + .build() + ); + PriceDataWrapper updatedPriceData = PriceDataWrapper.of( + PriceData.builder().from(priceData2.priceData()) + .assetPrice(AssetPrice.of(UnsignedLong.valueOf(1000))) + .build() + ); + OracleSet oracleUpdate = OracleSet.builder().from(oracleSet) + .lastLedgerSequence(ledgerEntry.ledgerIndexSafe().plus(UnsignedInteger.valueOf(4)).unsignedIntegerValue()) + .lastUpdateTime(lastUpdateTime2) + .sequence(oracleSet.sequence().plus(UnsignedInteger.ONE)) + .priceDataSeries(Lists.newArrayList( + // New asset pair should get added + newPriceData, + // Same asset pair without assetPrice should delete + PriceDataWrapper.of( + PriceData.builder().from(priceData1.priceData()) + .scale(Optional.empty()) + .assetPrice(Optional.empty()) + .build() + ), + // Updating assetPrice should update an existing price data entry. + updatedPriceData + )) + .build(); + + SingleSignedTransaction signedOracleUpdate = signatureService.sign( + sourceKeyPair.privateKey(), oracleUpdate + ); + SubmitResult oracleUpdateSubmitResult = xrplClient.submit(signedOracleUpdate); + assertThat(oracleUpdateSubmitResult.engineResult()).isEqualTo("tesSUCCESS"); + + Finality updateFinality = scanForFinality( + signedOracleUpdate.hash(), + accountInfo.ledgerIndexSafe(), + oracleUpdate.lastLedgerSequence().get(), + oracleUpdate.sequence(), + sourceKeyPair.publicKey().deriveAddress() + ); + assertThat(updateFinality.finalityStatus()).isEqualTo(FinalityStatus.VALIDATED_SUCCESS); + + ledgerEntry = xrplClient.ledgerEntry( + LedgerEntryRequestParams.oracle( + OracleLedgerEntryParams.builder() + .oracleDocumentId(oracleSet.oracleDocumentId()) + .account(sourceKeyPair.publicKey().deriveAddress()) + .build(), + LedgerSpecifier.VALIDATED + ) + ); + oracleObject = ledgerEntry.node(); + assertThat(oracleObject.owner()).isEqualTo(sourceKeyPair.publicKey().deriveAddress()); + assertThat(oracleObject.provider()).isEqualTo(provider); + assertThat(oracleObject.assetClass()).isEqualTo(assetClass); + assertThat(oracleObject.lastUpdateTime()).isEqualTo(lastUpdateTime2); + assertThat(oracleObject.uri()).isNotEmpty().get().isEqualTo(uri); + assertThat(oracleObject.priceDataSeries()).containsExactlyInAnyOrder(newPriceData, updatedPriceData); + + OracleDelete oracleDelete = OracleDelete.builder() + .account(sourceKeyPair.publicKey().deriveAddress()) + .fee(oracleSet.fee()) + .sequence(oracleUpdate.sequence().plus(UnsignedInteger.ONE)) + .lastLedgerSequence(ledgerEntry.ledgerIndexSafe().plus(UnsignedInteger.valueOf(4)).unsignedIntegerValue()) + .signingPublicKey(sourceKeyPair.publicKey()) + .oracleDocumentId(oracleSet.oracleDocumentId()) + .build(); + SingleSignedTransaction signedOracleDelete = signatureService.sign( + sourceKeyPair.privateKey(), oracleDelete + ); + SubmitResult oracleDeleteSubmitResult = xrplClient.submit(signedOracleDelete); + assertThat(oracleDeleteSubmitResult.engineResult()).isEqualTo("tesSUCCESS"); + + Finality deleteFinality = scanForFinality( + signedOracleDelete.hash(), + accountInfo.ledgerIndexSafe(), + oracleDelete.lastLedgerSequence().get(), + oracleDelete.sequence(), + sourceKeyPair.publicKey().deriveAddress() + ); + assertThat(deleteFinality.finalityStatus()).isEqualTo(FinalityStatus.VALIDATED_SUCCESS); + + assertThatThrownBy(() -> xrplClient.ledgerEntry( + LedgerEntryRequestParams.oracle( + OracleLedgerEntryParams.builder() + .oracleDocumentId(oracleSet.oracleDocumentId()) + .account(sourceKeyPair.publicKey().deriveAddress()) + .build(), + LedgerSpecifier.VALIDATED + ) + )).isInstanceOf(JsonRpcClientErrorException.class) + .hasMessage("entryNotFound (n/a)"); + } + + @Test + void createTwoOraclesAndGetAggregatePrice() throws JsonRpcClientErrorException, JsonProcessingException { + KeyPair sourceKeyPair = createRandomAccountEd25519(); + + AccountInfoResult accountInfo = this.scanForResult( + () -> this.getValidatedAccountInfo(sourceKeyPair.publicKey().deriveAddress()) + ); + + FeeResult feeResult = xrplClient.fee(); + OracleProvider provider = OracleProvider.of(BaseEncoding.base16().encode("DIA".getBytes())); + OracleUri uri = OracleUri.of(BaseEncoding.base16().encode("http://example.com".getBytes())); + UnsignedInteger lastUpdateTime = unixTimestamp(); + String assetClass = BaseEncoding.base16().encode("currency".getBytes()); + PriceDataWrapper priceData1 = PriceDataWrapper.of( + PriceData.builder() + .baseAsset("XRP") + .quoteAsset("EUR") + .assetPrice(AssetPrice.of(UnsignedLong.ONE)) + .build() + ); + OracleSet oracleSet = OracleSet.builder() + .account(sourceKeyPair.publicKey().deriveAddress()) + .fee(FeeUtils.computeNetworkFees(feeResult).recommendedFee()) + .sequence(accountInfo.accountData().sequence()) + .signingPublicKey(sourceKeyPair.publicKey()) + .lastLedgerSequence(accountInfo.ledgerIndexSafe().plus(UnsignedInteger.valueOf(4)).unsignedIntegerValue()) + .oracleDocumentId(OracleDocumentId.of(UnsignedInteger.ONE)) + .provider(provider) + .uri(uri) + .lastUpdateTime(lastUpdateTime) + .assetClass(assetClass) + .addPriceDataSeries(priceData1) + .build(); + + SingleSignedTransaction signedOracleSet = signatureService.sign(sourceKeyPair.privateKey(), oracleSet); + SubmitResult oracleSetSubmitResult = xrplClient.submit(signedOracleSet); + assertThat(oracleSetSubmitResult.engineResult()).isEqualTo("tesSUCCESS"); + + Finality finality = scanForFinality( + signedOracleSet.hash(), + accountInfo.ledgerIndexSafe(), + oracleSet.lastLedgerSequence().get(), + oracleSet.sequence(), + sourceKeyPair.publicKey().deriveAddress() + ); + assertThat(finality.finalityStatus()).isEqualTo(FinalityStatus.VALIDATED_SUCCESS); + + PriceDataWrapper priceData2 = PriceDataWrapper.of( + PriceData.builder() + .baseAsset("XRP") + .quoteAsset("EUR") + .assetPrice(AssetPrice.of(UnsignedLong.valueOf(2))) + .build() + ); + OracleSet oracleSet2 = OracleSet.builder() + .account(sourceKeyPair.publicKey().deriveAddress()) + .fee(FeeUtils.computeNetworkFees(feeResult).recommendedFee()) + .sequence(accountInfo.accountData().sequence().plus(UnsignedInteger.ONE)) + .signingPublicKey(sourceKeyPair.publicKey()) + .lastLedgerSequence(accountInfo.ledgerIndexSafe().plus(UnsignedInteger.valueOf(8)).unsignedIntegerValue()) + .oracleDocumentId(OracleDocumentId.of(UnsignedInteger.valueOf(2))) + .provider(provider) + .uri(uri) + .lastUpdateTime(lastUpdateTime) + .assetClass(assetClass) + .addPriceDataSeries(priceData2) + .build(); + + SingleSignedTransaction signedOracleSet2 = signatureService.sign(sourceKeyPair.privateKey(), oracleSet2); + SubmitResult oracleSetSubmitResult2 = xrplClient.submit(signedOracleSet2); + assertThat(oracleSetSubmitResult2.engineResult()).isEqualTo("tesSUCCESS"); + + Finality finality2 = scanForFinality( + signedOracleSet2.hash(), + accountInfo.ledgerIndexSafe(), + oracleSet2.lastLedgerSequence().get(), + oracleSet2.sequence(), + sourceKeyPair.publicKey().deriveAddress() + ); + assertThat(finality2.finalityStatus()).isEqualTo(FinalityStatus.VALIDATED_SUCCESS); + + GetAggregatePriceResult aggregatePrice = xrplClient.getAggregatePrice( + GetAggregatePriceRequestParams.builder() + .ledgerSpecifier(LedgerSpecifier.VALIDATED) + .baseAsset("XRP") + .quoteAsset("EUR") + .trim(UnsignedInteger.ONE) + .addOracles( + OracleLedgerEntryParams.builder() + .account(sourceKeyPair.publicKey().deriveAddress()) + .oracleDocumentId(oracleSet.oracleDocumentId()) + .build(), + OracleLedgerEntryParams.builder() + .account(sourceKeyPair.publicKey().deriveAddress()) + .oracleDocumentId(oracleSet2.oracleDocumentId()) + .build() + ) + .build() + ); + assertThat(aggregatePrice.median()).isEqualTo(BigDecimal.valueOf(1.5)); + assertThat(aggregatePrice.status()).isNotEmpty().get().isEqualTo("success"); + assertThat(aggregatePrice.entireSet().mean()).isEqualTo(BigDecimal.valueOf(1.5)); + assertThat(aggregatePrice.entireSet().size()).isEqualTo(UnsignedLong.valueOf(2)); + assertThat(aggregatePrice.trimmedSet()).isNotEmpty().get().isEqualTo(aggregatePrice.entireSet()); + } + + private static UnsignedInteger unixTimestamp() { + return UnsignedInteger.valueOf(System.currentTimeMillis() / 1000L); + } +} diff --git a/xrpl4j-integration-tests/src/test/java/org/xrpl/xrpl4j/tests/ServerInfoIT.java b/xrpl4j-integration-tests/src/test/java/org/xrpl/xrpl4j/tests/ServerInfoIT.java index 5c4a9d0ad..fe068612a 100644 --- a/xrpl4j-integration-tests/src/test/java/org/xrpl/xrpl4j/tests/ServerInfoIT.java +++ b/xrpl4j-integration-tests/src/test/java/org/xrpl/xrpl4j/tests/ServerInfoIT.java @@ -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. @@ -20,53 +20,83 @@ * =========================LICENSE_END================================== */ +import static org.assertj.core.api.Fail.fail; + +import com.google.common.primitives.UnsignedInteger; import okhttp3.HttpUrl; -import org.junit.jupiter.api.Disabled; import org.junit.jupiter.api.Test; -import org.junit.jupiter.api.Timeout; -import org.slf4j.Logger; -import org.slf4j.LoggerFactory; import org.xrpl.xrpl4j.client.JsonRpcClientErrorException; import org.xrpl.xrpl4j.client.XrplClient; import org.xrpl.xrpl4j.model.client.serverinfo.ServerInfo; - -import java.util.concurrent.TimeUnit; +import org.xrpl.xrpl4j.model.transactions.NetworkId; /** * Integration test for different types of ServerInfo values. */ public class ServerInfoIT { - Logger logger = LoggerFactory.getLogger(this.getClass()); - /** - * This test was written to run a long-running test on different Server Info models. Server Info is an implementation - * detail and different servers could respond differently. This test essentially verifies the correctness of these - * models. In the future, if the response structure changes, this test will alarm us to take action but has to be run - * manually everytime since it is disabled for CI. + * This test validates actual Server Info models. {@link ServerInfo} is an implementation detail and different servers + * could respond differently, so this test essentially verifies the correctness of these models. * * @throws JsonRpcClientErrorException If {@code jsonRpcClient} throws an error. * @throws InterruptedException If {@link Thread} is interrupted. + * @see "https://xrpl.org/docs/tutorials/public-servers/#mainnet" */ - @Timeout(value = 3, unit = TimeUnit.HOURS) @Test - @Disabled public void testServerInfoAcrossAllTypes() throws JsonRpcClientErrorException, InterruptedException { - XrplClient rippledClient = getXrplClient(HttpUrl.parse("https://s1.cbdc-sandbox.rippletest.net:51234")); - XrplClient reportingClient = getXrplClient(HttpUrl.parse("https://s2-reporting.ripple.com:51234")); - XrplClient clioClient = getXrplClient(HttpUrl.parse("https://s2-clio.ripple.com:51234")); - - ServerInfo info; - - while (true) { - info = rippledClient.serverInformation().info(); - logger.info("Rippled info was mapped correctly. " + getType(info)); - info = reportingClient.serverInformation().info(); - logger.info("Reporting mode info was mapped correctly. " + getType(info)); - info = clioClient.serverInformation().info(); - logger.info("Clio info was mapped correctly. " + getType(info)); - Thread.sleep(2000); - } + + // XRPL Cluster + getXrplClient(HttpUrl.parse("https://xrplcluster.com/")).serverInformation().info().handle( + this::assertValidNetworkId, + clioServerInfo -> fail("Shouldn't be a Clio server"), + reportingModeServerInfo -> fail("Shouldn't be a Reporting server") + ); + + // XRPL Cluster (testnet) + // Not executed due to rate limiting. + + // Ripple Mainnet (s1) + getXrplClient(HttpUrl.parse("https://s1.ripple.com:51234")).serverInformation().info().handle( + rippledServerInfo -> fail("Shouldn't be a rippled server"), + this::assertValidNetworkId, + reportingModeServerInfo -> fail("Shouldn't be a Reporting server") + ); + + // Ripple Mainnet (s2) + getXrplClient(HttpUrl.parse("https://s2.ripple.com:51234")).serverInformation().info().handle( + rippledServerInfo -> fail("Shouldn't be a rippled server"), + this::assertValidNetworkId, + reportingModeServerInfo -> fail("Shouldn't be a Reporting server") + ); + + // Ripple Testnet + getXrplClient(HttpUrl.parse("https://s.altnet.rippletest.net:51234/")).serverInformation().info().handle( + this::assertValidNetworkId, + clioServerInfo -> fail("Shouldn't be a Clio server"), + reportingModeServerInfo -> fail("Shouldn't be a Reporting server") + ); + + // Ripple Testnet (Clio) + getXrplClient(HttpUrl.parse("https://clio.altnet.rippletest.net:51234/")).serverInformation().info().handle( + rippledServerInfo -> fail("Shouldn't be a rippled server"), + this::assertValidNetworkId, + reportingModeServerInfo -> fail("Shouldn't be a Reporting server") + ); + + // Ripple Devnet + getXrplClient(HttpUrl.parse("https://s.devnet.rippletest.net:51234/")).serverInformation().info().handle( + this::assertValidNetworkId, + clioServerInfo -> fail("Shouldn't be a Clio server"), + reportingModeServerInfo -> fail("Shouldn't be a Reporting server") + ); + + // Ripple Devnet (Clio) + getXrplClient(HttpUrl.parse("https://clio.devnet.rippletest.net:51234/")).serverInformation().info().handle( + rippledServerInfo -> fail("Shouldn't be a rippled server"), + this::assertValidNetworkId, + reportingModeServerInfo -> fail("Shouldn't be a Reporting server") + ); } private String getType(ServerInfo info) { @@ -80,4 +110,11 @@ private String getType(ServerInfo info) { private XrplClient getXrplClient(HttpUrl serverUrl) { return new XrplClient(serverUrl); } + + private void assertValidNetworkId(ServerInfo serverInfo) { + serverInfo.networkId() + .map(NetworkId::value) + .map($ -> $.equals(UnsignedInteger.ZERO)) + .orElseThrow(() -> new RuntimeException("networkId should have existed")); + } } diff --git a/xrpl4j-integration-tests/src/test/java/org/xrpl/xrpl4j/tests/SubmitPaymentIT.java b/xrpl4j-integration-tests/src/test/java/org/xrpl/xrpl4j/tests/SubmitPaymentIT.java index 5ee2a97bf..893e54dae 100644 --- a/xrpl4j-integration-tests/src/test/java/org/xrpl/xrpl4j/tests/SubmitPaymentIT.java +++ b/xrpl4j-integration-tests/src/test/java/org/xrpl/xrpl4j/tests/SubmitPaymentIT.java @@ -114,10 +114,19 @@ public void sendPaymentFromSecp256k1KeyPair() throws JsonRpcClientErrorException private void assertPaymentCloseTimeMatchesLedgerCloseTime(TransactionResult validatedPayment) throws JsonRpcClientErrorException { - LedgerResult ledger = xrplClient.ledger( - LedgerRequestParams.builder() - .ledgerSpecifier(LedgerSpecifier.of(validatedPayment.ledgerIndex().get())) - .build() + + LedgerResult ledger = this.scanForResult( + () -> { + try { + return xrplClient.ledger( + LedgerRequestParams.builder() + .ledgerSpecifier(LedgerSpecifier.of(validatedPayment.ledgerIndex().get())) + .build() + ); + } catch (JsonRpcClientErrorException e) { + throw new RuntimeException(e); + } + } ); assertThat(validatedPayment.closeDateHuman()).isNotEmpty(); diff --git a/xrpl4j-integration-tests/src/test/java/org/xrpl/xrpl4j/tests/TicketIT.java b/xrpl4j-integration-tests/src/test/java/org/xrpl/xrpl4j/tests/TicketIT.java index d4531f336..f156c841c 100644 --- a/xrpl4j-integration-tests/src/test/java/org/xrpl/xrpl4j/tests/TicketIT.java +++ b/xrpl4j-integration-tests/src/test/java/org/xrpl/xrpl4j/tests/TicketIT.java @@ -45,6 +45,7 @@ import org.xrpl.xrpl4j.model.transactions.metadata.MetaLedgerEntryType; import java.util.List; +import java.util.Optional; public class TicketIT extends AbstractIT { @@ -91,8 +92,11 @@ void createTicketAndUseSequenceNumber() throws JsonRpcClientErrorException, Json .map(AffectedNode::ledgerIndex) .get(); - AccountInfoResult accountInfoAfterTicketCreate = getValidatedAccountInfo(sourceKeyPair.publicKey().deriveAddress()); - assertThat(accountInfoAfterTicketCreate.accountData().ticketCount()).isNotEmpty().get() + Optional ticketCount = this.scanForResult( + () -> getValidatedAccountInfo(sourceKeyPair.publicKey().deriveAddress()), + result -> result.accountData().ticketCount().isPresent() + ).accountData().ticketCount(); + assertThat(ticketCount).isNotEmpty().get() .isEqualTo(ticketCreate.ticketCount()); List tickets = getValidatedAccountObjects( diff --git a/xrpl4j-integration-tests/src/test/java/org/xrpl/xrpl4j/tests/XChainIT.java b/xrpl4j-integration-tests/src/test/java/org/xrpl/xrpl4j/tests/XChainIT.java index f98919765..e5012517c 100644 --- a/xrpl4j-integration-tests/src/test/java/org/xrpl/xrpl4j/tests/XChainIT.java +++ b/xrpl4j-integration-tests/src/test/java/org/xrpl/xrpl4j/tests/XChainIT.java @@ -196,17 +196,17 @@ void testXChainAddAccountCreateAttestation() throws JsonProcessingException, Jso assertThat(newFields.xChainAccountCreateCount()).isNotEmpty().get().isEqualTo(XChainCount.of(UnsignedLong.ONE)); assertThat(newFields.xChainCreateAccountAttestations()).containsExactly( ImmutableMetaXChainCreateAccountAttestation.builder() - .xChainCreateAccountProofSig( - ImmutableMetaXChainCreateAccountProofSig.builder() - .amount(transaction.amount()) - .signatureReward(transaction.signatureReward()) - .attestationRewardAccount(transaction.attestationRewardAccount()) - .attestationSignerAccount(transaction.attestationSignerAccount()) - .destination(transaction.destination()) - .publicKey(testBridge.witnessKeyPair().publicKey()) - .wasLockingChainSend(false) - .build() - ) + .xChainCreateAccountProofSig( + ImmutableMetaXChainCreateAccountProofSig.builder() + .amount(transaction.amount()) + .signatureReward(transaction.signatureReward()) + .attestationRewardAccount(transaction.attestationRewardAccount()) + .attestationSignerAccount(transaction.attestationSignerAccount()) + .destination(transaction.destination()) + .publicKey(testBridge.witnessKeyPair().publicKey()) + .wasLockingChainSend(false) + .build() + ) .build() ); @@ -221,16 +221,16 @@ void testXChainAddAccountCreateAttestation() throws JsonProcessingException, Jso assertThat(claimIdObject.xChainBridge()).isEqualTo(testBridge.bridge()); assertThat(claimIdObject.xChainCreateAccountAttestations()).containsExactly( XChainCreateAccountAttestation.of( - XChainCreateAccountProofSig.builder() - .amount(transaction.amount()) - .signatureReward(transaction.signatureReward().get()) - .attestationRewardAccount(transaction.attestationRewardAccount()) - .attestationSignerAccount(transaction.attestationSignerAccount()) - .destination(transaction.destination()) - .publicKey(testBridge.witnessKeyPair().publicKey()) - .wasLockingChainSend(false) - .build() - ) + XChainCreateAccountProofSig.builder() + .amount(transaction.amount()) + .signatureReward(transaction.signatureReward().get()) + .attestationRewardAccount(transaction.attestationRewardAccount()) + .attestationSignerAccount(transaction.attestationSignerAccount()) + .destination(transaction.destination()) + .publicKey(testBridge.witnessKeyPair().publicKey()) + .wasLockingChainSend(false) + .build() + ) ); // Create an attestation for witness 2 to sign @@ -362,36 +362,38 @@ void testXChainAddClaimAttestationXrpToXrpBridge() throws JsonRpcClientErrorExce @Test void testAddClaimAttestationIouToIouBridge() throws JsonRpcClientErrorException, JsonProcessingException { KeyPair lockingDoor = Seed.ed25519Seed().deriveKeyPair(); - KeyPair issuer = Seed.ed25519Seed().deriveKeyPair(); + KeyPair lockingChainIssuer = Seed.ed25519Seed().deriveKeyPair(); - KeyPair source = this.createRandomAccountEd25519(); + KeyPair lockingChainSource = this.createRandomAccountEd25519(); XrpCurrencyAmount fee = FeeUtils.computeNetworkFees(xrplClient.fee()).recommendedFee(); - enableRippling(source, fee); + enableRippling(lockingChainSource, fee); KeyPair destination = this.createRandomAccountEd25519(); this.createTrustLine( - "USD", - "1000000000", - source, destination, + IssuedCurrencyAmount.builder() + .issuer(lockingChainSource.publicKey().deriveAddress()) + .currency("USD") + .value("1000000000") + .build(), fee ); TestBridge iouBridge = setupBridge( - source, + lockingChainSource, XChainBridge.builder() .lockingChainDoor(lockingDoor.publicKey().deriveAddress()) .lockingChainIssue( Issue.builder() - .issuer(issuer.publicKey().deriveAddress()) + .issuer(lockingChainIssuer.publicKey().deriveAddress()) .currency("USD") .build() ) - .issuingChainDoor(source.publicKey().deriveAddress()) + .issuingChainDoor(lockingChainSource.publicKey().deriveAddress()) .issuingChainIssue( Issue.builder() - .issuer(source.publicKey().deriveAddress()) + .issuer(lockingChainSource.publicKey().deriveAddress()) .currency("USD") .build() ) @@ -401,15 +403,17 @@ void testAddClaimAttestationIouToIouBridge() throws JsonRpcClientErrorException, KeyPair otherChainSource = this.createRandomAccountEd25519(); IssuedCurrencyAmount amount = IssuedCurrencyAmount.builder() + .issuer(lockingChainIssuer.publicKey().deriveAddress()) .currency("USD") - .issuer(issuer.publicKey().deriveAddress()) .value("10") .build(); AccountInfoResult sourceAccountInfo = this.scanForResult( - () -> this.getValidatedAccountInfo(source.publicKey().deriveAddress())); + () -> this.getValidatedAccountInfo(lockingChainSource.publicKey().deriveAddress())); - XChainOwnedClaimIdObject claimIdObject = createClaimId(sourceAccountInfo, fee, source, iouBridge, otherChainSource); + XChainOwnedClaimIdObject claimIdObject = createClaimId( + sourceAccountInfo, fee, lockingChainSource, iouBridge, otherChainSource + ); XChainAddClaimAttestation addAttestation = addClaimAttestation( otherChainSource,