Skip to content

Commit c3aca0f

Browse files
authored
Support XRPFees amendment (#508)
* adjust SetFee to work with post-XRPFees amendment format * re introduce custom Deserializer for BaseFee/BaseFeeDrops because BaseFee is hex and BaseFeeDrops is decimal * Update Javadoc and comments
1 parent f9020da commit c3aca0f

File tree

5 files changed

+244
-82
lines changed

5 files changed

+244
-82
lines changed

xrpl4j-core/src/main/java/org/xrpl/xrpl4j/model/client/ledger/LedgerEntryResult.java

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -26,6 +26,8 @@ public interface LedgerEntryResult<T extends LedgerObject> extends XrplResult {
2626
/**
2727
* Construct a {@code LedgerEntryResult} builder.
2828
*
29+
* @param <T> The type of {@link LedgerObject}.
30+
*
2931
* @return An {@link ImmutableLedgerEntryResult.Builder}.
3032
*/
3133
static <T extends LedgerObject> ImmutableLedgerEntryResult.Builder<T> builder() {
Lines changed: 61 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,61 @@
1+
package org.xrpl.xrpl4j.model.jackson.modules;
2+
3+
/*-
4+
* ========================LICENSE_START=================================
5+
* xrpl4j :: model
6+
* %%
7+
* Copyright (C) 2020 - 2022 XRPL Foundation and its contributors
8+
* %%
9+
* Licensed under the Apache License, Version 2.0 (the "License");
10+
* you may not use this file except in compliance with the License.
11+
* You may obtain a copy of the License at
12+
*
13+
* http://www.apache.org/licenses/LICENSE-2.0
14+
*
15+
* Unless required by applicable law or agreed to in writing, software
16+
* distributed under the License is distributed on an "AS IS" BASIS,
17+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
18+
* See the License for the specific language governing permissions and
19+
* limitations under the License.
20+
* =========================LICENSE_END==================================
21+
*/
22+
23+
import com.fasterxml.jackson.core.JsonParser;
24+
import com.fasterxml.jackson.databind.DeserializationContext;
25+
import com.fasterxml.jackson.databind.deser.std.StdDeserializer;
26+
import com.google.common.primitives.UnsignedLong;
27+
import org.xrpl.xrpl4j.model.transactions.SetFee;
28+
import org.xrpl.xrpl4j.model.transactions.XrpCurrencyAmount;
29+
30+
import java.io.IOException;
31+
32+
/**
33+
* Custom Jackson deserializer for {@link XrpCurrencyAmount} instances found in {@link SetFee}.
34+
*
35+
* <p>Before the <a href="https://xrpl.org/resources/known-amendments/#xrpfees">XRPFees amendment</a>, a {@link SetFee}
36+
* transaction serializes its `BaseFee` to a hex string. After the
37+
* <a href="https://xrpl.org/resources/known-amendments/#xrpfees">XRPFees amendment</a>, a {@link SetFee} transaction
38+
* serializes its `BaseFee` to a decimal string.
39+
*
40+
* @see "https://xrpl.org/resources/known-amendments/#xrpfees"
41+
*/
42+
public class BaseFeeDropsDeserializer extends StdDeserializer<XrpCurrencyAmount> {
43+
44+
/**
45+
* No-args constructor.
46+
*/
47+
public BaseFeeDropsDeserializer() {
48+
super(XrpCurrencyAmount.class);
49+
}
50+
51+
@Override
52+
public XrpCurrencyAmount deserialize(JsonParser jsonParser, DeserializationContext ctxt) throws IOException {
53+
// Pre-XRPFees, SetFee transactions serialize `BaseFee` to a hex string. Post XRPFees SetFee transactions
54+
// have a `BaseFeeDrops` field which is a decimal string.
55+
if (jsonParser.currentName().equals("BaseFee")) {
56+
return XrpCurrencyAmount.of(UnsignedLong.valueOf(jsonParser.getText(), 16));
57+
} else {
58+
return XrpCurrencyAmount.ofDrops(jsonParser.getValueAsLong());
59+
}
60+
}
61+
}

xrpl4j-core/src/main/java/org/xrpl/xrpl4j/model/transactions/SetFee.java

Lines changed: 73 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -9,9 +9,9 @@
99
* Licensed under the Apache License, Version 2.0 (the "License");
1010
* you may not use this file except in compliance with the License.
1111
* You may obtain a copy of the License at
12-
*
12+
*
1313
* http://www.apache.org/licenses/LICENSE-2.0
14-
*
14+
*
1515
* Unless required by applicable law or agreed to in writing, software
1616
* distributed under the License is distributed on an "AS IS" BASIS,
1717
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
@@ -20,12 +20,15 @@
2020
* =========================LICENSE_END==================================
2121
*/
2222

23+
import com.fasterxml.jackson.annotation.JsonAlias;
24+
import com.fasterxml.jackson.annotation.JsonIgnore;
2325
import com.fasterxml.jackson.annotation.JsonProperty;
2426
import com.fasterxml.jackson.databind.annotation.JsonDeserialize;
2527
import com.fasterxml.jackson.databind.annotation.JsonSerialize;
2628
import com.google.common.primitives.UnsignedInteger;
2729
import org.immutables.value.Value;
2830
import org.xrpl.xrpl4j.model.client.common.LedgerIndex;
31+
import org.xrpl.xrpl4j.model.jackson.modules.BaseFeeDropsDeserializer;
2932

3033
import java.util.Optional;
3134

@@ -53,34 +56,92 @@ static ImmutableSetFee.Builder builder() {
5356
* The charge, in drops of XRP, for the reference transaction, as hex. (This is the transaction cost before scaling
5457
* for load.)
5558
*
56-
* @return A hex {@link String} basefee value.
59+
* <p>This method only exists for historical purposes. When deserialized from a {@link SetFee} transaction from
60+
* ledgers prior to the {@code XRPFees} amendment, this field will still be set based on {@link #baseFeeDrops()}.
61+
*
62+
* @return A hex {@link String} baseFee value.
63+
*/
64+
@Value.Derived
65+
@JsonIgnore
66+
default String baseFee() {
67+
return baseFeeDrops().value().toString(16);
68+
}
69+
70+
/**
71+
* The charge, in drops of XRP, for the reference transaction (This is the transaction cost before scaling for load).
72+
*
73+
* <p>This field will also be populated with the {@code BaseFee} value from any {@link SetFee} transactions
74+
* that occurred before the XRPFees amendment took effect.</p>
75+
*
76+
* @return An {@link XrpCurrencyAmount}.
5777
*/
58-
@JsonProperty("BaseFee")
59-
String baseFee();
78+
@JsonProperty("BaseFeeDrops")
79+
@JsonAlias("BaseFee")
80+
@JsonDeserialize(using = BaseFeeDropsDeserializer.class)
81+
XrpCurrencyAmount baseFeeDrops();
6082

6183
/**
62-
* The cost, in fee units, of the reference transaction.
84+
* The cost, in fee units, of the reference transaction. {@link SetFee} transactions deserialized from ledgers prior
85+
* to the {@code XRPFees} amendment will always have this field, but transactions deserialized from ledgers post
86+
* {@code XRPFees} activation will never have this field.
6387
*
6488
* @return An {@link UnsignedInteger} cost of ref transaction.
6589
*/
6690
@JsonProperty("ReferenceFeeUnits")
67-
UnsignedInteger referenceFeeUnits();
91+
Optional<UnsignedInteger> referenceFeeUnits();
6892

6993
/**
7094
* The base reserve, in drops.
7195
*
72-
* @return An {@link UnsignedInteger} base reverse value in {@link org.xrpl.xrpl4j.model.client.fees.FeeDrops}.
96+
* <p>This method only exists for historical purposes. When deserialized from a {@link SetFee} transaction from
97+
* ledgers prior to the {@code XRPFees} amendment, this field will still be set based on {@link #reserveBaseDrops()}}.
98+
*
99+
* @return An {@link UnsignedInteger} base reserve value in {@link org.xrpl.xrpl4j.model.client.fees.FeeDrops}.
73100
*/
74-
@JsonProperty("ReserveBase")
75-
UnsignedInteger reserveBase();
101+
@Value.Derived
102+
@JsonIgnore
103+
default UnsignedInteger reserveBase() {
104+
return UnsignedInteger.valueOf(reserveBaseDrops().value().longValue());
105+
}
106+
107+
/**
108+
* The base reserve, as an {@link XrpCurrencyAmount}.
109+
*
110+
* <p>This field will also be populated with the {@code ReserveBase} value from any {@link SetFee} transactions
111+
* that occurred before the XRPFees amendment took effect.</p>
112+
*
113+
* @return An {@link XrpCurrencyAmount}.
114+
*/
115+
@JsonProperty("ReserveBaseDrops")
116+
@JsonAlias("ReserveBase")
117+
XrpCurrencyAmount reserveBaseDrops();
76118

77119
/**
78120
* The incremental reserve, in drops.
79121
*
122+
* <p>This method only exists for historical purposes. When deserialized from a {@link SetFee} transaction from
123+
* ledgers prior to the {@code XRPFees} amendment, this field will still be set based on
124+
* {@link #reserveIncrementDrops()}.
125+
*
80126
* @return An {@link UnsignedInteger} incremental reserve in {@link org.xrpl.xrpl4j.model.client.fees.FeeDrops}.
81127
*/
82-
@JsonProperty("ReserveIncrement")
83-
UnsignedInteger reserveIncrement();
128+
@Value.Derived
129+
@JsonIgnore
130+
default UnsignedInteger reserveIncrement() {
131+
return UnsignedInteger.valueOf(reserveIncrementDrops().value().longValue());
132+
}
133+
134+
/**
135+
* The incremental reserve, as an {@link XrpCurrencyAmount}.
136+
*
137+
* <p>This field will also be populated with the {@code ReserveIncrement} value from any {@link SetFee} transactions
138+
* that occurred before the XRPFees amendment took effect.</p>
139+
*
140+
* @return An {@link XrpCurrencyAmount}.
141+
*/
142+
@JsonProperty("ReserveIncrementDrops")
143+
@JsonAlias("ReserveIncrement")
144+
XrpCurrencyAmount reserveIncrementDrops();
84145

85146
/**
86147
* The index of the ledger version where this pseudo-transaction appears. This distinguishes the pseudo-transaction

xrpl4j-core/src/test/java/org/xrpl/xrpl4j/model/transactions/SetFeeTest.java

Lines changed: 108 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -22,25 +22,57 @@
2222

2323
import static org.assertj.core.api.Assertions.assertThat;
2424

25+
import com.fasterxml.jackson.core.JsonProcessingException;
2526
import com.google.common.primitives.UnsignedInteger;
2627
import com.google.common.primitives.UnsignedLong;
28+
import org.json.JSONException;
2729
import org.junit.jupiter.api.Test;
30+
import org.xrpl.xrpl4j.model.AbstractJsonTest;
2831
import org.xrpl.xrpl4j.model.client.common.LedgerIndex;
2932

3033
import java.util.Optional;
3134

32-
public class SetFeeTest {
35+
/**
36+
* Unit tests for {@link SetFee}.
37+
*/
38+
public class SetFeeTest extends AbstractJsonTest {
39+
40+
@Test
41+
public void testConstructWithNoFeeUnits() {
42+
SetFee setFee = SetFee.builder()
43+
.account(Address.of("rrrrrrrrrrrrrrrrrrrrrhoLvTp"))
44+
.fee(XrpCurrencyAmount.ofDrops(12))
45+
.sequence(UnsignedInteger.valueOf(2470665))
46+
.baseFeeDrops(XrpCurrencyAmount.ofDrops(10))
47+
.reserveBaseDrops(XrpCurrencyAmount.ofDrops(20000000))
48+
.reserveIncrementDrops(XrpCurrencyAmount.ofDrops(5000000))
49+
.ledgerSequence(Optional.of(LedgerIndex.of(UnsignedInteger.valueOf(67850752))))
50+
.build();
51+
52+
assertThat(setFee.transactionType()).isEqualTo(TransactionType.SET_FEE);
53+
assertThat(setFee.account()).isEqualTo(Address.of("rrrrrrrrrrrrrrrrrrrrrhoLvTp"));
54+
assertThat(setFee.fee().value()).isEqualTo(UnsignedLong.valueOf(12));
55+
assertThat(setFee.sequence()).isEqualTo(UnsignedInteger.valueOf(2470665));
56+
assertThat(setFee.ledgerSequence()).isNotEmpty().get().isEqualTo(LedgerIndex.of(UnsignedInteger.valueOf(67850752)));
57+
assertThat(setFee.baseFee()).isEqualTo("a");
58+
assertThat(setFee.baseFeeDrops()).isEqualTo(XrpCurrencyAmount.ofDrops(10));
59+
assertThat(setFee.referenceFeeUnits()).isEmpty();
60+
assertThat(setFee.reserveIncrement()).isEqualTo(UnsignedInteger.valueOf(5000000));
61+
assertThat(setFee.reserveIncrementDrops()).isEqualTo(XrpCurrencyAmount.ofDrops(5000000));
62+
assertThat(setFee.reserveBase()).isEqualTo(UnsignedInteger.valueOf(20000000));
63+
assertThat(setFee.reserveBaseDrops()).isEqualTo(XrpCurrencyAmount.ofDrops(20000000));
64+
}
3365

3466
@Test
35-
public void testBuilder() {
67+
public void testConstructWithFeeUnits() {
3668
SetFee setFee = SetFee.builder()
3769
.account(Address.of("rrrrrrrrrrrrrrrrrrrrrhoLvTp"))
3870
.fee(XrpCurrencyAmount.ofDrops(12))
3971
.sequence(UnsignedInteger.valueOf(2470665))
40-
.baseFee("000000000000000A")
72+
.baseFeeDrops(XrpCurrencyAmount.ofDrops(10))
73+
.reserveBaseDrops(XrpCurrencyAmount.ofDrops(20000000))
74+
.reserveIncrementDrops(XrpCurrencyAmount.ofDrops(5000000))
4175
.referenceFeeUnits(UnsignedInteger.valueOf(10))
42-
.reserveBase(UnsignedInteger.valueOf(20000000))
43-
.reserveIncrement(UnsignedInteger.valueOf(5000000))
4476
.ledgerSequence(Optional.of(LedgerIndex.of(UnsignedInteger.valueOf(67850752))))
4577
.build();
4678

@@ -49,8 +81,78 @@ public void testBuilder() {
4981
assertThat(setFee.fee().value()).isEqualTo(UnsignedLong.valueOf(12));
5082
assertThat(setFee.sequence()).isEqualTo(UnsignedInteger.valueOf(2470665));
5183
assertThat(setFee.ledgerSequence()).isNotEmpty().get().isEqualTo(LedgerIndex.of(UnsignedInteger.valueOf(67850752)));
52-
assertThat(setFee.referenceFeeUnits()).isEqualTo(UnsignedInteger.valueOf(10));
84+
assertThat(setFee.baseFee()).isEqualTo("a");
85+
assertThat(setFee.baseFeeDrops()).isEqualTo(XrpCurrencyAmount.ofDrops(10));
86+
assertThat(setFee.referenceFeeUnits()).isNotEmpty().get().isEqualTo(UnsignedInteger.valueOf(10));
5387
assertThat(setFee.reserveIncrement()).isEqualTo(UnsignedInteger.valueOf(5000000));
88+
assertThat(setFee.reserveIncrementDrops()).isEqualTo(XrpCurrencyAmount.ofDrops(5000000));
5489
assertThat(setFee.reserveBase()).isEqualTo(UnsignedInteger.valueOf(20000000));
90+
assertThat(setFee.reserveBaseDrops()).isEqualTo(XrpCurrencyAmount.ofDrops(20000000));
91+
}
92+
93+
@Test
94+
public void testDeserializePreXrpFeesTransaction() throws JsonProcessingException {
95+
SetFee expected = SetFee.builder()
96+
.account(Address.of("rrrrrrrrrrrrrrrrrrrrrhoLvTp"))
97+
.fee(XrpCurrencyAmount.ofDrops(12))
98+
.sequence(UnsignedInteger.valueOf(2470665))
99+
.baseFeeDrops(XrpCurrencyAmount.ofDrops(10))
100+
.referenceFeeUnits(UnsignedInteger.valueOf(10))
101+
.reserveBaseDrops(XrpCurrencyAmount.ofDrops(20000000))
102+
.reserveIncrementDrops(XrpCurrencyAmount.ofDrops(5000000))
103+
.ledgerSequence(Optional.of(LedgerIndex.of(UnsignedInteger.valueOf(67850752))))
104+
.build();
105+
106+
String json = "{" +
107+
"\"Account\":\"rrrrrrrrrrrrrrrrrrrrrhoLvTp\"," +
108+
"\"Fee\":\"12\"," +
109+
"\"LedgerSequence\":67850752," +
110+
"\"Sequence\":2470665," +
111+
"\"SigningPubKey\":\"\"," +
112+
"\"TransactionType\":\"SetFee\"," +
113+
"\"ReserveIncrement\":5000000," +
114+
"\"ReserveBase\":20000000," +
115+
"\"ReferenceFeeUnits\":10," +
116+
"\"BaseFee\":\"a\"}";
117+
118+
Transaction actual = objectMapper.readValue(json, Transaction.class);
119+
assertThat(actual).isEqualTo(expected);
120+
121+
String reserialized = objectMapper.writeValueAsString(actual);
122+
Transaction redeserialized = objectMapper.readValue(reserialized, Transaction.class);
123+
124+
assertThat(redeserialized).isEqualTo(expected);
125+
}
126+
127+
@Test
128+
public void testDeserializePostXrpFeesTransaction() throws JsonProcessingException {
129+
SetFee expected = SetFee.builder()
130+
.account(Address.of("rrrrrrrrrrrrrrrrrrrrrhoLvTp"))
131+
.fee(XrpCurrencyAmount.ofDrops(0))
132+
.sequence(UnsignedInteger.valueOf(0))
133+
.baseFeeDrops(XrpCurrencyAmount.ofDrops(10))
134+
.reserveBaseDrops(XrpCurrencyAmount.ofDrops(10000000))
135+
.reserveIncrementDrops(XrpCurrencyAmount.ofDrops(2000000))
136+
.ledgerSequence(Optional.of(LedgerIndex.of(UnsignedInteger.valueOf(66462465))))
137+
.build();
138+
139+
String json = "{\n" +
140+
" \"Account\": \"rrrrrrrrrrrrrrrrrrrrrhoLvTp\",\n" +
141+
" \"BaseFeeDrops\": \"10\",\n" +
142+
" \"Fee\": \"0\",\n" +
143+
" \"LedgerSequence\": 66462465,\n" +
144+
" \"ReserveBaseDrops\": \"10000000\",\n" +
145+
" \"ReserveIncrementDrops\": \"2000000\",\n" +
146+
" \"Sequence\": 0,\n" +
147+
" \"SigningPubKey\": \"\",\n" +
148+
" \"TransactionType\": \"SetFee\"}";
149+
150+
Transaction actual = objectMapper.readValue(json, Transaction.class);
151+
assertThat(actual).isEqualTo(expected);
152+
153+
String reserialized = objectMapper.writeValueAsString(actual);
154+
Transaction redeserialized = objectMapper.readValue(reserialized, Transaction.class);
155+
156+
assertThat(redeserialized).isEqualTo(expected);
55157
}
56158
}

0 commit comments

Comments
 (0)