Skip to content

Commit

Permalink
Fixes #474 (#535)
Browse files Browse the repository at this point in the history
* Fixes #474 by adding the `isNegative` function to `CurrencyAmount`, and then properly supporting negative value detection in XRP and IssuedCurrency amounts.
* Fixes #527.
* Fixes #473
* Updated and enhanced unit tests
  • Loading branch information
sappenin committed Aug 23, 2024
1 parent 2f07675 commit a1669af
Show file tree
Hide file tree
Showing 25 changed files with 25,236 additions and 114 deletions.
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 @@ -31,6 +31,7 @@
import org.xrpl.xrpl4j.codec.binary.BinaryCodecObjectMapperFactory;
import org.xrpl.xrpl4j.codec.binary.math.MathUtils;
import org.xrpl.xrpl4j.codec.binary.serdes.BinaryParser;
import org.xrpl.xrpl4j.model.immutables.FluentCompareTo;

import java.math.BigDecimal;
import java.math.BigInteger;
Expand All @@ -41,7 +42,9 @@
class AmountType extends SerializedType<AmountType> {

public static final BigDecimal MAX_DROPS = new BigDecimal("1e17");
public static final BigDecimal MIN_DROPS = new BigDecimal("-1e17");
public static final BigDecimal MIN_XRP = new BigDecimal("1e-6");
public static final BigDecimal MAX_NEGATIVE_XRP = new BigDecimal("-1e-6");

public static final String DEFAULT_AMOUNT_HEX = "4000000000000000";
public static final String ZERO_CURRENCY_AMOUNT_HEX = "8000000000000000";
Expand All @@ -50,18 +53,18 @@ class AmountType extends SerializedType<AmountType> {
private static final int MAX_IOU_PRECISION = 16;

/**
* According to <a href=https://xrpl.org/currency-formats.html#currency-formats>xrpl.org</a>,
* the minimum token value exponent is -96. However, because the value field is converted from a {@link String}
* to a {@link BigDecimal} when encoding/decoding, and because {@link BigDecimal} defaults to using single
* digit number, the minimum exponent in this context is -96 + 15, as XRPL amounts have a precision of 15 digits.
* According to <a href=https://xrpl.org/currency-formats.html#currency-formats>xrpl.org</a>, the minimum token value
* exponent is -96. However, because the value field is converted from a {@link String} to a {@link BigDecimal} when
* encoding/decoding, and because {@link BigDecimal} defaults to using single digit number, the minimum exponent in
* this context is -96 + 15, as XRPL amounts have a precision of 15 digits.
*/
private static final int MIN_IOU_EXPONENT = -81;

/**
* According to <a href=https://xrpl.org/currency-formats.html#currency-formats>xrpl.org</a>,
* the maximum token value exponent is 80. However, because the value field is converted from a {@link String}
* to a {@link BigDecimal} when encoding/decoding, and because {@link BigDecimal} defaults to using single
* digit number, the maximum exponent in this context is 80 + 15, as XRPL amounts have a precision of 15 digits.
* According to <a href=https://xrpl.org/currency-formats.html#currency-formats>xrpl.org</a>, the maximum token value
* exponent is 80. However, because the value field is converted from a {@link String} to a {@link BigDecimal} when
* encoding/decoding, and because {@link BigDecimal} defaults to using single digit number, the maximum exponent in
* this context is 80 + 15, as XRPL amounts have a precision of 15 digits.
*/
private static final int MAX_IOU_EXPONENT = 95;

Expand All @@ -88,8 +91,15 @@ private static void assertXrpIsValid(String amount) {
}
BigDecimal value = new BigDecimal(amount);
if (!value.equals(BigDecimal.ZERO)) {
if (value.compareTo(MIN_XRP) < 0 || value.compareTo(MAX_DROPS) > 0) {
throw new IllegalArgumentException(amount + " is an illegal amount");
final FluentCompareTo<BigDecimal> fluentValue = FluentCompareTo.is(value);
if (value.signum() < 0) { // `value` is negative
if (fluentValue.greaterThan(MAX_NEGATIVE_XRP) || fluentValue.lessThan(MIN_DROPS)) {
throw new IllegalArgumentException(String.format("%s is an illegal amount", amount));
}
} else { // `value` is positive
if (fluentValue.lessThan(MIN_XRP) || fluentValue.greaterThan(MAX_DROPS)) {
throw new IllegalArgumentException(String.format("%s is an illegal amount", amount));
}
}
}
}
Expand Down Expand Up @@ -142,14 +152,19 @@ public AmountType fromJson(JsonNode value) throws JsonProcessingException {
if (value.isValueNode()) {
assertXrpIsValid(value.asText());

UnsignedByteArray number = UnsignedByteArray.fromHex(
final boolean isValueNegative = value.asText().startsWith("-");
final UnsignedByteArray number = UnsignedByteArray.fromHex(
ByteUtils.padded(
UnsignedLong.valueOf(value.asText()).toString(16),
64 / 4
UnsignedLong
.valueOf(isValueNegative ? value.asText().substring(1) : value.asText())
.toString(16),
16 // <-- 64 / 4
)
);
byte[] rawBytes = number.toByteArray();
rawBytes[0] |= 0x40;
final byte[] rawBytes = number.toByteArray();
if (!isValueNegative) {
rawBytes[0] |= 0x40;
}
return new AmountType(UnsignedByteArray.of(rawBytes));
}

Expand All @@ -172,7 +187,7 @@ public AmountType fromJson(JsonNode value) throws JsonProcessingException {
private UnsignedByteArray getAmountBytes(BigDecimal number) {
BigInteger paddedNumber = MathUtils.toPaddedBigInteger(number, 16);
byte[] amountBytes = ByteUtils.toByteArray(paddedNumber, 8);
amountBytes[0] |= 0x80;
amountBytes[0] |= (byte) 0x80;
if (number.compareTo(BigDecimal.ZERO) > 0) {
amountBytes[0] |= 0x40;
}
Expand All @@ -182,8 +197,8 @@ private UnsignedByteArray getAmountBytes(BigDecimal number) {
throw new IllegalArgumentException("exponent out of range");
}
UnsignedByte exponentByte = UnsignedByte.of(97 + exponent - 15);
amountBytes[0] |= exponentByte.asInt() >>> 2;
amountBytes[1] |= (exponentByte.asInt() & 0x03) << 6;
amountBytes[0] |= (byte) (exponentByte.asInt() >>> 2);
amountBytes[1] |= (byte) ((exponentByte.asInt() & 0x03) << 6);

return UnsignedByteArray.of(amountBytes);
}
Expand Down
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,13 @@
* =========================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.Auxiliary;
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 +49,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 +71,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 +95,16 @@ 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
@Auxiliary
default boolean isNegative() {
return value().startsWith("-");
}

}
Loading

0 comments on commit a1669af

Please sign in to comment.