mirror of
https://github.com/Qortal/qortal.git
synced 2025-02-11 17:55:50 +00:00
Merge pull request #244 from kennycud/master
Balance Recorder & Hard Forks
This commit is contained in:
commit
ea9a24dca2
173
src/main/java/org/hsqldb/jdbc/HSQLDBPoolMonitored.java
Normal file
173
src/main/java/org/hsqldb/jdbc/HSQLDBPoolMonitored.java
Normal file
@ -0,0 +1,173 @@
|
||||
package org.hsqldb.jdbc;
|
||||
|
||||
import org.apache.logging.log4j.LogManager;
|
||||
import org.apache.logging.log4j.Logger;
|
||||
import org.hsqldb.jdbc.pool.JDBCPooledConnection;
|
||||
import org.qortal.data.system.DbConnectionInfo;
|
||||
import org.qortal.repository.hsqldb.HSQLDBRepositoryFactory;
|
||||
|
||||
import javax.sql.ConnectionEvent;
|
||||
import javax.sql.PooledConnection;
|
||||
import java.sql.Connection;
|
||||
import java.sql.SQLException;
|
||||
import java.util.Comparator;
|
||||
import java.util.List;
|
||||
import java.util.concurrent.ConcurrentHashMap;
|
||||
import java.util.stream.Collectors;
|
||||
|
||||
/**
|
||||
* Class HSQLDBPoolMonitored
|
||||
*
|
||||
* This class uses the same logic as HSQLDBPool. The only difference is it monitors the state of every connection
|
||||
* to the database. This is used for debugging purposes only.
|
||||
*/
|
||||
public class HSQLDBPoolMonitored extends HSQLDBPool {
|
||||
|
||||
private static final Logger LOGGER = LogManager.getLogger(HSQLDBRepositoryFactory.class);
|
||||
|
||||
private static final String EMPTY = "Empty";
|
||||
private static final String AVAILABLE = "Available";
|
||||
private static final String ALLOCATED = "Allocated";
|
||||
|
||||
private ConcurrentHashMap<Integer, DbConnectionInfo> infoByIndex;
|
||||
|
||||
public HSQLDBPoolMonitored(int poolSize) {
|
||||
super(poolSize);
|
||||
|
||||
this.infoByIndex = new ConcurrentHashMap<>(poolSize);
|
||||
}
|
||||
|
||||
/**
|
||||
* Tries to retrieve a new connection using the properties that have already been
|
||||
* set.
|
||||
*
|
||||
* @return a connection to the data source, or null if no spare connections in pool
|
||||
* @exception SQLException if a database access error occurs
|
||||
*/
|
||||
public Connection tryConnection() throws SQLException {
|
||||
for (int i = 0; i < states.length(); i++) {
|
||||
if (states.compareAndSet(i, RefState.available, RefState.allocated)) {
|
||||
JDBCPooledConnection pooledConnection = connections[i];
|
||||
|
||||
if (pooledConnection == null)
|
||||
// Probably shutdown situation
|
||||
return null;
|
||||
|
||||
infoByIndex.put(i, new DbConnectionInfo(System.currentTimeMillis(), Thread.currentThread().getName(), ALLOCATED));
|
||||
|
||||
return pooledConnection.getConnection();
|
||||
}
|
||||
|
||||
if (states.compareAndSet(i, RefState.empty, RefState.allocated)) {
|
||||
try {
|
||||
JDBCPooledConnection pooledConnection = (JDBCPooledConnection) source.getPooledConnection();
|
||||
|
||||
if (pooledConnection == null)
|
||||
// Probably shutdown situation
|
||||
return null;
|
||||
|
||||
pooledConnection.addConnectionEventListener(this);
|
||||
pooledConnection.addStatementEventListener(this);
|
||||
connections[i] = pooledConnection;
|
||||
|
||||
infoByIndex.put(i, new DbConnectionInfo(System.currentTimeMillis(), Thread.currentThread().getName(), ALLOCATED));
|
||||
|
||||
return pooledConnection.getConnection();
|
||||
} catch (SQLException e) {
|
||||
states.set(i, RefState.empty);
|
||||
infoByIndex.put(i, new DbConnectionInfo(System.currentTimeMillis(), Thread.currentThread().getName(), EMPTY));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
public Connection getConnection() throws SQLException {
|
||||
int var1 = 300;
|
||||
if (this.source.loginTimeout != 0) {
|
||||
var1 = this.source.loginTimeout * 10;
|
||||
}
|
||||
|
||||
if (this.closed) {
|
||||
throw new SQLException("connection pool is closed");
|
||||
} else {
|
||||
for(int var2 = 0; var2 < var1; ++var2) {
|
||||
for(int var3 = 0; var3 < this.states.length(); ++var3) {
|
||||
if (this.states.compareAndSet(var3, 1, 2)) {
|
||||
infoByIndex.put(var3, new DbConnectionInfo(System.currentTimeMillis(), Thread.currentThread().getName(), ALLOCATED));
|
||||
return this.connections[var3].getConnection();
|
||||
}
|
||||
|
||||
if (this.states.compareAndSet(var3, 0, 2)) {
|
||||
try {
|
||||
JDBCPooledConnection var4 = (JDBCPooledConnection)this.source.getPooledConnection();
|
||||
var4.addConnectionEventListener(this);
|
||||
var4.addStatementEventListener(this);
|
||||
this.connections[var3] = var4;
|
||||
|
||||
infoByIndex.put(var3, new DbConnectionInfo(System.currentTimeMillis(), Thread.currentThread().getName(), ALLOCATED));
|
||||
|
||||
return this.connections[var3].getConnection();
|
||||
} catch (SQLException var6) {
|
||||
this.states.set(var3, 0);
|
||||
infoByIndex.put(var3, new DbConnectionInfo(System.currentTimeMillis(), Thread.currentThread().getName(), EMPTY));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
try {
|
||||
Thread.sleep(100L);
|
||||
} catch (InterruptedException var5) {
|
||||
}
|
||||
}
|
||||
|
||||
throw JDBCUtil.invalidArgument();
|
||||
}
|
||||
}
|
||||
|
||||
public void connectionClosed(ConnectionEvent event) {
|
||||
PooledConnection connection = (PooledConnection) event.getSource();
|
||||
|
||||
for (int i = 0; i < connections.length; i++) {
|
||||
if (connections[i] == connection) {
|
||||
states.set(i, RefState.available);
|
||||
infoByIndex.put(i, new DbConnectionInfo(System.currentTimeMillis(), Thread.currentThread().getName(), AVAILABLE));
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public void connectionErrorOccurred(ConnectionEvent event) {
|
||||
PooledConnection connection = (PooledConnection) event.getSource();
|
||||
|
||||
for (int i = 0; i < connections.length; i++) {
|
||||
if (connections[i] == connection) {
|
||||
states.set(i, RefState.allocated);
|
||||
connections[i] = null;
|
||||
states.set(i, RefState.empty);
|
||||
infoByIndex.put(i, new DbConnectionInfo(System.currentTimeMillis(), Thread.currentThread().getName(), EMPTY));
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public List<DbConnectionInfo> getDbConnectionsStates() {
|
||||
|
||||
return infoByIndex.values().stream()
|
||||
.sorted(Comparator.comparingLong(DbConnectionInfo::getUpdated))
|
||||
.collect(Collectors.toList());
|
||||
}
|
||||
|
||||
private int findConnectionIndex(ConnectionEvent connectionEvent) {
|
||||
PooledConnection pooledConnection = (PooledConnection) connectionEvent.getSource();
|
||||
|
||||
for(int i = 0; i < this.connections.length; ++i) {
|
||||
if (this.connections[i] == pooledConnection) {
|
||||
return i;
|
||||
}
|
||||
}
|
||||
|
||||
return -1;
|
||||
}
|
||||
}
|
@ -216,7 +216,16 @@ public class Account {
|
||||
String myAddress = accountData.getAddress();
|
||||
|
||||
int blockchainHeight = this.repository.getBlockRepository().getBlockchainHeight();
|
||||
int levelToMint = BlockChain.getInstance().getMinAccountLevelToMint();
|
||||
|
||||
int levelToMint;
|
||||
|
||||
if( blockchainHeight >= BlockChain.getInstance().getIgnoreLevelForRewardShareHeight() ) {
|
||||
levelToMint = 0;
|
||||
}
|
||||
else {
|
||||
levelToMint = BlockChain.getInstance().getMinAccountLevelToMint();
|
||||
}
|
||||
|
||||
int level = accountData.getLevel();
|
||||
int groupIdToMint = BlockChain.getInstance().getMintingGroupId();
|
||||
int nameCheckHeight = BlockChain.getInstance().getOnlyMintWithNameHeight();
|
||||
@ -306,6 +315,9 @@ public class Account {
|
||||
if (Account.isFounder(accountData.getFlags()) && accountData.getBlocksMintedPenalty() == 0)
|
||||
return true;
|
||||
|
||||
if( this.repository.getBlockRepository().getBlockchainHeight() >= BlockChain.getInstance().getIgnoreLevelForRewardShareHeight() )
|
||||
return true;
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
|
50
src/main/java/org/qortal/api/model/DatasetStatus.java
Normal file
50
src/main/java/org/qortal/api/model/DatasetStatus.java
Normal file
@ -0,0 +1,50 @@
|
||||
package org.qortal.api.model;
|
||||
|
||||
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 DatasetStatus {
|
||||
|
||||
private String name;
|
||||
|
||||
private long count;
|
||||
|
||||
public DatasetStatus() {}
|
||||
|
||||
public DatasetStatus(String name, long count) {
|
||||
this.name = name;
|
||||
this.count = count;
|
||||
}
|
||||
|
||||
public String getName() {
|
||||
return name;
|
||||
}
|
||||
|
||||
public long getCount() {
|
||||
return count;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean equals(Object o) {
|
||||
if (this == o) return true;
|
||||
if (o == null || getClass() != o.getClass()) return false;
|
||||
DatasetStatus that = (DatasetStatus) o;
|
||||
return count == that.count && Objects.equals(name, that.name);
|
||||
}
|
||||
|
||||
@Override
|
||||
public int hashCode() {
|
||||
return Objects.hash(name, count);
|
||||
}
|
||||
|
||||
@Override
|
||||
public String toString() {
|
||||
return "DatasetStatus{" +
|
||||
"name='" + name + '\'' +
|
||||
", count=" + count +
|
||||
'}';
|
||||
}
|
||||
}
|
@ -16,9 +16,13 @@ import org.qortal.api.model.AggregatedOrder;
|
||||
import org.qortal.api.model.TradeWithOrderInfo;
|
||||
import org.qortal.api.resource.TransactionsResource.ConfirmationStatus;
|
||||
import org.qortal.asset.Asset;
|
||||
import org.qortal.controller.hsqldb.HSQLDBBalanceRecorder;
|
||||
import org.qortal.crypto.Crypto;
|
||||
import org.qortal.data.account.AccountBalanceData;
|
||||
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.OrderData;
|
||||
import org.qortal.data.asset.RecentTradeData;
|
||||
@ -33,6 +37,7 @@ import org.qortal.transaction.Transaction;
|
||||
import org.qortal.transaction.Transaction.ValidationResult;
|
||||
import org.qortal.transform.TransformationException;
|
||||
import org.qortal.transform.transaction.*;
|
||||
import org.qortal.utils.BalanceRecorderUtils;
|
||||
import org.qortal.utils.Base58;
|
||||
|
||||
import javax.servlet.http.HttpServletRequest;
|
||||
@ -42,6 +47,7 @@ import javax.ws.rs.core.MediaType;
|
||||
import java.util.ArrayList;
|
||||
import java.util.Collections;
|
||||
import java.util.List;
|
||||
import java.util.Optional;
|
||||
import java.util.stream.Collectors;
|
||||
|
||||
@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, false));
|
||||
|
||||
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
|
||||
@Path("/openorders/{assetid}/{otherassetid}")
|
||||
@Operation(
|
||||
|
@ -19,6 +19,8 @@ import org.qortal.crypto.Crypto;
|
||||
import org.qortal.data.account.AccountData;
|
||||
import org.qortal.data.block.BlockData;
|
||||
import org.qortal.data.block.BlockSummaryData;
|
||||
import org.qortal.data.block.DecodedOnlineAccountData;
|
||||
import org.qortal.data.network.OnlineAccountData;
|
||||
import org.qortal.data.transaction.TransactionData;
|
||||
import org.qortal.repository.BlockArchiveReader;
|
||||
import org.qortal.repository.DataException;
|
||||
@ -27,6 +29,7 @@ import org.qortal.repository.RepositoryManager;
|
||||
import org.qortal.transform.TransformationException;
|
||||
import org.qortal.transform.block.BlockTransformer;
|
||||
import org.qortal.utils.Base58;
|
||||
import org.qortal.utils.Blocks;
|
||||
import org.qortal.utils.Triple;
|
||||
|
||||
import javax.servlet.http.HttpServletRequest;
|
||||
@ -45,6 +48,7 @@ import java.util.ArrayList;
|
||||
import java.util.Arrays;
|
||||
import java.util.Comparator;
|
||||
import java.util.List;
|
||||
import java.util.Set;
|
||||
|
||||
@Path("/blocks")
|
||||
@Tag(name = "Blocks")
|
||||
@ -889,4 +893,50 @@ public class BlocksResource {
|
||||
throw ApiExceptionFactory.INSTANCE.createException(request, ApiError.REPOSITORY_ISSUE, e);
|
||||
}
|
||||
}
|
||||
|
||||
@GET
|
||||
@Path("/onlineaccounts/{height}")
|
||||
@Operation(
|
||||
summary = "Get online accounts for block",
|
||||
description = "Returns the online accounts who submitted signatures for this block",
|
||||
responses = {
|
||||
@ApiResponse(
|
||||
description = "online accounts",
|
||||
content = @Content(
|
||||
array = @ArraySchema(
|
||||
schema = @Schema(
|
||||
implementation = DecodedOnlineAccountData.class
|
||||
)
|
||||
)
|
||||
)
|
||||
)
|
||||
}
|
||||
)
|
||||
@ApiErrors({
|
||||
ApiError.BLOCK_UNKNOWN, ApiError.REPOSITORY_ISSUE
|
||||
})
|
||||
public Set<DecodedOnlineAccountData> getOnlineAccounts(@PathParam("height") int height) {
|
||||
|
||||
try (final Repository repository = RepositoryManager.getRepository()) {
|
||||
|
||||
// get block from database
|
||||
BlockData blockData = repository.getBlockRepository().fromHeight(height);
|
||||
|
||||
// if block data is not in the database, then try the archive
|
||||
if (blockData == null) {
|
||||
blockData = repository.getBlockArchiveRepository().fromHeight(height);
|
||||
|
||||
// if the block is not in the database or the archive, then the block is unknown
|
||||
if( blockData == null ) {
|
||||
throw ApiExceptionFactory.INSTANCE.createException(request, ApiError.BLOCK_UNKNOWN);
|
||||
}
|
||||
}
|
||||
|
||||
Set<DecodedOnlineAccountData> onlineAccounts = Blocks.getDecodedOnlineAccountsForBlock(repository, blockData);
|
||||
|
||||
return onlineAccounts;
|
||||
} catch (DataException e) {
|
||||
throw ApiExceptionFactory.INSTANCE.createException(request, ApiError.REPOSITORY_ISSUE);
|
||||
}
|
||||
}
|
||||
}
|
@ -52,6 +52,8 @@ import java.util.*;
|
||||
import java.util.function.Supplier;
|
||||
import java.util.stream.Collectors;
|
||||
|
||||
|
||||
|
||||
@Path("/crosschain")
|
||||
@Tag(name = "Cross-Chain")
|
||||
public class CrossChainResource {
|
||||
@ -255,6 +257,12 @@ public class CrossChainResource {
|
||||
description = "Only return trades that completed on/after this timestamp (milliseconds since epoch)",
|
||||
example = "1597310000000"
|
||||
) @QueryParam("minimumTimestamp") Long minimumTimestamp,
|
||||
@Parameter(
|
||||
description = "Optionally filter by buyer Qortal address"
|
||||
) @QueryParam("buyerAddress") String buyerAddress,
|
||||
@Parameter(
|
||||
description = "Optionally filter by seller Qortal address"
|
||||
) @QueryParam("sellerAddress") String sellerAddress,
|
||||
@Parameter( ref = "limit") @QueryParam("limit") Integer limit,
|
||||
@Parameter( ref = "offset" ) @QueryParam("offset") Integer offset,
|
||||
@Parameter( ref = "reverse" ) @QueryParam("reverse") Boolean reverse) {
|
||||
@ -296,7 +304,7 @@ public class CrossChainResource {
|
||||
byte[] codeHash = acctInfo.getKey().value;
|
||||
ACCT acct = acctInfo.getValue().get();
|
||||
|
||||
List<ATStateData> atStates = repository.getATRepository().getMatchingFinalATStates(codeHash,
|
||||
List<ATStateData> atStates = repository.getATRepository().getMatchingFinalATStates(codeHash, buyerAddress, sellerAddress,
|
||||
isFinished, acct.getModeByteOffset(), (long) AcctMode.REDEEMED.value, minimumFinalHeight,
|
||||
limit, offset, reverse);
|
||||
|
||||
|
@ -32,6 +32,7 @@ import org.qortal.controller.Synchronizer.SynchronizationResult;
|
||||
import org.qortal.controller.repository.BlockArchiveRebuilder;
|
||||
import org.qortal.data.account.MintingAccountData;
|
||||
import org.qortal.data.account.RewardShareData;
|
||||
import org.qortal.data.system.DbConnectionInfo;
|
||||
import org.qortal.network.Network;
|
||||
import org.qortal.network.Peer;
|
||||
import org.qortal.network.PeerAddress;
|
||||
@ -40,6 +41,7 @@ import org.qortal.repository.DataException;
|
||||
import org.qortal.repository.Repository;
|
||||
import org.qortal.repository.RepositoryManager;
|
||||
import org.qortal.settings.Settings;
|
||||
import org.qortal.data.system.SystemInfo;
|
||||
import org.qortal.utils.Base58;
|
||||
import org.qortal.utils.NTP;
|
||||
|
||||
@ -52,6 +54,7 @@ import java.net.InetSocketAddress;
|
||||
import java.net.UnknownHostException;
|
||||
import java.nio.file.Files;
|
||||
import java.nio.file.Paths;
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
import java.util.concurrent.TimeUnit;
|
||||
import java.util.concurrent.TimeoutException;
|
||||
@ -1064,4 +1067,50 @@ public class AdminResource {
|
||||
return "true";
|
||||
}
|
||||
|
||||
@GET
|
||||
@Path("/systeminfo")
|
||||
@Operation(
|
||||
summary = "System Information",
|
||||
description = "System memory usage and available processors.",
|
||||
responses = {
|
||||
@ApiResponse(
|
||||
description = "memory usage and available processors",
|
||||
content = @Content(mediaType = MediaType.APPLICATION_JSON, schema = @Schema(implementation = SystemInfo.class))
|
||||
)
|
||||
}
|
||||
)
|
||||
@ApiErrors({ApiError.REPOSITORY_ISSUE})
|
||||
public SystemInfo getSystemInformation() {
|
||||
|
||||
SystemInfo info
|
||||
= new SystemInfo(
|
||||
Runtime.getRuntime().freeMemory(),
|
||||
Runtime.getRuntime().totalMemory() - Runtime.getRuntime().freeMemory(),
|
||||
Runtime.getRuntime().totalMemory(),
|
||||
Runtime.getRuntime().maxMemory(),
|
||||
Runtime.getRuntime().availableProcessors());
|
||||
|
||||
return info;
|
||||
}
|
||||
|
||||
@GET
|
||||
@Path("/dbstates")
|
||||
@Operation(
|
||||
summary = "Get DB States",
|
||||
description = "Get DB States",
|
||||
responses = {
|
||||
@ApiResponse(
|
||||
content = @Content(mediaType = MediaType.APPLICATION_JSON, array = @ArraySchema(schema = @Schema(implementation = DbConnectionInfo.class)))
|
||||
)
|
||||
}
|
||||
)
|
||||
public List<DbConnectionInfo> getDbConnectionsStates() {
|
||||
|
||||
try {
|
||||
return Controller.REPOSITORY_FACTORY.getDbConnectionsStates();
|
||||
} catch (Exception e) {
|
||||
LOGGER.error(e.getMessage(), e);
|
||||
return new ArrayList<>(0);
|
||||
}
|
||||
}
|
||||
}
|
@ -98,7 +98,7 @@ public class TradeOffersWebSocket extends ApiWebSocket implements Listener {
|
||||
byte[] codeHash = acctInfo.getKey().value;
|
||||
ACCT acct = acctInfo.getValue().get();
|
||||
|
||||
List<ATStateData> atStates = repository.getATRepository().getMatchingFinalATStates(codeHash,
|
||||
List<ATStateData> atStates = repository.getATRepository().getMatchingFinalATStates(codeHash, null, null,
|
||||
isFinished, dataByteOffset, expectedValue, minimumFinalHeight,
|
||||
null, null, null);
|
||||
|
||||
@ -259,7 +259,7 @@ public class TradeOffersWebSocket extends ApiWebSocket implements Listener {
|
||||
ACCT acct = acctInfo.getValue().get();
|
||||
|
||||
Integer dataByteOffset = acct.getModeByteOffset();
|
||||
List<ATStateData> initialAtStates = repository.getATRepository().getMatchingFinalATStates(codeHash,
|
||||
List<ATStateData> initialAtStates = repository.getATRepository().getMatchingFinalATStates(codeHash, null, null,
|
||||
isFinished, dataByteOffset, expectedValue, minimumFinalHeight,
|
||||
null, null, null);
|
||||
|
||||
@ -298,7 +298,7 @@ public class TradeOffersWebSocket extends ApiWebSocket implements Listener {
|
||||
byte[] codeHash = acctInfo.getKey().value;
|
||||
ACCT acct = acctInfo.getValue().get();
|
||||
|
||||
List<ATStateData> historicAtStates = repository.getATRepository().getMatchingFinalATStates(codeHash,
|
||||
List<ATStateData> historicAtStates = repository.getATRepository().getMatchingFinalATStates(codeHash, null, null,
|
||||
isFinished, dataByteOffset, expectedValue, minimumFinalHeight,
|
||||
null, null, null);
|
||||
|
||||
|
@ -23,8 +23,10 @@ import org.qortal.data.at.ATStateData;
|
||||
import org.qortal.data.block.BlockData;
|
||||
import org.qortal.data.block.BlockSummaryData;
|
||||
import org.qortal.data.block.BlockTransactionData;
|
||||
import org.qortal.data.group.GroupAdminData;
|
||||
import org.qortal.data.network.OnlineAccountData;
|
||||
import org.qortal.data.transaction.TransactionData;
|
||||
import org.qortal.group.Group;
|
||||
import org.qortal.repository.*;
|
||||
import org.qortal.settings.Settings;
|
||||
import org.qortal.transaction.AtTransaction;
|
||||
@ -170,6 +172,19 @@ public class Block {
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Get Effective Minting Level
|
||||
*
|
||||
* @return the effective minting level, if a data exception is thrown, it catches the exception and returns a zero
|
||||
*/
|
||||
public int getEffectiveMintingLevel() {
|
||||
try {
|
||||
return this.mintingAccount.getEffectiveMintingLevel();
|
||||
} catch (DataException e) {
|
||||
return 0;
|
||||
}
|
||||
}
|
||||
|
||||
public Account getMintingAccount() {
|
||||
return this.mintingAccount;
|
||||
}
|
||||
@ -186,7 +201,7 @@ public class Block {
|
||||
* @return account-level share "bin" from blockchain config, or null if founder / none found
|
||||
*/
|
||||
public AccountLevelShareBin getShareBin(int blockHeight) {
|
||||
if (this.isMinterFounder)
|
||||
if (this.isMinterFounder && blockHeight < BlockChain.getInstance().getAdminsReplaceFoundersHeight())
|
||||
return null;
|
||||
|
||||
final int accountLevel = this.mintingAccountData.getLevel();
|
||||
@ -403,7 +418,9 @@ public class Block {
|
||||
onlineAccounts.removeIf(a -> a.getNonce() == null || a.getNonce() < 0);
|
||||
|
||||
// After feature trigger, remove any online accounts that are level 0
|
||||
if (height >= BlockChain.getInstance().getOnlineAccountMinterLevelValidationHeight()) {
|
||||
// but only if they are before the ignore level feature trigger
|
||||
if (height < BlockChain.getInstance().getIgnoreLevelForRewardShareHeight() &&
|
||||
height >= BlockChain.getInstance().getOnlineAccountMinterLevelValidationHeight()) {
|
||||
onlineAccounts.removeIf(a -> {
|
||||
try {
|
||||
return Account.getRewardShareEffectiveMintingLevel(repository, a.getPublicKey()) == 0;
|
||||
@ -736,14 +753,6 @@ public class Block {
|
||||
List<ExpandedAccount> expandedAccounts = new ArrayList<>();
|
||||
|
||||
for (RewardShareData rewardShare : this.cachedOnlineRewardShares) {
|
||||
int groupId = BlockChain.getInstance().getMintingGroupId();
|
||||
String address = rewardShare.getMinter();
|
||||
boolean isMinterGroupMember = repository.getGroupRepository().memberExists(groupId, address);
|
||||
|
||||
if (this.getBlockData().getHeight() < BlockChain.getInstance().getFixBatchRewardHeight())
|
||||
expandedAccounts.add(new ExpandedAccount(repository, rewardShare));
|
||||
|
||||
if (this.getBlockData().getHeight() >= BlockChain.getInstance().getFixBatchRewardHeight() && isMinterGroupMember)
|
||||
expandedAccounts.add(new ExpandedAccount(repository, rewardShare));
|
||||
}
|
||||
|
||||
@ -1154,23 +1163,32 @@ public class Block {
|
||||
if (onlineRewardShares == null)
|
||||
return ValidationResult.ONLINE_ACCOUNT_UNKNOWN;
|
||||
|
||||
// After feature trigger, require all online account minters to be greater than level 0
|
||||
if (this.getBlockData().getHeight() >= BlockChain.getInstance().getOnlineAccountMinterLevelValidationHeight()) {
|
||||
List<ExpandedAccount> expandedAccounts = this.getExpandedAccounts();
|
||||
for (ExpandedAccount account : expandedAccounts) {
|
||||
int groupId = BlockChain.getInstance().getMintingGroupId();
|
||||
String address = account.getMintingAccount().getAddress();
|
||||
boolean isMinterGroupMember = repository.getGroupRepository().memberExists(groupId, address);
|
||||
// After feature trigger, require all online account minters to be greater than level 0,
|
||||
// but only if it is before the feature trigger where we ignore level again
|
||||
if (this.blockData.getHeight() < BlockChain.getInstance().getIgnoreLevelForRewardShareHeight() &&
|
||||
this.getBlockData().getHeight() >= BlockChain.getInstance().getOnlineAccountMinterLevelValidationHeight()) {
|
||||
List<ExpandedAccount> expandedAccounts
|
||||
= this.getExpandedAccounts().stream()
|
||||
.filter(expandedAccount -> expandedAccount.isMinterMember)
|
||||
.collect(Collectors.toList());
|
||||
|
||||
for (ExpandedAccount account : expandedAccounts) {
|
||||
if (account.getMintingAccount().getEffectiveMintingLevel() == 0)
|
||||
return ValidationResult.ONLINE_ACCOUNTS_INVALID;
|
||||
|
||||
if (this.getBlockData().getHeight() >= BlockChain.getInstance().getFixBatchRewardHeight()) {
|
||||
if (!isMinterGroupMember)
|
||||
if (!account.isMinterMember)
|
||||
return ValidationResult.ONLINE_ACCOUNTS_INVALID;
|
||||
}
|
||||
}
|
||||
}
|
||||
else if (this.blockData.getHeight() >= BlockChain.getInstance().getIgnoreLevelForRewardShareHeight()){
|
||||
Optional<ExpandedAccount> anyInvalidAccount
|
||||
= this.getExpandedAccounts().stream()
|
||||
.filter(account -> !account.isMinterMember)
|
||||
.findAny();
|
||||
if( anyInvalidAccount.isPresent() ) return ValidationResult.ONLINE_ACCOUNTS_INVALID;
|
||||
}
|
||||
|
||||
// If block is past a certain age then we simply assume the signatures were correct
|
||||
long signatureRequirementThreshold = NTP.getTime() - BlockChain.getInstance().getOnlineAccountSignaturesMinLifetime();
|
||||
@ -1659,7 +1677,17 @@ public class Block {
|
||||
final List<Integer> cumulativeBlocksByLevel = BlockChain.getInstance().getCumulativeBlocksByLevel();
|
||||
final int maximumLevel = cumulativeBlocksByLevel.size() - 1;
|
||||
|
||||
final List<ExpandedAccount> expandedAccounts = this.getExpandedAccounts();
|
||||
final List<ExpandedAccount> expandedAccounts;
|
||||
|
||||
if (this.getBlockData().getHeight() < BlockChain.getInstance().getFixBatchRewardHeight()) {
|
||||
expandedAccounts = this.getExpandedAccounts().stream().collect(Collectors.toList());
|
||||
}
|
||||
else {
|
||||
expandedAccounts
|
||||
= this.getExpandedAccounts().stream()
|
||||
.filter(expandedAccount -> expandedAccount.isMinterMember)
|
||||
.collect(Collectors.toList());
|
||||
}
|
||||
|
||||
Set<AccountData> allUniqueExpandedAccounts = new HashSet<>();
|
||||
for (ExpandedAccount expandedAccount : expandedAccounts) {
|
||||
@ -2059,7 +2087,17 @@ public class Block {
|
||||
final List<Integer> cumulativeBlocksByLevel = BlockChain.getInstance().getCumulativeBlocksByLevel();
|
||||
final int maximumLevel = cumulativeBlocksByLevel.size() - 1;
|
||||
|
||||
final List<ExpandedAccount> expandedAccounts = this.getExpandedAccounts();
|
||||
final List<ExpandedAccount> expandedAccounts;
|
||||
|
||||
if (this.getBlockData().getHeight() < BlockChain.getInstance().getFixBatchRewardHeight()) {
|
||||
expandedAccounts = this.getExpandedAccounts().stream().collect(Collectors.toList());
|
||||
}
|
||||
else {
|
||||
expandedAccounts
|
||||
= this.getExpandedAccounts().stream()
|
||||
.filter(expandedAccount -> expandedAccount.isMinterMember)
|
||||
.collect(Collectors.toList());
|
||||
}
|
||||
|
||||
Set<AccountData> allUniqueExpandedAccounts = new HashSet<>();
|
||||
for (ExpandedAccount expandedAccount : expandedAccounts) {
|
||||
@ -2263,7 +2301,17 @@ public class Block {
|
||||
List<BlockRewardCandidate> rewardCandidates = new ArrayList<>();
|
||||
|
||||
// All online accounts
|
||||
final List<ExpandedAccount> expandedAccounts = this.getExpandedAccounts();
|
||||
final List<ExpandedAccount> expandedAccounts;
|
||||
|
||||
if (this.getBlockData().getHeight() < BlockChain.getInstance().getFixBatchRewardHeight()) {
|
||||
expandedAccounts = this.getExpandedAccounts().stream().collect(Collectors.toList());
|
||||
}
|
||||
else {
|
||||
expandedAccounts
|
||||
= this.getExpandedAccounts().stream()
|
||||
.filter(expandedAccount -> expandedAccount.isMinterMember)
|
||||
.collect(Collectors.toList());
|
||||
}
|
||||
|
||||
/*
|
||||
* Distribution rules:
|
||||
@ -2388,7 +2436,7 @@ public class Block {
|
||||
final long qoraHoldersShare = BlockChain.getInstance().getQoraHoldersShareAtHeight(this.blockData.getHeight());
|
||||
|
||||
// Perform account-level-based reward scaling if appropriate
|
||||
if (!haveFounders) {
|
||||
if (!haveFounders && this.blockData.getHeight() < BlockChain.getInstance().getAdminsReplaceFoundersHeight() ) {
|
||||
// Recalculate distribution ratios based on candidates
|
||||
|
||||
// Nothing shared? This shouldn't happen
|
||||
@ -2424,18 +2472,104 @@ public class Block {
|
||||
}
|
||||
|
||||
// Add founders as reward candidate if appropriate
|
||||
if (haveFounders) {
|
||||
if (haveFounders && this.blockData.getHeight() < BlockChain.getInstance().getAdminsReplaceFoundersHeight()) {
|
||||
// Yes: add to reward candidates list
|
||||
BlockRewardDistributor founderDistributor = (distributionAmount, balanceChanges) -> distributeBlockRewardShare(distributionAmount, onlineFounderAccounts, balanceChanges);
|
||||
|
||||
final long foundersShare = 1_00000000 - totalShares;
|
||||
BlockRewardCandidate rewardCandidate = new BlockRewardCandidate("Founders", foundersShare, founderDistributor);
|
||||
rewardCandidates.add(rewardCandidate);
|
||||
LOGGER.info("logging foundersShare prior to reward modifications {}",foundersShare);
|
||||
}
|
||||
else if (this.blockData.getHeight() >= BlockChain.getInstance().getAdminsReplaceFoundersHeight()) {
|
||||
try (final Repository repository = RepositoryManager.getRepository()) {
|
||||
GroupRepository groupRepository = repository.getGroupRepository();
|
||||
|
||||
// all minter admins
|
||||
List<String> minterAdmins
|
||||
= groupRepository.getGroupAdmins(BlockChain.getInstance().getMintingGroupId()).stream()
|
||||
.map(GroupAdminData::getAdmin)
|
||||
.collect(Collectors.toList());
|
||||
|
||||
// all minter admins that are online
|
||||
List<ExpandedAccount> onlineMinterAdminAccounts
|
||||
= expandedAccounts.stream()
|
||||
.filter(expandedAccount -> minterAdmins.contains(expandedAccount.getMintingAccount().getAddress()))
|
||||
.collect(Collectors.toList());
|
||||
|
||||
long minterAdminShare;
|
||||
|
||||
if( onlineMinterAdminAccounts.isEmpty() ) {
|
||||
minterAdminShare = 0;
|
||||
}
|
||||
else {
|
||||
BlockRewardDistributor minterAdminDistributor
|
||||
= (distributionAmount, balanceChanges)
|
||||
->
|
||||
distributeBlockRewardShare(distributionAmount, onlineMinterAdminAccounts, balanceChanges);
|
||||
|
||||
long adminShare = 1_00000000 - totalShares;
|
||||
LOGGER.info("initial total Shares: {}", totalShares);
|
||||
LOGGER.info("logging adminShare after hardfork, this is the primary reward that will be split {}", adminShare);
|
||||
|
||||
minterAdminShare = adminShare / 2;
|
||||
BlockRewardCandidate minterAdminRewardCandidate
|
||||
= new BlockRewardCandidate("Minter Admins", minterAdminShare, minterAdminDistributor);
|
||||
rewardCandidates.add(minterAdminRewardCandidate);
|
||||
|
||||
totalShares += minterAdminShare;
|
||||
}
|
||||
|
||||
LOGGER.info("MINTER ADMIN SHARE: {}",minterAdminShare);
|
||||
|
||||
// all dev admins
|
||||
List<String> devAdminAddresses
|
||||
= groupRepository.getGroupAdmins(1).stream()
|
||||
.map(GroupAdminData::getAdmin)
|
||||
.collect(Collectors.toList());
|
||||
|
||||
LOGGER.info("Removing NULL Account Address, Dev Admin Count = {}", devAdminAddresses.size());
|
||||
devAdminAddresses.removeIf( address -> Group.NULL_OWNER_ADDRESS.equals(address) );
|
||||
LOGGER.info("Removed NULL Account Address, Dev Admin Count = {}", devAdminAddresses.size());
|
||||
|
||||
BlockRewardDistributor devAdminDistributor
|
||||
= (distributionAmount, balanceChanges) -> distributeToAccounts(distributionAmount, devAdminAddresses, balanceChanges);
|
||||
|
||||
long devAdminShare = 1_00000000 - totalShares;
|
||||
LOGGER.info("DEV ADMIN SHARE: {}",devAdminShare);
|
||||
BlockRewardCandidate devAdminRewardCandidate
|
||||
= new BlockRewardCandidate("Dev Admins", devAdminShare,devAdminDistributor);
|
||||
rewardCandidates.add(devAdminRewardCandidate);
|
||||
}
|
||||
}
|
||||
|
||||
return rewardCandidates;
|
||||
}
|
||||
|
||||
/**
|
||||
* Distribute To Accounts
|
||||
*
|
||||
* Merges distribute shares to a map of distribution shares.
|
||||
*
|
||||
* @param distributionAmount the amount to distribute
|
||||
* @param accountAddressess the addresses to distribute to
|
||||
* @param balanceChanges the map of distribution shares, this gets appended to
|
||||
*
|
||||
* @return the total amount mapped to addresses for distribution
|
||||
*/
|
||||
public static long distributeToAccounts(long distributionAmount, List<String> accountAddressess, Map<String, Long> balanceChanges) {
|
||||
|
||||
if( accountAddressess.isEmpty() ) return 0;
|
||||
|
||||
long distibutionShare = distributionAmount / accountAddressess.size();
|
||||
|
||||
for(String accountAddress : accountAddressess ) {
|
||||
balanceChanges.merge(accountAddress, distibutionShare, Long::sum);
|
||||
}
|
||||
|
||||
return distibutionShare * accountAddressess.size();
|
||||
}
|
||||
|
||||
private static long distributeBlockRewardShare(long distributionAmount, List<ExpandedAccount> accounts, Map<String, Long> balanceChanges) {
|
||||
// Collate all expanded accounts by minting account
|
||||
Map<String, List<ExpandedAccount>> accountsByMinter = new HashMap<>();
|
||||
|
@ -88,7 +88,11 @@ public class BlockChain {
|
||||
onlyMintWithNameHeight,
|
||||
removeOnlyMintWithNameHeight,
|
||||
groupMemberCheckHeight,
|
||||
fixBatchRewardHeight
|
||||
fixBatchRewardHeight,
|
||||
adminsReplaceFoundersHeight,
|
||||
nullGroupMembershipHeight,
|
||||
ignoreLevelForRewardShareHeight,
|
||||
adminQueryFixHeight
|
||||
}
|
||||
|
||||
// Custom transaction fees
|
||||
@ -662,6 +666,22 @@ public class BlockChain {
|
||||
return this.featureTriggers.get(FeatureTrigger.fixBatchRewardHeight.name()).intValue();
|
||||
}
|
||||
|
||||
public int getAdminsReplaceFoundersHeight() {
|
||||
return this.featureTriggers.get(FeatureTrigger.adminsReplaceFoundersHeight.name()).intValue();
|
||||
}
|
||||
|
||||
public int getNullGroupMembershipHeight() {
|
||||
return this.featureTriggers.get(FeatureTrigger.nullGroupMembershipHeight.name()).intValue();
|
||||
}
|
||||
|
||||
public int getIgnoreLevelForRewardShareHeight() {
|
||||
return this.featureTriggers.get(FeatureTrigger.ignoreLevelForRewardShareHeight.name()).intValue();
|
||||
}
|
||||
|
||||
public int getAdminQueryFixHeight() {
|
||||
return this.featureTriggers.get(FeatureTrigger.adminQueryFixHeight.name()).intValue();
|
||||
}
|
||||
|
||||
// More complex getters for aspects that change by height or timestamp
|
||||
|
||||
public long getRewardAtHeight(int ourHeight) {
|
||||
|
@ -2,15 +2,19 @@ package org.qortal.controller.hsqldb;
|
||||
|
||||
import org.apache.logging.log4j.LogManager;
|
||||
import org.apache.logging.log4j.Logger;
|
||||
import org.apache.logging.log4j.util.PropertySource;
|
||||
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.settings.Settings;
|
||||
import org.qortal.utils.BalanceRecorderUtils;
|
||||
|
||||
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.stream.Collectors;
|
||||
|
||||
public class HSQLDBBalanceRecorder extends Thread{
|
||||
@ -23,6 +27,8 @@ public class HSQLDBBalanceRecorder extends Thread{
|
||||
|
||||
private ConcurrentHashMap<String, List<AccountBalanceData>> balancesByAddress = new ConcurrentHashMap<>();
|
||||
|
||||
private CopyOnWriteArrayList<BlockHeightRangeAddressAmounts> balanceDynamics = new CopyOnWriteArrayList<>();
|
||||
|
||||
private int priorityRequested;
|
||||
private int frequency;
|
||||
private int capacity;
|
||||
@ -61,36 +67,52 @@ public class HSQLDBBalanceRecorder extends Thread{
|
||||
|
||||
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) {
|
||||
ArrayList<AccountBalanceData> data;
|
||||
public List<BlockHeightRangeAddressAmounts> getLatestDynamics(int limit, long offset) {
|
||||
|
||||
Optional<Integer> lastHeight = getLastHeight();
|
||||
|
||||
if(lastHeight.isPresent() ) {
|
||||
List<AccountBalanceData> latest = this.balancesByHeight.get(lastHeight.get());
|
||||
|
||||
if( latest != null ) {
|
||||
data = new ArrayList<>(latest.size());
|
||||
data.addAll(
|
||||
latest.stream()
|
||||
.sorted(Comparator.comparingDouble(AccountBalanceData::getBalance).reversed())
|
||||
List<BlockHeightRangeAddressAmounts> latest = this.balanceDynamics.stream()
|
||||
.sorted(BalanceRecorderUtils.BLOCK_HEIGHT_RANGE_ADDRESS_AMOUNTS_COMPARATOR.reversed())
|
||||
.skip(offset)
|
||||
.limit(limit)
|
||||
.collect(Collectors.toList())
|
||||
);
|
||||
}
|
||||
else {
|
||||
data = new ArrayList<>(0);
|
||||
}
|
||||
}
|
||||
else {
|
||||
data = new ArrayList<>(0);
|
||||
.collect(Collectors.toList());
|
||||
|
||||
return latest;
|
||||
}
|
||||
|
||||
return data;
|
||||
public List<BlockHeightRange> getRanges(Integer offset, Integer limit, Boolean reverse) {
|
||||
|
||||
if( reverse ) {
|
||||
return this.balanceDynamics.stream()
|
||||
.map(BlockHeightRangeAddressAmounts::getRange)
|
||||
.sorted(BalanceRecorderUtils.BLOCK_HEIGHT_RANGE_COMPARATOR.reversed())
|
||||
.skip(offset)
|
||||
.limit(limit)
|
||||
.collect(Collectors.toList());
|
||||
}
|
||||
else {
|
||||
return this.balanceDynamics.stream()
|
||||
.map(BlockHeightRangeAddressAmounts::getRange)
|
||||
.sorted(BalanceRecorderUtils.BLOCK_HEIGHT_RANGE_COMPARATOR)
|
||||
.skip(offset)
|
||||
.limit(limit)
|
||||
.collect(Collectors.toList());
|
||||
}
|
||||
}
|
||||
|
||||
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() {
|
||||
|
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 +
|
||||
'}';
|
||||
}
|
||||
}
|
59
src/main/java/org/qortal/data/account/BlockHeightRange.java
Normal file
59
src/main/java/org/qortal/data/account/BlockHeightRange.java
Normal file
@ -0,0 +1,59 @@
|
||||
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;
|
||||
|
||||
private boolean isRewardDistribution;
|
||||
|
||||
public BlockHeightRange() {
|
||||
}
|
||||
|
||||
public BlockHeightRange(int begin, int end, boolean isRewardDistribution) {
|
||||
this.begin = begin;
|
||||
this.end = end;
|
||||
this.isRewardDistribution = isRewardDistribution;
|
||||
}
|
||||
|
||||
public int getBegin() {
|
||||
return begin;
|
||||
}
|
||||
|
||||
public int getEnd() {
|
||||
return end;
|
||||
}
|
||||
|
||||
public boolean isRewardDistribution() {
|
||||
return isRewardDistribution;
|
||||
}
|
||||
|
||||
@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 +
|
||||
", isRewardDistribution=" + isRewardDistribution +
|
||||
'}';
|
||||
}
|
||||
}
|
@ -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 +
|
||||
'}';
|
||||
}
|
||||
}
|
@ -0,0 +1,85 @@
|
||||
package org.qortal.data.block;
|
||||
|
||||
import javax.xml.bind.annotation.XmlAccessType;
|
||||
import javax.xml.bind.annotation.XmlAccessorType;
|
||||
import java.util.Objects;
|
||||
|
||||
// All properties to be converted to JSON via JAX-RS
|
||||
@XmlAccessorType(XmlAccessType.FIELD)
|
||||
public class DecodedOnlineAccountData {
|
||||
|
||||
private long onlineTimestamp;
|
||||
private String minter;
|
||||
private String recipient;
|
||||
private int sharePercent;
|
||||
private boolean minterGroupMember;
|
||||
private String name;
|
||||
private int level;
|
||||
|
||||
public DecodedOnlineAccountData() {
|
||||
}
|
||||
|
||||
public DecodedOnlineAccountData(long onlineTimestamp, String minter, String recipient, int sharePercent, boolean minterGroupMember, String name, int level) {
|
||||
this.onlineTimestamp = onlineTimestamp;
|
||||
this.minter = minter;
|
||||
this.recipient = recipient;
|
||||
this.sharePercent = sharePercent;
|
||||
this.minterGroupMember = minterGroupMember;
|
||||
this.name = name;
|
||||
this.level = level;
|
||||
}
|
||||
|
||||
public long getOnlineTimestamp() {
|
||||
return onlineTimestamp;
|
||||
}
|
||||
|
||||
public String getMinter() {
|
||||
return minter;
|
||||
}
|
||||
|
||||
public String getRecipient() {
|
||||
return recipient;
|
||||
}
|
||||
|
||||
public int getSharePercent() {
|
||||
return sharePercent;
|
||||
}
|
||||
|
||||
public boolean isMinterGroupMember() {
|
||||
return minterGroupMember;
|
||||
}
|
||||
|
||||
public String getName() {
|
||||
return name;
|
||||
}
|
||||
|
||||
public int getLevel() {
|
||||
return level;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean equals(Object o) {
|
||||
if (this == o) return true;
|
||||
if (o == null || getClass() != o.getClass()) return false;
|
||||
DecodedOnlineAccountData that = (DecodedOnlineAccountData) o;
|
||||
return onlineTimestamp == that.onlineTimestamp && sharePercent == that.sharePercent && minterGroupMember == that.minterGroupMember && level == that.level && Objects.equals(minter, that.minter) && Objects.equals(recipient, that.recipient) && Objects.equals(name, that.name);
|
||||
}
|
||||
|
||||
@Override
|
||||
public int hashCode() {
|
||||
return Objects.hash(onlineTimestamp, minter, recipient, sharePercent, minterGroupMember, name, level);
|
||||
}
|
||||
|
||||
@Override
|
||||
public String toString() {
|
||||
return "DecodedOnlineAccountData{" +
|
||||
"onlineTimestamp=" + onlineTimestamp +
|
||||
", minter='" + minter + '\'' +
|
||||
", recipient='" + recipient + '\'' +
|
||||
", sharePercent=" + sharePercent +
|
||||
", minterGroupMember=" + minterGroupMember +
|
||||
", name='" + name + '\'' +
|
||||
", level=" + level +
|
||||
'}';
|
||||
}
|
||||
}
|
35
src/main/java/org/qortal/data/system/DbConnectionInfo.java
Normal file
35
src/main/java/org/qortal/data/system/DbConnectionInfo.java
Normal file
@ -0,0 +1,35 @@
|
||||
package org.qortal.data.system;
|
||||
|
||||
import javax.xml.bind.annotation.XmlAccessType;
|
||||
import javax.xml.bind.annotation.XmlAccessorType;
|
||||
|
||||
@XmlAccessorType(XmlAccessType.FIELD)
|
||||
public class DbConnectionInfo {
|
||||
|
||||
private long updated;
|
||||
|
||||
private String owner;
|
||||
|
||||
private String state;
|
||||
|
||||
public DbConnectionInfo() {
|
||||
}
|
||||
|
||||
public DbConnectionInfo(long timeOpened, String owner, String state) {
|
||||
this.updated = timeOpened;
|
||||
this.owner = owner;
|
||||
this.state = state;
|
||||
}
|
||||
|
||||
public long getUpdated() {
|
||||
return updated;
|
||||
}
|
||||
|
||||
public String getOwner() {
|
||||
return owner;
|
||||
}
|
||||
|
||||
public String getState() {
|
||||
return state;
|
||||
}
|
||||
}
|
49
src/main/java/org/qortal/data/system/SystemInfo.java
Normal file
49
src/main/java/org/qortal/data/system/SystemInfo.java
Normal file
@ -0,0 +1,49 @@
|
||||
package org.qortal.data.system;
|
||||
|
||||
import javax.xml.bind.annotation.XmlAccessType;
|
||||
import javax.xml.bind.annotation.XmlAccessorType;
|
||||
|
||||
@XmlAccessorType(XmlAccessType.FIELD)
|
||||
public class SystemInfo {
|
||||
|
||||
private long freeMemory;
|
||||
|
||||
private long memoryInUse;
|
||||
|
||||
private long totalMemory;
|
||||
|
||||
private long maxMemory;
|
||||
|
||||
private int availableProcessors;
|
||||
|
||||
public SystemInfo() {
|
||||
}
|
||||
|
||||
public SystemInfo(long freeMemory, long memoryInUse, long totalMemory, long maxMemory, int availableProcessors) {
|
||||
this.freeMemory = freeMemory;
|
||||
this.memoryInUse = memoryInUse;
|
||||
this.totalMemory = totalMemory;
|
||||
this.maxMemory = maxMemory;
|
||||
this.availableProcessors = availableProcessors;
|
||||
}
|
||||
|
||||
public long getFreeMemory() {
|
||||
return freeMemory;
|
||||
}
|
||||
|
||||
public long getMemoryInUse() {
|
||||
return memoryInUse;
|
||||
}
|
||||
|
||||
public long getTotalMemory() {
|
||||
return totalMemory;
|
||||
}
|
||||
|
||||
public long getMaxMemory() {
|
||||
return maxMemory;
|
||||
}
|
||||
|
||||
public int getAvailableProcessors() {
|
||||
return availableProcessors;
|
||||
}
|
||||
}
|
@ -2,6 +2,7 @@ package org.qortal.group;
|
||||
|
||||
import org.qortal.account.Account;
|
||||
import org.qortal.account.PublicKeyAccount;
|
||||
import org.qortal.block.BlockChain;
|
||||
import org.qortal.controller.Controller;
|
||||
import org.qortal.crypto.Crypto;
|
||||
import org.qortal.data.group.*;
|
||||
@ -150,8 +151,13 @@ public class Group {
|
||||
// Adminship
|
||||
|
||||
private GroupAdminData getAdmin(String admin) throws DataException {
|
||||
if( repository.getBlockRepository().getBlockchainHeight() < BlockChain.getInstance().getAdminQueryFixHeight()) {
|
||||
return groupRepository.getAdminFaulty(this.groupData.getGroupId(), admin);
|
||||
}
|
||||
else {
|
||||
return groupRepository.getAdmin(this.groupData.getGroupId(), admin);
|
||||
}
|
||||
}
|
||||
|
||||
private boolean adminExists(String admin) throws DataException {
|
||||
return groupRepository.adminExists(this.groupData.getGroupId(), admin);
|
||||
|
@ -76,7 +76,7 @@ public interface ATRepository {
|
||||
* Although <tt>expectedValue</tt>, if provided, is natively an unsigned long,
|
||||
* the data segment comparison is done via unsigned hex string.
|
||||
*/
|
||||
public List<ATStateData> getMatchingFinalATStates(byte[] codeHash, Boolean isFinished,
|
||||
public List<ATStateData> getMatchingFinalATStates(byte[] codeHash, String buyerAddress, String sellerAddress, Boolean isFinished,
|
||||
Integer dataByteOffset, Long expectedValue, Integer minimumFinalHeight,
|
||||
Integer limit, Integer offset, Boolean reverse) throws DataException;
|
||||
|
||||
|
@ -48,6 +48,8 @@ public interface GroupRepository {
|
||||
|
||||
// Group Admins
|
||||
|
||||
public GroupAdminData getAdminFaulty(int groupId, String address) throws DataException;
|
||||
|
||||
public GroupAdminData getAdmin(int groupId, String address) throws DataException;
|
||||
|
||||
public boolean adminExists(int groupId, String address) throws DataException;
|
||||
|
@ -1,6 +1,7 @@
|
||||
package org.qortal.repository.hsqldb;
|
||||
|
||||
import com.google.common.primitives.Longs;
|
||||
|
||||
import org.apache.logging.log4j.LogManager;
|
||||
import org.apache.logging.log4j.Logger;
|
||||
import org.qortal.controller.Controller;
|
||||
@ -16,6 +17,8 @@ import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
import java.util.Set;
|
||||
|
||||
import org.qortal.data.account.AccountData;
|
||||
|
||||
public class HSQLDBATRepository implements ATRepository {
|
||||
|
||||
private static final Logger LOGGER = LogManager.getLogger(HSQLDBATRepository.class);
|
||||
@ -400,7 +403,7 @@ public class HSQLDBATRepository implements ATRepository {
|
||||
}
|
||||
|
||||
@Override
|
||||
public List<ATStateData> getMatchingFinalATStates(byte[] codeHash, Boolean isFinished,
|
||||
public List<ATStateData> getMatchingFinalATStates(byte[] codeHash, String buyerAddress, String sellerAddress, Boolean isFinished,
|
||||
Integer dataByteOffset, Long expectedValue, Integer minimumFinalHeight,
|
||||
Integer limit, Integer offset, Boolean reverse) throws DataException {
|
||||
StringBuilder sql = new StringBuilder(1024);
|
||||
@ -421,10 +424,14 @@ public class HSQLDBATRepository implements ATRepository {
|
||||
|
||||
// Order by AT_address and height to use compound primary key as index
|
||||
// Both must be the same direction (DESC) also
|
||||
sql.append("ORDER BY ATStates.AT_address DESC, ATStates.height DESC "
|
||||
+ "LIMIT 1 "
|
||||
+ ") AS FinalATStates "
|
||||
+ "WHERE code_hash = ? ");
|
||||
sql.append("ORDER BY ATStates.height DESC LIMIT 1) AS FinalATStates ");
|
||||
|
||||
// Optional LEFT JOIN with ATTRANSACTIONS for buyerAddress
|
||||
if (buyerAddress != null && !buyerAddress.isEmpty()) {
|
||||
sql.append("LEFT JOIN ATTRANSACTIONS tx ON tx.at_address = ATs.AT_address ");
|
||||
}
|
||||
|
||||
sql.append("WHERE ATs.code_hash = ? ");
|
||||
bindParams.add(codeHash);
|
||||
|
||||
if (isFinished != null) {
|
||||
@ -443,6 +450,20 @@ public class HSQLDBATRepository implements ATRepository {
|
||||
bindParams.add(rawExpectedValue);
|
||||
}
|
||||
|
||||
if (buyerAddress != null && !buyerAddress.isEmpty()) {
|
||||
sql.append("AND tx.recipient = ? ");
|
||||
bindParams.add(buyerAddress);
|
||||
}
|
||||
|
||||
|
||||
if (sellerAddress != null && !sellerAddress.isEmpty()) {
|
||||
// Convert sellerAddress to publicKey (method depends on your implementation)
|
||||
AccountData accountData = this.repository.getAccountRepository().getAccount(sellerAddress);
|
||||
byte[] publicKey = accountData.getPublicKey();
|
||||
sql.append("AND ATs.creator = ? ");
|
||||
bindParams.add(publicKey);
|
||||
}
|
||||
|
||||
sql.append(" ORDER BY FinalATStates.height ");
|
||||
if (reverse != null && reverse)
|
||||
sql.append("DESC");
|
||||
@ -483,7 +504,7 @@ public class HSQLDBATRepository implements ATRepository {
|
||||
Integer dataByteOffset, Long expectedValue,
|
||||
int minimumCount, int maximumCount, long minimumPeriod) throws DataException {
|
||||
// We need most recent entry first so we can use its timestamp to slice further results
|
||||
List<ATStateData> mostRecentStates = this.getMatchingFinalATStates(codeHash, isFinished,
|
||||
List<ATStateData> mostRecentStates = this.getMatchingFinalATStates(codeHash, null, null, isFinished,
|
||||
dataByteOffset, expectedValue, null,
|
||||
1, 0, true);
|
||||
|
||||
|
@ -3,15 +3,24 @@ 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;
|
||||
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.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;
|
||||
|
||||
import java.sql.ResultSet;
|
||||
import java.sql.SQLException;
|
||||
@ -28,6 +37,7 @@ import java.util.Optional;
|
||||
import java.util.Timer;
|
||||
import java.util.TimerTask;
|
||||
import java.util.concurrent.ConcurrentHashMap;
|
||||
import java.util.concurrent.CopyOnWriteArrayList;
|
||||
import java.util.function.Function;
|
||||
import java.util.function.Predicate;
|
||||
import java.util.function.Supplier;
|
||||
@ -172,6 +182,9 @@ public class HSQLDBCacheUtils {
|
||||
// retain only candidates with names
|
||||
Stream<ArbitraryResourceData> stream = candidates.stream().filter(candidate -> candidate.name != null );
|
||||
|
||||
if(exclude.isPresent())
|
||||
stream = stream.filter( candidate -> !exclude.get().get().contains( candidate.name ));
|
||||
|
||||
// filter by service
|
||||
if( service.isPresent() )
|
||||
stream = stream.filter(candidate -> candidate.service.equals(service.get()));
|
||||
@ -389,14 +402,15 @@ public class HSQLDBCacheUtils {
|
||||
/**
|
||||
* Start Recording Balances
|
||||
*
|
||||
* @param queue the queue to add to, remove oldest data if necssary
|
||||
* @param repository the db repsoitory
|
||||
* @param balancesByHeight height -> account balances
|
||||
* @param balanceDynamics every balance dynamic
|
||||
* @param priorityRequested the requested thread priority
|
||||
* @param frequency the recording frequencies, in minutes
|
||||
* @param capacity the maximum size of balanceDynamics
|
||||
*/
|
||||
public static void startRecordingBalances(
|
||||
final ConcurrentHashMap<Integer, List<AccountBalanceData>> balancesByHeight,
|
||||
final ConcurrentHashMap<String, List<AccountBalanceData>> balancesByAddress,
|
||||
CopyOnWriteArrayList<BlockHeightRangeAddressAmounts> balanceDynamics,
|
||||
int priorityRequested,
|
||||
int frequency,
|
||||
int capacity) {
|
||||
@ -409,12 +423,113 @@ public class HSQLDBCacheUtils {
|
||||
|
||||
Thread.currentThread().setName(BALANCE_RECORDER_TIMER_TASK);
|
||||
|
||||
try (final HSQLDBRepository repository = (HSQLDBRepository) Controller.REPOSITORY_FACTORY.getRepository()) {
|
||||
while (balancesByHeight.size() > capacity + 1) {
|
||||
Optional<Integer> firstHeight = balancesByHeight.keySet().stream().sorted().findFirst();
|
||||
int currentHeight = recordCurrentBalances(balancesByHeight);
|
||||
|
||||
if (firstHeight.isPresent()) balancesByHeight.remove(firstHeight.get());
|
||||
LOGGER.debug("recorded balances: height = " + currentHeight);
|
||||
|
||||
// remove invalidated recordings, recording after current height
|
||||
BalanceRecorderUtils.removeRecordingsAboveHeight(currentHeight, balancesByHeight);
|
||||
|
||||
// remove invalidated dynamics, on or after current height
|
||||
BalanceRecorderUtils.removeDynamicsOnOrAboveHeight(currentHeight, balanceDynamics);
|
||||
|
||||
// if there are 2 or more recordings, then produce balance dynamics for the first 2 recordings
|
||||
if( balancesByHeight.size() > 1 ) {
|
||||
|
||||
Optional<Integer> priorHeight = BalanceRecorderUtils.getPriorHeight(currentHeight, balancesByHeight);
|
||||
|
||||
// if there is a prior height
|
||||
if(priorHeight.isPresent()) {
|
||||
|
||||
boolean isRewardDistribution = BalanceRecorderUtils.isRewardDistributionRange(priorHeight.get(), currentHeight);
|
||||
|
||||
// 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 {
|
||||
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");
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
// wait 5 minutes
|
||||
timer.scheduleAtFixedRate(task, 300_000, frequency * 60_000);
|
||||
}
|
||||
|
||||
private static void produceBalanceDynamics(int currentHeight, Optional<Integer> priorHeight, boolean isRewardDistribution, ConcurrentHashMap<Integer, List<AccountBalanceData>> balancesByHeight, CopyOnWriteArrayList<BlockHeightRangeAddressAmounts> balanceDynamics, int capacity) {
|
||||
BlockHeightRange blockHeightRange = new BlockHeightRange(priorHeight.get(), currentHeight, isRewardDistribution);
|
||||
|
||||
LOGGER.debug("building dynamics for block heights: range = " + blockHeightRange);
|
||||
|
||||
List<AccountBalanceData> currentBalances = balancesByHeight.get(currentHeight);
|
||||
|
||||
ArrayList<TransactionData> transactions = getTransactionDataForBlocks(blockHeightRange);
|
||||
|
||||
LOGGER.info("transactions counted for balance adjustments: count = " + transactions.size());
|
||||
List<AddressAmountData> 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<TransactionData> getTransactionDataForBlocks(BlockHeightRange blockHeightRange) {
|
||||
ArrayList<TransactionData> transactions;
|
||||
|
||||
try (final Repository repository = RepositoryManager.getRepository()) {
|
||||
List<byte[]> 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<Integer, List<AccountBalanceData>> balancesByHeight) {
|
||||
int currentHeight;
|
||||
|
||||
try (final HSQLDBRepository repository = (HSQLDBRepository) Controller.REPOSITORY_FACTORY.getRepository()) {
|
||||
|
||||
// get current balances
|
||||
List<AccountBalanceData> accountBalances = getAccountBalances(repository);
|
||||
@ -427,43 +542,17 @@ public class HSQLDBCacheUtils {
|
||||
// map all new balances to the current height
|
||||
balancesByHeight.put(data.get().getHeight(), accountBalances);
|
||||
|
||||
// for each new balance, map to address
|
||||
for (AccountBalanceData accountBalance : accountBalances) {
|
||||
|
||||
// get recorded balances for this address
|
||||
List<AccountBalanceData> establishedBalances
|
||||
= balancesByAddress.getOrDefault(accountBalance.getAddress(), new ArrayList<>(0));
|
||||
|
||||
// start a new list of recordings for this address, add the new balance and add the established
|
||||
// balances
|
||||
List<AccountBalanceData> balances = new ArrayList<>(establishedBalances.size() + 1);
|
||||
balances.add(accountBalance);
|
||||
balances.addAll(establishedBalances);
|
||||
|
||||
// reset tha balances for this address
|
||||
balancesByAddress.put(accountBalance.getAddress(), balances);
|
||||
|
||||
// TODO: reduce account balances to capacity
|
||||
}
|
||||
|
||||
// reduce height balances to capacity
|
||||
while( balancesByHeight.size() > capacity ) {
|
||||
Optional<Integer> lowestHeight
|
||||
= balancesByHeight.entrySet().stream()
|
||||
.min(Comparator.comparingInt(Map.Entry::getKey))
|
||||
.map(Map.Entry::getKey);
|
||||
|
||||
if (lowestHeight.isPresent()) balancesByHeight.entrySet().remove(lowestHeight);
|
||||
currentHeight = data.get().getHeight();
|
||||
}
|
||||
else {
|
||||
currentHeight = Integer.MAX_VALUE;
|
||||
}
|
||||
} catch (DataException e) {
|
||||
LOGGER.error(e.getMessage(), e);
|
||||
currentHeight = Integer.MAX_VALUE;
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
// wait 5 minutes
|
||||
timer.scheduleAtFixedRate(task, 300_000, frequency * 60_000);
|
||||
return currentHeight;
|
||||
}
|
||||
|
||||
/**
|
||||
|
@ -454,41 +454,41 @@ public class HSQLDBDatabaseUpdates {
|
||||
|
||||
case 12:
|
||||
// Groups
|
||||
// NOTE: We need to set Groups to `Groups` here to avoid SQL Standard Keywords in HSQLDB v2.7.4
|
||||
stmt.execute("CREATE TABLE `Groups` (group_id GroupID, owner QortalAddress NOT NULL, group_name GroupName NOT NULL, "
|
||||
// NOTE: We need to set Groups to `GROUPS` here to avoid SQL Standard Keywords in HSQLDB v2.7.4
|
||||
stmt.execute("CREATE TABLE `GROUPS` (group_id GroupID, owner QortalAddress NOT NULL, group_name GroupName NOT NULL, "
|
||||
+ "created_when EpochMillis NOT NULL, updated_when EpochMillis, is_open BOOLEAN NOT NULL, "
|
||||
+ "approval_threshold TINYINT NOT NULL, min_block_delay INTEGER NOT NULL, max_block_delay INTEGER NOT NULL, "
|
||||
+ "reference Signature, creation_group_id GroupID, reduced_group_name GroupName NOT NULL, "
|
||||
+ "description GenericDescription NOT NULL, PRIMARY KEY (group_id))");
|
||||
// For finding groups by name
|
||||
stmt.execute("CREATE INDEX GroupNameIndex on `Groups` (group_name)");
|
||||
stmt.execute("CREATE INDEX GroupNameIndex on `GROUPS` (group_name)");
|
||||
// For finding groups by reduced name
|
||||
stmt.execute("CREATE INDEX GroupReducedNameIndex on `Groups` (reduced_group_name)");
|
||||
stmt.execute("CREATE INDEX GroupReducedNameIndex on `GROUPS` (reduced_group_name)");
|
||||
// For finding groups by owner
|
||||
stmt.execute("CREATE INDEX GroupOwnerIndex ON `Groups` (owner)");
|
||||
stmt.execute("CREATE INDEX GroupOwnerIndex ON `GROUPS` (owner)");
|
||||
|
||||
// We need a corresponding trigger to make sure new group_id values are assigned sequentially starting from 1
|
||||
stmt.execute("CREATE TRIGGER Group_ID_Trigger BEFORE INSERT ON `Groups` "
|
||||
stmt.execute("CREATE TRIGGER Group_ID_Trigger BEFORE INSERT ON `GROUPS` "
|
||||
+ "REFERENCING NEW ROW AS new_row FOR EACH ROW WHEN (new_row.group_id IS NULL) "
|
||||
+ "SET new_row.group_id = (SELECT IFNULL(MAX(group_id) + 1, 1) FROM `Groups`)");
|
||||
+ "SET new_row.group_id = (SELECT IFNULL(MAX(group_id) + 1, 1) FROM `GROUPS`)");
|
||||
|
||||
// Admins
|
||||
stmt.execute("CREATE TABLE GroupAdmins (group_id GroupID, admin QortalAddress, reference Signature NOT NULL, "
|
||||
+ "PRIMARY KEY (group_id, admin), FOREIGN KEY (group_id) REFERENCES `Groups` (group_id) ON DELETE CASCADE)");
|
||||
+ "PRIMARY KEY (group_id, admin), FOREIGN KEY (group_id) REFERENCES `GROUPS` (group_id) ON DELETE CASCADE)");
|
||||
// For finding groups by admin address
|
||||
stmt.execute("CREATE INDEX GroupAdminIndex ON GroupAdmins (admin)");
|
||||
|
||||
// Members
|
||||
stmt.execute("CREATE TABLE GroupMembers (group_id GroupID, address QortalAddress, "
|
||||
+ "joined_when EpochMillis NOT NULL, reference Signature NOT NULL, "
|
||||
+ "PRIMARY KEY (group_id, address), FOREIGN KEY (group_id) REFERENCES `Groups` (group_id) ON DELETE CASCADE)");
|
||||
+ "PRIMARY KEY (group_id, address), FOREIGN KEY (group_id) REFERENCES `GROUPS` (group_id) ON DELETE CASCADE)");
|
||||
// For finding groups by member address
|
||||
stmt.execute("CREATE INDEX GroupMemberIndex ON GroupMembers (address)");
|
||||
|
||||
// Invites
|
||||
stmt.execute("CREATE TABLE GroupInvites (group_id GroupID, inviter QortalAddress, invitee QortalAddress, "
|
||||
+ "expires_when EpochMillis, reference Signature, "
|
||||
+ "PRIMARY KEY (group_id, invitee), FOREIGN KEY (group_id) REFERENCES `Groups` (group_id) ON DELETE CASCADE)");
|
||||
+ "PRIMARY KEY (group_id, invitee), FOREIGN KEY (group_id) REFERENCES `GROUPS` (group_id) ON DELETE CASCADE)");
|
||||
// For finding invites sent by inviter
|
||||
stmt.execute("CREATE INDEX GroupInviteInviterIndex ON GroupInvites (inviter)");
|
||||
// For finding invites by group
|
||||
@ -504,7 +504,7 @@ public class HSQLDBDatabaseUpdates {
|
||||
// NULL expires_when means does not expire!
|
||||
stmt.execute("CREATE TABLE GroupBans (group_id GroupID, offender QortalAddress, admin QortalAddress NOT NULL, "
|
||||
+ "banned_when EpochMillis NOT NULL, reason GenericDescription NOT NULL, expires_when EpochMillis, reference Signature NOT NULL, "
|
||||
+ "PRIMARY KEY (group_id, offender), FOREIGN KEY (group_id) REFERENCES `Groups` (group_id) ON DELETE CASCADE)");
|
||||
+ "PRIMARY KEY (group_id, offender), FOREIGN KEY (group_id) REFERENCES `GROUPS` (group_id) ON DELETE CASCADE)");
|
||||
// For expiry maintenance
|
||||
stmt.execute("CREATE INDEX GroupBanExpiryIndex ON GroupBans (expires_when)");
|
||||
break;
|
||||
|
@ -351,7 +351,7 @@ public class HSQLDBGroupRepository implements GroupRepository {
|
||||
// Group Admins
|
||||
|
||||
@Override
|
||||
public GroupAdminData getAdmin(int groupId, String address) throws DataException {
|
||||
public GroupAdminData getAdminFaulty(int groupId, String address) throws DataException {
|
||||
try (ResultSet resultSet = this.repository.checkedExecute("SELECT admin, reference FROM GroupAdmins WHERE group_id = ?", groupId)) {
|
||||
if (resultSet == null)
|
||||
return null;
|
||||
@ -365,6 +365,21 @@ public class HSQLDBGroupRepository implements GroupRepository {
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public GroupAdminData getAdmin(int groupId, String address) throws DataException {
|
||||
try (ResultSet resultSet = this.repository.checkedExecute("SELECT admin, reference FROM GroupAdmins WHERE group_id = ? AND admin = ?", groupId, address)) {
|
||||
if (resultSet == null)
|
||||
return null;
|
||||
|
||||
String admin = resultSet.getString(1);
|
||||
byte[] reference = resultSet.getBytes(2);
|
||||
|
||||
return new GroupAdminData(groupId, admin, reference);
|
||||
} catch (SQLException e) {
|
||||
throw new DataException("Unable to fetch group admin from repository", e);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean adminExists(int groupId, String address) throws DataException {
|
||||
try {
|
||||
|
@ -5,6 +5,8 @@ import org.apache.logging.log4j.Logger;
|
||||
import org.hsqldb.HsqlException;
|
||||
import org.hsqldb.error.ErrorCode;
|
||||
import org.hsqldb.jdbc.HSQLDBPool;
|
||||
import org.hsqldb.jdbc.HSQLDBPoolMonitored;
|
||||
import org.qortal.data.system.DbConnectionInfo;
|
||||
import org.qortal.repository.DataException;
|
||||
import org.qortal.repository.Repository;
|
||||
import org.qortal.repository.RepositoryFactory;
|
||||
@ -14,6 +16,8 @@ import java.sql.Connection;
|
||||
import java.sql.DriverManager;
|
||||
import java.sql.SQLException;
|
||||
import java.sql.Statement;
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
import java.util.Properties;
|
||||
|
||||
public class HSQLDBRepositoryFactory implements RepositoryFactory {
|
||||
@ -57,7 +61,13 @@ public class HSQLDBRepositoryFactory implements RepositoryFactory {
|
||||
HSQLDBRepository.attemptRecovery(connectionUrl, "backup");
|
||||
}
|
||||
|
||||
if(Settings.getInstance().isConnectionPoolMonitorEnabled()) {
|
||||
this.connectionPool = new HSQLDBPoolMonitored(Settings.getInstance().getRepositoryConnectionPoolSize());
|
||||
}
|
||||
else {
|
||||
this.connectionPool = new HSQLDBPool(Settings.getInstance().getRepositoryConnectionPoolSize());
|
||||
}
|
||||
|
||||
this.connectionPool.setUrl(this.connectionUrl);
|
||||
|
||||
Properties properties = new Properties();
|
||||
@ -153,4 +163,19 @@ public class HSQLDBRepositoryFactory implements RepositoryFactory {
|
||||
return HSQLDBRepository.isDeadlockException(e);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get Connection States
|
||||
*
|
||||
* Get the database connection states, if database connection pool monitoring is enabled.
|
||||
*
|
||||
* @return the connection states if enabled, otherwise an empty list
|
||||
*/
|
||||
public List<DbConnectionInfo> getDbConnectionsStates() {
|
||||
if( Settings.getInstance().isConnectionPoolMonitorEnabled() ) {
|
||||
return ((HSQLDBPoolMonitored) this.connectionPool).getDbConnectionsStates();
|
||||
}
|
||||
else {
|
||||
return new ArrayList<>(0);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -444,14 +444,70 @@ public class Settings {
|
||||
*/
|
||||
private long archivingPause = 3000;
|
||||
|
||||
/**
|
||||
* Enable Balance Recorder?
|
||||
*
|
||||
* True for balance recording, otherwise 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 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;
|
||||
|
||||
/**
|
||||
* 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;
|
||||
|
||||
/**
|
||||
* Is Reward Recording Only
|
||||
*
|
||||
* Set true to only retain the recordings that cover reward distributions, otherwise set false.
|
||||
*/
|
||||
private boolean rewardRecordingOnly = true;
|
||||
|
||||
/**
|
||||
* Is The Connection Monitored?
|
||||
*
|
||||
* Is the database connection pooled monitored?
|
||||
*/
|
||||
private boolean connectionPoolMonitorEnabled = false;
|
||||
|
||||
// Domain mapping
|
||||
public static class ThreadLimit {
|
||||
private String messageType;
|
||||
@ -1257,4 +1313,24 @@ public class Settings {
|
||||
public boolean isBalanceRecorderEnabled() {
|
||||
return balanceRecorderEnabled;
|
||||
}
|
||||
|
||||
public long getMinimumBalanceRecording() {
|
||||
return minimumBalanceRecording;
|
||||
}
|
||||
|
||||
public long getTopBalanceLoggingLimit() {
|
||||
return topBalanceLoggingLimit;
|
||||
}
|
||||
|
||||
public int getBalanceRecorderRollbackAllowance() {
|
||||
return balanceRecorderRollbackAllowance;
|
||||
}
|
||||
|
||||
public boolean isRewardRecordingOnly() {
|
||||
return rewardRecordingOnly;
|
||||
}
|
||||
|
||||
public boolean isConnectionPoolMonitorEnabled() {
|
||||
return connectionPoolMonitorEnabled;
|
||||
}
|
||||
}
|
||||
|
@ -388,6 +388,7 @@ public class ArbitraryTransaction extends Transaction {
|
||||
// Get the latest transaction
|
||||
ArbitraryTransactionData latestTransactionData = repository.getArbitraryRepository().getLatestTransaction(arbitraryTransactionData.getName(), arbitraryTransactionData.getService(), null, arbitraryTransactionData.getIdentifier());
|
||||
if (latestTransactionData == null) {
|
||||
LOGGER.info("We don't have a latest transaction, so delete from cache: arbitraryResourceData = " + arbitraryResourceData);
|
||||
// We don't have a latest transaction, so delete from cache
|
||||
repository.getArbitraryRepository().delete(arbitraryResourceData);
|
||||
return;
|
||||
@ -397,11 +398,14 @@ public class ArbitraryTransaction extends Transaction {
|
||||
ArbitraryResourceData existingArbitraryResourceData = repository.getArbitraryRepository()
|
||||
.getArbitraryResource(service, name, identifier);
|
||||
|
||||
LOGGER.info("updating existing arbitraryResourceData" + existingArbitraryResourceData);
|
||||
|
||||
// Check for existing cached data
|
||||
if (existingArbitraryResourceData == null) {
|
||||
// Nothing exists yet, so set creation date from the current transaction (it will be reduced later if needed)
|
||||
arbitraryResourceData.created = arbitraryTransactionData.getTimestamp();
|
||||
arbitraryResourceData.updated = null;
|
||||
LOGGER.info("updated = null, reason = existingArbitraryResourceData == null" );
|
||||
}
|
||||
else {
|
||||
// An entry already exists - update created time from current transaction if this is older
|
||||
@ -411,14 +415,20 @@ public class ArbitraryTransaction extends Transaction {
|
||||
if (existingArbitraryResourceData.created == latestTransactionData.getTimestamp()) {
|
||||
// Latest transaction matches created time, so it hasn't been updated
|
||||
arbitraryResourceData.updated = null;
|
||||
LOGGER.info(
|
||||
"updated = null, reason: existingArbitraryResourceData.created == latestTransactionData.getTimestamp() == " +
|
||||
existingArbitraryResourceData.created );
|
||||
}
|
||||
else {
|
||||
arbitraryResourceData.updated = latestTransactionData.getTimestamp();
|
||||
LOGGER.info("setting updated to a non-null value");
|
||||
}
|
||||
}
|
||||
|
||||
arbitraryResourceData.size = latestTransactionData.getSize();
|
||||
|
||||
LOGGER.info("saving updated arbitraryResourceData: updated = " + arbitraryResourceData.updated);
|
||||
|
||||
// Save
|
||||
repository.getArbitraryRepository().save(arbitraryResourceData);
|
||||
}
|
||||
|
@ -2,6 +2,7 @@ package org.qortal.transaction;
|
||||
|
||||
import org.qortal.account.Account;
|
||||
import org.qortal.asset.Asset;
|
||||
import org.qortal.block.BlockChain;
|
||||
import org.qortal.crypto.Crypto;
|
||||
import org.qortal.data.group.GroupData;
|
||||
import org.qortal.data.transaction.CancelGroupBanTransactionData;
|
||||
@ -12,6 +13,7 @@ import org.qortal.repository.Repository;
|
||||
|
||||
import java.util.Collections;
|
||||
import java.util.List;
|
||||
import java.util.Objects;
|
||||
|
||||
public class CancelGroupBanTransaction extends Transaction {
|
||||
|
||||
@ -70,9 +72,26 @@ public class CancelGroupBanTransaction extends Transaction {
|
||||
if (!this.repository.getGroupRepository().adminExists(groupId, admin.getAddress()))
|
||||
return ValidationResult.NOT_GROUP_ADMIN;
|
||||
|
||||
// Can't unban if not group's current owner
|
||||
if( this.repository.getBlockRepository().getBlockchainHeight() < BlockChain.getInstance().getNullGroupMembershipHeight() ) {
|
||||
// Can't cancel ban if not group's current owner
|
||||
if (!admin.getAddress().equals(groupData.getOwner()))
|
||||
return ValidationResult.INVALID_GROUP_OWNER;
|
||||
}
|
||||
// if( this.repository.getBlockRepository().getBlockchainHeight() >= BlockChain.getInstance().getNullGroupMembershipHeight() )
|
||||
else {
|
||||
String groupOwner = this.repository.getGroupRepository().getOwner(groupId);
|
||||
boolean groupOwnedByNullAccount = Objects.equals(groupOwner, Group.NULL_OWNER_ADDRESS);
|
||||
|
||||
// if null ownership group, then check for admin approval
|
||||
if(groupOwnedByNullAccount ) {
|
||||
// Require approval if transaction relates to a group owned by the null account
|
||||
if (!this.needsGroupApproval())
|
||||
return ValidationResult.GROUP_APPROVAL_REQUIRED;
|
||||
}
|
||||
// Can't cancel ban if not group's current owner
|
||||
else if (!admin.getAddress().equals(groupData.getOwner()))
|
||||
return ValidationResult.INVALID_GROUP_OWNER;
|
||||
}
|
||||
|
||||
Account member = getMember();
|
||||
|
||||
|
@ -2,6 +2,7 @@ package org.qortal.transaction;
|
||||
|
||||
import org.qortal.account.Account;
|
||||
import org.qortal.asset.Asset;
|
||||
import org.qortal.block.BlockChain;
|
||||
import org.qortal.crypto.Crypto;
|
||||
import org.qortal.data.group.GroupData;
|
||||
import org.qortal.data.transaction.CancelGroupInviteTransactionData;
|
||||
@ -12,6 +13,7 @@ import org.qortal.repository.Repository;
|
||||
|
||||
import java.util.Collections;
|
||||
import java.util.List;
|
||||
import java.util.Objects;
|
||||
|
||||
public class CancelGroupInviteTransaction extends Transaction {
|
||||
|
||||
@ -80,6 +82,16 @@ public class CancelGroupInviteTransaction extends Transaction {
|
||||
if (admin.getConfirmedBalance(Asset.QORT) < this.cancelGroupInviteTransactionData.getFee())
|
||||
return ValidationResult.NO_BALANCE;
|
||||
|
||||
// if null ownership group, then check for admin approval
|
||||
if( this.repository.getBlockRepository().getBlockchainHeight() >= BlockChain.getInstance().getNullGroupMembershipHeight() ) {
|
||||
String groupOwner = this.repository.getGroupRepository().getOwner(groupId);
|
||||
boolean groupOwnedByNullAccount = Objects.equals(groupOwner, Group.NULL_OWNER_ADDRESS);
|
||||
|
||||
// Require approval if transaction relates to a group owned by the null account
|
||||
if (groupOwnedByNullAccount && !this.needsGroupApproval())
|
||||
return ValidationResult.GROUP_APPROVAL_REQUIRED;
|
||||
}
|
||||
|
||||
return ValidationResult.OK;
|
||||
}
|
||||
|
||||
|
@ -2,6 +2,7 @@ package org.qortal.transaction;
|
||||
|
||||
import org.qortal.account.Account;
|
||||
import org.qortal.asset.Asset;
|
||||
import org.qortal.block.BlockChain;
|
||||
import org.qortal.crypto.Crypto;
|
||||
import org.qortal.data.group.GroupData;
|
||||
import org.qortal.data.transaction.GroupBanTransactionData;
|
||||
@ -12,6 +13,7 @@ import org.qortal.repository.Repository;
|
||||
|
||||
import java.util.Collections;
|
||||
import java.util.List;
|
||||
import java.util.Objects;
|
||||
|
||||
public class GroupBanTransaction extends Transaction {
|
||||
|
||||
@ -70,9 +72,25 @@ public class GroupBanTransaction extends Transaction {
|
||||
if (!this.repository.getGroupRepository().adminExists(groupId, admin.getAddress()))
|
||||
return ValidationResult.NOT_GROUP_ADMIN;
|
||||
|
||||
if( this.repository.getBlockRepository().getBlockchainHeight() < BlockChain.getInstance().getNullGroupMembershipHeight() ) {
|
||||
// Can't ban if not group's current owner
|
||||
if (!admin.getAddress().equals(groupData.getOwner()))
|
||||
return ValidationResult.INVALID_GROUP_OWNER;
|
||||
}
|
||||
// if( this.repository.getBlockRepository().getBlockchainHeight() >= BlockChain.getInstance().getNullGroupMembershipHeight() )
|
||||
else {
|
||||
String groupOwner = this.repository.getGroupRepository().getOwner(groupId);
|
||||
boolean groupOwnedByNullAccount = Objects.equals(groupOwner, Group.NULL_OWNER_ADDRESS);
|
||||
|
||||
// if null ownership group, then check for admin approval
|
||||
if(groupOwnedByNullAccount ) {
|
||||
// Require approval if transaction relates to a group owned by the null account
|
||||
if (!this.needsGroupApproval())
|
||||
return ValidationResult.GROUP_APPROVAL_REQUIRED;
|
||||
}
|
||||
else if (!admin.getAddress().equals(groupData.getOwner()))
|
||||
return ValidationResult.INVALID_GROUP_OWNER;
|
||||
}
|
||||
|
||||
Account offender = getOffender();
|
||||
|
||||
|
@ -2,6 +2,7 @@ package org.qortal.transaction;
|
||||
|
||||
import org.qortal.account.Account;
|
||||
import org.qortal.asset.Asset;
|
||||
import org.qortal.block.BlockChain;
|
||||
import org.qortal.crypto.Crypto;
|
||||
import org.qortal.data.transaction.GroupInviteTransactionData;
|
||||
import org.qortal.data.transaction.TransactionData;
|
||||
@ -11,6 +12,7 @@ import org.qortal.repository.Repository;
|
||||
|
||||
import java.util.Collections;
|
||||
import java.util.List;
|
||||
import java.util.Objects;
|
||||
|
||||
public class GroupInviteTransaction extends Transaction {
|
||||
|
||||
@ -85,6 +87,16 @@ public class GroupInviteTransaction extends Transaction {
|
||||
if (admin.getConfirmedBalance(Asset.QORT) < this.groupInviteTransactionData.getFee())
|
||||
return ValidationResult.NO_BALANCE;
|
||||
|
||||
// if null ownership group, then check for admin approval
|
||||
if( this.repository.getBlockRepository().getBlockchainHeight() >= BlockChain.getInstance().getNullGroupMembershipHeight() ) {
|
||||
String groupOwner = this.repository.getGroupRepository().getOwner(groupId);
|
||||
boolean groupOwnedByNullAccount = Objects.equals(groupOwner, Group.NULL_OWNER_ADDRESS);
|
||||
|
||||
// Require approval if transaction relates to a group owned by the null account
|
||||
if (groupOwnedByNullAccount && !this.needsGroupApproval())
|
||||
return ValidationResult.GROUP_APPROVAL_REQUIRED;
|
||||
}
|
||||
|
||||
return ValidationResult.OK;
|
||||
}
|
||||
|
||||
|
@ -3,6 +3,7 @@ package org.qortal.transaction;
|
||||
import org.qortal.account.Account;
|
||||
import org.qortal.account.PublicKeyAccount;
|
||||
import org.qortal.asset.Asset;
|
||||
import org.qortal.block.BlockChain;
|
||||
import org.qortal.crypto.Crypto;
|
||||
import org.qortal.data.group.GroupData;
|
||||
import org.qortal.data.transaction.GroupKickTransactionData;
|
||||
@ -14,6 +15,7 @@ import org.qortal.repository.Repository;
|
||||
|
||||
import java.util.Collections;
|
||||
import java.util.List;
|
||||
import java.util.Objects;
|
||||
|
||||
public class GroupKickTransaction extends Transaction {
|
||||
|
||||
@ -82,9 +84,26 @@ public class GroupKickTransaction extends Transaction {
|
||||
if (!admin.getAddress().equals(groupData.getOwner()) && groupRepository.adminExists(groupId, member.getAddress()))
|
||||
return ValidationResult.INVALID_GROUP_OWNER;
|
||||
|
||||
if( this.repository.getBlockRepository().getBlockchainHeight() < BlockChain.getInstance().getNullGroupMembershipHeight() ) {
|
||||
// Can't kick if not group's current owner
|
||||
if (!admin.getAddress().equals(groupData.getOwner()))
|
||||
return ValidationResult.INVALID_GROUP_OWNER;
|
||||
}
|
||||
// if( this.repository.getBlockRepository().getBlockchainHeight() >= BlockChain.getInstance().getNullGroupMembershipHeight() )
|
||||
else {
|
||||
String groupOwner = this.repository.getGroupRepository().getOwner(groupId);
|
||||
boolean groupOwnedByNullAccount = Objects.equals(groupOwner, Group.NULL_OWNER_ADDRESS);
|
||||
|
||||
// if null ownership group, then check for admin approval
|
||||
if(groupOwnedByNullAccount ) {
|
||||
// Require approval if transaction relates to a group owned by the null account
|
||||
if (!this.needsGroupApproval())
|
||||
return ValidationResult.GROUP_APPROVAL_REQUIRED;
|
||||
}
|
||||
// Can't kick if not group's current owner
|
||||
else if (!admin.getAddress().equals(groupData.getOwner()))
|
||||
return ValidationResult.INVALID_GROUP_OWNER;
|
||||
}
|
||||
|
||||
// Check creator has enough funds
|
||||
if (admin.getConfirmedBalance(Asset.QORT) < this.groupKickTransactionData.getFee())
|
||||
|
@ -65,11 +65,11 @@ public abstract class Transaction {
|
||||
UPDATE_GROUP(23, true),
|
||||
ADD_GROUP_ADMIN(24, true),
|
||||
REMOVE_GROUP_ADMIN(25, true),
|
||||
GROUP_BAN(26, false),
|
||||
CANCEL_GROUP_BAN(27, false),
|
||||
GROUP_KICK(28, false),
|
||||
GROUP_INVITE(29, false),
|
||||
CANCEL_GROUP_INVITE(30, false),
|
||||
GROUP_BAN(26, true),
|
||||
CANCEL_GROUP_BAN(27, true),
|
||||
GROUP_KICK(28, true),
|
||||
GROUP_INVITE(29, true),
|
||||
CANCEL_GROUP_INVITE(30, true),
|
||||
JOIN_GROUP(31, false),
|
||||
LEAVE_GROUP(32, false),
|
||||
GROUP_APPROVAL(33, false),
|
||||
|
319
src/main/java/org/qortal/utils/BalanceRecorderUtils.java
Normal file
319
src/main/java/org/qortal/utils/BalanceRecorderUtils.java
Normal file
@ -0,0 +1,319 @@
|
||||
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.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;
|
||||
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<TransactionData> transactions) {
|
||||
|
||||
Map<String, Long> amountsByAddress = new HashMap<>(transactions.size());
|
||||
|
||||
for( TransactionData transactionData : transactions ) {
|
||||
|
||||
mapBalanceModificationsForTransaction(amountsByAddress, transactionData);
|
||||
}
|
||||
|
||||
List<AddressAmountData> 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());
|
||||
|
||||
return addressAmounts;
|
||||
}
|
||||
|
||||
public static AddressAmountData adjustAddressAmount(long adjustment, AddressAmountData data) {
|
||||
|
||||
return new AddressAmountData(data.getAddress(), data.getAmount() - adjustment);
|
||||
}
|
||||
|
||||
public static void mapBalanceModificationsForTransaction(Map<String, Long> 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<String, Long> 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<String, Long> 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<String, Long> 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<String, Long> 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<String, Long> 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<String, Long> 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<String, Long> amountsByAddress, Long amount, String sender, Optional<String> 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<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;
|
||||
}
|
||||
|
||||
/**
|
||||
* 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;
|
||||
}
|
||||
}
|
99
src/main/java/org/qortal/utils/Blocks.java
Normal file
99
src/main/java/org/qortal/utils/Blocks.java
Normal file
@ -0,0 +1,99 @@
|
||||
package org.qortal.utils;
|
||||
|
||||
import io.druid.extendedset.intset.ConciseSet;
|
||||
import org.apache.logging.log4j.LogManager;
|
||||
import org.apache.logging.log4j.Logger;
|
||||
import org.qortal.block.BlockChain;
|
||||
import org.qortal.data.account.AddressLevelPairing;
|
||||
import org.qortal.data.account.RewardShareData;
|
||||
import org.qortal.data.block.BlockData;
|
||||
import org.qortal.data.block.DecodedOnlineAccountData;
|
||||
import org.qortal.data.group.GroupMemberData;
|
||||
import org.qortal.data.naming.NameData;
|
||||
import org.qortal.repository.DataException;
|
||||
import org.qortal.repository.Repository;
|
||||
import org.qortal.transform.block.BlockTransformer;
|
||||
|
||||
import java.util.HashSet;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.Set;
|
||||
import java.util.stream.Collectors;
|
||||
|
||||
/**
|
||||
* Class Blocks
|
||||
*
|
||||
* Methods for block related logic.
|
||||
*/
|
||||
public class Blocks {
|
||||
|
||||
private static final Logger LOGGER = LogManager.getLogger(Blocks.class);
|
||||
|
||||
/**
|
||||
* Get Decode Online Accounts For Block
|
||||
*
|
||||
* @param repository the data repository
|
||||
* @param blockData the block data
|
||||
*
|
||||
* @return the online accounts set to the block
|
||||
*
|
||||
* @throws DataException
|
||||
*/
|
||||
public static Set<DecodedOnlineAccountData> getDecodedOnlineAccountsForBlock(Repository repository, BlockData blockData) throws DataException {
|
||||
try {
|
||||
// get all online account indices from block
|
||||
ConciseSet onlineAccountIndices = BlockTransformer.decodeOnlineAccounts(blockData.getEncodedOnlineAccounts());
|
||||
|
||||
// get online reward shares from the online accounts on the block
|
||||
List<RewardShareData> onlineRewardShares = repository.getAccountRepository().getRewardSharesByIndexes(onlineAccountIndices.toArray());
|
||||
|
||||
// online timestamp for block
|
||||
long onlineTimestamp = blockData.getOnlineAccountsTimestamp();
|
||||
Set<DecodedOnlineAccountData> onlineAccounts = new HashSet<>();
|
||||
|
||||
// all minting group member addresses
|
||||
List<String> mintingGroupAddresses
|
||||
= repository.getGroupRepository()
|
||||
.getGroupMembers(BlockChain.getInstance().getMintingGroupId()).stream()
|
||||
.map(GroupMemberData::getMember)
|
||||
.collect(Collectors.toList());
|
||||
|
||||
// all names, indexed by address
|
||||
Map<String, String> nameByAddress
|
||||
= repository.getNameRepository()
|
||||
.getAllNames().stream()
|
||||
.collect(Collectors.toMap(NameData::getOwner, NameData::getName));
|
||||
|
||||
// all accounts at level 1 or higher, indexed by address
|
||||
Map<String, Integer> levelByAddress
|
||||
= repository.getAccountRepository().getAddressLevelPairings(1).stream()
|
||||
.collect(Collectors.toMap(AddressLevelPairing::getAddress, AddressLevelPairing::getLevel));
|
||||
|
||||
// for each reward share where the minter is online,
|
||||
// construct the data object and add it to the return list
|
||||
for (RewardShareData onlineRewardShare : onlineRewardShares) {
|
||||
String minter = onlineRewardShare.getMinter();
|
||||
DecodedOnlineAccountData onlineAccountData
|
||||
= new DecodedOnlineAccountData(
|
||||
onlineTimestamp,
|
||||
minter,
|
||||
onlineRewardShare.getRecipient(),
|
||||
onlineRewardShare.getSharePercent(),
|
||||
mintingGroupAddresses.contains(minter),
|
||||
nameByAddress.get(minter),
|
||||
levelByAddress.get(minter)
|
||||
);
|
||||
|
||||
onlineAccounts.add(onlineAccountData);
|
||||
}
|
||||
|
||||
return onlineAccounts;
|
||||
} catch (DataException e) {
|
||||
throw e;
|
||||
} catch (Exception e ) {
|
||||
LOGGER.error(e.getMessage(), e);
|
||||
|
||||
return new HashSet<>(0);
|
||||
}
|
||||
}
|
||||
}
|
@ -113,7 +113,11 @@
|
||||
"onlyMintWithNameHeight": 1900300,
|
||||
"removeOnlyMintWithNameHeight": 1935500,
|
||||
"groupMemberCheckHeight": 1902700,
|
||||
"fixBatchRewardHeight": 1945900
|
||||
"fixBatchRewardHeight": 1945900,
|
||||
"adminsReplaceFoundersHeight": 9999999,
|
||||
"nullGroupMembershipHeight": 9999999,
|
||||
"ignoreLevelForRewardShareHeight": 9999999,
|
||||
"adminQueryFixHeight": 9999999
|
||||
},
|
||||
"checkpoints": [
|
||||
{ "height": 1136300, "signature": "3BbwawEF2uN8Ni5ofpJXkukoU8ctAPxYoFB7whq9pKfBnjfZcpfEJT4R95NvBDoTP8WDyWvsUvbfHbcr9qSZuYpSKZjUQTvdFf6eqznHGEwhZApWfvXu6zjGCxYCp65F4jsVYYJjkzbjmkCg5WAwN5voudngA23kMK6PpTNygapCzXt" }
|
||||
|
@ -20,17 +20,21 @@
|
||||
width: 100%;
|
||||
text-align: center;
|
||||
z-index: 1000;
|
||||
top: 45%;
|
||||
top: 50%;
|
||||
-ms-transform: translateY(-50%);
|
||||
transform: translateY(-50%);
|
||||
transform: translate(-50% , -50%);
|
||||
left: 50%;
|
||||
}
|
||||
#panel {
|
||||
text-align: center;
|
||||
background: white;
|
||||
word-wrap: break-word;
|
||||
width: 350px;
|
||||
max-width: 100%;
|
||||
margin: auto;
|
||||
padding: 25px;
|
||||
border-radius: 30px;
|
||||
box-sizing: border-box;
|
||||
}
|
||||
#status {
|
||||
color: #03a9f4;
|
||||
|
@ -405,7 +405,7 @@ public class RepositoryTests extends Common {
|
||||
Integer offset = null;
|
||||
Boolean reverse = null;
|
||||
|
||||
hsqldb.getATRepository().getMatchingFinalATStates(codeHash, isFinished, dataByteOffset, expectedValue, minimumFinalHeight, limit, offset, reverse);
|
||||
hsqldb.getATRepository().getMatchingFinalATStates(codeHash,null, null, isFinished, dataByteOffset, expectedValue, minimumFinalHeight, limit, offset, reverse);
|
||||
} catch (DataException e) {
|
||||
fail("HSQLDB bug #1580");
|
||||
}
|
||||
|
@ -26,7 +26,7 @@ public class CrossChainApiTests extends ApiCommon {
|
||||
@Test
|
||||
public void testGetCompletedTrades() {
|
||||
long minimumTimestamp = System.currentTimeMillis();
|
||||
assertNoApiError((limit, offset, reverse) -> this.crossChainResource.getCompletedTrades(SPECIFIC_BLOCKCHAIN, minimumTimestamp, limit, offset, reverse));
|
||||
assertNoApiError((limit, offset, reverse) -> this.crossChainResource.getCompletedTrades(SPECIFIC_BLOCKCHAIN, minimumTimestamp, null, null, limit, offset, reverse));
|
||||
}
|
||||
|
||||
@Test
|
||||
@ -35,8 +35,8 @@ public class CrossChainApiTests extends ApiCommon {
|
||||
Integer offset = null;
|
||||
Boolean reverse = null;
|
||||
|
||||
assertApiError(ApiError.INVALID_CRITERIA, () -> this.crossChainResource.getCompletedTrades(SPECIFIC_BLOCKCHAIN, -1L /*minimumTimestamp*/, limit, offset, reverse));
|
||||
assertApiError(ApiError.INVALID_CRITERIA, () -> this.crossChainResource.getCompletedTrades(SPECIFIC_BLOCKCHAIN, 0L /*minimumTimestamp*/, limit, offset, reverse));
|
||||
assertApiError(ApiError.INVALID_CRITERIA, () -> this.crossChainResource.getCompletedTrades(SPECIFIC_BLOCKCHAIN, -1L /*minimumTimestamp*/, null, null, limit, offset, reverse));
|
||||
assertApiError(ApiError.INVALID_CRITERIA, () -> this.crossChainResource.getCompletedTrades(SPECIFIC_BLOCKCHAIN, 0L /*minimumTimestamp*/, null, null, limit, offset, reverse));
|
||||
}
|
||||
|
||||
}
|
||||
|
@ -218,6 +218,8 @@ public class AtRepositoryTests extends Common {
|
||||
|
||||
List<ATStateData> atStates = repository.getATRepository().getMatchingFinalATStates(
|
||||
codeHash,
|
||||
null,
|
||||
null,
|
||||
isFinished,
|
||||
dataByteOffset,
|
||||
expectedValue,
|
||||
@ -264,6 +266,8 @@ public class AtRepositoryTests extends Common {
|
||||
|
||||
List<ATStateData> atStates = repository.getATRepository().getMatchingFinalATStates(
|
||||
codeHash,
|
||||
null,
|
||||
null,
|
||||
isFinished,
|
||||
dataByteOffset,
|
||||
expectedValue,
|
||||
|
58
src/test/java/org/qortal/test/block/BlockTests.java
Normal file
58
src/test/java/org/qortal/test/block/BlockTests.java
Normal file
@ -0,0 +1,58 @@
|
||||
package org.qortal.test.block;
|
||||
|
||||
import org.checkerframework.checker.units.qual.K;
|
||||
import org.junit.Assert;
|
||||
import org.junit.Test;
|
||||
import org.qortal.block.Block;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.HashMap;
|
||||
import java.util.List;
|
||||
import java.util.Optional;
|
||||
|
||||
public class BlockTests {
|
||||
|
||||
@Test
|
||||
public void testDistributeToAccountsOneDistribution(){
|
||||
List<String> addresses = new ArrayList<>();
|
||||
addresses.add("a");
|
||||
addresses.add("b");
|
||||
addresses.add("c");
|
||||
|
||||
HashMap<String, Long> balanceByAddress = new HashMap<>();
|
||||
long total = Block.distributeToAccounts( 10L, addresses, balanceByAddress);
|
||||
|
||||
Assert.assertEquals(9, total);
|
||||
|
||||
Assert.assertEquals(3, balanceByAddress.size());
|
||||
Assert.assertTrue(balanceByAddress.containsKey("a"));
|
||||
Assert.assertTrue(balanceByAddress.containsKey("b"));
|
||||
Assert.assertTrue(balanceByAddress.containsKey("c"));
|
||||
Assert.assertEquals(3L, balanceByAddress.getOrDefault("a", 0L).longValue());
|
||||
Assert.assertEquals(3L, balanceByAddress.getOrDefault("b", 0L).longValue());
|
||||
Assert.assertEquals(3L, balanceByAddress.getOrDefault("c", 0L).longValue());
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testDistributeToAccountsTwoDistributions(){
|
||||
List<String> addresses = new ArrayList<>();
|
||||
addresses.add("a");
|
||||
addresses.add("b");
|
||||
addresses.add("c");
|
||||
|
||||
HashMap<String, Long> balanceByAddress = new HashMap<>();
|
||||
long total1 = Block.distributeToAccounts( 10L, addresses, balanceByAddress);
|
||||
long total2 = Block.distributeToAccounts( 20L, addresses, balanceByAddress);
|
||||
|
||||
Assert.assertEquals(9, total1);
|
||||
Assert.assertEquals(18, total2);
|
||||
|
||||
Assert.assertEquals(3, balanceByAddress.size());
|
||||
Assert.assertTrue(balanceByAddress.containsKey("a"));
|
||||
Assert.assertTrue(balanceByAddress.containsKey("b"));
|
||||
Assert.assertTrue(balanceByAddress.containsKey("c"));
|
||||
Assert.assertEquals(9L, balanceByAddress.getOrDefault("a", 0L).longValue());
|
||||
Assert.assertEquals(9L, balanceByAddress.getOrDefault("b", 0L).longValue());
|
||||
Assert.assertEquals(9L, balanceByAddress.getOrDefault("c", 0L).longValue());
|
||||
}
|
||||
}
|
@ -4,7 +4,11 @@ import org.junit.After;
|
||||
import org.junit.Before;
|
||||
import org.junit.Test;
|
||||
import org.qortal.account.PrivateKeyAccount;
|
||||
import org.qortal.block.Block;
|
||||
import org.qortal.block.BlockChain;
|
||||
import org.qortal.data.group.GroupAdminData;
|
||||
import org.qortal.data.transaction.*;
|
||||
import org.qortal.group.Group;
|
||||
import org.qortal.repository.DataException;
|
||||
import org.qortal.repository.Repository;
|
||||
import org.qortal.repository.RepositoryManager;
|
||||
@ -16,6 +20,8 @@ import org.qortal.test.common.transaction.TestTransaction;
|
||||
import org.qortal.transaction.Transaction;
|
||||
import org.qortal.transaction.Transaction.ValidationResult;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
import static org.junit.Assert.*;
|
||||
|
||||
/**
|
||||
@ -40,8 +46,14 @@ import static org.junit.Assert.*;
|
||||
*/
|
||||
public class DevGroupAdminTests extends Common {
|
||||
|
||||
public static final int NULL_GROUP_MEMBERSHIP_HEIGHT = BlockChain.getInstance().getNullGroupMembershipHeight();
|
||||
private static final int DEV_GROUP_ID = 1;
|
||||
|
||||
public static final String ALICE = "alice";
|
||||
public static final String BOB = "bob";
|
||||
public static final String CHLOE = "chloe";
|
||||
public static final String DILBERT = "dilbert";
|
||||
|
||||
@Before
|
||||
public void beforeTest() throws DataException {
|
||||
Common.useDefaultSettings();
|
||||
@ -55,8 +67,8 @@ public class DevGroupAdminTests extends Common {
|
||||
@Test
|
||||
public void testGroupKickMember() throws DataException {
|
||||
try (final Repository repository = RepositoryManager.getRepository()) {
|
||||
PrivateKeyAccount alice = Common.getTestAccount(repository, "alice");
|
||||
PrivateKeyAccount bob = Common.getTestAccount(repository, "bob");
|
||||
PrivateKeyAccount alice = Common.getTestAccount(repository, ALICE);
|
||||
PrivateKeyAccount bob = Common.getTestAccount(repository, BOB);
|
||||
|
||||
// Dev group
|
||||
int groupId = DEV_GROUP_ID;
|
||||
@ -80,16 +92,10 @@ public class DevGroupAdminTests extends Common {
|
||||
|
||||
// Attempt to kick Bob
|
||||
result = groupKick(repository, alice, groupId, bob.getAddress());
|
||||
// Should be OK
|
||||
assertEquals(ValidationResult.OK, result);
|
||||
// Should not be OK, cannot kick member out of null owned group
|
||||
assertNotSame(ValidationResult.OK, result);
|
||||
|
||||
// Confirm Bob no longer a member
|
||||
assertFalse(isMember(repository, bob.getAddress(), groupId));
|
||||
|
||||
// Orphan last block
|
||||
BlockUtils.orphanLastBlock(repository);
|
||||
|
||||
// Confirm Bob now a member
|
||||
// Confirm Bob remains a member
|
||||
assertTrue(isMember(repository, bob.getAddress(), groupId));
|
||||
}
|
||||
}
|
||||
@ -97,8 +103,8 @@ public class DevGroupAdminTests extends Common {
|
||||
@Test
|
||||
public void testGroupKickAdmin() throws DataException {
|
||||
try (final Repository repository = RepositoryManager.getRepository()) {
|
||||
PrivateKeyAccount alice = Common.getTestAccount(repository, "alice");
|
||||
PrivateKeyAccount bob = Common.getTestAccount(repository, "bob");
|
||||
PrivateKeyAccount alice = Common.getTestAccount(repository, ALICE);
|
||||
PrivateKeyAccount bob = Common.getTestAccount(repository, BOB);
|
||||
|
||||
// Dev group
|
||||
int groupId = DEV_GROUP_ID;
|
||||
@ -123,7 +129,7 @@ public class DevGroupAdminTests extends Common {
|
||||
assertEquals("incorrect transaction approval status", Transaction.ApprovalStatus.PENDING, approvalStatus);
|
||||
|
||||
// Have Alice approve Bob's approval-needed transaction
|
||||
GroupUtils.approveTransaction(repository, "alice", addGroupAdminTransactionData.getSignature(), true);
|
||||
GroupUtils.approveTransaction(repository, ALICE, addGroupAdminTransactionData.getSignature(), true);
|
||||
|
||||
// Mint a block so that the transaction becomes approved
|
||||
BlockUtils.mintBlock(repository);
|
||||
@ -167,8 +173,8 @@ public class DevGroupAdminTests extends Common {
|
||||
@Test
|
||||
public void testGroupBanMember() throws DataException {
|
||||
try (final Repository repository = RepositoryManager.getRepository()) {
|
||||
PrivateKeyAccount alice = Common.getTestAccount(repository, "alice");
|
||||
PrivateKeyAccount bob = Common.getTestAccount(repository, "bob");
|
||||
PrivateKeyAccount alice = Common.getTestAccount(repository, ALICE);
|
||||
PrivateKeyAccount bob = Common.getTestAccount(repository, BOB);
|
||||
|
||||
// Dev group
|
||||
int groupId = DEV_GROUP_ID;
|
||||
@ -183,18 +189,13 @@ public class DevGroupAdminTests extends Common {
|
||||
|
||||
// Attempt to ban Bob
|
||||
result = groupBan(repository, alice, groupId, bob.getAddress());
|
||||
// Should be OK
|
||||
assertEquals(ValidationResult.OK, result);
|
||||
|
||||
// Bob attempts to rejoin
|
||||
result = joinGroup(repository, bob, groupId);
|
||||
// Should NOT be OK
|
||||
// Should not be OK, cannot ban someone from a null owned group
|
||||
assertNotSame(ValidationResult.OK, result);
|
||||
|
||||
// Orphan last block (Bob ban)
|
||||
BlockUtils.orphanLastBlock(repository);
|
||||
// Delete unconfirmed group-ban transaction
|
||||
TransactionUtils.deleteUnconfirmedTransactions(repository);
|
||||
// Bob attempts to join
|
||||
result = joinGroup(repository, bob, groupId);
|
||||
// Should be OK, but won't actually get him in the group
|
||||
assertEquals(ValidationResult.OK, result);
|
||||
|
||||
// Confirm Bob is not a member
|
||||
assertFalse(isMember(repository, bob.getAddress(), groupId));
|
||||
@ -204,65 +205,38 @@ public class DevGroupAdminTests extends Common {
|
||||
|
||||
// Bob to join
|
||||
result = joinGroup(repository, bob, groupId);
|
||||
// Should be OK
|
||||
assertEquals(ValidationResult.OK, result);
|
||||
// Should not be OK, bob should already be a member, he joined before the invite and
|
||||
// the invite served as an approval
|
||||
assertEquals(ValidationResult.ALREADY_GROUP_MEMBER, result);
|
||||
|
||||
// Confirm Bob now a member
|
||||
// Confirm Bob now a member, now that he got an invite
|
||||
assertTrue(isMember(repository, bob.getAddress(), groupId));
|
||||
|
||||
// Attempt to ban Bob
|
||||
result = groupBan(repository, alice, groupId, bob.getAddress());
|
||||
// Should be OK
|
||||
assertEquals(ValidationResult.OK, result);
|
||||
// Should not be OK, because you can ban a member of a null owned group
|
||||
assertNotSame(ValidationResult.OK, result);
|
||||
|
||||
// Confirm Bob no longer a member
|
||||
assertFalse(isMember(repository, bob.getAddress(), groupId));
|
||||
// Confirm Bob is still a member
|
||||
assertTrue(isMember(repository, bob.getAddress(), groupId));
|
||||
|
||||
// Bob attempts to rejoin
|
||||
result = joinGroup(repository, bob, groupId);
|
||||
// Should NOT be OK
|
||||
// Should NOT be OK, because he is already a member
|
||||
assertNotSame(ValidationResult.OK, result);
|
||||
|
||||
// Cancel Bob's ban
|
||||
result = cancelGroupBan(repository, alice, groupId, bob.getAddress());
|
||||
// Should be OK
|
||||
assertEquals(ValidationResult.OK, result);
|
||||
|
||||
// Bob attempts to rejoin
|
||||
result = joinGroup(repository, bob, groupId);
|
||||
// Should be OK
|
||||
assertEquals(ValidationResult.OK, result);
|
||||
|
||||
// Orphan last block (Bob join)
|
||||
BlockUtils.orphanLastBlock(repository);
|
||||
// Delete unconfirmed join-group transaction
|
||||
TransactionUtils.deleteUnconfirmedTransactions(repository);
|
||||
|
||||
// Orphan last block (Cancel Bob ban)
|
||||
BlockUtils.orphanLastBlock(repository);
|
||||
// Delete unconfirmed cancel-ban transaction
|
||||
TransactionUtils.deleteUnconfirmedTransactions(repository);
|
||||
|
||||
// Bob attempts to rejoin
|
||||
result = joinGroup(repository, bob, groupId);
|
||||
// Should NOT be OK
|
||||
// Should not be OK, because there was no ban to begin with
|
||||
assertNotSame(ValidationResult.OK, result);
|
||||
|
||||
// Orphan last block (Bob ban)
|
||||
BlockUtils.orphanLastBlock(repository);
|
||||
// Delete unconfirmed group-ban transaction
|
||||
TransactionUtils.deleteUnconfirmedTransactions(repository);
|
||||
|
||||
// Confirm Bob now a member
|
||||
assertTrue(isMember(repository, bob.getAddress(), groupId));
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testGroupBanAdmin() throws DataException {
|
||||
try (final Repository repository = RepositoryManager.getRepository()) {
|
||||
PrivateKeyAccount alice = Common.getTestAccount(repository, "alice");
|
||||
PrivateKeyAccount bob = Common.getTestAccount(repository, "bob");
|
||||
PrivateKeyAccount alice = Common.getTestAccount(repository, ALICE);
|
||||
PrivateKeyAccount bob = Common.getTestAccount(repository, BOB);
|
||||
|
||||
// Dev group
|
||||
int groupId = DEV_GROUP_ID;
|
||||
@ -286,7 +260,7 @@ public class DevGroupAdminTests extends Common {
|
||||
assertEquals("incorrect transaction approval status", Transaction.ApprovalStatus.PENDING, approvalStatus);
|
||||
|
||||
// Have Alice approve Bob's approval-needed transaction
|
||||
GroupUtils.approveTransaction(repository, "alice", addGroupAdminTransactionData.getSignature(), true);
|
||||
GroupUtils.approveTransaction(repository, ALICE, addGroupAdminTransactionData.getSignature(), true);
|
||||
|
||||
// Mint a block so that the transaction becomes approved
|
||||
BlockUtils.mintBlock(repository);
|
||||
@ -321,6 +295,322 @@ public class DevGroupAdminTests extends Common {
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testAddAdmin2of3() throws DataException {
|
||||
try (final Repository repository = RepositoryManager.getRepository()) {
|
||||
|
||||
// establish accounts
|
||||
PrivateKeyAccount alice = Common.getTestAccount(repository, ALICE);
|
||||
PrivateKeyAccount bob = Common.getTestAccount(repository, BOB);
|
||||
PrivateKeyAccount chloe = Common.getTestAccount(repository, CHLOE);
|
||||
PrivateKeyAccount dilbert = Common.getTestAccount(repository, DILBERT);
|
||||
|
||||
// assert admin statuses
|
||||
assertEquals(2, repository.getGroupRepository().countGroupAdmins(DEV_GROUP_ID).intValue());
|
||||
assertTrue(isAdmin(repository, Group.NULL_OWNER_ADDRESS, DEV_GROUP_ID));
|
||||
assertTrue(isAdmin(repository, alice.getAddress(), DEV_GROUP_ID));
|
||||
assertFalse(isAdmin(repository, bob.getAddress(), DEV_GROUP_ID));
|
||||
assertFalse(isAdmin(repository, chloe.getAddress(), DEV_GROUP_ID));
|
||||
assertFalse(isAdmin(repository, dilbert.getAddress(), DEV_GROUP_ID));
|
||||
|
||||
// confirm Bob is not a member
|
||||
assertFalse(isMember(repository, bob.getAddress(), DEV_GROUP_ID));
|
||||
|
||||
// alice invites bob
|
||||
ValidationResult result = groupInvite(repository, alice, DEV_GROUP_ID, bob.getAddress(), 3600);
|
||||
assertSame(ValidationResult.OK, result);
|
||||
|
||||
// bob joins
|
||||
joinGroup(repository, bob, DEV_GROUP_ID);
|
||||
|
||||
// confirm Bob is a member now, but still not an admin
|
||||
assertTrue(isMember(repository, bob.getAddress(), DEV_GROUP_ID));
|
||||
assertFalse(isAdmin(repository, bob.getAddress(), DEV_GROUP_ID));
|
||||
|
||||
// bob creates transaction to add himself as an admin
|
||||
TransactionData addGroupAdminTransactionData1 = addGroupAdmin(repository, bob, DEV_GROUP_ID, bob.getAddress());
|
||||
|
||||
// bob creates add admin transaction for himself, alice signs which is 50% approval while 40% is needed
|
||||
signForGroupApproval(repository, addGroupAdminTransactionData1, List.of(alice));
|
||||
|
||||
// assert 3 admins in group and bob is an admin now
|
||||
assertEquals(3, repository.getGroupRepository().countGroupAdmins(DEV_GROUP_ID).intValue() );
|
||||
assertTrue(isAdmin(repository, bob.getAddress(), DEV_GROUP_ID));
|
||||
|
||||
// bob invites chloe
|
||||
result = groupInvite(repository, bob, DEV_GROUP_ID, chloe.getAddress(), 3600);
|
||||
assertSame(ValidationResult.OK, result);
|
||||
|
||||
// chloe joins
|
||||
joinGroup(repository, chloe, DEV_GROUP_ID);
|
||||
|
||||
// confirm Chloe is a member now, but still not an admin
|
||||
assertTrue(isMember(repository, chloe.getAddress(), DEV_GROUP_ID));
|
||||
assertFalse(isAdmin(repository, chloe.getAddress(), DEV_GROUP_ID));
|
||||
|
||||
// chloe creates transaction to add herself as an admin
|
||||
TransactionData addChloeAsGroupAdmin = addGroupAdmin(repository, chloe, DEV_GROUP_ID, chloe.getAddress());
|
||||
|
||||
// no one has signed, so it should be pending
|
||||
Transaction.ApprovalStatus addChloeAsGroupAdminStatus1 = GroupUtils.getApprovalStatus(repository, addChloeAsGroupAdmin.getSignature());
|
||||
assertEquals( Transaction.ApprovalStatus.PENDING, addChloeAsGroupAdminStatus1);
|
||||
|
||||
// signer 1
|
||||
Transaction.ApprovalStatus addChloeAsGroupAdminStatus2 = signForGroupApproval(repository, addChloeAsGroupAdmin, List.of(alice));
|
||||
|
||||
// 1 out of 3 has signed, so it should be pending, because it is less than 40%
|
||||
assertEquals( Transaction.ApprovalStatus.PENDING, addChloeAsGroupAdminStatus2);
|
||||
|
||||
// signer 2
|
||||
Transaction.ApprovalStatus addChloeAsGroupAdminStatus3 = signForGroupApproval(repository, addChloeAsGroupAdmin, List.of(bob));
|
||||
|
||||
// 2 out of 3 has signed, so it should be approved, because it is more than 40%
|
||||
assertEquals( Transaction.ApprovalStatus.APPROVED, addChloeAsGroupAdminStatus3);
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testNullOwnershipMembership() throws DataException{
|
||||
try (final Repository repository = RepositoryManager.getRepository()) {
|
||||
|
||||
Block block = BlockUtils.mintBlocks(repository, NULL_GROUP_MEMBERSHIP_HEIGHT);
|
||||
assertEquals(NULL_GROUP_MEMBERSHIP_HEIGHT + 1, block.getBlockData().getHeight().intValue());
|
||||
|
||||
// establish accounts
|
||||
PrivateKeyAccount alice = Common.getTestAccount(repository, ALICE);
|
||||
PrivateKeyAccount bob = Common.getTestAccount(repository, BOB);
|
||||
PrivateKeyAccount chloe = Common.getTestAccount(repository, CHLOE);
|
||||
PrivateKeyAccount dilbert = Common.getTestAccount(repository, DILBERT);
|
||||
|
||||
// assert admin statuses
|
||||
assertEquals(2, repository.getGroupRepository().countGroupAdmins(DEV_GROUP_ID).intValue());
|
||||
assertTrue(isAdmin(repository, Group.NULL_OWNER_ADDRESS, DEV_GROUP_ID));
|
||||
assertTrue(isAdmin(repository, alice.getAddress(), DEV_GROUP_ID));
|
||||
assertFalse(isAdmin(repository, bob.getAddress(), DEV_GROUP_ID));
|
||||
assertFalse(isAdmin(repository, chloe.getAddress(), DEV_GROUP_ID));
|
||||
assertFalse(isAdmin(repository, dilbert.getAddress(), DEV_GROUP_ID));
|
||||
|
||||
// confirm Bob is not a member
|
||||
assertFalse(isMember(repository, bob.getAddress(), DEV_GROUP_ID));
|
||||
|
||||
// alice invites bob, alice signs which is 50% approval while 40% is needed
|
||||
TransactionData createInviteTransactionData = createGroupInviteForGroupApproval(repository, alice, DEV_GROUP_ID, bob.getAddress(), 3600);
|
||||
Transaction.ApprovalStatus bobsInviteStatus = signForGroupApproval(repository, createInviteTransactionData, List.of(alice));
|
||||
|
||||
// assert approval
|
||||
assertEquals(Transaction.ApprovalStatus.APPROVED, bobsInviteStatus);
|
||||
|
||||
// bob joins
|
||||
joinGroup(repository, bob, DEV_GROUP_ID);
|
||||
|
||||
// confirm Bob is a member now, but still not an admin
|
||||
assertTrue(isMember(repository, bob.getAddress(), DEV_GROUP_ID));
|
||||
assertFalse(isAdmin(repository, bob.getAddress(), DEV_GROUP_ID));
|
||||
|
||||
// bob creates transaction to add himself as an admin
|
||||
TransactionData addGroupAdminTransactionData1 = addGroupAdmin(repository, bob, DEV_GROUP_ID, bob.getAddress());
|
||||
|
||||
// bob creates add admin transaction for himself, alice signs which is 50% approval while 40% is needed
|
||||
signForGroupApproval(repository, addGroupAdminTransactionData1, List.of(alice));
|
||||
|
||||
// assert 3 admins in group and bob is an admin now
|
||||
assertEquals(3, repository.getGroupRepository().countGroupAdmins(DEV_GROUP_ID).intValue());
|
||||
assertTrue(isAdmin(repository, bob.getAddress(), DEV_GROUP_ID));
|
||||
|
||||
// bob invites chloe, bob signs which is 33% approval while 40% is needed
|
||||
TransactionData chloeInvite = createGroupInviteForGroupApproval(repository, bob, DEV_GROUP_ID, chloe.getAddress(), 3600);
|
||||
Transaction.ApprovalStatus chloeInviteStatus = signForGroupApproval(repository, chloeInvite, List.of(bob));
|
||||
|
||||
// assert pending
|
||||
assertEquals(Transaction.ApprovalStatus.PENDING, chloeInviteStatus);
|
||||
|
||||
// alice signs which is 66% approval while 40% is needed
|
||||
chloeInviteStatus = signForGroupApproval(repository, chloeInvite, List.of(alice));
|
||||
|
||||
// assert approval
|
||||
assertEquals(Transaction.ApprovalStatus.APPROVED, chloeInviteStatus);
|
||||
|
||||
// chloe joins
|
||||
joinGroup(repository, chloe, DEV_GROUP_ID);
|
||||
|
||||
// assert chloe is in the group
|
||||
assertTrue(isMember(repository, bob.getAddress(), DEV_GROUP_ID));
|
||||
|
||||
// alice kicks chloe, alice signs which is 33% approval while 40% is needed
|
||||
TransactionData chloeKick = createGroupKickForGroupApproval(repository, alice, DEV_GROUP_ID, chloe.getAddress(),"testing chloe kick");
|
||||
Transaction.ApprovalStatus chloeKickStatus = signForGroupApproval(repository, chloeKick, List.of(alice));
|
||||
|
||||
// assert pending
|
||||
assertEquals(Transaction.ApprovalStatus.PENDING, chloeKickStatus);
|
||||
|
||||
// assert chloe is still in the group
|
||||
assertTrue(isMember(repository, bob.getAddress(), DEV_GROUP_ID));
|
||||
|
||||
// bob signs which is 66% approval while 40% is needed
|
||||
chloeKickStatus = signForGroupApproval(repository, chloeKick, List.of(bob));
|
||||
|
||||
// assert approval
|
||||
assertEquals(Transaction.ApprovalStatus.APPROVED, chloeKickStatus);
|
||||
|
||||
// assert chloe is not in the group
|
||||
assertFalse(isMember(repository, chloe.getAddress(), DEV_GROUP_ID));
|
||||
|
||||
// bob invites chloe, alice and bob signs which is 66% approval while 40% is needed
|
||||
TransactionData chloeInviteAgain = createGroupInviteForGroupApproval(repository, bob, DEV_GROUP_ID, chloe.getAddress(), 3600);
|
||||
Transaction.ApprovalStatus chloeInviteAgainStatus = signForGroupApproval(repository, chloeInviteAgain, List.of(alice, bob));
|
||||
|
||||
// assert approved
|
||||
assertEquals(Transaction.ApprovalStatus.APPROVED, chloeInviteAgainStatus);
|
||||
|
||||
// chloe joins again
|
||||
joinGroup(repository, chloe, DEV_GROUP_ID);
|
||||
|
||||
// assert chloe is in the group
|
||||
assertTrue(isMember(repository, bob.getAddress(), DEV_GROUP_ID));
|
||||
|
||||
// alice bans chloe, alice signs which is 33% approval while 40% is needed
|
||||
TransactionData chloeBan = createGroupBanForGroupApproval(repository, alice, DEV_GROUP_ID, chloe.getAddress(), "testing group ban", 3600);
|
||||
Transaction.ApprovalStatus chloeBanStatus1 = signForGroupApproval(repository, chloeBan, List.of(alice));
|
||||
|
||||
// assert pending
|
||||
assertEquals(Transaction.ApprovalStatus.PENDING, chloeBanStatus1);
|
||||
|
||||
// bob signs which 66% approval while 40% is needed
|
||||
Transaction.ApprovalStatus chloeBanStatus2 = signForGroupApproval(repository, chloeBan, List.of(bob));
|
||||
|
||||
// assert approved
|
||||
assertEquals(Transaction.ApprovalStatus.APPROVED, chloeBanStatus2);
|
||||
|
||||
// assert chloe is not in the group
|
||||
assertFalse(isMember(repository, chloe.getAddress(), DEV_GROUP_ID));
|
||||
|
||||
// bob invites chloe, alice and bob signs which is 66% approval while 40% is needed
|
||||
ValidationResult chloeInviteValidation = signAndImportGroupInvite(repository, bob, DEV_GROUP_ID, chloe.getAddress(), 3600);
|
||||
|
||||
// assert banned status on invite attempt
|
||||
assertEquals(ValidationResult.BANNED_FROM_GROUP, chloeInviteValidation);
|
||||
|
||||
// bob cancel ban on chloe, bob signs which is 33% approval while 40% is needed
|
||||
TransactionData chloeCancelBan = createCancelGroupBanForGroupApproval( repository, bob, DEV_GROUP_ID, chloe.getAddress());
|
||||
Transaction.ApprovalStatus chloeCancelBanStatus1 = signForGroupApproval(repository, chloeCancelBan, List.of(bob));
|
||||
|
||||
// assert pending
|
||||
assertEquals(Transaction.ApprovalStatus.PENDING, chloeCancelBanStatus1);
|
||||
|
||||
// alice signs which is 66% approval while 40% is needed
|
||||
Transaction.ApprovalStatus chloeCancelBanStatus2 = signForGroupApproval(repository, chloeCancelBan, List.of(alice));
|
||||
|
||||
// assert approved
|
||||
assertEquals(Transaction.ApprovalStatus.APPROVED, chloeCancelBanStatus2);
|
||||
|
||||
// bob invites chloe, alice and bob signs which is 66% approval while 40% is needed
|
||||
TransactionData chloeInvite4 = createGroupInviteForGroupApproval(repository, bob, DEV_GROUP_ID, chloe.getAddress(), 3600);
|
||||
Transaction.ApprovalStatus chloeInvite4Status = signForGroupApproval(repository, chloeInvite4, List.of(alice, bob));
|
||||
|
||||
// assert approved
|
||||
assertEquals(Transaction.ApprovalStatus.APPROVED, chloeInvite4Status);
|
||||
|
||||
// chloe joins again
|
||||
joinGroup(repository, chloe, DEV_GROUP_ID);
|
||||
|
||||
// assert chloe is in the group
|
||||
assertTrue(isMember(repository, chloe.getAddress(), DEV_GROUP_ID));
|
||||
|
||||
// bob invites dilbert, alice and bob signs which is 66% approval while 40% is needed
|
||||
TransactionData dilbertInvite1 = createGroupInviteForGroupApproval(repository, bob, DEV_GROUP_ID, dilbert.getAddress(), 3600);
|
||||
Transaction.ApprovalStatus dibertInviteStatus1 = signForGroupApproval(repository, dilbertInvite1, List.of(alice, bob));
|
||||
|
||||
// assert approved
|
||||
assertEquals(Transaction.ApprovalStatus.APPROVED, dibertInviteStatus1);
|
||||
|
||||
// alice cancels dilbert's invite, alice signs which is 33% approval while 40% is needed
|
||||
TransactionData cancelDilbertInvite = createCancelInviteForGroupApproval(repository, alice, DEV_GROUP_ID, dilbert.getAddress());
|
||||
Transaction.ApprovalStatus cancelDilbertInviteStatus1 = signForGroupApproval(repository, cancelDilbertInvite, List.of(alice));
|
||||
|
||||
// assert pending
|
||||
assertEquals(Transaction.ApprovalStatus.PENDING, cancelDilbertInviteStatus1);
|
||||
|
||||
// dilbert joins before the group approves cancellation
|
||||
joinGroup(repository, dilbert, DEV_GROUP_ID);
|
||||
|
||||
// assert dilbert is in the group
|
||||
assertTrue(isMember(repository, dilbert.getAddress(), DEV_GROUP_ID));
|
||||
|
||||
// alice kicks out dilbert, alice and bob sign which is 66% approval while 40% is needed
|
||||
TransactionData kickDilbert = createGroupKickForGroupApproval(repository, alice, DEV_GROUP_ID, dilbert.getAddress(), "he is sneaky");
|
||||
Transaction.ApprovalStatus kickDilbertStatus = signForGroupApproval(repository, kickDilbert, List.of(alice, bob));
|
||||
|
||||
// assert approved
|
||||
assertEquals(Transaction.ApprovalStatus.APPROVED, kickDilbertStatus);
|
||||
|
||||
// assert dilbert is out of the group
|
||||
assertFalse(isMember(repository, dilbert.getAddress(), DEV_GROUP_ID));
|
||||
|
||||
// bob invites dilbert again, alice and bob signs which is 66% approval while 40% is needed
|
||||
TransactionData dilbertInvite2 = createGroupInviteForGroupApproval(repository, bob, DEV_GROUP_ID, dilbert.getAddress(), 3600);
|
||||
Transaction.ApprovalStatus dibertInviteStatus2 = signForGroupApproval(repository, dilbertInvite2, List.of(alice, bob));
|
||||
|
||||
// assert approved
|
||||
assertEquals(Transaction.ApprovalStatus.APPROVED, dibertInviteStatus2);
|
||||
|
||||
// alice cancels dilbert's invite, alice and bob signs which is 66% approval while 40% is needed
|
||||
TransactionData cancelDilbertInvite2 = createCancelInviteForGroupApproval(repository, alice, DEV_GROUP_ID, dilbert.getAddress());
|
||||
Transaction.ApprovalStatus cancelDilbertInviteStatus2 = signForGroupApproval(repository, cancelDilbertInvite2, List.of(alice, bob));
|
||||
|
||||
// assert approved
|
||||
assertEquals(Transaction.ApprovalStatus.APPROVED, cancelDilbertInviteStatus2);
|
||||
|
||||
// dilbert tries to join after the group approves cancellation
|
||||
joinGroup(repository, dilbert, DEV_GROUP_ID);
|
||||
|
||||
// assert dilbert is not in the group
|
||||
assertFalse(isMember(repository, dilbert.getAddress(), DEV_GROUP_ID));
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testGetAdmin() throws DataException{
|
||||
try (final Repository repository = RepositoryManager.getRepository()) {
|
||||
|
||||
// establish accounts
|
||||
PrivateKeyAccount alice = Common.getTestAccount(repository, ALICE);
|
||||
PrivateKeyAccount bob = Common.getTestAccount(repository, BOB);
|
||||
|
||||
GroupAdminData aliceAdminData = repository.getGroupRepository().getAdmin(DEV_GROUP_ID, alice.getAddress());
|
||||
|
||||
assertNotNull(aliceAdminData);
|
||||
assertEquals( alice.getAddress(), aliceAdminData.getAdmin() );
|
||||
assertEquals( DEV_GROUP_ID, aliceAdminData.getGroupId());
|
||||
|
||||
GroupAdminData bobAdminData = repository.getGroupRepository().getAdmin(DEV_GROUP_ID, bob.getAddress());
|
||||
|
||||
assertNull(bobAdminData);
|
||||
}
|
||||
}
|
||||
|
||||
private Transaction.ApprovalStatus signForGroupApproval(Repository repository, TransactionData data, List<PrivateKeyAccount> signers) throws DataException {
|
||||
|
||||
for (PrivateKeyAccount signer : signers) {
|
||||
signTransactionDataForGroupApproval(repository, signer, data);
|
||||
}
|
||||
|
||||
BlockUtils.mintBlocks(repository, 2);
|
||||
|
||||
// return approval status
|
||||
return GroupUtils.getApprovalStatus(repository, data.getSignature());
|
||||
}
|
||||
|
||||
private static void signTransactionDataForGroupApproval(Repository repository, PrivateKeyAccount signer, TransactionData transactionData) throws DataException {
|
||||
byte[] reference = signer.getLastReference();
|
||||
long timestamp = repository.getTransactionRepository().fromSignature(reference).getTimestamp() + 1;
|
||||
|
||||
BaseTransactionData baseTransactionData
|
||||
= new BaseTransactionData(timestamp, Group.NO_GROUP, reference, signer.getPublicKey(), GroupUtils.fee, null);
|
||||
TransactionData groupApprovalTransactionData
|
||||
= new GroupApprovalTransactionData(baseTransactionData, transactionData.getSignature(), true);
|
||||
|
||||
TransactionUtils.signAndImportValid(repository, groupApprovalTransactionData, signer);
|
||||
}
|
||||
|
||||
private ValidationResult joinGroup(Repository repository, PrivateKeyAccount joiner, int groupId) throws DataException {
|
||||
JoinGroupTransactionData transactionData = new JoinGroupTransactionData(TestTransaction.generateBase(joiner), groupId);
|
||||
@ -332,9 +622,31 @@ public class DevGroupAdminTests extends Common {
|
||||
return result;
|
||||
}
|
||||
|
||||
private void groupInvite(Repository repository, PrivateKeyAccount admin, int groupId, String invitee, int timeToLive) throws DataException {
|
||||
private ValidationResult groupInvite(Repository repository, PrivateKeyAccount admin, int groupId, String invitee, int timeToLive) throws DataException {
|
||||
GroupInviteTransactionData transactionData = new GroupInviteTransactionData(TestTransaction.generateBase(admin), groupId, invitee, timeToLive);
|
||||
ValidationResult result = TransactionUtils.signAndImport(repository, transactionData, admin);
|
||||
|
||||
if (result == ValidationResult.OK)
|
||||
BlockUtils.mintBlock(repository);
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
private TransactionData createGroupInviteForGroupApproval(Repository repository, PrivateKeyAccount admin, int groupId, String invitee, int timeToLive) throws DataException {
|
||||
GroupInviteTransactionData transactionData = new GroupInviteTransactionData(TestTransaction.generateBase(admin, groupId), groupId, invitee, timeToLive);
|
||||
TransactionUtils.signAndMint(repository, transactionData, admin);
|
||||
return transactionData;
|
||||
}
|
||||
|
||||
private TransactionData createCancelInviteForGroupApproval(Repository repository, PrivateKeyAccount admin, int groupId, String inviteeToCancel) throws DataException {
|
||||
CancelGroupInviteTransactionData transactionData = new CancelGroupInviteTransactionData(TestTransaction.generateBase(admin, groupId), groupId, inviteeToCancel);
|
||||
TransactionUtils.signAndMint(repository, transactionData, admin);
|
||||
return transactionData;
|
||||
}
|
||||
|
||||
private ValidationResult signAndImportGroupInvite(Repository repository, PrivateKeyAccount admin, int groupId, String invitee, int timeToLive) throws DataException {
|
||||
GroupInviteTransactionData transactionData = new GroupInviteTransactionData(TestTransaction.generateBase(admin, groupId), groupId, invitee, timeToLive);
|
||||
return TransactionUtils.signAndImport(repository, transactionData, admin);
|
||||
}
|
||||
|
||||
private ValidationResult groupKick(Repository repository, PrivateKeyAccount admin, int groupId, String member) throws DataException {
|
||||
@ -347,6 +659,13 @@ public class DevGroupAdminTests extends Common {
|
||||
return result;
|
||||
}
|
||||
|
||||
private TransactionData createGroupKickForGroupApproval(Repository repository, PrivateKeyAccount admin, int groupId, String kicked, String reason) throws DataException {
|
||||
GroupKickTransactionData transactionData = new GroupKickTransactionData(TestTransaction.generateBase(admin, groupId), groupId, kicked, reason);
|
||||
TransactionUtils.signAndMint(repository, transactionData, admin);
|
||||
|
||||
return transactionData;
|
||||
}
|
||||
|
||||
private ValidationResult groupBan(Repository repository, PrivateKeyAccount admin, int groupId, String member) throws DataException {
|
||||
GroupBanTransactionData transactionData = new GroupBanTransactionData(TestTransaction.generateBase(admin), groupId, member, "testing", 0);
|
||||
ValidationResult result = TransactionUtils.signAndImport(repository, transactionData, admin);
|
||||
@ -357,6 +676,13 @@ public class DevGroupAdminTests extends Common {
|
||||
return result;
|
||||
}
|
||||
|
||||
private TransactionData createGroupBanForGroupApproval(Repository repository, PrivateKeyAccount admin, int groupId, String banned, String reason, int timeToLive) throws DataException {
|
||||
GroupBanTransactionData transactionData = new GroupBanTransactionData(TestTransaction.generateBase(admin, groupId), groupId, banned, reason, timeToLive);
|
||||
TransactionUtils.signAndMint(repository, transactionData, admin);
|
||||
|
||||
return transactionData;
|
||||
}
|
||||
|
||||
private ValidationResult cancelGroupBan(Repository repository, PrivateKeyAccount admin, int groupId, String member) throws DataException {
|
||||
CancelGroupBanTransactionData transactionData = new CancelGroupBanTransactionData(TestTransaction.generateBase(admin), groupId, member);
|
||||
ValidationResult result = TransactionUtils.signAndImport(repository, transactionData, admin);
|
||||
@ -367,6 +693,14 @@ public class DevGroupAdminTests extends Common {
|
||||
return result;
|
||||
}
|
||||
|
||||
private TransactionData createCancelGroupBanForGroupApproval(Repository repository, PrivateKeyAccount admin, int groupId, String unbanned ) throws DataException {
|
||||
CancelGroupBanTransactionData transactionData = new CancelGroupBanTransactionData( TestTransaction.generateBase(admin, groupId), groupId, unbanned);
|
||||
|
||||
TransactionUtils.signAndMint(repository, transactionData, admin);
|
||||
|
||||
return transactionData;
|
||||
}
|
||||
|
||||
private TransactionData addGroupAdmin(Repository repository, PrivateKeyAccount owner, int groupId, String member) throws DataException {
|
||||
AddGroupAdminTransactionData transactionData = new AddGroupAdminTransactionData(TestTransaction.generateBase(owner), groupId, member);
|
||||
transactionData.setTxGroupId(groupId);
|
||||
|
@ -399,7 +399,7 @@ public class HSQLDBCacheUtilsTests {
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testExcludeBlockedPositive() {
|
||||
public void testExcludeBlockedNegative() {
|
||||
|
||||
ArbitraryResourceData data = new ArbitraryResourceData();
|
||||
data.name = "Joe";
|
||||
@ -413,6 +413,21 @@ public class HSQLDBCacheUtilsTests {
|
||||
);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testExcludeBlockedPositive() {
|
||||
|
||||
ArbitraryResourceData data = new ArbitraryResourceData();
|
||||
data.name = "Joe";
|
||||
|
||||
Supplier<List<String>> supplier = () -> List.of("Joe");
|
||||
|
||||
filterListByMap(
|
||||
List.of(data),
|
||||
NAME_LEVEL, new HashMap<>(Map.of(EXCLUDE_BLOCKED, supplier)),
|
||||
0
|
||||
);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testIncludeMetadataPositive() {
|
||||
|
||||
|
@ -0,0 +1,763 @@
|
||||
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;
|
||||
import java.util.concurrent.ConcurrentHashMap;
|
||||
import java.util.concurrent.CopyOnWriteArrayList;
|
||||
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));
|
||||
|
||||
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, 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);
|
||||
|
||||
Assert.assertTrue( compare > 0);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testAddressAmountComparatorForwardOrder() {
|
||||
|
||||
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);
|
||||
|
||||
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, new ArrayList<>(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, new ArrayList<>(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 testBuildBalanceDynamicOneAccountAdjustment() {
|
||||
List<AccountBalanceData> balances = new ArrayList<>(1);
|
||||
balances.add(new AccountBalanceData(RECIPIENT_ADDRESS, 0, 20));
|
||||
|
||||
List<AccountBalanceData> priorBalances = new ArrayList<>(0);
|
||||
priorBalances.add(new AccountBalanceData(RECIPIENT_ADDRESS, 0, 12));
|
||||
|
||||
List<TransactionData> 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<AddressAmountData> 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() {
|
||||
|
||||
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, new ArrayList<>(0));
|
||||
|
||||
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, false);
|
||||
dynamics.add(new BlockHeightRangeAddressAmounts(range1, new ArrayList<>()));
|
||||
|
||||
BlockHeightRange range2 = new BlockHeightRange(1, 4, false);
|
||||
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, false);
|
||||
dynamics.add(new BlockHeightRangeAddressAmounts(range1, new ArrayList<>()));
|
||||
|
||||
BlockHeightRange range2 = new BlockHeightRange(6, 11, false);
|
||||
dynamics.add((new BlockHeightRangeAddressAmounts(range2, new ArrayList<>())));
|
||||
|
||||
BlockHeightRange range3 = new BlockHeightRange(22, 16, false);
|
||||
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, 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, 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<String, Long> 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<String, Long> 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<String, Long> 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<String, Long> 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<String, Long> 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<String, Long> amountsByAddress = new HashMap<>();
|
||||
|
||||
List<PaymentData> 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<String, Long> amountsByAddress = new HashMap<>();
|
||||
|
||||
List<PaymentData> 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<String, Long> amountsByAddress = new HashMap<>();
|
||||
|
||||
List<PaymentData> 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<String, Long> 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<String, Long> 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<String, Long> 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<String, Long> amountsByAddress, long amount, String address) {
|
||||
Long amountForAddress = amountsByAddress.get(address);
|
||||
|
||||
Assert.assertTrue(amountsByAddress.containsKey(address));
|
||||
Assert.assertNotNull(amountForAddress);
|
||||
Assert.assertEquals(amount, amountForAddress.longValue());
|
||||
}
|
||||
}
|
48
src/test/java/org/qortal/test/utils/TestUtils.java
Normal file
48
src/test/java/org/qortal/test/utils/TestUtils.java
Normal file
@ -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();
|
||||
}
|
||||
}
|
@ -103,7 +103,12 @@
|
||||
"onlyMintWithNameHeight": 9999999999990,
|
||||
"groupMemberCheckHeight": 9999999999999,
|
||||
"decreaseOnlineAccountsDifficultyTimestamp": 9999999999999,
|
||||
"removeOnlyMintWithNameHeight": 9999999999999
|
||||
"removeOnlyMintWithNameHeight": 9999999999999,
|
||||
"fixBatchRewardHeight": 9999999999999,
|
||||
"adminsReplaceFoundersHeight": 9999999999999,
|
||||
"ignoreLevelForRewardShareHeight": 9999999999999,
|
||||
"nullGroupMembershipHeight": 20,
|
||||
"adminQueryFixHeight": 9999999999999
|
||||
},
|
||||
"genesisInfo": {
|
||||
"version": 4,
|
||||
|
Loading…
x
Reference in New Issue
Block a user