/****************************************************************************** * Copyright © 2013-2016 The Nxt Core Developers. * * * * See the AUTHORS.txt, DEVELOPER-AGREEMENT.txt and LICENSE.txt files at * * the top-level directory of this distribution for the individual copyright * * holder information and the developer policies on copyright and licensing. * * * * Unless otherwise agreed in a custom licensing agreement, no part of the * * Nxt software, including this file, may be copied, modified, propagated, * * or distributed except according to the terms contained in the LICENSE.txt * * file. * * * * Removal or modification of this copyright notice is prohibited. * * * ******************************************************************************/ package nxt; import nxt.AccountLedger.LedgerEvent; import nxt.db.DbClause; import nxt.db.DbIterator; import nxt.db.DbKey; import nxt.db.VersionedEntityDbTable; import java.sql.Connection; import java.sql.PreparedStatement; import java.sql.ResultSet; import java.sql.SQLException; public abstract class Order { private static void matchOrders(long assetId) { Order.Ask askOrder; Order.Bid bidOrder; while ((askOrder = Ask.getNextOrder(assetId)) != null && (bidOrder = Bid.getNextOrder(assetId)) != null) { if (askOrder.getPriceNQT() > bidOrder.getPriceNQT()) { break; } Trade trade = Trade.addTrade(assetId, askOrder, bidOrder); askOrder.updateQuantityQNT(Math.subtractExact(askOrder.getQuantityQNT(), trade.getQuantityQNT())); Account askAccount = Account.getAccount(askOrder.getAccountId()); askAccount.addToBalanceAndUnconfirmedBalanceNQT(LedgerEvent.ASSET_TRADE, askOrder.getId(), Math.multiplyExact(trade.getQuantityQNT(), trade.getPriceNQT())); askAccount.addToAssetBalanceQNT(LedgerEvent.ASSET_TRADE, askOrder.getId(), assetId, -trade.getQuantityQNT()); bidOrder.updateQuantityQNT(Math.subtractExact(bidOrder.getQuantityQNT(), trade.getQuantityQNT())); Account bidAccount = Account.getAccount(bidOrder.getAccountId()); bidAccount.addToAssetAndUnconfirmedAssetBalanceQNT(LedgerEvent.ASSET_TRADE, bidOrder.getId(), assetId, trade.getQuantityQNT()); bidAccount.addToBalanceNQT(LedgerEvent.ASSET_TRADE, bidOrder.getId(), -Math.multiplyExact(trade.getQuantityQNT(), trade.getPriceNQT())); bidAccount.addToUnconfirmedBalanceNQT(LedgerEvent.ASSET_TRADE, bidOrder.getId(), Math.multiplyExact(trade.getQuantityQNT(), (bidOrder.getPriceNQT() - trade.getPriceNQT()))); } } static void init() { Ask.init(); Bid.init(); } private final long id; private final long accountId; private final long assetId; private final long priceNQT; private final int creationHeight; private final short transactionIndex; private final int transactionHeight; private long quantityQNT; private Order(Transaction transaction, Attachment.ColoredCoinsOrderPlacement attachment) { this.id = transaction.getId(); this.accountId = transaction.getSenderId(); this.assetId = attachment.getAssetId(); this.quantityQNT = attachment.getQuantityQNT(); this.priceNQT = attachment.getPriceNQT(); this.creationHeight = Nxt.getBlockchain().getHeight(); this.transactionIndex = transaction.getIndex(); this.transactionHeight = transaction.getHeight(); } private Order(ResultSet rs) throws SQLException { this.id = rs.getLong("id"); this.accountId = rs.getLong("account_id"); this.assetId = rs.getLong("asset_id"); this.priceNQT = rs.getLong("price"); this.quantityQNT = rs.getLong("quantity"); this.creationHeight = rs.getInt("creation_height"); this.transactionIndex = rs.getShort("transaction_index"); this.transactionHeight = rs.getInt("transaction_height"); } private void save(Connection con, String table) throws SQLException { try (PreparedStatement pstmt = con.prepareStatement("MERGE INTO " + table + " (id, account_id, asset_id, " + "price, quantity, creation_height, transaction_index, transaction_height, height, latest) KEY (id, height) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, TRUE)")) { int i = 0; pstmt.setLong(++i, this.id); pstmt.setLong(++i, this.accountId); pstmt.setLong(++i, this.assetId); pstmt.setLong(++i, this.priceNQT); pstmt.setLong(++i, this.quantityQNT); pstmt.setInt(++i, this.creationHeight); pstmt.setShort(++i, this.transactionIndex); pstmt.setInt(++i, this.transactionHeight); pstmt.setInt(++i, Nxt.getBlockchain().getHeight()); pstmt.executeUpdate(); } } public final long getId() { return id; } public final long getAccountId() { return accountId; } public final long getAssetId() { return assetId; } public final long getPriceNQT() { return priceNQT; } public final long getQuantityQNT() { return quantityQNT; } public final int getHeight() { return creationHeight; } public final int getTransactionIndex() { return transactionIndex; } public final int getTransactionHeight() { return transactionHeight; } @Override public String toString() { return getClass().getSimpleName() + " id: " + Long.toUnsignedString(id) + " account: " + Long.toUnsignedString(accountId) + " asset: " + Long.toUnsignedString(assetId) + " price: " + priceNQT + " quantity: " + quantityQNT + " height: " + creationHeight + " transactionIndex: " + transactionIndex + " transactionHeight: " + transactionHeight; } private void setQuantityQNT(long quantityQNT) { this.quantityQNT = quantityQNT; } /* private int compareTo(Order o) { if (height < o.height) { return -1; } else if (height > o.height) { return 1; } else { if (id < o.id) { return -1; } else if (id > o.id) { return 1; } else { return 0; } } } */ public static final class Ask extends Order { private static final DbKey.LongKeyFactory<Ask> askOrderDbKeyFactory = new DbKey.LongKeyFactory<Ask>("id") { @Override public DbKey newKey(Ask ask) { return ask.dbKey; } }; private static final VersionedEntityDbTable<Ask> askOrderTable = new VersionedEntityDbTable<Ask>("ask_order", askOrderDbKeyFactory) { @Override protected Ask load(Connection con, ResultSet rs) throws SQLException { return new Ask(rs); } @Override protected void save(Connection con, Ask ask) throws SQLException { ask.save(con, table); } @Override protected String defaultSort() { return " ORDER BY creation_height DESC "; } }; public static int getCount() { return askOrderTable.getCount(); } public static Ask getAskOrder(long orderId) { return askOrderTable.get(askOrderDbKeyFactory.newKey(orderId)); } public static DbIterator<Ask> getAll(int from, int to) { return askOrderTable.getAll(from, to); } public static DbIterator<Ask> getAskOrdersByAccount(long accountId, int from, int to) { return askOrderTable.getManyBy(new DbClause.LongClause("account_id", accountId), from, to); } public static DbIterator<Ask> getAskOrdersByAsset(long assetId, int from, int to) { return askOrderTable.getManyBy(new DbClause.LongClause("asset_id", assetId), from, to); } public static DbIterator<Ask> getAskOrdersByAccountAsset(final long accountId, final long assetId, int from, int to) { DbClause dbClause = new DbClause.LongClause("account_id", accountId).and(new DbClause.LongClause("asset_id", assetId)); return askOrderTable.getManyBy(dbClause, from, to); } public static DbIterator<Ask> getSortedOrders(long assetId, int from, int to) { return askOrderTable.getManyBy(new DbClause.LongClause("asset_id", assetId), from, to, " ORDER BY price ASC, creation_height ASC, transaction_height ASC, transaction_index ASC "); } private static Ask getNextOrder(long assetId) { try (Connection con = Db.db.getConnection(); PreparedStatement pstmt = con.prepareStatement("SELECT * FROM ask_order WHERE asset_id = ? " + "AND latest = TRUE ORDER BY price ASC, creation_height ASC, transaction_height ASC, transaction_index ASC LIMIT 1")) { pstmt.setLong(1, assetId); try (DbIterator<Ask> askOrders = askOrderTable.getManyBy(con, pstmt, true)) { return askOrders.hasNext() ? askOrders.next() : null; } } catch (SQLException e) { throw new RuntimeException(e.toString(), e); } } static void addOrder(Transaction transaction, Attachment.ColoredCoinsAskOrderPlacement attachment) { Ask order = new Ask(transaction, attachment); askOrderTable.insert(order); matchOrders(attachment.getAssetId()); } static void removeOrder(long orderId) { askOrderTable.delete(getAskOrder(orderId)); } static void init() {} private final DbKey dbKey; private Ask(Transaction transaction, Attachment.ColoredCoinsAskOrderPlacement attachment) { super(transaction, attachment); this.dbKey = askOrderDbKeyFactory.newKey(super.id); } private Ask(ResultSet rs) throws SQLException { super(rs); this.dbKey = askOrderDbKeyFactory.newKey(super.id); } private void save(Connection con, String table) throws SQLException { super.save(con, table); } private void updateQuantityQNT(long quantityQNT) { super.setQuantityQNT(quantityQNT); if (quantityQNT > 0) { askOrderTable.insert(this); } else if (quantityQNT == 0) { askOrderTable.delete(this); } else { throw new IllegalArgumentException("Negative quantity: " + quantityQNT + " for order: " + Long.toUnsignedString(getId())); } } /* @Override public int compareTo(Ask o) { if (this.getPriceNQT() < o.getPriceNQT()) { return -1; } else if (this.getPriceNQT() > o.getPriceNQT()) { return 1; } else { return super.compareTo(o); } } */ } public static final class Bid extends Order { private static final DbKey.LongKeyFactory<Bid> bidOrderDbKeyFactory = new DbKey.LongKeyFactory<Bid>("id") { @Override public DbKey newKey(Bid bid) { return bid.dbKey; } }; private static final VersionedEntityDbTable<Bid> bidOrderTable = new VersionedEntityDbTable<Bid>("bid_order", bidOrderDbKeyFactory) { @Override protected Bid load(Connection con, ResultSet rs) throws SQLException { return new Bid(rs); } @Override protected void save(Connection con, Bid bid) throws SQLException { bid.save(con, table); } @Override protected String defaultSort() { return " ORDER BY creation_height DESC "; } }; public static int getCount() { return bidOrderTable.getCount(); } public static Bid getBidOrder(long orderId) { return bidOrderTable.get(bidOrderDbKeyFactory.newKey(orderId)); } public static DbIterator<Bid> getAll(int from, int to) { return bidOrderTable.getAll(from, to); } public static DbIterator<Bid> getBidOrdersByAccount(long accountId, int from, int to) { return bidOrderTable.getManyBy(new DbClause.LongClause("account_id", accountId), from, to); } public static DbIterator<Bid> getBidOrdersByAsset(long assetId, int from, int to) { return bidOrderTable.getManyBy(new DbClause.LongClause("asset_id", assetId), from, to); } public static DbIterator<Bid> getBidOrdersByAccountAsset(final long accountId, final long assetId, int from, int to) { DbClause dbClause = new DbClause.LongClause("account_id", accountId).and(new DbClause.LongClause("asset_id", assetId)); return bidOrderTable.getManyBy(dbClause, from, to); } public static DbIterator<Bid> getSortedOrders(long assetId, int from, int to) { return bidOrderTable.getManyBy(new DbClause.LongClause("asset_id", assetId), from, to, " ORDER BY price DESC, creation_height ASC, transaction_height ASC, transaction_index ASC "); } private static Bid getNextOrder(long assetId) { try (Connection con = Db.db.getConnection(); PreparedStatement pstmt = con.prepareStatement("SELECT * FROM bid_order WHERE asset_id = ? " + "AND latest = TRUE ORDER BY price DESC, creation_height ASC, transaction_height ASC, transaction_index ASC LIMIT 1")) { pstmt.setLong(1, assetId); try (DbIterator<Bid> bidOrders = bidOrderTable.getManyBy(con, pstmt, true)) { return bidOrders.hasNext() ? bidOrders.next() : null; } } catch (SQLException e) { throw new RuntimeException(e.toString(), e); } } static void addOrder(Transaction transaction, Attachment.ColoredCoinsBidOrderPlacement attachment) { Bid order = new Bid(transaction, attachment); bidOrderTable.insert(order); matchOrders(attachment.getAssetId()); } static void removeOrder(long orderId) { bidOrderTable.delete(getBidOrder(orderId)); } static void init() {} private final DbKey dbKey; private Bid(Transaction transaction, Attachment.ColoredCoinsBidOrderPlacement attachment) { super(transaction, attachment); this.dbKey = bidOrderDbKeyFactory.newKey(super.id); } private Bid(ResultSet rs) throws SQLException { super(rs); this.dbKey = bidOrderDbKeyFactory.newKey(super.id); } private void save(Connection con, String table) throws SQLException { super.save(con, table); } private void updateQuantityQNT(long quantityQNT) { super.setQuantityQNT(quantityQNT); if (quantityQNT > 0) { bidOrderTable.insert(this); } else if (quantityQNT == 0) { bidOrderTable.delete(this); } else { throw new IllegalArgumentException("Negative quantity: " + quantityQNT + " for order: " + Long.toUnsignedString(getId())); } } /* @Override public int compareTo(Bid o) { if (this.getPriceNQT() > o.getPriceNQT()) { return -1; } else if (this.getPriceNQT() < o.getPriceNQT()) { return 1; } else { return super.compareTo(o); } } */ } }