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.
2020 * =========================LICENSE_END==================================
2121 */
2222
23+ import com .fasterxml .jackson .annotation .JsonIgnore ;
2324import com .fasterxml .jackson .annotation .JsonProperty ;
2425import com .fasterxml .jackson .databind .annotation .JsonDeserialize ;
2526import com .fasterxml .jackson .databind .annotation .JsonSerialize ;
2627import com .google .common .base .Preconditions ;
28+ import com .google .common .io .BaseEncoding ;
2729import com .google .common .primitives .UnsignedInteger ;
2830import com .google .common .primitives .UnsignedLong ;
2931import com .ripple .cryptoconditions .Condition ;
32+ import com .ripple .cryptoconditions .CryptoConditionReader ;
33+ import com .ripple .cryptoconditions .CryptoConditionWriter ;
3034import com .ripple .cryptoconditions .Fulfillment ;
35+ import com .ripple .cryptoconditions .der .DerEncodingException ;
3136import org .immutables .value .Value ;
37+ import org .slf4j .Logger ;
38+ import org .slf4j .LoggerFactory ;
3239import org .xrpl .xrpl4j .model .flags .TransactionFlags ;
3340import 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 ;
3545import java .util .Objects ;
3646import java .util .Optional ;
3747
4353@ JsonDeserialize (as = ImmutableEscrowFinish .class )
4454public 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