package com.ripple.core.coretypes; import com.ripple.core.coretypes.uint.UInt64; import com.ripple.core.fields.AmountField; import com.ripple.core.fields.Field; import com.ripple.core.fields.Type; import com.ripple.core.serialized.BinaryParser; import com.ripple.core.serialized.BytesSink; import com.ripple.core.serialized.SerializedType; import com.ripple.core.serialized.TypeTranslator; import org.json.JSONObject; import java.math.BigDecimal; import java.math.BigInteger; import java.math.MathContext; import java.math.RoundingMode; /** * In ripple, amounts are either XRP, the native currency, or an IOU of * a given currency as issued by a designated account. */ public class Amount extends Number implements SerializedType, Comparable<Amount> { private static BigDecimal TAKER_PAYS_FOR_THAT_DAMN_OFFER = new BigDecimal("1000000000000.000100"); // public static final Amount NEUTRAL_ZERO = new Amount(Currency.NEUTRAL, AccountID.NEUTRAL); /** * Thrown when an Amount is constructed with an invalid value */ public static class PrecisionError extends RuntimeException { public Amount illegal; public PrecisionError(String s) { super(s); } public PrecisionError(String s, Amount amount) { super(s); illegal = amount; } } // For rounding/multiplying/dividing public static final MathContext MATH_CONTEXT = new MathContext(16, RoundingMode.HALF_UP); // The maximum amount of digits in mantissa of an IOU amount public static final int MAXIMUM_IOU_PRECISION = 16; // The smallest quantity of an XRP is a drop, 1 millionth of an XRP public static final int MAXIMUM_NATIVE_SCALE = 6; // Defines bounds for native amounts public static final BigDecimal MAX_NATIVE_VALUE = parseDecimal("100,000,000,000.0"); public static final BigDecimal MIN_NATIVE_VALUE = parseDecimal("0.000,001"); // These are flags used when serializing to binary form public static final UInt64 BINARY_FLAG_IS_IOU = new UInt64("8000000000000000", 16); public static final UInt64 BINARY_FLAG_IS_NON_NEGATIVE_NATIVE = new UInt64("4000000000000000", 16); public static final Amount ONE_XRP = fromString("1.0"); // The quantity of XRP or Issue(currency/issuer pairing) // When native, the value unit is XRP, not drops. private BigDecimal value; private Currency currency; // If the currency is XRP private boolean isNative; // Normally, in the constructor of an Amount the value is checked // that it's scale/precision and quantity are correctly bounded. // If unbounded is true, these checks are skipped. // This is there for historical ledgers that contain amounts that // would now be considered malformed (in the sense of the transaction // engine result class temMALFORMED) private boolean unbounded = false; // The ZERO account is used for specifying the issuer for native // amounts. In practice the issuer is never used when an // amount is native. private AccountID issuer; // While internally the value is stored as a BigDecimal // the mantissa and exponent, as per the binary // format can be computed. // The mantissa is computed lazily, then cached private UInt64 mantissa = null; // The exponent is always calculated. private int exponent; public Amount(BigDecimal value, Currency currency, AccountID issuer) { this(value, currency, issuer, false); } public Amount(BigDecimal value) { isNative = true; currency = Currency.XRP; this.setAndCheckValue(value); } public Amount(BigDecimal value, Currency currency, AccountID issuer, boolean isNative, boolean unbounded) { this.isNative = isNative; this.currency = currency; this.unbounded = unbounded; this.setAndCheckValue(value); // done AFTER set value which sets some default values this.issuer = issuer; } public Amount(Currency currency, AccountID account) { this(BigDecimal.ZERO, currency, account); } // Private constructors Amount(BigDecimal newValue, Currency currency, AccountID issuer, boolean isNative) { this(newValue, currency, issuer, isNative, false); } private Amount(BigDecimal value, String currency, String issuer) { this(value, currency); if (issuer != null) { this.issuer = AccountID.fromString(issuer); } } public Amount(BigDecimal value, String currency) { isNative = false; this.currency = Currency.fromString(currency); this.setAndCheckValue(value); } private void setAndCheckValue(BigDecimal value) { this.value = value.stripTrailingZeros(); initialize(); } private void initialize() { if (isNative()) { issuer = AccountID.XRP_ISSUER; if (!unbounded) { checkXRPBounds(); } // Offset is unused for native amounts exponent = -6; // compared to drops. } else { issuer = AccountID.NEUTRAL; exponent = calculateExponent(); if (value.precision() > MAXIMUM_IOU_PRECISION && !unbounded) { String err = "value precision of " + value.precision() + " is greater than maximum " + "iou precision of " + MAXIMUM_IOU_PRECISION; throw new PrecisionError(err, this); } } } private Amount newValue(BigDecimal newValue) { return newValue(newValue, false, false); } private Amount newValue(BigDecimal newValue, boolean round, boolean unbounded) { if (round) { newValue = roundValue(newValue, isNative); } return new Amount(newValue, currency, issuer, isNative, unbounded); } private Amount newValue(BigDecimal val, boolean round) { return newValue(val, round, false); } /* Getters and Setters */ public BigDecimal value() { return value; } public Currency currency() { return currency; } public AccountID issuer() { return issuer; } public Issue issue() { // TODO: store the currency and issuer as an Issue return new Issue(currency, issuer); } public UInt64 mantissa() { if (mantissa == null) { mantissa = calculateMantissa(); } return mantissa; } public int exponent() { return exponent; } public boolean isNative() { return isNative; } public String currencyString() { return currency.toString(); } public String issuerString() { if (issuer == null) { return ""; } return issuer.toString(); } /* Offset & Mantissa Helpers */ /** * @return a positive value for the mantissa */ private UInt64 calculateMantissa() { if (isNative()) { return new UInt64(bigIntegerDrops().abs()); } else { return new UInt64(bigIntegerIOUMantissa()); } } protected int calculateExponent() { return -MAXIMUM_IOU_PRECISION + value.precision() - value.scale(); } public BigInteger bigIntegerIOUMantissa() { return exactBigIntegerScaledByPowerOfTen(-exponent).abs(); } private BigInteger bigIntegerDrops() { return exactBigIntegerScaledByPowerOfTen(MAXIMUM_NATIVE_SCALE); } private BigInteger exactBigIntegerScaledByPowerOfTen(int n) { return value.scaleByPowerOfTen(n).toBigIntegerExact(); } /* Equality testing */ private boolean equalValue(Amount amt) { return compareTo(amt) == 0; } @Override public boolean equals(Object obj) { if (obj instanceof Amount) { return equals((Amount) obj); } return super.equals(obj); } public boolean equals(Amount amt) { return equalValue(amt) && currency.equals(amt.currency) && (isNative() || issuer.equals(amt.issuer)); } public boolean equalsExceptIssuer(Amount amt) { return equalValue(amt) && currencyString().equals(amt.currencyString()); } public int compareTo(Amount amount) { return value.compareTo(amount.value); } public boolean isZero() { return value.signum() == 0; } public boolean isNegative() { return value.signum() == -1; } // Maybe you want !isNegative() // Any amount that !isNegative() isn't necessarily positive // Is a zero amount strictly positive? no public boolean isPositive() { return value.signum() == 1; } /** Arithmetic Operations There's no checking if an amount is of a different currency/issuer. All operations return amounts of the same currency/issuer as the first operand. eg. amountOne.add(amountTwo) The currency/issuer of the resultant amount, is that of `amountOne` Divide and multiply are equivalent to the javascript ripple-lib ratio_human and product_human. */ public Amount add(BigDecimal augend) { return newValue(value.add(augend), true); } public Amount add(Amount augend) { return add(augend.value); } public Amount add(Number augend) { return add(BigDecimal.valueOf(augend.doubleValue())); } public Amount subtract(BigDecimal subtrahend) { return newValue(value.subtract(subtrahend), true); } public Amount subtract(Amount subtrahend) { return subtract(subtrahend.value); } public Amount subtract(Number subtrahend) { return subtract(BigDecimal.valueOf(subtrahend.doubleValue())); } public Amount multiply(BigDecimal divisor) { return newValue(value.multiply(divisor, MATH_CONTEXT), true); } public Amount multiply(Amount multiplicand) { return multiply(multiplicand.value); } public Amount multiply(Number multiplicand) { return multiply(BigDecimal.valueOf(multiplicand.doubleValue())); } public Amount divide(BigDecimal divisor) { return newValue(value.divide(divisor, MATH_CONTEXT), true); } public Amount divide(Amount divisor) { return divide(divisor.value); } public Amount divide(Number divisor) { return divide(BigDecimal.valueOf(divisor.doubleValue())); } public Amount negate() { return newValue(value.negate()); } public Amount abs() { return newValue(value.abs()); } public Amount min(Amount val) { return (compareTo(val) <= 0 ? this : val); } public Amount max(Amount val) { return (compareTo(val) >= 0 ? this : val); } /* Offer related helpers */ public BigDecimal computeQuality(Amount toExchangeThisWith) { return value.divide(toExchangeThisWith.value, MathContext.DECIMAL128); } /** * @return Amount * The real native unit is a drop, one million of which are an XRP. * We want `one` unit at XRP scale (1e6 drops), or if it's an IOU, * just `one`. */ public Amount one() { if (isNative()) { return ONE_XRP; } else { return issue().amount(1); } } /* Serialized Type implementation */ @Override public Object toJSON() { if (isNative()) { return toDropsString(); } else { return toJSONObject(); } } public JSONObject toJSONObject() { if (isNative()) { throw new RuntimeException("Native amounts must be serialized as a string"); } JSONObject out = new JSONObject(); out.put("currency", currencyString()); out.put("value", valueText()); out.put("issuer", issuerString()); return out; } @Override public byte[] toBytes() { return translate.toBytes(this); } @Override public String toHex() { return translate.toHex(this); } @Override public void toBytesSink(BytesSink to) { UInt64 man = mantissa(); if (isNative()) { if (!isNegative()) { man = man.or(BINARY_FLAG_IS_NON_NEGATIVE_NATIVE); } to.add(man.toByteArray()); } else { int exponent = exponent(); UInt64 packed; if (isZero()) { packed = BINARY_FLAG_IS_IOU; } else if (isNegative()) { packed = man.or(new UInt64(512 + 0 + 97 + exponent).shiftLeft(64 - 10)); } else { packed = man.or(new UInt64(512 + 256 + 97 + exponent).shiftLeft(64 - 10)); } to.add(packed.toByteArray()); to.add(currency.bytes()); to.add(issuer.bytes()); } } @Override public Type type() { return Type.Amount; } public static class Translator extends TypeTranslator<Amount> { @Override public Amount fromString(String s) { // We need to use the full dotted.path here, otherwise // we get confused with the AmountField Amount return com.ripple.core.coretypes.Amount.fromString(s); } @Override public Amount fromParser(BinaryParser parser, Integer hint) { BigDecimal value; byte[] mantissa = parser.read(8); byte b1 = mantissa[0], b2 = mantissa[1]; boolean isIOU = (b1 & 0x80) != 0; boolean isPositive = (b1 & 0x40) != 0; int sign = isPositive ? 1 : -1; if (isIOU) { mantissa[0] = 0; Currency curr = Currency.translate.fromParser(parser); AccountID issuer = AccountID.translate.fromParser(parser); int exponent = ((b1 & 0x3F) << 2) + ((b2 & 0xff) >> 6) - 97; mantissa[1] &= 0x3F; value = new BigDecimal(new BigInteger(sign, mantissa), -exponent); return new Amount(value, curr, issuer, false); } else { mantissa[0] &= 0x3F; value = xrpFromDropsMantissa(mantissa, sign); return new Amount(value); } } @Override public String toString(Amount obj) { return obj.stringRepr(); } @Override public JSONObject toJSONObject(Amount obj) { return obj.toJSONObject(); } @Override public Amount fromJSONObject(JSONObject jsonObject) { String valueString = jsonObject.getString("value"); String issuerString = jsonObject.getString("issuer"); String currencyString = jsonObject.getString("currency"); return new Amount(new BigDecimal(valueString), currencyString, issuerString); } } static public Translator translate = new Translator(); public static BigDecimal xrpFromDropsMantissa(byte[] mantissa, int sign) { return new BigDecimal(new BigInteger(sign, mantissa), 6); } /* Number overides */ @Override public int intValue() { return value.intValueExact(); } @Override public long longValue() { return value.longValueExact(); } @Override public float floatValue() { return value.floatValue(); } @Override public double doubleValue() { return value.doubleValue(); } public BigInteger bigIntegerValue() { return value.toBigIntegerExact(); } public Amount newIssuer(AccountID issuer) { return new Amount(value, currency, issuer); } public Amount copy() { return new Amount(value, currency, issuer, isNative, unbounded); } // Static constructors public static Amount fromString(String val) { if (val.contains("/")) { return fromIOUString(val); } else if (val.contains(".")) { return fromXrpString(val); } else { return fromDropString(val); } } public static Amount fromDropString(String val) { BigDecimal drops = new BigDecimal(val).scaleByPowerOfTen(-6); checkDropsValueWhole(val); return new Amount(drops); } public static Amount fromIOUString(String val) { String[] split = val.split("/"); if (split.length == 1) { throw new RuntimeException("IOU string must be in the form number/currencyString or number/currencyString/issuerString"); } else if (split.length == 2) { return new Amount(new BigDecimal(split[0]), split[1]); } else { return new Amount(new BigDecimal(split[0]), split[1], split[2]); } } @Deprecated private static Amount fromXrpString(String valueString) { BigDecimal val = new BigDecimal(valueString); return new Amount(val); } /** * @return A String representation as used by ripple json format */ public String stringRepr() { if (isNative()) { return toDropsString(); } else { return iouTextFull(); } } public String toDropsString() { if (!isNative()) { throw new RuntimeException("Amount is not native"); } return bigIntegerDrops().toString(); } private String iouText() { return String.format("%s/%s", valueText(), currencyString()); } public String iouTextFull() { return String.format("%s/%s/%s", valueText(), currencyString(), issuerString()); } public String toTextFull() { if (isNative()) { return nativeText(); } else { return iouTextFull(); } } public String nativeText() { return String.format("%s/XRP", valueText()); } @Override public String toString() { return toTextFull(); } public String toText() { if (isNative()) { return nativeText(); } else { return iouText(); } } /** * @return A String containing the value as a decimal number (in XRP scale) */ public String valueText() { return value.signum() == 0 ? "0" : value().toPlainString(); } public void checkLowerDropBound(BigDecimal val) { if (val.scale() > 6) { PrecisionError bigger = getOutOfBoundsError(val, "smaller than min native value", MIN_NATIVE_VALUE); bigger.illegal = this; throw bigger; } } public void checkUpperBound(BigDecimal val) { if (val.compareTo(MAX_NATIVE_VALUE) == 1) { PrecisionError bigger = getOutOfBoundsError(val, "bigger than max native value ", MAX_NATIVE_VALUE); bigger.illegal = this; throw bigger; } } private static PrecisionError getOutOfBoundsError(BigDecimal abs, String sized, BigDecimal bound) { return new PrecisionError(abs.toPlainString() + " absolute XRP is " + sized + bound); } public void checkXRPBounds() { BigDecimal v = value.abs(); if (v.compareTo(TAKER_PAYS_FOR_THAT_DAMN_OFFER) == 0) { return; } checkLowerDropBound(v); checkUpperBound(v); } private static int significantDigits(BigDecimal input) { input = input.stripTrailingZeros(); return input.scale() < 0 ? input.precision() - input.scale() : input.precision(); } public int significantDigits() { return significantDigits(value); } public static void checkDropsValueWhole(String drops) { boolean contains = drops.contains("."); if (contains) { throw new RuntimeException("Drops string contains floating point is decimal"); } } public static BigDecimal roundValue(BigDecimal value, boolean nativeSrc) { int i = value.precision() - value.scale(); return value.setScale(nativeSrc ? MAXIMUM_NATIVE_SCALE : MAXIMUM_IOU_PRECISION - i, MATH_CONTEXT.getRoundingMode()); } private static BigDecimal parseDecimal(String s) { return new BigDecimal(s.replace(",", "")); //# .scaleByPowerOfTen(6); } private static AmountField amountField(final Field f) { return new AmountField() { @Override public Field getField() { return f; } }; } static public AmountField Amount = amountField(Field.Amount); static public AmountField Balance = amountField(Field.Balance); static public AmountField LimitAmount = amountField(Field.LimitAmount); static public AmountField DeliveredAmount = amountField(Field.DeliveredAmount); static public AmountField TakerPays = amountField(Field.TakerPays); static public AmountField TakerGets = amountField(Field.TakerGets); static public AmountField LowLimit = amountField(Field.LowLimit); static public AmountField HighLimit = amountField(Field.HighLimit); static public AmountField Fee = amountField(Field.Fee); static public AmountField SendMax = amountField(Field.SendMax); static public AmountField MinimumOffer = amountField(Field.MinimumOffer); static public AmountField RippleEscrow = amountField(Field.RippleEscrow); static public AmountField taker_gets_funded = amountField(Field.taker_gets_funded); static public AmountField taker_pays_funded = amountField(Field.taker_pays_funded); }