package com.ripple.core.coretypes;
import com.ripple.core.coretypes.hash.Hash160;
import com.ripple.core.coretypes.uint.UInt64;
import com.ripple.core.serialized.BinaryParser;
import com.ripple.core.serialized.BytesSink;
import com.ripple.encodings.common.B16;
import java.math.BigDecimal;
import java.math.BigInteger;
import java.math.MathContext;
import java.util.Date;
import java.util.concurrent.TimeUnit;
/**
* Funnily enough, yes, in rippled a currency is represented by a Hash160 type.
* For the sake of consistency and convenience, this quirk is repeated here.
*
* https://gist.github.com/justmoon/8597643
*/
public class Currency extends Hash160 {
public static final Currency NEUTRAL = new Currency(BigInteger.ONE.toByteArray());
public static final Currency XRP = new Currency(BigInteger.ZERO.toByteArray());
@Override
public Object toJSON() {
return translate.toJSON(this);
}
@Override
public byte[] toBytes() {
return translate.toBytes(this);
}
@Override
public String toHex() {
return translate.toHex(this);
}
@Override
public void toBytesSink(BytesSink to) {
translate.toBytesSink(this, to);
}
public boolean isNative() {
return this == Currency.XRP || equals(Currency.XRP);
}
public boolean isIOU() {
return !isNative();
}
public static enum Type {
HASH,
ISO, // three letter isoCode
DEMURRAGE,
UNKNOWN;
public static Type fromByte(byte typeByte) {
if (typeByte == 0x00) {
return ISO;
} else if (typeByte == 0x01) {
return DEMURRAGE;
} else if ((typeByte & 0x80) != 0) {
return HASH;
} else {
return UNKNOWN;
}
}
}
Type type;
public static class Demurrage {
Date interestStart;
String isoCode;
double interestRate;
static public BigDecimal applyRate(BigDecimal amount, BigDecimal rate, TimeUnit time, long units) {
BigDecimal appliedRate = getSeconds(time, units).divide(rate, MathContext.DECIMAL64);
BigDecimal factor = BigDecimal.valueOf(Math.exp(appliedRate.doubleValue()));
return amount.multiply(factor, MathContext.DECIMAL64);
}
static public BigDecimal calculateRate(BigDecimal rate, TimeUnit time, long units) {
BigDecimal seconds = getSeconds(time, units);
BigDecimal log = ln(rate);
return seconds.divide(log, MathContext.DECIMAL64);
}
private static BigDecimal ln(BigDecimal bd) {
return BigDecimal.valueOf(Math.log(bd.doubleValue()));
}
private static BigDecimal getSeconds(TimeUnit time, long units) {
return BigDecimal.valueOf(time.toSeconds(units));
}
public Demurrage(byte[] bytes) {
BinaryParser parser = new BinaryParser(bytes);
parser.skip(1); // The type
isoCode = isoCodeFromBytesAndOffset(parser.read(3), 0);// The isoCode
interestStart = RippleDate.fromParser(parser);
long l = UInt64.translate.fromParser(parser).longValue();
interestRate = Double.longBitsToDouble(l);
}
}
public Demurrage demurrage = null;
public Currency(byte[] bytes) {
super(bytes);
type = Type.fromByte(this.hash[0]);
if (type == Type.DEMURRAGE) {
demurrage = new Demurrage(bytes);
}
}
/**
* It's better to extend HashTranslator than the Hash160.Translator directly
* That way the generics can still vibe with the @Override
*/
public static class CurrencyTranslator extends HashTranslator<Currency> {
@Override
public int byteWidth() {
return 20;
}
@Override
public Currency newInstance(byte[] b) {
return new Currency(b);
}
@Override
public Object toJSON(Currency obj) {
return obj.toString();
}
@Override
public Currency fromString(String value) {
if (value.length() == 40 /* byteWidth() * 2 */) {
return newInstance(B16.decode(value));
} else if (value.equals("XRP")) {
return XRP;
} else {
if (!(value.length() == 3)) {
// if (!value.matches("[A-Z0-9]{3}")) {
throw new RuntimeException("Currency code must be 3 characters");
}
return newInstance(encodeCurrency(value));
}
}
}
public static Currency fromString(String currency) {
return translate.fromString(currency);
}
@Override
public String toString() {
switch (type) {
case ISO:
String code = getCurrencyCodeFromTLCBytes(bytes());
if (code.equals("XRP")) {
// HEX of the bytes
return super.toString();
} else if (code.equals("\0\0\0")) {
return "XRP";
} else {
// the 3 letter isoCode
return code;
}
case HASH:
case DEMURRAGE:
case UNKNOWN:
default:
return super.toString();
}
}
public String humanCode() {
if (type == Type.ISO) {
return getCurrencyCodeFromTLCBytes(hash);
} else if (type == Type.DEMURRAGE) {
return isoCodeFromBytesAndOffset(hash, 1);
} else {
throw new IllegalStateException("No human code for currency of type " + type);
}
}
@Override
public boolean equals(Object obj) {
if (obj instanceof Currency) {
Currency other = (Currency) obj;
byte[] bytes = this.bytes();
byte[] otherBytes = other.bytes();
if (type == Type.ISO && other.type == Type.ISO) {
return (bytes[12] == otherBytes[12] &&
bytes[13] == otherBytes[13] &&
bytes[14] == otherBytes[14]);
}
}
return super.equals(obj); // Full comparison
}
public static CurrencyTranslator translate = new CurrencyTranslator();
/*
* The following are static methods, legacy from when there was no
* usage of Currency objects, just String with "XRP" ambiguity.
* */
public static byte[] encodeCurrency(String currencyCode) {
byte[] currencyBytes = new byte[20];
currencyBytes[12] = (byte) currencyCode.codePointAt(0);
currencyBytes[13] = (byte) currencyCode.codePointAt(1);
currencyBytes[14] = (byte) currencyCode.codePointAt(2);
return currencyBytes;
}
public static String getCurrencyCodeFromTLCBytes(byte[] bytes) {
int i;
boolean zeroInNonCurrencyBytes = true;
for (i = 0; i < 20; i++) {
zeroInNonCurrencyBytes = zeroInNonCurrencyBytes &&
((i == 12 || i == 13 || i == 14) || // currency bytes (0 or any other)
bytes[i] == 0); // non currency bytes (0)
}
if (zeroInNonCurrencyBytes) {
return isoCodeFromBytesAndOffset(bytes, 12);
} else {
throw new IllegalStateException("Currency is invalid");
}
}
private static char charFrom(byte[] bytes, int i) {
return (char) bytes[i];
}
private static String isoCodeFromBytesAndOffset(byte[] bytes, int offset) {
char a = charFrom(bytes, offset);
char b = charFrom(bytes, offset + 1);
char c = charFrom(bytes, offset + 2);
return "" + a + b + c;
}
}