From dae2fd62bbfca5edd764d132b670fb233dc15ad9 Mon Sep 17 00:00:00 2001 From: nkramer44 Date: Wed, 20 Sep 2023 16:10:35 -0400 Subject: [PATCH] Add support for Clawback (#485) * add support for clawback amendment * checkstyle * fix normalization functions in AccountSet * mark all Clawback code @Beta * mark CLAWBACK transaction type beta --- .../xrpl4j/crypto/signing/SignatureUtils.java | 9 + .../xrpl4j/model/flags/AccountRootFlags.java | 24 +++ .../xrpl4j/model/transactions/AccountSet.java | 23 ++- .../xrpl4j/model/transactions/Clawback.java | 55 ++++++ .../model/transactions/Transaction.java | 1 + .../model/transactions/TransactionType.java | 12 +- .../crypto/signing/SignatureUtilsTest.java | 38 ++++ .../model/flags/AccountRootFlagsTests.java | 18 +- .../model/transactions/AccountSetTests.java | 8 +- .../model/transactions/ClawbackTest.java | 118 +++++++++++++ .../org/xrpl/xrpl4j/tests/AbstractIT.java | 35 ++++ .../org/xrpl/xrpl4j/tests/ClawbackIT.java | 166 ++++++++++++++++++ .../xrpl/xrpl4j/tests/IssuedCurrencyIT.java | 152 +++++----------- .../src/test/resources/rippled/rippled.cfg | 4 + 14 files changed, 540 insertions(+), 123 deletions(-) create mode 100644 xrpl4j-core/src/main/java/org/xrpl/xrpl4j/model/transactions/Clawback.java create mode 100644 xrpl4j-core/src/test/java/org/xrpl/xrpl4j/model/transactions/ClawbackTest.java create mode 100644 xrpl4j-integration-tests/src/test/java/org/xrpl/xrpl4j/tests/ClawbackIT.java diff --git a/xrpl4j-core/src/main/java/org/xrpl/xrpl4j/crypto/signing/SignatureUtils.java b/xrpl4j-core/src/main/java/org/xrpl/xrpl4j/crypto/signing/SignatureUtils.java index 6ade9325c..13bb2b6b1 100644 --- a/xrpl4j-core/src/main/java/org/xrpl/xrpl4j/crypto/signing/SignatureUtils.java +++ b/xrpl4j-core/src/main/java/org/xrpl/xrpl4j/crypto/signing/SignatureUtils.java @@ -34,6 +34,7 @@ import org.xrpl.xrpl4j.model.transactions.CheckCancel; import org.xrpl.xrpl4j.model.transactions.CheckCash; import org.xrpl.xrpl4j.model.transactions.CheckCreate; +import org.xrpl.xrpl4j.model.transactions.Clawback; import org.xrpl.xrpl4j.model.transactions.DepositPreAuth; import org.xrpl.xrpl4j.model.transactions.EscrowCancel; import org.xrpl.xrpl4j.model.transactions.EscrowCreate; @@ -271,6 +272,10 @@ public SingleSignedTransaction addSignatureToTransact transactionWithSignature = TicketCreate.builder().from((TicketCreate) transaction) .transactionSignature(signature) .build(); + } else if (Clawback.class.isAssignableFrom(transaction.getClass())) { + transactionWithSignature = Clawback.builder().from((Clawback) transaction) + .transactionSignature(signature) + .build(); } else { // Should never happen, but will in a unit test if we miss one. throw new IllegalArgumentException("Signing fields could not be added to the transaction."); @@ -405,6 +410,10 @@ public T addMultiSignaturesToTransaction(T transaction, transactionWithSignatures = TicketCreate.builder().from((TicketCreate) transaction) .signers(signers) .build(); + } else if (Clawback.class.isAssignableFrom(transaction.getClass())) { + transactionWithSignatures = Clawback.builder().from((Clawback) transaction) + .signers(signers) + .build(); } else { // Should never happen, but will in a unit test if we miss one. throw new IllegalArgumentException("Signing fields could not be added to the transaction."); diff --git a/xrpl4j-core/src/main/java/org/xrpl/xrpl4j/model/flags/AccountRootFlags.java b/xrpl4j-core/src/main/java/org/xrpl/xrpl4j/model/flags/AccountRootFlags.java index 0df9b28ba..ca841206f 100644 --- a/xrpl4j-core/src/main/java/org/xrpl/xrpl4j/model/flags/AccountRootFlags.java +++ b/xrpl4j-core/src/main/java/org/xrpl/xrpl4j/model/flags/AccountRootFlags.java @@ -20,6 +20,7 @@ * =========================LICENSE_END================================== */ +import com.google.common.annotations.Beta; import org.xrpl.xrpl4j.model.transactions.AccountSet; /** @@ -97,6 +98,15 @@ public class AccountRootFlags extends Flags { */ public static final AccountRootFlags DISALLOW_INCOMING_TRUSTLINE = new AccountRootFlags(0x20000000); + /** + * Constant {@link AccountRootFlags} for the {@code lsfAllowTrustLineClawback} account flag. + * + *

This constant will be marked {@link Beta} until the Clawback amendment is enabled on mainnet. Its API is subject + * to change.

+ */ + @Beta + public static final AccountRootFlags ALLOW_TRUSTLINE_CLAWBACK = new AccountRootFlags(0x80000000L); + /** * Required-args Constructor. * @@ -110,6 +120,7 @@ private AccountRootFlags(final long value) { * Construct {@link AccountRootFlags} with a given value. * * @param value The long-number encoded flags value of this {@link AccountRootFlags}. + * * @return New {@link AccountRootFlags}. */ public static AccountRootFlags of(long value) { @@ -235,4 +246,17 @@ public boolean lsfDisallowIncomingPayChan() { public boolean lsfDisallowIncomingTrustline() { return this.isSet(AccountRootFlags.DISALLOW_INCOMING_TRUSTLINE); } + + /** + * Allows trustline clawback on this account. + * + *

This constant will be marked {@link Beta} until the Clawback amendment is enabled on mainnet. Its API is subject + * to change.

+ * + * @return {@code true} if {@code lsfAllowTrustLineClawback} is set, otherwise {@code false}. + */ + @Beta + public boolean lsfAllowTrustLineClawback() { + return this.isSet(AccountRootFlags.ALLOW_TRUSTLINE_CLAWBACK); + } } diff --git a/xrpl4j-core/src/main/java/org/xrpl/xrpl4j/model/transactions/AccountSet.java b/xrpl4j-core/src/main/java/org/xrpl/xrpl4j/model/transactions/AccountSet.java index d558d1989..be1af6b7a 100644 --- a/xrpl4j-core/src/main/java/org/xrpl/xrpl4j/model/transactions/AccountSet.java +++ b/xrpl4j-core/src/main/java/org/xrpl/xrpl4j/model/transactions/AccountSet.java @@ -26,11 +26,14 @@ import com.fasterxml.jackson.annotation.JsonValue; import com.fasterxml.jackson.databind.annotation.JsonDeserialize; import com.fasterxml.jackson.databind.annotation.JsonSerialize; +import com.google.common.annotations.Beta; import com.google.common.base.Preconditions; import com.google.common.primitives.UnsignedInteger; import org.immutables.value.Value; import org.xrpl.xrpl4j.model.flags.AccountSetTransactionFlags; +import java.util.Arrays; +import java.util.Collections; import java.util.Optional; /** @@ -143,7 +146,7 @@ default AccountSet normalizeClearFlag() { // 2. JSON has ClearFlag and jackson sets clearFlagRawValue. // This value will never be negative due to XRPL representing this kind of flag as an unsigned number, // so no lower bound check is required. - if (clearFlagRawValue().get().longValue() <= 15) { + if (clearFlagRawValue().get().longValue() <= AccountSetFlag.MAX_VALUE) { // Set clearFlag to clearFlagRawValue if clearFlagRawValue matches a valid AccountSetFlag variant. return AccountSet.builder().from(this) .clearFlag(AccountSetFlag.forValue(clearFlagRawValue().get().intValue())) @@ -236,7 +239,7 @@ default AccountSet normalizeSetFlag() { // 2. JSON has ClearFlag and jackson sets setFlagRawValue. // This value will never be negative due to XRPL representing this kind of flag as an unsigned number, // so no lower bound check is required. - if (setFlagRawValue().get().longValue() <= 15) { + if (setFlagRawValue().get().longValue() <= AccountSetFlag.MAX_VALUE) { // Set setFlag to setFlagRawValue if setFlagRawValue matches a valid AccountSetFlag variant. return AccountSet.builder().from(this) .setFlag(AccountSetFlag.forValue(setFlagRawValue().get().intValue())) @@ -440,7 +443,15 @@ enum AccountSetFlag { /** * Block incoming Trustlines. */ - DISALLOW_INCOMING_TRUSTLINE(15); + DISALLOW_INCOMING_TRUSTLINE(15), + /** + * Enable clawback on the account's trustlines. + * + *

This value will be marked {@link Beta} until the Clawback amendment is enabled on mainnet. Its API is subject + * to change.

+ */ + @Beta + ALLOW_TRUSTLINE_CLAWBACK(16); final int value; @@ -448,6 +459,12 @@ enum AccountSetFlag { this.value = value; } + /** + * The maximum underlying value of AccountSetFlags. This is useful for the normalization methods of AccountSet + * so that adding a new AccountSetFlag does not require a change to those normalization functions. + */ + static final int MAX_VALUE = Collections.max(Arrays.asList(AccountSetFlag.values())).getValue(); + /** * To deserialize enums with integer values, you need to specify this factory method with the {@link JsonCreator} * annotation, otherwise Jackson treats the JSON integer value as an ordinal. diff --git a/xrpl4j-core/src/main/java/org/xrpl/xrpl4j/model/transactions/Clawback.java b/xrpl4j-core/src/main/java/org/xrpl/xrpl4j/model/transactions/Clawback.java new file mode 100644 index 000000000..8c5c3bdcf --- /dev/null +++ b/xrpl4j-core/src/main/java/org/xrpl/xrpl4j/model/transactions/Clawback.java @@ -0,0 +1,55 @@ +package org.xrpl.xrpl4j.model.transactions; + +import com.fasterxml.jackson.annotation.JsonProperty; +import com.fasterxml.jackson.databind.annotation.JsonDeserialize; +import com.fasterxml.jackson.databind.annotation.JsonSerialize; +import com.google.common.annotations.Beta; +import org.immutables.value.Value; +import org.immutables.value.Value.Immutable; +import org.xrpl.xrpl4j.model.flags.TransactionFlags; + +/** + * Clawback an issued currency that exists on a Trustline. + * + *

This class will be marked {@link Beta} until the Clawback amendment is enabled on mainnet. Its API is subject + * to change.

+ */ +@Immutable +@JsonSerialize(as = ImmutableClawback.class) +@JsonDeserialize(as = ImmutableClawback.class) +@Beta +public interface Clawback extends Transaction { + + /** + * Construct a {@code Clawback} builder. + * + * @return An {@link ImmutableClawback.Builder}. + */ + static ImmutableClawback.Builder builder() { + return ImmutableClawback.builder(); + } + + /** + * Set of {@link TransactionFlags}s for this {@link Clawback}, which only allows the + * {@code tfFullyCanonicalSig} flag, which is deprecated. + * + * @return Always {@link TransactionFlags#EMPTY}. + */ + @JsonProperty("Flags") + @Value.Default + default TransactionFlags flags() { + return TransactionFlags.EMPTY; + } + + /** + * Indicates the amount being clawed back, as well as the counterparty from which the amount is being clawed back + * from. This amount must not exceed the holder's balance and must be greater than zero. The issuer in this amount + * must not be the same as the source account of this transaction. + * + * @return An {@link IssuedCurrencyAmount} indicating the amount to clawback. + */ + @JsonProperty("Amount") + IssuedCurrencyAmount amount(); + + +} diff --git a/xrpl4j-core/src/main/java/org/xrpl/xrpl4j/model/transactions/Transaction.java b/xrpl4j-core/src/main/java/org/xrpl/xrpl4j/model/transactions/Transaction.java index 3eca5a090..5fb42c7bb 100644 --- a/xrpl4j-core/src/main/java/org/xrpl/xrpl4j/model/transactions/Transaction.java +++ b/xrpl4j-core/src/main/java/org/xrpl/xrpl4j/model/transactions/Transaction.java @@ -71,6 +71,7 @@ public interface Transaction { .put(ImmutableTrustSet.class, TransactionType.TRUST_SET) .put(ImmutableTicketCreate.class, TransactionType.TICKET_CREATE) .put(ImmutableUnlModify.class, TransactionType.UNL_MODIFY) + .put(ImmutableClawback.class, TransactionType.CLAWBACK) .build(); /** diff --git a/xrpl4j-core/src/main/java/org/xrpl/xrpl4j/model/transactions/TransactionType.java b/xrpl4j-core/src/main/java/org/xrpl/xrpl4j/model/transactions/TransactionType.java index 1d01502ae..63b5fe393 100644 --- a/xrpl4j-core/src/main/java/org/xrpl/xrpl4j/model/transactions/TransactionType.java +++ b/xrpl4j-core/src/main/java/org/xrpl/xrpl4j/model/transactions/TransactionType.java @@ -21,6 +21,7 @@ */ import com.fasterxml.jackson.annotation.JsonValue; +import com.google.common.annotations.Beta; /** * Enumeration of the types of Transactions on the XRP Ledger. @@ -160,7 +161,16 @@ public enum TransactionType { /** * The {@link TransactionType} for the {@link UnlModify} transaction. */ - UNL_MODIFY("UNLModify"); + UNL_MODIFY("UNLModify"), + + /** + * The {@link TransactionType} for the {@link Clawback} transaction. + * + *

This constant will be marked {@link Beta} until the Clawback amendment is enabled on mainnet. Its API is subject + * to change.

+ */ + @Beta + CLAWBACK("Clawback"); private final String value; diff --git a/xrpl4j-core/src/test/java/org/xrpl/xrpl4j/crypto/signing/SignatureUtilsTest.java b/xrpl4j-core/src/test/java/org/xrpl/xrpl4j/crypto/signing/SignatureUtilsTest.java index 0590a68c1..d9f5e6fa0 100644 --- a/xrpl4j-core/src/test/java/org/xrpl/xrpl4j/crypto/signing/SignatureUtilsTest.java +++ b/xrpl4j-core/src/test/java/org/xrpl/xrpl4j/crypto/signing/SignatureUtilsTest.java @@ -53,6 +53,7 @@ import org.xrpl.xrpl4j.model.transactions.CheckCancel; import org.xrpl.xrpl4j.model.transactions.CheckCash; import org.xrpl.xrpl4j.model.transactions.CheckCreate; +import org.xrpl.xrpl4j.model.transactions.Clawback; import org.xrpl.xrpl4j.model.transactions.DepositPreAuth; import org.xrpl.xrpl4j.model.transactions.EscrowCancel; import org.xrpl.xrpl4j.model.transactions.EscrowCreate; @@ -573,6 +574,25 @@ void addSignatureToTicketCreate() { addSignatureToTransactionHelper(ticketCreate); } + @Test + void addSignatureToClawback() { + Clawback clawback = Clawback.builder() + .account(sourcePublicKey.deriveAddress()) + .fee(XrpCurrencyAmount.ofDrops(10)) + .sequence(UnsignedInteger.ONE) + .signingPublicKey(sourcePublicKey) + .amount( + IssuedCurrencyAmount.builder() + .currency("FOO") + .issuer(Address.of("rsA2LpzuawewSBQXkiju3YQTMzW13pAAdW")) + .value("314.159") + .build() + ) + .build(); + + addSignatureToTransactionHelper(clawback); + } + @Test public void addSignatureToTransactionUnsupported() { assertThrows(IllegalArgumentException.class, () -> addSignatureToTransactionHelper(transactionMock)); @@ -892,6 +912,24 @@ void addMultiSignaturesToTicketCreate() { addMultiSignatureToTransactionHelper(ticketCreate); } + @Test + void addMultiSignaturesToClawback() { + Clawback clawback = Clawback.builder() + .account(sourcePublicKey.deriveAddress()) + .fee(XrpCurrencyAmount.ofDrops(10)) + .sequence(UnsignedInteger.ONE) + .amount( + IssuedCurrencyAmount.builder() + .currency("FOO") + .issuer(Address.of("rsA2LpzuawewSBQXkiju3YQTMzW13pAAdW")) + .value("314.159") + .build() + ) + .build(); + + addMultiSignatureToTransactionHelper(clawback); + } + @Test public void addMultiSignaturesToTransactionUnsupported() { when(transactionMock.transactionSignature()).thenReturn(Optional.empty()); diff --git a/xrpl4j-core/src/test/java/org/xrpl/xrpl4j/model/flags/AccountRootFlagsTests.java b/xrpl4j-core/src/test/java/org/xrpl/xrpl4j/model/flags/AccountRootFlagsTests.java index c8b4ed840..94f8e71b0 100644 --- a/xrpl4j-core/src/test/java/org/xrpl/xrpl4j/model/flags/AccountRootFlagsTests.java +++ b/xrpl4j-core/src/test/java/org/xrpl/xrpl4j/model/flags/AccountRootFlagsTests.java @@ -34,7 +34,7 @@ public class AccountRootFlagsTests extends AbstractFlagsTest { public static Stream data() { - return getBooleanCombinations(13); + return getBooleanCombinations(14); } @ParameterizedTest @@ -53,7 +53,8 @@ public void testDeriveIndividualFlagsFromFlags( boolean lsfDisallowIncomingNFTokenOffer, boolean lsfDisallowIncomingCheck, boolean lsfDisallowIncomingPayChan, - boolean lsfDisallowIncomingTrustline + boolean lsfDisallowIncomingTrustline, + boolean lsfAllowTrustlineClawback ) { long expectedFlags = (lsfDefaultRipple ? AccountRootFlags.DEFAULT_RIPPLE.getValue() : 0L) | (lsfDepositAuth ? AccountRootFlags.DEPOSIT_AUTH.getValue() : 0L) | @@ -67,7 +68,8 @@ public void testDeriveIndividualFlagsFromFlags( (lsfDisallowIncomingNFTokenOffer ? AccountRootFlags.DISALLOW_INCOMING_NFT_OFFER.getValue() : 0L) | (lsfDisallowIncomingCheck ? AccountRootFlags.DISALLOW_INCOMING_CHECK.getValue() : 0L) | (lsfDisallowIncomingPayChan ? AccountRootFlags.DISALLOW_INCOMING_PAY_CHAN.getValue() : 0L) | - (lsfDisallowIncomingTrustline ? AccountRootFlags.DISALLOW_INCOMING_TRUSTLINE.getValue() : 0L); + (lsfDisallowIncomingTrustline ? AccountRootFlags.DISALLOW_INCOMING_TRUSTLINE.getValue() : 0L) | + (lsfAllowTrustlineClawback ? AccountRootFlags.ALLOW_TRUSTLINE_CLAWBACK.getValue() : 0L); Flags flagsFromFlags = AccountRootFlags.of( (lsfDefaultRipple ? AccountRootFlags.DEFAULT_RIPPLE : AccountRootFlags.UNSET), (lsfDepositAuth ? AccountRootFlags.DEPOSIT_AUTH : AccountRootFlags.UNSET), @@ -81,7 +83,8 @@ public void testDeriveIndividualFlagsFromFlags( (lsfDisallowIncomingNFTokenOffer ? AccountRootFlags.DISALLOW_INCOMING_NFT_OFFER : AccountRootFlags.UNSET), (lsfDisallowIncomingCheck ? AccountRootFlags.DISALLOW_INCOMING_CHECK : AccountRootFlags.UNSET), (lsfDisallowIncomingPayChan ? AccountRootFlags.DISALLOW_INCOMING_PAY_CHAN : AccountRootFlags.UNSET), - (lsfDisallowIncomingTrustline ? AccountRootFlags.DISALLOW_INCOMING_TRUSTLINE : AccountRootFlags.UNSET) + (lsfDisallowIncomingTrustline ? AccountRootFlags.DISALLOW_INCOMING_TRUSTLINE : AccountRootFlags.UNSET), + (lsfAllowTrustlineClawback ? AccountRootFlags.ALLOW_TRUSTLINE_CLAWBACK : AccountRootFlags.UNSET) ); assertThat(flagsFromFlags.getValue()).isEqualTo(expectedFlags); @@ -102,6 +105,7 @@ public void testDeriveIndividualFlagsFromFlags( assertThat(flagsFromLong.lsfDisallowIncomingCheck()).isEqualTo(lsfDisallowIncomingCheck); assertThat(flagsFromLong.lsfDisallowIncomingPayChan()).isEqualTo(lsfDisallowIncomingPayChan); assertThat(flagsFromLong.lsfDisallowIncomingTrustline()).isEqualTo(lsfDisallowIncomingTrustline); + assertThat(flagsFromLong.lsfAllowTrustLineClawback()).isEqualTo(lsfAllowTrustlineClawback); } @ParameterizedTest @@ -120,7 +124,8 @@ void testJson( boolean lsfDisallowIncomingNFTokenOffer, boolean lsfDisallowIncomingCheck, boolean lsfDisallowIncomingPayChan, - boolean lsfDisallowIncomingTrustline + boolean lsfDisallowIncomingTrustline, + boolean lsfAllowTrustlineClawback ) throws JSONException, JsonProcessingException { Flags flags = AccountRootFlags.of( (lsfDefaultRipple ? AccountRootFlags.DEFAULT_RIPPLE : AccountRootFlags.UNSET), @@ -135,7 +140,8 @@ void testJson( (lsfDisallowIncomingNFTokenOffer ? AccountRootFlags.DISALLOW_INCOMING_NFT_OFFER : AccountRootFlags.UNSET), (lsfDisallowIncomingCheck ? AccountRootFlags.DISALLOW_INCOMING_CHECK : AccountRootFlags.UNSET), (lsfDisallowIncomingPayChan ? AccountRootFlags.DISALLOW_INCOMING_PAY_CHAN : AccountRootFlags.UNSET), - (lsfDisallowIncomingTrustline ? AccountRootFlags.DISALLOW_INCOMING_TRUSTLINE : AccountRootFlags.UNSET) + (lsfDisallowIncomingTrustline ? AccountRootFlags.DISALLOW_INCOMING_TRUSTLINE : AccountRootFlags.UNSET), + (lsfAllowTrustlineClawback ? AccountRootFlags.ALLOW_TRUSTLINE_CLAWBACK : AccountRootFlags.UNSET) ); FlagsWrapper flagsWrapper = FlagsWrapper.of(flags); diff --git a/xrpl4j-core/src/test/java/org/xrpl/xrpl4j/model/transactions/AccountSetTests.java b/xrpl4j-core/src/test/java/org/xrpl/xrpl4j/model/transactions/AccountSetTests.java index 4d1991dfa..5eb5a4479 100644 --- a/xrpl4j-core/src/test/java/org/xrpl/xrpl4j/model/transactions/AccountSetTests.java +++ b/xrpl4j-core/src/test/java/org/xrpl/xrpl4j/model/transactions/AccountSetTests.java @@ -152,12 +152,12 @@ void testWithEmptyClearFlagAndPresentInvalidRawValue() { .account(Address.of("rf1BiGeXwwQoi8Z2ueFYTEXSwuJYfV2Jpn")) .fee(XrpCurrencyAmount.ofDrops(12)) .sequence(UnsignedInteger.valueOf(5)) - .clearFlagRawValue(UnsignedInteger.valueOf(16)) + .clearFlagRawValue(UnsignedInteger.valueOf(AccountSetFlag.MAX_VALUE + 1)) .build(); assertThat(accountSet.clearFlag()).isEmpty(); assertThat(accountSet.clearFlagRawValue()).isNotEmpty().get() - .isEqualTo(UnsignedInteger.valueOf(16)); + .isEqualTo(UnsignedInteger.valueOf(AccountSetFlag.MAX_VALUE + 1)); } ////////////////////////////////////// @@ -245,12 +245,12 @@ void testWithEmptySetFlagAndPresentInvalidRawValue() { .account(Address.of("rf1BiGeXwwQoi8Z2ueFYTEXSwuJYfV2Jpn")) .fee(XrpCurrencyAmount.ofDrops(12)) .sequence(UnsignedInteger.valueOf(5)) - .setFlagRawValue(UnsignedInteger.valueOf(16)) + .setFlagRawValue(UnsignedInteger.valueOf(AccountSetFlag.MAX_VALUE + 1)) .build(); assertThat(accountSet.setFlag()).isEmpty(); assertThat(accountSet.setFlagRawValue()).isNotEmpty().get() - .isEqualTo(UnsignedInteger.valueOf(16)); + .isEqualTo(UnsignedInteger.valueOf(AccountSetFlag.MAX_VALUE + 1)); accountSet = AccountSet.builder() .account(Address.of("rf1BiGeXwwQoi8Z2ueFYTEXSwuJYfV2Jpn")) diff --git a/xrpl4j-core/src/test/java/org/xrpl/xrpl4j/model/transactions/ClawbackTest.java b/xrpl4j-core/src/test/java/org/xrpl/xrpl4j/model/transactions/ClawbackTest.java new file mode 100644 index 000000000..5eeb0410a --- /dev/null +++ b/xrpl4j-core/src/test/java/org/xrpl/xrpl4j/model/transactions/ClawbackTest.java @@ -0,0 +1,118 @@ +package org.xrpl.xrpl4j.model.transactions; + +import com.fasterxml.jackson.core.JsonProcessingException; +import com.google.common.primitives.UnsignedInteger; +import org.json.JSONException; +import org.junit.jupiter.api.Test; +import org.xrpl.xrpl4j.crypto.keys.PublicKey; +import org.xrpl.xrpl4j.model.AbstractJsonTest; +import org.xrpl.xrpl4j.model.flags.TransactionFlags; + +class ClawbackTest extends AbstractJsonTest { + + @Test + void testJsonWithoutFlags() throws JSONException, JsonProcessingException { + Clawback clawback = Clawback.builder() + .account(Address.of("rp6abvbTbjoce8ZDJkT6snvxTZSYMBCC9S")) + .fee(XrpCurrencyAmount.ofDrops(10)) + .sequence(UnsignedInteger.ONE) + .signingPublicKey(PublicKey.fromBase16EncodedPublicKey( + "02356E89059A75438887F9FEE2056A2890DB82A68353BE9C0C0C8F89C0018B37FC" + )) + .amount( + IssuedCurrencyAmount.builder() + .currency("FOO") + .issuer(Address.of("rsA2LpzuawewSBQXkiju3YQTMzW13pAAdW")) + .value("314.159") + .build() + ) + .build(); + + String json = "{\n" + + " \"TransactionType\": \"Clawback\",\n" + + " \"Account\": \"rp6abvbTbjoce8ZDJkT6snvxTZSYMBCC9S\",\n" + + " \"Amount\": {\n" + + " \"currency\": \"FOO\",\n" + + " \"issuer\": \"rsA2LpzuawewSBQXkiju3YQTMzW13pAAdW\",\n" + + " \"value\": \"314.159\"\n" + + " },\n" + + " \"Fee\": \"10\",\n" + + " \"Sequence\": 1,\n" + + " \"SigningPubKey\": \"02356E89059A75438887F9FEE2056A2890DB82A68353BE9C0C0C8F89C0018B37FC\"\n" + + "}"; + + assertCanSerializeAndDeserialize(clawback, json); + } + + @Test + void testJsonWithZeroFlags() throws JSONException, JsonProcessingException { + Clawback clawback = Clawback.builder() + .account(Address.of("rp6abvbTbjoce8ZDJkT6snvxTZSYMBCC9S")) + .fee(XrpCurrencyAmount.ofDrops(10)) + .sequence(UnsignedInteger.ONE) + .signingPublicKey(PublicKey.fromBase16EncodedPublicKey( + "02356E89059A75438887F9FEE2056A2890DB82A68353BE9C0C0C8F89C0018B37FC" + )) + .flags(TransactionFlags.UNSET) + .amount( + IssuedCurrencyAmount.builder() + .currency("FOO") + .issuer(Address.of("rsA2LpzuawewSBQXkiju3YQTMzW13pAAdW")) + .value("314.159") + .build() + ) + .build(); + + String json = "{\n" + + " \"TransactionType\": \"Clawback\",\n" + + " \"Account\": \"rp6abvbTbjoce8ZDJkT6snvxTZSYMBCC9S\",\n" + + " \"Amount\": {\n" + + " \"currency\": \"FOO\",\n" + + " \"issuer\": \"rsA2LpzuawewSBQXkiju3YQTMzW13pAAdW\",\n" + + " \"value\": \"314.159\"\n" + + " },\n" + + " \"Fee\": \"10\",\n" + + " \"Flags\": 0,\n" + + " \"Sequence\": 1,\n" + + " \"SigningPubKey\": \"02356E89059A75438887F9FEE2056A2890DB82A68353BE9C0C0C8F89C0018B37FC\"\n" + + "}"; + + assertCanSerializeAndDeserialize(clawback, json); + } + + @Test + void testJsonWithNonZeroFlags() throws JSONException, JsonProcessingException { + Clawback clawback = Clawback.builder() + .account(Address.of("rp6abvbTbjoce8ZDJkT6snvxTZSYMBCC9S")) + .fee(XrpCurrencyAmount.ofDrops(10)) + .sequence(UnsignedInteger.ONE) + .signingPublicKey(PublicKey.fromBase16EncodedPublicKey( + "02356E89059A75438887F9FEE2056A2890DB82A68353BE9C0C0C8F89C0018B37FC" + )) + .flags(TransactionFlags.FULLY_CANONICAL_SIG) + .amount( + IssuedCurrencyAmount.builder() + .currency("FOO") + .issuer(Address.of("rsA2LpzuawewSBQXkiju3YQTMzW13pAAdW")) + .value("314.159") + .build() + ) + .build(); + + String json = String.format("{\n" + + " \"TransactionType\": \"Clawback\",\n" + + " \"Account\": \"rp6abvbTbjoce8ZDJkT6snvxTZSYMBCC9S\",\n" + + " \"Amount\": {\n" + + " \"currency\": \"FOO\",\n" + + " \"issuer\": \"rsA2LpzuawewSBQXkiju3YQTMzW13pAAdW\",\n" + + " \"value\": \"314.159\"\n" + + " },\n" + + " \"Fee\": \"10\",\n" + + " \"Flags\": %s,\n" + + " \"Sequence\": 1,\n" + + " \"SigningPubKey\": \"02356E89059A75438887F9FEE2056A2890DB82A68353BE9C0C0C8F89C0018B37FC\"\n" + + "}", TransactionFlags.FULLY_CANONICAL_SIG); + + assertCanSerializeAndDeserialize(clawback, json); + } +} \ No newline at end of file diff --git a/xrpl4j-integration-tests/src/test/java/org/xrpl/xrpl4j/tests/AbstractIT.java b/xrpl4j-integration-tests/src/test/java/org/xrpl/xrpl4j/tests/AbstractIT.java index 6b240ac37..76315b2c7 100644 --- a/xrpl4j-integration-tests/src/test/java/org/xrpl/xrpl4j/tests/AbstractIT.java +++ b/xrpl4j-integration-tests/src/test/java/org/xrpl/xrpl4j/tests/AbstractIT.java @@ -23,9 +23,11 @@ import static org.assertj.core.api.Assertions.assertThat; import static org.awaitility.Awaitility.given; import static org.hamcrest.CoreMatchers.notNullValue; +import static org.hamcrest.Matchers.equalTo; import static org.hamcrest.core.Is.is; import com.fasterxml.jackson.core.JsonProcessingException; +import com.google.common.primitives.UnsignedInteger; import com.google.common.primitives.UnsignedLong; import org.awaitility.Durations; import org.slf4j.Logger; @@ -44,6 +46,8 @@ import org.xrpl.xrpl4j.crypto.signing.SingleSignedTransaction; import org.xrpl.xrpl4j.crypto.signing.bc.BcDerivedKeySignatureService; import org.xrpl.xrpl4j.crypto.signing.bc.BcSignatureService; +import org.xrpl.xrpl4j.model.client.Finality; +import org.xrpl.xrpl4j.model.client.FinalityStatus; import org.xrpl.xrpl4j.model.client.XrplResult; import org.xrpl.xrpl4j.model.client.accounts.AccountChannelsRequestParams; import org.xrpl.xrpl4j.model.client.accounts.AccountChannelsResult; @@ -54,6 +58,7 @@ import org.xrpl.xrpl4j.model.client.accounts.AccountObjectsRequestParams; import org.xrpl.xrpl4j.model.client.accounts.AccountObjectsResult; import org.xrpl.xrpl4j.model.client.accounts.TrustLine; +import org.xrpl.xrpl4j.model.client.common.LedgerIndex; import org.xrpl.xrpl4j.model.client.common.LedgerSpecifier; import org.xrpl.xrpl4j.model.client.ledger.LedgerRequestParams; import org.xrpl.xrpl4j.model.client.ledger.LedgerResult; @@ -204,6 +209,36 @@ protected void fundAccount(final Address address) { // Ledger Helpers ////////////////////// + protected Finality scanForFinality( + Hash256 transactionHash, + LedgerIndex submittedOnLedgerIndex, + UnsignedInteger lastLedgerSequence, + UnsignedInteger transactionAccountSequence, + Address account + ) { + return given() + .pollInterval(POLL_INTERVAL) + .atMost(Durations.ONE_MINUTE.dividedBy(2)) + .ignoreException(RuntimeException.class) + .await() + .until( + () -> xrplClient.isFinal( + transactionHash, + submittedOnLedgerIndex, + lastLedgerSequence, + transactionAccountSequence, + account + ), + is(equalTo( + Finality.builder() + .finalityStatus(FinalityStatus.VALIDATED_SUCCESS) + .resultCode(TransactionResultCodes.TES_SUCCESS) + .build() + ) + ) + ); + } + protected T scanForResult(Supplier resultSupplier, Predicate condition) { return given() .atMost(Durations.ONE_MINUTE.dividedBy(2)) diff --git a/xrpl4j-integration-tests/src/test/java/org/xrpl/xrpl4j/tests/ClawbackIT.java b/xrpl4j-integration-tests/src/test/java/org/xrpl/xrpl4j/tests/ClawbackIT.java new file mode 100644 index 000000000..f67df95d5 --- /dev/null +++ b/xrpl4j-integration-tests/src/test/java/org/xrpl/xrpl4j/tests/ClawbackIT.java @@ -0,0 +1,166 @@ +package org.xrpl.xrpl4j.tests; + +import static org.assertj.core.api.AssertionsForClassTypes.assertThat; + +import com.fasterxml.jackson.core.JsonProcessingException; +import com.google.common.primitives.UnsignedInteger; +import org.junit.jupiter.api.Test; +import org.xrpl.xrpl4j.client.JsonRpcClientErrorException; +import org.xrpl.xrpl4j.crypto.keys.KeyPair; +import org.xrpl.xrpl4j.crypto.signing.SingleSignedTransaction; +import org.xrpl.xrpl4j.model.client.accounts.AccountInfoRequestParams; +import org.xrpl.xrpl4j.model.client.accounts.AccountInfoResult; +import org.xrpl.xrpl4j.model.client.accounts.TrustLine; +import org.xrpl.xrpl4j.model.client.fees.FeeResult; +import org.xrpl.xrpl4j.model.client.fees.FeeUtils; +import org.xrpl.xrpl4j.model.client.transactions.SubmitResult; +import org.xrpl.xrpl4j.model.transactions.AccountSet; +import org.xrpl.xrpl4j.model.transactions.AccountSet.AccountSetFlag; +import org.xrpl.xrpl4j.model.transactions.Address; +import org.xrpl.xrpl4j.model.transactions.Clawback; +import org.xrpl.xrpl4j.model.transactions.IssuedCurrencyAmount; +import org.xrpl.xrpl4j.model.transactions.TransactionResultCodes; +import org.xrpl.xrpl4j.model.transactions.XrpCurrencyAmount; + +public class ClawbackIT extends AbstractIT { + + @Test + void issueBalanceAndClawback() throws JsonRpcClientErrorException, JsonProcessingException { + KeyPair issuerKeyPair = createRandomAccountEd25519(); + KeyPair holderKeyPair = createRandomAccountEd25519(); + + FeeResult feeResult = xrplClient.fee(); + AccountInfoResult issuerAccount = this.scanForResult( + () -> this.getValidatedAccountInfo(issuerKeyPair.publicKey().deriveAddress()) + ); + + XrpCurrencyAmount fee = FeeUtils.computeNetworkFees(feeResult).recommendedFee(); + setAllowClawback(issuerKeyPair, issuerAccount, fee); + + createTrustLine( + "USD", + "10000", + issuerKeyPair, + holderKeyPair, + fee + ); + + sendIssuedCurrency( + "USD", + "100", + issuerKeyPair, + holderKeyPair, + fee + ); + + issuerAccount = this.getValidatedAccountInfo(issuerAccount.accountData().account()); + clawback( + "USD", + "10", + holderKeyPair.publicKey().deriveAddress(), + issuerKeyPair, + issuerAccount, + fee + ); + + TrustLine trustline = this.getValidatedAccountLines( + issuerAccount.accountData().account(), + holderKeyPair.publicKey().deriveAddress() + ).lines().stream() + .filter(line -> line.currency().equals("USD")) + .findFirst() + .orElseThrow(() -> new RuntimeException("No trustline found.")); + + assertThat(trustline.balance()).isEqualTo("-90"); + + issuerAccount = this.getValidatedAccountInfo(issuerAccount.accountData().account()); + clawback( + "USD", + "90", + holderKeyPair.publicKey().deriveAddress(), + issuerKeyPair, + issuerAccount, + fee + ); + + trustline = this.getValidatedAccountLines( + issuerAccount.accountData().account(), + holderKeyPair.publicKey().deriveAddress() + ).lines().stream() + .filter(line -> line.currency().equals("USD")) + .findFirst() + .orElseThrow(() -> new RuntimeException("No trustline found.")); + assertThat(trustline.balance()).isEqualTo("0"); + } + + private void clawback( + String currencyCode, + String amount, + Address holderAddress, + KeyPair issuerKeyPair, + AccountInfoResult issuerAccountInfo, + XrpCurrencyAmount fee + ) throws JsonRpcClientErrorException, JsonProcessingException { + Clawback clawback = Clawback.builder() + .account(issuerKeyPair.publicKey().deriveAddress()) + .fee(fee) + .sequence(issuerAccountInfo.accountData().sequence()) + .signingPublicKey(issuerKeyPair.publicKey()) + .amount( + IssuedCurrencyAmount.builder() + .currency(currencyCode) + .value(amount) + .issuer(holderAddress) + .build() + ) + .lastLedgerSequence(issuerAccountInfo.ledgerIndexSafe().unsignedIntegerValue().plus(UnsignedInteger.valueOf(4))) + .build(); + + SingleSignedTransaction signedClawback = signatureService.sign(issuerKeyPair.privateKey(), clawback); + SubmitResult submitResult = xrplClient.submit(signedClawback); + assertThat(submitResult.engineResult()).isEqualTo(TransactionResultCodes.TES_SUCCESS); + + scanForFinality( + signedClawback.hash(), + issuerAccountInfo.ledgerIndexSafe(), + clawback.lastLedgerSequence().get(), + clawback.sequence(), + clawback.account() + ); + } + + private void setAllowClawback( + KeyPair issuerKeyPair, + AccountInfoResult issuerAccount, + XrpCurrencyAmount fee + ) throws JsonRpcClientErrorException, JsonProcessingException { + AccountSet accountSet = AccountSet.builder() + .account(issuerAccount.accountData().account()) + .fee(fee) + .sequence(issuerAccount.accountData().sequence()) + .signingPublicKey(issuerKeyPair.publicKey()) + .lastLedgerSequence(issuerAccount.ledgerIndexSafe().unsignedIntegerValue().plus(UnsignedInteger.valueOf(4))) + .setFlag(AccountSetFlag.ALLOW_TRUSTLINE_CLAWBACK) + .build(); + + SingleSignedTransaction signedAccountSet = signatureService.sign( + issuerKeyPair.privateKey(), accountSet + ); + SubmitResult submitResult = xrplClient.submit(signedAccountSet); + assertThat(submitResult.engineResult()).isEqualTo(TransactionResultCodes.TES_SUCCESS); + + scanForFinality( + signedAccountSet.hash(), + issuerAccount.ledgerIndexSafe(), + accountSet.lastLedgerSequence().get(), + accountSet.sequence(), + accountSet.account() + ); + + AccountInfoResult accountInfoAfterSet = xrplClient.accountInfo( + AccountInfoRequestParams.of(issuerAccount.accountData().account()) + ); + + assertThat(accountInfoAfterSet.accountData().flags().lsfAllowTrustLineClawback()).isTrue(); + } +} diff --git a/xrpl4j-integration-tests/src/test/java/org/xrpl/xrpl4j/tests/IssuedCurrencyIT.java b/xrpl4j-integration-tests/src/test/java/org/xrpl/xrpl4j/tests/IssuedCurrencyIT.java index 5ab7f776e..17f84fa61 100644 --- a/xrpl4j-integration-tests/src/test/java/org/xrpl/xrpl4j/tests/IssuedCurrencyIT.java +++ b/xrpl4j-integration-tests/src/test/java/org/xrpl/xrpl4j/tests/IssuedCurrencyIT.java @@ -181,7 +181,7 @@ public void issueIssuedCurrencyBalance() throws JsonRpcClientErrorException, Jso /////////////////////////// // Send some xrpl4jCoin to the counterparty account. - issueBalance( + sendIssuedCurrency( xrpl4jCoin, trustLine.limitPeer(), issuerKeyPair, counterpartyKeyPair, FeeUtils.computeNetworkFees(feeResult).recommendedFee() ); @@ -245,11 +245,23 @@ public void sendSimpleRipplingIssuedCurrencyPayment() throws JsonRpcClientErrorE /////////////////////////// // Issuer issues 50 USD to alice - issueBalance("USD", "50", issuerKeyPair, aliceKeyPair, FeeUtils.computeNetworkFees(feeResult).recommendedFee()); + sendIssuedCurrency( + "USD", + "50", + issuerKeyPair, + aliceKeyPair, + FeeUtils.computeNetworkFees(feeResult).recommendedFee() + ); /////////////////////////// // Issuer issues 50 USD to bob - issueBalance("USD", "50", issuerKeyPair, bobKeyPair, FeeUtils.computeNetworkFees(feeResult).recommendedFee()); + sendIssuedCurrency( + "USD", + "50", + issuerKeyPair, + bobKeyPair, + FeeUtils.computeNetworkFees(feeResult).recommendedFee() + ); /////////////////////////// // Try to find a path for this Payment. @@ -386,22 +398,46 @@ public void sendMultiHopSameCurrencyPayment() throws JsonRpcClientErrorException /////////////////////////// // Issue 10 USD from issuerA to charlie. // IssuerA now owes Charlie 10 USD. - issueBalance("USD", "10", issuerAKeyPair, charlieKeyPair, FeeUtils.computeNetworkFees(feeResult).recommendedFee()); + sendIssuedCurrency( + "USD", + "10", + issuerAKeyPair, + charlieKeyPair, + FeeUtils.computeNetworkFees(feeResult).recommendedFee() + ); /////////////////////////// // Issue 1 USD from issuerA to emily. // IssuerA now owes Emily 1 USD - issueBalance("USD", "1", issuerAKeyPair, emilyKeyPair, FeeUtils.computeNetworkFees(feeResult).recommendedFee()); + sendIssuedCurrency( + "USD", + "1", + issuerAKeyPair, + emilyKeyPair, + FeeUtils.computeNetworkFees(feeResult).recommendedFee() + ); /////////////////////////// // Issue 100 USD from issuerB to emily. // IssuerB now owes Emily 100 USD - issueBalance("USD", "100", issuerBKeyPair, emilyKeyPair, FeeUtils.computeNetworkFees(feeResult).recommendedFee()); + sendIssuedCurrency( + "USD", + "100", + issuerBKeyPair, + emilyKeyPair, + FeeUtils.computeNetworkFees(feeResult).recommendedFee() + ); /////////////////////////// // Issue 2 USD from issuerB to daniel. // IssuerB now owes Daniel 2 USD - issueBalance("USD", "2", issuerBKeyPair, danielKeyPair, FeeUtils.computeNetworkFees(feeResult).recommendedFee()); + sendIssuedCurrency( + "USD", + "2", + issuerBKeyPair, + danielKeyPair, + FeeUtils.computeNetworkFees(feeResult).recommendedFee() + ); /////////////////////////// // Look for a payment path from charlie to daniel. @@ -523,106 +559,4 @@ public void setDefaultRipple(KeyPair issuerKeyPair, FeeResult feeResult) ); } - /** - * Send issued currency funds from an issuer to a counterparty. - * - * @param currency The currency code to send. - * @param value The amount of currency to send. - * @param issuerKeyPair The {@link KeyPair} of the issuer account. - * @param counterpartyKeyPair The {@link KeyPair} of the counterparty account. - * @param fee The current network fee, as an {@link XrpCurrencyAmount}. - * @throws JsonRpcClientErrorException If anything goes wrong while communicating with rippled. - */ - public void issueBalance( - String currency, - String value, - KeyPair issuerKeyPair, - KeyPair counterpartyKeyPair, - XrpCurrencyAmount fee - ) throws JsonRpcClientErrorException, JsonProcessingException { - /////////////////////////// - // Issuer sends a payment with the issued currency to the counterparty - AccountInfoResult issuerAccountInfo = this.scanForResult( - () -> getValidatedAccountInfo(issuerKeyPair.publicKey().deriveAddress()) - ); - - Payment fundCounterparty = Payment.builder() - .account(issuerKeyPair.publicKey().deriveAddress()) - .fee(fee) - .sequence(issuerAccountInfo.accountData().sequence()) - .destination(counterpartyKeyPair.publicKey().deriveAddress()) - .amount(IssuedCurrencyAmount.builder() - .issuer(issuerKeyPair.publicKey().deriveAddress()) - .currency(currency) - .value(value) - .build()) - .signingPublicKey(issuerKeyPair.publicKey()) - .build(); - - SingleSignedTransaction signedFundCounterparty = signatureService.sign( - issuerKeyPair.privateKey(), fundCounterparty - ); - SubmitResult paymentResult = xrplClient.submit(signedFundCounterparty); - assertThat(paymentResult.engineResult()).isEqualTo("tesSUCCESS"); - logger.info( - "Payment transaction successful: https://testnet.xrpl.org/transactions/{}", - paymentResult.transactionResult().hash() - ); - - this.scanForResult(() -> getValidatedTransaction(paymentResult.transactionResult().hash(), Payment.class)); - } - - /** - * Create a trustline between the given issuer and counterparty accounts for the given currency code and with the - * given limit. - * - * @param currency The currency code of the trustline to create. - * @param value The trustline limit of the trustline to create. - * @param issuerKeyPair The {@link KeyPair} of the issuer account. - * @param counterpartyKeyPair The {@link KeyPair} of the counterparty account. - * @param fee The current network fee, as an {@link XrpCurrencyAmount}. - * @return The {@link TrustLine} that gets created. - * @throws JsonRpcClientErrorException If anything goes wrong while communicating with rippled. - */ - public TrustLine createTrustLine( - String currency, - String value, - KeyPair issuerKeyPair, - KeyPair counterpartyKeyPair, - XrpCurrencyAmount fee - ) throws JsonRpcClientErrorException, JsonProcessingException { - AccountInfoResult counterpartyAccountInfo = this.scanForResult( - () -> this.getValidatedAccountInfo(counterpartyKeyPair.publicKey().deriveAddress()) - ); - - TrustSet trustSet = TrustSet.builder() - .account(counterpartyKeyPair.publicKey().deriveAddress()) - .fee(fee) - .sequence(counterpartyAccountInfo.accountData().sequence()) - .limitAmount(IssuedCurrencyAmount.builder() - .currency(currency) - .issuer(issuerKeyPair.publicKey().deriveAddress()) - .value(value) - .build()) - .signingPublicKey(counterpartyKeyPair.publicKey()) - .build(); - - SingleSignedTransaction signedTrustSet = signatureService.sign(counterpartyKeyPair.privateKey(), - trustSet); - SubmitResult trustSetSubmitResult = xrplClient.submit(signedTrustSet); - assertThat(trustSetSubmitResult.engineResult()).isEqualTo("tesSUCCESS"); - logger.info( - "TrustSet transaction successful: https://testnet.xrpl.org/transactions/{}", - trustSetSubmitResult.transactionResult().hash() - ); - - return scanForResult( - () -> - getValidatedAccountLines(issuerKeyPair.publicKey().deriveAddress(), - counterpartyKeyPair.publicKey().deriveAddress()), - linesResult -> !linesResult.lines().isEmpty() - ) - .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 5947fe6d9..8b9565494 100644 --- a/xrpl4j-integration-tests/src/test/resources/rippled/rippled.cfg +++ b/xrpl4j-integration-tests/src/test/resources/rippled/rippled.cfg @@ -1284,3 +1284,7 @@ fixTrustLinesToSelf fixUniversalNumber ImmediateOfferKilled XRPFees +fixNFTokenRemint +fixReducedOffersV1 +Clawback +AMM \ No newline at end of file