diff --git a/src/main/java/org/qortal/api/resource/AssetsResource.java b/src/main/java/org/qortal/api/resource/AssetsResource.java index 6de01434..49ed251a 100644 --- a/src/main/java/org/qortal/api/resource/AssetsResource.java +++ b/src/main/java/org/qortal/api/resource/AssetsResource.java @@ -283,7 +283,7 @@ public class AssetsResource { Optional recorder = HSQLDBBalanceRecorder.getInstance(); if( recorder.isPresent()) { - Optional addressAmounts = recorder.get().getAddressAmounts(new BlockHeightRange(begin, end)); + Optional addressAmounts = recorder.get().getAddressAmounts(new BlockHeightRange(begin, end, false)); if( addressAmounts.isPresent() ) { return addressAmounts.get().getAmounts().stream() diff --git a/src/main/java/org/qortal/data/account/BlockHeightRange.java b/src/main/java/org/qortal/data/account/BlockHeightRange.java index fd356cb3..0ddb60bf 100644 --- a/src/main/java/org/qortal/data/account/BlockHeightRange.java +++ b/src/main/java/org/qortal/data/account/BlockHeightRange.java @@ -12,12 +12,15 @@ public class BlockHeightRange { private int end; + private boolean isRewardDistribution; + public BlockHeightRange() { } - public BlockHeightRange(int begin, int end) { + public BlockHeightRange(int begin, int end, boolean isRewardDistribution) { this.begin = begin; this.end = end; + this.isRewardDistribution = isRewardDistribution; } public int getBegin() { @@ -28,6 +31,10 @@ public class BlockHeightRange { return end; } + public boolean isRewardDistribution() { + return isRewardDistribution; + } + @Override public boolean equals(Object o) { if (this == o) return true; @@ -46,6 +53,7 @@ public class BlockHeightRange { return "BlockHeightRange{" + "begin=" + begin + ", end=" + end + + ", isRewardDistribution=" + isRewardDistribution + '}'; } } diff --git a/src/main/java/org/qortal/repository/hsqldb/HSQLDBCacheUtils.java b/src/main/java/org/qortal/repository/hsqldb/HSQLDBCacheUtils.java index f4d33b87..6e8dc8a8 100644 --- a/src/main/java/org/qortal/repository/hsqldb/HSQLDBCacheUtils.java +++ b/src/main/java/org/qortal/repository/hsqldb/HSQLDBCacheUtils.java @@ -3,6 +3,7 @@ package org.qortal.repository.hsqldb; import org.apache.logging.log4j.LogManager; import org.apache.logging.log4j.Logger; import org.qortal.api.SearchMode; +import org.qortal.api.resource.TransactionsResource; import org.qortal.arbitrary.misc.Category; import org.qortal.arbitrary.misc.Service; import org.qortal.controller.Controller; @@ -14,7 +15,10 @@ import org.qortal.data.arbitrary.ArbitraryResourceCache; import org.qortal.data.arbitrary.ArbitraryResourceData; import org.qortal.data.arbitrary.ArbitraryResourceMetadata; import org.qortal.data.arbitrary.ArbitraryResourceStatus; +import org.qortal.data.transaction.TransactionData; import org.qortal.repository.DataException; +import org.qortal.repository.Repository; +import org.qortal.repository.RepositoryManager; import org.qortal.settings.Settings; import org.qortal.utils.BalanceRecorderUtils; @@ -434,37 +438,11 @@ public class HSQLDBCacheUtils { // if there is a prior height if(priorHeight.isPresent()) { - BlockHeightRange blockHeightRange = new BlockHeightRange(priorHeight.get(), currentHeight); + boolean isRewardDistribution = BalanceRecorderUtils.isRewardDistributionRange(priorHeight.get(), currentHeight); - LOGGER.debug("building dynamics for block heights: range = " + blockHeightRange); - - List currentBalances = balancesByHeight.get(currentHeight); - - List currentDynamics - = BalanceRecorderUtils.buildBalanceDynamics( - currentBalances, - balancesByHeight.get(priorHeight.get()), - Settings.getInstance().getMinimumBalanceRecording()); - - LOGGER.debug("dynamics built: count = " + currentDynamics.size()); - - if(LOGGER.isDebugEnabled()) - currentDynamics.stream() - .sorted(Comparator.comparingLong(AddressAmountData::getAmount).reversed()) - .limit(Settings.getInstance().getTopBalanceLoggingLimit()) - .forEach(top5Dynamic -> LOGGER.debug("Top Dynamics = " + top5Dynamic)); - - BlockHeightRangeAddressAmounts amounts - = new BlockHeightRangeAddressAmounts( blockHeightRange, currentDynamics ); - - balanceDynamics.add(amounts); - - BalanceRecorderUtils.removeRecordingsBelowHeight(currentHeight - Settings.getInstance().getBalanceRecorderRollbackAllowance(), balancesByHeight); - - while(balanceDynamics.size() > capacity) { - BlockHeightRangeAddressAmounts oldestDynamics = BalanceRecorderUtils.removeOldestDynamics(balanceDynamics); - - LOGGER.debug("removing oldest dynamics: range " + oldestDynamics.getRange()); + // if this range has a reward recording block or if other blocks are enabled for recording + if( isRewardDistribution || !Settings.getInstance().isRewardRecordingOnly() ) { + produceBalanceDynamics(currentHeight, priorHeight, isRewardDistribution, balancesByHeight, balanceDynamics, capacity); } } else { @@ -482,6 +460,69 @@ public class HSQLDBCacheUtils { timer.scheduleAtFixedRate(task, 300_000, frequency * 60_000); } + private static void produceBalanceDynamics(int currentHeight, Optional priorHeight, boolean isRewardDistribution, ConcurrentHashMap> balancesByHeight, CopyOnWriteArrayList balanceDynamics, int capacity) { + BlockHeightRange blockHeightRange = new BlockHeightRange(priorHeight.get(), currentHeight, isRewardDistribution); + + LOGGER.debug("building dynamics for block heights: range = " + blockHeightRange); + + List currentBalances = balancesByHeight.get(currentHeight); + + ArrayList transactions = getTransactionDataForBlocks(blockHeightRange); + + LOGGER.info("transactions counted for balance adjustments: count = " + transactions.size()); + List currentDynamics + = BalanceRecorderUtils.buildBalanceDynamics( + currentBalances, + balancesByHeight.get(priorHeight.get()), + Settings.getInstance().getMinimumBalanceRecording(), + transactions); + + LOGGER.debug("dynamics built: count = " + currentDynamics.size()); + + if(LOGGER.isDebugEnabled()) + currentDynamics.stream() + .sorted(Comparator.comparingLong(AddressAmountData::getAmount).reversed()) + .limit(Settings.getInstance().getTopBalanceLoggingLimit()) + .forEach(top5Dynamic -> LOGGER.debug("Top Dynamics = " + top5Dynamic)); + + BlockHeightRangeAddressAmounts amounts + = new BlockHeightRangeAddressAmounts( blockHeightRange, currentDynamics ); + + balanceDynamics.add(amounts); + + BalanceRecorderUtils.removeRecordingsBelowHeight(currentHeight - Settings.getInstance().getBalanceRecorderRollbackAllowance(), balancesByHeight); + + while(balanceDynamics.size() > capacity) { + BlockHeightRangeAddressAmounts oldestDynamics = BalanceRecorderUtils.removeOldestDynamics(balanceDynamics); + + LOGGER.debug("removing oldest dynamics: range " + oldestDynamics.getRange()); + } + } + + private static ArrayList getTransactionDataForBlocks(BlockHeightRange blockHeightRange) { + ArrayList transactions; + + try (final Repository repository = RepositoryManager.getRepository()) { + List signatures + = repository.getTransactionRepository().getSignaturesMatchingCriteria( + blockHeightRange.getBegin() + 1, blockHeightRange.getEnd() - blockHeightRange.getBegin(), + null, null,null, null, null, + TransactionsResource.ConfirmationStatus.CONFIRMED, + null, null, null); + + transactions = new ArrayList<>(signatures.size()); + for (byte[] signature : signatures) { + transactions.add(repository.getTransactionRepository().fromSignature(signature)); + } + + LOGGER.debug(String.format("Found %s transactions for " + blockHeightRange, transactions.size())); + } catch (Exception e) { + transactions = new ArrayList<>(0); + LOGGER.warn("Problems getting transactions for balance recording: " + e.getMessage()); + } + return transactions; + } + private static int recordCurrentBalances(ConcurrentHashMap> balancesByHeight) { int currentHeight; diff --git a/src/main/java/org/qortal/settings/Settings.java b/src/main/java/org/qortal/settings/Settings.java index 5222f22e..deee0075 100644 --- a/src/main/java/org/qortal/settings/Settings.java +++ b/src/main/java/org/qortal/settings/Settings.java @@ -494,7 +494,14 @@ public class Settings { */ private int balanceRecorderRollbackAllowance = 100; - // Domain mapping + /** + * Is Reward Recording Only + * + * Set true to only retain the recordings that cover reward distributions, otherwise set false. + */ + private boolean rewardRecordingOnly = true; + + // Domain mapping public static class ThreadLimit { private String messageType; private Integer limit; @@ -1311,4 +1318,8 @@ public class Settings { public int getBalanceRecorderRollbackAllowance() { return balanceRecorderRollbackAllowance; } + + public boolean isRewardRecordingOnly() { + return rewardRecordingOnly; + } } diff --git a/src/main/java/org/qortal/utils/BalanceRecorderUtils.java b/src/main/java/org/qortal/utils/BalanceRecorderUtils.java index 44d8f5e8..8ad346ac 100644 --- a/src/main/java/org/qortal/utils/BalanceRecorderUtils.java +++ b/src/main/java/org/qortal/utils/BalanceRecorderUtils.java @@ -1,13 +1,26 @@ package org.qortal.utils; +import org.qortal.block.Block; +import org.qortal.crypto.Crypto; +import org.qortal.data.PaymentData; import org.qortal.data.account.AccountBalanceData; import org.qortal.data.account.AddressAmountData; import org.qortal.data.account.BlockHeightRange; import org.qortal.data.account.BlockHeightRangeAddressAmounts; +import org.qortal.data.transaction.ATTransactionData; +import org.qortal.data.transaction.BaseTransactionData; +import org.qortal.data.transaction.BuyNameTransactionData; +import org.qortal.data.transaction.CreateAssetOrderTransactionData; +import org.qortal.data.transaction.DeployAtTransactionData; +import org.qortal.data.transaction.MultiPaymentTransactionData; +import org.qortal.data.transaction.PaymentTransactionData; +import org.qortal.data.transaction.TransactionData; +import org.qortal.data.transaction.TransferAssetTransactionData; -import java.util.ArrayList; import java.util.Comparator; +import java.util.HashMap; import java.util.List; +import java.util.Map; import java.util.Optional; import java.util.concurrent.ConcurrentHashMap; import java.util.concurrent.CopyOnWriteArrayList; @@ -67,22 +80,190 @@ public class BalanceRecorderUtils { } } - public static List buildBalanceDynamics(final List balances, final List priorBalances, long minimum) { + public static List buildBalanceDynamics( + final List balances, + final List priorBalances, + long minimum, + List transactions) { - List addressAmounts = new ArrayList<>(balances.size()); + Map amountsByAddress = new HashMap<>(transactions.size()); - // prior balance - addressAmounts.addAll( - balances.stream() + for( TransactionData transactionData : transactions ) { + + mapBalanceModificationsForTransaction(amountsByAddress, transactionData); + } + + List addressAmounts + = balances.stream() .map(balance -> buildBalanceDynamicsForAccount(priorBalances, balance)) + .map( data -> adjustAddressAmount(amountsByAddress.getOrDefault(data.getAddress(), 0L), data)) .filter(ADDRESS_AMOUNT_DATA_NOT_ZERO) - .filter( data -> data.getAmount() >= minimum) - .collect(Collectors.toList()) - ); + .filter(data -> data.getAmount() >= minimum) + .collect(Collectors.toList()); return addressAmounts; } + public static AddressAmountData adjustAddressAmount(long adjustment, AddressAmountData data) { + + return new AddressAmountData(data.getAddress(), data.getAmount() - adjustment); + } + + public static void mapBalanceModificationsForTransaction(Map amountsByAddress, TransactionData transactionData) { + String creatorAddress; + + // AT Transaction + if( transactionData instanceof ATTransactionData) { + creatorAddress = mapBalanceModificationsForAtTransaction(amountsByAddress, (ATTransactionData) transactionData); + } + // Buy Name Transaction + else if( transactionData instanceof BuyNameTransactionData) { + creatorAddress = mapBalanceModificationsForBuyNameTransaction(amountsByAddress, (BuyNameTransactionData) transactionData); + } + // Create Asset Order Transaction + else if( transactionData instanceof CreateAssetOrderTransactionData) { + //TODO I'm not sure how to handle this one. This hasn't been used at this point in the blockchain. + + creatorAddress = Crypto.toAddress(transactionData.getCreatorPublicKey()); + } + // Deploy AT Transaction + else if( transactionData instanceof DeployAtTransactionData ) { + creatorAddress = mapBalanceModificationsForDeployAtTransaction(amountsByAddress, (DeployAtTransactionData) transactionData); + } + // Multi Payment Transaction + else if( transactionData instanceof MultiPaymentTransactionData) { + creatorAddress = mapBalanceModificationsForMultiPaymentTransaction(amountsByAddress, (MultiPaymentTransactionData) transactionData); + } + // Payment Transaction + else if( transactionData instanceof PaymentTransactionData ) { + creatorAddress = mapBalanceModicationsForPaymentTransaction(amountsByAddress, (PaymentTransactionData) transactionData); + } + // Transfer Asset Transaction + else if( transactionData instanceof TransferAssetTransactionData) { + creatorAddress = mapBalanceModificationsForTransferAssetTransaction(amountsByAddress, (TransferAssetTransactionData) transactionData); + } + // Other Transactions + else { + creatorAddress = Crypto.toAddress(transactionData.getCreatorPublicKey()); + } + + // all transactions modify the balance for fees + mapBalanceModifications(amountsByAddress, transactionData.getFee(), creatorAddress, Optional.empty()); + } + + public static String mapBalanceModificationsForTransferAssetTransaction(Map amountsByAddress, TransferAssetTransactionData transferAssetData) { + String creatorAddress = Crypto.toAddress(transferAssetData.getSenderPublicKey()); + + if( transferAssetData.getAssetId() == 0) { + mapBalanceModifications( + amountsByAddress, + transferAssetData.getAmount(), + creatorAddress, + Optional.of(transferAssetData.getRecipient()) + ); + } + return creatorAddress; + } + + public static String mapBalanceModicationsForPaymentTransaction(Map amountsByAddress, PaymentTransactionData paymentData) { + String creatorAddress = Crypto.toAddress(paymentData.getCreatorPublicKey()); + + mapBalanceModifications(amountsByAddress, + paymentData.getAmount(), + creatorAddress, + Optional.of(paymentData.getRecipient()) + ); + return creatorAddress; + } + + public static String mapBalanceModificationsForMultiPaymentTransaction(Map amountsByAddress, MultiPaymentTransactionData multiPaymentData) { + String creatorAddress = Crypto.toAddress(multiPaymentData.getCreatorPublicKey()); + + for(PaymentData payment : multiPaymentData.getPayments() ) { + mapBalanceModificationsForTransaction( + amountsByAddress, + getPaymentTransactionData(multiPaymentData, payment) + ); + } + return creatorAddress; + } + + public static String mapBalanceModificationsForDeployAtTransaction(Map amountsByAddress, DeployAtTransactionData transactionData) { + String creatorAddress; + DeployAtTransactionData deployAtData = transactionData; + + creatorAddress = Crypto.toAddress(deployAtData.getCreatorPublicKey()); + + if( deployAtData.getAssetId() == 0 ) { + mapBalanceModifications( + amountsByAddress, + deployAtData.getAmount(), + creatorAddress, + Optional.of(deployAtData.getAtAddress()) + ); + } + return creatorAddress; + } + + public static String mapBalanceModificationsForBuyNameTransaction(Map amountsByAddress, BuyNameTransactionData transactionData) { + String creatorAddress; + BuyNameTransactionData buyNameData = transactionData; + + creatorAddress = Crypto.toAddress(buyNameData.getCreatorPublicKey()); + + mapBalanceModifications( + amountsByAddress, + buyNameData.getAmount(), + creatorAddress, + Optional.of(buyNameData.getSeller()) + ); + return creatorAddress; + } + + public static String mapBalanceModificationsForAtTransaction(Map amountsByAddress, ATTransactionData transactionData) { + String creatorAddress; + ATTransactionData atData = transactionData; + creatorAddress = atData.getATAddress(); + + if( atData.getAssetId() != null && atData.getAssetId() == 0) { + mapBalanceModifications( + amountsByAddress, + atData.getAmount(), + creatorAddress, + Optional.of(atData.getRecipient()) + ); + } + return creatorAddress; + } + + public static PaymentTransactionData getPaymentTransactionData(MultiPaymentTransactionData multiPaymentData, PaymentData payment) { + return new PaymentTransactionData( + new BaseTransactionData( + multiPaymentData.getTimestamp(), + multiPaymentData.getTxGroupId(), + multiPaymentData.getReference(), + multiPaymentData.getCreatorPublicKey(), + 0L, + multiPaymentData.getSignature() + ), + payment.getRecipient(), + payment.getAmount() + ); + } + + public static void mapBalanceModifications(Map amountsByAddress, Long amount, String sender, Optional recipient) { + amountsByAddress.put( + sender, + amountsByAddress.getOrDefault(sender, 0L) - amount + ); + + if( recipient.isPresent() ) + amountsByAddress.put( + recipient.get(), + amountsByAddress.getOrDefault(recipient.get(), 0L) + amount + ); + } + public static void removeRecordingsAboveHeight(int currentHeight, ConcurrentHashMap> balancesByHeight) { balancesByHeight.entrySet().stream() .filter(heightWithBalances -> heightWithBalances.getKey() > currentHeight) @@ -116,4 +297,23 @@ public class BalanceRecorderUtils { .sorted(Comparator.reverseOrder()).findFirst(); return priorHeight; } + + /** + * Is Reward Distribution Range? + * + * @param start start height, exclusive + * @param end end height, inclusive + * + * @return true there is a reward distribution block within this block range + */ + public static boolean isRewardDistributionRange(int start, int end) { + + // iterate through the block height until a reward distribution block or the end of the range + for( int i = start + 1; i <= end; i++) { + if( Block.isRewardDistributionBlock(i) ) return true; + } + + // no reward distribution blocks found within range + return false; + } } diff --git a/src/test/java/org/qortal/test/utils/BalanceRecorderUtilsTests.java b/src/test/java/org/qortal/test/utils/BalanceRecorderUtilsTests.java index 746c845f..0a35657a 100644 --- a/src/test/java/org/qortal/test/utils/BalanceRecorderUtilsTests.java +++ b/src/test/java/org/qortal/test/utils/BalanceRecorderUtilsTests.java @@ -2,13 +2,27 @@ package org.qortal.test.utils; import org.junit.Assert; import org.junit.Test; +import org.qortal.asset.Asset; +import org.qortal.block.Block; +import org.qortal.crypto.Crypto; +import org.qortal.data.PaymentData; import org.qortal.data.account.AccountBalanceData; import org.qortal.data.account.AddressAmountData; import org.qortal.data.account.BlockHeightRange; import org.qortal.data.account.BlockHeightRangeAddressAmounts; +import org.qortal.data.transaction.ATTransactionData; +import org.qortal.data.transaction.BaseTransactionData; +import org.qortal.data.transaction.BuyNameTransactionData; +import org.qortal.data.transaction.DeployAtTransactionData; +import org.qortal.data.transaction.MultiPaymentTransactionData; +import org.qortal.data.transaction.PaymentTransactionData; +import org.qortal.data.transaction.RegisterNameTransactionData; +import org.qortal.data.transaction.TransactionData; +import org.qortal.data.transaction.TransferAssetTransactionData; import org.qortal.utils.BalanceRecorderUtils; import java.util.ArrayList; +import java.util.HashMap; import java.util.List; import java.util.Map; import java.util.Optional; @@ -18,6 +32,10 @@ import java.util.stream.Collectors; public class BalanceRecorderUtilsTests { + public static final String RECIPIENT_ADDRESS = "recipient"; + public static final String AT_ADDRESS = "atAddress"; + public static final String OTHER = "Other"; + @Test public void testNotZeroForZero() { boolean test = BalanceRecorderUtils.ADDRESS_AMOUNT_DATA_NOT_ZERO.test( new AddressAmountData("", 0)); @@ -42,8 +60,8 @@ public class BalanceRecorderUtilsTests { @Test public void testAddressAmountComparatorReverseOrder() { - BlockHeightRangeAddressAmounts addressAmounts1 = new BlockHeightRangeAddressAmounts(new BlockHeightRange(2, 3), new ArrayList<>(0)); - BlockHeightRangeAddressAmounts addressAmounts2 = new BlockHeightRangeAddressAmounts(new BlockHeightRange(1, 2), new ArrayList<>(0)); + BlockHeightRangeAddressAmounts addressAmounts1 = new BlockHeightRangeAddressAmounts(new BlockHeightRange(2, 3, false), new ArrayList<>(0)); + BlockHeightRangeAddressAmounts addressAmounts2 = new BlockHeightRangeAddressAmounts(new BlockHeightRange(1, 2, false), new ArrayList<>(0)); int compare = BalanceRecorderUtils.BLOCK_HEIGHT_RANGE_ADDRESS_AMOUNTS_COMPARATOR.compare(addressAmounts1, addressAmounts2); @@ -53,8 +71,8 @@ public class BalanceRecorderUtilsTests { @Test public void testAddressAmountComparatorForwardOrder() { - BlockHeightRangeAddressAmounts addressAmounts1 = new BlockHeightRangeAddressAmounts(new BlockHeightRange(1, 2), new ArrayList<>(0)); - BlockHeightRangeAddressAmounts addressAmounts2 = new BlockHeightRangeAddressAmounts(new BlockHeightRange(2, 3), new ArrayList<>(0)); + BlockHeightRangeAddressAmounts addressAmounts1 = new BlockHeightRangeAddressAmounts(new BlockHeightRange(1, 2, false), new ArrayList<>(0)); + BlockHeightRangeAddressAmounts addressAmounts2 = new BlockHeightRangeAddressAmounts(new BlockHeightRange(2, 3, false), new ArrayList<>(0)); int compare = BalanceRecorderUtils.BLOCK_HEIGHT_RANGE_ADDRESS_AMOUNTS_COMPARATOR.compare(addressAmounts1, addressAmounts2); @@ -124,7 +142,7 @@ public class BalanceRecorderUtilsTests { List priorBalances = new ArrayList<>(1); priorBalances.add(new AccountBalanceData(address, 0, 1)); - List dynamics = BalanceRecorderUtils.buildBalanceDynamics(balances, priorBalances, 0); + List dynamics = BalanceRecorderUtils.buildBalanceDynamics(balances, priorBalances, 0, new ArrayList<>(0)); Assert.assertNotNull(dynamics); Assert.assertEquals(1, dynamics.size()); @@ -145,7 +163,7 @@ public class BalanceRecorderUtilsTests { List priorBalances = new ArrayList<>(0); - List dynamics = BalanceRecorderUtils.buildBalanceDynamics(balances, priorBalances, 0); + List dynamics = BalanceRecorderUtils.buildBalanceDynamics(balances, priorBalances, 0, new ArrayList<>(0)); Assert.assertNotNull(dynamics); Assert.assertEquals(1, dynamics.size()); @@ -156,6 +174,55 @@ public class BalanceRecorderUtilsTests { Assert.assertEquals(2, addressAmountData.getAmount()); } + @Test + public void testBuildBalanceDynamicOneAccountAdjustment() { + List balances = new ArrayList<>(1); + balances.add(new AccountBalanceData(RECIPIENT_ADDRESS, 0, 20)); + + List priorBalances = new ArrayList<>(0); + priorBalances.add(new AccountBalanceData(RECIPIENT_ADDRESS, 0, 12)); + + List transactions = new ArrayList<>(); + + final long amount = 5L; + final long fee = 1L; + + boolean exceptionThrown = false; + + try { + byte[] creatorPublicKey = TestUtils.generatePublicKey(); + + PaymentTransactionData paymentData + = new PaymentTransactionData( + new BaseTransactionData(0L, 0, null, creatorPublicKey, fee, null), + RECIPIENT_ADDRESS, + amount + ); + + transactions.add(paymentData); + + List dynamics + = BalanceRecorderUtils.buildBalanceDynamics( + balances, + priorBalances, + 0, + transactions + ); + + Assert.assertNotNull(dynamics); + Assert.assertEquals(1, dynamics.size()); + + AddressAmountData addressAmountData = dynamics.get(0); + Assert.assertNotNull(addressAmountData); + Assert.assertEquals(RECIPIENT_ADDRESS, addressAmountData.getAddress()); + Assert.assertEquals(3, addressAmountData.getAmount()); + } catch( Exception e ) { + exceptionThrown = true; + } + + Assert.assertFalse(exceptionThrown); + } + @Test public void testBuildBalanceDynamicsTwoAccountsNegativeValues() { @@ -170,7 +237,7 @@ public class BalanceRecorderUtilsTests { priorBalances.add(new AccountBalanceData(address2, 0, 200)); priorBalances.add(new AccountBalanceData(address1, 0, 5000)); - List dynamics = BalanceRecorderUtils.buildBalanceDynamics(balances, priorBalances, -100L); + List dynamics = BalanceRecorderUtils.buildBalanceDynamics(balances, priorBalances, -100L, new ArrayList<>(0)); Assert.assertNotNull(dynamics); Assert.assertEquals(2, dynamics.size()); @@ -304,10 +371,10 @@ public class BalanceRecorderUtilsTests { CopyOnWriteArrayList dynamics = new CopyOnWriteArrayList<>(); - BlockHeightRange range1 = new BlockHeightRange(10, 20); + BlockHeightRange range1 = new BlockHeightRange(10, 20, false); dynamics.add(new BlockHeightRangeAddressAmounts(range1, new ArrayList<>())); - BlockHeightRange range2 = new BlockHeightRange(1, 4); + BlockHeightRange range2 = new BlockHeightRange(1, 4, false); dynamics.add(new BlockHeightRangeAddressAmounts(range2, new ArrayList<>())); Assert.assertEquals(2, dynamics.size()); @@ -323,13 +390,13 @@ public class BalanceRecorderUtilsTests { CopyOnWriteArrayList dynamics = new CopyOnWriteArrayList<>(); - BlockHeightRange range1 = new BlockHeightRange(1,5); + BlockHeightRange range1 = new BlockHeightRange(1,5, false); dynamics.add(new BlockHeightRangeAddressAmounts(range1, new ArrayList<>())); - BlockHeightRange range2 = new BlockHeightRange(6, 11); + BlockHeightRange range2 = new BlockHeightRange(6, 11, false); dynamics.add((new BlockHeightRangeAddressAmounts(range2, new ArrayList<>()))); - BlockHeightRange range3 = new BlockHeightRange(22, 16); + BlockHeightRange range3 = new BlockHeightRange(22, 16, false); dynamics.add(new BlockHeightRangeAddressAmounts(range3, new ArrayList<>())); Assert.assertEquals(3, dynamics.size()); @@ -344,18 +411,353 @@ public class BalanceRecorderUtilsTests { public void testRemoveOldestDynamicsTwice() { CopyOnWriteArrayList dynamics = new CopyOnWriteArrayList<>(); - dynamics.add(new BlockHeightRangeAddressAmounts(new BlockHeightRange(1, 5), new ArrayList<>())); - dynamics.add(new BlockHeightRangeAddressAmounts(new BlockHeightRange(5, 9), new ArrayList<>())); + dynamics.add(new BlockHeightRangeAddressAmounts(new BlockHeightRange(1, 5, false), new ArrayList<>())); + dynamics.add(new BlockHeightRangeAddressAmounts(new BlockHeightRange(5, 9, false), new ArrayList<>())); Assert.assertEquals(2, dynamics.size()); BalanceRecorderUtils.removeOldestDynamics(dynamics); Assert.assertEquals(1, dynamics.size()); - Assert.assertTrue(dynamics.get(0).getRange().equals(new BlockHeightRange(5, 9))); + Assert.assertTrue(dynamics.get(0).getRange().equals(new BlockHeightRange(5, 9, false))); BalanceRecorderUtils.removeOldestDynamics(dynamics); Assert.assertEquals(0, dynamics.size()); } + + @Test + public void testMapBalanceModificationsForPaymentTransaction() { + + boolean exceptionThrown = false; + + try { + final long amount = 1L; + final long fee = 1L; + + byte[] creatorPublicKey = TestUtils.generatePublicKey(); + + PaymentTransactionData paymentData + = new PaymentTransactionData( + new BaseTransactionData(0L, 0, null, creatorPublicKey, fee, null), + RECIPIENT_ADDRESS, + amount + ); + + // map balance modifications for addresses in the transaction + Map amountsByAddress = new HashMap<>(); + BalanceRecorderUtils.mapBalanceModicationsForPaymentTransaction(amountsByAddress, paymentData); + + // this will not add the fee, that is done in a different place + assertAmountsByAddress(amountsByAddress, amount, creatorPublicKey, RECIPIENT_ADDRESS); + } catch (Exception e) { + exceptionThrown = true; + } + + Assert.assertFalse(exceptionThrown); + } + + @Test + public void testMapBalanceModificationsForAssetOrderTransaction() { + + boolean exceptionThrown = false; + + try{ + final long amount = 1L; + final long fee = 1L; + + byte[] creatorPublicKey = TestUtils.generatePublicKey(); + + TransferAssetTransactionData transferAssetData + = new TransferAssetTransactionData( + new BaseTransactionData(0L, 0, null, creatorPublicKey, fee, null), + RECIPIENT_ADDRESS, + amount, + 0 + ); + + // map balance modifications for addresses in the transaction + Map amountsByAddress = new HashMap<>(); + BalanceRecorderUtils.mapBalanceModificationsForTransferAssetTransaction(amountsByAddress, transferAssetData); + + assertAmountsByAddress(amountsByAddress, amount, creatorPublicKey, RECIPIENT_ADDRESS); + } catch( Exception e) { + exceptionThrown = true; + } + + Assert.assertFalse(exceptionThrown); + } + + @Test + public void testMapBalanceModificationsForATTransactionMessageType() { + + boolean exceptionThrown = false; + + try { + + final long fee = 1L; + + byte[] creatorPublicKey = TestUtils.generatePublicKey(); + Map amountsByAddress = new HashMap<>(); + + ATTransactionData atTransactionData = new ATTransactionData(new BaseTransactionData(0L, 0, null, creatorPublicKey, fee, null), + AT_ADDRESS, + RECIPIENT_ADDRESS, + new byte[0]); + BalanceRecorderUtils.mapBalanceModificationsForAtTransaction( amountsByAddress, atTransactionData); + + // no balance changes for AT message + Assert.assertTrue(amountsByAddress.size() == 0); + } catch( Exception e) { + exceptionThrown = true; + } + + Assert.assertFalse(exceptionThrown); + } + + @Test + public void testMapBalanceModificationsForATTransactionPaymentType() { + + boolean exceptionThrown = false; + + try{ + final long amount = 1L; + final long fee = 1L; + + byte[] creatorPublicKey = TestUtils.generatePublicKey(); + + Map amountsByAddress = new HashMap<>(); + + ATTransactionData atTransactionData + = new ATTransactionData( + new BaseTransactionData(0L, 0, null, creatorPublicKey, fee, null), + AT_ADDRESS, + RECIPIENT_ADDRESS, + amount, + 0 + ); + + BalanceRecorderUtils.mapBalanceModificationsForAtTransaction( amountsByAddress, atTransactionData); + + assertAmountByAddress(amountsByAddress, amount, RECIPIENT_ADDRESS); + + assertAmountByAddress(amountsByAddress, -amount, AT_ADDRESS); + } catch( Exception e) { + exceptionThrown = true; + } + + Assert.assertFalse(exceptionThrown); + } + + @Test + public void testMapBalanceModificationsForBuyNameTransaction() { + + boolean exceptionThrown = false; + + try{ + final long amount = 100L; + final long fee = 1L; + + byte[] creatorPublicKey = TestUtils.generatePublicKey(); + Map amountsByAddress = new HashMap<>(); + + BuyNameTransactionData buyNameData + = new BuyNameTransactionData( + new BaseTransactionData(0L, 0, null, creatorPublicKey, fee, null), + "null", + amount, + RECIPIENT_ADDRESS + ); + + BalanceRecorderUtils.mapBalanceModificationsForBuyNameTransaction(amountsByAddress, buyNameData); + + assertAmountsByAddress(amountsByAddress, amount, creatorPublicKey, RECIPIENT_ADDRESS); + } catch( Exception e) { + exceptionThrown = true; + } + + Assert.assertFalse(exceptionThrown); + } + + @Test + public void testMapBalanceModificationsForMultiPaymentTransaction() { + + boolean exceptionThrown = false; + + try{ + final long amount = 100L; + final long fee = 1L; + + byte[] creatorPublicKey = TestUtils.generatePublicKey(); + Map amountsByAddress = new HashMap<>(); + + List payments = new ArrayList<>(); + + payments.add(new PaymentData(RECIPIENT_ADDRESS, 0, amount)); + + MultiPaymentTransactionData multiPayment + = new MultiPaymentTransactionData(new BaseTransactionData(0L, 0, null, creatorPublicKey, fee, null), + payments); + BalanceRecorderUtils.mapBalanceModificationsForMultiPaymentTransaction(amountsByAddress,multiPayment); + assertAmountsByAddress(amountsByAddress, amount, creatorPublicKey, RECIPIENT_ADDRESS); + } catch( Exception e ) { + exceptionThrown = true; + } + + Assert.assertFalse(exceptionThrown); + } + + @Test + public void testMapBalanceModificationsForMultiPaymentTransaction2PaymentsOneAddress() { + + boolean exceptionThrown = false; + + try{ + final long amount = 100L; + final long fee = 1L; + + byte[] creatorPublicKey = TestUtils.generatePublicKey(); + Map amountsByAddress = new HashMap<>(); + + List payments = new ArrayList<>(); + + payments.add(new PaymentData(RECIPIENT_ADDRESS, 0, amount)); + payments.add(new PaymentData(RECIPIENT_ADDRESS, 0, amount)); + + MultiPaymentTransactionData multiPayment + = new MultiPaymentTransactionData(new BaseTransactionData(0L, 0, null, creatorPublicKey, fee, null), + payments); + BalanceRecorderUtils.mapBalanceModificationsForMultiPaymentTransaction(amountsByAddress,multiPayment); + assertAmountsByAddress(amountsByAddress, 2*amount, creatorPublicKey, RECIPIENT_ADDRESS); + } catch( Exception e ) { + exceptionThrown = true; + } + + Assert.assertFalse(exceptionThrown); + } + + @Test + public void testMapBalanceModificationsForMultiPaymentTransaction2PaymentsTwoAddresses() { + + boolean exceptionThrown = false; + + try{ + final long amount = 100L; + final long fee = 1L; + + byte[] creatorPublicKey = TestUtils.generatePublicKey(); + Map amountsByAddress = new HashMap<>(); + + List payments = new ArrayList<>(); + + payments.add(new PaymentData(RECIPIENT_ADDRESS, 0, amount)); + payments.add(new PaymentData(OTHER, 0, amount)); + + MultiPaymentTransactionData multiPayment + = new MultiPaymentTransactionData(new BaseTransactionData(0L, 0, null, creatorPublicKey, fee, null), + payments); + BalanceRecorderUtils.mapBalanceModificationsForMultiPaymentTransaction(amountsByAddress,multiPayment); + assertAmountByAddress(amountsByAddress, amount, RECIPIENT_ADDRESS); + assertAmountByAddress(amountsByAddress, amount, OTHER); + + String creatorAddress = Crypto.toAddress(creatorPublicKey); + + assertAmountByAddress(amountsByAddress, 2*-amount, creatorAddress); + } catch( Exception e ) { + exceptionThrown = true; + } + + Assert.assertFalse(exceptionThrown); + } + + @Test + public void testMapBalanceModificationsForDeployAtTransaction() { + + boolean exceptionThrown = false; + + try{ + final long amount = 3L; + final long fee = 1L; + + byte[] creatorPublicKey = TestUtils.generatePublicKey(); + Map amountsByAddress = new HashMap<>(); + + DeployAtTransactionData deployAt + = new DeployAtTransactionData( + new BaseTransactionData(0L, 0, null, creatorPublicKey, fee, null), + AT_ADDRESS, "name", "description", "type", "tags", new byte[0], amount, Asset.QORT + ); + + BalanceRecorderUtils.mapBalanceModificationsForDeployAtTransaction(amountsByAddress,deployAt); + assertAmountsByAddress(amountsByAddress, amount, creatorPublicKey, AT_ADDRESS); + } catch( Exception e) { + exceptionThrown = true; + e.printStackTrace(); + } + + Assert.assertFalse(exceptionThrown); + } + + @Test + public void testMapBalanceModificationsForTransaction() { + + boolean exceptionThrown = false; + + try { + final long fee = 2; + + byte[] creatorPublicKey = TestUtils.generatePublicKey(); + Map amountsByAddress = new HashMap<>(); + + BalanceRecorderUtils.mapBalanceModificationsForTransaction( + amountsByAddress, + new RegisterNameTransactionData( + new BaseTransactionData(0L, 0, null, creatorPublicKey, fee, null), + "aaa", "data", "aaa") + ); + + String creatorAddress = Crypto.toAddress(creatorPublicKey); + + assertAmountByAddress(amountsByAddress, -fee, creatorAddress); + } catch(Exception e) { + exceptionThrown = true; + } + + Assert.assertFalse(exceptionThrown); + } + + @Test + public void testBlockHeightRangeEqualityTrue() { + + BlockHeightRange range1 = new BlockHeightRange(2, 4, false); + BlockHeightRange range2 = new BlockHeightRange(2, 4, true); + + Assert.assertTrue(range1.equals(range2)); + Assert.assertEquals(range1, range2); + } + + @Test + public void testBloHeightRangeEqualityFalse() { + + BlockHeightRange range1 = new BlockHeightRange(2, 3, true); + BlockHeightRange range2 = new BlockHeightRange(2, 4, true); + + Assert.assertFalse(range1.equals(range2)); + } + + private static void assertAmountsByAddress(Map amountsByAddress, long amount, byte[] creatorPublicKey, String recipientAddress) { + assertAmountByAddress(amountsByAddress, amount, recipientAddress); + + String creatorAddress = Crypto.toAddress(creatorPublicKey); + + assertAmountByAddress(amountsByAddress, -amount, creatorAddress); + } + + private static void assertAmountByAddress(Map amountsByAddress, long amount, String address) { + Long amountForAddress = amountsByAddress.get(address); + + Assert.assertTrue(amountsByAddress.containsKey(address)); + Assert.assertNotNull(amountForAddress); + Assert.assertEquals(amount, amountForAddress.longValue()); + } } \ No newline at end of file diff --git a/src/test/java/org/qortal/test/utils/TestUtils.java b/src/test/java/org/qortal/test/utils/TestUtils.java new file mode 100644 index 00000000..b66591a0 --- /dev/null +++ b/src/test/java/org/qortal/test/utils/TestUtils.java @@ -0,0 +1,48 @@ +package org.qortal.test.utils; + +import org.bouncycastle.jce.provider.BouncyCastleProvider; + +import java.security.KeyPair; +import java.security.KeyPairGenerator; +import java.security.MessageDigest; +import java.security.PublicKey; +import java.security.Security; + +public class TestUtils { + public static byte[] generatePublicKey() throws Exception { + // Add the Bouncy Castle provider + Security.addProvider(new BouncyCastleProvider()); + + // Generate a key pair + KeyPair keyPair = generateKeyPair(); + + // Get the public key + PublicKey publicKey = keyPair.getPublic(); + + // Get the public key as a byte array + byte[] publicKeyBytes = publicKey.getEncoded(); + + // Generate a RIPEMD160 message digest from the public key + byte[] ripeMd160Digest = generateRipeMd160Digest(publicKeyBytes); + + return ripeMd160Digest; + } + + public static KeyPair generateKeyPair() throws Exception { + // Generate a key pair using the RSA algorithm + KeyPairGenerator keyGen = KeyPairGenerator.getInstance("RSA"); + keyGen.initialize(2048); // Key size (bits) + return keyGen.generateKeyPair(); + } + + public static byte[] generateRipeMd160Digest(byte[] input) throws Exception { + // Create a RIPEMD160 message digest instance + MessageDigest ripeMd160 = MessageDigest.getInstance("RIPEMD160", new BouncyCastleProvider()); + + // Update the message digest with the input bytes + ripeMd160.update(input); + + // Get the message digest bytes + return ripeMd160.digest(); + } +}