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