Skip to content

Commit

Permalink
Add support for Clawback (#485)
Browse files Browse the repository at this point in the history
* add support for clawback amendment

* checkstyle

* fix normalization functions in AccountSet

* mark all Clawback code @beta

* mark CLAWBACK transaction type beta
  • Loading branch information
nkramer44 authored Sep 20, 2023
1 parent c0efe11 commit dae2fd6
Show file tree
Hide file tree
Showing 14 changed files with 540 additions and 123 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -271,6 +272,10 @@ public <T extends Transaction> SingleSignedTransaction<T> 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.");
Expand Down Expand Up @@ -405,6 +410,10 @@ public <T extends Transaction> 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.");
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@
* =========================LICENSE_END==================================
*/

import com.google.common.annotations.Beta;
import org.xrpl.xrpl4j.model.transactions.AccountSet;

/**
Expand Down Expand Up @@ -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.
*
* <p>This constant will be marked {@link Beta} until the Clawback amendment is enabled on mainnet. Its API is subject
* to change.</p>
*/
@Beta
public static final AccountRootFlags ALLOW_TRUSTLINE_CLAWBACK = new AccountRootFlags(0x80000000L);

/**
* Required-args Constructor.
*
Expand All @@ -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) {
Expand Down Expand Up @@ -235,4 +246,17 @@ public boolean lsfDisallowIncomingPayChan() {
public boolean lsfDisallowIncomingTrustline() {
return this.isSet(AccountRootFlags.DISALLOW_INCOMING_TRUSTLINE);
}

/**
* Allows trustline clawback on this account.
*
* <p>This constant will be marked {@link Beta} until the Clawback amendment is enabled on mainnet. Its API is subject
* to change.</p>
*
* @return {@code true} if {@code lsfAllowTrustLineClawback} is set, otherwise {@code false}.
*/
@Beta
public boolean lsfAllowTrustLineClawback() {
return this.isSet(AccountRootFlags.ALLOW_TRUSTLINE_CLAWBACK);
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -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;

/**
Expand Down Expand Up @@ -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()))
Expand Down Expand Up @@ -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()))
Expand Down Expand Up @@ -440,14 +443,28 @@ enum AccountSetFlag {
/**
* Block incoming Trustlines.
*/
DISALLOW_INCOMING_TRUSTLINE(15);
DISALLOW_INCOMING_TRUSTLINE(15),
/**
* Enable clawback on the account's trustlines.
*
* <p>This value will be marked {@link Beta} until the Clawback amendment is enabled on mainnet. Its API is subject
* to change.</p>
*/
@Beta
ALLOW_TRUSTLINE_CLAWBACK(16);

final int value;

AccountSetFlag(int value) {
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.
Expand Down
Original file line number Diff line number Diff line change
@@ -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.
*
* <p>This class will be marked {@link Beta} until the Clawback amendment is enabled on mainnet. Its API is subject
* to change.</p>
*/
@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();


}
Original file line number Diff line number Diff line change
Expand Up @@ -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();

/**
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand Down Expand Up @@ -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.
*
* <p>This constant will be marked {@link Beta} until the Clawback amendment is enabled on mainnet. Its API is subject
* to change.</p>
*/
@Beta
CLAWBACK("Clawback");

private final String value;

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -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));
Expand Down Expand Up @@ -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());
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -34,7 +34,7 @@
public class AccountRootFlagsTests extends AbstractFlagsTest {

public static Stream<Arguments> data() {
return getBooleanCombinations(13);
return getBooleanCombinations(14);
}

@ParameterizedTest
Expand All @@ -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) |
Expand All @@ -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),
Expand All @@ -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);

Expand All @@ -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
Expand All @@ -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),
Expand All @@ -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);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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));
}

//////////////////////////////////////
Expand Down Expand Up @@ -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"))
Expand Down
Loading

0 comments on commit dae2fd6

Please sign in to comment.