Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Support XRPFees amendment #508

Open
wants to merge 9 commits into
base: main
Choose a base branch
from
Original file line number Diff line number Diff line change
@@ -0,0 +1,60 @@
package org.xrpl.xrpl4j.model.jackson.modules;

/*-
* ========================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.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.XrpCurrencyAmount;

import java.io.IOException;

/**
* Custom Jackson deserializer for {@link XrpCurrencyAmount} instances found in {@link SetFee}.
* <p>
* Before the <a href="https://xrpl.org/resources/known-amendments/#xrpfees">XRPFees amendment</a>, a {@link SetFee}
* transaction serializes its `BaseFee` to a hex string. After the
* <a href="https://xrpl.org/resources/known-amendments/#xrpfees">XRPFees amendment</a>, a {@link SetFee} transaction
* serializes its `BaseFee` to a decimal string.
*
* @see "https://xrpl.org/resources/known-amendments/#xrpfees"
*/
public class BaseFeeDropsDeserializer extends StdDeserializer<XrpCurrencyAmount> {

/**
* No-args constructor.
*/
public BaseFeeDropsDeserializer() {
super(XrpCurrencyAmount.class);
}

@Override
public XrpCurrencyAmount deserialize(JsonParser jsonParser, DeserializationContext ctxt) throws IOException {
// Pre-XRPFees, SetFee transactions serialize `BaseFee` to a hex string. Post XRPFees SetFee transactions
// have a `BaseFeeDrops` field which is a decimal string.
if (jsonParser.currentName().equals("BaseFee")) {
return XrpCurrencyAmount.of(UnsignedLong.valueOf(jsonParser.getText(), 16));
} else {
return XrpCurrencyAmount.ofDrops(jsonParser.getValueAsLong());
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -9,9 +9,9 @@
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
*
* http://www.apache.org/licenses/LICENSE-2.0
*
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
Expand All @@ -20,14 +20,18 @@
* =========================LICENSE_END==================================
*/

import com.fasterxml.jackson.annotation.JsonAlias;
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.primitives.UnsignedInteger;
import org.immutables.value.Value;
import org.xrpl.xrpl4j.model.client.common.LedgerIndex;
import org.xrpl.xrpl4j.model.jackson.modules.BaseFeeDropsDeserializer;

import java.util.Optional;
import javax.annotation.Nullable;

/**
* A {@link SetFee} pseudo-transaction marks a change in transaction cost or reserve requirements as a result of Fee
Expand All @@ -53,34 +57,108 @@ static ImmutableSetFee.Builder builder() {
* The charge, in drops of XRP, for the reference transaction, as hex. (This is the transaction cost before scaling
* for load.)
*
* @return A hex {@link String} basefee value.
* @return A hex {@link String} baseFee value.
*
* @deprecated Prefer {@link #baseFeeDrops()} over this field because the XRPFees ammendment now serializes this
* object's base fee into XRP drops.
*/
@JsonProperty("BaseFee")
String baseFee();
@Value.Default
@Deprecated
@JsonIgnore
default String baseFee() {
return baseFeeDrops().value().toString(16);
}

/**
* The cost, in fee units, of the reference transaction.
* The charge, in drops of XRP, for the reference transaction (This is the transaction cost before scaling for load).
*
* <p>This field will also be populated with the {@code BaseFee} value from any {@link SetFee} transactions
* that occurred before the XRPFees amendment took effect.</p>
*
* @return An {@link XrpCurrencyAmount}.
*/
@JsonProperty("BaseFeeDrops")
@JsonAlias("BaseFee")
@JsonDeserialize(using = BaseFeeDropsDeserializer.class)
XrpCurrencyAmount baseFeeDrops();

/**
* The cost, in fee units, of the reference transaction. This field can be {@code null} if this {@link SetFee}
* transaction was included in a ledger after the XRPFees amendment was enabled because this amendment removes the
* {@code ReferenceFeeUnits} field from the {@link SetFee} transaction.
*
* @return An {@link UnsignedInteger} cost of ref transaction.
*
* @deprecated Prefer {@link #maybeReferenceFeeUnits()} over this field.
*/
@Deprecated
@Nullable
@Value.Default
@JsonIgnore
default UnsignedInteger referenceFeeUnits() {
return maybeReferenceFeeUnits().orElse(null);
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This feels like a breaking change.

E.g., imagine an application using an older version of xrpl4j is calling referenceFeeUnits. Then, that code upgrades to the version of xrpl4j that has this change (but the application usage of xrpl4j isn't changed because there's just a deprecation warning). If that application starts talking to a server with XRPFees amendment enabled, that code will throw a NPE likely.

I need to think more about this one -- unless the argument is that what I wrote above can't happen? Or maybe just is unlikely to happen?

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think you're right. Anyone using this method and not checking for null will get a NPE if XRPFees is enabled. Unfortunately I can't think of a way to support the XRPFees change without making this @Nullable. We could @Value.Default it to UnsignedInteger.ZERO to avoid creating NPE, but that almost seems harder to catch as a developer than getting a bunch of NPE in your logs.

}

/**
* The cost, in fee units, of the reference transaction, or empty if this {@link SetFee} transaction occurred after
* the XRPFees amendment was enabled.
*
* @return An optionally-present {@link UnsignedInteger}.
*/
@JsonProperty("ReferenceFeeUnits")
UnsignedInteger referenceFeeUnits();
Optional<UnsignedInteger> maybeReferenceFeeUnits();

/**
* The base reserve, in drops.
*
* @return An {@link UnsignedInteger} base reverse value in {@link org.xrpl.xrpl4j.model.client.fees.FeeDrops}.
* @return An {@link UnsignedInteger} base reserve value in {@link org.xrpl.xrpl4j.model.client.fees.FeeDrops}.
*
* @deprecated Prefer {@link #reserveBaseDrops()} over this field.
*/
@Value.Default
@Deprecated
@JsonIgnore
default UnsignedInteger reserveBase() {
return UnsignedInteger.valueOf(reserveBaseDrops().value().longValue());
}

/**
* The base reserve, as an {@link XrpCurrencyAmount}.
*
* <p>This field will also be populated with the {@code ReserveBase} value from any {@link SetFee} transactions
* that occurred before the XRPFees amendment took effect.</p>
*
* @return An {@link XrpCurrencyAmount}.
*/
@JsonProperty("ReserveBase")
UnsignedInteger reserveBase();
@JsonProperty("ReserveBaseDrops")
@JsonAlias("ReserveBase")
XrpCurrencyAmount reserveBaseDrops();

/**
* The incremental reserve, in drops.
*
* @return An {@link UnsignedInteger} incremental reserve in {@link org.xrpl.xrpl4j.model.client.fees.FeeDrops}.
*
* @deprecated Prefer {@link #reserveIncrementDrops()} over this field.
*/
@Deprecated
@JsonIgnore
@Value.Default
default UnsignedInteger reserveIncrement() {
return UnsignedInteger.valueOf(reserveIncrementDrops().value().longValue());
}

/**
* The incremental reserve, as an {@link XrpCurrencyAmount}.
*
* <p>This field will also be populated with the {@code ReserveIncrement} value from any {@link SetFee} transactions
* that occurred before the XRPFees amendment took effect.</p>
*
* @return An {@link XrpCurrencyAmount}.
*/
@JsonProperty("ReserveIncrement")
UnsignedInteger reserveIncrement();
@JsonProperty("ReserveIncrementDrops")
@JsonAlias("ReserveIncrement")
XrpCurrencyAmount reserveIncrementDrops();

/**
* The index of the ledger version where this pseudo-transaction appears. This distinguishes the pseudo-transaction
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -22,25 +22,58 @@

import static org.assertj.core.api.Assertions.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.client.common.LedgerIndex;

import java.util.Optional;

public class SetFeeTest {
/**
* Unit tests for {@link SetFee}.
*/
public class SetFeeTest extends AbstractJsonTest {

@Test
public void testConstructWithNoFeeUnits() {
SetFee setFee = SetFee.builder()
.account(Address.of("rrrrrrrrrrrrrrrrrrrrrhoLvTp"))
.fee(XrpCurrencyAmount.ofDrops(12))
.sequence(UnsignedInteger.valueOf(2470665))
.baseFeeDrops(XrpCurrencyAmount.ofDrops(10))
.reserveBaseDrops(XrpCurrencyAmount.ofDrops(20000000))
.reserveIncrementDrops(XrpCurrencyAmount.ofDrops(5000000))
.ledgerSequence(Optional.of(LedgerIndex.of(UnsignedInteger.valueOf(67850752))))
.build();

assertThat(setFee.transactionType()).isEqualTo(TransactionType.SET_FEE);
assertThat(setFee.account()).isEqualTo(Address.of("rrrrrrrrrrrrrrrrrrrrrhoLvTp"));
assertThat(setFee.fee().value()).isEqualTo(UnsignedLong.valueOf(12));
assertThat(setFee.sequence()).isEqualTo(UnsignedInteger.valueOf(2470665));
assertThat(setFee.ledgerSequence()).isNotEmpty().get().isEqualTo(LedgerIndex.of(UnsignedInteger.valueOf(67850752)));
assertThat(setFee.baseFee()).isEqualTo("a");
assertThat(setFee.baseFeeDrops()).isEqualTo(XrpCurrencyAmount.ofDrops(10));
assertThat(setFee.referenceFeeUnits()).isNull();
assertThat(setFee.maybeReferenceFeeUnits()).isEmpty();
assertThat(setFee.reserveIncrement()).isEqualTo(UnsignedInteger.valueOf(5000000));
assertThat(setFee.reserveIncrementDrops()).isEqualTo(XrpCurrencyAmount.ofDrops(5000000));
assertThat(setFee.reserveBase()).isEqualTo(UnsignedInteger.valueOf(20000000));
assertThat(setFee.reserveBaseDrops()).isEqualTo(XrpCurrencyAmount.ofDrops(20000000));
}

@Test
public void testBuilder() {
public void testConstructWithFeeUnits() {
SetFee setFee = SetFee.builder()
.account(Address.of("rrrrrrrrrrrrrrrrrrrrrhoLvTp"))
.fee(XrpCurrencyAmount.ofDrops(12))
.sequence(UnsignedInteger.valueOf(2470665))
.baseFee("000000000000000A")
.referenceFeeUnits(UnsignedInteger.valueOf(10))
.reserveBase(UnsignedInteger.valueOf(20000000))
.reserveIncrement(UnsignedInteger.valueOf(5000000))
.baseFeeDrops(XrpCurrencyAmount.ofDrops(10))
.reserveBaseDrops(XrpCurrencyAmount.ofDrops(20000000))
.reserveIncrementDrops(XrpCurrencyAmount.ofDrops(5000000))
.maybeReferenceFeeUnits(UnsignedInteger.valueOf(10))
.ledgerSequence(Optional.of(LedgerIndex.of(UnsignedInteger.valueOf(67850752))))
.build();

Expand All @@ -49,8 +82,79 @@ public void testBuilder() {
assertThat(setFee.fee().value()).isEqualTo(UnsignedLong.valueOf(12));
assertThat(setFee.sequence()).isEqualTo(UnsignedInteger.valueOf(2470665));
assertThat(setFee.ledgerSequence()).isNotEmpty().get().isEqualTo(LedgerIndex.of(UnsignedInteger.valueOf(67850752)));
assertThat(setFee.referenceFeeUnits()).isEqualTo(UnsignedInteger.valueOf(10));
assertThat(setFee.baseFee()).isEqualTo("a");
assertThat(setFee.baseFeeDrops()).isEqualTo(XrpCurrencyAmount.ofDrops(10));
assertThat(setFee.referenceFeeUnits()).isNotNull().isEqualTo(UnsignedInteger.valueOf(10));
assertThat(setFee.maybeReferenceFeeUnits()).isNotEmpty().get().isEqualTo(UnsignedInteger.valueOf(10));
assertThat(setFee.reserveIncrement()).isEqualTo(UnsignedInteger.valueOf(5000000));
assertThat(setFee.reserveIncrementDrops()).isEqualTo(XrpCurrencyAmount.ofDrops(5000000));
assertThat(setFee.reserveBase()).isEqualTo(UnsignedInteger.valueOf(20000000));
assertThat(setFee.reserveBaseDrops()).isEqualTo(XrpCurrencyAmount.ofDrops(20000000));
}

@Test
public void testDeserializePreXrpFeesTransaction() throws JsonProcessingException {
SetFee expected = SetFee.builder()
.account(Address.of("rrrrrrrrrrrrrrrrrrrrrhoLvTp"))
.fee(XrpCurrencyAmount.ofDrops(12))
.sequence(UnsignedInteger.valueOf(2470665))
.baseFeeDrops(XrpCurrencyAmount.ofDrops(10))
.maybeReferenceFeeUnits(UnsignedInteger.valueOf(10))
.reserveBaseDrops(XrpCurrencyAmount.ofDrops(20000000))
.reserveIncrementDrops(XrpCurrencyAmount.ofDrops(5000000))
.ledgerSequence(Optional.of(LedgerIndex.of(UnsignedInteger.valueOf(67850752))))
.build();

String json = "{" +
"\"Account\":\"rrrrrrrrrrrrrrrrrrrrrhoLvTp\"," +
"\"Fee\":\"12\"," +
"\"LedgerSequence\":67850752," +
"\"Sequence\":2470665," +
"\"SigningPubKey\":\"\"," +
"\"TransactionType\":\"SetFee\"," +
"\"ReserveIncrement\":5000000," +
"\"ReserveBase\":20000000," +
"\"ReferenceFeeUnits\":10," +
"\"BaseFee\":\"a\"}";

Transaction actual = objectMapper.readValue(json, Transaction.class);
assertThat(actual).isEqualTo(expected);

String reserialized = objectMapper.writeValueAsString(actual);
Transaction redeserialized = objectMapper.readValue(reserialized, Transaction.class);

assertThat(redeserialized).isEqualTo(expected);
}

@Test
public void testDeserializePostXrpFeesTransaction() throws JsonProcessingException {
SetFee expected = SetFee.builder()
.account(Address.of("rrrrrrrrrrrrrrrrrrrrrhoLvTp"))
.fee(XrpCurrencyAmount.ofDrops(0))
.sequence(UnsignedInteger.valueOf(0))
.baseFeeDrops(XrpCurrencyAmount.ofDrops(10))
.reserveBaseDrops(XrpCurrencyAmount.ofDrops(10000000))
.reserveIncrementDrops(XrpCurrencyAmount.ofDrops(2000000))
.ledgerSequence(Optional.of(LedgerIndex.of(UnsignedInteger.valueOf(66462465))))
.build();

String json = "{\n" +
" \"Account\": \"rrrrrrrrrrrrrrrrrrrrrhoLvTp\",\n" +
" \"BaseFeeDrops\": \"10\",\n" +
" \"Fee\": \"0\",\n" +
" \"LedgerSequence\": 66462465,\n" +
" \"ReserveBaseDrops\": \"10000000\",\n" +
" \"ReserveIncrementDrops\": \"2000000\",\n" +
" \"Sequence\": 0,\n" +
" \"SigningPubKey\": \"\",\n" +
" \"TransactionType\": \"SetFee\"}";

Transaction actual = objectMapper.readValue(json, Transaction.class);
assertThat(actual).isEqualTo(expected);

String reserialized = objectMapper.writeValueAsString(actual);
Transaction redeserialized = objectMapper.readValue(reserialized, Transaction.class);

assertThat(redeserialized).isEqualTo(expected);
}
}
Loading
Loading