package qora.assets;
import java.math.BigDecimal;
import java.math.BigInteger;
import java.math.RoundingMode;
import java.util.Arrays;
import java.util.List;
import com.google.common.primitives.Bytes;
import com.google.common.primitives.Longs;
import database.DBSet;
import qora.account.Account;
import qora.crypto.Base58;
import qora.transaction.Transaction;
public class Order implements Comparable<Order> {
private static final int ID_LENGTH = 64;
private static final int CREATOR_LENGTH = 25;
private static final int HAVE_LENGTH = 8;
private static final int WANT_LENGTH = 8;
private static final int AMOUNT_LENGTH = 12;
private static final int FULFILLED_LENGTH = 12;
private static final int PRICE_LENGTH = 12;
private static final int TIMESTAMP_LENGTH = 8;
private static final int BASE_LENGTH = ID_LENGTH + CREATOR_LENGTH + HAVE_LENGTH + WANT_LENGTH + AMOUNT_LENGTH + FULFILLED_LENGTH + PRICE_LENGTH + TIMESTAMP_LENGTH;
private BigInteger id;
private Account creator;
private long have;
private long want;
private BigDecimal amount;
private BigDecimal fulfilled;
private BigDecimal price;
private long timestamp;
public Order(BigInteger id, Account creator, long have, long want, BigDecimal amount, BigDecimal price, long timestamp)
{
this.id = id;
this.creator = creator;
this.have = have;
this.want = want;
this.amount = amount;
this.fulfilled = BigDecimal.ZERO.setScale(8);
this.price = price;
this.timestamp = timestamp;
}
public Order(BigInteger id, Account creator, long have, long want, BigDecimal amount, BigDecimal fulfilled, BigDecimal price, long timestamp)
{
this.id = id;
this.creator = creator;
this.have = have;
this.want = want;
this.amount = amount;
this.fulfilled = fulfilled;
this.price = price;
this.timestamp = timestamp;
}
//GETTERS/SETTERS
public BigInteger getId()
{
return this.id;
}
public Account getCreator()
{
return this.creator;
}
public long getHave()
{
return this.have;
}
public Asset getHaveAsset()
{
return this.getHaveAsset(DBSet.getInstance());
}
public Asset getHaveAsset(DBSet db)
{
return db.getAssetMap().get(this.have);
}
public long getWant()
{
return this.want;
}
public Asset getWantAsset()
{
return this.getWantAsset(DBSet.getInstance());
}
public Asset getWantAsset(DBSet db)
{
return db.getAssetMap().get(this.want);
}
public BigDecimal getAmount()
{
return this.amount;
}
public BigDecimal getAmountLeft()
{
return this.amount.subtract(this.fulfilled);
}
public BigDecimal getPrice()
{
return this.price;
}
public BigDecimal getFulfilled()
{
return this.fulfilled;
}
public long getTimestamp()
{
return this.timestamp;
}
public void setFulfilled(BigDecimal fulfilled)
{
this.fulfilled = fulfilled;
}
public boolean isFulfilled()
{
return this.fulfilled.compareTo(this.amount) == 0;
}
public List<Trade> getInitiatedTrades()
{
return this.getInitiatedTrades(DBSet.getInstance());
}
public List<Trade> getInitiatedTrades(DBSet db)
{
return db.getTradeMap().getInitiatedTrades(this);
}
public boolean isConfirmed()
{
return DBSet.getInstance().getOrderMap().contains(this.id) || DBSet.getInstance().getCompletedOrderMap().contains(this.id);
}
//PARSE/CONVERT
public static Order parse(byte[] data) throws Exception
{
//CHECK IF CORRECT LENGTH
if(data.length < BASE_LENGTH)
{
throw new Exception("Data does not match order length");
}
int position = 0;
//READ ID
byte[] idBytes = Arrays.copyOfRange(data, position, position + ID_LENGTH);
BigInteger id = new BigInteger(idBytes);
position += ID_LENGTH;
//READ CREATOR
byte[] creatorBytes = Arrays.copyOfRange(data, position, position + CREATOR_LENGTH);
Account creator = new Account(Base58.encode(creatorBytes));
position += CREATOR_LENGTH;
//READ HAVE
byte[] haveBytes = Arrays.copyOfRange(data, position, position + HAVE_LENGTH);
long have = Longs.fromByteArray(haveBytes);
position += HAVE_LENGTH;
//READ HAVE
byte[] wantBytes = Arrays.copyOfRange(data, position, position + WANT_LENGTH);
long want = Longs.fromByteArray(wantBytes);
position += WANT_LENGTH;
//READ AMOUNT
byte[] amountBytes = Arrays.copyOfRange(data, position, position + AMOUNT_LENGTH);
BigDecimal amount = new BigDecimal(new BigInteger(amountBytes), 8);
position += AMOUNT_LENGTH;
//READ FULFILLED
byte[] fulfilledBytes = Arrays.copyOfRange(data, position, position + FULFILLED_LENGTH);
BigDecimal fulfilled = new BigDecimal(new BigInteger(fulfilledBytes), 8);
position += FULFILLED_LENGTH;
//READ PRICE
byte[] priceBytes = Arrays.copyOfRange(data, position, position + PRICE_LENGTH);
BigDecimal price = new BigDecimal(new BigInteger(priceBytes), 8);
position += PRICE_LENGTH;
//READ TIMESTAMP
byte[] timestampBytes = Arrays.copyOfRange(data, position, position + TIMESTAMP_LENGTH);
long timestamp = Longs.fromByteArray(timestampBytes);
position += TIMESTAMP_LENGTH;
return new Order(id, creator, have, want, amount, fulfilled, price, timestamp);
}
public byte[] toBytes()
{
byte[] data = new byte[0];
//WRITE ID
byte[] idBytes = this.id.toByteArray();
byte[] fill = new byte[ID_LENGTH - idBytes.length];
idBytes = Bytes.concat(fill, idBytes);
data = Bytes.concat(data, idBytes);
//WRITE CREATOR
try
{
data = Bytes.concat(data , Base58.decode(this.creator.getAddress()));
}
catch(Exception e)
{
//DECODE EXCEPTION
}
//WRITE HAVE
byte[] haveBytes = Longs.toByteArray(this.have);
haveBytes = Bytes.ensureCapacity(haveBytes, HAVE_LENGTH, 0);
data = Bytes.concat(data, haveBytes);
//WRITE WANT
byte[] wantBytes = Longs.toByteArray(this.want);
wantBytes = Bytes.ensureCapacity(wantBytes, WANT_LENGTH, 0);
data = Bytes.concat(data, wantBytes);
//WRITE AMOUNT
byte[] amountBytes = this.amount.unscaledValue().toByteArray();
fill = new byte[AMOUNT_LENGTH - amountBytes.length];
amountBytes = Bytes.concat(fill, amountBytes);
data = Bytes.concat(data, amountBytes);
//WRITE FULFILLED
byte[] fulfilledBytes = this.fulfilled.unscaledValue().toByteArray();
fill = new byte[FULFILLED_LENGTH - fulfilledBytes.length];
fulfilledBytes = Bytes.concat(fill, fulfilledBytes);
data = Bytes.concat(data, fulfilledBytes);
//WRITE PRICE
byte[] priceBytes = this.price.unscaledValue().toByteArray();
fill = new byte[PRICE_LENGTH - priceBytes.length];
priceBytes = Bytes.concat(fill, priceBytes);
data = Bytes.concat(data, priceBytes);
//WRITE TIMESTAMP
byte[] timestampBytes = Longs.toByteArray(this.timestamp);
timestampBytes = Bytes.ensureCapacity(timestampBytes, TIMESTAMP_LENGTH, 0);
data = Bytes.concat(data, timestampBytes);
return data;
}
public int getDataLength()
{
return BASE_LENGTH;
}
//PROCESS/ORPHAN
public void process(DBSet db, Transaction transaction)
{
//REMOVE HAVE
this.creator.setConfirmedBalance(this.have, this.creator.getConfirmedBalance(this.have, db).subtract(this.amount), db);
//ADD ORDER TO DATABASE
db.getOrderMap().add(this.copy());
//GET ALL ORDERS(WANT, HAVE) LOWEST PRICE FIRST
List<Order> orders = db.getOrderMap().getOrders(this.want, this.have);
//TRY AND COMPLETE ORDERS
boolean completedOrder = true;
int i = 0;
while(completedOrder && i < orders.size())
{
//RESET COMPLETED
completedOrder = false;
//GET ORDER
Order order = orders.get(i);
//CALCULATE BUYING PRICE
BigDecimal buyingPrice = BigDecimal.ONE.setScale(8).divide(order.getPrice(), RoundingMode.DOWN);
//CHECK IF OWNERS OF BOTH ORDER ARE NOT THE SAME
//CHECK IF BUYING PRICE IS HIGHER OR EQUAL THEN OUR SELLING PRICE
if(buyingPrice.compareTo(this.price) >= 0)
{
//CALCULATE THE MAXIMUM AMOUNT WE COULD BUY
BigDecimal amount = order.getAmountLeft();
amount = amount.min(this.getAmountLeft().multiply(BigDecimal.ONE.setScale(8).divide(order.getPrice(), RoundingMode.DOWN)).setScale(8, RoundingMode.DOWN));
//CHECK IF WE CAN BUY ANYTHING
if(amount.compareTo(BigDecimal.ZERO) > 0)
{
//CALCULATE THE INCREMENTS AT WHICH WE HAVE TO BUY
BigDecimal increment = this.calculateBuyIncrement(order, db);
//CALCULATE THE AMOUNT WE CAN BUY
amount = amount.subtract(amount.remainder(increment));
//CALCULATE THE PRICE WE HAVE TO PAY
BigDecimal price = amount.multiply(order.getPrice()).setScale(8);
//CHECK IF AMOUNT AFTER ROUNDING IS NOT ZERO
if(amount.compareTo(BigDecimal.ZERO) > 0)
{
//CREATE TRADE
Trade trade = new Trade(this.getId(), order.getId(), amount, price, transaction.getTimestamp());
trade.process(db);
this.fulfilled = this.fulfilled.add(price);
}
//COMPLETED ORDER
completedOrder = true;
}
}
//INCREMENT I
i++;
}
}
public void orphan(DBSet db) {
//ORPHAN TRADES
for(Trade trade: this.getInitiatedTrades(db))
{
trade.orphan(db);
}
//REMOVE ORDER FROM DATABASE
db.getOrderMap().delete(this);
//REMOVE HAVE
this.creator.setConfirmedBalance(this.have, this.creator.getConfirmedBalance(this.have, db).add(this.amount), db);
}
private BigDecimal calculateBuyIncrement(Order order, DBSet db)
{
BigInteger multiplier = BigInteger.valueOf(100000000l);
//CALCULATE THE MINIMUM INCREMENT AT WHICH I CAN BUY USING GCD
BigInteger haveAmount = BigInteger.ONE.multiply(multiplier);
BigInteger priceAmount = order.getPrice().multiply(new BigDecimal(multiplier)).toBigInteger();
BigInteger gcd = haveAmount.gcd(priceAmount);
haveAmount = haveAmount.divide(gcd);
priceAmount = priceAmount.divide(gcd);
//CALCULATE GCD IN COMBINATION WITH DIVISIBILITY
if(this.getWantAsset(db).isDivisible())
{
haveAmount = haveAmount.multiply(multiplier);
}
if(this.getHaveAsset(db).isDivisible())
{
priceAmount = priceAmount.multiply(multiplier);
}
gcd = haveAmount.gcd(priceAmount);
//CALCULATE THE INCREMENT AT WHICH WE HAVE TO BUY
BigDecimal increment = new BigDecimal(haveAmount.divide(gcd));
if(this.getWantAsset(db).isDivisible())
{
increment = increment.divide(new BigDecimal(multiplier));
}
//RETURN
return increment;
}
//COMPARE
@Override
public int compareTo(Order order)
{
//COMPARE ONLY BY PRICE
return this.price.compareTo(order.getPrice());
}
//COPY
public Order copy()
{
try
{
return parse(this.toBytes());
}
catch (Exception e)
{
return null;
}
}
}