Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Fixes #474 #535

Merged
merged 23 commits into from
Aug 23, 2024
Merged
Show file tree
Hide file tree
Changes from 1 commit
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@
* =========================LICENSE_END==================================
*/

import java.math.BigDecimal;
import java.util.Objects;
import java.util.function.Consumer;
import java.util.function.Function;
Expand All @@ -34,9 +35,17 @@ public interface CurrencyAmount {
long ONE_XRP_IN_DROPS = 1_000_000L;
long MAX_XRP = 100_000_000_000L; // <-- per https://xrpl.org/rippleapi-reference.html#value
long MAX_XRP_IN_DROPS = MAX_XRP * ONE_XRP_IN_DROPS;
BigDecimal MAX_XRP_BD = BigDecimal.valueOf(MAX_XRP);

/**
* Handle this {@link CurrencyAmount} depending on its actual polymorphic sub-type.
* Indicates whether this amount is positive or negative.
*
* @return {@code true} if this amount is negative; {@code false} otherwise (i.e., if the value is 0 or positive).
*/
boolean isNegative();

/**
* Handle this {@link CurrencyAmount} depending on its actual polymorphic subtype.
*
* @param xrpCurrencyAmountHandler A {@link Consumer} that is called if this instance is of type
* {@link XrpCurrencyAmount}.
Expand All @@ -60,7 +69,7 @@ default void handle(
}

/**
* Map this {@link CurrencyAmount} to an instance of {@link R}, depending on its actualy polymorphic sub-type.
* Map this {@link CurrencyAmount} to an instance of {@link R}, depending on its actual polymorphic subtype.
*
* @param xrpCurrencyAmountMapper A {@link Function} that is called if this instance is of type
* {@link XrpCurrencyAmount}.
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand All @@ -20,9 +20,12 @@
* =========================LICENSE_END==================================
*/

import com.fasterxml.jackson.annotation.JsonIgnore;
import com.fasterxml.jackson.databind.annotation.JsonDeserialize;
import com.fasterxml.jackson.databind.annotation.JsonSerialize;
import org.immutables.value.Value;
import org.immutables.value.Value.Default;
import org.immutables.value.Value.Derived;

/**
* A {@link CurrencyAmount} for Issued Currencies on the XRP Ledger.
Expand All @@ -45,14 +48,14 @@ public interface IssuedCurrencyAmount extends CurrencyAmount {
String MIN_VALUE = "-9999999999999999e80";

/**
* The smallest possible positive value that an {@link IssuedCurrencyAmount} can have. Put another way,
* this value is the closest an {@link IssuedCurrencyAmount}'s {@link #value()} can be to zero if it is positive.
* The smallest possible positive value that an {@link IssuedCurrencyAmount} can have. Put another way, this value is
* the closest an {@link IssuedCurrencyAmount}'s {@link #value()} can be to zero if it is positive.
*/
String MIN_POSITIVE_VALUE = "1000000000000000e-96";

/**
* The largest possible negative value that an {@link IssuedCurrencyAmount} can have. Put another way,
* this value is the closest an {@link IssuedCurrencyAmount}'s {@link #value()} can be to zero if it is negative.
* The largest possible negative value that an {@link IssuedCurrencyAmount} can have. Put another way, this value is
* the closest an {@link IssuedCurrencyAmount}'s {@link #value()} can be to zero if it is negative.
*/
String MAX_NEGATIVE_VALUE = "-1000000000000000e-96";

Expand All @@ -67,10 +70,10 @@ static ImmutableIssuedCurrencyAmount.Builder builder() {

/**
* Quoted decimal representation of the amount of currency. This can include scientific notation, such as 1.23e11
* meaning 123,000,000,000. Both e and E may be used. Note that while this implementation merely holds a {@link
* String} with no value restrictions, the XRP Ledger does not tolerate unlimited precision values. Instead, non-XRP
* values (i.e., values held in this object) can have up to 16 decimal digits of precision, with a maximum value of
* 9999999999999999e80. The smallest positive non-XRP value is 1e-81.
* meaning 123,000,000,000. Both e and E may be used. Note that while this implementation merely holds a
* {@link String} with no value restrictions, the XRP Ledger does not tolerate unlimited precision values. Instead,
* non-XRP values (i.e., values held in this object) can have up to 16 decimal digits of precision, with a maximum
* value of 9999999999999999e80. The smallest positive non-XRP value is 1e-81.
*
* @return A {@link String} containing the amount of this issued currency.
*/
Expand All @@ -91,4 +94,15 @@ static ImmutableIssuedCurrencyAmount.Builder builder() {
*/
Address issuer();

/**
* Indicates whether this amount is positive or negative.
*
* @return {@code true} if this amount is negative; {@code false} otherwise (i.e., if the value is 0 or positive).
*/
@Derived
@JsonIgnore // <-- This is not actually part of the binary serialization format, so exclude from JSON
default boolean isNegative() {
return value().startsWith("-");
}

}
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@
* =========================LICENSE_END==================================
*/

import com.fasterxml.jackson.annotation.JsonIgnore;
import com.fasterxml.jackson.annotation.JsonRawValue;
import com.fasterxml.jackson.databind.annotation.JsonDeserialize;
import com.fasterxml.jackson.databind.annotation.JsonSerialize;
Expand All @@ -29,6 +30,7 @@
import com.google.common.primitives.UnsignedInteger;
import com.google.common.primitives.UnsignedLong;
import org.immutables.value.Value;
import org.immutables.value.Value.Default;
import org.xrpl.xrpl4j.model.immutables.FluentCompareTo;
import org.xrpl.xrpl4j.model.immutables.Wrapped;
import org.xrpl.xrpl4j.model.immutables.Wrapper;
Expand Down Expand Up @@ -171,14 +173,21 @@
static final DecimalFormat FORMATTER = new DecimalFormat("###,###");

/**
* Constructs an {@link XrpCurrencyAmount} using a number of drops.
* Constructs an {@link XrpCurrencyAmount} using a number of drops. Because XRP is capped to 100B units (1e17
* drops), this value will never overflow Java's signed long number.
*
* @param drops A long representing the number of drops of XRP of this amount.
*
* @return An {@link XrpCurrencyAmount} of {@code drops}.
*/
public static XrpCurrencyAmount ofDrops(long drops) {
return ofDrops(UnsignedLong.valueOf(drops));
public static XrpCurrencyAmount ofDrops(final long drops) {
if (drops < 0) {
// Normalize the drops value to be a positive number; indicated negativity via property.
return ofDrops(UnsignedLong.valueOf(drops * -1), true);
} else {
// Default to positive number and negativity indicator of `false`.
return ofDrops(UnsignedLong.valueOf(drops));
}
}

/**
Expand All @@ -188,8 +197,28 @@
*
* @return An {@link XrpCurrencyAmount} of {@code drops}.
*/
public static XrpCurrencyAmount ofDrops(UnsignedLong drops) {
return XrpCurrencyAmount.of(drops);
public static XrpCurrencyAmount ofDrops(final UnsignedLong drops) {
Objects.requireNonNull(drops);

// Note: ofDrops() throws an exception if too big.
return _XrpCurrencyAmount.ofDrops(drops, false);
}

/**
* Constructs an {@link XrpCurrencyAmount} using a number of drops.
*
* @param drops An {@link UnsignedLong} representing the number of drops of XRP of this amount.
* @param isNegative Indicates whether this amount is positive or negative.
*
* @return An {@link XrpCurrencyAmount} of {@code drops}.
*/
public static XrpCurrencyAmount ofDrops(final UnsignedLong drops, final boolean isNegative) {
Objects.requireNonNull(drops);

return XrpCurrencyAmount.builder()
.value(drops)
.isNegative(isNegative)
.build();
}

/**
Expand All @@ -199,11 +228,44 @@
*
* @return An {@link XrpCurrencyAmount} of the amount of drops in {@code amount}.
*/
public static XrpCurrencyAmount ofXrp(BigDecimal amount) {
if (FluentCompareTo.is(amount).notEqualTo(BigDecimal.ZERO)) {
Preconditions.checkArgument(FluentCompareTo.is(amount).greaterThanEqualTo(SMALLEST_XRP));
public static XrpCurrencyAmount ofXrp(final BigDecimal amount) {
Objects.requireNonNull(amount);

if (FluentCompareTo.is(amount).equalTo(BigDecimal.ZERO)) {
return ofDrops(UnsignedLong.ZERO);
}

// Whether positive or negative, clamp the amount to 0 if it's too small, or 100B if it's too big.
sappenin marked this conversation as resolved.
Show resolved Hide resolved
final BigDecimal absAmount = amount.abs();

Preconditions.checkArgument(
FluentCompareTo.is(absAmount).greaterThanEqualTo(SMALLEST_XRP),
String.format("Amount must be greater-than %s", SMALLEST_XRP)
);

Preconditions.checkArgument(
FluentCompareTo.is(absAmount).lessThanOrEqualTo(MAX_XRP_BD),
String.format("Amount must be less-than-or-equal to %s", MAX_XRP_BD)
);

if (amount.signum() == 0) { // zero
return ofDrops(UnsignedLong.ZERO); // <-- Should never happen per the first check, but just in case.s

Check warning on line 252 in xrpl4j-core/src/main/java/org/xrpl/xrpl4j/model/transactions/Wrappers.java

View check run for this annotation

Codecov / codecov/patch

xrpl4j-core/src/main/java/org/xrpl/xrpl4j/model/transactions/Wrappers.java#L252

Added line #L252 was not covered by tests
} else { // positive or negative
final boolean isNegative = amount.signum() < 0;
return ofDrops(UnsignedLong.valueOf(absAmount.scaleByPowerOfTen(6).toBigIntegerExact()), isNegative);
}
return ofDrops(UnsignedLong.valueOf(amount.scaleByPowerOfTen(6).toBigIntegerExact()));
}

/**
* Indicates whether this amount is positive or negative.
*
* @return {@code true} if this amount is negative; {@code false} otherwise (i.e., if the value is 0 or positive).
*/
@Default
@Override
@JsonIgnore // <-- This is not actually part of the binary serialization format, so exclude from JSON
sappenin marked this conversation as resolved.
Show resolved Hide resolved
public boolean isNegative() {
return false;
}

/**
Expand All @@ -213,8 +275,13 @@
* @return A {@link BigDecimal} representing this value denominated in whole XRP units.
*/
public BigDecimal toXrp() {
return new BigDecimal(this.value().bigIntegerValue())
sappenin marked this conversation as resolved.
Show resolved Hide resolved
final BigDecimal amount = new BigDecimal(this.value().bigIntegerValue())
.divide(BigDecimal.valueOf(ONE_XRP_IN_DROPS), MathContext.DECIMAL128);
if (this.isNegative()) {
return amount.negate();
} else {
return amount;
}
}

/**
Expand All @@ -225,7 +292,11 @@
* @return The sum of this amount and the {@code other} amount, as an {@link XrpCurrencyAmount}.
*/
public XrpCurrencyAmount plus(XrpCurrencyAmount other) {
return XrpCurrencyAmount.of(this.value().plus(other.value()));
// Convert each value to a long (positive or negative works)
long result =
this.value().longValue() * (this.isNegative() ? -1 : 1) +
other.value().longValue() * (other.isNegative() ? -1 : 1);
return XrpCurrencyAmount.ofDrops(result);
}

/**
Expand All @@ -236,7 +307,11 @@
* @return The difference of this amount and the {@code other} amount, as an {@link XrpCurrencyAmount}.
*/
public XrpCurrencyAmount minus(XrpCurrencyAmount other) {
return XrpCurrencyAmount.of(this.value().minus(other.value()));
// Convert each value to a long (positive or negative works)
long result =
this.value().longValue() * (this.isNegative() ? -1 : 1) -
other.value().longValue() * (other.isNegative() ? -1 : 1);
return XrpCurrencyAmount.ofDrops(result);
}

/**
Expand All @@ -247,12 +322,16 @@
* @return The product of this amount and the {@code other} amount, as an {@link XrpCurrencyAmount}.
*/
public XrpCurrencyAmount times(XrpCurrencyAmount other) {
return XrpCurrencyAmount.of(this.value().times(other.value()));
return XrpCurrencyAmount.ofDrops(this.value().times(other.value()), this.isNegative() | other.isNegative());
}

@Override
public String toString() {
return this.value().toString();
if (isNegative()) {
return "-" + this.value().toString();
sappenin marked this conversation as resolved.
Show resolved Hide resolved
} else {
return this.value().toString();
}
}

/**
Expand Down Expand Up @@ -504,8 +583,8 @@
/**
* A wrapped {@link com.google.common.primitives.UnsignedLong} containing an XChainClaimID.
*
* <p>This class will be marked {@link com.google.common.annotations.Beta} until the featureXChainBridge amendment is
* enabled on mainnet. Its API is subject to change.</p>
* <p>This class will be marked {@link com.google.common.annotations.Beta} until the featureXChainBridge amendment
* is enabled on mainnet. Its API is subject to change.</p>
*/
@Value.Immutable
@Wrapped
Expand All @@ -526,8 +605,8 @@
* wrapper mostly exists to ensure we serialize fields of this type as a hex String in JSON, as these fields are
* STUInt64s in rippled, which are hex encoded in JSON.
*
* <p>This class will be marked {@link com.google.common.annotations.Beta} until the featureXChainBridge amendment is
* enabled on mainnet. Its API is subject to change.</p>
* <p>This class will be marked {@link com.google.common.annotations.Beta} until the featureXChainBridge amendment
* is enabled on mainnet. Its API is subject to change.</p>
*/
@Value.Immutable
@Wrapped
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,7 @@
*/

/**
* Market interface for XRP Ledger Objects as represented in transaction metadata. Unlike descendants of
* Marker interface for XRP Ledger Objects as represented in transaction metadata. Unlike descendants of
* {@link org.xrpl.xrpl4j.model.ledger.LedgerObject}, all descendants of this interface will have all fields typed as
* {@link java.util.Optional} because ledger objects represented in transaction metadata often do not contain
* all fields of the ledger object.
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -32,13 +32,34 @@
import org.xrpl.xrpl4j.codec.binary.XrplBinaryCodec;
import org.xrpl.xrpl4j.model.jackson.ObjectMapperFactory;

import java.math.BigDecimal;
import java.nio.charset.StandardCharsets;

/**
* Unit tests for {@link CurrencyAmount}.
*/
public class CurrencyAmountTest {

@Test
public void isNegative() {
// Not negative
CurrencyAmount currencyAmount = new CurrencyAmount() {
@Override
public boolean isNegative() {
return false;
}
};
assertThat(currencyAmount.isNegative()).isFalse();
// Negative
currencyAmount = new CurrencyAmount() {
@Override
public boolean isNegative() {
return true;
}
};
assertThat(currencyAmount.isNegative()).isTrue();
}

@Test
public void handleXrp() {
XrpCurrencyAmount xrpCurrencyAmount = XrpCurrencyAmount.ofDrops(0L);
Expand All @@ -59,14 +80,12 @@ public void handleXrp() {
);

// Unhandled...
CurrencyAmount currencyAmount = new CurrencyAmount() {
};
CurrencyAmount currencyAmount = () -> false;
assertThrows(IllegalStateException.class, () ->
currencyAmount.handle(($) -> new Object(), ($) -> new Object())
);
}


@Test
public void handleIssuance() {
final IssuedCurrencyAmount issuedCurrencyAmount = IssuedCurrencyAmount.builder()
Expand Down Expand Up @@ -110,8 +129,7 @@ public void mapXrp() {
);

// Unhandled...
CurrencyAmount currencyAmount = new CurrencyAmount() {
};
CurrencyAmount currencyAmount = () -> false;
assertThrows(IllegalStateException.class, () ->
currencyAmount.map(($) -> new Object(), ($) -> new Object())
);
Expand Down Expand Up @@ -220,5 +238,6 @@ void testConstants() {
assertThat(CurrencyAmount.ONE_XRP_IN_DROPS).isEqualTo(1_000_000L);
assertThat(CurrencyAmount.MAX_XRP).isEqualTo(100_000_000_000L);
assertThat(CurrencyAmount.MAX_XRP_IN_DROPS).isEqualTo(100_000_000_000_000_000L);
assertThat(CurrencyAmount.MAX_XRP_BD).isEqualTo(new BigDecimal(100_000_000_000L));
}
}
Loading
Loading