diff --git a/xrpl4j-core/src/main/java/org/xrpl/xrpl4j/model/transactions/AmmBid.java b/xrpl4j-core/src/main/java/org/xrpl/xrpl4j/model/transactions/AmmBid.java index 24872f827..b52da0635 100644 --- a/xrpl4j-core/src/main/java/org/xrpl/xrpl4j/model/transactions/AmmBid.java +++ b/xrpl4j-core/src/main/java/org/xrpl/xrpl4j/model/transactions/AmmBid.java @@ -68,19 +68,35 @@ default TransactionFlags flags() { * Pay at least this amount for the slot. Setting this value higher makes it harder for others to outbid you. If * omitted, pay the minimum necessary to win the bid. * - * @return An optionally present {@link IssuedCurrencyAmount}. + *

+ * In a well-formed transaction, this field is always an {@link IssuedCurrencyAmount}. However, the XRPL will fail AMM + * transactions that specify {@link XrpCurrencyAmount}s with a {@code tec} error code, which means these malformed + * transactions can be included in validated ledgers. Therefore, this field is typed as a {@link CurrencyAmount} so + * that malformed transactions can be correctly deserialized. See #529 + *

+ * + * @return An optionally present {@link CurrencyAmount}. */ @JsonProperty("BidMin") - Optional bidMin(); + Optional bidMin(); /** * Pay at most this amount for the slot. If the cost to win the bid is higher than this amount, the transaction fails. * If omitted, pay as much as necessary to win the bid. * - * @return An optionally present {@link IssuedCurrencyAmount}. + *

+ * In a well-formed transaction, this field is always an {@link IssuedCurrencyAmount}. However, the XRPL will fail AMM + * transactions that specify {@link XrpCurrencyAmount}s with a {@code tec} error code, which means these malformed + * transactions can be included in validated ledgers. Therefore, this field is typed as a {@link CurrencyAmount} so + * that malformed transactions can be correctly deserialized. See #529 + *

+ * + * @return An optionally present {@link CurrencyAmount}. */ @JsonProperty("BidMax") - Optional bidMax(); + Optional bidMax(); /** * A list of up to 4 additional accounts that you allow to trade at the discounted fee. This cannot include the diff --git a/xrpl4j-core/src/main/java/org/xrpl/xrpl4j/model/transactions/AmmDeposit.java b/xrpl4j-core/src/main/java/org/xrpl/xrpl4j/model/transactions/AmmDeposit.java index 1336e27a6..72bc376c0 100644 --- a/xrpl4j-core/src/main/java/org/xrpl/xrpl4j/model/transactions/AmmDeposit.java +++ b/xrpl4j-core/src/main/java/org/xrpl/xrpl4j/model/transactions/AmmDeposit.java @@ -84,10 +84,18 @@ static ImmutableAmmDeposit.Builder builder() { /** * How many of the AMM's LP Tokens to buy. * - * @return An optionally present {@link IssuedCurrencyAmount}. + *

+ * In a well-formed transaction, this field is always an {@link IssuedCurrencyAmount}. However, the XRPL will fail AMM + * transactions that specify {@link XrpCurrencyAmount}s with a {@code tec} error code, which means these malformed + * transactions can be included in validated ledgers. Therefore, this field is typed as a {@link CurrencyAmount} so + * that malformed transactions can be correctly deserialized. See #529 + *

+ * + * @return An optionally present {@link CurrencyAmount}. */ @JsonProperty("LPTokenOut") - Optional lpTokenOut(); + Optional lpTokenOut(); /** * An optional {@link TradingFee} to set on the AMM instance. This field is only honored if the AMM's LP token balance diff --git a/xrpl4j-core/src/main/java/org/xrpl/xrpl4j/model/transactions/AmmWithdraw.java b/xrpl4j-core/src/main/java/org/xrpl/xrpl4j/model/transactions/AmmWithdraw.java index 3e0998fde..89ad06bec 100644 --- a/xrpl4j-core/src/main/java/org/xrpl/xrpl4j/model/transactions/AmmWithdraw.java +++ b/xrpl4j-core/src/main/java/org/xrpl/xrpl4j/model/transactions/AmmWithdraw.java @@ -86,9 +86,17 @@ static ImmutableAmmWithdraw.Builder builder() { /** * How many of the AMM's LP Tokens to buy. * + *

+ * In a well-formed transaction, this field is always an {@link IssuedCurrencyAmount}. However, the XRPL will fail AMM + * transactions that specify {@link XrpCurrencyAmount}s with a {@code tec} error code, which means these malformed + * transactions can be included in validated ledgers. Therefore, this field is typed as a {@link CurrencyAmount} so + * that malformed transactions can be correctly deserialized. See #529 + *

+ * * @return An optionally present {@link IssuedCurrencyAmount}. */ @JsonProperty("LPTokensIn") - Optional lpTokensIn(); + Optional lpTokensIn(); } diff --git a/xrpl4j-core/src/test/java/org/xrpl/xrpl4j/model/transactions/AmmBidTest.java b/xrpl4j-core/src/test/java/org/xrpl/xrpl4j/model/transactions/AmmBidTest.java index 91f51139b..0975e3aa0 100644 --- a/xrpl4j-core/src/test/java/org/xrpl/xrpl4j/model/transactions/AmmBidTest.java +++ b/xrpl4j-core/src/test/java/org/xrpl/xrpl4j/model/transactions/AmmBidTest.java @@ -5,6 +5,7 @@ import org.json.JSONException; import org.junit.jupiter.api.Test; import org.xrpl.xrpl4j.crypto.keys.PublicKey; +import org.xrpl.xrpl4j.crypto.signing.Signature; import org.xrpl.xrpl4j.model.AbstractJsonTest; import org.xrpl.xrpl4j.model.flags.TransactionFlags; import org.xrpl.xrpl4j.model.ledger.AuthAccount; @@ -249,4 +250,66 @@ void testJsonWithMinAndMax() throws JSONException, JsonProcessingException { assertCanSerializeAndDeserialize(bid, json); } + + /** + * Test that ensures the problematic transaction found in #529 is deserializable. + */ + @Test + void testJsonWithXrpAmountBidMinAndMax() throws JSONException, JsonProcessingException { + AmmBid ammBid = AmmBid.builder() + .account(Address.of("rammersz4CroiyvbkzeZN1sBDCK9P8DvxF")) + .asset(Issue.XRP) + .asset2( + Issue.builder() + .issuer(Address.of("rswh1fvyLqHizBS2awu1vs6QcmwTBd9qiv")) + .currency("XAH") + .build() + ) + .addAuthAccounts( + AuthAccountWrapper.of( + AuthAccount.of(Address.of("rapido5rxPmP4YkMZZEeXSHqWefxHEkqv6")) + ) + ) + .bidMax(XrpCurrencyAmount.ofDrops(10)) + .bidMin(XrpCurrencyAmount.ofDrops(10)) + .fee(XrpCurrencyAmount.ofDrops(10)) + .flags(TransactionFlags.FULLY_CANONICAL_SIG) + .sequence(UnsignedInteger.valueOf(87704195)) + .signingPublicKey( + PublicKey.fromBase16EncodedPublicKey("ED2D15BC6B61D6520011E4C794C5B320E584106154D0865BB095D70DA9A2A57B57") + ) + .transactionSignature( + Signature.fromBase16("F652BD5369F6EE9A8A1490BD37B8240CEE2B4B6EF94D22EC2DBB6912AA729B829" + + "FC3D7E24B30A1E6CC11F868CE229B105398719152B9BEE8992A56D654F79C0A") + ) + .build(); + String json = "{\n" + + " \"Account\": \"rammersz4CroiyvbkzeZN1sBDCK9P8DvxF\",\n" + + " \"Asset\": {\n" + + " \"currency\": \"XRP\"\n" + + " },\n" + + " \"Asset2\": {\n" + + " \"currency\": \"XAH\",\n" + + " \"issuer\": \"rswh1fvyLqHizBS2awu1vs6QcmwTBd9qiv\"\n" + + " },\n" + + " \"AuthAccounts\": [\n" + + " {\n" + + " \"AuthAccount\": {\n" + + " \"Account\": \"rapido5rxPmP4YkMZZEeXSHqWefxHEkqv6\"\n" + + " }\n" + + " }\n" + + " ],\n" + + " \"BidMax\": \"10\",\n" + + " \"BidMin\": \"10\",\n" + + " \"Fee\": \"10\",\n" + + " \"Flags\": 2147483648,\n" + + " \"Sequence\": 87704195,\n" + + " \"SigningPubKey\": \"ED2D15BC6B61D6520011E4C794C5B320E584106154D0865BB095D70DA9A2A57B57\",\n" + + " \"TransactionType\": \"AMMBid\",\n" + + " \"TxnSignature\": \"F652BD5369F6EE9A8A1490BD37B8240CEE2B4B6EF94D22EC2DBB6912AA729B829FC3D7E24B30A" + + "1E6CC11F868CE229B105398719152B9BEE8992A56D654F79C0A\"\n" + + "}"; + assertCanSerializeAndDeserialize(ammBid, json); + } } \ No newline at end of file diff --git a/xrpl4j-core/src/test/java/org/xrpl/xrpl4j/model/transactions/AmmDepositTest.java b/xrpl4j-core/src/test/java/org/xrpl/xrpl4j/model/transactions/AmmDepositTest.java index 9f4c5c1f9..004816e7d 100644 --- a/xrpl4j-core/src/test/java/org/xrpl/xrpl4j/model/transactions/AmmDepositTest.java +++ b/xrpl4j-core/src/test/java/org/xrpl/xrpl4j/model/transactions/AmmDepositTest.java @@ -70,6 +70,47 @@ void constructLpTokenDepositAndTestJson() throws JSONException, JsonProcessingEx assertCanSerializeAndDeserialize(deposit, json); } + @Test + void constructLpTokenDepositWithXrpLpTokenAmountAndTestJson() throws JSONException, JsonProcessingException { + AmmDeposit deposit = AmmDeposit.builder() + .account(Address.of("rJVUeRqDFNs2xqA7ncVE6ZoAhPUoaJJSQm")) + .fee(XrpCurrencyAmount.ofDrops(10)) + .signingPublicKey( + PublicKey.fromBase16EncodedPublicKey("02356E89059A75438887F9FEE2056A2890DB82A68353BE9C0C0C8F89C0018B37FC") + ) + .flags(AmmDepositFlags.LP_TOKEN) + .asset(Issue.XRP) + .asset2( + Issue.builder() + .issuer(Address.of("rP9jPyP5kyvFRb6ZiRghAGw5u8SGAmU4bd")) + .currency("TST") + .build() + ) + .lpTokenOut(XrpCurrencyAmount.ofDrops(10)) + .build(); + + assertThat(deposit.flags()).isEqualTo(AmmDepositFlags.LP_TOKEN); + + String json = "{\n" + + " \"Account\" : \"" + deposit.account() + "\",\n" + + " \"LPTokenOut\" : \"10\",\n" + + " \"Asset2\" : {\n" + + " \"currency\" : \"TST\",\n" + + " \"issuer\" : \"rP9jPyP5kyvFRb6ZiRghAGw5u8SGAmU4bd\"\n" + + " },\n" + + " \"Asset\" : {\n" + + " \"currency\" : \"XRP\"\n" + + " },\n" + + " \"Fee\" : \"10\",\n" + + " \"Flags\" : " + AmmDepositFlags.LP_TOKEN + ",\n" + + " \"Sequence\" : 0,\n" + + " \"SigningPubKey\" : \"02356E89059A75438887F9FEE2056A2890DB82A68353BE9C0C0C8F89C0018B37FC\",\n" + + " \"TransactionType\" : \"AMMDeposit\"\n" + + "}"; + + assertCanSerializeAndDeserialize(deposit, json); + } + @Test void constructTwoAssetDepositAndTestJson() throws JSONException, JsonProcessingException { AmmDeposit deposit = AmmDeposit.builder() diff --git a/xrpl4j-core/src/test/java/org/xrpl/xrpl4j/model/transactions/AmmWithdrawTest.java b/xrpl4j-core/src/test/java/org/xrpl/xrpl4j/model/transactions/AmmWithdrawTest.java index 493af7fe3..8f89e64ff 100644 --- a/xrpl4j-core/src/test/java/org/xrpl/xrpl4j/model/transactions/AmmWithdrawTest.java +++ b/xrpl4j-core/src/test/java/org/xrpl/xrpl4j/model/transactions/AmmWithdrawTest.java @@ -34,6 +34,28 @@ void constructLpTokenWithdrawAndTestJson() throws JSONException, JsonProcessingE assertCanSerializeAndDeserialize(withdraw, json); } + @Test + void constructLpTokenWithdrawWithXrpCurrencyAmountAndTestJson() throws JSONException, JsonProcessingException { + AmmWithdraw withdraw = baseBuilder() + .flags(AmmWithdrawFlags.LP_TOKEN) + .lpTokensIn(XrpCurrencyAmount.ofDrops(10)) + .build(); + + String json = "{\n" + + " \"Account\" : \"rJVUeRqDFNs2xqA7ncVE6ZoAhPUoaJJSQm\",\n" + + " \"LPTokensIn\" : \"10\"," + + " \"Asset\" : " + objectMapper.writeValueAsString(withdraw.asset()) + "," + + " \"Asset2\" : " + objectMapper.writeValueAsString(withdraw.asset2()) + "," + + " \"Fee\" : \"10\",\n" + + " \"Flags\" : " + AmmWithdrawFlags.LP_TOKEN + ",\n" + + " \"Sequence\" : 0,\n" + + " \"SigningPubKey\" : \"02356E89059A75438887F9FEE2056A2890DB82A68353BE9C0C0C8F89C0018B37FC\",\n" + + " \"TransactionType\" : \"AMMWithdraw\"\n" + + "}"; + + assertCanSerializeAndDeserialize(withdraw, json); + } + @Test void constructWithdrawAllAndTestJson() throws JSONException, JsonProcessingException { AmmWithdraw withdraw = baseBuilder() diff --git a/xrpl4j-integration-tests/src/test/java/org/xrpl/xrpl4j/tests/LedgerResultIT.java b/xrpl4j-integration-tests/src/test/java/org/xrpl/xrpl4j/tests/LedgerResultIT.java index de2809483..cf135b60a 100644 --- a/xrpl4j-integration-tests/src/test/java/org/xrpl/xrpl4j/tests/LedgerResultIT.java +++ b/xrpl4j-integration-tests/src/test/java/org/xrpl/xrpl4j/tests/LedgerResultIT.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. @@ -25,9 +25,17 @@ import org.junit.jupiter.api.Test; import org.xrpl.xrpl4j.client.JsonRpcClientErrorException; +import org.xrpl.xrpl4j.client.XrplClient; import org.xrpl.xrpl4j.model.client.common.LedgerSpecifier; import org.xrpl.xrpl4j.model.client.ledger.LedgerRequestParams; import org.xrpl.xrpl4j.model.client.ledger.LedgerResult; +import org.xrpl.xrpl4j.model.client.transactions.TransactionResult; +import org.xrpl.xrpl4j.model.transactions.AmmBid; +import org.xrpl.xrpl4j.model.transactions.Hash256; +import org.xrpl.xrpl4j.model.transactions.Transaction; +import org.xrpl.xrpl4j.tests.environment.XrplEnvironment; + +import java.util.Optional; /** * These tests ensure {@link LedgerResult}s can be constructed from any JSON responses rippled sends back. @@ -88,4 +96,28 @@ void getClosedLedgerResult() throws JsonRpcClientErrorException { assertThat(ledgerResult.ledger().closeTimeHuman()).isNotEmpty(); assertThat(ledgerResult.ledger().parentCloseTime()).isNotEmpty(); } + + /** + * Pulls down ledger BAA4AF508E9A9CB7685D9E61B82486681E52E9B920904B0650499497F050D8FA, which had an + * {@link org.xrpl.xrpl4j.model.transactions.AmmBid} transaction with an + * {@link org.xrpl.xrpl4j.model.transactions.XrpCurrencyAmount} in the {@link AmmBid#bidMax()} field. + */ + @Test + void canDeserializeLedger_Issue529() throws JsonRpcClientErrorException { + XrplClient mainnetClient = XrplEnvironment.getConfiguredMainnetEnvironment().getXrplClient(); + LedgerResult ledger = mainnetClient.ledger( + LedgerRequestParams.builder() + .transactions(true) + .ledgerSpecifier( + LedgerSpecifier.of(Hash256.of("BAA4AF508E9A9CB7685D9E61B82486681E52E9B920904B0650499497F050D8FA"))) + .build() + ); + + Optional> problematicTransaction = ledger.ledger().transactions().stream() + .filter(transaction -> transaction.hash() + .equals(Hash256.of("6A8BC948E1309219EA8E14905D0EA9BB94E24429DE5B15CDE8916CDBCE42034B"))) + .findFirst(); + + assertThat(problematicTransaction).isNotEmpty(); + } }