public static DbIterator<RewardRecipientAssignment> getAccountsWithRewardRecipient( Long recipientId) { return rewardRecipientAssignmentTable.getManyBy( getAccountsWithRewardRecipientClause(recipientId, Nxt.getBlockchain().getHeight() + 1), 0, -1); }
public long getEffectiveBalanceNXT() { Block lastBlock = Nxt.getBlockchain().getLastBlock(); if (lastBlock.getHeight() >= Constants.TRANSPARENT_FORGING_BLOCK_6 && (getPublicKey() == null || lastBlock.getHeight() - keyHeight <= 1440)) { return 0; // cfb: Accounts with the public key revealed less than 1440 blocks ago are not // allowed to generate blocks } if (lastBlock.getHeight() < Constants.TRANSPARENT_FORGING_BLOCK_3 && this.creationHeight < Constants.TRANSPARENT_FORGING_BLOCK_2) { if (this.creationHeight == 0) { return getBalanceNQT() / Constants.ONE_NXT; } if (lastBlock.getHeight() - this.creationHeight < 1440) { return 0; } long receivedInlastBlock = 0; for (Transaction transaction : lastBlock.getTransactions()) { if (id == transaction.getRecipientId()) { receivedInlastBlock += transaction.getAmountNQT(); } } return (getBalanceNQT() - receivedInlastBlock) / Constants.ONE_NXT; } if (lastBlock.getHeight() < currentLeasingHeightFrom) { return (getGuaranteedBalanceNQT(1440) + getLessorsGuaranteedBalanceNQT()) / Constants.ONE_NXT; } return getLessorsGuaranteedBalanceNQT() / Constants.ONE_NXT; }
void leaseEffectiveBalance(long lesseeId, short period) { Account lessee = Account.getAccount(lesseeId); if (lessee != null && lessee.getPublicKey() != null) { int height = Nxt.getBlockchain().getHeight(); if (currentLeasingHeightFrom == Integer.MAX_VALUE) { currentLeasingHeightFrom = height + 1440; currentLeasingHeightTo = currentLeasingHeightFrom + period; currentLesseeId = lesseeId; nextLeasingHeightFrom = Integer.MAX_VALUE; accountTable.insert(this); leaseListeners.notify( new AccountLease( this.getId(), lesseeId, currentLeasingHeightFrom, currentLeasingHeightTo), Event.LEASE_SCHEDULED); } else { nextLeasingHeightFrom = height + 1440; if (nextLeasingHeightFrom < currentLeasingHeightTo) { nextLeasingHeightFrom = currentLeasingHeightTo; } nextLeasingHeightTo = nextLeasingHeightFrom + period; nextLesseeId = lesseeId; accountTable.insert(this); leaseListeners.notify( new AccountLease(this.getId(), lesseeId, nextLeasingHeightFrom, nextLeasingHeightTo), Event.LEASE_SCHEDULED); } } }
private void save(Connection con) throws SQLException { try (PreparedStatement pstmt = con.prepareStatement( "MERGE INTO account (id, creation_height, public_key, " + "key_height, balance, unconfirmed_balance, forged_balance, name, description, " + "current_leasing_height_from, current_leasing_height_to, current_lessee_id, " + "next_leasing_height_from, next_leasing_height_to, next_lessee_id, " + "height, latest) " + "KEY (id, height) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, TRUE)")) { int i = 0; pstmt.setLong(++i, this.getId()); pstmt.setInt(++i, this.getCreationHeight()); DbUtils.setBytes(pstmt, ++i, this.getPublicKey()); pstmt.setInt(++i, this.getKeyHeight()); pstmt.setLong(++i, this.getBalanceNQT()); pstmt.setLong(++i, this.getUnconfirmedBalanceNQT()); pstmt.setLong(++i, this.getForgedBalanceNQT()); DbUtils.setString(pstmt, ++i, this.getName()); DbUtils.setString(pstmt, ++i, this.getDescription()); DbUtils.setIntZeroToNull(pstmt, ++i, this.getCurrentLeasingHeightFrom()); DbUtils.setIntZeroToNull(pstmt, ++i, this.getCurrentLeasingHeightTo()); DbUtils.setLongZeroToNull(pstmt, ++i, this.getCurrentLesseeId()); DbUtils.setIntZeroToNull(pstmt, ++i, this.getNextLeasingHeightFrom()); DbUtils.setIntZeroToNull(pstmt, ++i, this.getNextLeasingHeightTo()); DbUtils.setLongZeroToNull(pstmt, ++i, this.getNextLesseeId()); pstmt.setInt(++i, Nxt.getBlockchain().getHeight()); pstmt.executeUpdate(); } }
private void addToGuaranteedBalanceNQT(long amountNQT) { if (amountNQT <= 0) { return; } int blockchainHeight = Nxt.getBlockchain().getHeight(); try (Connection con = Db.getConnection(); PreparedStatement pstmtSelect = con.prepareStatement( "SELECT additions FROM account_guaranteed_balance " + "WHERE account_id = ? and height = ?"); PreparedStatement pstmtUpdate = con.prepareStatement( "MERGE INTO account_guaranteed_balance (account_id, " + " additions, height) KEY (account_id, height) VALUES(?, ?, ?)")) { pstmtSelect.setLong(1, this.id); pstmtSelect.setInt(2, blockchainHeight); try (ResultSet rs = pstmtSelect.executeQuery()) { long additions = amountNQT; if (rs.next()) { additions = Convert.safeAdd(additions, rs.getLong("additions")); } pstmtUpdate.setLong(1, this.id); pstmtUpdate.setLong(2, additions); pstmtUpdate.setInt(3, blockchainHeight); pstmtUpdate.executeUpdate(); } } catch (SQLException e) { throw new RuntimeException(e.toString(), e); } }
public long getGuaranteedBalanceNQT(final int numberOfConfirmations, final int currentHeight) { if (numberOfConfirmations >= Nxt.getBlockchain().getHeight()) { return 0; } if (numberOfConfirmations > 2880 || numberOfConfirmations < 0) { throw new IllegalArgumentException( "Number of required confirmations must be between 0 and " + 2880); } int height = currentHeight - numberOfConfirmations; try (Connection con = Db.getConnection(); PreparedStatement pstmt = con.prepareStatement( "SELECT SUM (additions) AS additions " + "FROM account_guaranteed_balance WHERE account_id = ? AND height > ? AND height <= ?")) { pstmt.setLong(1, this.id); pstmt.setInt(2, height); pstmt.setInt(3, currentHeight); try (ResultSet rs = pstmt.executeQuery()) { if (!rs.next()) { return balanceNQT; } return Math.max(Convert.safeSubtract(balanceNQT, rs.getLong("additions")), 0); } } catch (SQLException e) { throw new RuntimeException(e.toString(), e); } }
private Account(long id) { if (id != Crypto.rsDecode(Crypto.rsEncode(id))) { Logger.logMessage("CRITICAL ERROR: Reed-Solomon encoding fails for " + id); } this.id = id; this.dbKey = accountDbKeyFactory.newKey(this.id); this.creationHeight = Nxt.getBlockchain().getHeight(); currentLeasingHeightFrom = Integer.MAX_VALUE; }
static { Nxt.getBlockchainProcessor() .addListener( new Listener<Block>() { @Override public void notify(Block block) { int height = block.getHeight(); try (DbIterator<Account> leasingAccounts = getLeasingAccounts()) { while (leasingAccounts.hasNext()) { Account account = leasingAccounts.next(); if (height == account.currentLeasingHeightFrom) { leaseListeners.notify( new AccountLease( account.getId(), account.currentLesseeId, height, account.currentLeasingHeightTo), Event.LEASE_STARTED); } else if (height == account.currentLeasingHeightTo) { leaseListeners.notify( new AccountLease( account.getId(), account.currentLesseeId, account.currentLeasingHeightFrom, height), Event.LEASE_ENDED); if (account.nextLeasingHeightFrom == Integer.MAX_VALUE) { account.currentLeasingHeightFrom = Integer.MAX_VALUE; account.currentLesseeId = 0; accountTable.insert(account); } else { account.currentLeasingHeightFrom = account.nextLeasingHeightFrom; account.currentLeasingHeightTo = account.nextLeasingHeightTo; account.currentLesseeId = account.nextLesseeId; account.nextLeasingHeightFrom = Integer.MAX_VALUE; account.nextLesseeId = 0; accountTable.insert(account); if (height == account.currentLeasingHeightFrom) { leaseListeners.notify( new AccountLease( account.getId(), account.currentLesseeId, height, account.currentLeasingHeightTo), Event.LEASE_STARTED); } } } } } } }, BlockchainProcessor.Event.AFTER_BLOCK_APPLY); }
private void save(Connection con) throws SQLException { try (PreparedStatement pstmt = con.prepareStatement( "INSERT INTO vote (id, poll_id, voter_id, " + "vote_bytes, height) VALUES (?, ?, ?, ?, ?)")) { int i = 0; pstmt.setLong(++i, this.id); pstmt.setLong(++i, this.pollId); pstmt.setLong(++i, this.voterId); pstmt.setBytes(++i, this.voteBytes); pstmt.setInt(++i, Nxt.getBlockchain().getHeight()); pstmt.executeUpdate(); } }
private void save(Connection con) throws SQLException { try (PreparedStatement pstmt = con.prepareStatement( "MERGE INTO reward_recip_assign " + "(account_id, prev_recip_id, recip_id, from_height, height, latest) KEY (account_id, height) VALUES (?, ?, ?, ?, ?, TRUE)")) { int i = 0; pstmt.setLong(++i, this.accountId); pstmt.setLong(++i, this.prevRecipientId); pstmt.setLong(++i, this.recipientId); pstmt.setInt(++i, this.fromHeight); pstmt.setInt(++i, Nxt.getBlockchain().getHeight()); pstmt.executeUpdate(); } }
/** Process nxt.ledgerAccounts */ static { List<String> ledgerAccounts = Nxt.getStringListProperty("nxt.ledgerAccounts"); ledgerEnabled = !ledgerAccounts.isEmpty(); trackAllAccounts = ledgerAccounts.contains("*"); if (ledgerEnabled) { if (trackAllAccounts) { Logger.logInfoMessage("Account ledger is tracking all accounts"); } else { for (String account : ledgerAccounts) { try { trackAccounts.add(Convert.parseAccountId(account)); Logger.logInfoMessage("Account ledger is tracking account " + account); } catch (RuntimeException e) { Logger.logErrorMessage("Account " + account + " is not valid; ignored"); } } } } else { Logger.logInfoMessage("Account ledger is not enabled"); } int temp = Nxt.getIntProperty("nxt.ledgerLogUnconfirmed", 1); logUnconfirmed = (temp >= 0 && temp <= 2 ? temp : 1); }
private void save(Connection con) throws SQLException { try (PreparedStatement pstmt = con.prepareStatement( "MERGE INTO account_asset " + "(account_id, asset_id, quantity, unconfirmed_quantity, height, latest) " + "KEY (account_id, asset_id, height) VALUES (?, ?, ?, ?, ?, TRUE)")) { int i = 0; pstmt.setLong(++i, this.accountId); pstmt.setLong(++i, this.assetId); pstmt.setLong(++i, this.quantityQNT); pstmt.setLong(++i, this.unconfirmedQuantityQNT); pstmt.setInt(++i, Nxt.getBlockchain().getHeight()); pstmt.executeUpdate(); } }
@Override void validateAttachment(Transaction transaction) throws NxtException.ValidationException { Attachment.MonetarySystemPublishExchangeOffer attachment = (Attachment.MonetarySystemPublishExchangeOffer) transaction.getAttachment(); if (attachment.getBuyRateNQT() <= 0 || attachment.getSellRateNQT() <= 0 || attachment.getBuyRateNQT() > attachment.getSellRateNQT()) { throw new NxtException.NotValidException( String.format( "Invalid exchange offer, buy rate %d and sell rate %d has to be larger than 0, buy rate cannot be larger than sell rate", attachment.getBuyRateNQT(), attachment.getSellRateNQT())); } if (attachment.getTotalBuyLimit() < 0 || attachment.getTotalSellLimit() < 0 || attachment.getInitialBuySupply() < 0 || attachment.getInitialSellSupply() < 0 || attachment.getExpirationHeight() < 0) { throw new NxtException.NotValidException( "Invalid exchange offer, units and height cannot be negative: " + attachment.getJSONObject()); } if (attachment.getTotalBuyLimit() < attachment.getInitialBuySupply() || attachment.getTotalSellLimit() < attachment.getInitialSellSupply()) { throw new NxtException.NotValidException( "Initial supplies must not exceed total limits"); } if (Nxt.getBlockchain().getHeight() > Constants.SHUFFLING_BLOCK) { if (attachment.getTotalBuyLimit() == 0 && attachment.getTotalSellLimit() == 0) { throw new NxtException.NotCurrentlyValidException( "Total buy and sell limits cannot be both 0"); } if (attachment.getInitialBuySupply() == 0 && attachment.getInitialSellSupply() == 0) { throw new NxtException.NotCurrentlyValidException( "Initial buy and sell supply cannot be both 0"); } } if (attachment.getExpirationHeight() <= attachment.getFinishValidationHeight(transaction)) { throw new NxtException.NotCurrentlyValidException( "Expiration height must be after transaction execution height"); } Currency currency = Currency.getCurrency(attachment.getCurrencyId()); CurrencyType.validate(currency, transaction); if (!currency.isActive()) { throw new NxtException.NotCurrentlyValidException( "Currency not currently active: " + attachment.getJSONObject()); } }
public static void setRewardRecipientAssignment(Long id, Long recipient) { int currentHeight = Nxt.getBlockchain().getLastBlock().getHeight(); RewardRecipientAssignment assignment = getRewardRecipientAssignment(id); if (assignment == null) { assignment = new RewardRecipientAssignment( id, id, recipient, (int) (currentHeight + Constants.BURST_REWARD_RECIPIENT_ASSIGNMENT_WAIT_TIME)); } else { assignment.setRecipient( recipient, (int) (currentHeight + Constants.BURST_REWARD_RECIPIENT_ASSIGNMENT_WAIT_TIME)); } rewardRecipientAssignmentTable.insert(assignment); }
private void save(Connection con) throws SQLException { try (PreparedStatement pstmt = con.prepareStatement( "INSERT INTO asset (id, account_id, name, " + "description, quantity, decimals, height) VALUES (?, ?, ?, ?, ?, ?, ?)")) { int i = 0; pstmt.setLong(++i, this.getId()); pstmt.setLong(++i, this.getAccountId()); pstmt.setString(++i, this.getName()); pstmt.setString(++i, this.getDescription()); pstmt.setLong(++i, this.getQuantityQNT()); pstmt.setByte(++i, this.getDecimals()); pstmt.setInt(++i, Nxt.getBlockchain().getHeight()); pstmt.executeUpdate(); } }
@Override void undoAttachmentUnconfirmed(Transaction transaction, Account senderAccount) { Attachment.MonetarySystemReserveIncrease attachment = (Attachment.MonetarySystemReserveIncrease) transaction.getAttachment(); long reserveSupply; Currency currency = Currency.getCurrency(attachment.getCurrencyId()); if (currency != null) { reserveSupply = currency.getReserveSupply(); } else { // currency must have been deleted, get reserve supply from the original issuance // transaction Transaction currencyIssuance = Nxt.getBlockchain().getTransaction(attachment.getCurrencyId()); Attachment.MonetarySystemCurrencyIssuance currencyIssuanceAttachment = (Attachment.MonetarySystemCurrencyIssuance) currencyIssuance.getAttachment(); reserveSupply = currencyIssuanceAttachment.getReserveSupply(); } senderAccount.addToUnconfirmedBalanceNQT( Math.multiplyExact(reserveSupply, attachment.getAmountPerUnitNQT())); }
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(CurrencyExchangeOffer::removeOffer); }, BlockchainProcessor.Event.AFTER_BLOCK_APPLY); }
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; }
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(); } }
@Override public DbIterator<TransactionImpl> getTransactions( Account account, int numberOfConfirmations, byte type, byte subtype, int blockTimestamp, boolean withMessage, boolean phasedOnly, boolean nonPhasedOnly, int from, int to) { if (phasedOnly && nonPhasedOnly) { throw new IllegalArgumentException( "At least one of phasedOnly or nonPhasedOnly must be false"); } int height = numberOfConfirmations > 0 ? getHeight() - numberOfConfirmations : Integer.MAX_VALUE; if (height < 0) { throw new IllegalArgumentException( "Number of confirmations required " + numberOfConfirmations + " exceeds current blockchain height " + getHeight()); } Connection con = null; try { StringBuilder buf = new StringBuilder(); buf.append("SELECT * FROM transaction WHERE recipient_id = ? AND sender_id <> ? "); if (blockTimestamp > 0) { buf.append("AND block_timestamp >= ? "); } if (type >= 0) { buf.append("AND type = ? "); if (subtype >= 0) { buf.append("AND subtype = ? "); } } if (height < Integer.MAX_VALUE) { buf.append("AND height <= ? "); } if (withMessage) { buf.append("AND (has_message = TRUE OR has_encrypted_message = TRUE "); buf.append( "OR ((has_prunable_message = TRUE OR has_prunable_encrypted_message = TRUE) AND timestamp > ?)) "); } if (phasedOnly) { buf.append("AND phased = TRUE "); } else if (nonPhasedOnly) { buf.append("AND phased = FALSE "); } buf.append("UNION ALL SELECT * FROM transaction WHERE sender_id = ? "); if (blockTimestamp > 0) { buf.append("AND block_timestamp >= ? "); } if (type >= 0) { buf.append("AND type = ? "); if (subtype >= 0) { buf.append("AND subtype = ? "); } } if (height < Integer.MAX_VALUE) { buf.append("AND height <= ? "); } if (withMessage) { buf.append( "AND (has_message = TRUE OR has_encrypted_message = TRUE OR has_encrypttoself_message = TRUE "); buf.append( "OR ((has_prunable_message = TRUE OR has_prunable_encrypted_message = TRUE) AND timestamp > ?)) "); } if (phasedOnly) { buf.append("AND phased = TRUE "); } else if (nonPhasedOnly) { buf.append("AND phased = FALSE "); } buf.append("ORDER BY block_timestamp DESC, transaction_index DESC"); buf.append(DbUtils.limitsClause(from, to)); con = Db.db.getConnection(); PreparedStatement pstmt; int i = 0; pstmt = con.prepareStatement(buf.toString()); pstmt.setLong(++i, account.getId()); pstmt.setLong(++i, account.getId()); if (blockTimestamp > 0) { pstmt.setInt(++i, blockTimestamp); } if (type >= 0) { pstmt.setByte(++i, type); if (subtype >= 0) { pstmt.setByte(++i, subtype); } } if (height < Integer.MAX_VALUE) { pstmt.setInt(++i, height); } int prunableExpiration = Constants.INCLUDE_EXPIRED_PRUNABLE ? 0 : Nxt.getEpochTime() - Constants.MAX_PRUNABLE_LIFETIME; if (withMessage) { pstmt.setInt(++i, prunableExpiration); } pstmt.setLong(++i, account.getId()); if (blockTimestamp > 0) { pstmt.setInt(++i, blockTimestamp); } if (type >= 0) { pstmt.setByte(++i, type); if (subtype >= 0) { pstmt.setByte(++i, subtype); } } if (height < Integer.MAX_VALUE) { pstmt.setInt(++i, height); } if (withMessage) { pstmt.setInt(++i, prunableExpiration); } DbUtils.setLimits(++i, pstmt, from, to); return getTransactions(con, pstmt); } catch (SQLException e) { DbUtils.close(con); throw new RuntimeException(e.toString(), e); } }
@Override public Transaction parseTransaction(byte[] bytes) throws NxtException.ValidationException { try { boolean useNQT = Nxt.getBlockchain().getLastBlock().getHeight() >= Constants.NQT_BLOCK; ByteBuffer buffer = ByteBuffer.wrap(bytes); buffer.order(ByteOrder.LITTLE_ENDIAN); byte type = buffer.get(); byte subtype = buffer.get(); int timestamp = buffer.getInt(); short deadline = buffer.getShort(); byte[] senderPublicKey = new byte[32]; buffer.get(senderPublicKey); Long recipientId = buffer.getLong(); long amountNQT; long feeNQT; String referencedTransactionFullHash = null; Long referencedTransactionId = null; if (!useNQT) { amountNQT = ((long) buffer.getInt()) * Constants.ONE_NXT; feeNQT = ((long) buffer.getInt()) * Constants.ONE_NXT; referencedTransactionId = Convert.zeroToNull(buffer.getLong()); } else { amountNQT = buffer.getLong(); feeNQT = buffer.getLong(); byte[] referencedTransactionFullHashBytes = new byte[32]; buffer.get(referencedTransactionFullHashBytes); if (Convert.emptyToNull(referencedTransactionFullHashBytes) != null) { referencedTransactionFullHash = Convert.toHexString(referencedTransactionFullHashBytes); referencedTransactionId = Convert.fullHashToId(referencedTransactionFullHash); } } byte[] signature = new byte[64]; buffer.get(signature); signature = Convert.emptyToNull(signature); TransactionType transactionType = TransactionType.findTransactionType(type, subtype); TransactionImpl transaction; if (!useNQT) { transaction = new TransactionImpl( transactionType, timestamp, deadline, senderPublicKey, recipientId, amountNQT, feeNQT, referencedTransactionId, signature); } else { transaction = new TransactionImpl( transactionType, timestamp, deadline, senderPublicKey, recipientId, amountNQT, feeNQT, referencedTransactionFullHash, signature); } transactionType.loadAttachment(transaction, buffer); return transaction; } catch (RuntimeException e) { throw new NxtException.ValidationException(e.toString(), e); } }
@Override boolean isBlockDuplicate( Transaction transaction, Map<TransactionType, Map<String, Integer>> duplicates) { return Nxt.getBlockchain().getHeight() > Constants.SHUFFLING_BLOCK && isDuplicate(CURRENCY_ISSUANCE, getName(), duplicates, true); }
public DbIterator<Account> getLessors() { return accountTable.getManyBy(getLessorsClause(Nxt.getBlockchain().getHeight()), 0, -1); }
public long getGuaranteedBalanceNQT(final int numberOfConfirmations) { return getGuaranteedBalanceNQT(numberOfConfirmations, Nxt.getBlockchain().getHeight()); }
public final boolean isFinished() { return finishHeight <= Nxt.getBlockchain().getHeight(); }
/** Maintain a ledger of changes to selected accounts */ public class AccountLedger { /** Account ledger is enabled */ private static final boolean ledgerEnabled; /** Track all accounts */ private static final boolean trackAllAccounts; /** Accounts to track */ private static final SortedSet<Long> trackAccounts = new TreeSet<>(); /** Unconfirmed logging */ private static final int logUnconfirmed; /** Number of blocks to keep when trimming */ public static final int trimKeep = Nxt.getIntProperty("nxt.ledgerTrimKeep", 30000); /** Blockchain */ private static final Blockchain blockchain = Nxt.getBlockchain(); /** Blockchain processor */ private static final BlockchainProcessor blockchainProcessor = Nxt.getBlockchainProcessor(); /** Pending ledger entries */ private static final List<LedgerEntry> pendingEntries = new ArrayList<>(); /** Process nxt.ledgerAccounts */ static { List<String> ledgerAccounts = Nxt.getStringListProperty("nxt.ledgerAccounts"); ledgerEnabled = !ledgerAccounts.isEmpty(); trackAllAccounts = ledgerAccounts.contains("*"); if (ledgerEnabled) { if (trackAllAccounts) { Logger.logInfoMessage("Account ledger is tracking all accounts"); } else { for (String account : ledgerAccounts) { try { trackAccounts.add(Convert.parseAccountId(account)); Logger.logInfoMessage("Account ledger is tracking account " + account); } catch (RuntimeException e) { Logger.logErrorMessage("Account " + account + " is not valid; ignored"); } } } } else { Logger.logInfoMessage("Account ledger is not enabled"); } int temp = Nxt.getIntProperty("nxt.ledgerLogUnconfirmed", 1); logUnconfirmed = (temp >= 0 && temp <= 2 ? temp : 1); } /** Account ledger table */ private static class AccountLedgerTable extends DerivedDbTable { /** Create the account ledger table */ public AccountLedgerTable() { super("account_ledger"); } /** * Insert an entry into the table * * @param ledgerEntry Ledger entry */ public void insert(LedgerEntry ledgerEntry) { try (Connection con = db.getConnection()) { ledgerEntry.save(con); } catch (SQLException e) { throw new RuntimeException(e.toString(), e); } } /** * Trim the account ledger table * * @param height Trim height */ @Override public void trim(int height) { if (trimKeep <= 0) return; try (Connection con = db.getConnection(); PreparedStatement pstmt = con.prepareStatement("DELETE FROM account_ledger WHERE height <= ?")) { int trimHeight = Math.max(blockchain.getHeight() - trimKeep, 0); pstmt.setInt(1, trimHeight); pstmt.executeUpdate(); } catch (SQLException e) { throw new RuntimeException(e.toString(), e); } } } private static final AccountLedgerTable accountLedgerTable = new AccountLedgerTable(); /** * Initialization * * <p>We don't do anything but we need to be called from Nxt.init() in order to register our table */ static void init() {} /** Account ledger listener events */ public enum Event { ADD_ENTRY } /** Account ledger listeners */ private static final Listeners<LedgerEntry, Event> listeners = new Listeners<>(); /** * Add a listener * * @param listener Listener * @param eventType Event to listen for * @return True if the listener was added */ public static boolean addListener(Listener<LedgerEntry> listener, Event eventType) { return listeners.addListener(listener, eventType); } /** * Remove a listener * * @param listener Listener * @param eventType Event to listen for * @return True if the listener was removed */ public static boolean removeListener(Listener<LedgerEntry> listener, Event eventType) { return listeners.removeListener(listener, eventType); } static boolean mustLogEntry(long accountId, boolean isUnconfirmed) { // // Must be tracking this account // if (!ledgerEnabled || (!trackAllAccounts && !trackAccounts.contains(accountId))) { return false; } // confirmed changes only occur while processing block, and unconfirmed changes are // only logged while processing block if (!blockchainProcessor.isProcessingBlock()) { return false; } // // Log unconfirmed changes only when processing a block and logUnconfirmed does not equal 0 // Log confirmed changes unless logUnconfirmed equals 2 // if (isUnconfirmed && logUnconfirmed == 0) { return false; } if (!isUnconfirmed && logUnconfirmed == 2) { return false; } if (trimKeep > 0 && blockchain.getHeight() <= Constants.LAST_KNOWN_BLOCK - trimKeep) { return false; } // // Don't log account changes if we are scanning the blockchain and the current height // is less than the minimum account_ledger trim height // if (blockchainProcessor.isScanning() && trimKeep > 0 && blockchain.getHeight() <= blockchainProcessor.getInitialScanHeight() - trimKeep) { return false; } return true; } /** * Log an event in the account_ledger table * * @param ledgerEntry Ledger entry */ static void logEntry(LedgerEntry ledgerEntry) { // // Must be in a database transaction // if (!Db.db.isInTransaction()) { throw new IllegalStateException("Not in transaction"); } // // Combine multiple ledger entries // int index = pendingEntries.indexOf(ledgerEntry); if (index >= 0) { LedgerEntry existingEntry = pendingEntries.remove(index); ledgerEntry.updateChange(existingEntry.getChange()); long adjustedBalance = existingEntry.getBalance() - existingEntry.getChange(); for (; index < pendingEntries.size(); index++) { existingEntry = pendingEntries.get(index); if (existingEntry.getAccountId() == ledgerEntry.getAccountId() && existingEntry.getHolding() == ledgerEntry.getHolding() && ((existingEntry.getHoldingId() == null && ledgerEntry.getHoldingId() == null) || (existingEntry.getHoldingId() != null && existingEntry.getHoldingId().equals(ledgerEntry.getHoldingId())))) { adjustedBalance += existingEntry.getChange(); existingEntry.setBalance(adjustedBalance); } } } pendingEntries.add(ledgerEntry); } /** Commit pending ledger entries */ static void commitEntries() { for (LedgerEntry ledgerEntry : pendingEntries) { accountLedgerTable.insert(ledgerEntry); listeners.notify(ledgerEntry, Event.ADD_ENTRY); } pendingEntries.clear(); } /** Clear pending ledger entries */ static void clearEntries() { pendingEntries.clear(); } /** * Return a single entry identified by the ledger entry identifier * * @param ledgerId Ledger entry identifier * @return Ledger entry or null if entry not found */ public static LedgerEntry getEntry(long ledgerId) { if (!ledgerEnabled) return null; LedgerEntry entry; try (Connection con = Db.db.getConnection(); PreparedStatement stmt = con.prepareStatement("SELECT * FROM account_ledger WHERE db_id = ?")) { stmt.setLong(1, ledgerId); try (ResultSet rs = stmt.executeQuery()) { if (rs.next()) entry = new LedgerEntry(rs); else entry = null; } } catch (SQLException e) { throw new RuntimeException(e.toString(), e); } return entry; } /** * Return the ledger entries sorted in descending insert order * * @param accountId Account identifier or zero if no account identifier * @param event Ledger event or null * @param eventId Ledger event identifier or zero if no event identifier * @param holding Ledger holding or null * @param holdingId Ledger holding identifier or zero if no holding identifier * @param firstIndex First matching entry index, inclusive * @param lastIndex Last matching entry index, inclusive * @return List of ledger entries */ public static List<LedgerEntry> getEntries( long accountId, LedgerEvent event, long eventId, LedgerHolding holding, long holdingId, int firstIndex, int lastIndex) { if (!ledgerEnabled) { return Collections.emptyList(); } List<LedgerEntry> entryList = new ArrayList<>(); // // Build the SELECT statement to search the entries StringBuilder sb = new StringBuilder(128); sb.append("SELECT * FROM account_ledger "); if (accountId != 0 || event != null || holding != null) { sb.append("WHERE "); } if (accountId != 0) { sb.append("account_id = ? "); } if (event != null) { if (accountId != 0) { sb.append("AND "); } sb.append("event_type = ? "); if (eventId != 0) sb.append("AND event_id = ? "); } if (holding != null) { if (accountId != 0 || event != null) { sb.append("AND "); } sb.append("holding_type = ? "); if (holdingId != 0) sb.append("AND holding_id = ? "); } sb.append("ORDER BY db_id DESC "); sb.append(DbUtils.limitsClause(firstIndex, lastIndex)); // // Get the ledger entries // blockchain.readLock(); try (Connection con = Db.db.getConnection(); PreparedStatement pstmt = con.prepareStatement(sb.toString())) { int i = 0; if (accountId != 0) { pstmt.setLong(++i, accountId); } if (event != null) { pstmt.setByte(++i, (byte) event.getCode()); if (eventId != 0) { pstmt.setLong(++i, eventId); } } if (holding != null) { pstmt.setByte(++i, (byte) holding.getCode()); if (holdingId != 0) { pstmt.setLong(++i, holdingId); } } DbUtils.setLimits(++i, pstmt, firstIndex, lastIndex); try (ResultSet rs = pstmt.executeQuery()) { while (rs.next()) { entryList.add(new LedgerEntry(rs)); } } } catch (SQLException e) { throw new RuntimeException(e.toString(), e); } finally { blockchain.readUnlock(); } return entryList; } /** * Ledger events * * <p>There must be a ledger event defined for each transaction (type,subtype) pair. When adding a * new event, do not change the existing code assignments since these codes are stored in the * event_type field of the account_ledger table. */ public enum LedgerEvent { // Block and Transaction BLOCK_GENERATED(1, false), REJECT_PHASED_TRANSACTION(2, true), TRANSACTION_FEE(50, true), // TYPE_PAYMENT ORDINARY_PAYMENT(3, true), // TYPE_MESSAGING ACCOUNT_INFO(4, true), ALIAS_ASSIGNMENT(5, true), ALIAS_BUY(6, true), ALIAS_DELETE(7, true), ALIAS_SELL(8, true), ARBITRARY_MESSAGE(9, true), HUB_ANNOUNCEMENT(10, true), PHASING_VOTE_CASTING(11, true), POLL_CREATION(12, true), VOTE_CASTING(13, true), ACCOUNT_PROPERTY(56, true), ACCOUNT_PROPERTY_DELETE(57, true), // TYPE_COLORED_COINS ASSET_ASK_ORDER_CANCELLATION(14, true), ASSET_ASK_ORDER_PLACEMENT(15, true), ASSET_BID_ORDER_CANCELLATION(16, true), ASSET_BID_ORDER_PLACEMENT(17, true), ASSET_DIVIDEND_PAYMENT(18, true), ASSET_ISSUANCE(19, true), ASSET_TRADE(20, true), ASSET_TRANSFER(21, true), ASSET_DELETE(49, true), // TYPE_DIGITAL_GOODS DIGITAL_GOODS_DELISTED(22, true), DIGITAL_GOODS_DELISTING(23, true), DIGITAL_GOODS_DELIVERY(24, true), DIGITAL_GOODS_FEEDBACK(25, true), DIGITAL_GOODS_LISTING(26, true), DIGITAL_GOODS_PRICE_CHANGE(27, true), DIGITAL_GOODS_PURCHASE(28, true), DIGITAL_GOODS_PURCHASE_EXPIRED(29, true), DIGITAL_GOODS_QUANTITY_CHANGE(30, true), DIGITAL_GOODS_REFUND(31, true), // TYPE_ACCOUNT_CONTROL ACCOUNT_CONTROL_EFFECTIVE_BALANCE_LEASING(32, true), ACCOUNT_CONTROL_PHASING_ONLY(55, true), // TYPE_CURRENCY CURRENCY_DELETION(33, true), CURRENCY_DISTRIBUTION(34, true), CURRENCY_EXCHANGE(35, true), CURRENCY_EXCHANGE_BUY(36, true), CURRENCY_EXCHANGE_SELL(37, true), CURRENCY_ISSUANCE(38, true), CURRENCY_MINTING(39, true), CURRENCY_OFFER_EXPIRED(40, true), CURRENCY_OFFER_REPLACED(41, true), CURRENCY_PUBLISH_EXCHANGE_OFFER(42, true), CURRENCY_RESERVE_CLAIM(43, true), CURRENCY_RESERVE_INCREASE(44, true), CURRENCY_TRANSFER(45, true), CURRENCY_UNDO_CROWDFUNDING(46, true), // TYPE_DATA TAGGED_DATA_UPLOAD(47, true), TAGGED_DATA_EXTEND(48, true), // TYPE_SHUFFLING SHUFFLING_REGISTRATION(51, true), SHUFFLING_PROCESSING(52, true), SHUFFLING_CANCELLATION(53, true), SHUFFLING_DISTRIBUTION(54, true); /** Event code mapping */ private static final Map<Integer, LedgerEvent> eventMap = new HashMap<>(); static { for (LedgerEvent event : values()) { if (eventMap.put(event.code, event) != null) { throw new RuntimeException("LedgerEvent code " + event.code + " reused"); } } } /** Event code */ private final int code; /** Event identifier is a transaction */ private final boolean isTransaction; /** * Create the ledger event * * @param code Event code * @param isTransaction Event identifier is a transaction */ LedgerEvent(int code, boolean isTransaction) { this.code = code; this.isTransaction = isTransaction; } /** * Check if the event identifier is a transaction * * @return TRUE if the event identifier is a transaction */ public boolean isTransaction() { return isTransaction; } /** * Return the event code * * @return Event code */ public int getCode() { return code; } /** * Get the event from the event code * * @param code Event code * @return Event */ public static LedgerEvent fromCode(int code) { LedgerEvent event = eventMap.get(code); if (event == null) { throw new IllegalArgumentException("LedgerEvent code " + code + " is unknown"); } return event; } } /** * Ledger holdings * * <p>When adding a new holding, do not change the existing code assignments since they are stored * in the holding_type field of the account_ledger table. */ public enum LedgerHolding { UNCONFIRMED_NXT_BALANCE(1, true), NXT_BALANCE(2, false), UNCONFIRMED_ASSET_BALANCE(3, true), ASSET_BALANCE(4, false), UNCONFIRMED_CURRENCY_BALANCE(5, true), CURRENCY_BALANCE(6, false); /** Holding code mapping */ private static final Map<Integer, LedgerHolding> holdingMap = new HashMap<>(); static { for (LedgerHolding holding : values()) { if (holdingMap.put(holding.code, holding) != null) { throw new RuntimeException("LedgerHolding code " + holding.code + " reused"); } } } /** Holding code */ private final int code; /** Unconfirmed holding */ private final boolean isUnconfirmed; /** * Create the holding event * * @param code Holding code * @param isUnconfirmed TRUE if the holding is unconfirmed */ LedgerHolding(int code, boolean isUnconfirmed) { this.code = code; this.isUnconfirmed = isUnconfirmed; } /** * Check if the holding is unconfirmed * * @return TRUE if the holding is unconfirmed */ public boolean isUnconfirmed() { return this.isUnconfirmed; } /** * Return the holding code * * @return Holding code */ public int getCode() { return code; } /** * Get the holding from the holding code * * @param code Holding code * @return Holding */ public static LedgerHolding fromCode(int code) { LedgerHolding holding = holdingMap.get(code); if (holding == null) { throw new IllegalArgumentException("LedgerHolding code " + code + " is unknown"); } return holding; } } /** Ledger entry */ public static class LedgerEntry { /** Ledger identifier */ private long ledgerId = -1; /** Ledger event */ private final LedgerEvent event; /** Associated event identifier */ private final long eventId; /** Account identifier */ private final long accountId; /** Holding */ private final LedgerHolding holding; /** Holding identifier */ private final Long holdingId; /** Change in balance */ private long change; /** New balance */ private long balance; /** Block identifier */ private final long blockId; /** Blockchain height */ private final int height; /** Block timestamp */ private final int timestamp; /** * Create a ledger entry * * @param event Event * @param eventId Event identifier * @param accountId Account identifier * @param holding Holding or null * @param holdingId Holding identifier or null * @param change Change in balance * @param balance New balance */ public LedgerEntry( LedgerEvent event, long eventId, long accountId, LedgerHolding holding, Long holdingId, long change, long balance) { this.event = event; this.eventId = eventId; this.accountId = accountId; this.holding = holding; this.holdingId = holdingId; this.change = change; this.balance = balance; Block block = blockchain.getLastBlock(); this.blockId = block.getId(); this.height = block.getHeight(); this.timestamp = block.getTimestamp(); } /** * Create a ledger entry * * @param event Event * @param eventId Event identifier * @param accountId Account identifier * @param change Change in balance * @param balance New balance */ public LedgerEntry(LedgerEvent event, long eventId, long accountId, long change, long balance) { this(event, eventId, accountId, null, null, change, balance); } /** * Create a ledger entry from a database entry * * @param rs Result set * @throws SQLException Database error occurred */ private LedgerEntry(ResultSet rs) throws SQLException { ledgerId = rs.getLong("db_id"); event = LedgerEvent.fromCode(rs.getByte("event_type")); eventId = rs.getLong("event_id"); accountId = rs.getLong("account_id"); int holdingType = rs.getByte("holding_type"); if (holdingType >= 0) { holding = LedgerHolding.fromCode(holdingType); } else { holding = null; } long id = rs.getLong("holding_id"); if (rs.wasNull()) { holdingId = null; } else { holdingId = id; } change = rs.getLong("change"); balance = rs.getLong("balance"); blockId = rs.getLong("block_id"); height = rs.getInt("height"); timestamp = rs.getInt("timestamp"); } /** * Return the ledger identifier * * @return Ledger identifier or -1 if not set */ public long getLedgerId() { return ledgerId; } /** * Return the ledger event * * @return Ledger event */ public LedgerEvent getEvent() { return event; } /** * Return the associated event identifier * * @return Event identifier */ public long getEventId() { return eventId; } /** * Return the account identifier * * @return Account identifier */ public long getAccountId() { return accountId; } /** * Return the holding * * @return Holding or null if there is no holding */ public LedgerHolding getHolding() { return holding; } /** * Return the holding identifier * * @return Holding identifier or null if there is no holding identifier */ public Long getHoldingId() { return holdingId; } /** * Update the balance change * * @param amount Change amount */ private void updateChange(long amount) { change += amount; } /** * Return the balance change * * @return Balance changes */ public long getChange() { return change; } /** * Set the new balance * * @param balance New balance */ private void setBalance(long balance) { this.balance = balance; } /** * Return the new balance * * @return New balance */ public long getBalance() { return balance; } /** * Return the block identifier * * @return Block identifier */ public long getBlockId() { return blockId; } /** * Return the height * * @return Height */ public int getHeight() { return height; } /** * Return the timestamp * * @return Timestamp */ public int getTimestamp() { return timestamp; } /** * Return the hash code * * @return Hash code */ @Override public int hashCode() { return (Long.hashCode(accountId) ^ event.getCode() ^ Long.hashCode(eventId) ^ (holding != null ? holding.getCode() : 0) ^ (holdingId != null ? Long.hashCode(holdingId) : 0)); } /** * Check if two ledger events are equal * * @param obj Ledger event to check * @return TRUE if the ledger events are the same */ @Override public boolean equals(Object obj) { return (obj != null && (obj instanceof LedgerEntry) && accountId == ((LedgerEntry) obj).accountId && event == ((LedgerEntry) obj).event && eventId == ((LedgerEntry) obj).eventId && holding == ((LedgerEntry) obj).holding && (holdingId != null ? holdingId.equals(((LedgerEntry) obj).holdingId) : ((LedgerEntry) obj).holdingId == null)); } /** * Save the ledger entry * * @param con Database connection * @throws SQLException Database error occurred */ private void save(Connection con) throws SQLException { try (PreparedStatement stmt = con.prepareStatement( "INSERT INTO account_ledger " + "(account_id, event_type, event_id, holding_type, holding_id, change, balance, " + "block_id, height, timestamp) " + "VALUES(?, ?, ?, ?, ?, ?, ?, ?, ?, ?)", Statement.RETURN_GENERATED_KEYS)) { int i = 0; stmt.setLong(++i, accountId); stmt.setByte(++i, (byte) event.getCode()); stmt.setLong(++i, eventId); if (holding != null) { stmt.setByte(++i, (byte) holding.getCode()); } else { stmt.setByte(++i, (byte) -1); } DbUtils.setLong(stmt, ++i, holdingId); stmt.setLong(++i, change); stmt.setLong(++i, balance); stmt.setLong(++i, blockId); stmt.setInt(++i, height); stmt.setInt(++i, timestamp); stmt.executeUpdate(); try (ResultSet rs = stmt.getGeneratedKeys()) { if (rs.next()) { ledgerId = rs.getLong(1); } } } } } }