diff --git a/xrpl4j-core/src/main/java/org/xrpl/xrpl4j/model/transactions/EscrowFinish.java b/xrpl4j-core/src/main/java/org/xrpl/xrpl4j/model/transactions/EscrowFinish.java index 379d6a833..9aa5d8464 100644 --- a/xrpl4j-core/src/main/java/org/xrpl/xrpl4j/model/transactions/EscrowFinish.java +++ b/xrpl4j-core/src/main/java/org/xrpl/xrpl4j/model/transactions/EscrowFinish.java @@ -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. @@ -20,18 +20,28 @@ * =========================LICENSE_END================================== */ +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.base.Preconditions; +import com.google.common.io.BaseEncoding; import com.google.common.primitives.UnsignedInteger; import com.google.common.primitives.UnsignedLong; import com.ripple.cryptoconditions.Condition; +import com.ripple.cryptoconditions.CryptoConditionReader; +import com.ripple.cryptoconditions.CryptoConditionWriter; import com.ripple.cryptoconditions.Fulfillment; +import com.ripple.cryptoconditions.der.DerEncodingException; import org.immutables.value.Value; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; import org.xrpl.xrpl4j.model.flags.TransactionFlags; import org.xrpl.xrpl4j.model.immutables.FluentCompareTo; +import org.xrpl.xrpl4j.model.transactions.AccountSet.AccountSetFlag; +import java.util.Arrays; +import java.util.Locale; import java.util.Objects; import java.util.Optional; @@ -43,6 +53,8 @@ @JsonDeserialize(as = ImmutableEscrowFinish.class) public interface EscrowFinish extends Transaction { + Logger logger = LoggerFactory.getLogger(EscrowFinish.class); + /** * Construct a builder for this class. * @@ -62,6 +74,7 @@ static ImmutableEscrowFinish.Builder builder() { * purposes. * * @return An {@link XrpCurrencyAmount} representing the computed fee. + * * @see "https://xrpl.org/escrowfinish.html" */ static XrpCurrencyAmount computeFee(final XrpCurrencyAmount currentLedgerFeeDrops, final Fulfillment fulfillment) { @@ -78,8 +91,8 @@ static XrpCurrencyAmount computeFee(final XrpCurrencyAmount currentLedgerFeeDrop } /** - * Set of {@link TransactionFlags}s for this {@link EscrowFinish}, which only allows the - * {@code tfFullyCanonicalSig} flag, which is deprecated. + * Set of {@link TransactionFlags}s for this {@link EscrowFinish}, which only allows the {@code tfFullyCanonicalSig} + * flag, which is deprecated. * *

The value of the flags cannot be set manually, but exists for JSON serialization/deserialization only and for * proper signature computation in rippled. @@ -111,34 +124,204 @@ default TransactionFlags flags() { /** * Hex value matching the previously-supplied PREIMAGE-SHA-256 crypto-condition of the held payment. * + *

If this field is empty, developers should check if {@link #conditionRawValue()} is also empty. If + * {@link #conditionRawValue()} is present, it means that the {@code "Condition"} field of the transaction was not a + * well-formed crypto-condition but was still present in a transaction on ledger.

+ * * @return An {@link Optional} of type {@link Condition} containing the escrow condition. */ - @JsonProperty("Condition") + @JsonIgnore Optional condition(); /** - * Hex value of the PREIMAGE-SHA-256 crypto-condition fulfillment matching the held payment's {@code condition}. + * The raw, hex-encoded PREIMAGE-SHA-256 crypto-condition of the escrow. + * + *

Developers should prefer setting {@link #condition()} and leaving this field empty when constructing a new + * {@link EscrowFinish}. This field is used to serialize and deserialize the {@code "Condition"} field in JSON, the + * XRPL will sometimes include an {@link EscrowFinish} in its ledger even if the crypto condition is malformed. + * Without this field, xrpl4j would fail to deserialize those transactions, as {@link #condition()} is typed as a + * {@link Condition}, which tries to decode the condition from DER.

+ * + *

Note that a similar field does not exist on {@link EscrowCreate}, + * {@link org.xrpl.xrpl4j.model.ledger.EscrowObject}, or + * {@link org.xrpl.xrpl4j.model.transactions.metadata.MetaEscrowObject} because {@link EscrowCreate}s with + * malformed conditions will never be included in a ledger by the XRPL. Because of this fact, an + * {@link org.xrpl.xrpl4j.model.ledger.EscrowObject} and + * {@link org.xrpl.xrpl4j.model.transactions.metadata.MetaEscrowObject} will also never contain a malformed + * crypto condition.

+ * + * @return An {@link Optional} {@link String} containing the hex-encoded PREIMAGE-SHA-256 condition. + */ + @JsonProperty("Condition") + Optional conditionRawValue(); + + /** + * Hex value of the PREIMAGE-SHA-256 crypto-condition fulfillment matching the held payment's {@link #condition()}. + * + *

If this field is empty, developers should check if {@link #fulfillmentRawValue()} is also empty. If + * {@link #fulfillmentRawValue()} is present, it means that the {@code "Fulfillment"} field of the transaction was not + * a well-formed crypto-condition fulfillment but was still present in a transaction on ledger.

* * @return An {@link Optional} of type {@link Fulfillment} containing the fulfillment for the escrow's condition. */ - @JsonProperty("Fulfillment") + @JsonIgnore Optional> fulfillment(); /** - * Validate fields. + * The raw, hex-encoded value of the PREIMAGE-SHA-256 crypto-condition fulfillment matching the held payment's + * {@link #condition()}. + * + *

Developers should prefer setting {@link #fulfillment()} and leaving this field empty when constructing a new + * {@link EscrowFinish}. This field is used to serialize and deserialize the {@code "Fulfillment"} field in JSON, the + * XRPL will sometimes include an {@link EscrowFinish} in its ledger even if the crypto fulfillment is malformed. + * Without this field, xrpl4j would fail to deserialize those transactions, as {@link #fulfillment()} is typed as a + * {@link Fulfillment}, which tries to decode the fulfillment from DER.

+ * + * @return An {@link Optional} {@link String} containing the hex-encoded PREIMAGE-SHA-256 fulfillment. + */ + @JsonProperty("Fulfillment") + Optional fulfillmentRawValue(); + + /** + * Normalization method to try to get {@link #condition()} and {@link #conditionRawValue()} to match. + * + *

If neither field is present, there is nothing to do.

+ *

If both fields are present, there is nothing to do, but we will check that {@link #condition()}'s + * underlying value equals {@link #conditionRawValue()}.

+ *

If {@link #condition()} is present but {@link #conditionRawValue()} is empty, we set + * {@link #conditionRawValue()} to the underlying value of {@link #condition()}.

+ *

If {@link #condition()} is empty and {@link #conditionRawValue()} is present, we will set + * {@link #condition()} to the {@link Condition} representing the raw condition value, or leave + * {@link #condition()} empty if {@link #conditionRawValue()} is a malformed {@link Condition}.

+ * + * @return A normalized {@link EscrowFinish}. */ @Value.Check - default void check() { - fulfillment().ifPresent(f -> { - UnsignedLong feeInDrops = fee().value(); - Preconditions.checkState(condition().isPresent(), - "If a fulfillment is specified, the corresponding condition must also be specified."); - Preconditions.checkState(FluentCompareTo.is(feeInDrops).greaterThanEqualTo(UnsignedLong.valueOf(330)), - "If a fulfillment is specified, the fee must be set to 330 or greater."); + default EscrowFinish normalizeCondition() { + try { + if (!condition().isPresent() && !conditionRawValue().isPresent()) { + // If both are empty, nothing to do. + return this; + } else if (condition().isPresent() && conditionRawValue().isPresent()) { + // Both will be present if: + // 1. A developer set them both manually (in the builder) + // 2. This method has already been called. + + // We should check that the condition()'s value matches the raw value. + Preconditions.checkState( + Arrays.equals(CryptoConditionWriter.writeCondition(condition().get()), + BaseEncoding.base16().decode(conditionRawValue().get())), + "condition and conditionRawValue should be equivalent if both are present." + ); + return this; + } else if (condition().isPresent() && !conditionRawValue().isPresent()) { + // This can only happen if the developer only set condition() because condition() will never be set + // after deserializing from JSON. In this case, we need to set conditionRawValue to match setFlag. + return EscrowFinish.builder().from(this) + .conditionRawValue(BaseEncoding.base16().encode(CryptoConditionWriter.writeCondition(condition().get()))) + .build(); + } else { // condition is empty and conditionRawValue is present + // This can happen if: + // 1. A developer sets conditionRawValue manually in the builder + // 2. JSON has Condition and Jackson sets conditionRawValue + + // In this case, we should try to read conditionRawValue to a Condition. If that fails, condition() + // will remain empty, otherwise we will set condition(). + try { + Condition condition = CryptoConditionReader.readCondition( + BaseEncoding.base16().decode(conditionRawValue().get().toUpperCase(Locale.US)) + ); + return EscrowFinish.builder().from(this) + .condition(condition) + .build(); + } catch (DerEncodingException | IllegalArgumentException e) { + logger.warn( + "EscrowFinish Condition was malformed. conditionRawValue() will contain the condition value, but " + + "condition() will be empty: {}", + e.getMessage(), + e + ); + return this; + } } - ); - condition().ifPresent($ -> Preconditions.checkState(fulfillment().isPresent(), - "If a condition is specified, the corresponding fulfillment must also be specified.")); + + } catch (DerEncodingException e) { + // This should never happen. CryptoconditionWriter.writeCondition errantly declares that it can throw + // a DerEncodingException, but nowhere in its implementation does it throw. + throw new RuntimeException(e); + } + } + + /** + * Normalization method to try to get {@link #fulfillment()} and {@link #fulfillmentRawValue()} to match. + * + *

If neither field is present, there is nothing to do.

+ *

If both fields are present, there is nothing to do, but we will check that {@link #fulfillment()}'s + * underlying value equals {@link #fulfillmentRawValue()}.

+ *

If {@link #fulfillment()} is present but {@link #fulfillmentRawValue()} is empty, we set + * {@link #fulfillmentRawValue()} to the underlying value of {@link #fulfillment()}.

+ *

If {@link #fulfillment()} is empty and {@link #fulfillmentRawValue()} is present, we will set + * {@link #fulfillment()} to the {@link Fulfillment} representing the raw fulfillment value, or leave + * {@link #fulfillment()} empty if {@link #fulfillmentRawValue()} is a malformed {@link Fulfillment}.

+ * + * @return A normalized {@link EscrowFinish}. + */ + @Value.Check + default EscrowFinish normalizeFulfillment() { + try { + if (!fulfillment().isPresent() && !fulfillmentRawValue().isPresent()) { + // If both are empty, nothing to do. + return this; + } else if (fulfillment().isPresent() && fulfillmentRawValue().isPresent()) { + // Both will be present if: + // 1. A developer set them both manually (in the builder) + // 2. This method has already been called. + + // We should check that the fulfillment()'s value matches the raw value. + Preconditions.checkState( + Arrays.equals(CryptoConditionWriter.writeFulfillment(fulfillment().get()), + BaseEncoding.base16().decode(fulfillmentRawValue().get())), + "fulfillment and fulfillmentRawValue should be equivalent if both are present." + ); + return this; + } else if (fulfillment().isPresent() && !fulfillmentRawValue().isPresent()) { + // This can only happen if the developer only set fulfillment() because fulfillment() will never be set + // after deserializing from JSON. In this case, we need to set fulfillmentRawValue to match setFlag. + return EscrowFinish.builder().from(this) + .fulfillmentRawValue( + BaseEncoding.base16().encode(CryptoConditionWriter.writeFulfillment(fulfillment().get())) + ) + .build(); + } else { // fulfillment is empty and fulfillmentRawValue is present + // This can happen if: + // 1. A developer sets fulfillmentRawValue manually in the builder + // 2. JSON has Condition and Jackson sets fulfillmentRawValue + + // In this case, we should try to read fulfillmentRawValue to a Condition. If that fails, fulfillment() + // will remain empty, otherwise we will set fulfillment(). + try { + Fulfillment fulfillment = CryptoConditionReader.readFulfillment( + BaseEncoding.base16().decode(fulfillmentRawValue().get().toUpperCase(Locale.US)) + ); + return EscrowFinish.builder().from(this) + .fulfillment(fulfillment) + .build(); + } catch (DerEncodingException | IllegalArgumentException e) { + logger.warn( + "EscrowFinish Fulfillment was malformed. fulfillmentRawValue() will contain the fulfillment value, " + + "but fulfillment() will be empty: {}", + e.getMessage(), + e + ); + return this; + } + } + + } catch (DerEncodingException e) { + // This should never happen. CryptoconditionWriter.writeCondition errantly declares that it can throw + // a DerEncodingException, but nowhere in its implementation does it throw. + throw new RuntimeException(e); + } } } diff --git a/xrpl4j-core/src/test/java/org/xrpl/xrpl4j/model/transactions/EscrowFinishTest.java b/xrpl4j-core/src/test/java/org/xrpl/xrpl4j/model/transactions/EscrowFinishTest.java index 804f8005f..232bf6b6b 100644 --- a/xrpl4j-core/src/test/java/org/xrpl/xrpl4j/model/transactions/EscrowFinishTest.java +++ b/xrpl4j-core/src/test/java/org/xrpl/xrpl4j/model/transactions/EscrowFinishTest.java @@ -21,13 +21,21 @@ */ import static org.assertj.core.api.Assertions.assertThat; -import static org.junit.jupiter.api.Assertions.assertThrows; +import static org.assertj.core.api.Assertions.assertThatThrownBy; +import com.fasterxml.jackson.core.JsonProcessingException; +import com.google.common.io.BaseEncoding; import com.google.common.primitives.UnsignedInteger; +import com.ripple.cryptoconditions.Condition; +import com.ripple.cryptoconditions.CryptoConditionReader; import com.ripple.cryptoconditions.Fulfillment; +import com.ripple.cryptoconditions.PreimageSha256Condition; import com.ripple.cryptoconditions.PreimageSha256Fulfillment; +import com.ripple.cryptoconditions.der.DerEncodingException; import org.assertj.core.api.Assertions; +import org.junit.jupiter.api.BeforeAll; import org.junit.jupiter.api.Test; +import org.xrpl.xrpl4j.model.jackson.ObjectMapperFactory; import org.xrpl.xrpl4j.model.transactions.ImmutableEscrowFinish.Builder; /** @@ -35,8 +43,27 @@ */ public class EscrowFinishTest { + public static final String GOOD_CONDITION_STR = + "A0258020E3B0C44298FC1C149AFBF4C8996FB92427AE41E4649B934CA495991B7852B855810100"; + + public static final String GOOD_FULFILLMENT_STR = + "A0028000"; + + private static Condition condition; + private static Fulfillment fulfillment; + + @BeforeAll + static void beforeAll() { + try { + condition = CryptoConditionReader.readCondition(BaseEncoding.base16().decode(GOOD_CONDITION_STR)); + fulfillment = CryptoConditionReader.readFulfillment(BaseEncoding.base16().decode(GOOD_FULFILLMENT_STR)); + } catch (DerEncodingException e) { + throw new RuntimeException(e); + } + } + @Test - public void testNormalizeWithNoFulfillmentNoCondition() { + public void constructWithNoFulfillmentNoCondition() { EscrowFinish actual = EscrowFinish.builder() .fee(XrpCurrencyAmount.ofDrops(1)) .account(Address.of("rvYAfWj5gh67oV6fW32ZzP3Aw4Eubs59Ba")) @@ -46,7 +73,9 @@ public void testNormalizeWithNoFulfillmentNoCondition() { .build(); assertThat(actual.condition()).isNotPresent(); + assertThat(actual.conditionRawValue()).isNotPresent(); assertThat(actual.fulfillment()).isNotPresent(); + assertThat(actual.fulfillmentRawValue()).isNotPresent(); assertThat(actual.fee()).isEqualTo(XrpCurrencyAmount.ofDrops(1)); assertThat(actual.account()).isEqualTo(Address.of("rvYAfWj5gh67oV6fW32ZzP3Aw4Eubs59Ba")); assertThat(actual.sequence()).isEqualTo(UnsignedInteger.ONE); @@ -54,86 +83,220 @@ public void testNormalizeWithNoFulfillmentNoCondition() { assertThat(actual.offerSequence()).isEqualTo(UnsignedInteger.ZERO); } + //////////////////////////////// + // normalizeCondition tests + //////////////////////////////// + + @Test + void normalizeWithNoConditionNoRawValue() { + EscrowFinish actual = EscrowFinish.builder() + .fee(XrpCurrencyAmount.ofDrops(1)) + .account(Address.of("rvYAfWj5gh67oV6fW32ZzP3Aw4Eubs59Ba")) + .sequence(UnsignedInteger.ONE) + .owner(Address.of("rN7n7otQDd6FczFgLdSqtcsAUxDkw6fzRH")) + .offerSequence(UnsignedInteger.ZERO) + .build(); + + assertThat(actual.condition()).isEmpty(); + assertThat(actual.conditionRawValue()).isEmpty(); + } + + @Test + void normalizeWithConditionAndRawValueMatching() { + EscrowFinish actual = EscrowFinish.builder() + .fee(XrpCurrencyAmount.ofDrops(1)) + .account(Address.of("rvYAfWj5gh67oV6fW32ZzP3Aw4Eubs59Ba")) + .sequence(UnsignedInteger.ONE) + .owner(Address.of("rN7n7otQDd6FczFgLdSqtcsAUxDkw6fzRH")) + .offerSequence(UnsignedInteger.ZERO) + .condition(condition) + .conditionRawValue(GOOD_CONDITION_STR) + .build(); + + assertThat(actual.condition()).isNotEmpty().get().isEqualTo(condition); + assertThat(actual.conditionRawValue()).isNotEmpty().get().isEqualTo(GOOD_CONDITION_STR); + } + + @Test + void normalizeWithConditionAndRawValueNonMatching() { + assertThatThrownBy(() -> EscrowFinish.builder() + .fee(XrpCurrencyAmount.ofDrops(1)) + .account(Address.of("rvYAfWj5gh67oV6fW32ZzP3Aw4Eubs59Ba")) + .sequence(UnsignedInteger.ONE) + .owner(Address.of("rN7n7otQDd6FczFgLdSqtcsAUxDkw6fzRH")) + .offerSequence(UnsignedInteger.ZERO) + .condition(condition) + // This is slightly different than GOOD_CONDITION_STR + .conditionRawValue("A0258020E3B0C44298FC1C149ABCD4C8996FB92427AE41E4649B934CA495991B7852B855810100") + .build() + ).isInstanceOf(IllegalStateException.class) + .hasMessage("condition and conditionRawValue should be equivalent if both are present."); + } + @Test - public void testNormalizeWithFulfillmentNoCondition() { - Fulfillment fulfillment = PreimageSha256Fulfillment.from("ssh".getBytes()); - - assertThrows( - IllegalStateException.class, - () -> EscrowFinish.builder() - .fee(XrpCurrencyAmount.ofDrops(1)) - .account(Address.of("rN7n7otQDd6FczFgLdSqtcsAUxDkw6fzRH")) - .sequence(UnsignedInteger.ONE) - .owner(Address.of("rN7n7otQDd6FczFgLdSqtcsAUxDkw6fzRH")) - .offerSequence(UnsignedInteger.ZERO) - .fulfillment(fulfillment) - .build(), - "If a fulfillment is specified, the corresponding condition must also be specified." - ); + void normalizeWithConditionPresentAndNoRawValue() { + EscrowFinish actual = EscrowFinish.builder() + .fee(XrpCurrencyAmount.ofDrops(1)) + .account(Address.of("rvYAfWj5gh67oV6fW32ZzP3Aw4Eubs59Ba")) + .sequence(UnsignedInteger.ONE) + .owner(Address.of("rN7n7otQDd6FczFgLdSqtcsAUxDkw6fzRH")) + .offerSequence(UnsignedInteger.ZERO) + .condition(condition) + .build(); + + assertThat(actual.conditionRawValue()).isNotEmpty().get().isEqualTo(GOOD_CONDITION_STR); } @Test - public void testNormalizeWithNoFulfillmentAndCondition() { - Fulfillment fulfillment = PreimageSha256Fulfillment.from("ssh".getBytes()); - - assertThrows( - IllegalStateException.class, - () -> EscrowFinish.builder() - .fee(XrpCurrencyAmount.ofDrops(1)) - .account(Address.of("rN7n7otQDd6FczFgLdSqtcsAUxDkw6fzRH")) - .sequence(UnsignedInteger.ONE) - .owner(Address.of("rN7n7otQDd6FczFgLdSqtcsAUxDkw6fzRH")) - .offerSequence(UnsignedInteger.ZERO) - .condition(fulfillment.getDerivedCondition()) - .build(), - "If a condition is specified, the corresponding fulfillment must also be specified." - ); + void normalizeWithNoConditionAndRawValueForValidCondition() { + EscrowFinish actual = EscrowFinish.builder() + .fee(XrpCurrencyAmount.ofDrops(1)) + .account(Address.of("rvYAfWj5gh67oV6fW32ZzP3Aw4Eubs59Ba")) + .sequence(UnsignedInteger.ONE) + .owner(Address.of("rN7n7otQDd6FczFgLdSqtcsAUxDkw6fzRH")) + .offerSequence(UnsignedInteger.ZERO) + .conditionRawValue(GOOD_CONDITION_STR) + .build(); + + assertThat(actual.condition()).isNotEmpty().get().isEqualTo(condition); } @Test - public void testNormalizeWithFulfillmentAndConditionButFeeLow() { - // We expect the + void normalizeWithNoConditionAndRawValueForMalformedCondition() { + EscrowFinish actual = EscrowFinish.builder() + .fee(XrpCurrencyAmount.ofDrops(1)) + .account(Address.of("rvYAfWj5gh67oV6fW32ZzP3Aw4Eubs59Ba")) + .sequence(UnsignedInteger.ONE) + .owner(Address.of("rN7n7otQDd6FczFgLdSqtcsAUxDkw6fzRH")) + .offerSequence(UnsignedInteger.ZERO) + .conditionRawValue("1234") + .build(); - Fulfillment fulfillment = PreimageSha256Fulfillment.from("ssh".getBytes()); + assertThat(actual.condition()).isEmpty(); + assertThat(actual.conditionRawValue()).isNotEmpty().get().isEqualTo("1234"); + } + @Test + void normalizeWithNoConditionAndRawValueForBadHexLengthCondition() { EscrowFinish actual = EscrowFinish.builder() - .fee(XrpCurrencyAmount.ofDrops(330)) - .account(Address.of("rN7n7otQDd6FczFgLdSqtcsAUxDkw6fzRH")) + .fee(XrpCurrencyAmount.ofDrops(1)) + .account(Address.of("rvYAfWj5gh67oV6fW32ZzP3Aw4Eubs59Ba")) + .sequence(UnsignedInteger.ONE) + .owner(Address.of("rN7n7otQDd6FczFgLdSqtcsAUxDkw6fzRH")) + .offerSequence(UnsignedInteger.ZERO) + .conditionRawValue("123") + .build(); + + assertThat(actual.condition()).isEmpty(); + assertThat(actual.conditionRawValue()).isNotEmpty().get().isEqualTo("123"); + } + + //////////////////////////////// + // normalizeFulfillment tests + //////////////////////////////// + + @Test + void normalizeWithNoFulfillmentNoRawValue() { + EscrowFinish actual = EscrowFinish.builder() + .fee(XrpCurrencyAmount.ofDrops(1)) + .account(Address.of("rvYAfWj5gh67oV6fW32ZzP3Aw4Eubs59Ba")) + .sequence(UnsignedInteger.ONE) + .owner(Address.of("rN7n7otQDd6FczFgLdSqtcsAUxDkw6fzRH")) + .offerSequence(UnsignedInteger.ZERO) + .build(); + + assertThat(actual.fulfillment()).isEmpty(); + assertThat(actual.fulfillmentRawValue()).isEmpty(); + } + + @Test + void normalizeWithFulfillmentAndRawValueMatching() { + EscrowFinish actual = EscrowFinish.builder() + .fee(XrpCurrencyAmount.ofDrops(1)) + .account(Address.of("rvYAfWj5gh67oV6fW32ZzP3Aw4Eubs59Ba")) .sequence(UnsignedInteger.ONE) .owner(Address.of("rN7n7otQDd6FczFgLdSqtcsAUxDkw6fzRH")) .offerSequence(UnsignedInteger.ZERO) .fulfillment(fulfillment) - .condition(fulfillment.getDerivedCondition()) + .fulfillmentRawValue(GOOD_FULFILLMENT_STR) .build(); - assertThat(actual.condition()).isPresent(); - assertThat(actual.account()).isEqualTo(Address.of("rN7n7otQDd6FczFgLdSqtcsAUxDkw6fzRH")); - assertThat(actual.sequence()).isEqualTo(UnsignedInteger.ONE); - assertThat(actual.owner()).isEqualTo(Address.of("rN7n7otQDd6FczFgLdSqtcsAUxDkw6fzRH")); - assertThat(actual.offerSequence()).isEqualTo(UnsignedInteger.ZERO); + assertThat(actual.fulfillment()).isNotEmpty().get().isEqualTo(fulfillment); + assertThat(actual.fulfillmentRawValue()).isNotEmpty().get().isEqualTo(GOOD_FULFILLMENT_STR); + } - assertThat(actual.fulfillment()).isPresent(); - assertThat(actual.fulfillment().get()).isEqualTo(fulfillment); - assertThat(actual.fee()).isEqualTo(XrpCurrencyAmount.ofDrops(330)); + @Test + void normalizeWithFulfillmentAndRawValueNonMatching() { + assertThatThrownBy(() -> EscrowFinish.builder() + .fee(XrpCurrencyAmount.ofDrops(1)) + .account(Address.of("rvYAfWj5gh67oV6fW32ZzP3Aw4Eubs59Ba")) + .sequence(UnsignedInteger.ONE) + .owner(Address.of("rN7n7otQDd6FczFgLdSqtcsAUxDkw6fzRH")) + .offerSequence(UnsignedInteger.ZERO) + .fulfillment(fulfillment) + // This is slightly different than GOOD_FULFILLMENT_STR + .fulfillmentRawValue("A0011000") + .build() + ).isInstanceOf(IllegalStateException.class) + .hasMessage("fulfillment and fulfillmentRawValue should be equivalent if both are present."); } @Test - public void testNormalizeWithFeeTooLow() { - Fulfillment fulfillment = PreimageSha256Fulfillment.from("ssh".getBytes()); - - assertThrows( - IllegalStateException.class, - () -> EscrowFinish.builder() - .fee(XrpCurrencyAmount.ofDrops(1)) - .account(Address.of("rN7n7otQDd6FczFgLdSqtcsAUxDkw6fzRH")) - .sequence(UnsignedInteger.ONE) - .owner(Address.of("rN7n7otQDd6FczFgLdSqtcsAUxDkw6fzRH")) - .offerSequence(UnsignedInteger.ZERO) - .fulfillment(fulfillment) - .condition(fulfillment.getDerivedCondition()) - .build(), - "If a fulfillment is specified, the fee must be set to 330 or greater." - ); + void normalizeWithFulfillmentPresentAndNoRawValue() { + EscrowFinish actual = EscrowFinish.builder() + .fee(XrpCurrencyAmount.ofDrops(1)) + .account(Address.of("rvYAfWj5gh67oV6fW32ZzP3Aw4Eubs59Ba")) + .sequence(UnsignedInteger.ONE) + .owner(Address.of("rN7n7otQDd6FczFgLdSqtcsAUxDkw6fzRH")) + .offerSequence(UnsignedInteger.ZERO) + .fulfillment(fulfillment) + .build(); + + assertThat(actual.fulfillmentRawValue()).isNotEmpty().get().isEqualTo(GOOD_FULFILLMENT_STR); + } + + @Test + void normalizeWithNoFulfillmentAndRawValueForValidFulfillment() { + EscrowFinish actual = EscrowFinish.builder() + .fee(XrpCurrencyAmount.ofDrops(1)) + .account(Address.of("rvYAfWj5gh67oV6fW32ZzP3Aw4Eubs59Ba")) + .sequence(UnsignedInteger.ONE) + .owner(Address.of("rN7n7otQDd6FczFgLdSqtcsAUxDkw6fzRH")) + .offerSequence(UnsignedInteger.ZERO) + .fulfillmentRawValue(GOOD_FULFILLMENT_STR) + .build(); + + assertThat(actual.fulfillment()).isNotEmpty().get().isEqualTo(fulfillment); + } + + @Test + void normalizeWithNoFulfillmentAndRawValueForMalformedFulfillment() { + EscrowFinish actual = EscrowFinish.builder() + .fee(XrpCurrencyAmount.ofDrops(1)) + .account(Address.of("rvYAfWj5gh67oV6fW32ZzP3Aw4Eubs59Ba")) + .sequence(UnsignedInteger.ONE) + .owner(Address.of("rN7n7otQDd6FczFgLdSqtcsAUxDkw6fzRH")) + .offerSequence(UnsignedInteger.ZERO) + .fulfillmentRawValue("1234") + .build(); + + assertThat(actual.fulfillment()).isEmpty(); + assertThat(actual.fulfillmentRawValue()).isNotEmpty().get().isEqualTo("1234"); + } + + @Test + void normalizeWithNoFulfillmentAndRawValueForBadHexLengthFulfillment() { + EscrowFinish actual = EscrowFinish.builder() + .fee(XrpCurrencyAmount.ofDrops(1)) + .account(Address.of("rvYAfWj5gh67oV6fW32ZzP3Aw4Eubs59Ba")) + .sequence(UnsignedInteger.ONE) + .owner(Address.of("rN7n7otQDd6FczFgLdSqtcsAUxDkw6fzRH")) + .offerSequence(UnsignedInteger.ZERO) + .fulfillmentRawValue("123") + .build(); + + assertThat(actual.fulfillment()).isEmpty(); + assertThat(actual.fulfillmentRawValue()).isNotEmpty().get().isEqualTo("123"); } @Test diff --git a/xrpl4j-core/src/test/java/org/xrpl/xrpl4j/model/transactions/json/EscrowJsonTests.java b/xrpl4j-core/src/test/java/org/xrpl/xrpl4j/model/transactions/json/EscrowJsonTests.java index df7609290..6c7c39f87 100644 --- a/xrpl4j-core/src/test/java/org/xrpl/xrpl4j/model/transactions/json/EscrowJsonTests.java +++ b/xrpl4j-core/src/test/java/org/xrpl/xrpl4j/model/transactions/json/EscrowJsonTests.java @@ -20,14 +20,13 @@ * =========================LICENSE_END================================== */ -import static org.junit.jupiter.api.Assertions.assertThrows; +import static org.assertj.core.api.AssertionsForClassTypes.assertThat; import com.fasterxml.jackson.core.JsonProcessingException; import com.google.common.io.BaseEncoding; import com.google.common.primitives.UnsignedInteger; import com.google.common.primitives.UnsignedLong; import com.ripple.cryptoconditions.CryptoConditionReader; -import com.ripple.cryptoconditions.PreimageSha256Fulfillment; import com.ripple.cryptoconditions.der.DerEncodingException; import org.json.JSONException; import org.junit.jupiter.api.Test; @@ -352,19 +351,46 @@ public void testEscrowFinishJsonWithNonZeroFlags() } @Test - public void testEscrowFinishJsonWithFeeTooLow() { - assertThrows( - IllegalStateException.class, - () -> EscrowFinish.builder() - .account(Address.of("rf1BiGeXwwQoi8Z2ueFYTEXSwuJYfV2Jpn")) - .fee(XrpCurrencyAmount.ofDrops(3)) - .sequence(UnsignedInteger.ONE) - .owner(Address.of("rf1BiGeXwwQoi8Z2ueFYTEXSwuJYfV2Jpn")) - .offerSequence(UnsignedInteger.valueOf(7)) - .condition(PreimageSha256Fulfillment.from(new byte[48]).getDerivedCondition()) - .fulfillment(PreimageSha256Fulfillment.from(new byte[60])) - .build(), - "If a fulfillment is specified, the fee must be set to 330 or greater." - ); + void testEscrowFinishJsonWithMalformedCondition() throws JsonProcessingException { + String json = "{\n" + + " \"Account\": \"rKWZ2fDqE5B9XorAcEQZD46H6HEdJQVNdb\",\n" + + " \"Condition\": \"A02580209423ED2EF4CACA8CA4AFC08D3F5EC60A545FD7A97E66E16EA0E2E2\",\n" + + " \"Fee\": \"563\",\n" + + " \"Fulfillment\": \"A02280203377850F1B3A4322F1562DF6F75D584596ABE5B9C76EEA8301F56CB942ACC69B\",\n" + + " \"LastLedgerSequence\": 40562057,\n" + + " \"OfferSequence\": 40403748,\n" + + " \"Owner\": \"r3iocgQwoGNCYyvvt8xuWv2XYXx7Z2gQqd\",\n" + + " \"Sequence\": 39899485,\n" + + " \"SigningPubKey\": \"ED09285829A011D520A1873A0E2F1014F5D6B66A6DDE6953FC02C8185EAFA6A1B0\",\n" + + " \"TransactionType\": \"EscrowFinish\",\n" + + " \"TxnSignature\": \"A3E64F6B8D1D7C4FBC9663FCD217F86C3529EC2E2F16442DD48D1B66EEE30EAC2CE960A76080F74BC749" + + "8CA7BBFB822BEE9E8F767114D7B54F7403A7CB672501\"\n" + + "}"; + + EscrowFinish escrowFinish = objectMapper.readValue(json, EscrowFinish.class); + assertThat(escrowFinish.condition()).isEmpty(); + assertThat(escrowFinish.conditionRawValue()).isNotEmpty().get() + .isEqualTo("A02580209423ED2EF4CACA8CA4AFC08D3F5EC60A545FD7A97E66E16EA0E2E2"); + } + + @Test + void testEscrowFinishJsonWithMalformedFulfillment() throws JsonProcessingException { + String json = String.format("{\n" + + " \"Account\": \"rf1BiGeXwwQoi8Z2ueFYTEXSwuJYfV2Jpn\",\n" + + " \"TransactionType\": \"EscrowFinish\",\n" + + " \"Owner\": \"rf1BiGeXwwQoi8Z2ueFYTEXSwuJYfV2Jpn\",\n" + + " \"OfferSequence\": 7,\n" + + " \"Condition\": \"A0258020E3B0C44298FC1C149AFBF4C8996FB92427AE41E4649B934CA495991B7852B855810100\",\n" + + " \"Fulfillment\": \"123\",\n" + + " \"Sequence\": 1,\n" + + " \"Flags\": %s,\n" + + " \"SigningPubKey\" : \"02356E89059A75438887F9FEE2056A2890DB82A68353BE9C0C0C8F89C0018B37FC\",\n" + + " \"Fee\": \"330\"\n" + + "}", TransactionFlags.FULLY_CANONICAL_SIG.getValue()); + + EscrowFinish escrowFinish = objectMapper.readValue(json, EscrowFinish.class); + assertThat(escrowFinish.fulfillment()).isEmpty(); + assertThat(escrowFinish.fulfillmentRawValue()).isNotEmpty().get() + .isEqualTo("123"); } }