/******************************************************************************
* 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.LedgerEntry;
import nxt.AccountLedger.LedgerEvent;
import nxt.AccountLedger.LedgerHolding;
import nxt.crypto.Crypto;
import nxt.crypto.EncryptedData;
import nxt.db.DbClause;
import nxt.db.DbIterator;
import nxt.db.DbKey;
import nxt.db.DbUtils;
import nxt.db.DerivedDbTable;
import nxt.db.VersionedEntityDbTable;
import nxt.db.VersionedPersistentDbTable;
import nxt.util.Convert;
import nxt.util.Listener;
import nxt.util.Listeners;
import nxt.util.Logger;
import java.sql.Connection;
import java.sql.PreparedStatement;
import java.sql.ResultSet;
import java.sql.SQLException;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collections;
import java.util.EnumSet;
import java.util.List;
import java.util.Set;
@SuppressWarnings({"UnusedDeclaration", "SuspiciousNameCombination"})
public final class Account {
public enum Event {
BALANCE, UNCONFIRMED_BALANCE, ASSET_BALANCE, UNCONFIRMED_ASSET_BALANCE, CURRENCY_BALANCE, UNCONFIRMED_CURRENCY_BALANCE,
LEASE_SCHEDULED, LEASE_STARTED, LEASE_ENDED
}
public enum ControlType {
PHASING_ONLY
}
public static final class AccountAsset {
private final long accountId;
private final long assetId;
private final DbKey dbKey;
private long quantityQNT;
private long unconfirmedQuantityQNT;
private AccountAsset(long accountId, long assetId, long quantityQNT, long unconfirmedQuantityQNT) {
this.accountId = accountId;
this.assetId = assetId;
this.dbKey = accountAssetDbKeyFactory.newKey(this.accountId, this.assetId);
this.quantityQNT = quantityQNT;
this.unconfirmedQuantityQNT = unconfirmedQuantityQNT;
}
private AccountAsset(ResultSet rs) throws SQLException {
this.accountId = rs.getLong("account_id");
this.assetId = rs.getLong("asset_id");
this.dbKey = accountAssetDbKeyFactory.newKey(this.accountId, this.assetId);
this.quantityQNT = rs.getLong("quantity");
this.unconfirmedQuantityQNT = rs.getLong("unconfirmed_quantity");
}
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();
}
}
public long getAccountId() {
return accountId;
}
public long getAssetId() {
return assetId;
}
public long getQuantityQNT() {
return quantityQNT;
}
public long getUnconfirmedQuantityQNT() {
return unconfirmedQuantityQNT;
}
private void save() {
checkBalance(this.accountId, this.quantityQNT, this.unconfirmedQuantityQNT);
if (this.quantityQNT > 0 || this.unconfirmedQuantityQNT > 0) {
accountAssetTable.insert(this);
} else {
accountAssetTable.delete(this);
}
}
@Override
public String toString() {
return "AccountAsset account_id: " + Long.toUnsignedString(accountId) + " asset_id: " + Long.toUnsignedString(assetId)
+ " quantity: " + quantityQNT + " unconfirmedQuantity: " + unconfirmedQuantityQNT;
}
}
@SuppressWarnings("UnusedDeclaration")
public static final class AccountCurrency {
private final long accountId;
private final long currencyId;
private final DbKey dbKey;
private long units;
private long unconfirmedUnits;
private AccountCurrency(long accountId, long currencyId, long quantityQNT, long unconfirmedQuantityQNT) {
this.accountId = accountId;
this.currencyId = currencyId;
this.dbKey = accountCurrencyDbKeyFactory.newKey(this.accountId, this.currencyId);
this.units = quantityQNT;
this.unconfirmedUnits = unconfirmedQuantityQNT;
}
private AccountCurrency(ResultSet rs) throws SQLException {
this.accountId = rs.getLong("account_id");
this.currencyId = rs.getLong("currency_id");
this.dbKey = accountAssetDbKeyFactory.newKey(this.accountId, this.currencyId);
this.units = rs.getLong("units");
this.unconfirmedUnits = rs.getLong("unconfirmed_units");
}
private void save(Connection con) throws SQLException {
try (PreparedStatement pstmt = con.prepareStatement("MERGE INTO account_currency "
+ "(account_id, currency_id, units, unconfirmed_units, height, latest) "
+ "KEY (account_id, currency_id, height) VALUES (?, ?, ?, ?, ?, TRUE)")) {
int i = 0;
pstmt.setLong(++i, this.accountId);
pstmt.setLong(++i, this.currencyId);
pstmt.setLong(++i, this.units);
pstmt.setLong(++i, this.unconfirmedUnits);
pstmt.setInt(++i, Nxt.getBlockchain().getHeight());
pstmt.executeUpdate();
}
}
public long getAccountId() {
return accountId;
}
public long getCurrencyId() {
return currencyId;
}
public long getUnits() {
return units;
}
public long getUnconfirmedUnits() {
return unconfirmedUnits;
}
private void save() {
checkBalance(this.accountId, this.units, this.unconfirmedUnits);
if (this.units > 0 || this.unconfirmedUnits > 0) {
accountCurrencyTable.insert(this);
} else if (this.units == 0 && this.unconfirmedUnits == 0) {
accountCurrencyTable.delete(this);
}
}
@Override
public String toString() {
return "AccountCurrency account_id: " + Long.toUnsignedString(accountId) + " currency_id: " + Long.toUnsignedString(currencyId)
+ " quantity: " + units + " unconfirmedQuantity: " + unconfirmedUnits;
}
}
public static final class AccountLease {
private final long lessorId;
private final DbKey dbKey;
private long currentLesseeId;
private int currentLeasingHeightFrom;
private int currentLeasingHeightTo;
private long nextLesseeId;
private int nextLeasingHeightFrom;
private int nextLeasingHeightTo;
private AccountLease(long lessorId,
int currentLeasingHeightFrom, int currentLeasingHeightTo, long currentLesseeId) {
this.lessorId = lessorId;
this.dbKey = accountLeaseDbKeyFactory.newKey(this.lessorId);
this.currentLeasingHeightFrom = currentLeasingHeightFrom;
this.currentLeasingHeightTo = currentLeasingHeightTo;
this.currentLesseeId = currentLesseeId;
}
private AccountLease(ResultSet rs) throws SQLException {
this.lessorId = rs.getLong("lessor_id");
this.dbKey = accountLeaseDbKeyFactory.newKey(this.lessorId);
this.currentLeasingHeightFrom = rs.getInt("current_leasing_height_from");
this.currentLeasingHeightTo = rs.getInt("current_leasing_height_to");
this.currentLesseeId = rs.getLong("current_lessee_id");
this.nextLeasingHeightFrom = rs.getInt("next_leasing_height_from");
this.nextLeasingHeightTo = rs.getInt("next_leasing_height_to");
this.nextLesseeId = rs.getLong("next_lessee_id");
}
private void save(Connection con) throws SQLException {
try (PreparedStatement pstmt = con.prepareStatement("MERGE INTO account_lease "
+ "(lessor_id, 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 (lessor_id, height) VALUES (?, ?, ?, ?, ?, ?, ?, ?, TRUE)")) {
int i = 0;
pstmt.setLong(++i, this.lessorId);
DbUtils.setIntZeroToNull(pstmt, ++i, this.currentLeasingHeightFrom);
DbUtils.setIntZeroToNull(pstmt, ++i, this.currentLeasingHeightTo);
DbUtils.setLongZeroToNull(pstmt, ++i, this.currentLesseeId);
DbUtils.setIntZeroToNull(pstmt, ++i, this.nextLeasingHeightFrom);
DbUtils.setIntZeroToNull(pstmt, ++i, this.nextLeasingHeightTo);
DbUtils.setLongZeroToNull(pstmt, ++i, this.nextLesseeId);
pstmt.setInt(++i, Nxt.getBlockchain().getHeight());
pstmt.executeUpdate();
}
}
public long getLessorId() {
return lessorId;
}
public long getCurrentLesseeId() {
return currentLesseeId;
}
public int getCurrentLeasingHeightFrom() {
return currentLeasingHeightFrom;
}
public int getCurrentLeasingHeightTo() {
return currentLeasingHeightTo;
}
public long getNextLesseeId() {
return nextLesseeId;
}
public int getNextLeasingHeightFrom() {
return nextLeasingHeightFrom;
}
public int getNextLeasingHeightTo() {
return nextLeasingHeightTo;
}
}
public static final class AccountInfo {
private final long accountId;
private final DbKey dbKey;
private String name;
private String description;
private AccountInfo(long accountId, String name, String description) {
this.accountId = accountId;
this.dbKey = accountInfoDbKeyFactory.newKey(this.accountId);
this.name = name;
this.description = description;
}
private AccountInfo(ResultSet rs) throws SQLException {
this.accountId = rs.getLong("account_id");
this.dbKey = accountInfoDbKeyFactory.newKey(this.accountId);
this.name = rs.getString("name");
this.description = rs.getString("description");
}
private void save(Connection con) throws SQLException {
try (PreparedStatement pstmt = con.prepareStatement("MERGE INTO account_info "
+ "(account_id, name, description, height, latest) "
+ "KEY (account_id, height) VALUES (?, ?, ?, ?, TRUE)")) {
int i = 0;
pstmt.setLong(++i, this.accountId);
DbUtils.setString(pstmt, ++i, this.name);
DbUtils.setString(pstmt, ++i, this.description);
pstmt.setInt(++i, Nxt.getBlockchain().getHeight());
pstmt.executeUpdate();
}
}
public long getAccountId() {
return accountId;
}
public String getName() {
return name;
}
public String getDescription() {
return description;
}
private void save() {
if (this.name != null || this.description != null) {
accountInfoTable.insert(this);
} else {
accountInfoTable.delete(this);
}
}
}
public static final class AccountProperty {
private final long id;
private final DbKey dbKey;
private final long recipientId;
private final long setterId;
private String property;
private String value;
private AccountProperty(long id, long recipientId, long setterId, String property, String value) {
this.id = id;
this.dbKey = accountPropertyDbKeyFactory.newKey(this.id);
this.recipientId = recipientId;
this.setterId = setterId;
this.property = property;
this.value = value;
}
private AccountProperty(ResultSet rs) throws SQLException {
this.id = rs.getLong("id");
this.dbKey = accountPropertyDbKeyFactory.newKey(this.id);
this.recipientId = rs.getLong("recipient_id");
long setterId = rs.getLong("setter_id");
this.setterId = setterId == 0 ? recipientId : setterId;
this.property = rs.getString("property");
this.value = rs.getString("value");
}
private void save(Connection con) throws SQLException {
try (PreparedStatement pstmt = con.prepareStatement("MERGE INTO account_property "
+ "(id, recipient_id, setter_id, property, value, height, latest) "
+ "KEY (id, height) VALUES (?, ?, ?, ?, ?, ?, TRUE)")) {
int i = 0;
pstmt.setLong(++i, this.id);
pstmt.setLong(++i, this.recipientId);
DbUtils.setLongZeroToNull(pstmt, ++i, this.setterId != this.recipientId ? this.setterId : 0);
DbUtils.setString(pstmt, ++i, this.property);
DbUtils.setString(pstmt, ++i, this.value);
pstmt.setInt(++i, Nxt.getBlockchain().getHeight());
pstmt.executeUpdate();
}
}
public long getId() {
return id;
}
public long getRecipientId() {
return recipientId;
}
public long getSetterId() {
return setterId;
}
public String getProperty() {
return property;
}
public String getValue() {
return value;
}
}
public static final class PublicKey {
private final long accountId;
private final DbKey dbKey;
private byte[] publicKey;
private int height;
private PublicKey(long accountId, byte[] publicKey) {
this.accountId = accountId;
this.dbKey = publicKeyDbKeyFactory.newKey(accountId);
this.publicKey = publicKey;
this.height = Nxt.getBlockchain().getHeight();
}
private PublicKey(ResultSet rs) throws SQLException {
this.accountId = rs.getLong("account_id");
this.dbKey = publicKeyDbKeyFactory.newKey(accountId);
this.publicKey = rs.getBytes("public_key");
this.height = rs.getInt("height");
}
private void save(Connection con) throws SQLException {
height = Nxt.getBlockchain().getHeight();
try (PreparedStatement pstmt = con.prepareStatement("MERGE INTO public_key (account_id, public_key, height, latest) "
+ "KEY (account_id, height) VALUES (?, ?, ?, TRUE)")) {
int i = 0;
pstmt.setLong(++i, accountId);
DbUtils.setBytes(pstmt, ++i, publicKey);
pstmt.setInt(++i, height);
pstmt.executeUpdate();
}
}
public long getAccountId() {
return accountId;
}
public byte[] getPublicKey() {
return publicKey;
}
public int getHeight() {
return height;
}
}
static class DoubleSpendingException extends RuntimeException {
DoubleSpendingException(String message, long accountId, long confirmed, long unconfirmed) {
super(message + " account: " + Long.toUnsignedString(accountId) + " confirmed: " + confirmed + " unconfirmed: " + unconfirmed);
}
}
private static final DbKey.LongKeyFactory<Account> accountDbKeyFactory = new DbKey.LongKeyFactory<Account>("id") {
@Override
public DbKey newKey(Account account) {
return account.dbKey;
}
@Override
public Account newEntity(DbKey dbKey) {
return new Account(((DbKey.LongKey)dbKey).getId());
}
};
private static final VersionedEntityDbTable<Account> accountTable = new VersionedEntityDbTable<Account>("account", accountDbKeyFactory) {
@Override
protected Account load(Connection con, ResultSet rs) throws SQLException {
return new Account(rs);
}
@Override
protected void save(Connection con, Account account) throws SQLException {
account.save(con);
}
};
private static final DbKey.LongKeyFactory<AccountInfo> accountInfoDbKeyFactory = new DbKey.LongKeyFactory<AccountInfo>("account_id") {
@Override
public DbKey newKey(AccountInfo accountInfo) {
return accountInfo.dbKey;
}
};
private static final DbKey.LongKeyFactory<AccountLease> accountLeaseDbKeyFactory = new DbKey.LongKeyFactory<AccountLease>("lessor_id") {
@Override
public DbKey newKey(AccountLease accountLease) {
return accountLease.dbKey;
}
};
private static final VersionedEntityDbTable<AccountLease> accountLeaseTable = new VersionedEntityDbTable<AccountLease>("account_lease",
accountLeaseDbKeyFactory) {
@Override
protected AccountLease load(Connection con, ResultSet rs) throws SQLException {
return new AccountLease(rs);
}
@Override
protected void save(Connection con, AccountLease accountLease) throws SQLException {
accountLease.save(con);
}
};
private static final VersionedEntityDbTable<AccountInfo> accountInfoTable = new VersionedEntityDbTable<AccountInfo>("account_info",
accountInfoDbKeyFactory, "name,description") {
@Override
protected AccountInfo load(Connection con, ResultSet rs) throws SQLException {
return new AccountInfo(rs);
}
@Override
protected void save(Connection con, AccountInfo accountInfo) throws SQLException {
accountInfo.save(con);
}
};
private static final DbKey.LongKeyFactory<PublicKey> publicKeyDbKeyFactory = new DbKey.LongKeyFactory<PublicKey>("account_id") {
@Override
public DbKey newKey(PublicKey publicKey) {
return publicKey.dbKey;
}
@Override
public PublicKey newEntity(DbKey dbKey) {
return new PublicKey(((DbKey.LongKey)dbKey).getId(), null);
}
};
private static final VersionedPersistentDbTable<PublicKey> publicKeyTable = new VersionedPersistentDbTable<PublicKey>("public_key", publicKeyDbKeyFactory) {
@Override
protected PublicKey load(Connection con, ResultSet rs) throws SQLException {
return new PublicKey(rs);
}
@Override
protected void save(Connection con, PublicKey publicKey) throws SQLException {
publicKey.save(con);
}
};
private static final DbKey.LinkKeyFactory<AccountAsset> accountAssetDbKeyFactory = new DbKey.LinkKeyFactory<AccountAsset>("account_id", "asset_id") {
@Override
public DbKey newKey(AccountAsset accountAsset) {
return accountAsset.dbKey;
}
};
private static final VersionedEntityDbTable<AccountAsset> accountAssetTable = new VersionedEntityDbTable<AccountAsset>("account_asset", accountAssetDbKeyFactory) {
@Override
protected AccountAsset load(Connection con, ResultSet rs) throws SQLException {
return new AccountAsset(rs);
}
@Override
protected void save(Connection con, AccountAsset accountAsset) throws SQLException {
accountAsset.save(con);
}
@Override
public void trim(int height) {
super.trim(Math.max(0, height - Constants.MAX_DIVIDEND_PAYMENT_ROLLBACK));
}
@Override
public void checkAvailable(int height) {
if (height + Constants.MAX_DIVIDEND_PAYMENT_ROLLBACK < Nxt.getBlockchainProcessor().getMinRollbackHeight()) {
throw new IllegalArgumentException("Historical data as of height " + height +" not available.");
}
if (height > Nxt.getBlockchain().getHeight()) {
throw new IllegalArgumentException("Height " + height + " exceeds blockchain height " + Nxt.getBlockchain().getHeight());
}
}
@Override
protected String defaultSort() {
return " ORDER BY quantity DESC, account_id, asset_id ";
}
};
private static final DbKey.LinkKeyFactory<AccountCurrency> accountCurrencyDbKeyFactory = new DbKey.LinkKeyFactory<AccountCurrency>("account_id", "currency_id") {
@Override
public DbKey newKey(AccountCurrency accountCurrency) {
return accountCurrency.dbKey;
}
};
private static final VersionedEntityDbTable<AccountCurrency> accountCurrencyTable = new VersionedEntityDbTable<AccountCurrency>("account_currency", accountCurrencyDbKeyFactory) {
@Override
protected AccountCurrency load(Connection con, ResultSet rs) throws SQLException {
return new AccountCurrency(rs);
}
@Override
protected void save(Connection con, AccountCurrency accountCurrency) throws SQLException {
accountCurrency.save(con);
}
@Override
protected String defaultSort() {
return " ORDER BY units DESC, account_id, currency_id ";
}
};
private static final DerivedDbTable accountGuaranteedBalanceTable = new DerivedDbTable("account_guaranteed_balance") {
@Override
public void trim(int height) {
try (Connection con = Db.db.getConnection();
PreparedStatement pstmtDelete = con.prepareStatement("DELETE FROM account_guaranteed_balance "
+ "WHERE height < ? AND height >= 0")) {
pstmtDelete.setInt(1, height - Constants.GUARANTEED_BALANCE_CONFIRMATIONS);
pstmtDelete.executeUpdate();
} catch (SQLException e) {
throw new RuntimeException(e.toString(), e);
}
}
};
private static final DbKey.LongKeyFactory<AccountProperty> accountPropertyDbKeyFactory = new DbKey.LongKeyFactory<AccountProperty>("id") {
@Override
public DbKey newKey(AccountProperty accountProperty) {
return accountProperty.dbKey;
}
};
private static final VersionedEntityDbTable<AccountProperty> accountPropertyTable = new VersionedEntityDbTable<AccountProperty>("account_property", accountPropertyDbKeyFactory) {
@Override
protected AccountProperty load(Connection con, ResultSet rs) throws SQLException {
return new AccountProperty(rs);
}
@Override
protected void save(Connection con, AccountProperty accountProperty) throws SQLException {
accountProperty.save(con);
}
};
private static final Listeners<Account,Event> listeners = new Listeners<>();
private static final Listeners<AccountAsset,Event> assetListeners = new Listeners<>();
private static final Listeners<AccountCurrency,Event> currencyListeners = new Listeners<>();
private static final Listeners<AccountLease,Event> leaseListeners = new Listeners<>();
public static boolean addListener(Listener<Account> listener, Event eventType) {
return listeners.addListener(listener, eventType);
}
public static boolean removeListener(Listener<Account> listener, Event eventType) {
return listeners.removeListener(listener, eventType);
}
public static boolean addAssetListener(Listener<AccountAsset> listener, Event eventType) {
return assetListeners.addListener(listener, eventType);
}
public static boolean removeAssetListener(Listener<AccountAsset> listener, Event eventType) {
return assetListeners.removeListener(listener, eventType);
}
public static boolean addCurrencyListener(Listener<AccountCurrency> listener, Event eventType) {
return currencyListeners.addListener(listener, eventType);
}
public static boolean removeCurrencyListener(Listener<AccountCurrency> listener, Event eventType) {
return currencyListeners.removeListener(listener, eventType);
}
public static boolean addLeaseListener(Listener<AccountLease> listener, Event eventType) {
return leaseListeners.addListener(listener, eventType);
}
public static boolean removeLeaseListener(Listener<AccountLease> listener, Event eventType) {
return leaseListeners.removeListener(listener, eventType);
}
public static int getCount() {
return publicKeyTable.getCount();
}
public static int getAssetAccountCount(long assetId) {
return accountAssetTable.getCount(new DbClause.LongClause("asset_id", assetId));
}
public static int getAssetAccountCount(long assetId, int height) {
return accountAssetTable.getCount(new DbClause.LongClause("asset_id", assetId), height);
}
public static int getAccountAssetCount(long accountId) {
return accountAssetTable.getCount(new DbClause.LongClause("account_id", accountId));
}
public static int getAccountAssetCount(long accountId, int height) {
return accountAssetTable.getCount(new DbClause.LongClause("account_id", accountId), height);
}
public static int getCurrencyAccountCount(long currencyId) {
return accountCurrencyTable.getCount(new DbClause.LongClause("currency_id", currencyId));
}
public static int getCurrencyAccountCount(long currencyId, int height) {
return accountCurrencyTable.getCount(new DbClause.LongClause("currency_id", currencyId), height);
}
public static int getAccountCurrencyCount(long accountId) {
return accountCurrencyTable.getCount(new DbClause.LongClause("account_id", accountId));
}
public static int getAccountCurrencyCount(long accountId, int height) {
return accountCurrencyTable.getCount(new DbClause.LongClause("account_id", accountId), height);
}
public static int getAccountLeaseCount() {
return accountLeaseTable.getCount();
}
public static int getActiveLeaseCount() {
return accountTable.getCount(new DbClause.NotNullClause("active_lessee_id"));
}
public static AccountProperty getProperty(long propertyId) {
return accountPropertyTable.get(accountPropertyDbKeyFactory.newKey(propertyId));
}
public static DbIterator<AccountProperty> getProperties(long recipientId, long setterId, String property, int from, int to) {
if (recipientId == 0 && setterId == 0) {
throw new IllegalArgumentException("At least one of recipientId and setterId must be specified");
}
DbClause dbClause = null;
if (setterId == recipientId) {
dbClause = new DbClause.NullClause("setter_id");
} else if (setterId != 0) {
dbClause = new DbClause.LongClause("setter_id", setterId);
}
if (recipientId != 0) {
if (dbClause != null) {
dbClause = dbClause.and(new DbClause.LongClause("recipient_id", recipientId));
} else {
dbClause = new DbClause.LongClause("recipient_id", recipientId);
}
}
if (property != null) {
dbClause = dbClause.and(new DbClause.StringClause("property", property));
}
return accountPropertyTable.getManyBy(dbClause, from, to, " ORDER BY property ");
}
public static AccountProperty getProperty(long recipientId, String property) {
return getProperty(recipientId, property, recipientId);
}
public static AccountProperty getProperty(long recipientId, String property, long setterId) {
if (recipientId == 0 || setterId == 0) {
throw new IllegalArgumentException("Both recipientId and setterId must be specified");
}
DbClause dbClause = new DbClause.LongClause("recipient_id", recipientId);
dbClause = dbClause.and(new DbClause.StringClause("property", property));
if (setterId != recipientId) {
dbClause = dbClause.and(new DbClause.LongClause("setter_id", setterId));
} else {
dbClause = dbClause.and(new DbClause.NullClause("setter_id"));
}
return accountPropertyTable.getBy(dbClause);
}
public static Account getAccount(long id) {
DbKey dbKey = accountDbKeyFactory.newKey(id);
Account account = accountTable.get(dbKey);
if (account == null) {
PublicKey publicKey = publicKeyTable.get(publicKeyDbKeyFactory.newKey(id));
if (publicKey != null) {
account = accountTable.newEntity(dbKey);
account.publicKey = publicKey;
}
}
return account;
}
public static Account getAccount(long id, int height) {
DbKey dbKey = accountDbKeyFactory.newKey(id);
Account account = accountTable.get(dbKey, height);
if (account == null) {
PublicKey publicKey = publicKeyTable.get(publicKeyDbKeyFactory.newKey(id), height);
if (publicKey != null) {
account = accountTable.newEntity(dbKey);
account.publicKey = publicKey;
}
}
return account;
}
public static Account getAccount(byte[] publicKey) {
long accountId = getId(publicKey);
Account account = getAccount(accountId);
if (account == null) {
return null;
}
byte[] accountPublicKey = account.getPublicKey();
if (accountPublicKey == null || Arrays.equals(accountPublicKey, publicKey)) {
return account;
}
throw new RuntimeException("DUPLICATE KEY for account " + Long.toUnsignedString(accountId)
+ " existing key " + Convert.toHexString(accountPublicKey) + " new key " + Convert.toHexString(publicKey));
}
public static long getId(byte[] publicKey) {
byte[] publicKeyHash = Crypto.sha256().digest(publicKey);
return Convert.fullHashToId(publicKeyHash);
}
public static byte[] getPublicKey(long id) {
PublicKey publicKey = publicKeyTable.get(publicKeyDbKeyFactory.newKey(id));
return publicKey == null ? null : publicKey.publicKey;
}
static Account addOrGetAccount(long id) {
if (id == 0) {
throw new IllegalArgumentException("Invalid accountId 0");
}
DbKey accountDbKey = accountDbKeyFactory.newKey(id);
Account account = accountTable.get(accountDbKey);
if (account == null) {
account = accountTable.newEntity(accountDbKey);
DbKey pkDbKey = publicKeyDbKeyFactory.newKey(id);
PublicKey publicKey = publicKeyTable.get(pkDbKey);
if (publicKey == null) {
publicKey = publicKeyTable.newEntity(pkDbKey);
publicKeyTable.insert(publicKey);
}
account.publicKey = publicKey;
}
return account;
}
private static DbIterator<AccountLease> getLeaseChangingAccounts(final int height) {
Connection con = null;
try {
con = Db.db.getConnection();
PreparedStatement pstmt = con.prepareStatement(
"SELECT * FROM account_lease WHERE current_leasing_height_from = ? AND latest = TRUE "
+ "UNION ALL SELECT * FROM account_lease WHERE current_leasing_height_to = ? AND latest = TRUE "
+ "ORDER BY current_lessee_id, lessor_id");
int i = 0;
pstmt.setInt(++i, height);
pstmt.setInt(++i, height);
return accountLeaseTable.getManyBy(con, pstmt, true);
} catch (SQLException e) {
DbUtils.close(con);
throw new RuntimeException(e.toString(), e);
}
}
public static DbIterator<AccountAsset> getAccountAssets(long accountId, int from, int to) {
return accountAssetTable.getManyBy(new DbClause.LongClause("account_id", accountId), from, to);
}
public static DbIterator<AccountAsset> getAccountAssets(long accountId, int height, int from, int to) {
return accountAssetTable.getManyBy(new DbClause.LongClause("account_id", accountId), height, from, to);
}
public static AccountAsset getAccountAsset(long accountId, long assetId) {
return accountAssetTable.get(accountAssetDbKeyFactory.newKey(accountId, assetId));
}
public static AccountAsset getAccountAsset(long accountId, long assetId, int height) {
return accountAssetTable.get(accountAssetDbKeyFactory.newKey(accountId, assetId), height);
}
public static DbIterator<AccountAsset> getAssetAccounts(long assetId, int from, int to) {
return accountAssetTable.getManyBy(new DbClause.LongClause("asset_id", assetId), from, to, " ORDER BY quantity DESC, account_id ");
}
public static DbIterator<AccountAsset> getAssetAccounts(long assetId, int height, int from, int to) {
return accountAssetTable.getManyBy(new DbClause.LongClause("asset_id", assetId), height, from, to, " ORDER BY quantity DESC, account_id ");
}
public static AccountCurrency getAccountCurrency(long accountId, long currencyId) {
return accountCurrencyTable.get(accountCurrencyDbKeyFactory.newKey(accountId, currencyId));
}
public static AccountCurrency getAccountCurrency(long accountId, long currencyId, int height) {
return accountCurrencyTable.get(accountCurrencyDbKeyFactory.newKey(accountId, currencyId), height);
}
public static DbIterator<AccountCurrency> getAccountCurrencies(long accountId, int from, int to) {
return accountCurrencyTable.getManyBy(new DbClause.LongClause("account_id", accountId), from, to);
}
public static DbIterator<AccountCurrency> getAccountCurrencies(long accountId, int height, int from, int to) {
return accountCurrencyTable.getManyBy(new DbClause.LongClause("account_id", accountId), height, from, to);
}
public static DbIterator<AccountCurrency> getCurrencyAccounts(long currencyId, int from, int to) {
return accountCurrencyTable.getManyBy(new DbClause.LongClause("currency_id", currencyId), from, to);
}
public static DbIterator<AccountCurrency> getCurrencyAccounts(long currencyId, int height, int from, int to) {
return accountCurrencyTable.getManyBy(new DbClause.LongClause("currency_id", currencyId), height, from, to);
}
public static long getAssetBalanceQNT(long accountId, long assetId, int height) {
AccountAsset accountAsset = accountAssetTable.get(accountAssetDbKeyFactory.newKey(accountId, assetId), height);
return accountAsset == null ? 0 : accountAsset.quantityQNT;
}
public static long getAssetBalanceQNT(long accountId, long assetId) {
AccountAsset accountAsset = accountAssetTable.get(accountAssetDbKeyFactory.newKey(accountId, assetId));
return accountAsset == null ? 0 : accountAsset.quantityQNT;
}
public static long getUnconfirmedAssetBalanceQNT(long accountId, long assetId) {
AccountAsset accountAsset = accountAssetTable.get(accountAssetDbKeyFactory.newKey(accountId, assetId));
return accountAsset == null ? 0 : accountAsset.unconfirmedQuantityQNT;
}
public static long getCurrencyUnits(long accountId, long currencyId, int height) {
AccountCurrency accountCurrency = accountCurrencyTable.get(accountCurrencyDbKeyFactory.newKey(accountId, currencyId), height);
return accountCurrency == null ? 0 : accountCurrency.units;
}
public static long getCurrencyUnits(long accountId, long currencyId) {
AccountCurrency accountCurrency = accountCurrencyTable.get(accountCurrencyDbKeyFactory.newKey(accountId, currencyId));
return accountCurrency == null ? 0 : accountCurrency.units;
}
public static long getUnconfirmedCurrencyUnits(long accountId, long currencyId) {
AccountCurrency accountCurrency = accountCurrencyTable.get(accountCurrencyDbKeyFactory.newKey(accountId, currencyId));
return accountCurrency == null ? 0 : accountCurrency.unconfirmedUnits;
}
public static DbIterator<AccountInfo> searchAccounts(String query, int from, int to) {
return accountInfoTable.search(query, DbClause.EMPTY_CLAUSE, from, to);
}
static {
Nxt.getBlockchainProcessor().addListener(block -> {
int height = block.getHeight();
if (height < Constants.TRANSPARENT_FORGING_BLOCK_6) {
return;
}
List<AccountLease> changingLeases = new ArrayList<>();
try (DbIterator<AccountLease> leases = getLeaseChangingAccounts(height)) {
while (leases.hasNext()) {
changingLeases.add(leases.next());
}
}
for (AccountLease lease : changingLeases) {
Account lessor = Account.getAccount(lease.lessorId);
if (height == lease.currentLeasingHeightFrom) {
lessor.activeLesseeId = lease.currentLesseeId;
leaseListeners.notify(lease, Event.LEASE_STARTED);
} else if (height == lease.currentLeasingHeightTo) {
leaseListeners.notify(lease, Event.LEASE_ENDED);
lessor.activeLesseeId = 0;
if (lease.nextLeasingHeightFrom == 0) {
lease.currentLeasingHeightFrom = 0;
lease.currentLeasingHeightTo = 0;
lease.currentLesseeId = 0;
accountLeaseTable.delete(lease);
} else {
lease.currentLeasingHeightFrom = lease.nextLeasingHeightFrom;
lease.currentLeasingHeightTo = lease.nextLeasingHeightTo;
lease.currentLesseeId = lease.nextLesseeId;
lease.nextLeasingHeightFrom = 0;
lease.nextLeasingHeightTo = 0;
lease.nextLesseeId = 0;
accountLeaseTable.insert(lease);
if (height == lease.currentLeasingHeightFrom) {
lessor.activeLesseeId = lease.currentLesseeId;
leaseListeners.notify(lease, Event.LEASE_STARTED);
}
}
}
lessor.save();
}
}, BlockchainProcessor.Event.AFTER_BLOCK_APPLY);
}
static void init() {}
private final long id;
private final DbKey dbKey;
private PublicKey publicKey;
private long balanceNQT;
private long unconfirmedBalanceNQT;
private long forgedBalanceNQT;
private long activeLesseeId;
private Set<ControlType> controls;
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.controls = Collections.emptySet();
}
private Account(ResultSet rs) throws SQLException {
this.id = rs.getLong("id");
this.dbKey = accountDbKeyFactory.newKey(this.id);
this.balanceNQT = rs.getLong("balance");
this.unconfirmedBalanceNQT = rs.getLong("unconfirmed_balance");
this.forgedBalanceNQT = rs.getLong("forged_balance");
this.activeLesseeId = rs.getLong("active_lessee_id");
if (rs.getBoolean("has_control_phasing")) {
controls = Collections.unmodifiableSet(EnumSet.of(ControlType.PHASING_ONLY));
} else {
controls = Collections.emptySet();
}
}
private void save(Connection con) throws SQLException {
try (PreparedStatement pstmt = con.prepareStatement("MERGE INTO account (id, "
+ "balance, unconfirmed_balance, forged_balance, "
+ "active_lessee_id, has_control_phasing, height, latest) "
+ "KEY (id, height) VALUES (?, ?, ?, ?, ?, ?, ?, TRUE)")) {
int i = 0;
pstmt.setLong(++i, this.id);
pstmt.setLong(++i, this.balanceNQT);
pstmt.setLong(++i, this.unconfirmedBalanceNQT);
pstmt.setLong(++i, this.forgedBalanceNQT);
DbUtils.setLongZeroToNull(pstmt, ++i, this.activeLesseeId);
pstmt.setBoolean(++i, controls.contains(ControlType.PHASING_ONLY));
pstmt.setInt(++i, Nxt.getBlockchain().getHeight());
pstmt.executeUpdate();
}
}
private void save() {
if (balanceNQT == 0 && unconfirmedBalanceNQT == 0 && forgedBalanceNQT == 0 && activeLesseeId == 0 && controls.isEmpty()) {
accountTable.delete(this, true);
} else {
accountTable.insert(this);
}
}
public long getId() {
return id;
}
public AccountInfo getAccountInfo() {
return accountInfoTable.get(accountInfoDbKeyFactory.newKey(this.id));
}
void setAccountInfo(String name, String description) {
name = Convert.emptyToNull(name.trim());
description = Convert.emptyToNull(description.trim());
AccountInfo accountInfo = getAccountInfo();
if (accountInfo == null) {
accountInfo = new AccountInfo(id, name, description);
} else {
accountInfo.name = name;
accountInfo.description = description;
}
accountInfo.save();
}
public AccountLease getAccountLease() {
return accountLeaseTable.get(accountLeaseDbKeyFactory.newKey(this.id));
}
public byte[] getPublicKey() {
if (this.publicKey == null) {
this.publicKey = publicKeyTable.get(publicKeyDbKeyFactory.newKey(this.id));
}
return publicKey == null ? null : publicKey.publicKey;
}
public EncryptedData encryptTo(byte[] data, String senderSecretPhrase, boolean compress) {
if (getPublicKey() == null) {
throw new IllegalArgumentException("Recipient account doesn't have a public key set");
}
return Account.encryptTo(publicKey.publicKey, data, senderSecretPhrase, compress);
}
public static EncryptedData encryptTo(byte[] publicKey, byte[] data, String senderSecretPhrase, boolean compress) {
if (compress && data.length > 0) {
data = Convert.compress(data);
}
return EncryptedData.encrypt(data, senderSecretPhrase, publicKey);
}
public byte[] decryptFrom(EncryptedData encryptedData, String recipientSecretPhrase, boolean uncompress) {
if (getPublicKey() == null) {
throw new IllegalArgumentException("Sender account doesn't have a public key set");
}
return Account.decryptFrom(publicKey.publicKey, encryptedData, recipientSecretPhrase, uncompress);
}
public static byte[] decryptFrom(byte[] publicKey, EncryptedData encryptedData, String recipientSecretPhrase, boolean uncompress) {
byte[] decrypted = encryptedData.decrypt(recipientSecretPhrase, publicKey);
if (uncompress && decrypted.length > 0) {
decrypted = Convert.uncompress(decrypted);
}
return decrypted;
}
public long getBalanceNQT() {
return balanceNQT;
}
public long getUnconfirmedBalanceNQT() {
return unconfirmedBalanceNQT;
}
public long getForgedBalanceNQT() {
return forgedBalanceNQT;
}
public long getEffectiveBalanceNXT() {
return getEffectiveBalanceNXT(Nxt.getBlockchain().getHeight());
}
public long getEffectiveBalanceNXT(int height) {
if (height >= Constants.TRANSPARENT_FORGING_BLOCK_6) {
if (this.publicKey == null) {
this.publicKey = publicKeyTable.get(publicKeyDbKeyFactory.newKey(this.id));
}
if (this.publicKey == null || this.publicKey.publicKey == null || this.publicKey.height == 0 || height - this.publicKey.height <= 1440) {
return 0; // cfb: Accounts with the public key revealed less than 1440 blocks ago are not allowed to generate blocks
}
}
if (height < Constants.TRANSPARENT_FORGING_BLOCK_3) {
if (Arrays.binarySearch(Genesis.GENESIS_RECIPIENTS, id) >= 0) {
return balanceNQT / Constants.ONE_NXT;
}
long receivedInLastBlock = 0;
for (Transaction transaction : Nxt.getBlockchain().getBlockAtHeight(height).getTransactions()) {
if (id == transaction.getRecipientId()) {
receivedInLastBlock += transaction.getAmountNQT();
}
}
return (balanceNQT - receivedInLastBlock) / Constants.ONE_NXT;
}
long effectiveBalanceNQT = getLessorsGuaranteedBalanceNQT(height);
if (activeLesseeId == 0) {
effectiveBalanceNQT += getGuaranteedBalanceNQT(Constants.GUARANTEED_BALANCE_CONFIRMATIONS, height);
}
return (height > Constants.SHUFFLING_BLOCK && effectiveBalanceNQT < Constants.MIN_FORGING_BALANCE_NQT) ? 0 : effectiveBalanceNQT / Constants.ONE_NXT;
}
private long getLessorsGuaranteedBalanceNQT(int height) {
List<Account> lessors = new ArrayList<>();
try (DbIterator<Account> iterator = getLessors(height)) {
while (iterator.hasNext()) {
lessors.add(iterator.next());
}
}
Long[] lessorIds = new Long[lessors.size()];
long[] balances = new long[lessors.size()];
for (int i = 0; i < lessors.size(); i++) {
lessorIds[i] = lessors.get(i).getId();
balances[i] = lessors.get(i).getBalanceNQT();
}
try (Connection con = Db.db.getConnection();
PreparedStatement pstmt = con.prepareStatement("SELECT account_id, SUM (additions) AS additions "
+ "FROM account_guaranteed_balance, TABLE (id BIGINT=?) T WHERE account_id = T.id AND height > ? "
+ (height < Nxt.getBlockchain().getHeight() ? " AND height <= ? " : "")
+ " GROUP BY account_id ORDER BY account_id")) {
pstmt.setObject(1, lessorIds);
pstmt.setInt(2, height - Constants.GUARANTEED_BALANCE_CONFIRMATIONS);
if (height < Nxt.getBlockchain().getHeight()) {
pstmt.setInt(3, height);
}
long total = 0;
int i = 0;
try (ResultSet rs = pstmt.executeQuery()) {
while (rs.next()) {
long accountId = rs.getLong("account_id");
while (lessorIds[i] < accountId && i < lessorIds.length) {
total += balances[i++];
}
if (lessorIds[i] == accountId) {
total += Math.max(balances[i++] - rs.getLong("additions"), 0);
}
}
}
while (i < balances.length) {
total += balances[i++];
}
return total;
} catch (SQLException e) {
throw new RuntimeException(e.toString(), e);
}
}
public DbIterator<Account> getLessors() {
return accountTable.getManyBy(new DbClause.LongClause("active_lessee_id", id), 0, -1, " ORDER BY id ASC ");
}
public DbIterator<Account> getLessors(int height) {
return accountTable.getManyBy(new DbClause.LongClause("active_lessee_id", id), height, 0, -1, " ORDER BY id ASC ");
}
public long getGuaranteedBalanceNQT() {
return getGuaranteedBalanceNQT(Constants.GUARANTEED_BALANCE_CONFIRMATIONS, Nxt.getBlockchain().getHeight());
}
public long getGuaranteedBalanceNQT(final int numberOfConfirmations, final int currentHeight) {
int height = currentHeight - numberOfConfirmations;
if (height + Constants.GUARANTEED_BALANCE_CONFIRMATIONS < Nxt.getBlockchainProcessor().getMinRollbackHeight()
|| height > Nxt.getBlockchain().getHeight()) {
throw new IllegalArgumentException("Height " + height + " not available for guaranteed balance calculation");
}
try (Connection con = Db.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(Math.subtractExact(balanceNQT, rs.getLong("additions")), 0);
}
} catch (SQLException e) {
throw new RuntimeException(e.toString(), e);
}
}
public DbIterator<AccountAsset> getAssets(int from, int to) {
return accountAssetTable.getManyBy(new DbClause.LongClause("account_id", this.id), from, to);
}
public DbIterator<AccountAsset> getAssets(int height, int from, int to) {
return accountAssetTable.getManyBy(new DbClause.LongClause("account_id", this.id), height, from, to);
}
public DbIterator<Trade> getTrades(int from, int to) {
return Trade.getAccountTrades(this.id, from, to);
}
public DbIterator<AssetTransfer> getAssetTransfers(int from, int to) {
return AssetTransfer.getAccountAssetTransfers(this.id, from, to);
}
public DbIterator<CurrencyTransfer> getCurrencyTransfers(int from, int to) {
return CurrencyTransfer.getAccountCurrencyTransfers(this.id, from, to);
}
public DbIterator<Exchange> getExchanges(int from, int to) {
return Exchange.getAccountExchanges(this.id, from, to);
}
public AccountAsset getAsset(long assetId) {
return accountAssetTable.get(accountAssetDbKeyFactory.newKey(this.id, assetId));
}
public AccountAsset getAsset(long assetId, int height) {
return accountAssetTable.get(accountAssetDbKeyFactory.newKey(this.id, assetId), height);
}
public long getAssetBalanceQNT(long assetId) {
return getAssetBalanceQNT(this.id, assetId);
}
public long getAssetBalanceQNT(long assetId, int height) {
return getAssetBalanceQNT(this.id, assetId, height);
}
public long getUnconfirmedAssetBalanceQNT(long assetId) {
return getUnconfirmedAssetBalanceQNT(this.id, assetId);
}
public AccountCurrency getCurrency(long currencyId) {
return accountCurrencyTable.get(accountCurrencyDbKeyFactory.newKey(this.id, currencyId));
}
public AccountCurrency getCurrency(long currencyId, int height) {
return accountCurrencyTable.get(accountCurrencyDbKeyFactory.newKey(this.id, currencyId), height);
}
public DbIterator<AccountCurrency> getCurrencies(int from, int to) {
return accountCurrencyTable.getManyBy(new DbClause.LongClause("account_id", this.id), from, to);
}
public DbIterator<AccountCurrency> getCurrencies(int height, int from, int to) {
return accountCurrencyTable.getManyBy(new DbClause.LongClause("account_id", this.id), height, from, to);
}
public long getCurrencyUnits(long currencyId) {
return getCurrencyUnits(this.id, currencyId);
}
public long getCurrencyUnits(long currencyId, int height) {
return getCurrencyUnits(this.id, currencyId, height);
}
public long getUnconfirmedCurrencyUnits(long currencyId) {
return getUnconfirmedCurrencyUnits(this.id, currencyId);
}
public Set<ControlType> getControls() {
return controls;
}
void leaseEffectiveBalance(long lesseeId, int period) {
int height = Nxt.getBlockchain().getHeight();
AccountLease accountLease = accountLeaseTable.get(accountLeaseDbKeyFactory.newKey(id));
if (accountLease == null) {
accountLease = new AccountLease(id,
height + Constants.LEASING_DELAY,
height + Constants.LEASING_DELAY + period,
lesseeId);
} else if (accountLease.currentLesseeId == 0) {
accountLease.currentLeasingHeightFrom = height + Constants.LEASING_DELAY;
accountLease.currentLeasingHeightTo = height + Constants.LEASING_DELAY + period;
accountLease.currentLesseeId = lesseeId;
} else {
accountLease.nextLeasingHeightFrom = height + Constants.LEASING_DELAY;
if (accountLease.nextLeasingHeightFrom < accountLease.currentLeasingHeightTo) {
accountLease.nextLeasingHeightFrom = accountLease.currentLeasingHeightTo;
}
accountLease.nextLeasingHeightTo = accountLease.nextLeasingHeightFrom + period;
accountLease.nextLesseeId = lesseeId;
}
accountLeaseTable.insert(accountLease);
leaseListeners.notify(accountLease, Event.LEASE_SCHEDULED);
}
void addControl(ControlType control) {
if (controls.contains(control)) {
return;
}
EnumSet<ControlType> newControls = EnumSet.of(control);
newControls.addAll(controls);
controls = Collections.unmodifiableSet(newControls);
accountTable.insert(this);
}
void removeControl(ControlType control) {
if (!controls.contains(control)) {
return;
}
EnumSet<ControlType> newControls = EnumSet.copyOf(controls);
newControls.remove(control);
controls = Collections.unmodifiableSet(newControls);
accountTable.insert(this);
}
void setProperty(Transaction transaction, Account setterAccount, String property, String value) {
value = Convert.emptyToNull(value);
AccountProperty accountProperty = getProperty(this.id, property, setterAccount.id);
if (accountProperty == null) {
accountProperty = new AccountProperty(transaction.getId(), this.id, setterAccount.id, property, value);
} else {
accountProperty.value = value;
}
accountPropertyTable.insert(accountProperty);
}
void deleteProperty(long propertyId) {
AccountProperty accountProperty = accountPropertyTable.get(accountPropertyDbKeyFactory.newKey(propertyId));
if (accountProperty == null) {
return;
}
if (accountProperty.getSetterId() != this.id && accountProperty.getRecipientId() != this.id) {
throw new RuntimeException("Property " + Long.toUnsignedString(propertyId) + " cannot be deleted by " + Long.toUnsignedString(this.id));
}
accountPropertyTable.delete(accountProperty);
}
static boolean setOrVerify(long accountId, byte[] key) {
DbKey dbKey = publicKeyDbKeyFactory.newKey(accountId);
PublicKey publicKey = publicKeyTable.get(dbKey);
if (publicKey == null) {
publicKey = publicKeyTable.newEntity(dbKey);
}
if (publicKey.publicKey == null) {
publicKey.publicKey = key;
publicKey.height = Nxt.getBlockchain().getHeight();
return true;
}
return Arrays.equals(publicKey.publicKey, key);
}
void apply(byte[] key) {
DbKey dbKey = publicKeyDbKeyFactory.newKey(id);
PublicKey publicKey = publicKeyTable.get(dbKey);
if (publicKey == null) {
publicKey = publicKeyTable.newEntity(dbKey);
}
if (publicKey.publicKey == null) {
publicKey.publicKey = key;
publicKeyTable.insert(publicKey);
} else if (! Arrays.equals(publicKey.publicKey, key)) {
throw new IllegalStateException("Public key mismatch");
} else if (publicKey.height >= Nxt.getBlockchain().getHeight() - 1) {
PublicKey dbPublicKey = publicKeyTable.get(publicKeyDbKeyFactory.newKey(id), false);
if (dbPublicKey == null || dbPublicKey.publicKey == null) {
publicKeyTable.insert(publicKey);
}
}
this.publicKey = publicKey;
}
void addToAssetBalanceQNT(LedgerEvent event, long eventId, long assetId, long quantityQNT) {
if (quantityQNT == 0) {
return;
}
AccountAsset accountAsset;
accountAsset = accountAssetTable.get(accountAssetDbKeyFactory.newKey(this.id, assetId));
long assetBalance = accountAsset == null ? 0 : accountAsset.quantityQNT;
assetBalance = Math.addExact(assetBalance, quantityQNT);
if (accountAsset == null) {
accountAsset = new AccountAsset(this.id, assetId, assetBalance, 0);
} else {
accountAsset.quantityQNT = assetBalance;
}
accountAsset.save();
listeners.notify(this, Event.ASSET_BALANCE);
assetListeners.notify(accountAsset, Event.ASSET_BALANCE);
if (AccountLedger.mustLogEntry(this.id, false)) {
AccountLedger.logEntry(new LedgerEntry(event, eventId, this.id, LedgerHolding.ASSET_BALANCE, assetId,
quantityQNT, assetBalance));
}
}
void addToUnconfirmedAssetBalanceQNT(LedgerEvent event, long eventId, long assetId, long quantityQNT) {
if (quantityQNT == 0) {
return;
}
AccountAsset accountAsset;
accountAsset = accountAssetTable.get(accountAssetDbKeyFactory.newKey(this.id, assetId));
long unconfirmedAssetBalance = accountAsset == null ? 0 : accountAsset.unconfirmedQuantityQNT;
unconfirmedAssetBalance = Math.addExact(unconfirmedAssetBalance, quantityQNT);
if (accountAsset == null) {
accountAsset = new AccountAsset(this.id, assetId, 0, unconfirmedAssetBalance);
} else {
accountAsset.unconfirmedQuantityQNT = unconfirmedAssetBalance;
}
accountAsset.save();
listeners.notify(this, Event.UNCONFIRMED_ASSET_BALANCE);
assetListeners.notify(accountAsset, Event.UNCONFIRMED_ASSET_BALANCE);
if (AccountLedger.mustLogEntry(this.id, true)) {
AccountLedger.logEntry(new LedgerEntry(event, eventId, this.id,
LedgerHolding.UNCONFIRMED_ASSET_BALANCE, assetId,
quantityQNT, unconfirmedAssetBalance));
}
}
void addToAssetAndUnconfirmedAssetBalanceQNT(LedgerEvent event, long eventId, long assetId, long quantityQNT) {
if (quantityQNT == 0) {
return;
}
AccountAsset accountAsset;
accountAsset = accountAssetTable.get(accountAssetDbKeyFactory.newKey(this.id, assetId));
long assetBalance = accountAsset == null ? 0 : accountAsset.quantityQNT;
assetBalance = Math.addExact(assetBalance, quantityQNT);
long unconfirmedAssetBalance = accountAsset == null ? 0 : accountAsset.unconfirmedQuantityQNT;
unconfirmedAssetBalance = Math.addExact(unconfirmedAssetBalance, quantityQNT);
if (accountAsset == null) {
accountAsset = new AccountAsset(this.id, assetId, assetBalance, unconfirmedAssetBalance);
} else {
accountAsset.quantityQNT = assetBalance;
accountAsset.unconfirmedQuantityQNT = unconfirmedAssetBalance;
}
accountAsset.save();
listeners.notify(this, Event.ASSET_BALANCE);
listeners.notify(this, Event.UNCONFIRMED_ASSET_BALANCE);
assetListeners.notify(accountAsset, Event.ASSET_BALANCE);
assetListeners.notify(accountAsset, Event.UNCONFIRMED_ASSET_BALANCE);
if (AccountLedger.mustLogEntry(this.id, true)) {
AccountLedger.logEntry(new LedgerEntry(event, eventId, this.id,
LedgerHolding.UNCONFIRMED_ASSET_BALANCE, assetId,
quantityQNT, unconfirmedAssetBalance));
}
if (AccountLedger.mustLogEntry(this.id, false)) {
AccountLedger.logEntry(new LedgerEntry(event, eventId, this.id,
LedgerHolding.ASSET_BALANCE, assetId,
quantityQNT, assetBalance));
}
}
void addToCurrencyUnits(LedgerEvent event, long eventId, long currencyId, long units) {
if (units == 0) {
return;
}
AccountCurrency accountCurrency;
accountCurrency = accountCurrencyTable.get(accountCurrencyDbKeyFactory.newKey(this.id, currencyId));
long currencyUnits = accountCurrency == null ? 0 : accountCurrency.units;
currencyUnits = Math.addExact(currencyUnits, units);
if (accountCurrency == null) {
accountCurrency = new AccountCurrency(this.id, currencyId, currencyUnits, 0);
} else {
accountCurrency.units = currencyUnits;
}
accountCurrency.save();
listeners.notify(this, Event.CURRENCY_BALANCE);
currencyListeners.notify(accountCurrency, Event.CURRENCY_BALANCE);
if (AccountLedger.mustLogEntry(this.id, false)) {
AccountLedger.logEntry(new LedgerEntry(event, eventId, this.id, LedgerHolding.CURRENCY_BALANCE, currencyId,
units, currencyUnits));
}
}
void addToUnconfirmedCurrencyUnits(LedgerEvent event, long eventId, long currencyId, long units) {
if (units == 0) {
return;
}
AccountCurrency accountCurrency = accountCurrencyTable.get(accountCurrencyDbKeyFactory.newKey(this.id, currencyId));
long unconfirmedCurrencyUnits = accountCurrency == null ? 0 : accountCurrency.unconfirmedUnits;
unconfirmedCurrencyUnits = Math.addExact(unconfirmedCurrencyUnits, units);
if (accountCurrency == null) {
accountCurrency = new AccountCurrency(this.id, currencyId, 0, unconfirmedCurrencyUnits);
} else {
accountCurrency.unconfirmedUnits = unconfirmedCurrencyUnits;
}
accountCurrency.save();
listeners.notify(this, Event.UNCONFIRMED_CURRENCY_BALANCE);
currencyListeners.notify(accountCurrency, Event.UNCONFIRMED_CURRENCY_BALANCE);
if (AccountLedger.mustLogEntry(this.id, true)) {
AccountLedger.logEntry(new LedgerEntry(event, eventId, this.id,
LedgerHolding.UNCONFIRMED_CURRENCY_BALANCE, currencyId,
units, unconfirmedCurrencyUnits));
}
}
void addToCurrencyAndUnconfirmedCurrencyUnits(LedgerEvent event, long eventId, long currencyId, long units) {
if (units == 0) {
return;
}
AccountCurrency accountCurrency;
accountCurrency = accountCurrencyTable.get(accountCurrencyDbKeyFactory.newKey(this.id, currencyId));
long currencyUnits = accountCurrency == null ? 0 : accountCurrency.units;
currencyUnits = Math.addExact(currencyUnits, units);
long unconfirmedCurrencyUnits = accountCurrency == null ? 0 : accountCurrency.unconfirmedUnits;
unconfirmedCurrencyUnits = Math.addExact(unconfirmedCurrencyUnits, units);
if (accountCurrency == null) {
accountCurrency = new AccountCurrency(this.id, currencyId, currencyUnits, unconfirmedCurrencyUnits);
} else {
accountCurrency.units = currencyUnits;
accountCurrency.unconfirmedUnits = unconfirmedCurrencyUnits;
}
accountCurrency.save();
listeners.notify(this, Event.CURRENCY_BALANCE);
listeners.notify(this, Event.UNCONFIRMED_CURRENCY_BALANCE);
currencyListeners.notify(accountCurrency, Event.CURRENCY_BALANCE);
currencyListeners.notify(accountCurrency, Event.UNCONFIRMED_CURRENCY_BALANCE);
if (AccountLedger.mustLogEntry(this.id, true)) {
AccountLedger.logEntry(new LedgerEntry(event, eventId, this.id,
LedgerHolding.UNCONFIRMED_CURRENCY_BALANCE, currencyId,
units, unconfirmedCurrencyUnits));
}
if (AccountLedger.mustLogEntry(this.id, false)) {
AccountLedger.logEntry(new LedgerEntry(event, eventId, this.id,
LedgerHolding.CURRENCY_BALANCE, currencyId,
units, currencyUnits));
}
}
void addToBalanceNQT(LedgerEvent event, long eventId, long amountNQT) {
addToBalanceNQT(event, eventId, amountNQT, 0);
}
void addToBalanceNQT(LedgerEvent event, long eventId, long amountNQT, long feeNQT) {
if (amountNQT == 0 && feeNQT == 0) {
return;
}
long totalAmountNQT = Math.addExact(amountNQT, feeNQT);
this.balanceNQT = Math.addExact(this.balanceNQT, totalAmountNQT);
addToGuaranteedBalanceNQT(totalAmountNQT);
checkBalance(this.id, this.balanceNQT, this.unconfirmedBalanceNQT);
save();
listeners.notify(this, Event.BALANCE);
if (AccountLedger.mustLogEntry(this.id, false)) {
if (feeNQT != 0) {
AccountLedger.logEntry(new LedgerEntry(LedgerEvent.TRANSACTION_FEE, eventId, this.id,
LedgerHolding.NXT_BALANCE, null, feeNQT, this.balanceNQT - amountNQT));
}
if (amountNQT != 0) {
AccountLedger.logEntry(new LedgerEntry(event, eventId, this.id,
LedgerHolding.NXT_BALANCE, null, amountNQT, this.balanceNQT));
}
}
}
void addToUnconfirmedBalanceNQT(LedgerEvent event, long eventId, long amountNQT) {
addToUnconfirmedBalanceNQT(event, eventId, amountNQT, 0);
}
void addToUnconfirmedBalanceNQT(LedgerEvent event, long eventId, long amountNQT, long feeNQT) {
if (amountNQT == 0 && feeNQT == 0) {
return;
}
long totalAmountNQT = Math.addExact(amountNQT, feeNQT);
this.unconfirmedBalanceNQT = Math.addExact(this.unconfirmedBalanceNQT, totalAmountNQT);
checkBalance(this.id, this.balanceNQT, this.unconfirmedBalanceNQT);
save();
listeners.notify(this, Event.UNCONFIRMED_BALANCE);
if (AccountLedger.mustLogEntry(this.id, true)) {
if (feeNQT != 0) {
AccountLedger.logEntry(new LedgerEntry(LedgerEvent.TRANSACTION_FEE, eventId, this.id,
LedgerHolding.UNCONFIRMED_NXT_BALANCE, null, feeNQT, this.unconfirmedBalanceNQT - amountNQT));
}
if (amountNQT != 0) {
AccountLedger.logEntry(new LedgerEntry(event, eventId, this.id,
LedgerHolding.UNCONFIRMED_NXT_BALANCE, null, amountNQT, this.unconfirmedBalanceNQT));
}
}
}
void addToBalanceAndUnconfirmedBalanceNQT(LedgerEvent event, long eventId, long amountNQT) {
addToBalanceAndUnconfirmedBalanceNQT(event, eventId, amountNQT, 0);
}
void addToBalanceAndUnconfirmedBalanceNQT(LedgerEvent event, long eventId, long amountNQT, long feeNQT) {
if (amountNQT == 0 && feeNQT == 0) {
return;
}
long totalAmountNQT = Math.addExact(amountNQT, feeNQT);
this.balanceNQT = Math.addExact(this.balanceNQT, totalAmountNQT);
this.unconfirmedBalanceNQT = Math.addExact(this.unconfirmedBalanceNQT, totalAmountNQT);
addToGuaranteedBalanceNQT(totalAmountNQT);
checkBalance(this.id, this.balanceNQT, this.unconfirmedBalanceNQT);
save();
listeners.notify(this, Event.BALANCE);
listeners.notify(this, Event.UNCONFIRMED_BALANCE);
if (AccountLedger.mustLogEntry(this.id, true)) {
if (feeNQT != 0) {
AccountLedger.logEntry(new LedgerEntry(LedgerEvent.TRANSACTION_FEE, eventId, this.id,
LedgerHolding.UNCONFIRMED_NXT_BALANCE, null, feeNQT, this.unconfirmedBalanceNQT - amountNQT));
}
if (amountNQT != 0) {
AccountLedger.logEntry(new LedgerEntry(event, eventId, this.id,
LedgerHolding.UNCONFIRMED_NXT_BALANCE, null, amountNQT, this.unconfirmedBalanceNQT));
}
}
if (AccountLedger.mustLogEntry(this.id, false)) {
if (feeNQT != 0) {
AccountLedger.logEntry(new LedgerEntry(LedgerEvent.TRANSACTION_FEE, eventId, this.id,
LedgerHolding.NXT_BALANCE, null, feeNQT, this.balanceNQT - amountNQT));
}
if (amountNQT != 0) {
AccountLedger.logEntry(new LedgerEntry(event, eventId, this.id,
LedgerHolding.NXT_BALANCE, null, amountNQT, this.balanceNQT));
}
}
}
void addToForgedBalanceNQT(long amountNQT) {
if (amountNQT == 0) {
return;
}
this.forgedBalanceNQT = Math.addExact(this.forgedBalanceNQT, amountNQT);
save();
}
private static void checkBalance(long accountId, long confirmed, long unconfirmed) {
if (accountId == Genesis.CREATOR_ID) {
return;
}
if (confirmed < 0) {
throw new DoubleSpendingException("Negative balance or quantity: ", accountId, confirmed, unconfirmed);
}
if (unconfirmed < 0) {
throw new DoubleSpendingException("Negative unconfirmed balance or quantity: ", accountId, confirmed, unconfirmed);
}
if (unconfirmed > confirmed) {
throw new DoubleSpendingException("Unconfirmed exceeds confirmed balance or quantity: ", accountId, confirmed, unconfirmed);
}
}
private void addToGuaranteedBalanceNQT(long amountNQT) {
if (amountNQT <= 0) {
return;
}
int blockchainHeight = Nxt.getBlockchain().getHeight();
try (Connection con = Db.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 = Math.addExact(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);
}
}
void payDividends(final long transactionId, final long assetId, final int height, final long amountNQTPerQNT) {
long totalDividend = 0;
List<AccountAsset> accountAssets = new ArrayList<>();
try (DbIterator<AccountAsset> iterator = getAssetAccounts(assetId, height, 0, -1)) {
while (iterator.hasNext()) {
accountAssets.add(iterator.next());
}
}
for (final AccountAsset accountAsset : accountAssets) {
if (accountAsset.getAccountId() != this.id && accountAsset.getQuantityQNT() != 0) {
long dividend = Math.multiplyExact(accountAsset.getQuantityQNT(), amountNQTPerQNT);
Account.getAccount(accountAsset.getAccountId())
.addToBalanceAndUnconfirmedBalanceNQT(LedgerEvent.ASSET_DIVIDEND_PAYMENT, transactionId, dividend);
totalDividend += dividend;
}
}
this.addToBalanceNQT(LedgerEvent.ASSET_DIVIDEND_PAYMENT, transactionId, -totalDividend);
}
@Override
public String toString() {
return "Account " + Long.toUnsignedString(getId());
}
}