/****************************************************************************** * 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 java.sql.Connection; import java.sql.PreparedStatement; import java.sql.ResultSet; import java.sql.SQLException; import java.util.ArrayList; import java.util.List; public abstract class CurrencyExchangeOffer { public static final class AvailableOffers { private final long rateNQT; private final long units; private final long amountNQT; private AvailableOffers(long rateNQT, long units, long amountNQT) { this.rateNQT = rateNQT; this.units = units; this.amountNQT = amountNQT; } public long getRateNQT() { return rateNQT; } public long getUnits() { return units; } public long getAmountNQT() { return amountNQT; } } static { Nxt.getBlockchainProcessor().addListener(block -> { if (block.getHeight() <= Constants.MONETARY_SYSTEM_BLOCK) { return; } List<CurrencyBuyOffer> expired = new ArrayList<>(); try (DbIterator<CurrencyBuyOffer> offers = CurrencyBuyOffer.getOffers(new DbClause.IntClause("expiration_height", block.getHeight()), 0, -1)) { for (CurrencyBuyOffer offer : offers) { expired.add(offer); } } expired.forEach((offer) -> CurrencyExchangeOffer.removeOffer(LedgerEvent.CURRENCY_OFFER_EXPIRED, offer)); }, BlockchainProcessor.Event.AFTER_BLOCK_APPLY); } static void publishOffer(Transaction transaction, Attachment.MonetarySystemPublishExchangeOffer attachment) { CurrencyBuyOffer previousOffer = CurrencyBuyOffer.getOffer(attachment.getCurrencyId(), transaction.getSenderId()); if (previousOffer != null) { CurrencyExchangeOffer.removeOffer(LedgerEvent.CURRENCY_OFFER_REPLACED, previousOffer); } CurrencyBuyOffer.addOffer(transaction, attachment); CurrencySellOffer.addOffer(transaction, attachment); } private static AvailableOffers calculateTotal(List<CurrencyExchangeOffer> offers, final long units) { long totalAmountNQT = 0; long remainingUnits = units; long rateNQT = 0; for (CurrencyExchangeOffer offer : offers) { if (remainingUnits == 0) { break; } rateNQT = offer.getRateNQT(); long curUnits = Math.min(Math.min(remainingUnits, offer.getSupply()), offer.getLimit()); long curAmountNQT = Math.multiplyExact(curUnits, offer.getRateNQT()); totalAmountNQT = Math.addExact(totalAmountNQT, curAmountNQT); remainingUnits = Math.subtractExact(remainingUnits, curUnits); } return new AvailableOffers(rateNQT, Math.subtractExact(units, remainingUnits), totalAmountNQT); } static final DbClause availableOnlyDbClause = new DbClause.LongClause("unit_limit", DbClause.Op.NE, 0) .and(new DbClause.LongClause("supply", DbClause.Op.NE, 0)); public static AvailableOffers getAvailableToSell(final long currencyId, final long units) { return calculateTotal(getAvailableBuyOffers(currencyId, 0L), units); } private static List<CurrencyExchangeOffer> getAvailableBuyOffers(long currencyId, long minRateNQT) { List<CurrencyExchangeOffer> currencyExchangeOffers = new ArrayList<>(); DbClause dbClause = new DbClause.LongClause("currency_id", currencyId).and(availableOnlyDbClause); if (minRateNQT > 0) { dbClause = dbClause.and(new DbClause.LongClause("rate", DbClause.Op.GTE, minRateNQT)); } try (DbIterator<CurrencyBuyOffer> offers = CurrencyBuyOffer.getOffers(dbClause, 0, -1, " ORDER BY rate DESC, creation_height ASC, transaction_height ASC, transaction_index ASC ")) { for (CurrencyBuyOffer offer : offers) { currencyExchangeOffers.add(offer); } } return currencyExchangeOffers; } static void exchangeCurrencyForNXT(Transaction transaction, Account account, final long currencyId, final long rateNQT, final long units) { List<CurrencyExchangeOffer> currencyBuyOffers = getAvailableBuyOffers(currencyId, rateNQT); long totalAmountNQT = 0; long remainingUnits = units; for (CurrencyExchangeOffer offer : currencyBuyOffers) { if (remainingUnits == 0) { break; } long curUnits = Math.min(Math.min(remainingUnits, offer.getSupply()), offer.getLimit()); long curAmountNQT = Math.multiplyExact(curUnits, offer.getRateNQT()); totalAmountNQT = Math.addExact(totalAmountNQT, curAmountNQT); remainingUnits = Math.subtractExact(remainingUnits, curUnits); offer.decreaseLimitAndSupply(curUnits); long excess = offer.getCounterOffer().increaseSupply(curUnits); Account counterAccount = Account.getAccount(offer.getAccountId()); counterAccount.addToBalanceNQT(LedgerEvent.CURRENCY_EXCHANGE, offer.getId(), -curAmountNQT); counterAccount.addToCurrencyUnits(LedgerEvent.CURRENCY_EXCHANGE, offer.getId(), currencyId, curUnits); counterAccount.addToUnconfirmedCurrencyUnits(LedgerEvent.CURRENCY_EXCHANGE, offer.getId(), currencyId, excess); Exchange.addExchange(transaction, currencyId, offer, account.getId(), offer.getAccountId(), curUnits); } long transactionId = transaction.getId(); account.addToBalanceAndUnconfirmedBalanceNQT(LedgerEvent.CURRENCY_EXCHANGE, transactionId, totalAmountNQT); account.addToCurrencyUnits(LedgerEvent.CURRENCY_EXCHANGE, transactionId, currencyId, -(units - remainingUnits)); account.addToUnconfirmedCurrencyUnits(LedgerEvent.CURRENCY_EXCHANGE, transactionId, currencyId, remainingUnits); } public static AvailableOffers getAvailableToBuy(final long currencyId, final long units) { return calculateTotal(getAvailableSellOffers(currencyId, 0L), units); } private static List<CurrencyExchangeOffer> getAvailableSellOffers(long currencyId, long maxRateNQT) { List<CurrencyExchangeOffer> currencySellOffers = new ArrayList<>(); DbClause dbClause = new DbClause.LongClause("currency_id", currencyId).and(availableOnlyDbClause); if (maxRateNQT > 0) { dbClause = dbClause.and(new DbClause.LongClause("rate", DbClause.Op.LTE, maxRateNQT)); } try (DbIterator<CurrencySellOffer> offers = CurrencySellOffer.getOffers(dbClause, 0, -1, " ORDER BY rate ASC, creation_height ASC, transaction_height ASC, transaction_index ASC ")) { for (CurrencySellOffer offer : offers) { currencySellOffers.add(offer); } } return currencySellOffers; } static void exchangeNXTForCurrency(Transaction transaction, Account account, final long currencyId, final long rateNQT, final long units) { List<CurrencyExchangeOffer> currencySellOffers = getAvailableSellOffers(currencyId, rateNQT); if (Nxt.getBlockchain().getHeight() < Constants.SHUFFLING_BLOCK) { long totalUnits = 0; long totalAmountNQT = Math.multiplyExact(units, rateNQT); long remainingAmountNQT = totalAmountNQT; for (CurrencyExchangeOffer offer : currencySellOffers) { if (remainingAmountNQT == 0) { break; } long curUnits = Math.min(Math.min(remainingAmountNQT / offer.getRateNQT(), offer.getSupply()), offer.getLimit()); if (curUnits == 0) { continue; } long curAmountNQT = Math.multiplyExact(curUnits, offer.getRateNQT()); totalUnits = Math.addExact(totalUnits, curUnits); remainingAmountNQT = Math.subtractExact(remainingAmountNQT, curAmountNQT); offer.decreaseLimitAndSupply(curUnits); long excess = offer.getCounterOffer().increaseSupply(curUnits); Account counterAccount = Account.getAccount(offer.getAccountId()); counterAccount.addToBalanceNQT(LedgerEvent.CURRENCY_EXCHANGE, offer.getId(), curAmountNQT); counterAccount.addToUnconfirmedBalanceNQT(LedgerEvent.CURRENCY_EXCHANGE, offer.getId(), Math.addExact( Math.multiplyExact(curUnits - excess, offer.getRateNQT() - offer.getCounterOffer().getRateNQT()), Math.multiplyExact(excess, offer.getRateNQT()) ) ); counterAccount.addToCurrencyUnits(LedgerEvent.CURRENCY_EXCHANGE, offer.getId(), currencyId, -curUnits); Exchange.addExchange(transaction, currencyId, offer, offer.getAccountId(), account.getId(), curUnits); } long transactionId = transaction.getId(); account.addToCurrencyAndUnconfirmedCurrencyUnits(LedgerEvent.CURRENCY_EXCHANGE, transactionId, currencyId, totalUnits); account.addToBalanceNQT(LedgerEvent.CURRENCY_EXCHANGE, transactionId, -(totalAmountNQT - remainingAmountNQT)); account.addToUnconfirmedBalanceNQT(LedgerEvent.CURRENCY_EXCHANGE, transactionId, remainingAmountNQT); } else { long totalAmountNQT = 0; long remainingUnits = units; for (CurrencyExchangeOffer offer : currencySellOffers) { if (remainingUnits == 0) { break; } long curUnits = Math.min(Math.min(remainingUnits, offer.getSupply()), offer.getLimit()); long curAmountNQT = Math.multiplyExact(curUnits, offer.getRateNQT()); totalAmountNQT = Math.addExact(totalAmountNQT, curAmountNQT); remainingUnits = Math.subtractExact(remainingUnits, curUnits); offer.decreaseLimitAndSupply(curUnits); long excess = offer.getCounterOffer().increaseSupply(curUnits); Account counterAccount = Account.getAccount(offer.getAccountId()); counterAccount.addToBalanceNQT(LedgerEvent.CURRENCY_EXCHANGE, offer.getId(), curAmountNQT); counterAccount.addToUnconfirmedBalanceNQT(LedgerEvent.CURRENCY_EXCHANGE, offer.getId(), Math.addExact( Math.multiplyExact(curUnits - excess, offer.getRateNQT() - offer.getCounterOffer().getRateNQT()), Math.multiplyExact(excess, offer.getRateNQT()) ) ); counterAccount.addToCurrencyUnits(LedgerEvent.CURRENCY_EXCHANGE, offer.getId(), currencyId, -curUnits); Exchange.addExchange(transaction, currencyId, offer, offer.getAccountId(), account.getId(), curUnits); } long transactionId = transaction.getId(); account.addToCurrencyAndUnconfirmedCurrencyUnits(LedgerEvent.CURRENCY_EXCHANGE, transactionId, currencyId, Math.subtractExact(units, remainingUnits)); account.addToBalanceNQT(LedgerEvent.CURRENCY_EXCHANGE, transactionId, -totalAmountNQT); account.addToUnconfirmedBalanceNQT(LedgerEvent.CURRENCY_EXCHANGE, transactionId, Math.multiplyExact(units, rateNQT) - totalAmountNQT); } } static void removeOffer(LedgerEvent event, CurrencyBuyOffer buyOffer) { CurrencySellOffer sellOffer = buyOffer.getCounterOffer(); CurrencyBuyOffer.remove(buyOffer); CurrencySellOffer.remove(sellOffer); Account account = Account.getAccount(buyOffer.getAccountId()); account.addToUnconfirmedBalanceNQT(event, buyOffer.getId(), Math.multiplyExact(buyOffer.getSupply(), buyOffer.getRateNQT())); account.addToUnconfirmedCurrencyUnits(event, buyOffer.getId(), buyOffer.getCurrencyId(), sellOffer.getSupply()); } final long id; private final long currencyId; private final long accountId; private final long rateNQT; private long limit; // limit on the total sum of units for this offer across transactions private long supply; // total units supply for the offer private final int expirationHeight; private final int creationHeight; private final short transactionIndex; private final int transactionHeight; CurrencyExchangeOffer(long id, long currencyId, long accountId, long rateNQT, long limit, long supply, int expirationHeight, int transactionHeight, short transactionIndex) { this.id = id; this.currencyId = currencyId; this.accountId = accountId; this.rateNQT = rateNQT; this.limit = limit; this.supply = supply; this.expirationHeight = expirationHeight; this.creationHeight = Nxt.getBlockchain().getHeight(); this.transactionIndex = transactionIndex; this.transactionHeight = transactionHeight; } CurrencyExchangeOffer(ResultSet rs) throws SQLException { this.id = rs.getLong("id"); this.currencyId = rs.getLong("currency_id"); this.accountId = rs.getLong("account_id"); this.rateNQT = rs.getLong("rate"); this.limit = rs.getLong("unit_limit"); this.supply = rs.getLong("supply"); this.expirationHeight = rs.getInt("expiration_height"); this.creationHeight = rs.getInt("creation_height"); this.transactionIndex = rs.getShort("transaction_index"); this.transactionHeight = rs.getInt("transaction_height"); } void save(Connection con, String table) throws SQLException { try (PreparedStatement pstmt = con.prepareStatement("MERGE INTO " + table + " (id, currency_id, account_id, " + "rate, unit_limit, supply, expiration_height, 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.currencyId); pstmt.setLong(++i, this.accountId); pstmt.setLong(++i, this.rateNQT); pstmt.setLong(++i, this.limit); pstmt.setLong(++i, this.supply); pstmt.setInt(++i, this.expirationHeight); pstmt.setInt(++i, this.creationHeight); pstmt.setShort(++i, this.transactionIndex); pstmt.setInt(++i, this.transactionHeight); pstmt.setInt(++i, Nxt.getBlockchain().getHeight()); pstmt.executeUpdate(); } } public long getId() { return id; } public long getCurrencyId() { return currencyId; } public long getAccountId() { return accountId; } public long getRateNQT() { return rateNQT; } public long getLimit() { return limit; } public long getSupply() { return supply; } public int getExpirationHeight() { return expirationHeight; } public int getHeight() { return creationHeight; } public abstract CurrencyExchangeOffer getCounterOffer(); long increaseSupply(long delta) { long excess = Math.max(Math.addExact(supply, Math.subtractExact(delta, limit)), 0); supply += delta - excess; return excess; } void decreaseLimitAndSupply(long delta) { limit -= delta; supply -= delta; } }