diff --git a/xrpl4j-core/src/main/java/org/xrpl/xrpl4j/model/client/accounts/TrustLine.java b/xrpl4j-core/src/main/java/org/xrpl/xrpl4j/model/client/accounts/TrustLine.java index a9cd4c2a2..f8b3df51e 100644 --- a/xrpl4j-core/src/main/java/org/xrpl/xrpl4j/model/client/accounts/TrustLine.java +++ b/xrpl4j-core/src/main/java/org/xrpl/xrpl4j/model/client/accounts/TrustLine.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. @@ -170,4 +170,26 @@ default boolean freezePeer() { return false; } + /** + * Whether or not this account has deep-frozen this trust line. + * + * @return {@code true} if this account has deep-frozen this trust line, otherwise {@code false}. + */ + @JsonProperty("deep_freeze") + @Value.Default + default boolean deepFreeze() { + return false; + } + + /** + * Whether or not the peer account has deep-frozen this trust line. + * + * @return {@code true} if the peer account has deep-frozen this trust line, otherwise {@code false}. + */ + @JsonProperty("deep_freeze_peer") + @Value.Default + default boolean deepFreezePeer() { + return false; + } + } diff --git a/xrpl4j-core/src/main/java/org/xrpl/xrpl4j/model/flags/RippleStateFlags.java b/xrpl4j-core/src/main/java/org/xrpl/xrpl4j/model/flags/RippleStateFlags.java index 40b824a12..0d1ddcc2b 100644 --- a/xrpl4j-core/src/main/java/org/xrpl/xrpl4j/model/flags/RippleStateFlags.java +++ b/xrpl4j-core/src/main/java/org/xrpl/xrpl4j/model/flags/RippleStateFlags.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. @@ -67,6 +67,16 @@ public class RippleStateFlags extends Flags { */ public static final RippleStateFlags HIGH_FREEZE = new RippleStateFlags(0x00800000); + /** + * Constant {@link RippleStateFlags} for the {@code lsfLowDeepFreeze} flag. + */ + public static final RippleStateFlags LOW_DEEP_FREEZE = new RippleStateFlags(0x02000000); + + /** + * Constant {@link RippleStateFlags} for the {@code lsfHighDeepFreeze} flag. + */ + public static final RippleStateFlags HIGH_DEEP_FREEZE = new RippleStateFlags(0x04000000); + private RippleStateFlags(long value) { super(value); } @@ -157,4 +167,22 @@ public boolean lsfLowFreeze() { public boolean lsfHighFreeze() { return this.isSet(HIGH_FREEZE); } + + /** + * The low account has deep-frozen the trust line, preventing the high account from sending and receiving the asset. + * + * @return {@code true} if {@code lsfLowDeepFreeze} is set, otherwise {@code false}. + */ + public boolean lsfLowDeepFreeze() { + return this.isSet(LOW_DEEP_FREEZE); + } + + /** + * The high account has deep-frozen the trust line, preventing the low account from sending and receiving the asset. + * + * @return {@code true} if {@code lsfHighDeepFreeze} is set, otherwise {@code false}. + */ + public boolean lsfHighDeepFreeze() { + return this.isSet(HIGH_DEEP_FREEZE); + } } diff --git a/xrpl4j-core/src/main/java/org/xrpl/xrpl4j/model/flags/TrustSetFlags.java b/xrpl4j-core/src/main/java/org/xrpl/xrpl4j/model/flags/TrustSetFlags.java index 3ffd2330d..d6e64397b 100644 --- a/xrpl4j-core/src/main/java/org/xrpl/xrpl4j/model/flags/TrustSetFlags.java +++ b/xrpl4j-core/src/main/java/org/xrpl/xrpl4j/model/flags/TrustSetFlags.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. @@ -59,6 +59,16 @@ public class TrustSetFlags extends TransactionFlags { */ protected static final TrustSetFlags CLEAR_FREEZE = new TrustSetFlags(0x00200000); + /** + * Constant {@link TrustSetFlags} for the {@code tfSetDeepFreeze} flag. + */ + protected static final TrustSetFlags SET_DEEP_FREEZE = new TrustSetFlags(0x00400000); + + /** + * Constant {@link TrustSetFlags} for the {@code tfClearDeepFreeze} flag. + */ + protected static final TrustSetFlags CLEAR_DEEP_FREEZE = new TrustSetFlags(0x00800000); + private TrustSetFlags(long value) { super(value); } @@ -81,7 +91,9 @@ private static TrustSetFlags of( boolean tfSetNoRipple, boolean tfClearNoRipple, boolean tfSetFreeze, - boolean tfClearFreeze + boolean tfClearFreeze, + boolean tfSetDeepFreeze, + boolean tfClearDeepFreeze ) { return new TrustSetFlags( Flags.of( @@ -90,7 +102,9 @@ private static TrustSetFlags of( tfSetNoRipple ? SET_NO_RIPPLE : UNSET, tfClearNoRipple ? CLEAR_NO_RIPPLE : UNSET, tfSetFreeze ? SET_FREEZE : UNSET, - tfClearFreeze ? CLEAR_FREEZE : UNSET).getValue() + tfClearFreeze ? CLEAR_FREEZE : UNSET, + tfSetDeepFreeze ? SET_DEEP_FREEZE : UNSET, + tfClearDeepFreeze ? CLEAR_DEEP_FREEZE : UNSET).getValue() ); } @@ -171,6 +185,24 @@ public boolean tfClearFreeze() { return this.isSet(CLEAR_FREEZE); } + /** + * Deep freeze the trust line. + * + * @return {@code true} if {@code tfSetDeepFreeze} is set, otherwise {@code false}. + */ + public boolean tfSetDeepFreeze() { + return this.isSet(SET_DEEP_FREEZE); + } + + /** + * Clear deep freeze on the trust line. + * + * @return {@code true} if {@code tfClearDeepFreeze} is set, otherwise {@code false}. + */ + public boolean tfClearDeepFreeze() { + return this.isSet(CLEAR_DEEP_FREEZE); + } + /** * A builder class for {@link TrustSetFlags}. */ @@ -180,6 +212,8 @@ public static class Builder { private boolean tfClearNoRipple = false; private boolean tfSetFreeze = false; private boolean tfClearFreeze = false; + private boolean tfSetDeepFreeze = false; + private boolean tfClearDeepFreeze = false; /** * Set {@code tfSetfAuth} to the given value. @@ -233,6 +267,26 @@ public Builder tfClearFreeze() { return this; } + /** + * Set {@code tfSetDeepFreeze} to {@code true}. + * + * @return The same {@link Builder}. + */ + public Builder tfSetDeepFreeze() { + this.tfSetDeepFreeze = true; + return this; + } + + /** + * Set {@code tfClearDeepFreeze} to {@code true}. + * + * @return The same {@link Builder}. + */ + public Builder tfClearDeepFreeze() { + this.tfClearDeepFreeze = true; + return this; + } + /** * Build a new {@link TrustSetFlags} from the current boolean values. * @@ -245,7 +299,9 @@ public TrustSetFlags build() { tfSetNoRipple, tfClearNoRipple, tfSetFreeze, - tfClearFreeze + tfClearFreeze, + tfSetDeepFreeze, + tfClearDeepFreeze ); } } diff --git a/xrpl4j-core/src/test/java/org/xrpl/xrpl4j/model/client/accounts/AccountLinesResultJsonTests.java b/xrpl4j-core/src/test/java/org/xrpl/xrpl4j/model/client/accounts/AccountLinesResultJsonTests.java index d932edc4a..661eab7b6 100644 --- a/xrpl4j-core/src/test/java/org/xrpl/xrpl4j/model/client/accounts/AccountLinesResultJsonTests.java +++ b/xrpl4j-core/src/test/java/org/xrpl/xrpl4j/model/client/accounts/AccountLinesResultJsonTests.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. @@ -75,6 +75,8 @@ public void testJson() throws JsonProcessingException, JSONException { " \"peer_authorized\": false,\n" + " \"freeze\": false,\n" + " \"freeze_peer\": false,\n" + + " \"deep_freeze\": false,\n" + + " \"deep_freeze_peer\": false,\n" + " \"quality_in\": 0,\n" + " \"quality_out\": 0\n" + " },\n" + @@ -90,6 +92,8 @@ public void testJson() throws JsonProcessingException, JSONException { " \"peer_authorized\": false,\n" + " \"freeze\": false,\n" + " \"freeze_peer\": false,\n" + + " \"deep_freeze\": false,\n" + + " \"deep_freeze_peer\": false,\n" + " \"quality_in\": 0,\n" + " \"quality_out\": 0\n" + " }\n" + diff --git a/xrpl4j-core/src/test/java/org/xrpl/xrpl4j/model/flags/RippleStateFlagsTests.java b/xrpl4j-core/src/test/java/org/xrpl/xrpl4j/model/flags/RippleStateFlagsTests.java index 4c0ad6df6..d992207b5 100644 --- a/xrpl4j-core/src/test/java/org/xrpl/xrpl4j/model/flags/RippleStateFlagsTests.java +++ b/xrpl4j-core/src/test/java/org/xrpl/xrpl4j/model/flags/RippleStateFlagsTests.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. @@ -33,7 +33,7 @@ public class RippleStateFlagsTests extends AbstractFlagsTest { public static Stream data() { - return getBooleanCombinations(8); + return getBooleanCombinations(10); } @ParameterizedTest @@ -46,7 +46,9 @@ public void testDeriveIndividualFlagsFromFlags( boolean lsfLowNoRipple, boolean lsfHighNoRipple, boolean lsfLowFreeze, - boolean lsfHighFreeze + boolean lsfHighFreeze, + boolean lsfLowDeepFreeze, + boolean lsfHighDeepFreeze ) { long expectedFlags = getExpectedFlags( lsfLowReserve, @@ -56,7 +58,9 @@ public void testDeriveIndividualFlagsFromFlags( lsfLowNoRipple, lsfHighNoRipple, lsfLowFreeze, - lsfHighFreeze + lsfHighFreeze, + lsfLowDeepFreeze, + lsfHighDeepFreeze ); RippleStateFlags flags = RippleStateFlags.of(expectedFlags); @@ -69,6 +73,8 @@ public void testDeriveIndividualFlagsFromFlags( assertThat(flags.lsfHighNoRipple()).isEqualTo(lsfHighNoRipple); assertThat(flags.lsfLowFreeze()).isEqualTo(lsfLowFreeze); assertThat(flags.lsfHighFreeze()).isEqualTo(lsfHighFreeze); + assertThat(flags.lsfLowDeepFreeze()).isEqualTo(lsfLowDeepFreeze); + assertThat(flags.lsfHighDeepFreeze()).isEqualTo(lsfHighDeepFreeze); } @ParameterizedTest @@ -81,7 +87,9 @@ void testJson( boolean lsfLowNoRipple, boolean lsfHighNoRipple, boolean lsfLowFreeze, - boolean lsfHighFreeze + boolean lsfHighFreeze, + boolean lsfLowDeepFreeze, + boolean lsfHighDeepFreeze ) throws JSONException, JsonProcessingException { long expectedFlags = getExpectedFlags( lsfLowReserve, @@ -91,7 +99,9 @@ void testJson( lsfLowNoRipple, lsfHighNoRipple, lsfLowFreeze, - lsfHighFreeze + lsfHighFreeze, + lsfLowDeepFreeze, + lsfHighDeepFreeze ); RippleStateFlags flags = RippleStateFlags.of(expectedFlags); @@ -113,7 +123,9 @@ protected long getExpectedFlags( boolean lsfLowNoRipple, boolean lsfHighNoRipple, boolean lsfLowFreeze, - boolean lsfHighFreeze + boolean lsfHighFreeze, + boolean lsfLowDeepFreeze, + boolean lsfHighDeepFreeze ) { return (lsfLowReserve ? RippleStateFlags.LOW_RESERVE.getValue() : 0L) | (lsfHighReserve ? RippleStateFlags.HIGH_RESERVE.getValue() : 0L) | @@ -122,6 +134,8 @@ protected long getExpectedFlags( (lsfLowNoRipple ? RippleStateFlags.LOW_NO_RIPPLE.getValue() : 0L) | (lsfHighNoRipple ? RippleStateFlags.HIGH_NO_RIPPLE.getValue() : 0L) | (lsfLowFreeze ? RippleStateFlags.LOW_FREEZE.getValue() : 0L) | - (lsfHighFreeze ? RippleStateFlags.HIGH_FREEZE.getValue() : 0L); + (lsfHighFreeze ? RippleStateFlags.HIGH_FREEZE.getValue() : 0L) | + (lsfLowDeepFreeze ? RippleStateFlags.LOW_DEEP_FREEZE.getValue() : 0L) | + (lsfHighDeepFreeze ? RippleStateFlags.HIGH_DEEP_FREEZE.getValue() : 0L); } } diff --git a/xrpl4j-core/src/test/java/org/xrpl/xrpl4j/model/flags/TrustSetFlagsTests.java b/xrpl4j-core/src/test/java/org/xrpl/xrpl4j/model/flags/TrustSetFlagsTests.java index 79c13fa0f..83a55f51f 100644 --- a/xrpl4j-core/src/test/java/org/xrpl/xrpl4j/model/flags/TrustSetFlagsTests.java +++ b/xrpl4j-core/src/test/java/org/xrpl/xrpl4j/model/flags/TrustSetFlagsTests.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. @@ -34,7 +34,7 @@ public class TrustSetFlagsTests extends AbstractFlagsTest { public static Stream data() { - return getBooleanCombinations(5); + return getBooleanCombinations(7); } @ParameterizedTest @@ -44,7 +44,9 @@ public void testFlagsConstructionWithIndividualFlags( boolean tfSetNoRipple, boolean tfClearNoRipple, boolean tfSetFreeze, - boolean tfClearFreeze + boolean tfClearFreeze, + boolean tfSetDeepFreeze, + boolean tfClearDeepFreeze ) { TrustSetFlags.Builder builder = TrustSetFlags.builder() .tfSetfAuth(tfSetfAuth); @@ -65,6 +67,14 @@ public void testFlagsConstructionWithIndividualFlags( builder.tfClearFreeze(); } + if (tfSetDeepFreeze) { + builder.tfSetDeepFreeze(); + } + + if (tfClearDeepFreeze) { + builder.tfClearDeepFreeze(); + } + TrustSetFlags flags = builder.build(); long expectedFlags = getExpectedFlags( @@ -72,7 +82,9 @@ public void testFlagsConstructionWithIndividualFlags( tfSetNoRipple, tfClearNoRipple, tfSetFreeze, - tfClearFreeze + tfClearFreeze, + tfSetDeepFreeze, + tfClearDeepFreeze ); assertThat(flags.getValue()).isEqualTo(expectedFlags); } @@ -84,14 +96,18 @@ public void testDeriveIndividualFlagsFromFlags( boolean tfSetNoRipple, boolean tfClearNoRipple, boolean tfSetFreeze, - boolean tfClearFreeze + boolean tfClearFreeze, + boolean tfSetDeepFreeze, + boolean tfClearDeepFreeze ) { long expectedFlags = getExpectedFlags( tfSetfAuth, tfSetNoRipple, tfClearNoRipple, tfSetFreeze, - tfClearFreeze + tfClearFreeze, + tfSetDeepFreeze, + tfClearDeepFreeze ); TrustSetFlags flags = TrustSetFlags.of(expectedFlags); @@ -102,6 +118,8 @@ public void testDeriveIndividualFlagsFromFlags( assertThat(flags.tfClearNoRipple()).isEqualTo(tfClearNoRipple); assertThat(flags.tfSetFreeze()).isEqualTo(tfSetFreeze); assertThat(flags.tfClearFreeze()).isEqualTo(tfClearFreeze); + assertThat(flags.tfSetDeepFreeze()).isEqualTo(tfSetDeepFreeze); + assertThat(flags.tfClearDeepFreeze()).isEqualTo(tfClearDeepFreeze); } @Test @@ -114,6 +132,8 @@ void testEmptyFlags() { assertThat(flags.tfClearNoRipple()).isFalse(); assertThat(flags.tfSetFreeze()).isFalse(); assertThat(flags.tfClearFreeze()).isFalse(); + assertThat(flags.tfSetDeepFreeze()).isFalse(); + assertThat(flags.tfClearDeepFreeze()).isFalse(); assertThat(flags.tfFullyCanonicalSig()).isFalse(); assertThat(flags.getValue()).isEqualTo(0L); } @@ -125,14 +145,18 @@ void testJson( boolean tfSetNoRipple, boolean tfClearNoRipple, boolean tfSetFreeze, - boolean tfClearFreeze + boolean tfClearFreeze, + boolean tfSetDeepFreeze, + boolean tfClearDeepFreeze ) throws JSONException, JsonProcessingException { long expectedFlags = getExpectedFlags( tfSetfAuth, tfSetNoRipple, tfClearNoRipple, tfSetFreeze, - tfClearFreeze + tfClearFreeze, + tfSetDeepFreeze, + tfClearDeepFreeze ); TrustSetFlags flags = TrustSetFlags.of(expectedFlags); @@ -160,13 +184,17 @@ private long getExpectedFlags( boolean tfSetNoRipple, boolean tfClearNoRipple, boolean tfSetFreeze, - boolean tfClearFreeze + boolean tfClearFreeze, + boolean tfSetDeepFreeze, + boolean tfClearDeepFreeze ) { return (TrustSetFlags.FULLY_CANONICAL_SIG.getValue()) | (tfSetfAuth ? TrustSetFlags.SET_F_AUTH.getValue() : 0L) | (tfSetNoRipple ? TrustSetFlags.SET_NO_RIPPLE.getValue() : 0L) | (tfClearNoRipple ? TrustSetFlags.CLEAR_NO_RIPPLE.getValue() : 0L) | (tfSetFreeze ? TrustSetFlags.SET_FREEZE.getValue() : 0L) | - (tfClearFreeze ? TrustSetFlags.CLEAR_FREEZE.getValue() : 0L); + (tfClearFreeze ? TrustSetFlags.CLEAR_FREEZE.getValue() : 0L) | + (tfSetDeepFreeze ? TrustSetFlags.SET_DEEP_FREEZE.getValue() : 0L) | + (tfClearDeepFreeze ? TrustSetFlags.CLEAR_DEEP_FREEZE.getValue() : 0L); } } diff --git a/xrpl4j-integration-tests/src/test/java/org/xrpl/xrpl4j/tests/FreezeIssuedCurrencyIT.java b/xrpl4j-integration-tests/src/test/java/org/xrpl/xrpl4j/tests/FreezeIssuedCurrencyIT.java index 920dbb619..8379e6f53 100644 --- a/xrpl4j-integration-tests/src/test/java/org/xrpl/xrpl4j/tests/FreezeIssuedCurrencyIT.java +++ b/xrpl4j-integration-tests/src/test/java/org/xrpl/xrpl4j/tests/FreezeIssuedCurrencyIT.java @@ -265,6 +265,218 @@ public void issueAndFreezeFundsIndividual() throws JsonRpcClientErrorException, assertThat(badActorTrustLine.noRipplePeer()).isTrue(); } + /** + * This test creates a Trustline between an issuer and a badActor, issues funds to the badActor, then deep freezes the + * funds and validates that the badActor is unable to send or receive those funds (except to/from the issuer). + * It also verifies that the deep_freeze and deep_freeze_peer fields are correctly set in account_lines responses. + * + * @see "https://github.com/XRPLF/XRPL-Standards/tree/master/XLS-0077-deep-freeze" + */ + @Test + public void issueAndDeepFreezeFunds() throws JsonRpcClientErrorException, JsonProcessingException { + FeeResult feeResult = xrplClient.fee(); + + // Create a Trust Line between issuer and the bad actor. + TrustLine badActorTrustLine = this.createTrustLine( + badActorKeyPair, + IssuedCurrencyAmount.builder() + .currency(FreezeIssuedCurrencyIT.ISSUED_CURRENCY_CODE) + .issuer(issuerKeyPair.publicKey().deriveAddress()) + .value(FreezeIssuedCurrencyIT.TEN_THOUSAND) + .build(), + FeeUtils.computeNetworkFees(feeResult).recommendedFee(), + TrustSetFlags.builder().tfSetNoRipple().build() + ); + assertThat(badActorTrustLine.freeze()).isFalse(); + assertThat(badActorTrustLine.freezePeer()).isFalse(); + assertThat(badActorTrustLine.deepFreeze()).isFalse(); + assertThat(badActorTrustLine.deepFreezePeer()).isFalse(); + assertThat(badActorTrustLine.noRipple()).isFalse(); + assertThat(badActorTrustLine.noRipplePeer()).isTrue(); + + /////////////////////////// + // Create a Trust Line between issuer and the good actor. + TrustLine goodActorTrustLine = this.createTrustLine( + goodActorKeyPair, + IssuedCurrencyAmount.builder() + .currency(FreezeIssuedCurrencyIT.ISSUED_CURRENCY_CODE) + .issuer(issuerKeyPair.publicKey().deriveAddress()) + .value(FreezeIssuedCurrencyIT.TEN_THOUSAND) + .build(), + FeeUtils.computeNetworkFees(feeResult).recommendedFee(), + TrustSetFlags.builder().tfSetNoRipple().build() + ); + assertThat(goodActorTrustLine.freeze()).isFalse(); + assertThat(goodActorTrustLine.freezePeer()).isFalse(); + assertThat(goodActorTrustLine.deepFreeze()).isFalse(); + assertThat(goodActorTrustLine.deepFreezePeer()).isFalse(); + assertThat(goodActorTrustLine.noRipple()).isFalse(); + assertThat(goodActorTrustLine.noRipplePeer()).isTrue(); + + ///////////// + // Send Funds + ///////////// + + // Send funds from issuer to the badActor. + sendIssuedCurrency( + issuerKeyPair, + badActorKeyPair, + IssuedCurrencyAmount.builder() + .issuer(issuerKeyPair.publicKey().deriveAddress()) + .currency(FreezeIssuedCurrencyIT.ISSUED_CURRENCY_CODE) + .value(TEN_THOUSAND) + .build(), + FeeUtils.computeNetworkFees(feeResult).recommendedFee() + ); + + /////////////////////////// + // Validate that the TrustLine balance was updated as a result of the Payment. + // The trust line returned is from the perspective of the issuer, so the balance should be negative. + this.scanForResult(() -> getValidatedAccountLines(issuerKeyPair.publicKey().deriveAddress(), + badActorKeyPair.publicKey().deriveAddress()), + linesResult -> linesResult.lines().stream() + .anyMatch(line -> line.balance().equals("-" + TEN_THOUSAND)) + ); + + // Send funds from badActor to the goodActor. + sendIssuedCurrency( + badActorKeyPair, + goodActorKeyPair, + IssuedCurrencyAmount.builder() + .issuer(issuerKeyPair.publicKey().deriveAddress()) + .currency(FreezeIssuedCurrencyIT.ISSUED_CURRENCY_CODE) + .value(FIVE_THOUSAND) + .build(), + FeeUtils.computeNetworkFees(feeResult).recommendedFee() + ); + + /////////////////////////// + // Validate that the TrustLine balance was updated as a result of the Payment. + // The trust line returned is from the perspective of the issuer, so the balance should be negative. + this.scanForResult(() -> getValidatedAccountLines(issuerKeyPair.publicKey().deriveAddress(), + goodActorKeyPair.publicKey().deriveAddress()), + linesResult -> linesResult.lines().stream() + .anyMatch(line -> line.balance().equals("-" + FIVE_THOUSAND)) + ); + + // Deep-Freeze the trustline between the issuer and bad actor. + // According to XLS-77d, deep freeze requires regular freeze to be set first or in the same transaction. + badActorTrustLine = this.adjustTrustlineFreezeAndDeepFreeze( + issuerKeyPair, + badActorKeyPair, + FeeUtils.computeNetworkFees(feeResult).recommendedFee(), + FREEZE + ); + assertThat(badActorTrustLine.freeze()).isTrue(); + assertThat(badActorTrustLine.freezePeer()).isFalse(); + assertThat(badActorTrustLine.deepFreeze()).isTrue(); + assertThat(badActorTrustLine.deepFreezePeer()).isFalse(); + assertThat(badActorTrustLine.noRipple()).isFalse(); + assertThat(badActorTrustLine.noRipplePeer()).isTrue(); + + // Verify deep freeze fields from the badActor's perspective + TrustLine badActorPerspectiveTrustLine = this.scanForResult( + () -> getValidatedAccountLines(badActorKeyPair.publicKey().deriveAddress(), + issuerKeyPair.publicKey().deriveAddress()), + linesResult -> !linesResult.lines().isEmpty() + ).lines().get(0); + assertThat(badActorPerspectiveTrustLine.deepFreeze()).isFalse(); + assertThat(badActorPerspectiveTrustLine.deepFreezePeer()).isTrue(); + + ///////////// + // Assertions + ///////////// + + // 1) The counterparty cannot send or receive the deep-frozen currencies (except to/from the issuer) + // 2) Payments can still occur directly between the issuer and the deep-frozen counterparty + + // Try to send funds from badActor to goodActor should not work because the badActor is deep-frozen. + sendIssuedCurrency( + badActorKeyPair, + goodActorKeyPair, + IssuedCurrencyAmount.builder() + .issuer(issuerKeyPair.publicKey().deriveAddress()) + .currency(FreezeIssuedCurrencyIT.ISSUED_CURRENCY_CODE) + .value("1000") + .build(), + FeeUtils.computeNetworkFees(feeResult).recommendedFee(), + "tecPATH_DRY" + ); + + // Try to send funds from goodActor to badActor should not work because the badActor is deep-frozen. + sendIssuedCurrency( + goodActorKeyPair, + badActorKeyPair, + IssuedCurrencyAmount.builder() + .issuer(issuerKeyPair.publicKey().deriveAddress()) + .currency(FreezeIssuedCurrencyIT.ISSUED_CURRENCY_CODE) + .value("1000") + .build(), + FeeUtils.computeNetworkFees(feeResult).recommendedFee(), + "tecPATH_DRY" + ); + + // Sending from the badActor to the issuer should still work + sendIssuedCurrency( + badActorKeyPair, + issuerKeyPair, + IssuedCurrencyAmount.builder() + .issuer(issuerKeyPair.publicKey().deriveAddress()) + .currency(FreezeIssuedCurrencyIT.ISSUED_CURRENCY_CODE) + .value("2000") + .build(), + FeeUtils.computeNetworkFees(feeResult).recommendedFee() + ); + + // Sending from the issuer to the badActor should still work + sendIssuedCurrency( + issuerKeyPair, + badActorKeyPair, + IssuedCurrencyAmount.builder() + .issuer(issuerKeyPair.publicKey().deriveAddress()) + .currency(FreezeIssuedCurrencyIT.ISSUED_CURRENCY_CODE) + .value("1000") + .build(), + FeeUtils.computeNetworkFees(feeResult).recommendedFee() + ); + + // Clear the deep freeze and regular freeze on the bad actor. + // According to XLS-77d, cannot clear regular freeze without also clearing deep freeze. + badActorTrustLine = this.adjustTrustlineFreezeAndDeepFreeze( + issuerKeyPair, + badActorKeyPair, + FeeUtils.computeNetworkFees(feeResult).recommendedFee(), + UN_FREEZE + ); + assertThat(badActorTrustLine.freeze()).isFalse(); + assertThat(badActorTrustLine.freezePeer()).isFalse(); + assertThat(badActorTrustLine.deepFreeze()).isFalse(); + assertThat(badActorTrustLine.deepFreezePeer()).isFalse(); + assertThat(badActorTrustLine.noRipple()).isFalse(); + assertThat(badActorTrustLine.noRipplePeer()).isTrue(); + + // Verify deep freeze fields are cleared from the badActor's perspective + badActorPerspectiveTrustLine = this.scanForResult( + () -> getValidatedAccountLines(badActorKeyPair.publicKey().deriveAddress(), + issuerKeyPair.publicKey().deriveAddress()), + linesResult -> !linesResult.lines().isEmpty() + ).lines().get(0); + assertThat(badActorPerspectiveTrustLine.deepFreeze()).isFalse(); + assertThat(badActorPerspectiveTrustLine.deepFreezePeer()).isFalse(); + + // After clearing deep freeze, badActor should be able to send funds to goodActor again + sendIssuedCurrency( + badActorKeyPair, + goodActorKeyPair, + IssuedCurrencyAmount.builder() + .issuer(issuerKeyPair.publicKey().deriveAddress()) + .currency(FreezeIssuedCurrencyIT.ISSUED_CURRENCY_CODE) + .value("500") + .build(), + FeeUtils.computeNetworkFees(feeResult).recommendedFee() + ); + } + /** * This test creates a Trustline between an issuer and a badActor, issues funds to two counterparties, then globally * freezes the trustlines for the issuer. The test validates that neither the good nor the bad actor is able to send @@ -654,4 +866,71 @@ private AccountInfoResult adjustGlobalTrustlineFreeze( ); } + + /** + * Freeze and deep freeze an individual trustline that exists between the specified issuer and the specified + * counterparty for the {@link #ISSUED_CURRENCY_CODE}. According to XLS-77d, deep freeze requires regular freeze + * to be set first or in the same transaction, and regular freeze cannot be cleared without also clearing deep freeze. + * + * @param issuerKeyPair The {@link KeyPair} of the trustline issuer. + * @param counterpartyKeyPair The {@link KeyPair} of the trustline counterparty. + * @param fee The fee to spend to get the transaction into the ledger. + * @param freeze A boolean to toggle the trustline operation (i.e., {@code false} to clear both freezes + * and {@code true} to set both freezes). + * + * @return The {@link TrustLine} that was frozen/deep-frozen or unfrozen. + * + * @throws JsonRpcClientErrorException If anything goes wrong while communicating with rippled. + * @throws JsonProcessingException If there are any problems parsing JSON. + */ + private TrustLine adjustTrustlineFreezeAndDeepFreeze( + KeyPair issuerKeyPair, + KeyPair counterpartyKeyPair, + XrpCurrencyAmount fee, + boolean freeze + ) throws JsonRpcClientErrorException, JsonProcessingException { + AccountInfoResult issuerAccountInfo = this.scanForResult( + () -> this.getValidatedAccountInfo(issuerKeyPair.publicKey().deriveAddress()) + ); + + Builder flagsBuilder = TrustSetFlags.builder(); + if (freeze) { + // Set both regular freeze and deep freeze in the same transaction + flagsBuilder.tfSetFreeze().tfSetDeepFreeze(); + } else { + // Clear both regular freeze and deep freeze in the same transaction + flagsBuilder.tfClearFreeze().tfClearDeepFreeze(); + } + + TrustSet trustSet = TrustSet.builder() + .account(issuerKeyPair.publicKey().deriveAddress()) + .fee(fee) + .sequence(issuerAccountInfo.accountData().sequence()) + .limitAmount(IssuedCurrencyAmount.builder() + .currency(FreezeIssuedCurrencyIT.ISSUED_CURRENCY_CODE) + .issuer(counterpartyKeyPair.publicKey().deriveAddress()) + .value("0") + .build()) + .flags(flagsBuilder.build()) + .signingPublicKey(issuerKeyPair.publicKey()) + .build(); + + SingleSignedTransaction signedTrustSet = signatureService.sign(issuerKeyPair.privateKey(), trustSet); + SubmitResult trustSetSubmitResult = xrplClient.submit(signedTrustSet); + assertThat(trustSetSubmitResult.engineResult()).isEqualTo("tesSUCCESS"); + logger.info( + "TrustSet freeze and deep freeze transaction successful: https://testnet.xrpl.org/transactions/{}", + trustSetSubmitResult.transactionResult().hash() + ); + + return scanForResult( + () -> getValidatedAccountLines(issuerKeyPair.publicKey().deriveAddress(), + counterpartyKeyPair.publicKey().deriveAddress()), + accountLineResult -> accountLineResult.lines().stream() + .filter(trustLine -> trustLine.account().equals(counterpartyKeyPair.publicKey().deriveAddress())) + .anyMatch(trustLine -> trustLine.freeze() == freeze && trustLine.deepFreeze() == freeze) + ) + .lines().get(0); + + } } diff --git a/xrpl4j-integration-tests/src/test/resources/rippled/rippled.cfg b/xrpl4j-integration-tests/src/test/resources/rippled/rippled.cfg index f4bb59030..965994514 100644 --- a/xrpl4j-integration-tests/src/test/resources/rippled/rippled.cfg +++ b/xrpl4j-integration-tests/src/test/resources/rippled/rippled.cfg @@ -219,4 +219,5 @@ fixAMMv1_1 fixEmptyDID fixXChainRewardRounding PriceOracle -MPTokensV1 \ No newline at end of file +MPTokensV1 +DeepFreeze \ No newline at end of file