package cy.agorise.graphenej; import com.google.common.math.DoubleMath; import com.google.common.primitives.Bytes; import com.google.common.primitives.UnsignedLong; import com.google.gson.GsonBuilder; import com.google.gson.JsonDeserializationContext; import com.google.gson.JsonDeserializer; import com.google.gson.JsonElement; import com.google.gson.JsonObject; import com.google.gson.JsonParseException; import com.google.gson.JsonSerializationContext; import com.google.gson.JsonSerializer; import java.io.ByteArrayOutputStream; import java.io.DataOutput; import java.io.DataOutputStream; import java.io.IOException; import java.lang.reflect.Type; import java.math.BigDecimal; import java.math.RoundingMode; import cy.agorise.graphenej.errors.IncompatibleOperation; import cy.agorise.graphenej.interfaces.ByteSerializable; import cy.agorise.graphenej.interfaces.JsonSerializable; /** * Class used to represent a specific amount of a certain asset */ public class AssetAmount implements ByteSerializable, JsonSerializable { /** * Constants used in the JSON serialization procedure. */ public static final String KEY_AMOUNT = "amount"; public static final String KEY_ASSET_ID = "asset_id"; private UnsignedLong amount; private Asset asset; /** * Class constructor * @param amount The amount * @param asset The asset */ public AssetAmount(UnsignedLong amount, Asset asset){ this.amount = amount; this.asset = asset; } /** * Copy constructor * @param assetAmount The other instance */ public AssetAmount(AssetAmount assetAmount){ this.amount = UnsignedLong.valueOf(assetAmount.getAmount().toString()); this.asset = new Asset(assetAmount.getAsset()); } /** * Adds two asset amounts. They must refer to the same Asset type. * @param other: The other AssetAmount to add to this. * @return: A new instance of the AssetAmount class with the combined amount. */ public AssetAmount add(AssetAmount other){ if(!this.getAsset().getObjectId().equals(other.getAsset().getObjectId())){ throw new IncompatibleOperation("Cannot add two AssetAmount instances that refer to different assets"); } UnsignedLong combined = this.amount.plus(other.getAmount()); return new AssetAmount(combined, asset); } /** * Adds an aditional amount of base units to this AssetAmount. * @param additional: The amount to add. * @return: A new instance of the AssetAmount class with the added aditional. */ public AssetAmount add(long additional){ UnsignedLong combined = this.amount.plus(UnsignedLong.valueOf(additional)); return new AssetAmount(combined, asset); } /** * Subtracts another instance of AssetAmount from this one. This method will always * return absolute values. * @param other: The other asset amount to subtract from this. * @return: The absolute value of the subtraction of the other minus this asset amount. */ public AssetAmount subtract(AssetAmount other){ if(!this.getAsset().getObjectId().equals(other.getAsset().getObjectId())){ throw new IncompatibleOperation("Cannot subtract two AssetAmount instances that refer to different assets"); } UnsignedLong result = null; if(this.amount.compareTo(other.getAmount()) < 0){ result = other.getAmount().minus(this.amount); }else{ result = this.amount.minus(other.getAmount()); } return new AssetAmount(result, asset); } /** * Multiplies the current amount by a factor provided as the first parameter. The second parameter * specifies the rounding method to be used. * @param factor: The multiplying factor * @param roundingMode: The rounding mode as an instance of the {@link RoundingMode} class * @return The same AssetAmount instance, but with the changed amount value. */ public AssetAmount multiplyBy(double factor, RoundingMode roundingMode){ BigDecimal originalAmount = new BigDecimal(amount.bigIntegerValue()); BigDecimal decimalResult = originalAmount.multiply(new BigDecimal(factor)); UnsignedLong resultingAmount = UnsignedLong.valueOf(DoubleMath.roundToBigInteger(decimalResult.doubleValue(), roundingMode)); return new AssetAmount(resultingAmount, new Asset(asset)); } /** * Multiplies the current amount by a factor, using the {@link RoundingMode#HALF_DOWN} constant. * @param factor: The multiplying factor * @return The same AssetAmount instance, but with the changed amount value. */ public AssetAmount multiplyBy(double factor){ return this.multiplyBy(factor, RoundingMode.HALF_DOWN); } /** * Divides the current amount by a divisor provided as the first parameter. The second parameter * specifies the rounding method to be used. * @param divisor: The divisor * @return: The same AssetAMount instance, but with the divided amount value */ public AssetAmount divideBy(double divisor, RoundingMode roundingMode){ BigDecimal originalAmount = new BigDecimal(amount.bigIntegerValue()); BigDecimal decimalAmount = originalAmount.divide(new BigDecimal(divisor), 18, RoundingMode.HALF_UP); UnsignedLong resultingAmount = UnsignedLong.valueOf(DoubleMath.roundToBigInteger(decimalAmount.doubleValue(), roundingMode)); return new AssetAmount(resultingAmount, new Asset(asset)); } /** * Divides the current amount by a divisor provided as the first parameter, using * the {@link RoundingMode#HALF_DOWN} constant * @param divisor: The divisor * @return: The same AssetAMount instance, but with the divided amount value */ public AssetAmount divideBy(double divisor){ return this.divideBy(divisor, RoundingMode.HALF_DOWN); } public void setAmount(UnsignedLong amount){ this.amount = amount; } public UnsignedLong getAmount(){ return this.amount; } public Asset getAsset(){ return this.asset; } public void setAsset(Asset asset){ this.asset = asset; } @Override public byte[] toBytes() { ByteArrayOutputStream byteArrayOutputStream = new ByteArrayOutputStream(); DataOutput out = new DataOutputStream(byteArrayOutputStream); try { Varint.writeUnsignedVarLong(asset.instance, out); } catch (IOException e) { e.printStackTrace(); } // Getting asset id byte[] assetId = byteArrayOutputStream.toByteArray(); byte[] value = Util.revertLong(this.amount.longValue()); // Concatenating and returning value + asset id return Bytes.concat(value, assetId); } @Override public String toJsonString() { GsonBuilder gsonBuilder = new GsonBuilder(); gsonBuilder.registerTypeAdapter(AssetAmount.class, new AssetAmountSerializer()); return gsonBuilder.create().toJson(this); } @Override public JsonObject toJsonObject() { JsonObject jsonAmount = new JsonObject(); jsonAmount.addProperty(KEY_AMOUNT, amount); jsonAmount.addProperty(KEY_ASSET_ID, asset.getObjectId()); return jsonAmount; } @Override public String toString() { return String.format("(asset=%s, amount=%s)", asset.getObjectId(), amount.toString(10)); } /** * Custom serializer used to translate this object into the JSON-formatted entry we need for a transaction. */ public static class AssetAmountSerializer implements JsonSerializer { @Override public JsonElement serialize(AssetAmount assetAmount, Type type, JsonSerializationContext jsonSerializationContext) { JsonObject obj = new JsonObject(); obj.addProperty(KEY_AMOUNT, assetAmount.amount); obj.addProperty(KEY_ASSET_ID, assetAmount.asset.getObjectId()); return obj; } } /** * Custom deserializer used for this class */ public static class AssetAmountDeserializer implements JsonDeserializer { @Override public AssetAmount deserialize(JsonElement json, Type type, JsonDeserializationContext jsonDeserializationContext) throws JsonParseException { Long amount = json.getAsJsonObject().get(KEY_AMOUNT).getAsLong(); String assetId = json.getAsJsonObject().get(KEY_ASSET_ID).getAsString(); AssetAmount assetAmount = new AssetAmount(UnsignedLong.valueOf(amount), new Asset(assetId)); return assetAmount; } } }