mirror of
https://github.com/Qortal/qortal.git
synced 2025-03-13 11:12:31 +00:00
Account assets balances now height-dependant. QORT-from-QORA block reward fixes.
This commit is contained in:
parent
f5918bd9bf
commit
31cbc1f15b
@ -163,7 +163,7 @@ public class Account {
|
|||||||
* @throws DataException
|
* @throws DataException
|
||||||
*/
|
*/
|
||||||
public void setLastReference(byte[] reference) throws DataException {
|
public void setLastReference(byte[] reference) throws DataException {
|
||||||
LOGGER.trace(() -> String.format("Setting last reference for %s to %s", this.address, Base58.encode(reference)));
|
LOGGER.trace(() -> String.format("Setting last reference for %s to %s", this.address, (reference == null ? "null" : Base58.encode(reference))));
|
||||||
|
|
||||||
AccountData accountData = this.buildAccountData();
|
AccountData accountData = this.buildAccountData();
|
||||||
accountData.setReference(reference);
|
accountData.setReference(reference);
|
||||||
|
@ -1250,7 +1250,7 @@ public class Block {
|
|||||||
|
|
||||||
accountData.setBlocksMinted(accountData.getBlocksMinted() + 1);
|
accountData.setBlocksMinted(accountData.getBlocksMinted() + 1);
|
||||||
repository.getAccountRepository().setMintedBlockCount(accountData);
|
repository.getAccountRepository().setMintedBlockCount(accountData);
|
||||||
LOGGER.trace(() -> String.format("Block minted %s up to %d minted block%s", accountData.getAddress(), accountData.getBlocksMinted(), (accountData.getBlocksMinted() != 1 ? "s" : "")));
|
LOGGER.trace(() -> String.format("Block minter %s up to %d minted block%s", accountData.getAddress(), accountData.getBlocksMinted(), (accountData.getBlocksMinted() != 1 ? "s" : "")));
|
||||||
}
|
}
|
||||||
|
|
||||||
// We are only interested in accounts that are NOT founders and NOT already highest level
|
// We are only interested in accounts that are NOT founders and NOT already highest level
|
||||||
@ -1437,6 +1437,9 @@ public class Block {
|
|||||||
// Return AT fees and delete AT states from repository
|
// Return AT fees and delete AT states from repository
|
||||||
orphanAtFeesAndStates();
|
orphanAtFeesAndStates();
|
||||||
|
|
||||||
|
// Delete orphaned balances
|
||||||
|
this.repository.getAccountRepository().deleteBalancesFromHeight(this.blockData.getHeight());
|
||||||
|
|
||||||
// Delete block from blockchain
|
// Delete block from blockchain
|
||||||
this.repository.getBlockRepository().delete(this.blockData);
|
this.repository.getBlockRepository().delete(this.blockData);
|
||||||
this.blockData.setHeight(null);
|
this.blockData.setHeight(null);
|
||||||
@ -1640,12 +1643,9 @@ public class Block {
|
|||||||
qoraHoldersIterator.remove();
|
qoraHoldersIterator.remove();
|
||||||
} else {
|
} else {
|
||||||
// We're orphaning a block
|
// We're orphaning a block
|
||||||
// so disregard qora holders whose final block is earlier than this one
|
// so disregard qora holders who have already had their final qort-from-qora reward (i.e. reward reward block is earlier than this one)
|
||||||
QortFromQoraData qortFromQoraData = this.repository.getAccountRepository().getQortFromQoraInfo(qoraHolder.getAddress());
|
QortFromQoraData qortFromQoraData = this.repository.getAccountRepository().getQortFromQoraInfo(qoraHolder.getAddress());
|
||||||
if (qortFromQoraData == null)
|
if (qortFromQoraData != null && qortFromQoraData.getFinalBlockHeight() < this.blockData.getHeight())
|
||||||
throw new IllegalStateException(String.format("Missing QORT-from-QORA data for %s", qoraHolder.getAddress()));
|
|
||||||
|
|
||||||
if (qortFromQoraData.getFinalBlockHeight() != null && qortFromQoraData.getFinalBlockHeight() < this.blockData.getHeight())
|
|
||||||
qoraHoldersIterator.remove();
|
qoraHoldersIterator.remove();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -1660,18 +1660,14 @@ public class Block {
|
|||||||
for (int h = 0; h < qoraHolders.size(); ++h) {
|
for (int h = 0; h < qoraHolders.size(); ++h) {
|
||||||
AccountBalanceData qoraHolder = qoraHolders.get(h);
|
AccountBalanceData qoraHolder = qoraHolders.get(h);
|
||||||
|
|
||||||
final BigDecimal holderReward = qoraHoldersAmount.multiply(totalQoraHeld).divide(qoraHolder.getBalance(), RoundingMode.DOWN).setScale(8, RoundingMode.DOWN);
|
BigDecimal holderReward = qoraHoldersAmount.multiply(qoraHolder.getBalance()).divide(totalQoraHeld, RoundingMode.DOWN).setScale(8, RoundingMode.DOWN);
|
||||||
|
BigDecimal finalHolderReward = holderReward;
|
||||||
LOGGER.trace(() -> String.format("QORA holder %s has %s / %s QORA so share: %s",
|
LOGGER.trace(() -> String.format("QORA holder %s has %s / %s QORA so share: %s",
|
||||||
qoraHolder.getAddress(), qoraHolder.getBalance().toPlainString(), finalTotalQoraHeld, holderReward.toPlainString()));
|
qoraHolder.getAddress(), qoraHolder.getBalance().toPlainString(), finalTotalQoraHeld, finalHolderReward.toPlainString()));
|
||||||
|
|
||||||
Account qoraHolderAccount = new Account(repository, qoraHolder.getAddress());
|
Account qoraHolderAccount = new Account(repository, qoraHolder.getAddress());
|
||||||
QortFromQoraData qortFromQoraData = this.repository.getAccountRepository().getQortFromQoraInfo(qoraHolder.getAddress());
|
|
||||||
if (qortFromQoraData == null)
|
|
||||||
qortFromQoraData = new QortFromQoraData(qoraHolder.getAddress(), BigDecimal.ZERO.setScale(8), null);
|
|
||||||
|
|
||||||
BigDecimal qortFromQora = holderReward.divide(qoraPerQortReward, RoundingMode.DOWN);
|
BigDecimal newQortFromQoraBalance = qoraHolderAccount.getConfirmedBalance(Asset.QORT_FROM_QORA).add(holderReward);
|
||||||
|
|
||||||
BigDecimal newQortFromQoraBalance = qoraHolderAccount.getConfirmedBalance(Asset.QORT_FROM_QORA).add(qortFromQora);
|
|
||||||
|
|
||||||
// If processing, make sure we don't overpay
|
// If processing, make sure we don't overpay
|
||||||
if (totalAmount.signum() >= 0) {
|
if (totalAmount.signum() >= 0) {
|
||||||
@ -1681,41 +1677,39 @@ public class Block {
|
|||||||
// Reduce final QORT-from-QORA payment to match max
|
// Reduce final QORT-from-QORA payment to match max
|
||||||
BigDecimal adjustment = newQortFromQoraBalance.subtract(maxQortFromQora);
|
BigDecimal adjustment = newQortFromQoraBalance.subtract(maxQortFromQora);
|
||||||
|
|
||||||
qortFromQora = qortFromQora.subtract(adjustment);
|
holderReward = holderReward.subtract(adjustment);
|
||||||
newQortFromQoraBalance = newQortFromQoraBalance.subtract(adjustment);
|
newQortFromQoraBalance = newQortFromQoraBalance.subtract(adjustment);
|
||||||
|
|
||||||
// This is also qora holders final qort-from-qora block
|
// This is also qora holders final qort-from-qora block
|
||||||
qortFromQoraData.setFinalQortFromQora(qortFromQora);
|
QortFromQoraData qortFromQoraData = new QortFromQoraData(qoraHolder.getAddress(), holderReward, this.blockData.getHeight());
|
||||||
qortFromQoraData.setFinalBlockHeight(this.blockData.getHeight());
|
this.repository.getAccountRepository().save(qortFromQoraData);
|
||||||
|
|
||||||
BigDecimal finalQortFromQora = qortFromQora;
|
BigDecimal finalAdjustedHolderReward = holderReward;
|
||||||
LOGGER.trace(() -> String.format("QORA holder %s final share %s at height %d",
|
LOGGER.trace(() -> String.format("QORA holder %s final share %s at height %d",
|
||||||
qoraHolder.getAddress(), finalQortFromQora.toPlainString(), this.blockData.getHeight()));
|
qoraHolder.getAddress(), finalAdjustedHolderReward.toPlainString(), this.blockData.getHeight()));
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
// Orphaning
|
// Orphaning
|
||||||
if (qortFromQoraData.getFinalBlockHeight() != null) {
|
QortFromQoraData qortFromQoraData = this.repository.getAccountRepository().getQortFromQoraInfo(qoraHolder.getAddress());
|
||||||
|
if (qortFromQoraData != null) {
|
||||||
// Note use of negate() here as qortFromQora will be negative during orphaning,
|
// Note use of negate() here as qortFromQora will be negative during orphaning,
|
||||||
// but final qort-from-qora is stored in repository during processing (and hence positive).
|
// but final qort-from-qora is stored in repository during processing (and hence positive).
|
||||||
BigDecimal adjustment = qortFromQora.subtract(qortFromQoraData.getFinalQortFromQora().negate());
|
BigDecimal adjustment = holderReward.subtract(qortFromQoraData.getFinalQortFromQora().negate());
|
||||||
|
|
||||||
qortFromQora = qortFromQora.subtract(adjustment);
|
holderReward = holderReward.subtract(adjustment);
|
||||||
newQortFromQoraBalance = newQortFromQoraBalance.subtract(adjustment);
|
newQortFromQoraBalance = newQortFromQoraBalance.subtract(adjustment);
|
||||||
|
|
||||||
qortFromQoraData.setFinalQortFromQora(null);
|
this.repository.getAccountRepository().deleteQortFromQoraInfo(qoraHolder.getAddress());
|
||||||
qortFromQoraData.setFinalBlockHeight(null);
|
|
||||||
|
|
||||||
BigDecimal finalQortFromQora = qortFromQora;
|
BigDecimal finalAdjustedHolderReward = holderReward;
|
||||||
LOGGER.trace(() -> String.format("QORA holder %s final share %s was at height %d",
|
LOGGER.trace(() -> String.format("QORA holder %s final share %s was at height %d",
|
||||||
qoraHolder.getAddress(), finalQortFromQora.toPlainString(), this.blockData.getHeight()));
|
qoraHolder.getAddress(), finalAdjustedHolderReward.toPlainString(), this.blockData.getHeight()));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
qoraHolderAccount.setConfirmedBalance(Asset.QORT, qoraHolderAccount.getConfirmedBalance(Asset.QORT).add(qortFromQora));
|
qoraHolderAccount.setConfirmedBalance(Asset.QORT, qoraHolderAccount.getConfirmedBalance(Asset.QORT).add(holderReward));
|
||||||
qoraHolderAccount.setConfirmedBalance(Asset.QORT_FROM_QORA, newQortFromQoraBalance);
|
qoraHolderAccount.setConfirmedBalance(Asset.QORT_FROM_QORA, newQortFromQoraBalance);
|
||||||
|
|
||||||
this.repository.getAccountRepository().save(qortFromQoraData);
|
|
||||||
|
|
||||||
sharedAmount = sharedAmount.add(holderReward);
|
sharedAmount = sharedAmount.add(holderReward);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -86,18 +86,26 @@ public interface AccountRepository {
|
|||||||
|
|
||||||
public AccountBalanceData getBalance(String address, long assetId) throws DataException;
|
public AccountBalanceData getBalance(String address, long assetId) throws DataException;
|
||||||
|
|
||||||
|
/** Returns account balance data for address & assetId at (or before) passed block height. */
|
||||||
|
public AccountBalanceData getBalance(String address, long assetId, int height) throws DataException;
|
||||||
|
|
||||||
public enum BalanceOrdering {
|
public enum BalanceOrdering {
|
||||||
ASSET_BALANCE_ACCOUNT,
|
ASSET_BALANCE_ACCOUNT,
|
||||||
ACCOUNT_ASSET,
|
ACCOUNT_ASSET,
|
||||||
ASSET_ACCOUNT
|
ASSET_ACCOUNT
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public List<AccountBalanceData> getAssetBalances(long assetId, Boolean excludeZero) throws DataException;
|
||||||
|
|
||||||
public List<AccountBalanceData> getAssetBalances(List<String> addresses, List<Long> assetIds, BalanceOrdering balanceOrdering, Boolean excludeZero, Integer limit, Integer offset, Boolean reverse) throws DataException;
|
public List<AccountBalanceData> getAssetBalances(List<String> addresses, List<Long> assetIds, BalanceOrdering balanceOrdering, Boolean excludeZero, Integer limit, Integer offset, Boolean reverse) throws DataException;
|
||||||
|
|
||||||
public void save(AccountBalanceData accountBalanceData) throws DataException;
|
public void save(AccountBalanceData accountBalanceData) throws DataException;
|
||||||
|
|
||||||
public void delete(String address, long assetId) throws DataException;
|
public void delete(String address, long assetId) throws DataException;
|
||||||
|
|
||||||
|
/** Deletes orphaned balances at block height >= <tt>height</tt>. */
|
||||||
|
public int deleteBalancesFromHeight(int height) throws DataException;
|
||||||
|
|
||||||
// Reward-shares
|
// Reward-shares
|
||||||
|
|
||||||
public RewardShareData getRewardShare(byte[] mintingAccountPublicKey, String recipientAccount) throws DataException;
|
public RewardShareData getRewardShare(byte[] mintingAccountPublicKey, String recipientAccount) throws DataException;
|
||||||
@ -145,4 +153,6 @@ public interface AccountRepository {
|
|||||||
|
|
||||||
public void save(QortFromQoraData qortFromQoraData) throws DataException;
|
public void save(QortFromQoraData qortFromQoraData) throws DataException;
|
||||||
|
|
||||||
|
public int deleteQortFromQoraInfo(String address) throws DataException;
|
||||||
|
|
||||||
}
|
}
|
||||||
|
@ -259,7 +259,7 @@ public class HSQLDBAccountRepository implements AccountRepository {
|
|||||||
|
|
||||||
@Override
|
@Override
|
||||||
public AccountBalanceData getBalance(String address, long assetId) throws DataException {
|
public AccountBalanceData getBalance(String address, long assetId) throws DataException {
|
||||||
String sql = "SELECT balance FROM AccountBalances WHERE account = ? AND asset_id = ?";
|
String sql = "SELECT balance FROM AccountBalances WHERE account = ? AND asset_id = ? ORDER BY height DESC LIMIT 1";
|
||||||
|
|
||||||
try (ResultSet resultSet = this.repository.checkedExecute(sql, address, assetId)) {
|
try (ResultSet resultSet = this.repository.checkedExecute(sql, address, assetId)) {
|
||||||
if (resultSet == null)
|
if (resultSet == null)
|
||||||
@ -273,6 +273,48 @@ public class HSQLDBAccountRepository implements AccountRepository {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public AccountBalanceData getBalance(String address, long assetId, int height) throws DataException {
|
||||||
|
String sql = "SELECT balance FROM AccountBalances WHERE account = ? AND asset_id = ? AND height <= ? ORDER BY height DESC LIMIT 1";
|
||||||
|
|
||||||
|
try (ResultSet resultSet = this.repository.checkedExecute(sql, address, assetId, height)) {
|
||||||
|
if (resultSet == null)
|
||||||
|
return null;
|
||||||
|
|
||||||
|
BigDecimal balance = resultSet.getBigDecimal(1).setScale(8);
|
||||||
|
|
||||||
|
return new AccountBalanceData(address, assetId, balance);
|
||||||
|
} catch (SQLException e) {
|
||||||
|
throw new DataException("Unable to fetch account balance from repository", e);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public List<AccountBalanceData> getAssetBalances(long assetId, Boolean excludeZero) throws DataException {
|
||||||
|
StringBuilder sql = new StringBuilder(1024);
|
||||||
|
sql.append("SELECT account, IFNULL(balance, 0) FROM NewestAccountBalances WHERE asset_id = ?");
|
||||||
|
|
||||||
|
if (excludeZero != null && excludeZero)
|
||||||
|
sql.append(" AND balance != 0");
|
||||||
|
|
||||||
|
List<AccountBalanceData> accountBalances = new ArrayList<>();
|
||||||
|
try (ResultSet resultSet = this.repository.checkedExecute(sql.toString(), assetId)) {
|
||||||
|
if (resultSet == null)
|
||||||
|
return accountBalances;
|
||||||
|
|
||||||
|
do {
|
||||||
|
String address = resultSet.getString(1);
|
||||||
|
BigDecimal balance = resultSet.getBigDecimal(2).setScale(8);
|
||||||
|
|
||||||
|
accountBalances.add(new AccountBalanceData(address, assetId, balance));
|
||||||
|
} while (resultSet.next());
|
||||||
|
|
||||||
|
return accountBalances;
|
||||||
|
} catch (SQLException e) {
|
||||||
|
throw new DataException("Unable to fetch asset balances from repository", e);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public List<AccountBalanceData> getAssetBalances(List<String> addresses, List<Long> assetIds, BalanceOrdering balanceOrdering, Boolean excludeZero,
|
public List<AccountBalanceData> getAssetBalances(List<String> addresses, List<Long> assetIds, BalanceOrdering balanceOrdering, Boolean excludeZero,
|
||||||
Integer limit, Integer offset, Boolean reverse) throws DataException {
|
Integer limit, Integer offset, Boolean reverse) throws DataException {
|
||||||
@ -291,10 +333,10 @@ public class HSQLDBAccountRepository implements AccountRepository {
|
|||||||
}
|
}
|
||||||
|
|
||||||
sql.append(") AS Accounts (account) ");
|
sql.append(") AS Accounts (account) ");
|
||||||
sql.append("CROSS JOIN Assets LEFT OUTER JOIN AccountBalances USING (asset_id, account) ");
|
sql.append("CROSS JOIN Assets LEFT OUTER JOIN NewestAccountBalances USING (asset_id, account) ");
|
||||||
} else {
|
} else {
|
||||||
// Simplier, no-address query
|
// Simplier, no-address query
|
||||||
sql.append("AccountBalances NATURAL JOIN Assets ");
|
sql.append("NewestAccountBalances NATURAL JOIN Assets ");
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!assetIds.isEmpty()) {
|
if (!assetIds.isEmpty()) {
|
||||||
@ -378,6 +420,10 @@ public class HSQLDBAccountRepository implements AccountRepository {
|
|||||||
accountBalanceData.getBalance());
|
accountBalanceData.getBalance());
|
||||||
|
|
||||||
try {
|
try {
|
||||||
|
// Fill in 'height'
|
||||||
|
int height = this.repository.checkedExecute("SELECT COUNT(*) + 1 FROM Blocks").getInt(1);
|
||||||
|
saveHelper.bind("height", height);
|
||||||
|
|
||||||
saveHelper.execute(this.repository);
|
saveHelper.execute(this.repository);
|
||||||
} catch (SQLException e) {
|
} catch (SQLException e) {
|
||||||
throw new DataException("Unable to save account balance into repository", e);
|
throw new DataException("Unable to save account balance into repository", e);
|
||||||
@ -387,12 +433,21 @@ public class HSQLDBAccountRepository implements AccountRepository {
|
|||||||
@Override
|
@Override
|
||||||
public void delete(String address, long assetId) throws DataException {
|
public void delete(String address, long assetId) throws DataException {
|
||||||
try {
|
try {
|
||||||
this.repository.delete("AccountBalances", "account = ? and asset_id = ?", address, assetId);
|
this.repository.delete("AccountBalances", "account = ? AND asset_id = ?", address, assetId);
|
||||||
} catch (SQLException e) {
|
} catch (SQLException e) {
|
||||||
throw new DataException("Unable to delete account balance from repository", e);
|
throw new DataException("Unable to delete account balance from repository", e);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public int deleteBalancesFromHeight(int height) throws DataException {
|
||||||
|
try {
|
||||||
|
return this.repository.delete("AccountBalances", "height >= ?", height);
|
||||||
|
} catch (SQLException e) {
|
||||||
|
throw new DataException("Unable to delete old account balances from repository", e);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// Reward-Share
|
// Reward-Share
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
@ -695,4 +750,12 @@ public class HSQLDBAccountRepository implements AccountRepository {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public int deleteQortFromQoraInfo(String address) throws DataException {
|
||||||
|
try {
|
||||||
|
return this.repository.delete("AccountQortFromQoraInfo", "account = ?", address);
|
||||||
|
} catch (SQLException e) {
|
||||||
|
throw new DataException("Unable to delete qort-from-qora info from repository", e);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
@ -847,6 +847,23 @@ public class HSQLDBDatabaseUpdates {
|
|||||||
+ "PRIMARY KEY (account), FOREIGN KEY (account) REFERENCES Accounts (account) ON DELETE CASCADE)");
|
+ "PRIMARY KEY (account), FOREIGN KEY (account) REFERENCES Accounts (account) ON DELETE CASCADE)");
|
||||||
break;
|
break;
|
||||||
|
|
||||||
|
case 60: // Adding height to account balances
|
||||||
|
// We need to drop primary key first
|
||||||
|
stmt.execute("ALTER TABLE AccountBalances DROP PRIMARY KEY");
|
||||||
|
// Add height to account balances
|
||||||
|
stmt.execute("ALTER TABLE AccountBalances ADD COLUMN height INT NOT NULL DEFAULT 0 BEFORE BALANCE");
|
||||||
|
// Add new primary key
|
||||||
|
stmt.execute("ALTER TABLE AccountBalances ADD PRIMARY KEY (asset_id, account, height)");
|
||||||
|
/// Create a view for account balances at greatest height
|
||||||
|
stmt.execute("CREATE VIEW NewestAccountBalances (account, asset_id, balance) AS "
|
||||||
|
+ "SELECT AccountBalances.account, AccountBalances.asset_id, AccountBalances.balance FROM AccountBalances "
|
||||||
|
+ "LEFT OUTER JOIN AccountBalances AS NewerAccountBalances "
|
||||||
|
+ "ON NewerAccountBalances.account = AccountBalances.account "
|
||||||
|
+ "AND NewerAccountBalances.asset_id = AccountBalances.asset_id "
|
||||||
|
+ "AND NewerAccountBalances.height > AccountBalances.height "
|
||||||
|
+ "WHERE NewerAccountBalances.height IS NULL");
|
||||||
|
break;
|
||||||
|
|
||||||
default:
|
default:
|
||||||
// nothing to do
|
// nothing to do
|
||||||
return false;
|
return false;
|
||||||
|
180
src/test/java/org/qora/test/AccountBalanceTests.java
Normal file
180
src/test/java/org/qora/test/AccountBalanceTests.java
Normal file
@ -0,0 +1,180 @@
|
|||||||
|
package org.qora.test;
|
||||||
|
|
||||||
|
import static org.junit.Assert.assertNull;
|
||||||
|
import static org.junit.Assert.assertTrue;
|
||||||
|
|
||||||
|
import java.math.BigDecimal;
|
||||||
|
import java.util.Random;
|
||||||
|
|
||||||
|
import org.junit.After;
|
||||||
|
import org.junit.Before;
|
||||||
|
import org.junit.Test;
|
||||||
|
import org.qora.account.Account;
|
||||||
|
import org.qora.account.PrivateKeyAccount;
|
||||||
|
import org.qora.account.PublicKeyAccount;
|
||||||
|
import org.qora.asset.Asset;
|
||||||
|
import org.qora.block.BlockChain;
|
||||||
|
import org.qora.data.account.AccountBalanceData;
|
||||||
|
import org.qora.data.transaction.BaseTransactionData;
|
||||||
|
import org.qora.data.transaction.PaymentTransactionData;
|
||||||
|
import org.qora.data.transaction.TransactionData;
|
||||||
|
import org.qora.repository.DataException;
|
||||||
|
import org.qora.repository.Repository;
|
||||||
|
import org.qora.repository.RepositoryManager;
|
||||||
|
import org.qora.test.common.BlockUtils;
|
||||||
|
import org.qora.test.common.Common;
|
||||||
|
import org.qora.test.common.TestAccount;
|
||||||
|
import org.qora.test.common.TransactionUtils;
|
||||||
|
|
||||||
|
public class AccountBalanceTests extends Common {
|
||||||
|
|
||||||
|
@Before
|
||||||
|
public void beforeTest() throws DataException {
|
||||||
|
Common.useDefaultSettings();
|
||||||
|
}
|
||||||
|
|
||||||
|
@After
|
||||||
|
public void afterTest() throws DataException {
|
||||||
|
Common.orphanCheck();
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Tests that newer balances are returned instead of older ones. */
|
||||||
|
@Test
|
||||||
|
public void testNewerBalance() throws DataException {
|
||||||
|
try (final Repository repository = RepositoryManager.getRepository()) {
|
||||||
|
TestAccount alice = Common.getTestAccount(repository, "alice");
|
||||||
|
|
||||||
|
testNewerBalance(repository, alice);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private BigDecimal testNewerBalance(Repository repository, TestAccount testAccount) throws DataException {
|
||||||
|
// Grab initial balance
|
||||||
|
BigDecimal initialBalance = testAccount.getConfirmedBalance(Asset.QORT);
|
||||||
|
|
||||||
|
// Mint block to cause newer balance
|
||||||
|
BlockUtils.mintBlock(repository);
|
||||||
|
|
||||||
|
// Grab newer balance
|
||||||
|
BigDecimal newerBalance = testAccount.getConfirmedBalance(Asset.QORT);
|
||||||
|
|
||||||
|
// Confirm newer balance is greater than initial balance
|
||||||
|
assertTrue("Newer balance should be greater than initial balance", newerBalance.compareTo(initialBalance) > 0);
|
||||||
|
|
||||||
|
return initialBalance;
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Tests that orphaning reverts balance back to initial. */
|
||||||
|
@Test
|
||||||
|
public void testOrphanedBalance() throws DataException {
|
||||||
|
try (final Repository repository = RepositoryManager.getRepository()) {
|
||||||
|
TestAccount alice = Common.getTestAccount(repository, "alice");
|
||||||
|
|
||||||
|
BigDecimal initialBalance = testNewerBalance(repository, alice);
|
||||||
|
|
||||||
|
BlockUtils.orphanLastBlock(repository);
|
||||||
|
|
||||||
|
// Grab post-orphan balance
|
||||||
|
BigDecimal orphanedBalance = alice.getConfirmedBalance(Asset.QORT);
|
||||||
|
|
||||||
|
// Confirm post-orphan balance is same as initial
|
||||||
|
assertTrue("Post-orphan balance should match initial", orphanedBalance.equals(initialBalance));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Tests we can fetch initial balance when newer balance exists. */
|
||||||
|
@Test
|
||||||
|
public void testGetBalanceAtHeight() throws DataException {
|
||||||
|
try (final Repository repository = RepositoryManager.getRepository()) {
|
||||||
|
TestAccount alice = Common.getTestAccount(repository, "alice");
|
||||||
|
|
||||||
|
BigDecimal initialBalance = testNewerBalance(repository, alice);
|
||||||
|
|
||||||
|
// Fetch balance at height 1, even though newer balance exists
|
||||||
|
AccountBalanceData accountBalanceData = repository.getAccountRepository().getBalance(alice.getAddress(), Asset.QORT, 1);
|
||||||
|
BigDecimal genesisBalance = accountBalanceData.getBalance();
|
||||||
|
|
||||||
|
// Confirm genesis balance is same as initial
|
||||||
|
assertTrue("Genesis balance should match initial", genesisBalance.equals(initialBalance));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Tests we can fetch balance with a height where no balance change occurred. */
|
||||||
|
@Test
|
||||||
|
public void testGetBalanceAtNearestHeight() throws DataException {
|
||||||
|
Random random = new Random();
|
||||||
|
|
||||||
|
byte[] publicKey = new byte[32];
|
||||||
|
random.nextBytes(publicKey);
|
||||||
|
|
||||||
|
try (final Repository repository = RepositoryManager.getRepository()) {
|
||||||
|
PublicKeyAccount recipientAccount = new PublicKeyAccount(repository, publicKey);
|
||||||
|
|
||||||
|
// Mint a few blocks
|
||||||
|
for (int i = 0; i < 10; ++i)
|
||||||
|
BlockUtils.mintBlock(repository);
|
||||||
|
|
||||||
|
// Confirm recipient balance is zero
|
||||||
|
BigDecimal balance = recipientAccount.getConfirmedBalance(Asset.QORT);
|
||||||
|
assertTrue("recipient's balance should be zero", balance.signum() == 0);
|
||||||
|
|
||||||
|
// Send 1 QORT to recipient
|
||||||
|
TestAccount sendingAccount = Common.getTestAccount(repository, "alice");
|
||||||
|
pay(repository, sendingAccount, recipientAccount, BigDecimal.ONE);
|
||||||
|
|
||||||
|
// Mint some more blocks
|
||||||
|
for (int i = 0; i < 10; ++i)
|
||||||
|
BlockUtils.mintBlock(repository);
|
||||||
|
|
||||||
|
// Send more QORT to recipient
|
||||||
|
BigDecimal amount = BigDecimal.valueOf(random.nextInt(123456));
|
||||||
|
pay(repository, sendingAccount, recipientAccount, amount);
|
||||||
|
|
||||||
|
// Mint some more blocks
|
||||||
|
for (int i = 0; i < 10; ++i)
|
||||||
|
BlockUtils.mintBlock(repository);
|
||||||
|
|
||||||
|
// Confirm recipient balance is as expected
|
||||||
|
BigDecimal totalAmount = amount.add(BigDecimal.ONE);
|
||||||
|
balance = recipientAccount.getConfirmedBalance(Asset.QORT);
|
||||||
|
assertTrue("recipient's balance incorrect", balance.compareTo(totalAmount) == 0);
|
||||||
|
|
||||||
|
// Confirm balance as of 2 blocks ago
|
||||||
|
int height = repository.getBlockRepository().getBlockchainHeight();
|
||||||
|
balance = repository.getAccountRepository().getBalance(recipientAccount.getAddress(), Asset.QORT, height - 2).getBalance();
|
||||||
|
assertTrue("recipient's historic balance incorrect", balance.compareTo(totalAmount) == 0);
|
||||||
|
|
||||||
|
// Confirm balance prior to last payment
|
||||||
|
balance = repository.getAccountRepository().getBalance(recipientAccount.getAddress(), Asset.QORT, height - 15).getBalance();
|
||||||
|
assertTrue("recipient's historic balance incorrect", balance.compareTo(BigDecimal.ONE) == 0);
|
||||||
|
|
||||||
|
// Orphan blocks to before last payment
|
||||||
|
BlockUtils.orphanBlocks(repository, 10 + 5);
|
||||||
|
|
||||||
|
// Re-check balance from (now) invalid height
|
||||||
|
AccountBalanceData accountBalanceData = repository.getAccountRepository().getBalance(recipientAccount.getAddress(), Asset.QORT, height - 2);
|
||||||
|
balance = accountBalanceData.getBalance();
|
||||||
|
assertTrue("recipient's invalid-height balance should be one", balance.compareTo(BigDecimal.ONE) == 0);
|
||||||
|
|
||||||
|
// Orphan blocks to before initial 1 QORT payment
|
||||||
|
BlockUtils.orphanBlocks(repository, 10 + 5);
|
||||||
|
|
||||||
|
// Re-check balance from (now) invalid height
|
||||||
|
accountBalanceData = repository.getAccountRepository().getBalance(recipientAccount.getAddress(), Asset.QORT, height - 2);
|
||||||
|
assertNull("recipient's invalid-height balance data should be null", accountBalanceData);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private void pay(Repository repository, PrivateKeyAccount sendingAccount, Account recipientAccount, BigDecimal amount) throws DataException {
|
||||||
|
byte[] reference = sendingAccount.getLastReference();
|
||||||
|
long timestamp = repository.getTransactionRepository().fromSignature(reference).getTimestamp() + 1;
|
||||||
|
|
||||||
|
int txGroupId = 0;
|
||||||
|
BigDecimal fee = BlockChain.getInstance().getUnitFee();
|
||||||
|
BaseTransactionData baseTransactionData = new BaseTransactionData(timestamp, txGroupId, reference, sendingAccount.getPublicKey(), fee, null);
|
||||||
|
TransactionData transactionData = new PaymentTransactionData(baseTransactionData, recipientAccount.getAddress(), amount);
|
||||||
|
|
||||||
|
TransactionUtils.signAndMint(repository, transactionData, sendingAccount);
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
@ -1,5 +1,7 @@
|
|||||||
package org.qora.test.minting;
|
package org.qora.test.minting;
|
||||||
|
|
||||||
|
import static org.junit.Assert.*;
|
||||||
|
|
||||||
import java.math.BigDecimal;
|
import java.math.BigDecimal;
|
||||||
import java.math.RoundingMode;
|
import java.math.RoundingMode;
|
||||||
import java.util.List;
|
import java.util.List;
|
||||||
@ -12,6 +14,7 @@ import org.qora.account.PrivateKeyAccount;
|
|||||||
import org.qora.asset.Asset;
|
import org.qora.asset.Asset;
|
||||||
import org.qora.block.BlockChain;
|
import org.qora.block.BlockChain;
|
||||||
import org.qora.block.BlockChain.RewardByHeight;
|
import org.qora.block.BlockChain.RewardByHeight;
|
||||||
|
import org.qora.data.account.AccountBalanceData;
|
||||||
import org.qora.block.BlockMinter;
|
import org.qora.block.BlockMinter;
|
||||||
import org.qora.repository.DataException;
|
import org.qora.repository.DataException;
|
||||||
import org.qora.repository.Repository;
|
import org.qora.repository.Repository;
|
||||||
@ -105,14 +108,47 @@ public class RewardTests extends Common {
|
|||||||
BigDecimal qoraPerQort = BlockChain.getInstance().getQoraPerQortReward();
|
BigDecimal qoraPerQort = BlockChain.getInstance().getQoraPerQortReward();
|
||||||
|
|
||||||
try (final Repository repository = RepositoryManager.getRepository()) {
|
try (final Repository repository = RepositoryManager.getRepository()) {
|
||||||
Map<String, Map<Long, BigDecimal>> initialBalances = AccountUtils.getBalances(repository, Asset.QORT, Asset.QORT_FROM_QORA);
|
Map<String, Map<Long, BigDecimal>> initialBalances = AccountUtils.getBalances(repository, Asset.QORT, Asset.LEGACY_QORA, Asset.QORT_FROM_QORA);
|
||||||
|
|
||||||
BigDecimal blockReward = BlockUtils.getNextBlockReward(repository);
|
BigDecimal blockReward = BlockUtils.getNextBlockReward(repository);
|
||||||
|
|
||||||
|
// Fetch all legacy QORA holder balances
|
||||||
|
List<AccountBalanceData> qoraHolders = repository.getAccountRepository().getAssetBalances(Asset.LEGACY_QORA, true);
|
||||||
|
BigDecimal totalQoraHeld = BigDecimal.ZERO.setScale(8);
|
||||||
|
for (AccountBalanceData accountBalanceData : qoraHolders)
|
||||||
|
totalQoraHeld = totalQoraHeld.add(accountBalanceData.getBalance());
|
||||||
|
|
||||||
BlockUtils.mintBlock(repository);
|
BlockUtils.mintBlock(repository);
|
||||||
|
|
||||||
|
/*
|
||||||
|
* Example:
|
||||||
|
*
|
||||||
|
* Block reward is 100 QORT, QORA-holders' share is 0.20 (20%) = 20 QORT
|
||||||
|
*
|
||||||
|
* We hold 100 QORA
|
||||||
|
* Someone else holds 28 QORA
|
||||||
|
* Total QORA held: 128 QORA
|
||||||
|
*
|
||||||
|
* Our portion of that is 100 QORA / 128 QORA * 20 QORT = 15.625 QORT
|
||||||
|
*
|
||||||
|
* QORA holders earn at most 1 QORT per 250 QORA held.
|
||||||
|
*
|
||||||
|
* So we can earn at most 100 QORA / 250 QORAperQORT = 0.4 QORT
|
||||||
|
*
|
||||||
|
* Thus our block earning should be capped to 0.4 QORT.
|
||||||
|
*/
|
||||||
|
|
||||||
// Expected reward
|
// Expected reward
|
||||||
BigDecimal expectedReward = blockReward.multiply(qoraHoldersShare).divide(qoraPerQort, RoundingMode.DOWN);
|
BigDecimal qoraHoldersReward = blockReward.multiply(qoraHoldersShare);
|
||||||
|
assertTrue("QORA-holders share of block reward should be less than total block reward", qoraHoldersReward.compareTo(blockReward) < 0);
|
||||||
|
|
||||||
|
BigDecimal ourQoraHeld = initialBalances.get("chloe").get(Asset.LEGACY_QORA);
|
||||||
|
BigDecimal ourQoraReward = qoraHoldersReward.multiply(ourQoraHeld).divide(totalQoraHeld, RoundingMode.DOWN).setScale(8, RoundingMode.DOWN);
|
||||||
|
assertTrue("Our QORA-related reward should be less than total QORA-holders share of block reward", ourQoraReward.compareTo(qoraHoldersReward) < 0);
|
||||||
|
|
||||||
|
BigDecimal ourQortFromQoraCap = ourQoraHeld.divide(qoraPerQort, RoundingMode.DOWN);
|
||||||
|
|
||||||
|
BigDecimal expectedReward = ourQoraReward.min(ourQortFromQoraCap);
|
||||||
AccountUtils.assertBalance(repository, "chloe", Asset.QORT, initialBalances.get("chloe").get(Asset.QORT).add(expectedReward));
|
AccountUtils.assertBalance(repository, "chloe", Asset.QORT, initialBalances.get("chloe").get(Asset.QORT).add(expectedReward));
|
||||||
|
|
||||||
AccountUtils.assertBalance(repository, "chloe", Asset.QORT_FROM_QORA, initialBalances.get("chloe").get(Asset.QORT_FROM_QORA).add(expectedReward));
|
AccountUtils.assertBalance(repository, "chloe", Asset.QORT_FROM_QORA, initialBalances.get("chloe").get(Asset.QORT_FROM_QORA).add(expectedReward));
|
||||||
|
@ -53,7 +53,8 @@
|
|||||||
{ "type": "GENESIS", "recipient": "QaUpHNhT3Ygx6avRiKobuLdusppR5biXjL", "amount": "1000000" },
|
{ "type": "GENESIS", "recipient": "QaUpHNhT3Ygx6avRiKobuLdusppR5biXjL", "amount": "1000000" },
|
||||||
{ "type": "GENESIS", "recipient": "Qci5m9k4rcwe4ruKrZZQKka4FzUUMut3er", "amount": "1000000" },
|
{ "type": "GENESIS", "recipient": "Qci5m9k4rcwe4ruKrZZQKka4FzUUMut3er", "amount": "1000000" },
|
||||||
|
|
||||||
{ "type": "GENESIS", "recipient": "QaUpHNhT3Ygx6avRiKobuLdusppR5biXjL", "amount": "100", "assetId": 1 },
|
{ "type": "GENESIS", "recipient": "QaUpHNhT3Ygx6avRiKobuLdusppR5biXjL", "amount": "10000", "assetId": 1 },
|
||||||
|
{ "type": "GENESIS", "recipient": "Qci5m9k4rcwe4ruKrZZQKka4FzUUMut3er", "amount": "8", "assetId": 1 },
|
||||||
|
|
||||||
{ "type": "CREATE_GROUP", "creatorPublicKey": "2tiMr5LTpaWCgbRvkPK8TFd7k63DyHJMMFFsz9uBf1ZP", "owner": "QgV4s3xnzLhVBEJxcYui4u4q11yhUHsd9v", "groupName": "dev-group", "description": "developer group", "isOpen": false, "approvalThreshold": "PCT100", "minimumBlockDelay": 0, "maximumBlockDelay": 1440 },
|
{ "type": "CREATE_GROUP", "creatorPublicKey": "2tiMr5LTpaWCgbRvkPK8TFd7k63DyHJMMFFsz9uBf1ZP", "owner": "QgV4s3xnzLhVBEJxcYui4u4q11yhUHsd9v", "groupName": "dev-group", "description": "developer group", "isOpen": false, "approvalThreshold": "PCT100", "minimumBlockDelay": 0, "maximumBlockDelay": 1440 },
|
||||||
|
|
||||||
|
Loading…
x
Reference in New Issue
Block a user