Skip to content

Commit 96650dd

Browse files
authored
Allow for No-Op AccountSet SetFlag and ClearFlag values (#479)
* fix AccountSet desrialization for ClearFlags or SetFlags with large numbers * use if-else * fix typos * add comment
1 parent 42047e1 commit 96650dd

File tree

3 files changed

+418
-13
lines changed

3 files changed

+418
-13
lines changed

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

Lines changed: 176 additions & 11 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.
@@ -21,6 +21,7 @@
2121
*/
2222

2323
import com.fasterxml.jackson.annotation.JsonCreator;
24+
import com.fasterxml.jackson.annotation.JsonIgnore;
2425
import com.fasterxml.jackson.annotation.JsonProperty;
2526
import com.fasterxml.jackson.annotation.JsonValue;
2627
import com.fasterxml.jackson.databind.annotation.JsonDeserialize;
@@ -29,7 +30,6 @@
2930
import com.google.common.primitives.UnsignedInteger;
3031
import org.immutables.value.Value;
3132
import org.xrpl.xrpl4j.model.flags.AccountSetTransactionFlags;
32-
import org.xrpl.xrpl4j.model.flags.TransactionFlags;
3333

3434
import java.util.Optional;
3535

@@ -65,25 +65,189 @@ default AccountSetTransactionFlags flags() {
6565
/**
6666
* Unique identifier of a flag to disable for this account.
6767
*
68+
* <p>If this field is empty, developers should check if {@link #clearFlagRawValue()} is also empty. If
69+
* {@link #clearFlagRawValue()} is present, it means that the {@code ClearFlag} field of the transaction was not a
70+
* valid {@link AccountSetFlag} but was still present in a validated transaction on ledger.</p>
71+
*
6872
* <p>Because the preferred way of setting account flags is with {@link AccountSetFlag}s, this field should
6973
* not be set in conjunction with the {@link AccountSet#flags()} field.
7074
*
7175
* @return An {@link Optional} of type {@link AccountSetFlag} representing the flag to disable on this account.
7276
*/
73-
@JsonProperty("ClearFlag")
77+
@JsonIgnore
7478
Optional<AccountSetFlag> clearFlag();
7579

80+
/**
81+
* A flag to disable for this account, as an {@link UnsignedInteger}.
82+
*
83+
* <p>Developers should prefer setting {@link #clearFlag()} and leaving this field empty when constructing
84+
* a new {@link AccountSet}. This field is used to serialize and deserialize the {@code "ClearFlag"} field in JSON,
85+
* as some {@link AccountSet} transactions on the XRPL set the "ClearFlag" field to a number that is not recognized as
86+
* an asf flag by rippled. Without this field, xrpl4j would fail to deserialize those transactions, as
87+
* {@link AccountSetFlag} does not support arbitrary integer values.</p>
88+
*
89+
* <p>Additionally, using this field as the source of truth for JSON serialization/deserialization rather than
90+
* {@link #clearFlag()} allows developers to recompute the hash of a transaction that was deserialized from a rippled
91+
* RPC/WS result accurately. An alternative to this field would be to add an enum variant to {@link AccountSetFlag}
92+
* for unknown values, but binary serializing an {@link AccountSet} that was constructed by deserializing JSON would
93+
* result in a different binary blob than what exists on ledger.</p>
94+
*
95+
* @return An {@link Optional} {@link UnsignedInteger}.
96+
*/
97+
@JsonProperty("ClearFlag")
98+
Optional<UnsignedInteger> clearFlagRawValue();
99+
100+
/**
101+
* Normalization method to try to get {@link #clearFlag()}and {@link #clearFlagRawValue()} to match.
102+
*
103+
* <p>If neither field is present, there is nothing to do.</p>
104+
* <p>If both fields are present, there is nothing to do, but we will check that {@link #clearFlag()}'s
105+
* underlying value equals {@link #clearFlagRawValue()}.</p>
106+
* <p>If {@link #clearFlag()} is present but {@link #clearFlagRawValue()} is empty, we set
107+
* {@link #clearFlagRawValue()} to the underlying value of {@link #clearFlag()}.</p>
108+
* <p>If {@link #clearFlag()} is empty and {@link #clearFlagRawValue()} is present, we will set
109+
* {@link #clearFlag()} to the {@link AccountSetFlag} variant associated with {@link #clearFlagRawValue()}, or leave
110+
* {@link #clearFlag()} empty if {@link #clearFlagRawValue()} does not map to an {@link AccountSetFlag}.</p>
111+
*
112+
* @return A normalized {@link AccountSet}.
113+
*/
114+
@Value.Check
115+
default AccountSet normalizeClearFlag() {
116+
if (!clearFlag().isPresent() && !clearFlagRawValue().isPresent()) {
117+
// If both are empty, nothing to do.
118+
return this;
119+
} else if (clearFlag().isPresent() && clearFlagRawValue().isPresent()) {
120+
// Both will be present if:
121+
// 1. A developer set them both manually (in the builder)
122+
// 2. This normalize method has already been called.
123+
124+
// We should still check that the clearFlagRawValue matches the inner value of AccountSetFlag.
125+
Preconditions.checkState(
126+
clearFlag().get().getValue() == clearFlagRawValue().get().longValue(),
127+
String.format("clearFlag and clearFlagRawValue should be equivalent, but clearFlag's underlying " +
128+
"value was %s and clearFlagRawValue was %s",
129+
clearFlag().get().getValue(),
130+
clearFlagRawValue().get().longValue()
131+
)
132+
);
133+
return this;
134+
} else if (clearFlag().isPresent() && !clearFlagRawValue().isPresent()) {
135+
// This can only happen if the developer only set clearFlag(). In this case, we need to set clearFlagRawValue to
136+
// match clearFlag.
137+
return AccountSet.builder().from(this)
138+
.clearFlagRawValue(UnsignedInteger.valueOf(clearFlag().get().getValue()))
139+
.build();
140+
} else { // clearFlag not present and clearFlagRawValue is present
141+
// This can happen if:
142+
// 1. A developer sets clearFlagRawValue manually in the builder
143+
// 2. JSON has ClearFlag and jackson sets clearFlagRawValue.
144+
// This value will never be negative due to XRPL representing this kind of flag as an unsigned number,
145+
// so no lower bound check is required.
146+
if (clearFlagRawValue().get().longValue() <= 15) {
147+
// Set clearFlag to clearFlagRawValue if clearFlagRawValue matches a valid AccountSetFlag variant.
148+
return AccountSet.builder().from(this)
149+
.clearFlag(AccountSetFlag.forValue(clearFlagRawValue().get().intValue()))
150+
.build();
151+
} else {
152+
// Otherwise, leave clearFlag empty.
153+
return this;
154+
}
155+
}
156+
}
157+
76158
/**
77159
* Unique identifier of a flag to enable for this account.
78160
*
79-
* <p>Because the preferred way of setting account flags is with {@link AccountSetFlag}s, this field should not be set
80-
* in conjunction with the {@link AccountSet#flags()} field.
161+
* <p>If this field is empty, developers should check if {@link #setFlagRawValue()} is also empty. If
162+
* {@link #setFlagRawValue()} is present, it means that the {@code ClearFlag} field of the transaction was not a
163+
* valid {@link AccountSetFlag} but was still present in a validated transaction on ledger.</p>
164+
*
165+
* <p>Because the preferred way of setting account flags is with {@link AccountSetFlag}s, this field should not be
166+
* set in conjunction with the {@link AccountSet#flags()} field.
81167
*
82168
* @return An {@link Optional} of type {@link AccountSetFlag} representing the flag to enable on this account.
83169
*/
84-
@JsonProperty("SetFlag")
170+
@JsonIgnore
85171
Optional<AccountSetFlag> setFlag();
86172

173+
/**
174+
* A flag to disable for this account, as an {@link UnsignedInteger}.
175+
*
176+
* <p>Developers should prefer setting {@link #setFlag()} and leaving this field empty when constructing
177+
* a new {@link AccountSet}. This field is used to serialize and deserialize the {@code "ClearFlag"} field in JSON,
178+
* as some {@link AccountSet} transactions on the XRPL set the "ClearFlag" field to a number that is not recognized as
179+
* an asf flag by rippled. Without this field, xrpl4j would fail to deserialize those transactions, as
180+
* {@link AccountSetFlag} does not support arbitrary integer values.</p>
181+
*
182+
* <p>Additionally, using this field as the source of truth for JSON serialization/deserialization rather than
183+
* {@link #setFlag()} allows developers to recompute the hash of a transaction that was deserialized from a rippled
184+
* RPC/WS result accurately. An alternative to this field would be to add an enum variant to {@link AccountSetFlag}
185+
* for unknown values, but binary serializing an {@link AccountSet} that was constructed by deserializing JSON would
186+
* result in a different binary blob than what exists on ledger.</p>
187+
*
188+
* @return An {@link Optional} {@link UnsignedInteger}
189+
*/
190+
@JsonProperty("SetFlag")
191+
Optional<UnsignedInteger> setFlagRawValue();
192+
193+
/**
194+
* Normalization method to try to get {@link #setFlag()}and {@link #setFlagRawValue()} to match.
195+
*
196+
* <p>If neither field is present, there is nothing to do.</p>
197+
* <p>If both fields are present, there is nothing to do, but we will check that {@link #setFlag()}'s
198+
* underlying value equals {@link #setFlagRawValue()}.</p>
199+
* <p>If {@link #setFlag()} is present but {@link #setFlagRawValue()} is empty, we set
200+
* {@link #setFlagRawValue()} to the underlying value of {@link #setFlag()}.</p>
201+
* <p>If {@link #setFlag()} is empty and {@link #setFlagRawValue()} is present, we will set
202+
* {@link #setFlag()} to the {@link AccountSetFlag} variant associated with {@link #setFlagRawValue()}, or leave
203+
* {@link #setFlag()} empty if {@link #setFlagRawValue()} does not map to an {@link AccountSetFlag}.</p>
204+
*
205+
* @return A normalized {@link AccountSet}.
206+
*/
207+
@Value.Check
208+
default AccountSet normalizeSetFlag() {
209+
if (!setFlag().isPresent() && !setFlagRawValue().isPresent()) {
210+
// If both are empty, nothing to do.
211+
return this;
212+
} else if (setFlag().isPresent() && setFlagRawValue().isPresent()) {
213+
// Both will be present if:
214+
// 1. A developer set them both manually (in the builder)
215+
// 2. This normalize method has already been called.
216+
217+
// We should still check that the setFlagRawValue matches the inner value of AccountSetFlag.
218+
Preconditions.checkState(
219+
setFlag().get().getValue() == setFlagRawValue().get().longValue(),
220+
String.format("setFlag and setFlagRawValue should be equivalent, but setFlag's underlying " +
221+
"value was %s and setFlagRawValue was %s",
222+
setFlag().get().getValue(),
223+
setFlagRawValue().get().longValue()
224+
)
225+
);
226+
return this;
227+
} else if (setFlag().isPresent() && !setFlagRawValue().isPresent()) {
228+
// This can only happen if the developer only set setFlag(). In this case, we need to set setFlagRawValue to
229+
// match setFlag.
230+
return AccountSet.builder().from(this)
231+
.setFlagRawValue(UnsignedInteger.valueOf(setFlag().get().getValue()))
232+
.build();
233+
} else { // setFlag is empty and setFlagRawValue is present
234+
// This can happen if:
235+
// 1. A developer sets setFlagRawValue manually in the builder
236+
// 2. JSON has ClearFlag and jackson sets setFlagRawValue.
237+
// This value will never be negative due to XRPL representing this kind of flag as an unsigned number,
238+
// so no lower bound check is required.
239+
if (setFlagRawValue().get().longValue() <= 15) {
240+
// Set setFlag to setFlagRawValue if setFlagRawValue matches a valid AccountSetFlag variant.
241+
return AccountSet.builder().from(this)
242+
.setFlag(AccountSetFlag.forValue(setFlagRawValue().get().intValue()))
243+
.build();
244+
} else {
245+
// Otherwise, leave setFlag empty.
246+
return this;
247+
}
248+
}
249+
}
250+
87251
/**
88252
* The hex string of the lowercase ASCII of the domain for the account. For example, the domain example.com would be
89253
* represented as "6578616D706C652E636F6D".
@@ -132,8 +296,8 @@ default AccountSetTransactionFlags flags() {
132296
Optional<UnsignedInteger> tickSize();
133297

134298
/**
135-
* Sets an alternate account that is allowed to mint NFTokens on this
136-
* account's behalf using NFTokenMint's `Issuer` field.
299+
* Sets an alternate account that is allowed to mint NFTokens on this account's behalf using NFTokenMint's `Issuer`
300+
* field.
137301
*
138302
* @return An {@link Optional} field of type {@link Address}.
139303
*/
@@ -206,8 +370,8 @@ default void checkTickSize() {
206370
*/
207371
enum AccountSetFlag {
208372
/**
209-
* This flag will do nothing but exists to accurately deserialize AccountSet transactions whose {@code SetFlag}
210-
* or {@code ClearFlag} fields are zero.
373+
* This flag will do nothing but exists to accurately deserialize AccountSet transactions whose {@code SetFlag} or
374+
* {@code ClearFlag} fields are zero.
211375
*/
212376
NONE(0),
213377
/**
@@ -291,6 +455,7 @@ enum AccountSetFlag {
291455
* @param value The int value of the flag.
292456
*
293457
* @return The {@link AccountSetFlag} for the given integer value.
458+
*
294459
* @see "https://github.com/FasterXML/jackson-databind/issues/1850"
295460
*/
296461
@JsonCreator

0 commit comments

Comments
 (0)