Skip to content

Commit a2b0561

Browse files
committed
Merge branch 'main' of github.com:XRPLF/xrpl4j into releases/v3.2
2 parents 9430d3c + c0efe11 commit a2b0561

File tree

3 files changed

+468
-96
lines changed

3 files changed

+468
-96
lines changed

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

Lines changed: 201 additions & 18 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,18 +20,28 @@
2020
* =========================LICENSE_END==================================
2121
*/
2222

23+
import com.fasterxml.jackson.annotation.JsonIgnore;
2324
import com.fasterxml.jackson.annotation.JsonProperty;
2425
import com.fasterxml.jackson.databind.annotation.JsonDeserialize;
2526
import com.fasterxml.jackson.databind.annotation.JsonSerialize;
2627
import com.google.common.base.Preconditions;
28+
import com.google.common.io.BaseEncoding;
2729
import com.google.common.primitives.UnsignedInteger;
2830
import com.google.common.primitives.UnsignedLong;
2931
import com.ripple.cryptoconditions.Condition;
32+
import com.ripple.cryptoconditions.CryptoConditionReader;
33+
import com.ripple.cryptoconditions.CryptoConditionWriter;
3034
import com.ripple.cryptoconditions.Fulfillment;
35+
import com.ripple.cryptoconditions.der.DerEncodingException;
3136
import org.immutables.value.Value;
37+
import org.slf4j.Logger;
38+
import org.slf4j.LoggerFactory;
3239
import org.xrpl.xrpl4j.model.flags.TransactionFlags;
3340
import org.xrpl.xrpl4j.model.immutables.FluentCompareTo;
41+
import org.xrpl.xrpl4j.model.transactions.AccountSet.AccountSetFlag;
3442

43+
import java.util.Arrays;
44+
import java.util.Locale;
3545
import java.util.Objects;
3646
import java.util.Optional;
3747

@@ -43,6 +53,8 @@
4353
@JsonDeserialize(as = ImmutableEscrowFinish.class)
4454
public interface EscrowFinish extends Transaction {
4555

56+
Logger logger = LoggerFactory.getLogger(EscrowFinish.class);
57+
4658
/**
4759
* Construct a builder for this class.
4860
*
@@ -62,6 +74,7 @@ static ImmutableEscrowFinish.Builder builder() {
6274
* purposes.
6375
*
6476
* @return An {@link XrpCurrencyAmount} representing the computed fee.
77+
*
6578
* @see "https://xrpl.org/escrowfinish.html"
6679
*/
6780
static XrpCurrencyAmount computeFee(final XrpCurrencyAmount currentLedgerFeeDrops, final Fulfillment fulfillment) {
@@ -78,8 +91,8 @@ static XrpCurrencyAmount computeFee(final XrpCurrencyAmount currentLedgerFeeDrop
7891
}
7992

8093
/**
81-
* Set of {@link TransactionFlags}s for this {@link EscrowFinish}, which only allows the
82-
* {@code tfFullyCanonicalSig} flag, which is deprecated.
94+
* Set of {@link TransactionFlags}s for this {@link EscrowFinish}, which only allows the {@code tfFullyCanonicalSig}
95+
* flag, which is deprecated.
8396
*
8497
* <p>The value of the flags cannot be set manually, but exists for JSON serialization/deserialization only and for
8598
* proper signature computation in rippled.
@@ -111,34 +124,204 @@ default TransactionFlags flags() {
111124
/**
112125
* Hex value matching the previously-supplied PREIMAGE-SHA-256 crypto-condition of the held payment.
113126
*
127+
* <p>If this field is empty, developers should check if {@link #conditionRawValue()} is also empty. If
128+
* {@link #conditionRawValue()} is present, it means that the {@code "Condition"} field of the transaction was not a
129+
* well-formed crypto-condition but was still present in a transaction on ledger.</p>
130+
*
114131
* @return An {@link Optional} of type {@link Condition} containing the escrow condition.
115132
*/
116-
@JsonProperty("Condition")
133+
@JsonIgnore
117134
Optional<Condition> condition();
118135

119136
/**
120-
* Hex value of the PREIMAGE-SHA-256 crypto-condition fulfillment matching the held payment's {@code condition}.
137+
* The raw, hex-encoded PREIMAGE-SHA-256 crypto-condition of the escrow.
138+
*
139+
* <p>Developers should prefer setting {@link #condition()} and leaving this field empty when constructing a new
140+
* {@link EscrowFinish}. This field is used to serialize and deserialize the {@code "Condition"} field in JSON, the
141+
* XRPL will sometimes include an {@link EscrowFinish} in its ledger even if the crypto condition is malformed.
142+
* Without this field, xrpl4j would fail to deserialize those transactions, as {@link #condition()} is typed as a
143+
* {@link Condition}, which tries to decode the condition from DER.</p>
144+
*
145+
* <p>Note that a similar field does not exist on {@link EscrowCreate},
146+
* {@link org.xrpl.xrpl4j.model.ledger.EscrowObject}, or
147+
* {@link org.xrpl.xrpl4j.model.transactions.metadata.MetaEscrowObject} because {@link EscrowCreate}s with
148+
* malformed conditions will never be included in a ledger by the XRPL. Because of this fact, an
149+
* {@link org.xrpl.xrpl4j.model.ledger.EscrowObject} and
150+
* {@link org.xrpl.xrpl4j.model.transactions.metadata.MetaEscrowObject} will also never contain a malformed
151+
* crypto condition.</p>
152+
*
153+
* @return An {@link Optional} {@link String} containing the hex-encoded PREIMAGE-SHA-256 condition.
154+
*/
155+
@JsonProperty("Condition")
156+
Optional<String> conditionRawValue();
157+
158+
/**
159+
* Hex value of the PREIMAGE-SHA-256 crypto-condition fulfillment matching the held payment's {@link #condition()}.
160+
*
161+
* <p>If this field is empty, developers should check if {@link #fulfillmentRawValue()} is also empty. If
162+
* {@link #fulfillmentRawValue()} is present, it means that the {@code "Fulfillment"} field of the transaction was not
163+
* a well-formed crypto-condition fulfillment but was still present in a transaction on ledger.</p>
121164
*
122165
* @return An {@link Optional} of type {@link Fulfillment} containing the fulfillment for the escrow's condition.
123166
*/
124-
@JsonProperty("Fulfillment")
167+
@JsonIgnore
125168
Optional<Fulfillment<?>> fulfillment();
126169

127170
/**
128-
* Validate fields.
171+
* The raw, hex-encoded value of the PREIMAGE-SHA-256 crypto-condition fulfillment matching the held payment's
172+
* {@link #condition()}.
173+
*
174+
* <p>Developers should prefer setting {@link #fulfillment()} and leaving this field empty when constructing a new
175+
* {@link EscrowFinish}. This field is used to serialize and deserialize the {@code "Fulfillment"} field in JSON, the
176+
* XRPL will sometimes include an {@link EscrowFinish} in its ledger even if the crypto fulfillment is malformed.
177+
* Without this field, xrpl4j would fail to deserialize those transactions, as {@link #fulfillment()} is typed as a
178+
* {@link Fulfillment}, which tries to decode the fulfillment from DER.</p>
179+
*
180+
* @return An {@link Optional} {@link String} containing the hex-encoded PREIMAGE-SHA-256 fulfillment.
181+
*/
182+
@JsonProperty("Fulfillment")
183+
Optional<String> fulfillmentRawValue();
184+
185+
/**
186+
* Normalization method to try to get {@link #condition()} and {@link #conditionRawValue()} to match.
187+
*
188+
* <p>If neither field is present, there is nothing to do.</p>
189+
* <p>If both fields are present, there is nothing to do, but we will check that {@link #condition()}'s
190+
* underlying value equals {@link #conditionRawValue()}.</p>
191+
* <p>If {@link #condition()} is present but {@link #conditionRawValue()} is empty, we set
192+
* {@link #conditionRawValue()} to the underlying value of {@link #condition()}.</p>
193+
* <p>If {@link #condition()} is empty and {@link #conditionRawValue()} is present, we will set
194+
* {@link #condition()} to the {@link Condition} representing the raw condition value, or leave
195+
* {@link #condition()} empty if {@link #conditionRawValue()} is a malformed {@link Condition}.</p>
196+
*
197+
* @return A normalized {@link EscrowFinish}.
129198
*/
130199
@Value.Check
131-
default void check() {
132-
fulfillment().ifPresent(f -> {
133-
UnsignedLong feeInDrops = fee().value();
134-
Preconditions.checkState(condition().isPresent(),
135-
"If a fulfillment is specified, the corresponding condition must also be specified.");
136-
Preconditions.checkState(FluentCompareTo.is(feeInDrops).greaterThanEqualTo(UnsignedLong.valueOf(330)),
137-
"If a fulfillment is specified, the fee must be set to 330 or greater.");
200+
default EscrowFinish normalizeCondition() {
201+
try {
202+
if (!condition().isPresent() && !conditionRawValue().isPresent()) {
203+
// If both are empty, nothing to do.
204+
return this;
205+
} else if (condition().isPresent() && conditionRawValue().isPresent()) {
206+
// Both will be present if:
207+
// 1. A developer set them both manually (in the builder)
208+
// 2. This method has already been called.
209+
210+
// We should check that the condition()'s value matches the raw value.
211+
Preconditions.checkState(
212+
Arrays.equals(CryptoConditionWriter.writeCondition(condition().get()),
213+
BaseEncoding.base16().decode(conditionRawValue().get())),
214+
"condition and conditionRawValue should be equivalent if both are present."
215+
);
216+
return this;
217+
} else if (condition().isPresent() && !conditionRawValue().isPresent()) {
218+
// This can only happen if the developer only set condition() because condition() will never be set
219+
// after deserializing from JSON. In this case, we need to set conditionRawValue to match setFlag.
220+
return EscrowFinish.builder().from(this)
221+
.conditionRawValue(BaseEncoding.base16().encode(CryptoConditionWriter.writeCondition(condition().get())))
222+
.build();
223+
} else { // condition is empty and conditionRawValue is present
224+
// This can happen if:
225+
// 1. A developer sets conditionRawValue manually in the builder
226+
// 2. JSON has Condition and Jackson sets conditionRawValue
227+
228+
// In this case, we should try to read conditionRawValue to a Condition. If that fails, condition()
229+
// will remain empty, otherwise we will set condition().
230+
try {
231+
Condition condition = CryptoConditionReader.readCondition(
232+
BaseEncoding.base16().decode(conditionRawValue().get().toUpperCase(Locale.US))
233+
);
234+
return EscrowFinish.builder().from(this)
235+
.condition(condition)
236+
.build();
237+
} catch (DerEncodingException | IllegalArgumentException e) {
238+
logger.warn(
239+
"EscrowFinish Condition was malformed. conditionRawValue() will contain the condition value, but " +
240+
"condition() will be empty: {}",
241+
e.getMessage(),
242+
e
243+
);
244+
return this;
245+
}
138246
}
139-
);
140-
condition().ifPresent($ -> Preconditions.checkState(fulfillment().isPresent(),
141-
"If a condition is specified, the corresponding fulfillment must also be specified."));
247+
248+
} catch (DerEncodingException e) {
249+
// This should never happen. CryptoconditionWriter.writeCondition errantly declares that it can throw
250+
// a DerEncodingException, but nowhere in its implementation does it throw.
251+
throw new RuntimeException(e);
252+
}
253+
}
254+
255+
/**
256+
* Normalization method to try to get {@link #fulfillment()} and {@link #fulfillmentRawValue()} to match.
257+
*
258+
* <p>If neither field is present, there is nothing to do.</p>
259+
* <p>If both fields are present, there is nothing to do, but we will check that {@link #fulfillment()}'s
260+
* underlying value equals {@link #fulfillmentRawValue()}.</p>
261+
* <p>If {@link #fulfillment()} is present but {@link #fulfillmentRawValue()} is empty, we set
262+
* {@link #fulfillmentRawValue()} to the underlying value of {@link #fulfillment()}.</p>
263+
* <p>If {@link #fulfillment()} is empty and {@link #fulfillmentRawValue()} is present, we will set
264+
* {@link #fulfillment()} to the {@link Fulfillment} representing the raw fulfillment value, or leave
265+
* {@link #fulfillment()} empty if {@link #fulfillmentRawValue()} is a malformed {@link Fulfillment}.</p>
266+
*
267+
* @return A normalized {@link EscrowFinish}.
268+
*/
269+
@Value.Check
270+
default EscrowFinish normalizeFulfillment() {
271+
try {
272+
if (!fulfillment().isPresent() && !fulfillmentRawValue().isPresent()) {
273+
// If both are empty, nothing to do.
274+
return this;
275+
} else if (fulfillment().isPresent() && fulfillmentRawValue().isPresent()) {
276+
// Both will be present if:
277+
// 1. A developer set them both manually (in the builder)
278+
// 2. This method has already been called.
279+
280+
// We should check that the fulfillment()'s value matches the raw value.
281+
Preconditions.checkState(
282+
Arrays.equals(CryptoConditionWriter.writeFulfillment(fulfillment().get()),
283+
BaseEncoding.base16().decode(fulfillmentRawValue().get())),
284+
"fulfillment and fulfillmentRawValue should be equivalent if both are present."
285+
);
286+
return this;
287+
} else if (fulfillment().isPresent() && !fulfillmentRawValue().isPresent()) {
288+
// This can only happen if the developer only set fulfillment() because fulfillment() will never be set
289+
// after deserializing from JSON. In this case, we need to set fulfillmentRawValue to match setFlag.
290+
return EscrowFinish.builder().from(this)
291+
.fulfillmentRawValue(
292+
BaseEncoding.base16().encode(CryptoConditionWriter.writeFulfillment(fulfillment().get()))
293+
)
294+
.build();
295+
} else { // fulfillment is empty and fulfillmentRawValue is present
296+
// This can happen if:
297+
// 1. A developer sets fulfillmentRawValue manually in the builder
298+
// 2. JSON has Condition and Jackson sets fulfillmentRawValue
299+
300+
// In this case, we should try to read fulfillmentRawValue to a Condition. If that fails, fulfillment()
301+
// will remain empty, otherwise we will set fulfillment().
302+
try {
303+
Fulfillment<?> fulfillment = CryptoConditionReader.readFulfillment(
304+
BaseEncoding.base16().decode(fulfillmentRawValue().get().toUpperCase(Locale.US))
305+
);
306+
return EscrowFinish.builder().from(this)
307+
.fulfillment(fulfillment)
308+
.build();
309+
} catch (DerEncodingException | IllegalArgumentException e) {
310+
logger.warn(
311+
"EscrowFinish Fulfillment was malformed. fulfillmentRawValue() will contain the fulfillment value, " +
312+
"but fulfillment() will be empty: {}",
313+
e.getMessage(),
314+
e
315+
);
316+
return this;
317+
}
318+
}
319+
320+
} catch (DerEncodingException e) {
321+
// This should never happen. CryptoconditionWriter.writeCondition errantly declares that it can throw
322+
// a DerEncodingException, but nowhere in its implementation does it throw.
323+
throw new RuntimeException(e);
324+
}
142325
}
143326

144327
}

0 commit comments

Comments
 (0)