From 6e085faa3c146941bc67bf58d766e8be6166e087 Mon Sep 17 00:00:00 2001 From: nkramer44 Date: Thu, 13 Jul 2023 11:33:34 -0400 Subject: [PATCH] Add support for book_offers RPC (#440) * first pass at book_offers request/result * javadoc and test * add XrplClient method and call it in offerIT * fix javadoc * make BookOffersRequestParams.ledgerSpecifier non-optional so deserialization works correctly --------- Co-authored-by: David Fuelling --- .../org/xrpl/xrpl4j/client/XrplClient.java | 74 +++-- .../xrpl/xrpl4j/client/XrplClientTest.java | 32 +++ .../model/client/path/BookOffersOffer.java | 221 +++++++++++++++ .../client/path/BookOffersRequestParams.java | 77 ++++++ .../model/client/path/BookOffersResult.java | 110 ++++++++ .../model/jackson/modules/Xrpl4jModule.java | 2 +- .../org/xrpl/xrpl4j/model/ledger/Issue.java | 66 +++++ .../xrpl/xrpl4j/model/AbstractJsonTest.java | 37 ++- .../client/path/BookOffersOfferTest.java | 69 +++++ .../path/BookOffersRequestParamsTest.java | 69 +++++ .../client/path/BookOffersResultTest.java | 252 ++++++++++++++++++ .../xrpl/xrpl4j/model/ledger/IssueTest.java | 56 ++++ .../java/org/xrpl/xrpl4j/tests/OfferIT.java | 46 +++- 13 files changed, 1055 insertions(+), 56 deletions(-) create mode 100644 xrpl4j-core/src/main/java/org/xrpl/xrpl4j/model/client/path/BookOffersOffer.java create mode 100644 xrpl4j-core/src/main/java/org/xrpl/xrpl4j/model/client/path/BookOffersRequestParams.java create mode 100644 xrpl4j-core/src/main/java/org/xrpl/xrpl4j/model/client/path/BookOffersResult.java create mode 100644 xrpl4j-core/src/main/java/org/xrpl/xrpl4j/model/ledger/Issue.java create mode 100644 xrpl4j-core/src/test/java/org/xrpl/xrpl4j/model/client/path/BookOffersOfferTest.java create mode 100644 xrpl4j-core/src/test/java/org/xrpl/xrpl4j/model/client/path/BookOffersRequestParamsTest.java create mode 100644 xrpl4j-core/src/test/java/org/xrpl/xrpl4j/model/client/path/BookOffersResultTest.java create mode 100644 xrpl4j-core/src/test/java/org/xrpl/xrpl4j/model/ledger/IssueTest.java 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 96924d225..e7928a663 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 @@ -29,7 +29,6 @@ import com.google.common.primitives.UnsignedInteger; import com.google.common.primitives.UnsignedLong; import okhttp3.HttpUrl; -import org.immutables.value.Value; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import org.xrpl.xrpl4j.codec.binary.XrplBinaryCodec; @@ -67,6 +66,8 @@ import org.xrpl.xrpl4j.model.client.nft.NftBuyOffersResult; import org.xrpl.xrpl4j.model.client.nft.NftSellOffersRequestParams; import org.xrpl.xrpl4j.model.client.nft.NftSellOffersResult; +import org.xrpl.xrpl4j.model.client.path.BookOffersRequestParams; +import org.xrpl.xrpl4j.model.client.path.BookOffersResult; import org.xrpl.xrpl4j.model.client.path.DepositAuthorizedRequestParams; import org.xrpl.xrpl4j.model.client.path.DepositAuthorizedResult; import org.xrpl.xrpl4j.model.client.path.RipplePathFindRequestParams; @@ -84,34 +85,10 @@ import org.xrpl.xrpl4j.model.client.transactions.TransactionResult; import org.xrpl.xrpl4j.model.immutables.FluentCompareTo; import org.xrpl.xrpl4j.model.jackson.ObjectMapperFactory; -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.CheckCancel; -import org.xrpl.xrpl4j.model.transactions.CheckCash; -import org.xrpl.xrpl4j.model.transactions.CheckCreate; -import org.xrpl.xrpl4j.model.transactions.DepositPreAuth; -import org.xrpl.xrpl4j.model.transactions.EscrowCancel; -import org.xrpl.xrpl4j.model.transactions.EscrowCreate; -import org.xrpl.xrpl4j.model.transactions.EscrowFinish; import org.xrpl.xrpl4j.model.transactions.Hash256; -import org.xrpl.xrpl4j.model.transactions.NfTokenAcceptOffer; -import org.xrpl.xrpl4j.model.transactions.NfTokenBurn; -import org.xrpl.xrpl4j.model.transactions.NfTokenCancelOffer; -import org.xrpl.xrpl4j.model.transactions.NfTokenCreateOffer; -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.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.SetRegularKey; -import org.xrpl.xrpl4j.model.transactions.SignerListSet; -import org.xrpl.xrpl4j.model.transactions.TicketCreate; import org.xrpl.xrpl4j.model.transactions.Transaction; import org.xrpl.xrpl4j.model.transactions.TransactionMetadata; -import org.xrpl.xrpl4j.model.transactions.TrustSet; import java.util.Objects; import java.util.Optional; @@ -160,6 +137,7 @@ public XrplClient(final HttpUrl rippledUrl) { * @param signedTransaction A {@link SingleSignedTransaction} to submit. * * @return The {@link SubmitResult} resulting from the submission request. + * * @throws JsonRpcClientErrorException If {@code jsonRpcClient} throws an error. * @throws JsonProcessingException if any JSON is invalid. * @see "https://xrpl.org/submit.html" @@ -194,6 +172,7 @@ public SubmitResult submit(final SingleSignedTransact * @param A type parameter for the type of {@link Transaction} being submitted. * * @return A {@link SubmitMultiSignedResult} of type {@link T}. + * * @throws JsonRpcClientErrorException if {@code jsonRpcClient} throws an error. */ public SubmitMultiSignedResult submitMultisigned(MultiSignedTransaction transaction) @@ -214,6 +193,7 @@ public SubmitMultiSignedResult submitMultisigned(Mult * Get the current state of the open-ledger requirements for transaction costs. * * @return A {@link FeeResult} containing information about current transaction costs. + * * @throws JsonRpcClientErrorException If {@code jsonRpcClient} throws an error. * @see "https://xrpl.org/fee.html" */ @@ -229,6 +209,7 @@ public FeeResult fee() throws JsonRpcClientErrorException { * Get the ledger index of a tx result response. If not present, throw an exception. * * @return A string containing value of last validated ledger index. + * * @throws JsonRpcClientErrorException when client encounters errors related to calling rippled JSON RPC API.. */ protected UnsignedInteger getMostRecentlyValidatedLedgerIndex() throws JsonRpcClientErrorException { @@ -385,6 +366,7 @@ public Finality isFinal( * {@link ServerInfo}. * * @return A {@link ServerInfoResult} containing information about the server. + * * @throws JsonRpcClientErrorException If {@code jsonRpcClient} throws an error. * @see "https://xrpl.org/server_info.html" */ @@ -404,6 +386,7 @@ public ServerInfoResult serverInformation() * @param params The {@link AccountChannelsRequestParams} to send in the request. * * @return The {@link AccountChannelsResult} returned by the account_channels method call. + * * @throws JsonRpcClientErrorException If {@code jsonRpcClient} throws an error. */ public AccountChannelsResult accountChannels(AccountChannelsRequestParams params) throws JsonRpcClientErrorException { @@ -422,6 +405,7 @@ public AccountChannelsResult accountChannels(AccountChannelsRequestParams params * @param params The {@link AccountCurrenciesRequestParams} to send in the request. * * @return The {@link AccountCurrenciesResult} returned by the account_currencies method call. + * * @throws JsonRpcClientErrorException If {@code jsonRpcClient} throws an error. */ public AccountCurrenciesResult accountCurrencies( @@ -442,6 +426,7 @@ public AccountCurrenciesResult accountCurrencies( * @param params The {@link AccountInfoRequestParams} to send in the request. * * @return The {@link AccountInfoResult} returned by the account_info method call. + * * @throws JsonRpcClientErrorException If {@code jsonRpcClient} throws an error. */ public AccountInfoResult accountInfo(AccountInfoRequestParams params) throws JsonRpcClientErrorException { @@ -459,6 +444,7 @@ public AccountInfoResult accountInfo(AccountInfoRequestParams params) throws Jso * @param account to get the NFTs for. * * @return {@link AccountNftsResult} containing list of accounts for an address. + * * @throws JsonRpcClientErrorException If {@code jsonRpcClient} throws an error. */ public AccountNftsResult accountNfts(Address account) throws JsonRpcClientErrorException { @@ -469,12 +455,13 @@ public AccountNftsResult accountNfts(Address account) throws JsonRpcClientErrorE } /** - * Get the {@link AccountNftsResult} for the account specified in {@code params} by making an account_channels - * method call. + * Get the {@link AccountNftsResult} for the account specified in {@code params} by making an account_channels method + * call. * * @param params The {@link AccountNftsRequestParams} to send in the request. * * @return The {@link AccountNftsResult} returned by the account_nfts method call. + * * @throws JsonRpcClientErrorException If {@code jsonRpcClient} throws an error. */ public AccountNftsResult accountNfts(AccountNftsRequestParams params) throws JsonRpcClientErrorException { @@ -493,6 +480,7 @@ public AccountNftsResult accountNfts(AccountNftsRequestParams params) throws Jso * @param params The {@link NftBuyOffersRequestParams} to send in the request. * * @return The {@link NftBuyOffersResult} returned by the nft_buy_offers method call. + * * @throws JsonRpcClientErrorException If {@code jsonRpcClient} throws an error. */ public NftBuyOffersResult nftBuyOffers(NftBuyOffersRequestParams params) throws JsonRpcClientErrorException { @@ -511,6 +499,7 @@ public NftBuyOffersResult nftBuyOffers(NftBuyOffersRequestParams params) throws * @param params The {@link NftSellOffersRequestParams} to send in the request. * * @return The {@link NftSellOffersResult} returned by the nft_sell_offers method call. + * * @throws JsonRpcClientErrorException If {@code jsonRpcClient} throws an error. */ public NftSellOffersResult nftSellOffers(NftSellOffersRequestParams params) throws JsonRpcClientErrorException { @@ -529,6 +518,7 @@ public NftSellOffersResult nftSellOffers(NftSellOffersRequestParams params) thro * @param params The {@link AccountObjectsRequestParams} to send in the request. * * @return The {@link AccountObjectsResult} returned by the account_objects method call. + * * @throws JsonRpcClientErrorException If {@code jsonRpcClient} throws an error. */ public AccountObjectsResult accountObjects(AccountObjectsRequestParams params) throws JsonRpcClientErrorException { @@ -546,6 +536,7 @@ public AccountObjectsResult accountObjects(AccountObjectsRequestParams params) t * @param params The {@link AccountOffersRequestParams} to send in the request. * * @return The {@link AccountOffersResult} returned by the account_offers method call. + * * @throws JsonRpcClientErrorException If {@code jsonRpcClient} throws an error. */ public AccountOffersResult accountOffers(AccountOffersRequestParams params) throws JsonRpcClientErrorException { @@ -563,6 +554,7 @@ public AccountOffersResult accountOffers(AccountOffersRequestParams params) thro * @param params A {@link DepositAuthorizedRequestParams} to send in the request. * * @return The {@link DepositAuthorizedResult} returned by the deposit_authorized method call. + * * @throws JsonRpcClientErrorException If {@code jsonRpcClient} throws an error. */ public DepositAuthorizedResult depositAuthorized(DepositAuthorizedRequestParams params) @@ -581,6 +573,7 @@ public DepositAuthorizedResult depositAuthorized(DepositAuthorizedRequestParams * @param address The {@link Address} of the account to request. * * @return The {@link AccountTransactionsResult} returned by the account_tx method call. + * * @throws JsonRpcClientErrorException If {@code jsonRpcClient} throws an error. */ public AccountTransactionsResult accountTransactions(Address address) throws JsonRpcClientErrorException { @@ -596,6 +589,7 @@ public AccountTransactionsResult accountTransactions(Address address) throws Jso * @param params The {@link AccountTransactionsRequestParams} to send in the request. * * @return The {@link AccountTransactionsResult} returned by the account_tx method call. + * * @throws JsonRpcClientErrorException If {@code jsonRpcClient} throws an error. */ public AccountTransactionsResult accountTransactions(AccountTransactionsRequestParams params) @@ -616,6 +610,7 @@ public AccountTransactionsResult accountTransactions(AccountTransactionsRequestP * @param Type parameter for the type of {@link Transaction} that the {@link TransactionResult} will * * @return A {@link TransactionResult} containing the requested transaction and other metadata. + * * @throws JsonRpcClientErrorException If {@code jsonRpcClient} throws an error. */ public TransactionResult transaction( @@ -641,6 +636,7 @@ public TransactionResult transaction( * @param params The {@link LedgerRequestParams} to send in the request. * * @return A {@link LedgerResult} containing the ledger details. + * * @throws JsonRpcClientErrorException if {@code jsonRpcClient} throws an error. */ public LedgerResult ledger(LedgerRequestParams params) throws JsonRpcClientErrorException { @@ -658,6 +654,7 @@ public LedgerResult ledger(LedgerRequestParams params) throws JsonRpcClientError * @param params The {@link RipplePathFindRequestParams} to send in the request. * * @return A {@link RipplePathFindResult} containing possible paths. + * * @throws JsonRpcClientErrorException if {@code jsonRpcClient} throws an error. */ public RipplePathFindResult ripplePathFind(RipplePathFindRequestParams params) throws JsonRpcClientErrorException { @@ -669,12 +666,31 @@ public RipplePathFindResult ripplePathFind(RipplePathFindRequestParams params) t return jsonRpcClient.send(request, RipplePathFindResult.class); } + /** + * Send a {@code book_offers} RPC request. + * + * @param params The {@link BookOffersRequestParams} to send in the request. + * + * @return A {@link BookOffersResult}. + * + * @throws JsonRpcClientErrorException If {@code jsonRpcClient} throws an error. + */ + public BookOffersResult bookOffers(BookOffersRequestParams params) throws JsonRpcClientErrorException { + JsonRpcRequest request = JsonRpcRequest.builder() + .method(XrplMethods.BOOK_OFFERS) + .addParams(params) + .build(); + + return jsonRpcClient.send(request, BookOffersResult.class); + } + /** * Get the trust lines for a given account by sending an account_lines method request. * * @param params The {@link AccountLinesRequestParams} to send in the request. * * @return The {@link AccountLinesResult} containing the requested trust lines. + * * @throws JsonRpcClientErrorException if {@code jsonRpcClient} throws an error. */ public AccountLinesResult accountLines(AccountLinesRequestParams params) throws JsonRpcClientErrorException { @@ -692,6 +708,7 @@ public AccountLinesResult accountLines(AccountLinesRequestParams params) throws * @param params The {@link ChannelVerifyRequestParams} to send in the request. * * @return The result of the request, as a {@link ChannelVerifyResult}. + * * @throws JsonRpcClientErrorException if {@code jsonRpcClient} throws an error. */ public ChannelVerifyResult channelVerify(ChannelVerifyRequestParams params) throws JsonRpcClientErrorException { @@ -710,6 +727,7 @@ public ChannelVerifyResult channelVerify(ChannelVerifyRequestParams params) thro * @param params The {@link GatewayBalancesRequestParams} to send in the request. * * @return The result of the request, as a {@link GatewayBalancesResult}. + * * @throws JsonRpcClientErrorException if {@code jsonRpcClient} throws an error. */ public GatewayBalancesResult gatewayBalances( @@ -721,7 +739,7 @@ public GatewayBalancesResult gatewayBalances( .build(); return jsonRpcClient.send(request, GatewayBalancesResult.class); } - + public JsonRpcClient getJsonRpcClient() { return jsonRpcClient; } 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 2b5dc2009..f141f1a16 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 @@ -50,6 +50,7 @@ import org.xrpl.xrpl4j.crypto.signing.bc.BcSignatureService; import org.xrpl.xrpl4j.model.client.FinalityStatus; import org.xrpl.xrpl4j.model.client.XrplMethods; +import org.xrpl.xrpl4j.model.client.XrplRequestParams; import org.xrpl.xrpl4j.model.client.XrplResult; import org.xrpl.xrpl4j.model.client.accounts.AccountChannelsRequestParams; import org.xrpl.xrpl4j.model.client.accounts.AccountChannelsResult; @@ -80,8 +81,11 @@ import org.xrpl.xrpl4j.model.client.nft.NftBuyOffersResult; import org.xrpl.xrpl4j.model.client.nft.NftSellOffersRequestParams; import org.xrpl.xrpl4j.model.client.nft.NftSellOffersResult; +import org.xrpl.xrpl4j.model.client.path.BookOffersRequestParams; +import org.xrpl.xrpl4j.model.client.path.BookOffersResult; import org.xrpl.xrpl4j.model.client.path.DepositAuthorizedRequestParams; import org.xrpl.xrpl4j.model.client.path.DepositAuthorizedResult; +import org.xrpl.xrpl4j.model.client.path.ImmutableBookOffersRequestParams; import org.xrpl.xrpl4j.model.client.path.PathCurrency; import org.xrpl.xrpl4j.model.client.path.RipplePathFindRequestParams; import org.xrpl.xrpl4j.model.client.path.RipplePathFindResult; @@ -97,6 +101,7 @@ import org.xrpl.xrpl4j.model.client.transactions.TransactionResult; import org.xrpl.xrpl4j.model.flags.AccountRootFlags; import org.xrpl.xrpl4j.model.ledger.AccountRootObject; +import org.xrpl.xrpl4j.model.ledger.Issue; import org.xrpl.xrpl4j.model.transactions.Address; import org.xrpl.xrpl4j.model.transactions.Hash256; import org.xrpl.xrpl4j.model.transactions.NfTokenId; @@ -921,6 +926,33 @@ public void ripplePathFind() throws JsonRpcClientErrorException { assertThat(jsonRpcRequestArgumentCaptor.getValue().params().get(0)).isEqualTo(ripplePathFindRequestParams); } + @Test + void bookOffers() throws JsonRpcClientErrorException { + BookOffersRequestParams params = BookOffersRequestParams.builder() + .taker(Address.of("r9cZA1mLK5R5Am25ArfXFmqgNwjZgnfk59")) + .takerGets(Issue.XRP) + .takerPays( + Issue.builder() + .issuer(Address.of("rvYAfWj5gh67oV6fW32ZzP3Aw4Eubs59B")) + .currency("USD") + .build() + ) + .limit(UnsignedInteger.valueOf(10)) + .ledgerSpecifier(LedgerSpecifier.CURRENT) + .build(); + + BookOffersResult resultMock = mock(BookOffersResult.class); + when(jsonRpcClientMock.send( + JsonRpcRequest.builder() + .method(XrplMethods.BOOK_OFFERS) + .addParams(params) + .build(), + BookOffersResult.class + )).thenReturn(resultMock); + BookOffersResult result = xrplClient.bookOffers(params); + assertThat(result).isEqualTo(resultMock); + } + @Test public void accountLines() throws JsonRpcClientErrorException { AccountLinesRequestParams accountLinesRequestParams = AccountLinesRequestParams.builder() diff --git a/xrpl4j-core/src/main/java/org/xrpl/xrpl4j/model/client/path/BookOffersOffer.java b/xrpl4j-core/src/main/java/org/xrpl/xrpl4j/model/client/path/BookOffersOffer.java new file mode 100644 index 000000000..984d53cc0 --- /dev/null +++ b/xrpl4j-core/src/main/java/org/xrpl/xrpl4j/model/client/path/BookOffersOffer.java @@ -0,0 +1,221 @@ +package org.xrpl.xrpl4j.model.client.path; + +import com.fasterxml.jackson.annotation.JsonIgnore; +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.primitives.UnsignedInteger; +import org.immutables.value.Value; +import org.xrpl.xrpl4j.model.flags.OfferFlags; +import org.xrpl.xrpl4j.model.ledger.LedgerObject; +import org.xrpl.xrpl4j.model.transactions.Address; +import org.xrpl.xrpl4j.model.transactions.CurrencyAmount; +import org.xrpl.xrpl4j.model.transactions.Hash256; + +import java.math.BigDecimal; +import java.util.Optional; + +/** + * Representation of an {@link org.xrpl.xrpl4j.model.ledger.OfferObject} returned in responses to {@code book_offers} + * RPC requests. + * + *

Note that this object duplicates all the fields of {@link org.xrpl.xrpl4j.model.ledger.OfferObject} instead of + * simply containing an {@link org.xrpl.xrpl4j.model.ledger.OfferObject} field. The offer fields exist at the same JSON + * level as {@link BookOffersOffer}, but we cannot use {@link JsonUnwrapped} on a field of type + * {@link org.xrpl.xrpl4j.model.ledger.OfferObject} because it extends {@link LedgerObject} which has Jackson + * polymorphic annotations on it and {@link JsonUnwrapped} does not play nicely with polymorphic deserialization. + * + * @see "https://xrpl.org/book_offers.html" + */ +@Value.Immutable +@JsonSerialize(as = ImmutableBookOffersOffer.class) +@JsonDeserialize(as = ImmutableBookOffersOffer.class) +public interface BookOffersOffer { + + /** + * Construct a {@code BookOffersOffer} builder. + * + * @return An {@link ImmutableBookOffersOffer.Builder}. + */ + static ImmutableBookOffersOffer.Builder builder() { + return ImmutableBookOffersOffer.builder(); + } + + /** + * The value 0x006F, mapped to the string "Offer", indicates that this object is a + * {@link org.xrpl.xrpl4j.model.ledger.OfferObject} object. + * + * @return Always {@link org.xrpl.xrpl4j.model.ledger.LedgerObject.LedgerEntryType#OFFER}. + */ + @JsonProperty("LedgerEntryType") + @Value.Derived + default LedgerObject.LedgerEntryType ledgerEntryType() { + return LedgerObject.LedgerEntryType.OFFER; + } + + /** + * The sender of the {@link org.xrpl.xrpl4j.model.ledger.OfferObject}. Cashing the + * {@link org.xrpl.xrpl4j.model.ledger.OfferObject} debits this address's balance. + * + * @return The {@link Address} of the offer sender. + */ + @JsonProperty("Account") + Address account(); + + /** + * A bit-map of boolean flags. + * + * @return A {@link OfferFlags}. + */ + @JsonProperty("Flags") + OfferFlags flags(); + + /** + * The sequence number of the {@link org.xrpl.xrpl4j.model.transactions.OfferCreate} transaction that created this + * offer. + * + * @return An {@link UnsignedInteger} representing the sequence number. + */ + @JsonProperty("Sequence") + UnsignedInteger sequence(); + + /** + * The remaining amount and type of currency requested by the offer creator. + * + * @return A {@link CurrencyAmount}. + */ + @JsonProperty("TakerPays") + CurrencyAmount takerPays(); + + + /** + * The remaining amount and type of currency being provided by the offer creator. + * + * @return A {@link CurrencyAmount}. + */ + @JsonProperty("TakerGets") + CurrencyAmount takerGets(); + + + /** + * The ID of the Offer Directory that links to this offer. + * + * @return A {@link Hash256} containing the ID. + */ + @JsonProperty("BookDirectory") + Hash256 bookDirectory(); + + /** + * A hint indicating which page of the offer directory links to this object, in case the directory consists of + * multiple pages. + * + * @return A {@link String} containing the hint. + */ + @JsonProperty("BookNode") + String bookNode(); + + /** + * 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 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(); + + /** + * Indicates the time after which this offer is considered expired, in + * seconds since the Ripple Epoch. + * + * @return An {@link Optional} of type {@link UnsignedInteger} representing the expiration of this offer. + */ + @JsonProperty("Expiration") + Optional expiration(); + + /** + * The unique ID of the {@link org.xrpl.xrpl4j.model.ledger.OfferObject}. + * + * @return A {@link Hash256} containing the ID. + */ + Hash256 index(); + + /** + * Amount of the TakerGets currency the side placing the offer has available to be traded. (XRP is represented as + * drops; any other currency is represented as a decimal value.) If a trader has multiple offers in the same book, + * only the highest-ranked offer includes this field. + * + *

Use {@link #ownerFunds()} to get this value as a {@link BigDecimal}. + * + * @return An {@link Optional} {@link String}. + */ + @JsonProperty("owner_funds") + Optional ownerFundsString(); + + /** + * Gets the value of {@link #ownerFundsString()} as a {@link BigDecimal}. + * + * @return An {@link Optional} {@link BigDecimal}. + */ + @Value.Derived + @JsonIgnore + default Optional ownerFunds() { + return ownerFundsString().map(BigDecimal::new); + } + + /** + * The maximum amount of currency that the taker can get, given the funding status of the offer. + * + * @return An {@link Optional} {@link CurrencyAmount}. Only present in partially funded offers. + */ + @JsonProperty("taker_gets_funded") + Optional takerGetsFunded(); + + /** + * The maximum amount of currency that the taker would pay, given the funding status of the offer. + * + * @return An {@link Optional} {@link CurrencyAmount}. Only present in partially funded offers. + */ + @JsonProperty("taker_pays_funded") + Optional takerPaysFunded(); + + /** + * The exchange rate, as the ratio {@link #takerPays()} divided by {@link #takerGets()}. For fairness, offers that + * have the same quality are automatically taken first-in, first-out. (In other words, if multiple people offer to + * exchange currency at the same rate, the oldest offer is taken first.) + * + *

Use {@link #quality()} to get this value as a {@link BigDecimal}. + * + * @return A {@link String} containing the quality. + */ + @JsonProperty("quality") + String qualityString(); + + /** + * Get the value of {@link #qualityString()} as a {@link BigDecimal}. + * + * @return A {@link BigDecimal}. + */ + @Value.Derived + @JsonIgnore + default BigDecimal quality() { + return new BigDecimal(qualityString()); + } +} diff --git a/xrpl4j-core/src/main/java/org/xrpl/xrpl4j/model/client/path/BookOffersRequestParams.java b/xrpl4j-core/src/main/java/org/xrpl/xrpl4j/model/client/path/BookOffersRequestParams.java new file mode 100644 index 000000000..0979a303d --- /dev/null +++ b/xrpl4j-core/src/main/java/org/xrpl/xrpl4j/model/client/path/BookOffersRequestParams.java @@ -0,0 +1,77 @@ +package org.xrpl.xrpl4j.model.client.path; + +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.primitives.UnsignedInteger; +import org.immutables.value.Value; +import org.xrpl.xrpl4j.model.client.XrplRequestParams; +import org.xrpl.xrpl4j.model.client.common.LedgerSpecifier; +import org.xrpl.xrpl4j.model.ledger.Issue; +import org.xrpl.xrpl4j.model.transactions.Address; + +import java.util.Optional; + +/** + * Request parameters for a "book_offers" rippled API method call. + * + * @see "https://xrpl.org/book_offers.html" + */ +@Value.Immutable +@JsonSerialize(as = ImmutableBookOffersRequestParams.class) +@JsonDeserialize(as = ImmutableBookOffersRequestParams.class) +public interface BookOffersRequestParams extends XrplRequestParams { + + /** + * Construct a {@code BookOffersRequestParams} builder. + * + * @return An {@link ImmutableBookOffersRequestParams.Builder}. + */ + static ImmutableBookOffersRequestParams.Builder builder() { + return ImmutableBookOffersRequestParams.builder(); + } + + /** + * The asset the account taking the Offer would receive. + * + * @return An {@link Issue}. + */ + @JsonProperty("taker_gets") + Issue takerGets(); + + /** + * The asset the account taking the Offer would pay. + * + * @return An {@link Issue}. + */ + @JsonProperty("taker_pays") + Issue takerPays(); + + /** + * The Address of an account to use as a perspective. The response includes this account's Offers even if they are + * unfunded. (You can use this to see what Offers are above or below yours in the order book.) + * + * @return An optionally-present {@link Address}. + */ + @JsonProperty("taker") + Optional

taker(); + + /** + * 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(); + + /** + * Limit the number of offers to retrieve. Note that until + * #3534 is fixed, book_offers results will not be + * paginated. + * + * @return An optionally-present {@link UnsignedInteger}. + */ + Optional limit(); +} diff --git a/xrpl4j-core/src/main/java/org/xrpl/xrpl4j/model/client/path/BookOffersResult.java b/xrpl4j-core/src/main/java/org/xrpl/xrpl4j/model/client/path/BookOffersResult.java new file mode 100644 index 000000000..578d40775 --- /dev/null +++ b/xrpl4j-core/src/main/java/org/xrpl/xrpl4j/model/client/path/BookOffersResult.java @@ -0,0 +1,110 @@ +package org.xrpl.xrpl4j.model.client.path; + +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 org.immutables.value.Value; +import org.xrpl.xrpl4j.model.client.XrplResult; +import org.xrpl.xrpl4j.model.client.common.LedgerIndex; +import org.xrpl.xrpl4j.model.transactions.Hash256; + +import java.util.List; +import java.util.Optional; + +/** + * Response object for a "book_offers" rippled API method call. + * + * @see "https://xrpl.org/book_offers.html" + */ +@Value.Immutable +@JsonSerialize(as = ImmutableBookOffersResult.class) +@JsonDeserialize(as = ImmutableBookOffersResult.class) +public interface BookOffersResult extends XrplResult { + + /** + * Construct a {@code BookOffersResult} builder. + * + * @return An {@link ImmutableBookOffersResult.Builder}. + */ + static ImmutableBookOffersResult.Builder builder() { + return ImmutableBookOffersResult.builder(); + } + + /** + * A {@link List} of {@link BookOffersOffer}s containing information about the offers in the requested order book. + * + * @return A {@link List} of {@link BookOffersOffer}s. + */ + List offers(); + + /** + * A 20-byte hex string for the ledger version to use. + * + * @return An optionally-present {@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.")); + } + + /** + * (Omitted if ledger_current_index is provided instead) The ledger index of the ledger version used when retrieving + * this information. The information does not contain any changes from ledger versions newer than this one. + * + * @return An optionally-present {@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.")); + } + + /** + * (Omitted if ledger_index is provided instead) The ledger index of the current in-progress ledger, which was used + * when retrieving this information. + * + * @return An optionally-present {@link LedgerIndex}. + */ + @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.")); + } + +} diff --git a/xrpl4j-core/src/main/java/org/xrpl/xrpl4j/model/jackson/modules/Xrpl4jModule.java b/xrpl4j-core/src/main/java/org/xrpl/xrpl4j/model/jackson/modules/Xrpl4jModule.java index 3b36e0ebb..7d25486ed 100644 --- a/xrpl4j-core/src/main/java/org/xrpl/xrpl4j/model/jackson/modules/Xrpl4jModule.java +++ b/xrpl4j-core/src/main/java/org/xrpl/xrpl4j/model/jackson/modules/Xrpl4jModule.java @@ -24,6 +24,7 @@ import com.fasterxml.jackson.databind.module.SimpleModule; import org.xrpl.xrpl4j.codec.addresses.UnsignedByteArray; import org.xrpl.xrpl4j.model.client.common.LedgerIndex; +import org.xrpl.xrpl4j.model.client.path.BookOffersOffer; import org.xrpl.xrpl4j.model.client.serverinfo.ServerInfo; import org.xrpl.xrpl4j.model.flags.Flags; import org.xrpl.xrpl4j.model.transactions.CurrencyAmount; @@ -69,6 +70,5 @@ public Xrpl4jModule() { addSerializer(Flags.class, new FlagsSerializer()); addDeserializer(AffectedNode.class, new AffectedNodeDeserializer()); - } } diff --git a/xrpl4j-core/src/main/java/org/xrpl/xrpl4j/model/ledger/Issue.java b/xrpl4j-core/src/main/java/org/xrpl/xrpl4j/model/ledger/Issue.java new file mode 100644 index 000000000..613cb60e1 --- /dev/null +++ b/xrpl4j-core/src/main/java/org/xrpl/xrpl4j/model/ledger/Issue.java @@ -0,0 +1,66 @@ +package org.xrpl.xrpl4j.model.ledger; + +/*- + * ========================LICENSE_START================================= + * xrpl4j :: model + * %% + * Copyright (C) 2020 - 2022 XRPL Foundation and its contributors + * %% + * 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. + * See the License for the specific language governing permissions and + * limitations under the License. + * =========================LICENSE_END================================== + */ + +import com.fasterxml.jackson.databind.annotation.JsonDeserialize; +import com.fasterxml.jackson.databind.annotation.JsonSerialize; +import org.immutables.value.Value; +import org.xrpl.xrpl4j.model.transactions.Address; + +import java.util.Optional; + +/** + * Represents an asset on the ledger without an amount. + */ +@Value.Immutable +@JsonSerialize(as = ImmutableIssue.class) +@JsonDeserialize(as = ImmutableIssue.class) +public interface Issue { + + /** + * Constant {@link Issue} representing XRP. + */ + Issue XRP = Issue.builder().currency("XRP").build(); + + /** + * Construct a {@code Asset} builder. + * + * @return An {@link ImmutableIssue.Builder}. + */ + static ImmutableIssue.Builder builder() { + return ImmutableIssue.builder(); + } + + /** + * Either a 3 character currency code, or a 40 character hexadecimal encoded currency code value. + * + * @return A {@link String} containing the currency code. + */ + String currency(); + + /** + * The {@link Address} of the issuer of the currency, or empty if the currency is XRP. + * + * @return The {@link Address} of the issuer account. + */ + Optional
issuer(); + +} \ No newline at end of file diff --git a/xrpl4j-core/src/test/java/org/xrpl/xrpl4j/model/AbstractJsonTest.java b/xrpl4j-core/src/test/java/org/xrpl/xrpl4j/model/AbstractJsonTest.java index 50739800d..6ec181716 100644 --- a/xrpl4j-core/src/test/java/org/xrpl/xrpl4j/model/AbstractJsonTest.java +++ b/xrpl4j-core/src/test/java/org/xrpl/xrpl4j/model/AbstractJsonTest.java @@ -48,13 +48,21 @@ public void setUp() { objectMapper = ObjectMapperFactory.create(); } - protected void assertCanSerializeAndDeserialize(Transaction transaction, String json) - throws JsonProcessingException, JSONException { - String serialized = objectMapper.writeValueAsString(transaction); + protected void assertCanSerializeAndDeserialize( + T object, + String json, + Class clazz + ) throws JSONException, JsonProcessingException { + String serialized = objectMapper.writeValueAsString(object); JSONAssert.assertEquals(json, serialized, JSONCompareMode.STRICT); - Transaction deserialized = objectMapper.readValue(serialized, Transaction.class); - assertThat(deserialized).isEqualTo(transaction); + T deserialized = objectMapper.readValue(serialized, clazz); + assertThat(deserialized).isEqualTo(object); + } + + protected void assertCanSerializeAndDeserialize(Transaction transaction, String json) + throws JsonProcessingException, JSONException { + assertCanSerializeAndDeserialize(transaction, json, Transaction.class); } protected void assertCanSerializeAndDeserialize(XrplResult result, String json) @@ -77,28 +85,17 @@ protected void assertCanSerializeAndDeserialize(XrplRequestParams params, String protected void assertCanSerializeAndDeserialize(LedgerObject ledgerObject, String json) throws JsonProcessingException, JSONException { - String serialized = objectMapper.writeValueAsString(ledgerObject); - JSONAssert.assertEquals(json, serialized, JSONCompareMode.STRICT); - - LedgerObject deserialized = objectMapper.readValue(serialized, LedgerObject.class); - assertThat(deserialized).isEqualTo(ledgerObject); + assertCanSerializeAndDeserialize(ledgerObject, json, LedgerObject.class); } protected void assertCanSerializeAndDeserialize( ValidatedLedger serverInfoLedger, String json - ) - throws JsonProcessingException, JSONException { - String serialized = objectMapper.writeValueAsString(serverInfoLedger); - JSONAssert.assertEquals(json, serialized, JSONCompareMode.STRICT); - - ValidatedLedger deserialized = objectMapper.readValue( - serialized, ValidatedLedger.class - ); - assertThat(deserialized).isEqualTo(serverInfoLedger); + ) throws JsonProcessingException, JSONException { + assertCanSerializeAndDeserialize(serverInfoLedger, json, ValidatedLedger.class); } protected void assertCanDeserialize(String json, XrplResult result) throws JsonProcessingException { XrplResult deserialized = objectMapper.readValue(json, result.getClass()); assertThat(deserialized).isEqualTo(result); } -} +} \ No newline at end of file diff --git a/xrpl4j-core/src/test/java/org/xrpl/xrpl4j/model/client/path/BookOffersOfferTest.java b/xrpl4j-core/src/test/java/org/xrpl/xrpl4j/model/client/path/BookOffersOfferTest.java new file mode 100644 index 000000000..b97d887e6 --- /dev/null +++ b/xrpl4j-core/src/test/java/org/xrpl/xrpl4j/model/client/path/BookOffersOfferTest.java @@ -0,0 +1,69 @@ +package org.xrpl.xrpl4j.model.client.path; + +import static org.assertj.core.api.AssertionsForClassTypes.assertThat; + +import com.google.common.primitives.UnsignedInteger; +import org.junit.jupiter.api.Test; +import org.xrpl.xrpl4j.model.flags.OfferFlags; +import org.xrpl.xrpl4j.model.transactions.Address; +import org.xrpl.xrpl4j.model.transactions.Hash256; +import org.xrpl.xrpl4j.model.transactions.IssuedCurrencyAmount; +import org.xrpl.xrpl4j.model.transactions.XrpCurrencyAmount; + +import java.math.BigDecimal; +import java.util.Optional; + +class BookOffersOfferTest { + + @Test + void testOwnerFundsBigDecimal() { + BookOffersOffer offer = constructOffer(Optional.of("3389276844"), Optional.empty()); + assertThat(offer.ownerFunds()).isNotEmpty().get().isEqualTo(BigDecimal.valueOf(3389276844L)); + + offer = constructOffer(Optional.of("0"), Optional.empty()); + assertThat(offer.ownerFunds()).isNotEmpty().get().isEqualTo(BigDecimal.valueOf(0)); + + offer = constructOffer(Optional.of("0.0000000000000001"), Optional.empty()); + assertThat(offer.ownerFunds()).isNotEmpty(); + assertThat(offer.ownerFunds().get().toPlainString()).isEqualTo("0.0000000000000001"); + + offer = constructOffer(Optional.empty(), Optional.of("0.00000046645691")); + assertThat(offer.ownerFunds()).isEmpty(); + } + + @Test + void testQualityString() { + BookOffersOffer offer = constructOffer(Optional.empty(), Optional.of("0.0000000046645691")); + assertThat(offer.quality().toPlainString()).isEqualTo("0.0000000046645691"); + + offer = constructOffer(Optional.empty(), Optional.of("0")); + assertThat(offer.quality()).isEqualTo(BigDecimal.ZERO); + + offer = constructOffer(Optional.empty(), Optional.of("3389276844")); + assertThat(offer.quality()).isEqualTo(BigDecimal.valueOf(3389276844L)); + } + + private static BookOffersOffer constructOffer(Optional ownerFunds, Optional quality) { + return BookOffersOffer.builder() + .account(Address.of("rPbMHxs7vy5t6e19tYfqG7XJ6Fog8EPZLk")) + .bookDirectory(Hash256.of("DFA3B6DDAB58C7E8E5D944E736DA4B7046C30E4F460FD9DE4E109266A03C5B00")) + .bookNode("0") + .flags(OfferFlags.of(0)) + .ownerNode("0") + .previousTransactionId(Hash256.of("3E8D2F2CC7593C40FF8EC2A5316B7E308473DC90B50CC54AD4D487E4E6515545")) + .previousTransactionLedgerSequence(UnsignedInteger.valueOf(80959227)) + .sequence(UnsignedInteger.valueOf(1400403)) + .takerGets(XrpCurrencyAmount.ofDrops(2500000000L)) + .takerPays( + IssuedCurrencyAmount.builder() + .currency("USD") + .issuer(Address.of("rvYAfWj5gh67oV6fW32ZzP3Aw4Eubs59B")) + .value("1166.142275") + .build() + ) + .index(Hash256.of("D17EA19846F265BD1244F2864B85ECDB7C86D462C00C8B658A035358BDDD6DE2")) + .ownerFundsString(ownerFunds) + .qualityString(quality.orElse("0.00000046645691")) + .build(); + } +} \ No newline at end of file diff --git a/xrpl4j-core/src/test/java/org/xrpl/xrpl4j/model/client/path/BookOffersRequestParamsTest.java b/xrpl4j-core/src/test/java/org/xrpl/xrpl4j/model/client/path/BookOffersRequestParamsTest.java new file mode 100644 index 000000000..5b9cd42d7 --- /dev/null +++ b/xrpl4j-core/src/test/java/org/xrpl/xrpl4j/model/client/path/BookOffersRequestParamsTest.java @@ -0,0 +1,69 @@ +package org.xrpl.xrpl4j.model.client.path; + +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.ledger.Issue; +import org.xrpl.xrpl4j.model.transactions.Address; + +class BookOffersRequestParamsTest extends AbstractJsonTest { + + @Test + void testMinimalJson() throws JSONException, JsonProcessingException { + BookOffersRequestParams expected = BookOffersRequestParams.builder() + .takerGets(Issue.XRP) + .takerPays( + Issue.builder() + .currency("USD") + .issuer(Address.of("rvYAfWj5gh67oV6fW32ZzP3Aw4Eubs59B")) + .build() + ) + .ledgerSpecifier(LedgerSpecifier.CURRENT) + .build(); + String json = "{\n" + + " \"taker_gets\": {\n" + + " \"currency\": \"XRP\"\n" + + " },\n" + + " \"ledger_index\": \"current\",\n" + + " \"taker_pays\": {\n" + + " \"currency\": \"USD\",\n" + + " \"issuer\": \"rvYAfWj5gh67oV6fW32ZzP3Aw4Eubs59B\"\n" + + " }\n" + + " }"; + + assertCanSerializeAndDeserialize(expected, json); + } + + @Test + void testFullJson() throws JSONException, JsonProcessingException { + BookOffersRequestParams expected = BookOffersRequestParams.builder() + .takerGets(Issue.XRP) + .takerPays( + Issue.builder() + .currency("USD") + .issuer(Address.of("rvYAfWj5gh67oV6fW32ZzP3Aw4Eubs59B")) + .build() + ) + .ledgerSpecifier(LedgerSpecifier.CURRENT) + .taker(Address.of("r9cZA1mLK5R5Am25ArfXFmqgNwjZgnfk59")) + .limit(UnsignedInteger.ONE) + .build(); + String json = "{\n" + + " \"taker\": \"r9cZA1mLK5R5Am25ArfXFmqgNwjZgnfk59\",\n" + + " \"ledger_index\": \"current\",\n" + + " \"taker_gets\": {\n" + + " \"currency\": \"XRP\"\n" + + " },\n" + + " \"taker_pays\": {\n" + + " \"currency\": \"USD\",\n" + + " \"issuer\": \"rvYAfWj5gh67oV6fW32ZzP3Aw4Eubs59B\"\n" + + " },\n" + + " \"limit\": 1\n" + + " }"; + + assertCanSerializeAndDeserialize(expected, json); + } +} \ No newline at end of file diff --git a/xrpl4j-core/src/test/java/org/xrpl/xrpl4j/model/client/path/BookOffersResultTest.java b/xrpl4j-core/src/test/java/org/xrpl/xrpl4j/model/client/path/BookOffersResultTest.java new file mode 100644 index 000000000..8441ee8cc --- /dev/null +++ b/xrpl4j-core/src/test/java/org/xrpl/xrpl4j/model/client/path/BookOffersResultTest.java @@ -0,0 +1,252 @@ +package org.xrpl.xrpl4j.model.client.path; + +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 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.flags.OfferFlags; +import org.xrpl.xrpl4j.model.transactions.Address; +import org.xrpl.xrpl4j.model.transactions.Hash256; +import org.xrpl.xrpl4j.model.transactions.IssuedCurrencyAmount; +import org.xrpl.xrpl4j.model.transactions.XrpCurrencyAmount; + +class BookOffersResultTest extends AbstractJsonTest { + + @Test + void testJsonWithLedgerIndexAndHash() throws JSONException, JsonProcessingException { + BookOffersResult result = BookOffersResult.builder() + .ledgerHash(Hash256.of("6442396558D5EF9E64441A29A39759B52813B5E18B6AD86A602A36815037B98B")) + .ledgerIndex(LedgerIndex.of(UnsignedInteger.valueOf(80959235))) + .status("success") + .addOffers( + BookOffersOffer.builder() + .account(Address.of("rBTwLga3i2gz3doX6Gva3MgEV8ZCD8jjah")) + .bookDirectory(Hash256.of("DFA3B6DDAB58C7E8E5D944E736DA4B7046C30E4F460FD9DE4E10925C2010F800")) + .bookNode("0") + .flags(OfferFlags.of(0)) + .ownerNode("0") + .previousTransactionId(Hash256.of("606B3F8835E2B126CA1C331B2A8BE1C7DCC72B07610AF7B8B12E841B815B2F2C")) + .previousTransactionLedgerSequence(UnsignedInteger.valueOf(80959234)) + .sequence(UnsignedInteger.valueOf(99235111)) + .takerGets(XrpCurrencyAmount.ofDrops(28703000000L)) + .takerPays( + IssuedCurrencyAmount.builder() + .currency("USD") + .issuer(Address.of("rvYAfWj5gh67oV6fW32ZzP3Aw4Eubs59B")) + .value("13388.5832372") + .build() + ) + .index(Hash256.of("E72DCD2A088C8E1843100A345D636D6A017796037E83B47F2C7E0350B53A8374")) + .ownerFundsString("28767697713") + .qualityString("0.0000004664524") + .build(), + BookOffersOffer.builder() + .account(Address.of("rPbMHxs7vy5t6e19tYfqG7XJ6Fog8EPZLk")) + .bookDirectory(Hash256.of("DFA3B6DDAB58C7E8E5D944E736DA4B7046C30E4F460FD9DE4E109266A03C5B00")) + .bookNode("0") + .flags(OfferFlags.of(0)) + .ownerNode("0") + .previousTransactionId(Hash256.of("3E8D2F2CC7593C40FF8EC2A5316B7E308473DC90B50CC54AD4D487E4E6515545")) + .previousTransactionLedgerSequence(UnsignedInteger.valueOf(80959227)) + .sequence(UnsignedInteger.valueOf(1400403)) + .takerGets(XrpCurrencyAmount.ofDrops(2500000000L)) + .takerPays( + IssuedCurrencyAmount.builder() + .currency("USD") + .issuer(Address.of("rvYAfWj5gh67oV6fW32ZzP3Aw4Eubs59B")) + .value("1166.142275") + .build() + ) + .index(Hash256.of("D17EA19846F265BD1244F2864B85ECDB7C86D462C00C8B658A035358BDDD6DE2")) + .ownerFundsString("3389276844") + .qualityString("0.00000046645691") + .build() + ) + .build(); + + String json = "{\n" + + " \"ledger_hash\": \"6442396558D5EF9E64441A29A39759B52813B5E18B6AD86A602A36815037B98B\",\n" + + " \"ledger_index\": 80959235,\n" + + " \"offers\": [\n" + + " {\n" + + " \"Account\": \"rBTwLga3i2gz3doX6Gva3MgEV8ZCD8jjah\",\n" + + " \"BookDirectory\": \"DFA3B6DDAB58C7E8E5D944E736DA4B7046C30E4F460FD9DE4E10925C2010F800\",\n" + + " \"BookNode\": \"0\",\n" + + " \"Flags\": 0,\n" + + " \"LedgerEntryType\": \"Offer\",\n" + + " \"OwnerNode\": \"0\",\n" + + " \"PreviousTxnID\": \"606B3F8835E2B126CA1C331B2A8BE1C7DCC72B07610AF7B8B12E841B815B2F2C\",\n" + + " \"PreviousTxnLgrSeq\": 80959234,\n" + + " \"Sequence\": 99235111,\n" + + " \"TakerGets\": \"28703000000\",\n" + + " \"TakerPays\": {\n" + + " \"currency\": \"USD\",\n" + + " \"issuer\": \"rvYAfWj5gh67oV6fW32ZzP3Aw4Eubs59B\",\n" + + " \"value\": \"13388.5832372\"\n" + + " },\n" + + " \"index\": \"E72DCD2A088C8E1843100A345D636D6A017796037E83B47F2C7E0350B53A8374\",\n" + + " \"owner_funds\": \"28767697713\",\n" + + " \"quality\": \"0.0000004664524\"\n" + + " },\n" + + " {\n" + + " \"Account\": \"rPbMHxs7vy5t6e19tYfqG7XJ6Fog8EPZLk\",\n" + + " \"BookDirectory\": \"DFA3B6DDAB58C7E8E5D944E736DA4B7046C30E4F460FD9DE4E109266A03C5B00\",\n" + + " \"BookNode\": \"0\",\n" + + " \"Flags\": 0,\n" + + " \"LedgerEntryType\": \"Offer\",\n" + + " \"OwnerNode\": \"0\",\n" + + " \"PreviousTxnID\": \"3E8D2F2CC7593C40FF8EC2A5316B7E308473DC90B50CC54AD4D487E4E6515545\",\n" + + " \"PreviousTxnLgrSeq\": 80959227,\n" + + " \"Sequence\": 1400403,\n" + + " \"TakerGets\": \"2500000000\",\n" + + " \"TakerPays\": {\n" + + " \"currency\": \"USD\",\n" + + " \"issuer\": \"rvYAfWj5gh67oV6fW32ZzP3Aw4Eubs59B\",\n" + + " \"value\": \"1166.142275\"\n" + + " },\n" + + " \"index\": \"D17EA19846F265BD1244F2864B85ECDB7C86D462C00C8B658A035358BDDD6DE2\",\n" + + " \"owner_funds\": \"3389276844\",\n" + + " \"quality\": \"0.00000046645691\"\n" + + " }\n" + + " ],\n" + + " \"status\": \"success\"\n" + + " }"; + + assertCanSerializeAndDeserialize(result, json); + } + + @Test + void testJsonWithLedgerCurrentIndex() throws JSONException, JsonProcessingException { + BookOffersResult result = BookOffersResult.builder() + .ledgerCurrentIndex(LedgerIndex.of(UnsignedInteger.valueOf(80960755))) + .status("success") + .addOffers( + BookOffersOffer.builder() + .account(Address.of("rwsixWy8srCoeUMqABYGM3ayZX55jdVZW4")) + .bookDirectory(Hash256.of("DFA3B6DDAB58C7E8E5D944E736DA4B7046C30E4F460FD9DE4E11C37937E08000")) + .bookNode("0") + .expiration(UnsignedInteger.valueOf(772482259)) + .flags(OfferFlags.of(131072)) + .ownerNode("0") + .previousTransactionId(Hash256.of("BAC9508ECC0656A0F2B5D3B02A96F34F812DC0C37D477D14D59393A8DA6DB378")) + .previousTransactionLedgerSequence(UnsignedInteger.valueOf(80670980)) + .sequence(UnsignedInteger.valueOf(80649129)) + .takerGets(XrpCurrencyAmount.ofDrops(37716709495L)) + .takerPays( + IssuedCurrencyAmount.builder() + .currency("USD") + .issuer(Address.of("rvYAfWj5gh67oV6fW32ZzP3Aw4Eubs59B")) + .value("18858.3547475") + .build() + ) + .index(Hash256.of("B3BD47F0A8C545E7AB213938E0801078C004B5C7364E7A789DE3D83BA3B6E6B1")) + .ownerFundsString("41272254") + .qualityString("0.0000005") + .takerGetsFunded(XrpCurrencyAmount.ofDrops(41272254)) + .takerPaysFunded( + IssuedCurrencyAmount.builder() + .currency("USD") + .issuer(Address.of("rvYAfWj5gh67oV6fW32ZzP3Aw4Eubs59B")) + .value("20.636127") + .build() + ) + .build() + ) + .build(); + + String json = "{\n" + + " \"ledger_current_index\": 80960755,\n" + + " \"offers\": [\n" + + " {\n" + + " \"Account\": \"rwsixWy8srCoeUMqABYGM3ayZX55jdVZW4\",\n" + + " \"BookDirectory\": \"DFA3B6DDAB58C7E8E5D944E736DA4B7046C30E4F460FD9DE4E11C37937E08000\",\n" + + " \"BookNode\": \"0\",\n" + + " \"Expiration\": 772482259,\n" + + " \"Flags\": 131072,\n" + + " \"LedgerEntryType\": \"Offer\",\n" + + " \"OwnerNode\": \"0\",\n" + + " \"PreviousTxnID\": \"BAC9508ECC0656A0F2B5D3B02A96F34F812DC0C37D477D14D59393A8DA6DB378\",\n" + + " \"PreviousTxnLgrSeq\": 80670980,\n" + + " \"Sequence\": 80649129,\n" + + " \"TakerGets\": \"37716709495\",\n" + + " \"TakerPays\": {\n" + + " \"currency\": \"USD\",\n" + + " \"issuer\": \"rvYAfWj5gh67oV6fW32ZzP3Aw4Eubs59B\",\n" + + " \"value\": \"18858.3547475\"\n" + + " },\n" + + " \"index\": \"B3BD47F0A8C545E7AB213938E0801078C004B5C7364E7A789DE3D83BA3B6E6B1\",\n" + + " \"owner_funds\": \"41272254\",\n" + + " \"quality\": \"0.0000005\",\n" + + " \"taker_gets_funded\": \"41272254\",\n" + + " \"taker_pays_funded\": {\n" + + " \"currency\": \"USD\",\n" + + " \"issuer\": \"rvYAfWj5gh67oV6fW32ZzP3Aw4Eubs59B\",\n" + + " \"value\": \"20.636127\"\n" + + " }\n" + + " }\n" + + " ],\n" + + " \"status\": \"success\"\n" + + " }"; + + assertCanSerializeAndDeserialize(result, json); + } + + @Test + void testLedgerHashSafe() { + BookOffersResult result = BookOffersResult.builder() + .ledgerHash(Hash256.of("6442396558D5EF9E64441A29A39759B52813B5E18B6AD86A602A36815037B98B")) + .status("success") + .build(); + + assertThat(result.ledgerHash()).isNotEmpty().get().isEqualTo(result.ledgerHashSafe()); + + BookOffersResult resultWithoutHash = BookOffersResult.builder() + .status("success") + .build(); + + assertThatThrownBy(resultWithoutHash::ledgerHashSafe) + .isInstanceOf(IllegalStateException.class) + .hasMessage("Result did not contain a ledgerHash."); + } + + @Test + void testLedgerIndexSafe() { + BookOffersResult result = BookOffersResult.builder() + .ledgerIndex(LedgerIndex.of(UnsignedInteger.valueOf(80960755))) + .status("success") + .build(); + + assertThat(result.ledgerIndex()).isNotEmpty().get().isEqualTo(result.ledgerIndexSafe()); + + BookOffersResult resultWithoutLedgerIndex = BookOffersResult.builder() + .status("success") + .build(); + + assertThatThrownBy(resultWithoutLedgerIndex::ledgerIndexSafe) + .isInstanceOf(IllegalStateException.class) + .hasMessage("Result did not contain a ledgerIndex."); + } + + @Test + void testLedgerCurrentIndexSafe() { + BookOffersResult result = BookOffersResult.builder() + .ledgerCurrentIndex(LedgerIndex.of(UnsignedInteger.valueOf(80960755))) + .status("success") + .build(); + + assertThat(result.ledgerCurrentIndex()).isNotEmpty().get().isEqualTo(result.ledgerCurrentIndexSafe()); + + BookOffersResult resultWithoutLedgerCurrentIndex = BookOffersResult.builder() + .status("success") + .build(); + + assertThatThrownBy(resultWithoutLedgerCurrentIndex::ledgerCurrentIndexSafe) + .isInstanceOf(IllegalStateException.class) + .hasMessage("Result did not contain a ledgerCurrentIndex."); + } +} \ No newline at end of file diff --git a/xrpl4j-core/src/test/java/org/xrpl/xrpl4j/model/ledger/IssueTest.java b/xrpl4j-core/src/test/java/org/xrpl/xrpl4j/model/ledger/IssueTest.java new file mode 100644 index 000000000..94e897a17 --- /dev/null +++ b/xrpl4j-core/src/test/java/org/xrpl/xrpl4j/model/ledger/IssueTest.java @@ -0,0 +1,56 @@ +package org.xrpl.xrpl4j.model.ledger; + +import static org.assertj.core.api.AssertionsForClassTypes.assertThat; + +import com.fasterxml.jackson.core.JsonProcessingException; +import org.json.JSONException; +import org.junit.jupiter.api.Test; +import org.xrpl.xrpl4j.model.AbstractJsonTest; +import org.xrpl.xrpl4j.model.transactions.Address; + +class IssueTest extends AbstractJsonTest { + + @Test + void testXrp() { + assertThat(Issue.XRP.currency()).isEqualTo("XRP"); + assertThat(Issue.XRP.issuer()).isEmpty(); + } + + @Test + void testNonXrp() { + String usd = "USD"; + Address issuer = Address.of("rG1QQv2nh2gr7RCZ1P8YYcBUKCCN633jCn"); + Issue asset = Issue.builder() + .currency(usd) + .issuer(issuer) + .build(); + + assertThat(asset.currency()).isEqualTo(usd); + assertThat(asset.issuer()).isNotEmpty().get().isEqualTo(issuer); + } + + @Test + void testJsonForXrp() throws JSONException, JsonProcessingException { + String json = "{" + + " \"currency\": \"XRP\"" + + "}"; + + assertCanSerializeAndDeserialize(Issue.XRP, json, Issue.class); + } + + @Test + void testJsonForNonXrp() throws JSONException, JsonProcessingException { + String usd = "USD"; + Address issuer = Address.of("rG1QQv2nh2gr7RCZ1P8YYcBUKCCN633jCn"); + Issue asset = Issue.builder() + .currency(usd) + .issuer(issuer) + .build(); + String json = "{" + + " \"currency\": \"" + usd + "\"," + + " \"issuer\": \"" + asset.issuer().get().value() + "\"" + + "}"; + + assertCanSerializeAndDeserialize(asset, json, Issue.class); + } +} \ No newline at end of file diff --git a/xrpl4j-integration-tests/src/test/java/org/xrpl/xrpl4j/tests/OfferIT.java b/xrpl4j-integration-tests/src/test/java/org/xrpl/xrpl4j/tests/OfferIT.java index b87c1f934..6b54508a8 100644 --- a/xrpl4j-integration-tests/src/test/java/org/xrpl/xrpl4j/tests/OfferIT.java +++ b/xrpl4j-integration-tests/src/test/java/org/xrpl/xrpl4j/tests/OfferIT.java @@ -33,13 +33,19 @@ import org.xrpl.xrpl4j.crypto.keys.KeyPair; import org.xrpl.xrpl4j.crypto.signing.SingleSignedTransaction; 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.path.BookOffersRequestParams; +import org.xrpl.xrpl4j.model.client.path.BookOffersResult; import org.xrpl.xrpl4j.model.client.transactions.SubmitResult; import org.xrpl.xrpl4j.model.flags.OfferCreateFlags; +import org.xrpl.xrpl4j.model.flags.OfferFlags; +import org.xrpl.xrpl4j.model.ledger.Issue; import org.xrpl.xrpl4j.model.ledger.OfferObject; import org.xrpl.xrpl4j.model.ledger.RippleStateObject; import org.xrpl.xrpl4j.model.transactions.Address; +import org.xrpl.xrpl4j.model.transactions.ImmutableIssuedCurrencyAmount; import org.xrpl.xrpl4j.model.transactions.IssuedCurrencyAmount; import org.xrpl.xrpl4j.model.transactions.OfferCancel; import org.xrpl.xrpl4j.model.transactions.OfferCreate; @@ -80,18 +86,19 @@ public void ensureUsdIssued() throws JsonRpcClientErrorException, JsonProcessing ////////////////////// // Create an Offer UnsignedInteger sequence = accountInfoResult.accountData().sequence(); + XrpCurrencyAmount takerPays = XrpCurrencyAmount.ofXrp(BigDecimal.valueOf(200.0)); + IssuedCurrencyAmount takerGets = IssuedCurrencyAmount.builder() + .currency("USD") + .issuer(issuerKeyPair.publicKey().deriveAddress()) + .value("100") + .build(); OfferCreate offerCreate = OfferCreate.builder() .account(issuerKeyPair.publicKey().deriveAddress()) .fee(FeeUtils.computeNetworkFees(feeResult).recommendedFee()) .sequence(sequence) .signingPublicKey(issuerKeyPair.publicKey()) - .takerGets(IssuedCurrencyAmount.builder() - .currency("USD") - .issuer(issuerKeyPair.publicKey().deriveAddress()) - .value("100") - .build() - ) - .takerPays(XrpCurrencyAmount.ofXrp(BigDecimal.valueOf(200.0))) + .takerGets(takerGets) + .takerPays(takerPays) .flags(OfferCreateFlags.builder() .tfSell(true) .build()) @@ -109,6 +116,31 @@ public void ensureUsdIssued() throws JsonRpcClientErrorException, JsonProcessing "OfferCreate transaction successful: https://testnet.xrpl.org/transactions/{}", response.transactionResult().hash() ); + + BookOffersResult result = xrplClient.bookOffers( + BookOffersRequestParams.builder() + .taker(offerCreate.account()) + .takerGets( + Issue.builder() + .currency("USD") + .issuer(offerCreate.account()) + .build() + ) + .takerPays(Issue.XRP) + .ledgerSpecifier(LedgerSpecifier.CURRENT) + .build() + ); + + assertThat(result.offers()).asList().hasSize(1); + assertThat(result.offers().get(0).quality()) + .isEqualTo(BigDecimal.valueOf(takerPays.value().longValue()).divide(new BigDecimal(takerGets.value()))); + assertThat(result.offers().get(0).account()).isEqualTo(offerCreate.account()); + assertThat(result.offers().get(0).flags().lsfSell()).isTrue(); + assertThat(result.offers().get(0).flags().lsfPassive()).isFalse(); + assertThat(result.offers().get(0).sequence()).isEqualTo(offerCreate.sequence()); + assertThat(result.offers().get(0).takerPays()).isEqualTo(takerPays); + assertThat(result.offers().get(0).takerGets()).isEqualTo(takerGets); + assertThat(result.offers().get(0).ownerFunds()).isNotEmpty().get().isEqualTo(new BigDecimal(takerGets.value())); usdIssued = true; }