mirror of
https://github.com/Qortal/qortal.git
synced 2025-02-11 17:55:50 +00:00
Balance Recorder initial implementation.
This commit is contained in:
parent
543d0a7d22
commit
5e145de52b
@ -16,9 +16,13 @@ import org.qortal.api.model.AggregatedOrder;
|
|||||||
import org.qortal.api.model.TradeWithOrderInfo;
|
import org.qortal.api.model.TradeWithOrderInfo;
|
||||||
import org.qortal.api.resource.TransactionsResource.ConfirmationStatus;
|
import org.qortal.api.resource.TransactionsResource.ConfirmationStatus;
|
||||||
import org.qortal.asset.Asset;
|
import org.qortal.asset.Asset;
|
||||||
|
import org.qortal.controller.hsqldb.HSQLDBBalanceRecorder;
|
||||||
import org.qortal.crypto.Crypto;
|
import org.qortal.crypto.Crypto;
|
||||||
import org.qortal.data.account.AccountBalanceData;
|
import org.qortal.data.account.AccountBalanceData;
|
||||||
import org.qortal.data.account.AccountData;
|
import org.qortal.data.account.AccountData;
|
||||||
|
import org.qortal.data.account.AddressAmountData;
|
||||||
|
import org.qortal.data.account.BlockHeightRange;
|
||||||
|
import org.qortal.data.account.BlockHeightRangeAddressAmounts;
|
||||||
import org.qortal.data.asset.AssetData;
|
import org.qortal.data.asset.AssetData;
|
||||||
import org.qortal.data.asset.OrderData;
|
import org.qortal.data.asset.OrderData;
|
||||||
import org.qortal.data.asset.RecentTradeData;
|
import org.qortal.data.asset.RecentTradeData;
|
||||||
@ -33,6 +37,7 @@ import org.qortal.transaction.Transaction;
|
|||||||
import org.qortal.transaction.Transaction.ValidationResult;
|
import org.qortal.transaction.Transaction.ValidationResult;
|
||||||
import org.qortal.transform.TransformationException;
|
import org.qortal.transform.TransformationException;
|
||||||
import org.qortal.transform.transaction.*;
|
import org.qortal.transform.transaction.*;
|
||||||
|
import org.qortal.utils.BalanceRecorderUtils;
|
||||||
import org.qortal.utils.Base58;
|
import org.qortal.utils.Base58;
|
||||||
|
|
||||||
import javax.servlet.http.HttpServletRequest;
|
import javax.servlet.http.HttpServletRequest;
|
||||||
@ -42,6 +47,7 @@ import javax.ws.rs.core.MediaType;
|
|||||||
import java.util.ArrayList;
|
import java.util.ArrayList;
|
||||||
import java.util.Collections;
|
import java.util.Collections;
|
||||||
import java.util.List;
|
import java.util.List;
|
||||||
|
import java.util.Optional;
|
||||||
import java.util.stream.Collectors;
|
import java.util.stream.Collectors;
|
||||||
|
|
||||||
@Path("/assets")
|
@Path("/assets")
|
||||||
@ -179,6 +185,122 @@ public class AssetsResource {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@GET
|
||||||
|
@Path("/balancedynamicranges")
|
||||||
|
@Operation(
|
||||||
|
summary = "Get balance dynamic ranges listed.",
|
||||||
|
description = ".",
|
||||||
|
responses = {
|
||||||
|
@ApiResponse(
|
||||||
|
content = @Content(
|
||||||
|
array = @ArraySchema(
|
||||||
|
schema = @Schema(
|
||||||
|
implementation = BlockHeightRange.class
|
||||||
|
)
|
||||||
|
)
|
||||||
|
)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
)
|
||||||
|
public List<BlockHeightRange> getBalanceDynamicRanges(
|
||||||
|
@Parameter(ref = "offset") @QueryParam("offset") Integer offset,
|
||||||
|
@Parameter(ref = "limit") @QueryParam("limit") Integer limit,
|
||||||
|
@Parameter(ref = "reverse") @QueryParam("reverse") Boolean reverse) {
|
||||||
|
|
||||||
|
Optional<HSQLDBBalanceRecorder> recorder = HSQLDBBalanceRecorder.getInstance();
|
||||||
|
|
||||||
|
if( recorder.isPresent()) {
|
||||||
|
return recorder.get().getRanges(offset, limit, reverse);
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
return new ArrayList<>(0);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@GET
|
||||||
|
@Path("/balancedynamicrange/{height}")
|
||||||
|
@Operation(
|
||||||
|
summary = "Get balance dynamic range for a given height.",
|
||||||
|
description = ".",
|
||||||
|
responses = {
|
||||||
|
@ApiResponse(
|
||||||
|
content = @Content(
|
||||||
|
schema = @Schema(
|
||||||
|
implementation = BlockHeightRange.class
|
||||||
|
)
|
||||||
|
)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
)
|
||||||
|
@ApiErrors({
|
||||||
|
ApiError.INVALID_CRITERIA, ApiError.INVALID_DATA
|
||||||
|
})
|
||||||
|
public BlockHeightRange getBalanceDynamicRange(@PathParam("height") int height) {
|
||||||
|
|
||||||
|
Optional<HSQLDBBalanceRecorder> recorder = HSQLDBBalanceRecorder.getInstance();
|
||||||
|
|
||||||
|
if( recorder.isPresent()) {
|
||||||
|
Optional<BlockHeightRange> range = recorder.get().getRange(height);
|
||||||
|
|
||||||
|
if( range.isPresent() ) {
|
||||||
|
return range.get();
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
throw ApiExceptionFactory.INSTANCE.createException(request, ApiError.INVALID_CRITERIA);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
throw ApiExceptionFactory.INSTANCE.createException(request, ApiError.INVALID_DATA);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@GET
|
||||||
|
@Path("/balancedynamicamounts/{begin}/{end}")
|
||||||
|
@Operation(
|
||||||
|
summary = "Get balance dynamic ranges address amounts listed.",
|
||||||
|
description = ".",
|
||||||
|
responses = {
|
||||||
|
@ApiResponse(
|
||||||
|
content = @Content(
|
||||||
|
array = @ArraySchema(
|
||||||
|
schema = @Schema(
|
||||||
|
implementation = AddressAmountData.class
|
||||||
|
)
|
||||||
|
)
|
||||||
|
)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
)
|
||||||
|
@ApiErrors({
|
||||||
|
ApiError.INVALID_CRITERIA, ApiError.INVALID_DATA
|
||||||
|
})
|
||||||
|
public List<AddressAmountData> getBalanceDynamicAddressAmounts(
|
||||||
|
@PathParam("begin") int begin,
|
||||||
|
@PathParam("end") int end,
|
||||||
|
@Parameter(ref = "offset") @QueryParam("offset") Integer offset,
|
||||||
|
@Parameter(ref = "limit") @QueryParam("limit") Integer limit) {
|
||||||
|
|
||||||
|
Optional<HSQLDBBalanceRecorder> recorder = HSQLDBBalanceRecorder.getInstance();
|
||||||
|
|
||||||
|
if( recorder.isPresent()) {
|
||||||
|
Optional<BlockHeightRangeAddressAmounts> addressAmounts = recorder.get().getAddressAmounts(new BlockHeightRange(begin, end));
|
||||||
|
|
||||||
|
if( addressAmounts.isPresent() ) {
|
||||||
|
return addressAmounts.get().getAmounts().stream()
|
||||||
|
.sorted(BalanceRecorderUtils.ADDRESS_AMOUNT_DATA_COMPARATOR.reversed())
|
||||||
|
.skip(offset)
|
||||||
|
.limit(limit)
|
||||||
|
.collect(Collectors.toList());
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
throw ApiExceptionFactory.INSTANCE.createException(request, ApiError.INVALID_CRITERIA);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
throw ApiExceptionFactory.INSTANCE.createException(request, ApiError.INVALID_DATA);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
@GET
|
@GET
|
||||||
@Path("/openorders/{assetid}/{otherassetid}")
|
@Path("/openorders/{assetid}/{otherassetid}")
|
||||||
@Operation(
|
@Operation(
|
||||||
|
@ -2,15 +2,19 @@ package org.qortal.controller.hsqldb;
|
|||||||
|
|
||||||
import org.apache.logging.log4j.LogManager;
|
import org.apache.logging.log4j.LogManager;
|
||||||
import org.apache.logging.log4j.Logger;
|
import org.apache.logging.log4j.Logger;
|
||||||
|
import org.apache.logging.log4j.util.PropertySource;
|
||||||
import org.qortal.data.account.AccountBalanceData;
|
import org.qortal.data.account.AccountBalanceData;
|
||||||
|
import org.qortal.data.account.BlockHeightRange;
|
||||||
|
import org.qortal.data.account.BlockHeightRangeAddressAmounts;
|
||||||
import org.qortal.repository.hsqldb.HSQLDBCacheUtils;
|
import org.qortal.repository.hsqldb.HSQLDBCacheUtils;
|
||||||
import org.qortal.settings.Settings;
|
import org.qortal.settings.Settings;
|
||||||
|
import org.qortal.utils.BalanceRecorderUtils;
|
||||||
|
|
||||||
import java.util.ArrayList;
|
|
||||||
import java.util.Comparator;
|
import java.util.Comparator;
|
||||||
import java.util.List;
|
import java.util.List;
|
||||||
import java.util.Optional;
|
import java.util.Optional;
|
||||||
import java.util.concurrent.ConcurrentHashMap;
|
import java.util.concurrent.ConcurrentHashMap;
|
||||||
|
import java.util.concurrent.CopyOnWriteArrayList;
|
||||||
import java.util.stream.Collectors;
|
import java.util.stream.Collectors;
|
||||||
|
|
||||||
public class HSQLDBBalanceRecorder extends Thread{
|
public class HSQLDBBalanceRecorder extends Thread{
|
||||||
@ -23,6 +27,8 @@ public class HSQLDBBalanceRecorder extends Thread{
|
|||||||
|
|
||||||
private ConcurrentHashMap<String, List<AccountBalanceData>> balancesByAddress = new ConcurrentHashMap<>();
|
private ConcurrentHashMap<String, List<AccountBalanceData>> balancesByAddress = new ConcurrentHashMap<>();
|
||||||
|
|
||||||
|
private CopyOnWriteArrayList<BlockHeightRangeAddressAmounts> balanceDynamics = new CopyOnWriteArrayList<>();
|
||||||
|
|
||||||
private int priorityRequested;
|
private int priorityRequested;
|
||||||
private int frequency;
|
private int frequency;
|
||||||
private int capacity;
|
private int capacity;
|
||||||
@ -61,36 +67,52 @@ public class HSQLDBBalanceRecorder extends Thread{
|
|||||||
|
|
||||||
Thread.currentThread().setName("Balance Recorder");
|
Thread.currentThread().setName("Balance Recorder");
|
||||||
|
|
||||||
HSQLDBCacheUtils.startRecordingBalances(this.balancesByHeight, this.balancesByAddress, this.priorityRequested, this.frequency, this.capacity);
|
HSQLDBCacheUtils.startRecordingBalances(this.balancesByHeight, this.balanceDynamics, this.priorityRequested, this.frequency, this.capacity);
|
||||||
}
|
}
|
||||||
|
|
||||||
public List<AccountBalanceData> getLatestRecordings(int limit, long offset) {
|
public List<BlockHeightRangeAddressAmounts> getLatestDynamics(int limit, long offset) {
|
||||||
ArrayList<AccountBalanceData> data;
|
|
||||||
|
|
||||||
Optional<Integer> lastHeight = getLastHeight();
|
List<BlockHeightRangeAddressAmounts> latest = this.balanceDynamics.stream()
|
||||||
|
.sorted(BalanceRecorderUtils.BLOCK_HEIGHT_RANGE_ADDRESS_AMOUNTS_COMPARATOR.reversed())
|
||||||
|
.skip(offset)
|
||||||
|
.limit(limit)
|
||||||
|
.collect(Collectors.toList());
|
||||||
|
|
||||||
if(lastHeight.isPresent() ) {
|
return latest;
|
||||||
List<AccountBalanceData> latest = this.balancesByHeight.get(lastHeight.get());
|
}
|
||||||
|
|
||||||
if( latest != null ) {
|
public List<BlockHeightRange> getRanges(Integer offset, Integer limit, Boolean reverse) {
|
||||||
data = new ArrayList<>(latest.size());
|
|
||||||
data.addAll(
|
if( reverse ) {
|
||||||
latest.stream()
|
return this.balanceDynamics.stream()
|
||||||
.sorted(Comparator.comparingDouble(AccountBalanceData::getBalance).reversed())
|
.map(BlockHeightRangeAddressAmounts::getRange)
|
||||||
.skip(offset)
|
.sorted(BalanceRecorderUtils.BLOCK_HEIGHT_RANGE_COMPARATOR.reversed())
|
||||||
.limit(limit)
|
.skip(offset)
|
||||||
.collect(Collectors.toList())
|
.limit(limit)
|
||||||
);
|
.collect(Collectors.toList());
|
||||||
}
|
|
||||||
else {
|
|
||||||
data = new ArrayList<>(0);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
else {
|
else {
|
||||||
data = new ArrayList<>(0);
|
return this.balanceDynamics.stream()
|
||||||
|
.map(BlockHeightRangeAddressAmounts::getRange)
|
||||||
|
.sorted(BalanceRecorderUtils.BLOCK_HEIGHT_RANGE_COMPARATOR)
|
||||||
|
.skip(offset)
|
||||||
|
.limit(limit)
|
||||||
|
.collect(Collectors.toList());
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
return data;
|
public Optional<BlockHeightRangeAddressAmounts> getAddressAmounts(BlockHeightRange range) {
|
||||||
|
|
||||||
|
return this.balanceDynamics.stream()
|
||||||
|
.filter( dynamic -> dynamic.getRange().equals(range))
|
||||||
|
.findAny();
|
||||||
|
}
|
||||||
|
|
||||||
|
public Optional<BlockHeightRange> getRange( int height ) {
|
||||||
|
return this.balanceDynamics.stream()
|
||||||
|
.map(BlockHeightRangeAddressAmounts::getRange)
|
||||||
|
.filter( range -> range.getBegin() < height && range.getEnd() >= height )
|
||||||
|
.findAny();
|
||||||
}
|
}
|
||||||
|
|
||||||
private Optional<Integer> getLastHeight() {
|
private Optional<Integer> getLastHeight() {
|
||||||
|
54
src/main/java/org/qortal/data/account/AddressAmountData.java
Normal file
54
src/main/java/org/qortal/data/account/AddressAmountData.java
Normal file
@ -0,0 +1,54 @@
|
|||||||
|
package org.qortal.data.account;
|
||||||
|
|
||||||
|
import javax.xml.bind.annotation.XmlAccessType;
|
||||||
|
import javax.xml.bind.annotation.XmlAccessorType;
|
||||||
|
import javax.xml.bind.annotation.adapters.XmlJavaTypeAdapter;
|
||||||
|
import java.util.Objects;
|
||||||
|
|
||||||
|
// All properties to be converted to JSON via JAXB
|
||||||
|
@XmlAccessorType(XmlAccessType.FIELD)
|
||||||
|
public class AddressAmountData {
|
||||||
|
|
||||||
|
private String address;
|
||||||
|
|
||||||
|
@XmlJavaTypeAdapter(value = org.qortal.api.AmountTypeAdapter.class)
|
||||||
|
private long amount;
|
||||||
|
|
||||||
|
public AddressAmountData() {
|
||||||
|
}
|
||||||
|
|
||||||
|
public AddressAmountData(String address, long amount) {
|
||||||
|
|
||||||
|
this.address = address;
|
||||||
|
this.amount = amount;
|
||||||
|
}
|
||||||
|
|
||||||
|
public String getAddress() {
|
||||||
|
return address;
|
||||||
|
}
|
||||||
|
|
||||||
|
public long getAmount() {
|
||||||
|
return amount;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public boolean equals(Object o) {
|
||||||
|
if (this == o) return true;
|
||||||
|
if (o == null || getClass() != o.getClass()) return false;
|
||||||
|
AddressAmountData that = (AddressAmountData) o;
|
||||||
|
return amount == that.amount && Objects.equals(address, that.address);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public int hashCode() {
|
||||||
|
return Objects.hash(address, amount);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public String toString() {
|
||||||
|
return "AddressAmountData{" +
|
||||||
|
"address='" + address + '\'' +
|
||||||
|
", amount=" + amount +
|
||||||
|
'}';
|
||||||
|
}
|
||||||
|
}
|
51
src/main/java/org/qortal/data/account/BlockHeightRange.java
Normal file
51
src/main/java/org/qortal/data/account/BlockHeightRange.java
Normal file
@ -0,0 +1,51 @@
|
|||||||
|
package org.qortal.data.account;
|
||||||
|
|
||||||
|
import javax.xml.bind.annotation.XmlAccessType;
|
||||||
|
import javax.xml.bind.annotation.XmlAccessorType;
|
||||||
|
import java.util.Objects;
|
||||||
|
|
||||||
|
// All properties to be converted to JSON via JAXB
|
||||||
|
@XmlAccessorType(XmlAccessType.FIELD)
|
||||||
|
public class BlockHeightRange {
|
||||||
|
|
||||||
|
private int begin;
|
||||||
|
|
||||||
|
private int end;
|
||||||
|
|
||||||
|
public BlockHeightRange() {
|
||||||
|
}
|
||||||
|
|
||||||
|
public BlockHeightRange(int begin, int end) {
|
||||||
|
this.begin = begin;
|
||||||
|
this.end = end;
|
||||||
|
}
|
||||||
|
|
||||||
|
public int getBegin() {
|
||||||
|
return begin;
|
||||||
|
}
|
||||||
|
|
||||||
|
public int getEnd() {
|
||||||
|
return end;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public boolean equals(Object o) {
|
||||||
|
if (this == o) return true;
|
||||||
|
if (o == null || getClass() != o.getClass()) return false;
|
||||||
|
BlockHeightRange that = (BlockHeightRange) o;
|
||||||
|
return begin == that.begin && end == that.end;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public int hashCode() {
|
||||||
|
return Objects.hash(begin, end);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public String toString() {
|
||||||
|
return "BlockHeightRange{" +
|
||||||
|
"begin=" + begin +
|
||||||
|
", end=" + end +
|
||||||
|
'}';
|
||||||
|
}
|
||||||
|
}
|
@ -0,0 +1,52 @@
|
|||||||
|
package org.qortal.data.account;
|
||||||
|
|
||||||
|
import javax.xml.bind.annotation.XmlAccessType;
|
||||||
|
import javax.xml.bind.annotation.XmlAccessorType;
|
||||||
|
import java.util.List;
|
||||||
|
import java.util.Objects;
|
||||||
|
|
||||||
|
// All properties to be converted to JSON via JAXB
|
||||||
|
@XmlAccessorType(XmlAccessType.FIELD)
|
||||||
|
public class BlockHeightRangeAddressAmounts {
|
||||||
|
|
||||||
|
private BlockHeightRange range;
|
||||||
|
|
||||||
|
private List<AddressAmountData> amounts;
|
||||||
|
|
||||||
|
public BlockHeightRangeAddressAmounts() {
|
||||||
|
}
|
||||||
|
|
||||||
|
public BlockHeightRangeAddressAmounts(BlockHeightRange range, List<AddressAmountData> amounts) {
|
||||||
|
this.range = range;
|
||||||
|
this.amounts = amounts;
|
||||||
|
}
|
||||||
|
|
||||||
|
public BlockHeightRange getRange() {
|
||||||
|
return range;
|
||||||
|
}
|
||||||
|
|
||||||
|
public List<AddressAmountData> getAmounts() {
|
||||||
|
return amounts;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public boolean equals(Object o) {
|
||||||
|
if (this == o) return true;
|
||||||
|
if (o == null || getClass() != o.getClass()) return false;
|
||||||
|
BlockHeightRangeAddressAmounts that = (BlockHeightRangeAddressAmounts) o;
|
||||||
|
return Objects.equals(range, that.range) && Objects.equals(amounts, that.amounts);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public int hashCode() {
|
||||||
|
return Objects.hash(range, amounts);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public String toString() {
|
||||||
|
return "BlockHeightRangeAddressAmounts{" +
|
||||||
|
"range=" + range +
|
||||||
|
", amounts=" + amounts +
|
||||||
|
'}';
|
||||||
|
}
|
||||||
|
}
|
@ -7,11 +7,16 @@ import org.qortal.arbitrary.misc.Category;
|
|||||||
import org.qortal.arbitrary.misc.Service;
|
import org.qortal.arbitrary.misc.Service;
|
||||||
import org.qortal.controller.Controller;
|
import org.qortal.controller.Controller;
|
||||||
import org.qortal.data.account.AccountBalanceData;
|
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.arbitrary.ArbitraryResourceCache;
|
import org.qortal.data.arbitrary.ArbitraryResourceCache;
|
||||||
import org.qortal.data.arbitrary.ArbitraryResourceData;
|
import org.qortal.data.arbitrary.ArbitraryResourceData;
|
||||||
import org.qortal.data.arbitrary.ArbitraryResourceMetadata;
|
import org.qortal.data.arbitrary.ArbitraryResourceMetadata;
|
||||||
import org.qortal.data.arbitrary.ArbitraryResourceStatus;
|
import org.qortal.data.arbitrary.ArbitraryResourceStatus;
|
||||||
import org.qortal.repository.DataException;
|
import org.qortal.repository.DataException;
|
||||||
|
import org.qortal.settings.Settings;
|
||||||
|
import org.qortal.utils.BalanceRecorderUtils;
|
||||||
|
|
||||||
import java.sql.ResultSet;
|
import java.sql.ResultSet;
|
||||||
import java.sql.SQLException;
|
import java.sql.SQLException;
|
||||||
@ -28,6 +33,7 @@ import java.util.Optional;
|
|||||||
import java.util.Timer;
|
import java.util.Timer;
|
||||||
import java.util.TimerTask;
|
import java.util.TimerTask;
|
||||||
import java.util.concurrent.ConcurrentHashMap;
|
import java.util.concurrent.ConcurrentHashMap;
|
||||||
|
import java.util.concurrent.CopyOnWriteArrayList;
|
||||||
import java.util.function.Function;
|
import java.util.function.Function;
|
||||||
import java.util.function.Predicate;
|
import java.util.function.Predicate;
|
||||||
import java.util.function.Supplier;
|
import java.util.function.Supplier;
|
||||||
@ -389,14 +395,15 @@ public class HSQLDBCacheUtils {
|
|||||||
/**
|
/**
|
||||||
* Start Recording Balances
|
* Start Recording Balances
|
||||||
*
|
*
|
||||||
* @param queue the queue to add to, remove oldest data if necssary
|
* @param balancesByHeight height -> account balances
|
||||||
* @param repository the db repsoitory
|
* @param balanceDynamics every balance dynamic
|
||||||
* @param priorityRequested the requested thread priority
|
* @param priorityRequested the requested thread priority
|
||||||
* @param frequency the recording frequencies, in minutes
|
* @param frequency the recording frequencies, in minutes
|
||||||
|
* @param capacity the maximum size of balanceDynamics
|
||||||
*/
|
*/
|
||||||
public static void startRecordingBalances(
|
public static void startRecordingBalances(
|
||||||
final ConcurrentHashMap<Integer, List<AccountBalanceData>> balancesByHeight,
|
final ConcurrentHashMap<Integer, List<AccountBalanceData>> balancesByHeight,
|
||||||
final ConcurrentHashMap<String, List<AccountBalanceData>> balancesByAddress,
|
CopyOnWriteArrayList<BlockHeightRangeAddressAmounts> balanceDynamics,
|
||||||
int priorityRequested,
|
int priorityRequested,
|
||||||
int frequency,
|
int frequency,
|
||||||
int capacity) {
|
int capacity) {
|
||||||
@ -409,55 +416,64 @@ public class HSQLDBCacheUtils {
|
|||||||
|
|
||||||
Thread.currentThread().setName(BALANCE_RECORDER_TIMER_TASK);
|
Thread.currentThread().setName(BALANCE_RECORDER_TIMER_TASK);
|
||||||
|
|
||||||
try (final HSQLDBRepository repository = (HSQLDBRepository) Controller.REPOSITORY_FACTORY.getRepository()) {
|
int currentHeight = recordCurrentBalances(balancesByHeight);
|
||||||
while (balancesByHeight.size() > capacity + 1) {
|
|
||||||
Optional<Integer> firstHeight = balancesByHeight.keySet().stream().sorted().findFirst();
|
|
||||||
|
|
||||||
if (firstHeight.isPresent()) balancesByHeight.remove(firstHeight.get());
|
LOGGER.debug("recorded balances: height = " + currentHeight);
|
||||||
}
|
|
||||||
|
|
||||||
// get current balances
|
// remove invalidated recordings, recording after current height
|
||||||
List<AccountBalanceData> accountBalances = getAccountBalances(repository);
|
BalanceRecorderUtils.removeRecordingsAboveHeight(currentHeight, balancesByHeight);
|
||||||
|
|
||||||
// get anyone of the balances
|
// remove invalidated dynamics, on or after current height
|
||||||
Optional<AccountBalanceData> data = accountBalances.stream().findAny();
|
BalanceRecorderUtils.removeDynamicsOnOrAboveHeight(currentHeight, balanceDynamics);
|
||||||
|
|
||||||
// if there are any balances, then record them
|
// if there are 2 or more recordings, then produce balance dynamics for the first 2 recordings
|
||||||
if (data.isPresent()) {
|
if( balancesByHeight.size() > 1 ) {
|
||||||
// map all new balances to the current height
|
|
||||||
balancesByHeight.put(data.get().getHeight(), accountBalances);
|
|
||||||
|
|
||||||
// for each new balance, map to address
|
Optional<Integer> priorHeight = BalanceRecorderUtils.getPriorHeight(currentHeight, balancesByHeight);
|
||||||
for (AccountBalanceData accountBalance : accountBalances) {
|
|
||||||
|
|
||||||
// get recorded balances for this address
|
// if there is a prior height
|
||||||
List<AccountBalanceData> establishedBalances
|
if(priorHeight.isPresent()) {
|
||||||
= balancesByAddress.getOrDefault(accountBalance.getAddress(), new ArrayList<>(0));
|
|
||||||
|
|
||||||
// start a new list of recordings for this address, add the new balance and add the established
|
BlockHeightRange blockHeightRange = new BlockHeightRange(priorHeight.get(), currentHeight);
|
||||||
// balances
|
|
||||||
List<AccountBalanceData> balances = new ArrayList<>(establishedBalances.size() + 1);
|
|
||||||
balances.add(accountBalance);
|
|
||||||
balances.addAll(establishedBalances);
|
|
||||||
|
|
||||||
// reset tha balances for this address
|
LOGGER.debug("building dynamics for block heights: range = " + blockHeightRange);
|
||||||
balancesByAddress.put(accountBalance.getAddress(), balances);
|
|
||||||
|
|
||||||
// TODO: reduce account balances to capacity
|
List<AccountBalanceData> currentBalances = balancesByHeight.get(currentHeight);
|
||||||
}
|
|
||||||
|
|
||||||
// reduce height balances to capacity
|
List<AddressAmountData> currentDynamics
|
||||||
while( balancesByHeight.size() > capacity ) {
|
= BalanceRecorderUtils.buildBalanceDynamics(
|
||||||
Optional<Integer> lowestHeight
|
currentBalances,
|
||||||
= balancesByHeight.entrySet().stream()
|
balancesByHeight.get(priorHeight.get()),
|
||||||
.min(Comparator.comparingInt(Map.Entry::getKey))
|
Settings.getInstance().getMinimumBalanceRecording());
|
||||||
.map(Map.Entry::getKey);
|
|
||||||
|
|
||||||
if (lowestHeight.isPresent()) balancesByHeight.entrySet().remove(lowestHeight);
|
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());
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
} catch (DataException e) {
|
else {
|
||||||
LOGGER.error(e.getMessage(), e);
|
LOGGER.warn("Expecting prior height and nothing was discovered, current height = " + currentHeight);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
// else this should be the first recording
|
||||||
|
else {
|
||||||
|
LOGGER.info("first balance recording completed");
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
@ -466,6 +482,35 @@ public class HSQLDBCacheUtils {
|
|||||||
timer.scheduleAtFixedRate(task, 300_000, frequency * 60_000);
|
timer.scheduleAtFixedRate(task, 300_000, frequency * 60_000);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private static int recordCurrentBalances(ConcurrentHashMap<Integer, List<AccountBalanceData>> balancesByHeight) {
|
||||||
|
int currentHeight;
|
||||||
|
|
||||||
|
try (final HSQLDBRepository repository = (HSQLDBRepository) Controller.REPOSITORY_FACTORY.getRepository()) {
|
||||||
|
|
||||||
|
// get current balances
|
||||||
|
List<AccountBalanceData> accountBalances = getAccountBalances(repository);
|
||||||
|
|
||||||
|
// get anyone of the balances
|
||||||
|
Optional<AccountBalanceData> data = accountBalances.stream().findAny();
|
||||||
|
|
||||||
|
// if there are any balances, then record them
|
||||||
|
if (data.isPresent()) {
|
||||||
|
// map all new balances to the current height
|
||||||
|
balancesByHeight.put(data.get().getHeight(), accountBalances);
|
||||||
|
|
||||||
|
currentHeight = data.get().getHeight();
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
currentHeight = Integer.MAX_VALUE;
|
||||||
|
}
|
||||||
|
} catch (DataException e) {
|
||||||
|
LOGGER.error(e.getMessage(), e);
|
||||||
|
currentHeight = Integer.MAX_VALUE;
|
||||||
|
}
|
||||||
|
|
||||||
|
return currentHeight;
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Build Timer
|
* Build Timer
|
||||||
*
|
*
|
||||||
|
@ -444,14 +444,56 @@ public class Settings {
|
|||||||
*/
|
*/
|
||||||
private long archivingPause = 3000;
|
private long archivingPause = 3000;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Enable Balance Recorder?
|
||||||
|
*
|
||||||
|
* True for balance recording, otherwise false.
|
||||||
|
*/
|
||||||
private boolean balanceRecorderEnabled = false;
|
private boolean balanceRecorderEnabled = false;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Balance Recorder Priority
|
||||||
|
*
|
||||||
|
* The thread priority (1 is lowest, 10 is highest) of the balance recorder thread, if enabled.
|
||||||
|
*/
|
||||||
private int balanceRecorderPriority = 1;
|
private int balanceRecorderPriority = 1;
|
||||||
|
|
||||||
private int balanceRecorderFrequency = 2*60*1000;
|
/**
|
||||||
|
* Balance Recorder Frequency
|
||||||
|
*
|
||||||
|
* How often the balances will be recorded, if enabled, measured in minutes.
|
||||||
|
*/
|
||||||
|
private int balanceRecorderFrequency = 20;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Balance Recorder Capacity
|
||||||
|
*
|
||||||
|
* The number of balance recorder ranges will be held in memory.
|
||||||
|
*/
|
||||||
private int balanceRecorderCapacity = 1000;
|
private int balanceRecorderCapacity = 1000;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Minimum Balance Recording
|
||||||
|
*
|
||||||
|
* The minimum recored balance change in Qortoshis (1/100000000 QORT)
|
||||||
|
*/
|
||||||
|
private long minimumBalanceRecording = 100000000;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Top Balance Logging Limit
|
||||||
|
*
|
||||||
|
* When logging the number limit of top balance changes to show in the logs for any given block range.
|
||||||
|
*/
|
||||||
|
private long topBalanceLoggingLimit = 100;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Balance Recorder Rollback Allowance
|
||||||
|
*
|
||||||
|
* If the balance recorder is enabled, it must protect its prior balances by this number of blocks in case of
|
||||||
|
* a blockchain rollback and reorganization.
|
||||||
|
*/
|
||||||
|
private int balanceRecorderRollbackAllowance = 100;
|
||||||
|
|
||||||
// Domain mapping
|
// Domain mapping
|
||||||
public static class ThreadLimit {
|
public static class ThreadLimit {
|
||||||
private String messageType;
|
private String messageType;
|
||||||
@ -1257,4 +1299,16 @@ public class Settings {
|
|||||||
public boolean isBalanceRecorderEnabled() {
|
public boolean isBalanceRecorderEnabled() {
|
||||||
return balanceRecorderEnabled;
|
return balanceRecorderEnabled;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public long getMinimumBalanceRecording() {
|
||||||
|
return minimumBalanceRecording;
|
||||||
|
}
|
||||||
|
|
||||||
|
public long getTopBalanceLoggingLimit() {
|
||||||
|
return topBalanceLoggingLimit;
|
||||||
|
}
|
||||||
|
|
||||||
|
public int getBalanceRecorderRollbackAllowance() {
|
||||||
|
return balanceRecorderRollbackAllowance;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
119
src/main/java/org/qortal/utils/BalanceRecorderUtils.java
Normal file
119
src/main/java/org/qortal/utils/BalanceRecorderUtils.java
Normal file
@ -0,0 +1,119 @@
|
|||||||
|
package org.qortal.utils;
|
||||||
|
|
||||||
|
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 java.util.ArrayList;
|
||||||
|
import java.util.Comparator;
|
||||||
|
import java.util.List;
|
||||||
|
import java.util.Optional;
|
||||||
|
import java.util.concurrent.ConcurrentHashMap;
|
||||||
|
import java.util.concurrent.CopyOnWriteArrayList;
|
||||||
|
import java.util.function.Predicate;
|
||||||
|
import java.util.stream.Collectors;
|
||||||
|
|
||||||
|
public class BalanceRecorderUtils {
|
||||||
|
|
||||||
|
public static final Predicate<AddressAmountData> ADDRESS_AMOUNT_DATA_NOT_ZERO = addressAmount -> addressAmount.getAmount() != 0;
|
||||||
|
public static final Comparator<BlockHeightRangeAddressAmounts> BLOCK_HEIGHT_RANGE_ADDRESS_AMOUNTS_COMPARATOR = new Comparator<BlockHeightRangeAddressAmounts>() {
|
||||||
|
@Override
|
||||||
|
public int compare(BlockHeightRangeAddressAmounts amounts1, BlockHeightRangeAddressAmounts amounts2) {
|
||||||
|
return amounts1.getRange().getEnd() - amounts2.getRange().getEnd();
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
public static final Comparator<AddressAmountData> ADDRESS_AMOUNT_DATA_COMPARATOR = new Comparator<AddressAmountData>() {
|
||||||
|
@Override
|
||||||
|
public int compare(AddressAmountData addressAmountData, AddressAmountData t1) {
|
||||||
|
if( addressAmountData.getAmount() > t1.getAmount() ) {
|
||||||
|
return 1;
|
||||||
|
}
|
||||||
|
else if( addressAmountData.getAmount() < t1.getAmount() ) {
|
||||||
|
return -1;
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
public static final Comparator<BlockHeightRange> BLOCK_HEIGHT_RANGE_COMPARATOR = new Comparator<BlockHeightRange>() {
|
||||||
|
@Override
|
||||||
|
public int compare(BlockHeightRange range1, BlockHeightRange range2) {
|
||||||
|
return range1.getEnd() - range2.getEnd();
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Build Balance Dynmaics For Account
|
||||||
|
*
|
||||||
|
* @param priorBalances the balances prior to the current height, assuming only one balance per address
|
||||||
|
* @param accountBalance the current balance
|
||||||
|
*
|
||||||
|
* @return the difference between the current balance and the prior balance for the current balance address
|
||||||
|
*/
|
||||||
|
public static AddressAmountData buildBalanceDynamicsForAccount(List<AccountBalanceData> priorBalances, AccountBalanceData accountBalance) {
|
||||||
|
Optional<AccountBalanceData> matchingAccountPriorBalance
|
||||||
|
= priorBalances.stream()
|
||||||
|
.filter(priorBalance -> accountBalance.getAddress().equals(priorBalance.getAddress()))
|
||||||
|
.findFirst();
|
||||||
|
if(matchingAccountPriorBalance.isPresent()) {
|
||||||
|
return new AddressAmountData(accountBalance.getAddress(), accountBalance.getBalance() - matchingAccountPriorBalance.get().getBalance());
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
return new AddressAmountData(accountBalance.getAddress(), accountBalance.getBalance());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public static List<AddressAmountData> buildBalanceDynamics(final List<AccountBalanceData> balances, final List<AccountBalanceData> priorBalances, long minimum) {
|
||||||
|
|
||||||
|
List<AddressAmountData> addressAmounts = new ArrayList<>(balances.size());
|
||||||
|
|
||||||
|
// prior balance
|
||||||
|
addressAmounts.addAll(
|
||||||
|
balances.stream()
|
||||||
|
.map(balance -> buildBalanceDynamicsForAccount(priorBalances, balance))
|
||||||
|
.filter(ADDRESS_AMOUNT_DATA_NOT_ZERO)
|
||||||
|
.filter( data -> data.getAmount() >= minimum)
|
||||||
|
.collect(Collectors.toList())
|
||||||
|
);
|
||||||
|
|
||||||
|
return addressAmounts;
|
||||||
|
}
|
||||||
|
|
||||||
|
public static void removeRecordingsAboveHeight(int currentHeight, ConcurrentHashMap<Integer, List<AccountBalanceData>> balancesByHeight) {
|
||||||
|
balancesByHeight.entrySet().stream()
|
||||||
|
.filter(heightWithBalances -> heightWithBalances.getKey() > currentHeight)
|
||||||
|
.forEach(heightWithBalances -> balancesByHeight.remove(heightWithBalances.getKey()));
|
||||||
|
}
|
||||||
|
|
||||||
|
public static void removeRecordingsBelowHeight(int currentHeight, ConcurrentHashMap<Integer, List<AccountBalanceData>> balancesByHeight) {
|
||||||
|
balancesByHeight.entrySet().stream()
|
||||||
|
.filter(heightWithBalances -> heightWithBalances.getKey() < currentHeight)
|
||||||
|
.forEach(heightWithBalances -> balancesByHeight.remove(heightWithBalances.getKey()));
|
||||||
|
}
|
||||||
|
|
||||||
|
public static void removeDynamicsOnOrAboveHeight(int currentHeight, CopyOnWriteArrayList<BlockHeightRangeAddressAmounts> balanceDynamics) {
|
||||||
|
balanceDynamics.stream()
|
||||||
|
.filter(addressAmounts -> addressAmounts.getRange().getEnd() >= currentHeight)
|
||||||
|
.forEach(addressAmounts -> balanceDynamics.remove(addressAmounts));
|
||||||
|
}
|
||||||
|
|
||||||
|
public static BlockHeightRangeAddressAmounts removeOldestDynamics(CopyOnWriteArrayList<BlockHeightRangeAddressAmounts> balanceDynamics) {
|
||||||
|
BlockHeightRangeAddressAmounts oldestDynamics
|
||||||
|
= balanceDynamics.stream().sorted(BLOCK_HEIGHT_RANGE_ADDRESS_AMOUNTS_COMPARATOR).findFirst().get();
|
||||||
|
|
||||||
|
balanceDynamics.remove(oldestDynamics);
|
||||||
|
return oldestDynamics;
|
||||||
|
}
|
||||||
|
|
||||||
|
public static Optional<Integer> getPriorHeight(int currentHeight, ConcurrentHashMap<Integer, List<AccountBalanceData>> balancesByHeight) {
|
||||||
|
Optional<Integer> priorHeight
|
||||||
|
= balancesByHeight.keySet().stream()
|
||||||
|
.filter(height -> height < currentHeight)
|
||||||
|
.sorted(Comparator.reverseOrder()).findFirst();
|
||||||
|
return priorHeight;
|
||||||
|
}
|
||||||
|
}
|
@ -0,0 +1,361 @@
|
|||||||
|
package org.qortal.test.utils;
|
||||||
|
|
||||||
|
import org.junit.Assert;
|
||||||
|
import org.junit.Test;
|
||||||
|
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.utils.BalanceRecorderUtils;
|
||||||
|
|
||||||
|
import java.util.ArrayList;
|
||||||
|
import java.util.List;
|
||||||
|
import java.util.Map;
|
||||||
|
import java.util.Optional;
|
||||||
|
import java.util.concurrent.ConcurrentHashMap;
|
||||||
|
import java.util.concurrent.CopyOnWriteArrayList;
|
||||||
|
import java.util.stream.Collectors;
|
||||||
|
|
||||||
|
public class BalanceRecorderUtilsTests {
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void testNotZeroForZero() {
|
||||||
|
boolean test = BalanceRecorderUtils.ADDRESS_AMOUNT_DATA_NOT_ZERO.test( new AddressAmountData("", 0));
|
||||||
|
|
||||||
|
Assert.assertFalse(test);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void testNotZeroForPositive() {
|
||||||
|
boolean test = BalanceRecorderUtils.ADDRESS_AMOUNT_DATA_NOT_ZERO.test(new AddressAmountData("", 1));
|
||||||
|
|
||||||
|
Assert.assertTrue(test);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void testNotZeroForNegative() {
|
||||||
|
boolean test = BalanceRecorderUtils.ADDRESS_AMOUNT_DATA_NOT_ZERO.test( new AddressAmountData("", -10));
|
||||||
|
|
||||||
|
Assert.assertTrue(test);
|
||||||
|
}
|
||||||
|
|
||||||
|
@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));
|
||||||
|
|
||||||
|
int compare = BalanceRecorderUtils.BLOCK_HEIGHT_RANGE_ADDRESS_AMOUNTS_COMPARATOR.compare(addressAmounts1, addressAmounts2);
|
||||||
|
|
||||||
|
Assert.assertTrue( compare > 0);
|
||||||
|
}
|
||||||
|
|
||||||
|
@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));
|
||||||
|
|
||||||
|
int compare = BalanceRecorderUtils.BLOCK_HEIGHT_RANGE_ADDRESS_AMOUNTS_COMPARATOR.compare(addressAmounts1, addressAmounts2);
|
||||||
|
|
||||||
|
Assert.assertTrue( compare < 0 );
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void testAddressAmountDataComparator() {
|
||||||
|
|
||||||
|
AddressAmountData addressAmount1 = new AddressAmountData("a", 10);
|
||||||
|
AddressAmountData addressAmount2 = new AddressAmountData("b", 20);
|
||||||
|
|
||||||
|
int compare = BalanceRecorderUtils.ADDRESS_AMOUNT_DATA_COMPARATOR.compare(addressAmount1, addressAmount2);
|
||||||
|
|
||||||
|
Assert.assertTrue( compare < 0);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void testRemoveRecordingsBelowHeightNoBalances() {
|
||||||
|
|
||||||
|
int currentHeight = 5;
|
||||||
|
ConcurrentHashMap<Integer, List<AccountBalanceData>> balancesByHeight = new ConcurrentHashMap<>();
|
||||||
|
|
||||||
|
BalanceRecorderUtils.removeRecordingsBelowHeight(currentHeight, balancesByHeight);
|
||||||
|
|
||||||
|
Assert.assertEquals(0, balancesByHeight.size());
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void testRemoveRecordingsBelowHeightOneBalanceBelow() {
|
||||||
|
int currentHeight = 5;
|
||||||
|
|
||||||
|
ConcurrentHashMap<Integer, List<AccountBalanceData>> balancesByHeight = new ConcurrentHashMap<>(1);
|
||||||
|
|
||||||
|
balancesByHeight.put(1, new ArrayList<>(0));
|
||||||
|
|
||||||
|
Assert.assertEquals(1, balancesByHeight.size());
|
||||||
|
|
||||||
|
BalanceRecorderUtils.removeRecordingsBelowHeight(currentHeight, balancesByHeight);
|
||||||
|
|
||||||
|
Assert.assertEquals(0, balancesByHeight.size());
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void testRemoveRecordingsBelowHeightOneBalanceAbove() {
|
||||||
|
int currentHeight = 5;
|
||||||
|
|
||||||
|
ConcurrentHashMap<Integer, List<AccountBalanceData>> balancesByHeight = new ConcurrentHashMap<>(1);
|
||||||
|
|
||||||
|
balancesByHeight.put(10, new ArrayList<>(0));
|
||||||
|
|
||||||
|
Assert.assertEquals(1, balancesByHeight.size());
|
||||||
|
|
||||||
|
BalanceRecorderUtils.removeRecordingsBelowHeight(currentHeight, balancesByHeight);
|
||||||
|
|
||||||
|
Assert.assertEquals(1, balancesByHeight.size());
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void testBuildBalanceDynamicsOneAccountOneChange() {
|
||||||
|
|
||||||
|
String address = "a";
|
||||||
|
|
||||||
|
List<AccountBalanceData> balances = new ArrayList<>(1);
|
||||||
|
balances.add(new AccountBalanceData(address, 0, 2));
|
||||||
|
|
||||||
|
List<AccountBalanceData> priorBalances = new ArrayList<>(1);
|
||||||
|
priorBalances.add(new AccountBalanceData(address, 0, 1));
|
||||||
|
|
||||||
|
List<AddressAmountData> dynamics = BalanceRecorderUtils.buildBalanceDynamics(balances, priorBalances, 0);
|
||||||
|
|
||||||
|
Assert.assertNotNull(dynamics);
|
||||||
|
Assert.assertEquals(1, dynamics.size());
|
||||||
|
|
||||||
|
AddressAmountData addressAmountData = dynamics.get(0);
|
||||||
|
Assert.assertNotNull(addressAmountData);
|
||||||
|
Assert.assertEquals(address, addressAmountData.getAddress());
|
||||||
|
Assert.assertEquals(1, addressAmountData.getAmount());
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void testBuildBalanceDynamicsOneAccountNoPrior() {
|
||||||
|
|
||||||
|
String address = "a";
|
||||||
|
|
||||||
|
List<AccountBalanceData> balances = new ArrayList<>(1);
|
||||||
|
balances.add(new AccountBalanceData(address, 0, 2));
|
||||||
|
|
||||||
|
List<AccountBalanceData> priorBalances = new ArrayList<>(0);
|
||||||
|
|
||||||
|
List<AddressAmountData> dynamics = BalanceRecorderUtils.buildBalanceDynamics(balances, priorBalances, 0);
|
||||||
|
|
||||||
|
Assert.assertNotNull(dynamics);
|
||||||
|
Assert.assertEquals(1, dynamics.size());
|
||||||
|
|
||||||
|
AddressAmountData addressAmountData = dynamics.get(0);
|
||||||
|
Assert.assertNotNull(addressAmountData);
|
||||||
|
Assert.assertEquals(address, addressAmountData.getAddress());
|
||||||
|
Assert.assertEquals(2, addressAmountData.getAmount());
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void testBuildBalanceDynamicsTwoAccountsNegativeValues() {
|
||||||
|
|
||||||
|
String address1 = "a";
|
||||||
|
String address2 = "b";
|
||||||
|
|
||||||
|
List<AccountBalanceData> balances = new ArrayList<>(2);
|
||||||
|
balances.add(new AccountBalanceData(address1, 0, 10_000));
|
||||||
|
balances.add(new AccountBalanceData(address2, 0, 100));
|
||||||
|
|
||||||
|
List<AccountBalanceData> priorBalances = new ArrayList<>(2);
|
||||||
|
priorBalances.add(new AccountBalanceData(address2, 0, 200));
|
||||||
|
priorBalances.add(new AccountBalanceData(address1, 0, 5000));
|
||||||
|
|
||||||
|
List<AddressAmountData> dynamics = BalanceRecorderUtils.buildBalanceDynamics(balances, priorBalances, -100L);
|
||||||
|
|
||||||
|
Assert.assertNotNull(dynamics);
|
||||||
|
Assert.assertEquals(2, dynamics.size());
|
||||||
|
|
||||||
|
Map<String, Long> amountByAddress
|
||||||
|
= dynamics.stream()
|
||||||
|
.collect(Collectors.toMap(dynamic -> dynamic.getAddress(), dynamic -> dynamic.getAmount()));
|
||||||
|
|
||||||
|
Assert.assertTrue(amountByAddress.containsKey(address1));
|
||||||
|
|
||||||
|
long amount1 = amountByAddress.get(address1);
|
||||||
|
|
||||||
|
Assert.assertNotNull(amount1);
|
||||||
|
Assert.assertEquals(5000L, amount1 );
|
||||||
|
|
||||||
|
Assert.assertTrue(amountByAddress.containsKey(address2));
|
||||||
|
|
||||||
|
long amount2 = amountByAddress.get(address2);
|
||||||
|
|
||||||
|
Assert.assertNotNull(amount2);
|
||||||
|
Assert.assertEquals(-100L, amount2);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void testBuildBalanceDynamicsForAccountNoPriorAnyAccount() {
|
||||||
|
List<AccountBalanceData> priorBalances = new ArrayList<>(0);
|
||||||
|
AccountBalanceData accountBalance = new AccountBalanceData("a", 0, 10);
|
||||||
|
|
||||||
|
AddressAmountData dynamic = BalanceRecorderUtils.buildBalanceDynamicsForAccount(priorBalances, accountBalance);
|
||||||
|
|
||||||
|
Assert.assertNotNull(dynamic);
|
||||||
|
Assert.assertEquals(10, dynamic.getAmount());
|
||||||
|
Assert.assertEquals("a", dynamic.getAddress());
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void testBuildBalanceDynamicsForAccountNoPriorThisAccount() {
|
||||||
|
List<AccountBalanceData> priorBalances = new ArrayList<>(2);
|
||||||
|
priorBalances.add(new AccountBalanceData("b", 0, 100));
|
||||||
|
|
||||||
|
AccountBalanceData accountBalanceData = new AccountBalanceData("a", 0, 10);
|
||||||
|
|
||||||
|
AddressAmountData dynamic = BalanceRecorderUtils.buildBalanceDynamicsForAccount(priorBalances, accountBalanceData);
|
||||||
|
|
||||||
|
Assert.assertNotNull(dynamic);
|
||||||
|
Assert.assertEquals(10, dynamic.getAmount());
|
||||||
|
Assert.assertEquals("a", dynamic.getAddress());
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void testBuildBalanceDynamicsForAccountPriorForThisAndOthers() {
|
||||||
|
List<AccountBalanceData> priorBalances = new ArrayList<>(2);
|
||||||
|
priorBalances.add(new AccountBalanceData("a", 0, 100));
|
||||||
|
priorBalances.add(new AccountBalanceData("b", 0, 200));
|
||||||
|
priorBalances.add(new AccountBalanceData("c", 0, 300));
|
||||||
|
|
||||||
|
AccountBalanceData accountBalance = new AccountBalanceData("b", 0, 1000);
|
||||||
|
|
||||||
|
AddressAmountData dynamic = BalanceRecorderUtils.buildBalanceDynamicsForAccount(priorBalances, accountBalance);
|
||||||
|
|
||||||
|
Assert.assertNotNull(dynamic);
|
||||||
|
Assert.assertEquals(800, dynamic.getAmount());
|
||||||
|
Assert.assertEquals("b", dynamic.getAddress());
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void testRemoveRecordingAboveHeightOneOfTwo() {
|
||||||
|
|
||||||
|
int currentHeight = 10;
|
||||||
|
ConcurrentHashMap<Integer, List<AccountBalanceData>> balancesByHeight = new ConcurrentHashMap<>();
|
||||||
|
|
||||||
|
balancesByHeight.put(3, new ArrayList<>());
|
||||||
|
balancesByHeight.put(20, new ArrayList<>());
|
||||||
|
|
||||||
|
Assert.assertEquals(2, balancesByHeight.size());
|
||||||
|
|
||||||
|
BalanceRecorderUtils.removeRecordingsAboveHeight(currentHeight, balancesByHeight);
|
||||||
|
|
||||||
|
Assert.assertEquals(1, balancesByHeight.size());
|
||||||
|
Assert.assertTrue( balancesByHeight.containsKey(3));
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void testPriorHeightBeforeAfter() {
|
||||||
|
|
||||||
|
int currentHeight = 10;
|
||||||
|
ConcurrentHashMap<Integer, List<AccountBalanceData>> balancesByHeight = new ConcurrentHashMap<>();
|
||||||
|
balancesByHeight.put( 2, new ArrayList<>());
|
||||||
|
balancesByHeight.put(7, new ArrayList<>());
|
||||||
|
balancesByHeight.put(12, new ArrayList<>());
|
||||||
|
|
||||||
|
Optional<Integer> priorHeight = BalanceRecorderUtils.getPriorHeight(currentHeight, balancesByHeight);
|
||||||
|
|
||||||
|
Assert.assertNotNull(priorHeight);
|
||||||
|
Assert.assertTrue(priorHeight.isPresent());
|
||||||
|
Assert.assertEquals( 7, priorHeight.get().intValue());
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void testPriorHeightNoPriorAfterOnly() {
|
||||||
|
|
||||||
|
int currentHeight = 10;
|
||||||
|
ConcurrentHashMap<Integer, List<AccountBalanceData>> balancesByHeight = new ConcurrentHashMap<>();
|
||||||
|
balancesByHeight.put(12, new ArrayList<>());
|
||||||
|
|
||||||
|
Optional<Integer> priorHeight = BalanceRecorderUtils.getPriorHeight(currentHeight, balancesByHeight);
|
||||||
|
|
||||||
|
Assert.assertNotNull(priorHeight);
|
||||||
|
Assert.assertTrue(priorHeight.isEmpty());
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void testPriorHeightPriorOnly() {
|
||||||
|
|
||||||
|
int currentHeight = 10;
|
||||||
|
|
||||||
|
ConcurrentHashMap<Integer, List<AccountBalanceData>> balancesByHeight = new ConcurrentHashMap<>();
|
||||||
|
balancesByHeight.put(7, new ArrayList<>());
|
||||||
|
|
||||||
|
Optional<Integer> priorHeight = BalanceRecorderUtils.getPriorHeight(currentHeight, balancesByHeight);
|
||||||
|
|
||||||
|
Assert.assertNotNull(priorHeight);
|
||||||
|
Assert.assertTrue(priorHeight.isPresent());
|
||||||
|
Assert.assertEquals(7, priorHeight.get().intValue());
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void testRemoveDynamicsOnOrAboveHeightOneAbove() {
|
||||||
|
|
||||||
|
int currentHeight = 10;
|
||||||
|
|
||||||
|
CopyOnWriteArrayList<BlockHeightRangeAddressAmounts> dynamics = new CopyOnWriteArrayList<>();
|
||||||
|
|
||||||
|
BlockHeightRange range1 = new BlockHeightRange(10, 20);
|
||||||
|
dynamics.add(new BlockHeightRangeAddressAmounts(range1, new ArrayList<>()));
|
||||||
|
|
||||||
|
BlockHeightRange range2 = new BlockHeightRange(1, 4);
|
||||||
|
dynamics.add(new BlockHeightRangeAddressAmounts(range2, new ArrayList<>()));
|
||||||
|
|
||||||
|
Assert.assertEquals(2, dynamics.size());
|
||||||
|
BalanceRecorderUtils.removeDynamicsOnOrAboveHeight(currentHeight, dynamics);
|
||||||
|
|
||||||
|
Assert.assertEquals(1, dynamics.size());
|
||||||
|
Assert.assertEquals(range2, dynamics.get(0).getRange());
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void testRemoveDynamicsOnOrAboveOneOnOneAbove() {
|
||||||
|
int currentHeight = 11;
|
||||||
|
|
||||||
|
CopyOnWriteArrayList<BlockHeightRangeAddressAmounts> dynamics = new CopyOnWriteArrayList<>();
|
||||||
|
|
||||||
|
BlockHeightRange range1 = new BlockHeightRange(1,5);
|
||||||
|
dynamics.add(new BlockHeightRangeAddressAmounts(range1, new ArrayList<>()));
|
||||||
|
|
||||||
|
BlockHeightRange range2 = new BlockHeightRange(6, 11);
|
||||||
|
dynamics.add((new BlockHeightRangeAddressAmounts(range2, new ArrayList<>())));
|
||||||
|
|
||||||
|
BlockHeightRange range3 = new BlockHeightRange(22, 16);
|
||||||
|
dynamics.add(new BlockHeightRangeAddressAmounts(range3, new ArrayList<>()));
|
||||||
|
|
||||||
|
Assert.assertEquals(3, dynamics.size());
|
||||||
|
|
||||||
|
BalanceRecorderUtils.removeDynamicsOnOrAboveHeight(currentHeight, dynamics);
|
||||||
|
|
||||||
|
Assert.assertEquals(1, dynamics.size());
|
||||||
|
Assert.assertTrue( dynamics.get(0).getRange().equals(range1));
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void testRemoveOldestDynamicsTwice() {
|
||||||
|
CopyOnWriteArrayList<BlockHeightRangeAddressAmounts> dynamics = new CopyOnWriteArrayList<>();
|
||||||
|
|
||||||
|
dynamics.add(new BlockHeightRangeAddressAmounts(new BlockHeightRange(1, 5), new ArrayList<>()));
|
||||||
|
dynamics.add(new BlockHeightRangeAddressAmounts(new BlockHeightRange(5, 9), 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)));
|
||||||
|
|
||||||
|
BalanceRecorderUtils.removeOldestDynamics(dynamics);
|
||||||
|
|
||||||
|
Assert.assertEquals(0, dynamics.size());
|
||||||
|
}
|
||||||
|
}
|
Loading…
x
Reference in New Issue
Block a user