/****************************************************************************** * 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 nxt.util.Listener; import nxt.util.Listeners; import java.sql.Connection; import java.sql.PreparedStatement; import java.sql.ResultSet; import java.sql.SQLException; import java.util.ArrayList; import java.util.List; @SuppressWarnings("UnusedDeclaration") public final class Currency { public enum Event { BEFORE_DISTRIBUTE_CROWDFUNDING, BEFORE_UNDO_CROWDFUNDING, BEFORE_DELETE } private static final DbKey.LongKeyFactory<Currency> currencyDbKeyFactory = new DbKey.LongKeyFactory<Currency>("id") { @Override public DbKey newKey(Currency currency) { return currency.dbKey; } }; private static final VersionedEntityDbTable<Currency> currencyTable = new VersionedEntityDbTable<Currency>("currency", currencyDbKeyFactory, "code,name,description") { @Override protected Currency load(Connection con, ResultSet rs) throws SQLException { return new Currency(rs); } @Override protected void save(Connection con, Currency currency) throws SQLException { currency.save(con); } @Override public String defaultSort() { return " ORDER BY creation_height DESC "; } }; private static final class CurrencySupply { private final DbKey dbKey; private final long currencyId; private long currentSupply; private long currentReservePerUnitNQT; private CurrencySupply(Currency currency) { this.currencyId = currency.currencyId; this.dbKey = currencySupplyDbKeyFactory.newKey(this.currencyId); } private CurrencySupply(ResultSet rs) throws SQLException { this.currencyId = rs.getLong("id"); this.dbKey = currencySupplyDbKeyFactory.newKey(this.currencyId); this.currentSupply = rs.getLong("current_supply"); this.currentReservePerUnitNQT = rs.getLong("current_reserve_per_unit_nqt"); } private void save(Connection con) throws SQLException { try (PreparedStatement pstmt = con.prepareStatement("MERGE INTO currency_supply (id, current_supply, " + "current_reserve_per_unit_nqt, height, latest) " + "KEY (id, height) VALUES (?, ?, ?, ?, TRUE)")) { int i = 0; pstmt.setLong(++i, this.currencyId); pstmt.setLong(++i, this.currentSupply); pstmt.setLong(++i, this.currentReservePerUnitNQT); pstmt.setInt(++i, Nxt.getBlockchain().getHeight()); pstmt.executeUpdate(); } } } private static final DbKey.LongKeyFactory<CurrencySupply> currencySupplyDbKeyFactory = new DbKey.LongKeyFactory<CurrencySupply>("id") { @Override public DbKey newKey(CurrencySupply currencySupply) { return currencySupply.dbKey; } }; private static final VersionedEntityDbTable<CurrencySupply> currencySupplyTable = new VersionedEntityDbTable<CurrencySupply>("currency_supply", currencySupplyDbKeyFactory) { @Override protected CurrencySupply load(Connection con, ResultSet rs) throws SQLException { return new CurrencySupply(rs); } @Override protected void save(Connection con, CurrencySupply currencySupply) throws SQLException { currencySupply.save(con); } }; private static final Listeners<Currency,Event> listeners = new Listeners<>(); public static boolean addListener(Listener<Currency> listener, Event eventType) { return listeners.addListener(listener, eventType); } public static boolean removeListener(Listener<Currency> listener, Event eventType) { return listeners.removeListener(listener, eventType); } public static DbIterator<Currency> getAllCurrencies(int from, int to) { return currencyTable.getAll(from, to); } public static int getCount() { return currencyTable.getCount(); } public static Currency getCurrency(long id) { return currencyTable.get(currencyDbKeyFactory.newKey(id)); } public static Currency getCurrencyByName(String name) { return currencyTable.getBy(new DbClause.StringClause("name_lower", name.toLowerCase())); } public static Currency getCurrencyByCode(String code) { return currencyTable.getBy(new DbClause.StringClause("code", code.toUpperCase())); } public static DbIterator<Currency> getCurrencyIssuedBy(long accountId, int from, int to) { return currencyTable.getManyBy(new DbClause.LongClause("account_id", accountId), from, to); } public static DbIterator<Currency> searchCurrencies(String query, int from, int to) { return currencyTable.search(query, DbClause.EMPTY_CLAUSE, from, to, " ORDER BY ft.score DESC, currency.creation_height DESC "); } static void addCurrency(LedgerEvent event, long eventId, Transaction transaction, Account senderAccount, Attachment.MonetarySystemCurrencyIssuance attachment) { Currency oldCurrency; if ((oldCurrency = Currency.getCurrencyByCode(attachment.getCode())) != null) { oldCurrency.delete(event, eventId, senderAccount); } if ((oldCurrency = Currency.getCurrencyByCode(attachment.getName())) != null) { oldCurrency.delete(event, eventId, senderAccount); } if ((oldCurrency = Currency.getCurrencyByName(attachment.getName())) != null) { oldCurrency.delete(event, eventId, senderAccount); } if ((oldCurrency = Currency.getCurrencyByName(attachment.getCode())) != null) { oldCurrency.delete(event, eventId, senderAccount); } Currency currency = new Currency(transaction, attachment); currencyTable.insert(currency); if (currency.is(CurrencyType.MINTABLE) || currency.is(CurrencyType.RESERVABLE)) { CurrencySupply currencySupply = currency.getSupplyData(); currencySupply.currentSupply = attachment.getInitialSupply(); currencySupplyTable.insert(currencySupply); } } static { Nxt.getBlockchainProcessor().addListener(new CrowdFundingListener(), BlockchainProcessor.Event.AFTER_BLOCK_APPLY); } static void init() {} private final long currencyId; private final DbKey dbKey; private final long accountId; private final String name; private final String code; private final String description; private final int type; private final long maxSupply; private final long reserveSupply; private final int creationHeight; private final int issuanceHeight; private final long minReservePerUnitNQT; private final int minDifficulty; private final int maxDifficulty; private final byte ruleset; private final byte algorithm; private final byte decimals; private final long initialSupply; private CurrencySupply currencySupply; private Currency(Transaction transaction, Attachment.MonetarySystemCurrencyIssuance attachment) { this.currencyId = transaction.getId(); this.dbKey = currencyDbKeyFactory.newKey(this.currencyId); this.accountId = transaction.getSenderId(); this.name = attachment.getName(); this.code = attachment.getCode(); this.description = attachment.getDescription(); this.type = attachment.getType(); this.initialSupply = attachment.getInitialSupply(); this.reserveSupply = attachment.getReserveSupply(); this.maxSupply = attachment.getMaxSupply(); this.creationHeight = Nxt.getBlockchain().getHeight(); this.issuanceHeight = attachment.getIssuanceHeight(); this.minReservePerUnitNQT = attachment.getMinReservePerUnitNQT(); this.minDifficulty = attachment.getMinDifficulty(); this.maxDifficulty = attachment.getMaxDifficulty(); this.ruleset = attachment.getRuleset(); this.algorithm = attachment.getAlgorithm(); this.decimals = attachment.getDecimals(); } private Currency(ResultSet rs) throws SQLException { this.currencyId = rs.getLong("id"); this.dbKey = currencyDbKeyFactory.newKey(this.currencyId); this.accountId = rs.getLong("account_id"); this.name = rs.getString("name"); this.code = rs.getString("code"); this.description = rs.getString("description"); this.type = rs.getInt("type"); this.initialSupply = rs.getLong("initial_supply"); this.reserveSupply = rs.getLong("reserve_supply"); this.maxSupply = rs.getLong("max_supply"); this.creationHeight = rs.getInt("creation_height"); this.issuanceHeight = rs.getInt("issuance_height"); this.minReservePerUnitNQT = rs.getLong("min_reserve_per_unit_nqt"); this.minDifficulty = rs.getByte("min_difficulty") & 0xFF; this.maxDifficulty = rs.getByte("max_difficulty") & 0xFF; this.ruleset = rs.getByte("ruleset"); this.algorithm = rs.getByte("algorithm"); this.decimals = rs.getByte("decimals"); } private void save(Connection con) throws SQLException { try (PreparedStatement pstmt = con.prepareStatement("MERGE INTO currency (id, account_id, name, code, " + "description, type, initial_supply, reserve_supply, max_supply, creation_height, issuance_height, min_reserve_per_unit_nqt, " + "min_difficulty, max_difficulty, ruleset, algorithm, decimals, height, latest) " + "KEY (id, height) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, TRUE)")) { int i = 0; pstmt.setLong(++i, this.currencyId); pstmt.setLong(++i, this.accountId); pstmt.setString(++i, this.name); pstmt.setString(++i, this.code); pstmt.setString(++i, this.description); pstmt.setInt(++i, this.type); pstmt.setLong(++i, this.initialSupply); pstmt.setLong(++i, this.reserveSupply); pstmt.setLong(++i, this.maxSupply); pstmt.setInt(++i, this.creationHeight); pstmt.setInt(++i, this.issuanceHeight); pstmt.setLong(++i, this.minReservePerUnitNQT); pstmt.setByte(++i, (byte)this.minDifficulty); pstmt.setByte(++i, (byte)this.maxDifficulty); pstmt.setByte(++i, this.ruleset); pstmt.setByte(++i, this.algorithm); pstmt.setByte(++i, this.decimals); pstmt.setInt(++i, Nxt.getBlockchain().getHeight()); pstmt.executeUpdate(); } } public long getId() { return currencyId; } public long getAccountId() { return accountId; } public String getName() { return name; } public String getCode() { return code; } public String getDescription() { return description; } public int getType() { return type; } public long getInitialSupply() { return initialSupply; } public long getCurrentSupply() { if (!is(CurrencyType.RESERVABLE) && !is(CurrencyType.MINTABLE)) { return initialSupply; } if (getSupplyData() == null) { return 0; } return currencySupply.currentSupply; } public long getReserveSupply() { return reserveSupply; } public long getMaxSupply() { return maxSupply; } public int getCreationHeight() { return creationHeight; } public int getIssuanceHeight() { return issuanceHeight; } public long getMinReservePerUnitNQT() { return minReservePerUnitNQT; } public int getMinDifficulty() { return minDifficulty; } public int getMaxDifficulty() { return maxDifficulty; } public byte getRuleset() { return ruleset; } public byte getAlgorithm() { return algorithm; } public byte getDecimals() { return decimals; } public long getCurrentReservePerUnitNQT() { if (!is(CurrencyType.RESERVABLE) || getSupplyData() == null) { return 0; } return currencySupply.currentReservePerUnitNQT; } public boolean isActive() { return issuanceHeight <= BlockchainImpl.getInstance().getHeight(); } private CurrencySupply getSupplyData() { if (!is(CurrencyType.RESERVABLE) && !is(CurrencyType.MINTABLE)) { return null; } if (currencySupply == null) { currencySupply = currencySupplyTable.get(currencySupplyDbKeyFactory.newKey(currencyId)); if (currencySupply == null) { currencySupply = new CurrencySupply(this); } } return currencySupply; } static void increaseReserve(LedgerEvent event, long eventId, Account account, long currencyId, long amountPerUnitNQT) { Currency currency = Currency.getCurrency(currencyId); account.addToBalanceNQT(event, eventId, -Math.multiplyExact(currency.getReserveSupply(), amountPerUnitNQT)); CurrencySupply currencySupply = currency.getSupplyData(); currencySupply.currentReservePerUnitNQT += amountPerUnitNQT; currencySupplyTable.insert(currencySupply); CurrencyFounder.addOrUpdateFounder(currencyId, account.getId(), amountPerUnitNQT); } static void claimReserve(LedgerEvent event, long eventId, Account account, long currencyId, long units) { account.addToCurrencyUnits(event, eventId, currencyId, -units); Currency currency = Currency.getCurrency(currencyId); currency.increaseSupply(- units); account.addToBalanceAndUnconfirmedBalanceNQT(event, eventId, Math.multiplyExact(units, currency.getCurrentReservePerUnitNQT())); } static void transferCurrency(LedgerEvent event, long eventId, Account senderAccount, Account recipientAccount, long currencyId, long units) { senderAccount.addToCurrencyUnits(event, eventId, currencyId, -units); recipientAccount.addToCurrencyAndUnconfirmedCurrencyUnits(event, eventId, currencyId, units); } void increaseSupply(long units) { getSupplyData(); currencySupply.currentSupply += units; if (currencySupply.currentSupply > maxSupply || currencySupply.currentSupply < 0) { currencySupply.currentSupply -= units; throw new IllegalArgumentException("Cannot add " + units + " to current supply of " + currencySupply.currentSupply); } currencySupplyTable.insert(currencySupply); } public DbIterator<Account.AccountCurrency> getAccounts(int from, int to) { return Account.getCurrencyAccounts(this.currencyId, from, to); } public DbIterator<Account.AccountCurrency> getAccounts(int height, int from, int to) { return Account.getCurrencyAccounts(this.currencyId, height, from, to); } public DbIterator<Exchange> getExchanges(int from, int to) { return Exchange.getCurrencyExchanges(this.currencyId, from, to); } public DbIterator<CurrencyTransfer> getTransfers(int from, int to) { return CurrencyTransfer.getCurrencyTransfers(this.currencyId, from, to); } public boolean is(CurrencyType type) { return (this.type & type.getCode()) != 0; } public boolean canBeDeletedBy(long senderAccountId) { if (!is(CurrencyType.NON_SHUFFLEABLE) && Shuffling.getHoldingShufflingCount(currencyId, false) > 0) { return false; } if (!isActive()) { return senderAccountId == accountId; } if (is(CurrencyType.MINTABLE) && getCurrentSupply() < maxSupply && senderAccountId != accountId) { return false; } try (DbIterator<Account.AccountCurrency> accountCurrencies = Account.getCurrencyAccounts(this.currencyId, 0, -1)) { return ! accountCurrencies.hasNext() || accountCurrencies.next().getAccountId() == senderAccountId && ! accountCurrencies.hasNext(); } } void delete(LedgerEvent event, long eventId, Account senderAccount) { if (!canBeDeletedBy(senderAccount.getId())) { // shouldn't happen as ownership has already been checked in validate, but as a safety check throw new IllegalStateException("Currency " + Long.toUnsignedString(currencyId) + " not entirely owned by " + Long.toUnsignedString(senderAccount.getId())); } listeners.notify(this, Event.BEFORE_DELETE); if (is(CurrencyType.RESERVABLE)) { if (is(CurrencyType.CLAIMABLE) && isActive()) { senderAccount.addToUnconfirmedCurrencyUnits(event, eventId, currencyId, -senderAccount.getCurrencyUnits(currencyId)); Currency.claimReserve(event, eventId, senderAccount, currencyId, senderAccount.getCurrencyUnits(currencyId)); } if (!isActive()) { try (DbIterator<CurrencyFounder> founders = CurrencyFounder.getCurrencyFounders(currencyId, 0, Integer.MAX_VALUE)) { for (CurrencyFounder founder : founders) { Account.getAccount(founder.getAccountId()) .addToBalanceAndUnconfirmedBalanceNQT(event, eventId, Math.multiplyExact(reserveSupply, founder.getAmountPerUnitNQT())); } } } CurrencyFounder.remove(currencyId); } if (is(CurrencyType.EXCHANGEABLE)) { List<CurrencyBuyOffer> buyOffers = new ArrayList<>(); try (DbIterator<CurrencyBuyOffer> offers = CurrencyBuyOffer.getOffers(this, 0, -1)) { while (offers.hasNext()) { buyOffers.add(offers.next()); } } buyOffers.forEach((offer) -> CurrencyExchangeOffer.removeOffer(event, offer)); } if (is(CurrencyType.MINTABLE)) { CurrencyMint.deleteCurrency(this); } senderAccount.addToUnconfirmedCurrencyUnits(event, eventId, currencyId, -senderAccount.getUnconfirmedCurrencyUnits(currencyId)); senderAccount.addToCurrencyUnits(event, eventId, currencyId, -senderAccount.getCurrencyUnits(currencyId)); currencyTable.delete(this); } private static final class CrowdFundingListener implements Listener<Block> { @Override public void notify(Block block) { if (block.getHeight() <= Constants.MONETARY_SYSTEM_BLOCK) { return; } try (DbIterator<Currency> issuedCurrencies = currencyTable.getManyBy(new DbClause.IntClause("issuance_height", block.getHeight()), 0, -1)) { for (Currency currency : issuedCurrencies) { if (currency.getCurrentReservePerUnitNQT() < currency.getMinReservePerUnitNQT()) { listeners.notify(currency, Event.BEFORE_UNDO_CROWDFUNDING); undoCrowdFunding(currency); } else { listeners.notify(currency, Event.BEFORE_DISTRIBUTE_CROWDFUNDING); distributeCurrency(currency); } } } } private void undoCrowdFunding(Currency currency) { try (DbIterator<CurrencyFounder> founders = CurrencyFounder.getCurrencyFounders(currency.getId(), 0, Integer.MAX_VALUE)) { for (CurrencyFounder founder : founders) { Account.getAccount(founder.getAccountId()) .addToBalanceAndUnconfirmedBalanceNQT(LedgerEvent.CURRENCY_UNDO_CROWDFUNDING, currency.getId(), Math.multiplyExact(currency.getReserveSupply(), founder.getAmountPerUnitNQT())); } } Account.getAccount(currency.getAccountId()) .addToCurrencyAndUnconfirmedCurrencyUnits(LedgerEvent.CURRENCY_UNDO_CROWDFUNDING, currency.getId(), currency.getId(), - currency.getInitialSupply()); currencyTable.delete(currency); CurrencyFounder.remove(currency.getId()); } private void distributeCurrency(Currency currency) { long totalAmountPerUnit = 0; final long remainingSupply = currency.getReserveSupply() - currency.getInitialSupply(); List<CurrencyFounder> currencyFounders = new ArrayList<>(); try (DbIterator<CurrencyFounder> founders = CurrencyFounder.getCurrencyFounders(currency.getId(), 0, Integer.MAX_VALUE)) { for (CurrencyFounder founder : founders) { totalAmountPerUnit += founder.getAmountPerUnitNQT(); currencyFounders.add(founder); } } CurrencySupply currencySupply = currency.getSupplyData(); for (CurrencyFounder founder : currencyFounders) { long units = Math.multiplyExact(remainingSupply, founder.getAmountPerUnitNQT()) / totalAmountPerUnit; currencySupply.currentSupply += units; Account.getAccount(founder.getAccountId()) .addToCurrencyAndUnconfirmedCurrencyUnits(LedgerEvent.CURRENCY_DISTRIBUTION, currency.getId(), currency.getId(), units); } Account issuerAccount = Account.getAccount(currency.getAccountId()); issuerAccount.addToCurrencyAndUnconfirmedCurrencyUnits(LedgerEvent.CURRENCY_DISTRIBUTION, currency.getId(), currency.getId(), currency.getReserveSupply() - currency.getCurrentSupply()); if (!currency.is(CurrencyType.CLAIMABLE)) { issuerAccount.addToBalanceAndUnconfirmedBalanceNQT(LedgerEvent.CURRENCY_DISTRIBUTION, currency.getId(), Math.multiplyExact(totalAmountPerUnit, currency.getReserveSupply())); } currencySupply.currentSupply = currency.getReserveSupply(); currencySupplyTable.insert(currencySupply); } } }