Compare commits

..

No commits in common. "master" and "v4.6.6" have entirely different histories.

123 changed files with 683 additions and 6207 deletions

View File

@ -22,10 +22,6 @@ jobs:
java-version: '11' java-version: '11'
distribution: 'adopt' distribution: 'adopt'
- name: Load custom deps
run: |
mvn install -DskipTests=true --file pom.xml
- name: Run all tests - name: Run all tests
run: | run: |
mvn -B clean test -DskipTests=false --file pom.xml mvn -B clean test -DskipTests=false --file pom.xml

View File

@ -15,31 +15,20 @@ Building the future one block at a time. Welcome to Qortal.
# Building the Qortal Core from Source # Building the Qortal Core from Source
## Build / Run ## Build / run
- Requires Java 11. OpenJDK 11 recommended over Java SE. - Requires Java 11. OpenJDK 11 recommended over Java SE.
- Install Maven - Install Maven
- Use Maven to fetch dependencies and build: `mvn clean package` - Use Maven to fetch dependencies and build: `mvn clean package`
- Update Maven dependencies: `mvn install`
- Built JAR should be something like `target/qortal-1.0.jar` - Built JAR should be something like `target/qortal-1.0.jar`
- Create basic *settings.json* file: `echo '{}' > settings.json` - Create basic *settings.json* file: `echo '{}' > settings.json`
- Run JAR in same working directory as *settings.json*: `java -jar target/qortal-1.0.jar` - Run JAR in same working directory as *settings.json*: `java -jar target/qortal-1.0.jar`
- Wrap in shell script, add JVM flags, redirection, backgrounding, etc. as necessary. - Wrap in shell script, add JVM flags, redirection, backgrounding, etc. as necessary.
- Or use supplied example shell script: *start.sh* - Or use supplied example shell script: *start.sh*
## IntelliJ IDEA Configuration
- Run -> Edit Configurations
- Add New Application
- Name: qortal
- SDK: java 11
- Main Class: org.qortal.controller.Controller
- Program arguments: settings.json -Dlog4j.configurationFile=log4j2.properties -ea
- Environment variables: Djava.net.preferIPv4Stack=false
# Using a pre-built Qortal 'jar' binary # Using a pre-built Qortal 'jar' binary
If you prefer to utilize a released version of Qortal, you may do so by downloading one of the available releases from the releases page, that are also linked on https://qortal.org and https://qortal.dev. If you would prefer to utilize a released version of Qortal, you may do so by downloading one of the available releases from the releases page, that are also linked on https://qortal.org and https://qortal.dev.
# Learning Q-App Development # Learning Q-App Development

View File

@ -1,5 +0,0 @@
This is the production hsqldb-2.7.4 with the manifest file updated
Sealed: false
Allows the addition of the custom Qortal HSQLDBPool and Monitoring Classes

34
pom.xml
View File

@ -3,13 +3,12 @@
<modelVersion>4.0.0</modelVersion> <modelVersion>4.0.0</modelVersion>
<groupId>org.qortal</groupId> <groupId>org.qortal</groupId>
<artifactId>qortal</artifactId> <artifactId>qortal</artifactId>
<version>4.7.1</version> <version>4.6.6</version>
<packaging>jar</packaging> <packaging>jar</packaging>
<properties> <properties>
<project.build.sourceEncoding>UTF-8</project.build.sourceEncoding> <project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
<skipTests>true</skipTests> <skipTests>true</skipTests>
<altcoinj.version>7dc8c6f</altcoinj.version>
<altcoinj.version>7dc8c6f</altcoinj.version> <altcoinj.version>7dc8c6f</altcoinj.version>
<bitcoinj.version>0.15.10</bitcoinj.version> <bitcoinj.version>0.15.10</bitcoinj.version>
<bouncycastle.version>1.70</bouncycastle.version> <bouncycastle.version>1.70</bouncycastle.version>
@ -50,7 +49,6 @@
<maven-reproducible-build-plugin.version>0.17</maven-reproducible-build-plugin.version> <maven-reproducible-build-plugin.version>0.17</maven-reproducible-build-plugin.version>
<maven-resources-plugin.version>3.3.1</maven-resources-plugin.version> <maven-resources-plugin.version>3.3.1</maven-resources-plugin.version>
<maven-shade-plugin.version>3.6.0</maven-shade-plugin.version> <maven-shade-plugin.version>3.6.0</maven-shade-plugin.version>
<maven-install-plugin.version>3.1.3</maven-install-plugin.version>
<maven-surefire-plugin.version>3.5.2</maven-surefire-plugin.version> <maven-surefire-plugin.version>3.5.2</maven-surefire-plugin.version>
<protobuf.version>3.25.3</protobuf.version> <protobuf.version>3.25.3</protobuf.version>
<replacer.version>1.5.3</replacer.version> <replacer.version>1.5.3</replacer.version>
@ -305,30 +303,6 @@
</archive> </archive>
</configuration> </configuration>
</plugin> </plugin>
<!-- Copy modified hsqldb.jar to install / modified MANIFEST.MF-->
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-install-plugin</artifactId>
<version>${maven-install-plugin.version}</version>
<configuration>
<groupId>org.hsqldb</groupId>
<artifactId>hsqldb</artifactId>
<version>${hsqldb.version}</version>
<packaging>jar</packaging>
</configuration>
<executions>
<execution>
<phase>install</phase>
<goals>
<goal>install-file</goal>
</goals>
<configuration>
<file>${project.basedir}/lib/org/hsqldb/hsqldb/${hsqldb.version}/hsqldb.jar</file>
</configuration>
</execution>
</executions>
</plugin>
<plugin> <plugin>
<groupId>org.apache.maven.plugins</groupId> <groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-shade-plugin</artifactId> <artifactId>maven-shade-plugin</artifactId>
@ -476,7 +450,7 @@
<scope>provided</scope> <scope>provided</scope>
<!-- needed for build, not for runtime --> <!-- needed for build, not for runtime -->
</dependency> </dependency>
<!-- HSQLDB for repository should use local version with Sealed: false --> <!-- HSQLDB for repository -->
<dependency> <dependency>
<groupId>org.hsqldb</groupId> <groupId>org.hsqldb</groupId>
<artifactId>hsqldb</artifactId> <artifactId>hsqldb</artifactId>
@ -747,12 +721,12 @@
<!-- BouncyCastle for crypto, including TLS secure networking --> <!-- BouncyCastle for crypto, including TLS secure networking -->
<dependency> <dependency>
<groupId>org.bouncycastle</groupId> <groupId>org.bouncycastle</groupId>
<artifactId>bcprov-jdk15to18</artifactId> <artifactId>bcprov-jdk15on</artifactId>
<version>${bouncycastle.version}</version> <version>${bouncycastle.version}</version>
</dependency> </dependency>
<dependency> <dependency>
<groupId>org.bouncycastle</groupId> <groupId>org.bouncycastle</groupId>
<artifactId>bctls-jdk15to18</artifactId> <artifactId>bctls-jdk15on</artifactId>
<version>${bouncycastle.version}</version> <version>${bouncycastle.version}</version>
</dependency> </dependency>
<dependency> <dependency>

View File

@ -1,173 +0,0 @@
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;
}
}

View File

@ -14,7 +14,6 @@ import org.qortal.repository.NameRepository;
import org.qortal.repository.Repository; import org.qortal.repository.Repository;
import org.qortal.settings.Settings; import org.qortal.settings.Settings;
import org.qortal.utils.Base58; import org.qortal.utils.Base58;
import org.qortal.utils.Groups;
import javax.xml.bind.annotation.XmlAccessType; import javax.xml.bind.annotation.XmlAccessType;
import javax.xml.bind.annotation.XmlAccessorType; import javax.xml.bind.annotation.XmlAccessorType;
@ -217,18 +216,9 @@ public class Account {
String myAddress = accountData.getAddress(); String myAddress = accountData.getAddress();
int blockchainHeight = this.repository.getBlockRepository().getBlockchainHeight(); 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 level = accountData.getLevel();
List<Integer> groupIdsToMint = Groups.getGroupIdsToMint( BlockChain.getInstance(), blockchainHeight ); int groupIdToMint = BlockChain.getInstance().getMintingGroupId();
int nameCheckHeight = BlockChain.getInstance().getOnlyMintWithNameHeight(); int nameCheckHeight = BlockChain.getInstance().getOnlyMintWithNameHeight();
int groupCheckHeight = BlockChain.getInstance().getGroupMemberCheckHeight(); int groupCheckHeight = BlockChain.getInstance().getGroupMemberCheckHeight();
int removeNameCheckHeight = BlockChain.getInstance().getRemoveOnlyMintWithNameHeight(); int removeNameCheckHeight = BlockChain.getInstance().getRemoveOnlyMintWithNameHeight();
@ -262,9 +252,9 @@ public class Account {
if (blockchainHeight >= groupCheckHeight && blockchainHeight < removeNameCheckHeight) { if (blockchainHeight >= groupCheckHeight && blockchainHeight < removeNameCheckHeight) {
List<NameData> myName = nameRepository.getNamesByOwner(myAddress); List<NameData> myName = nameRepository.getNamesByOwner(myAddress);
if (Account.isFounder(accountData.getFlags())) { if (Account.isFounder(accountData.getFlags())) {
return accountData.getBlocksMintedPenalty() == 0 && !myName.isEmpty() && (isGroupValidated || Groups.memberExistsInAnyGroup(groupRepository, groupIdsToMint, myAddress)); return accountData.getBlocksMintedPenalty() == 0 && !myName.isEmpty() && (isGroupValidated || groupRepository.memberExists(groupIdToMint, myAddress));
} else { } else {
return level >= levelToMint && !myName.isEmpty() && (isGroupValidated || Groups.memberExistsInAnyGroup(groupRepository, groupIdsToMint, myAddress)); return level >= levelToMint && !myName.isEmpty() && (isGroupValidated || groupRepository.memberExists(groupIdToMint, myAddress));
} }
} }
@ -273,9 +263,9 @@ public class Account {
// Account's address is a member of the minter group // Account's address is a member of the minter group
if (blockchainHeight >= removeNameCheckHeight) { if (blockchainHeight >= removeNameCheckHeight) {
if (Account.isFounder(accountData.getFlags())) { if (Account.isFounder(accountData.getFlags())) {
return accountData.getBlocksMintedPenalty() == 0 && (isGroupValidated || Groups.memberExistsInAnyGroup(groupRepository, groupIdsToMint, myAddress)); return accountData.getBlocksMintedPenalty() == 0 && (isGroupValidated || groupRepository.memberExists(groupIdToMint, myAddress));
} else { } else {
return level >= levelToMint && (isGroupValidated || Groups.memberExistsInAnyGroup(groupRepository, groupIdsToMint, myAddress)); return level >= levelToMint && (isGroupValidated || groupRepository.memberExists(groupIdToMint, myAddress));
} }
} }
@ -316,9 +306,6 @@ public class Account {
if (Account.isFounder(accountData.getFlags()) && accountData.getBlocksMintedPenalty() == 0) if (Account.isFounder(accountData.getFlags()) && accountData.getBlocksMintedPenalty() == 0)
return true; return true;
if( this.repository.getBlockRepository().getBlockchainHeight() >= BlockChain.getInstance().getIgnoreLevelForRewardShareHeight() )
return true;
return false; return false;
} }

View File

@ -194,7 +194,6 @@ public class ApiService {
context.addServlet(AdminStatusWebSocket.class, "/websockets/admin/status"); context.addServlet(AdminStatusWebSocket.class, "/websockets/admin/status");
context.addServlet(BlocksWebSocket.class, "/websockets/blocks"); context.addServlet(BlocksWebSocket.class, "/websockets/blocks");
context.addServlet(DataMonitorSocket.class, "/websockets/datamonitor");
context.addServlet(ActiveChatsWebSocket.class, "/websockets/chat/active/*"); context.addServlet(ActiveChatsWebSocket.class, "/websockets/chat/active/*");
context.addServlet(ChatMessagesWebSocket.class, "/websockets/chat/messages"); context.addServlet(ChatMessagesWebSocket.class, "/websockets/chat/messages");
context.addServlet(TradeOffersWebSocket.class, "/websockets/crosschain/tradeoffers"); context.addServlet(TradeOffersWebSocket.class, "/websockets/crosschain/tradeoffers");

View File

@ -1,72 +0,0 @@
package org.qortal.api.model;
import io.swagger.v3.oas.annotations.media.Schema;
import org.qortal.data.crosschain.CrossChainTradeData;
import javax.xml.bind.annotation.XmlAccessType;
import javax.xml.bind.annotation.XmlAccessorType;
import javax.xml.bind.annotation.adapters.XmlJavaTypeAdapter;
// All properties to be converted to JSON via JAXB
@XmlAccessorType(XmlAccessType.FIELD)
public class CrossChainTradeLedgerEntry {
private String market;
private String currency;
@XmlJavaTypeAdapter(value = org.qortal.api.AmountTypeAdapter.class)
private long quantity;
@XmlJavaTypeAdapter(value = org.qortal.api.AmountTypeAdapter.class)
private long feeAmount;
private String feeCurrency;
@XmlJavaTypeAdapter(value = org.qortal.api.AmountTypeAdapter.class)
private long totalPrice;
private long tradeTimestamp;
protected CrossChainTradeLedgerEntry() {
/* For JAXB */
}
public CrossChainTradeLedgerEntry(String market, String currency, long quantity, long feeAmount, String feeCurrency, long totalPrice, long tradeTimestamp) {
this.market = market;
this.currency = currency;
this.quantity = quantity;
this.feeAmount = feeAmount;
this.feeCurrency = feeCurrency;
this.totalPrice = totalPrice;
this.tradeTimestamp = tradeTimestamp;
}
public String getMarket() {
return market;
}
public String getCurrency() {
return currency;
}
public long getQuantity() {
return quantity;
}
public long getFeeAmount() {
return feeAmount;
}
public String getFeeCurrency() {
return feeCurrency;
}
public long getTotalPrice() {
return totalPrice;
}
public long getTradeTimestamp() {
return tradeTimestamp;
}
}

View File

@ -1,50 +0,0 @@
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 +
'}';
}
}

View File

@ -33,13 +33,9 @@ import org.qortal.controller.arbitrary.ArbitraryDataStorageManager;
import org.qortal.controller.arbitrary.ArbitraryMetadataManager; import org.qortal.controller.arbitrary.ArbitraryMetadataManager;
import org.qortal.data.account.AccountData; import org.qortal.data.account.AccountData;
import org.qortal.data.arbitrary.ArbitraryCategoryInfo; import org.qortal.data.arbitrary.ArbitraryCategoryInfo;
import org.qortal.data.arbitrary.ArbitraryDataIndexDetail;
import org.qortal.data.arbitrary.ArbitraryDataIndexScoreKey;
import org.qortal.data.arbitrary.ArbitraryDataIndexScorecard;
import org.qortal.data.arbitrary.ArbitraryResourceData; import org.qortal.data.arbitrary.ArbitraryResourceData;
import org.qortal.data.arbitrary.ArbitraryResourceMetadata; import org.qortal.data.arbitrary.ArbitraryResourceMetadata;
import org.qortal.data.arbitrary.ArbitraryResourceStatus; import org.qortal.data.arbitrary.ArbitraryResourceStatus;
import org.qortal.data.arbitrary.IndexCache;
import org.qortal.data.naming.NameData; import org.qortal.data.naming.NameData;
import org.qortal.data.transaction.ArbitraryTransactionData; import org.qortal.data.transaction.ArbitraryTransactionData;
import org.qortal.data.transaction.TransactionData; import org.qortal.data.transaction.TransactionData;
@ -73,11 +69,8 @@ import java.nio.file.Files;
import java.nio.file.Paths; import java.nio.file.Paths;
import java.util.ArrayList; import java.util.ArrayList;
import java.util.Arrays; import java.util.Arrays;
import java.util.Comparator;
import java.util.List; import java.util.List;
import java.util.Map;
import java.util.Objects; import java.util.Objects;
import java.util.stream.Collectors;
@Path("/arbitrary") @Path("/arbitrary")
@Tag(name = "Arbitrary") @Tag(name = "Arbitrary")
@ -179,7 +172,6 @@ public class ArbitraryResource {
@Parameter(description = "Name (searches name field only)") @QueryParam("name") List<String> names, @Parameter(description = "Name (searches name field only)") @QueryParam("name") List<String> names,
@Parameter(description = "Title (searches title metadata field only)") @QueryParam("title") String title, @Parameter(description = "Title (searches title metadata field only)") @QueryParam("title") String title,
@Parameter(description = "Description (searches description metadata field only)") @QueryParam("description") String description, @Parameter(description = "Description (searches description metadata field only)") @QueryParam("description") String description,
@Parameter(description = "Keyword (searches description metadata field by keywords)") @QueryParam("keywords") List<String> keywords,
@Parameter(description = "Prefix only (if true, only the beginning of fields are matched)") @QueryParam("prefix") Boolean prefixOnly, @Parameter(description = "Prefix only (if true, only the beginning of fields are matched)") @QueryParam("prefix") Boolean prefixOnly,
@Parameter(description = "Exact match names only (if true, partial name matches are excluded)") @QueryParam("exactmatchnames") Boolean exactMatchNamesOnly, @Parameter(description = "Exact match names only (if true, partial name matches are excluded)") @QueryParam("exactmatchnames") Boolean exactMatchNamesOnly,
@Parameter(description = "Default resources (without identifiers) only") @QueryParam("default") Boolean defaultResource, @Parameter(description = "Default resources (without identifiers) only") @QueryParam("default") Boolean defaultResource,
@ -220,7 +212,7 @@ public class ArbitraryResource {
} }
List<ArbitraryResourceData> resources = repository.getArbitraryRepository() List<ArbitraryResourceData> resources = repository.getArbitraryRepository()
.searchArbitraryResources(service, query, identifier, names, title, description, keywords, usePrefixOnly, .searchArbitraryResources(service, query, identifier, names, title, description, usePrefixOnly,
exactMatchNames, defaultRes, mode, minLevel, followedOnly, excludeBlocked, includeMetadata, includeStatus, exactMatchNames, defaultRes, mode, minLevel, followedOnly, excludeBlocked, includeMetadata, includeStatus,
before, after, limit, offset, reverse); before, after, limit, offset, reverse);
@ -1193,90 +1185,6 @@ public class ArbitraryResource {
} }
} }
@GET
@Path("/indices")
@Operation(
summary = "Find matching arbitrary resource indices",
description = "",
responses = {
@ApiResponse(
description = "indices",
content = @Content(
array = @ArraySchema(
schema = @Schema(
implementation = ArbitraryDataIndexScorecard.class
)
)
)
)
}
)
public List<ArbitraryDataIndexScorecard> searchIndices(@QueryParam("terms") String[] terms) {
List<ArbitraryDataIndexDetail> indices = new ArrayList<>();
// get index details for each term
for( String term : terms ) {
List<ArbitraryDataIndexDetail> details = IndexCache.getInstance().getIndicesByTerm().get(term);
if( details != null ) {
indices.addAll(details);
}
}
// sum up the scores for each index with identical attributes
Map<ArbitraryDataIndexScoreKey, Double> scoreForKey
= indices.stream()
.collect(
Collectors.groupingBy(
index -> new ArbitraryDataIndexScoreKey(index.name, index.category, index.link),
Collectors.summingDouble(detail -> 1.0 / detail.rank)
)
);
// create scorecards for each index group and put them in descending order by score
List<ArbitraryDataIndexScorecard> scorecards
= scoreForKey.entrySet().stream().map(
entry
->
new ArbitraryDataIndexScorecard(
entry.getValue(),
entry.getKey().name,
entry.getKey().category,
entry.getKey().link)
)
.sorted(Comparator.comparingDouble(ArbitraryDataIndexScorecard::getScore).reversed())
.collect(Collectors.toList());
return scorecards;
}
@GET
@Path("/indices/{name}/{idPrefix}")
@Operation(
summary = "Find matching arbitrary resource indices for a registered name and identifier prefix",
description = "",
responses = {
@ApiResponse(
description = "indices",
content = @Content(
array = @ArraySchema(
schema = @Schema(
implementation = ArbitraryDataIndexDetail.class
)
)
)
)
}
)
public List<ArbitraryDataIndexDetail> searchIndicesByName(@PathParam("name") String name, @PathParam("idPrefix") String idPrefix) {
return
IndexCache.getInstance().getIndicesByIssuer()
.getOrDefault(name, new ArrayList<>(0)).stream()
.filter( indexDetail -> indexDetail.indexIdentifer.startsWith(idPrefix))
.collect(Collectors.toList());
}
// Shared methods // Shared methods

View File

@ -16,13 +16,9 @@ import org.qortal.api.model.AggregatedOrder;
import org.qortal.api.model.TradeWithOrderInfo; import org.qortal.api.model.TradeWithOrderInfo;
import org.qortal.api.resource.TransactionsResource.ConfirmationStatus; import org.qortal.api.resource.TransactionsResource.ConfirmationStatus;
import org.qortal.asset.Asset; import org.qortal.asset.Asset;
import org.qortal.controller.hsqldb.HSQLDBBalanceRecorder;
import org.qortal.crypto.Crypto; import org.qortal.crypto.Crypto;
import org.qortal.data.account.AccountBalanceData; import org.qortal.data.account.AccountBalanceData;
import org.qortal.data.account.AccountData; import org.qortal.data.account.AccountData;
import org.qortal.data.account.AddressAmountData;
import org.qortal.data.account.BlockHeightRange;
import org.qortal.data.account.BlockHeightRangeAddressAmounts;
import org.qortal.data.asset.AssetData; import org.qortal.data.asset.AssetData;
import org.qortal.data.asset.OrderData; import org.qortal.data.asset.OrderData;
import org.qortal.data.asset.RecentTradeData; import org.qortal.data.asset.RecentTradeData;
@ -37,7 +33,6 @@ import org.qortal.transaction.Transaction;
import org.qortal.transaction.Transaction.ValidationResult; import org.qortal.transaction.Transaction.ValidationResult;
import org.qortal.transform.TransformationException; import org.qortal.transform.TransformationException;
import org.qortal.transform.transaction.*; import org.qortal.transform.transaction.*;
import org.qortal.utils.BalanceRecorderUtils;
import org.qortal.utils.Base58; import org.qortal.utils.Base58;
import javax.servlet.http.HttpServletRequest; import javax.servlet.http.HttpServletRequest;
@ -47,7 +42,6 @@ import javax.ws.rs.core.MediaType;
import java.util.ArrayList; import java.util.ArrayList;
import java.util.Collections; import java.util.Collections;
import java.util.List; import java.util.List;
import java.util.Optional;
import java.util.stream.Collectors; import java.util.stream.Collectors;
@Path("/assets") @Path("/assets")
@ -185,122 +179,6 @@ 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 @GET
@Path("/openorders/{assetid}/{otherassetid}") @Path("/openorders/{assetid}/{otherassetid}")
@Operation( @Operation(

View File

@ -19,8 +19,6 @@ import org.qortal.crypto.Crypto;
import org.qortal.data.account.AccountData; import org.qortal.data.account.AccountData;
import org.qortal.data.block.BlockData; import org.qortal.data.block.BlockData;
import org.qortal.data.block.BlockSummaryData; 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.data.transaction.TransactionData;
import org.qortal.repository.BlockArchiveReader; import org.qortal.repository.BlockArchiveReader;
import org.qortal.repository.DataException; import org.qortal.repository.DataException;
@ -29,7 +27,6 @@ import org.qortal.repository.RepositoryManager;
import org.qortal.transform.TransformationException; import org.qortal.transform.TransformationException;
import org.qortal.transform.block.BlockTransformer; import org.qortal.transform.block.BlockTransformer;
import org.qortal.utils.Base58; import org.qortal.utils.Base58;
import org.qortal.utils.Blocks;
import org.qortal.utils.Triple; import org.qortal.utils.Triple;
import javax.servlet.http.HttpServletRequest; import javax.servlet.http.HttpServletRequest;
@ -48,7 +45,6 @@ import java.util.ArrayList;
import java.util.Arrays; import java.util.Arrays;
import java.util.Comparator; import java.util.Comparator;
import java.util.List; import java.util.List;
import java.util.Set;
@Path("/blocks") @Path("/blocks")
@Tag(name = "Blocks") @Tag(name = "Blocks")
@ -893,50 +889,4 @@ public class BlocksResource {
throw ApiExceptionFactory.INSTANCE.createException(request, ApiError.REPOSITORY_ISSUE, e); 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);
}
}
} }

View File

@ -10,13 +10,11 @@ import io.swagger.v3.oas.annotations.parameters.RequestBody;
import io.swagger.v3.oas.annotations.responses.ApiResponse; import io.swagger.v3.oas.annotations.responses.ApiResponse;
import io.swagger.v3.oas.annotations.security.SecurityRequirement; import io.swagger.v3.oas.annotations.security.SecurityRequirement;
import io.swagger.v3.oas.annotations.tags.Tag; import io.swagger.v3.oas.annotations.tags.Tag;
import org.glassfish.jersey.media.multipart.ContentDisposition;
import org.qortal.api.ApiError; import org.qortal.api.ApiError;
import org.qortal.api.ApiErrors; import org.qortal.api.ApiErrors;
import org.qortal.api.ApiExceptionFactory; import org.qortal.api.ApiExceptionFactory;
import org.qortal.api.Security; import org.qortal.api.Security;
import org.qortal.api.model.CrossChainCancelRequest; import org.qortal.api.model.CrossChainCancelRequest;
import org.qortal.api.model.CrossChainTradeLedgerEntry;
import org.qortal.api.model.CrossChainTradeSummary; import org.qortal.api.model.CrossChainTradeSummary;
import org.qortal.controller.tradebot.TradeBot; import org.qortal.controller.tradebot.TradeBot;
import org.qortal.crosschain.ACCT; import org.qortal.crosschain.ACCT;
@ -46,20 +44,14 @@ import org.qortal.utils.Base58;
import org.qortal.utils.ByteArray; import org.qortal.utils.ByteArray;
import org.qortal.utils.NTP; import org.qortal.utils.NTP;
import javax.servlet.ServletContext;
import javax.servlet.http.HttpServletRequest; import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import javax.ws.rs.*; import javax.ws.rs.*;
import javax.ws.rs.core.Context; import javax.ws.rs.core.Context;
import javax.ws.rs.core.HttpHeaders;
import javax.ws.rs.core.MediaType; import javax.ws.rs.core.MediaType;
import java.io.IOException;
import java.util.*; import java.util.*;
import java.util.function.Supplier; import java.util.function.Supplier;
import java.util.stream.Collectors; import java.util.stream.Collectors;
@Path("/crosschain") @Path("/crosschain")
@Tag(name = "Cross-Chain") @Tag(name = "Cross-Chain")
public class CrossChainResource { public class CrossChainResource {
@ -67,13 +59,6 @@ public class CrossChainResource {
@Context @Context
HttpServletRequest request; HttpServletRequest request;
@Context
HttpServletResponse response;
@Context
ServletContext context;
@GET @GET
@Path("/tradeoffers") @Path("/tradeoffers")
@Operation( @Operation(
@ -270,12 +255,6 @@ public class CrossChainResource {
description = "Only return trades that completed on/after this timestamp (milliseconds since epoch)", description = "Only return trades that completed on/after this timestamp (milliseconds since epoch)",
example = "1597310000000" example = "1597310000000"
) @QueryParam("minimumTimestamp") Long minimumTimestamp, ) @QueryParam("minimumTimestamp") Long minimumTimestamp,
@Parameter(
description = "Optionally filter by buyer Qortal public key"
) @QueryParam("buyerPublicKey") String buyerPublicKey58,
@Parameter(
description = "Optionally filter by seller Qortal public key"
) @QueryParam("sellerPublicKey") String sellerPublicKey58,
@Parameter( ref = "limit") @QueryParam("limit") Integer limit, @Parameter( ref = "limit") @QueryParam("limit") Integer limit,
@Parameter( ref = "offset" ) @QueryParam("offset") Integer offset, @Parameter( ref = "offset" ) @QueryParam("offset") Integer offset,
@Parameter( ref = "reverse" ) @QueryParam("reverse") Boolean reverse) { @Parameter( ref = "reverse" ) @QueryParam("reverse") Boolean reverse) {
@ -287,10 +266,6 @@ public class CrossChainResource {
if (minimumTimestamp != null && minimumTimestamp <= 0) if (minimumTimestamp != null && minimumTimestamp <= 0)
throw ApiExceptionFactory.INSTANCE.createException(request, ApiError.INVALID_CRITERIA); throw ApiExceptionFactory.INSTANCE.createException(request, ApiError.INVALID_CRITERIA);
// Decode public keys
byte[] buyerPublicKey = decodePublicKey(buyerPublicKey58);
byte[] sellerPublicKey = decodePublicKey(sellerPublicKey58);
final Boolean isFinished = Boolean.TRUE; final Boolean isFinished = Boolean.TRUE;
try (final Repository repository = RepositoryManager.getRepository()) { try (final Repository repository = RepositoryManager.getRepository()) {
@ -321,7 +296,7 @@ public class CrossChainResource {
byte[] codeHash = acctInfo.getKey().value; byte[] codeHash = acctInfo.getKey().value;
ACCT acct = acctInfo.getValue().get(); ACCT acct = acctInfo.getValue().get();
List<ATStateData> atStates = repository.getATRepository().getMatchingFinalATStates(codeHash, buyerPublicKey, sellerPublicKey, List<ATStateData> atStates = repository.getATRepository().getMatchingFinalATStates(codeHash,
isFinished, acct.getModeByteOffset(), (long) AcctMode.REDEEMED.value, minimumFinalHeight, isFinished, acct.getModeByteOffset(), (long) AcctMode.REDEEMED.value, minimumFinalHeight,
limit, offset, reverse); limit, offset, reverse);
@ -360,120 +335,6 @@ public class CrossChainResource {
} }
} }
/**
* Decode Public Key
*
* @param publicKey58 the public key in a string
*
* @return the public key in bytes
*/
private byte[] decodePublicKey(String publicKey58) {
if( publicKey58 == null ) return null;
if( publicKey58.isEmpty() ) return new byte[0];
byte[] publicKey;
try {
publicKey = Base58.decode(publicKey58);
} catch (NumberFormatException e) {
throw ApiExceptionFactory.INSTANCE.createException(request, ApiError.INVALID_PUBLIC_KEY, e);
}
// Correct size for public key?
if (publicKey.length != Transformer.PUBLIC_KEY_LENGTH)
throw ApiExceptionFactory.INSTANCE.createException(request, ApiError.INVALID_PUBLIC_KEY);
return publicKey;
}
@GET
@Path("/ledger/{publicKey}")
@Operation(
summary = "Accounting entries for all trades.",
description = "Returns accounting entries for all completed cross-chain trades",
responses = {
@ApiResponse(
content = @Content(
schema = @Schema(
type = "string",
format = "byte"
)
)
)
}
)
@ApiErrors({ApiError.INVALID_CRITERIA, ApiError.REPOSITORY_ISSUE})
public HttpServletResponse getLedgerEntries(
@PathParam("publicKey") String publicKey58,
@Parameter(
description = "Only return trades that completed on/after this timestamp (milliseconds since epoch)",
example = "1597310000000"
) @QueryParam("minimumTimestamp") Long minimumTimestamp) {
byte[] publicKey = decodePublicKey(publicKey58);
// minimumTimestamp (if given) needs to be positive
if (minimumTimestamp != null && minimumTimestamp <= 0)
throw ApiExceptionFactory.INSTANCE.createException(request, ApiError.INVALID_CRITERIA);
try (final Repository repository = RepositoryManager.getRepository()) {
Integer minimumFinalHeight = null;
if (minimumTimestamp != null) {
minimumFinalHeight = repository.getBlockRepository().getHeightFromTimestamp(minimumTimestamp);
// If not found in the block repository it will return either 0 or 1
if (minimumFinalHeight == 0 || minimumFinalHeight == 1) {
// Try the archive
minimumFinalHeight = repository.getBlockArchiveRepository().getHeightFromTimestamp(minimumTimestamp);
}
if (minimumFinalHeight == 0)
// We don't have any blocks since minimumTimestamp, let alone trades, so nothing to return
return response;
// height returned from repository is for block BEFORE timestamp
// but we want trades AFTER timestamp so bump height accordingly
minimumFinalHeight++;
}
List<CrossChainTradeLedgerEntry> crossChainTradeLedgerEntries = new ArrayList<>();
Map<ByteArray, Supplier<ACCT>> acctsByCodeHash = SupportedBlockchain.getAcctMap();
// collect ledger entries for each ACCT
for (Map.Entry<ByteArray, Supplier<ACCT>> acctInfo : acctsByCodeHash.entrySet()) {
byte[] codeHash = acctInfo.getKey().value;
ACCT acct = acctInfo.getValue().get();
// collect buys and sells
CrossChainUtils.collectLedgerEntries(publicKey, repository, minimumFinalHeight, crossChainTradeLedgerEntries, codeHash, acct, true);
CrossChainUtils.collectLedgerEntries(publicKey, repository, minimumFinalHeight, crossChainTradeLedgerEntries, codeHash, acct, false);
}
crossChainTradeLedgerEntries.sort((a, b) -> Longs.compare(a.getTradeTimestamp(), b.getTradeTimestamp()));
response.setStatus(HttpServletResponse.SC_OK);
response.setContentType("text/csv");
response.setHeader(
HttpHeaders.CONTENT_DISPOSITION,
ContentDisposition
.type("attachment")
.fileName(CrossChainUtils.createLedgerFileName(Crypto.toAddress(publicKey)))
.build()
.toString()
);
CrossChainUtils.writeToLedger( response.getWriter(), crossChainTradeLedgerEntries);
return response;
} catch (DataException e) {
throw ApiExceptionFactory.INSTANCE.createException(request, ApiError.REPOSITORY_ISSUE, e);
} catch (IOException e) {
response.setStatus(HttpServletResponse.SC_INTERNAL_SERVER_ERROR);
return response;
}
}
@GET @GET
@Path("/price/{blockchain}") @Path("/price/{blockchain}")
@Operation( @Operation(

View File

@ -10,36 +10,21 @@ import org.bitcoinj.script.ScriptBuilder;
import org.bouncycastle.util.Strings; import org.bouncycastle.util.Strings;
import org.json.simple.JSONObject; import org.json.simple.JSONObject;
import org.qortal.api.model.CrossChainTradeLedgerEntry;
import org.qortal.api.model.crosschain.BitcoinyTBDRequest; import org.qortal.api.model.crosschain.BitcoinyTBDRequest;
import org.qortal.crosschain.*; import org.qortal.crosschain.*;
import org.qortal.data.at.ATData; import org.qortal.data.at.ATData;
import org.qortal.data.at.ATStateData;
import org.qortal.data.crosschain.*; import org.qortal.data.crosschain.*;
import org.qortal.repository.DataException; import org.qortal.repository.DataException;
import org.qortal.repository.Repository; import org.qortal.repository.Repository;
import org.qortal.utils.Amounts;
import org.qortal.utils.BitTwiddling; import org.qortal.utils.BitTwiddling;
import java.io.BufferedWriter;
import java.io.IOException;
import java.io.OutputStreamWriter;
import java.io.PrintWriter;
import java.io.Writer;
import java.text.DateFormat;
import java.text.SimpleDateFormat;
import java.time.Instant;
import java.time.ZoneId;
import java.time.ZonedDateTime;
import java.util.*; import java.util.*;
import java.util.stream.Collectors; import java.util.stream.Collectors;
public class CrossChainUtils { public class CrossChainUtils {
public static final String QORT_CURRENCY_CODE = "QORT";
private static final Logger LOGGER = LogManager.getLogger(CrossChainUtils.class); private static final Logger LOGGER = LogManager.getLogger(CrossChainUtils.class);
public static final String CORE_API_CALL = "Core API Call"; public static final String CORE_API_CALL = "Core API Call";
public static final String QORTAL_EXCHANGE_LABEL = "Qortal";
public static ServerConfigurationInfo buildServerConfigurationInfo(Bitcoiny blockchain) { public static ServerConfigurationInfo buildServerConfigurationInfo(Bitcoiny blockchain) {
@ -647,128 +632,4 @@ public class CrossChainUtils {
byte[] lockTimeABytes = BitTwiddling.toBEByteArray((long) lockTimeA); byte[] lockTimeABytes = BitTwiddling.toBEByteArray((long) lockTimeA);
return Bytes.concat(partnerBitcoinPKH, hashOfSecretA, lockTimeABytes); return Bytes.concat(partnerBitcoinPKH, hashOfSecretA, lockTimeABytes);
} }
/**
* Write To Ledger
*
* @param writer the writer to the ledger
* @param entries the entries to write to the ledger
*
* @throws IOException
*/
public static void writeToLedger(Writer writer, List<CrossChainTradeLedgerEntry> entries) throws IOException {
BufferedWriter bufferedWriter = new BufferedWriter(writer);
StringJoiner header = new StringJoiner(",");
header.add("Market");
header.add("Currency");
header.add("Quantity");
header.add("Commission Paid");
header.add("Commission Currency");
header.add("Total Price");
header.add("Date Time");
header.add("Exchange");
bufferedWriter.append(header.toString());
DateFormat dateFormatter = new SimpleDateFormat("yyyyMMdd HH:mm");
dateFormatter.setTimeZone(TimeZone.getTimeZone("UTC"));
for( CrossChainTradeLedgerEntry entry : entries ) {
StringJoiner joiner = new StringJoiner(",");
joiner.add(entry.getMarket());
joiner.add(entry.getCurrency());
joiner.add(String.valueOf(Amounts.prettyAmount(entry.getQuantity())));
joiner.add(String.valueOf(Amounts.prettyAmount(entry.getFeeAmount())));
joiner.add(entry.getFeeCurrency());
joiner.add(String.valueOf(Amounts.prettyAmount(entry.getTotalPrice())));
joiner.add(dateFormatter.format(new Date(entry.getTradeTimestamp())));
joiner.add(QORTAL_EXCHANGE_LABEL);
bufferedWriter.newLine();
bufferedWriter.append(joiner.toString());
}
bufferedWriter.newLine();
bufferedWriter.flush();
}
/**
* Create Ledger File Name
*
* Create a file name the includes timestamp and address.
*
* @param address the address
*
* @return the file name created
*/
public static String createLedgerFileName(String address) {
DateFormat dateFormatter = new SimpleDateFormat("yyyyMMddHHmmss");
String fileName = "ledger-" + address + "-" + dateFormatter.format(new Date());
return fileName;
}
/**
* Collect Ledger Entries
*
* @param publicKey the public key for the ledger entries, buy and sell
* @param repository the data repository
* @param minimumFinalHeight the minimum block height for entries to be collected
* @param entries the ledger entries to add to
* @param codeHash code hash for the entry blockchain
* @param acct the ACCT for the entry blockchain
* @param isBuy true collecting entries for a buy, otherwise false
*
* @throws DataException
*/
public static void collectLedgerEntries(
byte[] publicKey,
Repository repository,
Integer minimumFinalHeight,
List<CrossChainTradeLedgerEntry> entries,
byte[] codeHash,
ACCT acct,
boolean isBuy) throws DataException {
// get all the final AT states for the code hash (foreign coin)
List<ATStateData> atStates
= repository.getATRepository().getMatchingFinalATStates(
codeHash,
isBuy ? publicKey : null,
!isBuy ? publicKey : null,
Boolean.TRUE, acct.getModeByteOffset(),
(long) AcctMode.REDEEMED.value,
minimumFinalHeight,
null, null, false
);
String foreignBlockchainCurrencyCode = acct.getBlockchain().getCurrencyCode();
// for each trade, build ledger entry, collect ledger entry
for (ATStateData atState : atStates) {
CrossChainTradeData crossChainTradeData = acct.populateTradeData(repository, atState);
// We also need block timestamp for use as trade timestamp
long localTimestamp = repository.getBlockRepository().getTimestampFromHeight(atState.getHeight());
if (localTimestamp == 0) {
// Try the archive
localTimestamp = repository.getBlockArchiveRepository().getTimestampFromHeight(atState.getHeight());
}
CrossChainTradeLedgerEntry ledgerEntry
= new CrossChainTradeLedgerEntry(
isBuy ? QORT_CURRENCY_CODE : foreignBlockchainCurrencyCode,
isBuy ? foreignBlockchainCurrencyCode : QORT_CURRENCY_CODE,
isBuy ? crossChainTradeData.qortAmount : crossChainTradeData.expectedForeignAmount,
0,
foreignBlockchainCurrencyCode,
isBuy ? crossChainTradeData.expectedForeignAmount : crossChainTradeData.qortAmount,
localTimestamp);
entries.add(ledgerEntry);
}
}
} }

View File

@ -32,7 +32,6 @@ import org.qortal.controller.Synchronizer.SynchronizationResult;
import org.qortal.controller.repository.BlockArchiveRebuilder; import org.qortal.controller.repository.BlockArchiveRebuilder;
import org.qortal.data.account.MintingAccountData; import org.qortal.data.account.MintingAccountData;
import org.qortal.data.account.RewardShareData; import org.qortal.data.account.RewardShareData;
import org.qortal.data.system.DbConnectionInfo;
import org.qortal.network.Network; import org.qortal.network.Network;
import org.qortal.network.Peer; import org.qortal.network.Peer;
import org.qortal.network.PeerAddress; import org.qortal.network.PeerAddress;
@ -41,7 +40,6 @@ import org.qortal.repository.DataException;
import org.qortal.repository.Repository; import org.qortal.repository.Repository;
import org.qortal.repository.RepositoryManager; import org.qortal.repository.RepositoryManager;
import org.qortal.settings.Settings; import org.qortal.settings.Settings;
import org.qortal.data.system.SystemInfo;
import org.qortal.utils.Base58; import org.qortal.utils.Base58;
import org.qortal.utils.NTP; import org.qortal.utils.NTP;
@ -54,7 +52,6 @@ import java.net.InetSocketAddress;
import java.net.UnknownHostException; import java.net.UnknownHostException;
import java.nio.file.Files; import java.nio.file.Files;
import java.nio.file.Paths; import java.nio.file.Paths;
import java.util.ArrayList;
import java.util.List; import java.util.List;
import java.util.concurrent.TimeUnit; import java.util.concurrent.TimeUnit;
import java.util.concurrent.TimeoutException; import java.util.concurrent.TimeoutException;
@ -1067,50 +1064,4 @@ public class AdminResource {
return "true"; 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);
}
}
} }

View File

@ -1,102 +0,0 @@
package org.qortal.api.websocket;
import org.apache.logging.log4j.LogManager;
import org.apache.logging.log4j.Logger;
import org.eclipse.jetty.websocket.api.Session;
import org.eclipse.jetty.websocket.api.WebSocketException;
import org.eclipse.jetty.websocket.api.annotations.OnWebSocketClose;
import org.eclipse.jetty.websocket.api.annotations.OnWebSocketConnect;
import org.eclipse.jetty.websocket.api.annotations.OnWebSocketError;
import org.eclipse.jetty.websocket.api.annotations.OnWebSocketMessage;
import org.eclipse.jetty.websocket.api.annotations.WebSocket;
import org.eclipse.jetty.websocket.servlet.WebSocketServletFactory;
import org.qortal.api.ApiError;
import org.qortal.controller.Controller;
import org.qortal.data.arbitrary.DataMonitorInfo;
import org.qortal.event.DataMonitorEvent;
import org.qortal.event.Event;
import org.qortal.event.EventBus;
import org.qortal.event.Listener;
import org.qortal.repository.DataException;
import org.qortal.repository.Repository;
import org.qortal.repository.RepositoryManager;
import org.qortal.utils.Base58;
import java.io.IOException;
import java.io.StringWriter;
import java.util.List;
@WebSocket
@SuppressWarnings("serial")
public class DataMonitorSocket extends ApiWebSocket implements Listener {
private static final Logger LOGGER = LogManager.getLogger(DataMonitorSocket.class);
@Override
public void configure(WebSocketServletFactory factory) {
LOGGER.info("configure");
factory.register(DataMonitorSocket.class);
EventBus.INSTANCE.addListener(this);
}
@Override
public void listen(Event event) {
if (!(event instanceof DataMonitorEvent))
return;
DataMonitorEvent dataMonitorEvent = (DataMonitorEvent) event;
for (Session session : getSessions())
sendDataEventSummary(session, buildInfo(dataMonitorEvent));
}
private DataMonitorInfo buildInfo(DataMonitorEvent dataMonitorEvent) {
return new DataMonitorInfo(
dataMonitorEvent.getTimestamp(),
dataMonitorEvent.getIdentifier(),
dataMonitorEvent.getName(),
dataMonitorEvent.getService(),
dataMonitorEvent.getDescription(),
dataMonitorEvent.getTransactionTimestamp(),
dataMonitorEvent.getLatestPutTimestamp()
);
}
@OnWebSocketConnect
@Override
public void onWebSocketConnect(Session session) {
super.onWebSocketConnect(session);
}
@OnWebSocketClose
@Override
public void onWebSocketClose(Session session, int statusCode, String reason) {
super.onWebSocketClose(session, statusCode, reason);
}
@OnWebSocketError
public void onWebSocketError(Session session, Throwable throwable) {
/* We ignore errors for now, but method here to silence log spam */
}
@OnWebSocketMessage
public void onWebSocketMessage(Session session, String message) {
LOGGER.info("onWebSocketMessage: message = " + message);
}
private void sendDataEventSummary(Session session, DataMonitorInfo dataMonitorInfo) {
StringWriter stringWriter = new StringWriter();
try {
marshall(stringWriter, dataMonitorInfo);
session.getRemote().sendStringByFuture(stringWriter.toString());
} catch (IOException | WebSocketException e) {
// No output this time
}
}
}

View File

@ -98,7 +98,7 @@ public class TradeOffersWebSocket extends ApiWebSocket implements Listener {
byte[] codeHash = acctInfo.getKey().value; byte[] codeHash = acctInfo.getKey().value;
ACCT acct = acctInfo.getValue().get(); ACCT acct = acctInfo.getValue().get();
List<ATStateData> atStates = repository.getATRepository().getMatchingFinalATStates(codeHash, null, null, List<ATStateData> atStates = repository.getATRepository().getMatchingFinalATStates(codeHash,
isFinished, dataByteOffset, expectedValue, minimumFinalHeight, isFinished, dataByteOffset, expectedValue, minimumFinalHeight,
null, null, null); null, null, null);
@ -259,7 +259,7 @@ public class TradeOffersWebSocket extends ApiWebSocket implements Listener {
ACCT acct = acctInfo.getValue().get(); ACCT acct = acctInfo.getValue().get();
Integer dataByteOffset = acct.getModeByteOffset(); Integer dataByteOffset = acct.getModeByteOffset();
List<ATStateData> initialAtStates = repository.getATRepository().getMatchingFinalATStates(codeHash, null, null, List<ATStateData> initialAtStates = repository.getATRepository().getMatchingFinalATStates(codeHash,
isFinished, dataByteOffset, expectedValue, minimumFinalHeight, isFinished, dataByteOffset, expectedValue, minimumFinalHeight,
null, null, null); null, null, null);
@ -298,7 +298,7 @@ public class TradeOffersWebSocket extends ApiWebSocket implements Listener {
byte[] codeHash = acctInfo.getKey().value; byte[] codeHash = acctInfo.getKey().value;
ACCT acct = acctInfo.getValue().get(); ACCT acct = acctInfo.getValue().get();
List<ATStateData> historicAtStates = repository.getATRepository().getMatchingFinalATStates(codeHash, null, null, List<ATStateData> historicAtStates = repository.getATRepository().getMatchingFinalATStates(codeHash,
isFinished, dataByteOffset, expectedValue, minimumFinalHeight, isFinished, dataByteOffset, expectedValue, minimumFinalHeight,
null, null, null); null, null, null);

View File

@ -5,10 +5,8 @@ import org.qortal.utils.Base58;
import java.io.IOException; import java.io.IOException;
import java.nio.charset.StandardCharsets; import java.nio.charset.StandardCharsets;
import java.nio.file.FileSystem;
import java.nio.file.Files; import java.nio.file.Files;
import java.nio.file.Path; import java.nio.file.Path;
import java.nio.file.Paths;
import java.security.MessageDigest; import java.security.MessageDigest;
import java.security.NoSuchAlgorithmException; import java.security.NoSuchAlgorithmException;
import java.util.Arrays; import java.util.Arrays;
@ -46,22 +44,12 @@ public class ArbitraryDataDigest {
continue; continue;
} }
// Account for \ VS / : Linux VS Windows
String pathString = relativePath.toString();
if(relativePath.getFileSystem().toString().contains("Windows")) {
pathString = pathString.replace("\\","/");
}
// Hash path // Hash path
byte[] filePathBytes = pathString.getBytes(StandardCharsets.UTF_8); byte[] filePathBytes = relativePath.toString().getBytes(StandardCharsets.UTF_8);
System.out.printf("Path: %s \n", pathString);
System.out.printf("Path Byte Array: %s \n", Arrays.toString(filePathBytes));
sha256.update(filePathBytes); sha256.update(filePathBytes);
// Hash contents // Hash contents
byte[] fileContent = Files.readAllBytes(path); byte[] fileContent = Files.readAllBytes(path);
System.out.printf("File Content: %s \n", Arrays.toString(fileContent));
sha256.update(fileContent); sha256.update(fileContent);
} }
this.hash = sha256.digest(); this.hash = sha256.digest();

View File

@ -439,15 +439,7 @@ public class ArbitraryDataReader {
// Ensure the complete hash matches the joined chunks // Ensure the complete hash matches the joined chunks
if (!Arrays.equals(arbitraryDataFile.digest(), transactionData.getData())) { if (!Arrays.equals(arbitraryDataFile.digest(), transactionData.getData())) {
// Delete the invalid file // Delete the invalid file
LOGGER.info("Deleting invalid file: path = " + arbitraryDataFile.getFilePath()); arbitraryDataFile.delete();
if( arbitraryDataFile.delete() ) {
LOGGER.info("Deleted invalid file successfully: path = " + arbitraryDataFile.getFilePath());
}
else {
LOGGER.warn("Could not delete invalid file: path = " + arbitraryDataFile.getFilePath());
}
throw new DataException("Unable to validate complete file hash"); throw new DataException("Unable to validate complete file hash");
} }
} }

View File

@ -37,7 +37,7 @@ public enum Service {
if (files != null && files[0] != null) { if (files != null && files[0] != null) {
final String extension = FilenameUtils.getExtension(files[0].getName()).toLowerCase(); final String extension = FilenameUtils.getExtension(files[0].getName()).toLowerCase();
// We must allow blank file extensions because these are used by data published from a plaintext or base64-encoded string // We must allow blank file extensions because these are used by data published from a plaintext or base64-encoded string
final List<String> allowedExtensions = Arrays.asList("qortal", "zip", "pdf", "txt", "odt", "ods", "doc", "docx", "xls", "xlsx", "ppt", "pptx", ""); final List<String> allowedExtensions = Arrays.asList("zip", "pdf", "txt", "odt", "ods", "doc", "docx", "xls", "xlsx", "ppt", "pptx", "");
if (extension == null || !allowedExtensions.contains(extension)) { if (extension == null || !allowedExtensions.contains(extension)) {
return ValidationResult.INVALID_FILE_EXTENSION; return ValidationResult.INVALID_FILE_EXTENSION;
} }

View File

@ -23,10 +23,8 @@ import org.qortal.data.at.ATStateData;
import org.qortal.data.block.BlockData; import org.qortal.data.block.BlockData;
import org.qortal.data.block.BlockSummaryData; import org.qortal.data.block.BlockSummaryData;
import org.qortal.data.block.BlockTransactionData; import org.qortal.data.block.BlockTransactionData;
import org.qortal.data.group.GroupAdminData;
import org.qortal.data.network.OnlineAccountData; import org.qortal.data.network.OnlineAccountData;
import org.qortal.data.transaction.TransactionData; import org.qortal.data.transaction.TransactionData;
import org.qortal.group.Group;
import org.qortal.repository.*; import org.qortal.repository.*;
import org.qortal.settings.Settings; import org.qortal.settings.Settings;
import org.qortal.transaction.AtTransaction; import org.qortal.transaction.AtTransaction;
@ -39,7 +37,6 @@ import org.qortal.transform.block.BlockTransformer;
import org.qortal.transform.transaction.TransactionTransformer; import org.qortal.transform.transaction.TransactionTransformer;
import org.qortal.utils.Amounts; import org.qortal.utils.Amounts;
import org.qortal.utils.Base58; import org.qortal.utils.Base58;
import org.qortal.utils.Groups;
import org.qortal.utils.NTP; import org.qortal.utils.NTP;
import java.io.ByteArrayOutputStream; import java.io.ByteArrayOutputStream;
@ -151,7 +148,7 @@ public class Block {
final BlockChain blockChain = BlockChain.getInstance(); final BlockChain blockChain = BlockChain.getInstance();
ExpandedAccount(Repository repository, RewardShareData rewardShareData, int blockHeight) throws DataException { ExpandedAccount(Repository repository, RewardShareData rewardShareData) throws DataException {
this.rewardShareData = rewardShareData; this.rewardShareData = rewardShareData;
this.sharePercent = this.rewardShareData.getSharePercent(); this.sharePercent = this.rewardShareData.getSharePercent();
@ -160,12 +157,7 @@ public class Block {
this.isMinterFounder = Account.isFounder(mintingAccountData.getFlags()); this.isMinterFounder = Account.isFounder(mintingAccountData.getFlags());
this.isRecipientAlsoMinter = this.rewardShareData.getRecipient().equals(this.mintingAccount.getAddress()); this.isRecipientAlsoMinter = this.rewardShareData.getRecipient().equals(this.mintingAccount.getAddress());
this.isMinterMember this.isMinterMember = repository.getGroupRepository().memberExists(BlockChain.getInstance().getMintingGroupId(), this.mintingAccount.getAddress());
= Groups.memberExistsInAnyGroup(
repository.getGroupRepository(),
Groups.getGroupIdsToMint(BlockChain.getInstance(), blockHeight),
this.mintingAccount.getAddress()
);
if (this.isRecipientAlsoMinter) { if (this.isRecipientAlsoMinter) {
// Self-share: minter is also recipient // Self-share: minter is also recipient
@ -178,19 +170,6 @@ 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() { public Account getMintingAccount() {
return this.mintingAccount; return this.mintingAccount;
} }
@ -207,7 +186,7 @@ public class Block {
* @return account-level share "bin" from blockchain config, or null if founder / none found * @return account-level share "bin" from blockchain config, or null if founder / none found
*/ */
public AccountLevelShareBin getShareBin(int blockHeight) { public AccountLevelShareBin getShareBin(int blockHeight) {
if (this.isMinterFounder && blockHeight < BlockChain.getInstance().getAdminsReplaceFoundersHeight()) if (this.isMinterFounder)
return null; return null;
final int accountLevel = this.mintingAccountData.getLevel(); final int accountLevel = this.mintingAccountData.getLevel();
@ -424,9 +403,7 @@ public class Block {
onlineAccounts.removeIf(a -> a.getNonce() == null || a.getNonce() < 0); onlineAccounts.removeIf(a -> a.getNonce() == null || a.getNonce() < 0);
// After feature trigger, remove any online accounts that are level 0 // After feature trigger, remove any online accounts that are level 0
// but only if they are before the ignore level feature trigger if (height >= BlockChain.getInstance().getOnlineAccountMinterLevelValidationHeight()) {
if (height < BlockChain.getInstance().getIgnoreLevelForRewardShareHeight() &&
height >= BlockChain.getInstance().getOnlineAccountMinterLevelValidationHeight()) {
onlineAccounts.removeIf(a -> { onlineAccounts.removeIf(a -> {
try { try {
return Account.getRewardShareEffectiveMintingLevel(repository, a.getPublicKey()) == 0; return Account.getRewardShareEffectiveMintingLevel(repository, a.getPublicKey()) == 0;
@ -441,9 +418,9 @@ public class Block {
if (height >= BlockChain.getInstance().getGroupMemberCheckHeight()) { if (height >= BlockChain.getInstance().getGroupMemberCheckHeight()) {
onlineAccounts.removeIf(a -> { onlineAccounts.removeIf(a -> {
try { try {
List<Integer> groupIdsToMint = Groups.getGroupIdsToMint(BlockChain.getInstance(), height); int groupId = BlockChain.getInstance().getMintingGroupId();
String address = Account.getRewardShareMintingAddress(repository, a.getPublicKey()); String address = Account.getRewardShareMintingAddress(repository, a.getPublicKey());
boolean isMinterGroupMember = Groups.memberExistsInAnyGroup(repository.getGroupRepository(), groupIdsToMint, address); boolean isMinterGroupMember = repository.getGroupRepository().memberExists(groupId, address);
return !isMinterGroupMember; return !isMinterGroupMember;
} catch (DataException e) { } catch (DataException e) {
// Something went wrong, so remove the account // Something went wrong, so remove the account
@ -759,7 +736,15 @@ public class Block {
List<ExpandedAccount> expandedAccounts = new ArrayList<>(); List<ExpandedAccount> expandedAccounts = new ArrayList<>();
for (RewardShareData rewardShare : this.cachedOnlineRewardShares) { for (RewardShareData rewardShare : this.cachedOnlineRewardShares) {
expandedAccounts.add(new ExpandedAccount(repository, rewardShare, this.blockData.getHeight())); 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));
} }
this.cachedExpandedAccounts = expandedAccounts; this.cachedExpandedAccounts = expandedAccounts;
@ -1169,32 +1154,23 @@ public class Block {
if (onlineRewardShares == null) if (onlineRewardShares == null)
return ValidationResult.ONLINE_ACCOUNT_UNKNOWN; return ValidationResult.ONLINE_ACCOUNT_UNKNOWN;
// After feature trigger, require all online account minters to be greater than level 0, // 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.getBlockData().getHeight() >= BlockChain.getInstance().getOnlineAccountMinterLevelValidationHeight()) {
if (this.blockData.getHeight() < BlockChain.getInstance().getIgnoreLevelForRewardShareHeight() && List<ExpandedAccount> expandedAccounts = this.getExpandedAccounts();
this.getBlockData().getHeight() >= BlockChain.getInstance().getOnlineAccountMinterLevelValidationHeight()) {
List<ExpandedAccount> expandedAccounts
= this.getExpandedAccounts().stream()
.filter(expandedAccount -> expandedAccount.isMinterMember)
.collect(Collectors.toList());
for (ExpandedAccount account : expandedAccounts) { for (ExpandedAccount account : expandedAccounts) {
int groupId = BlockChain.getInstance().getMintingGroupId();
String address = account.getMintingAccount().getAddress();
boolean isMinterGroupMember = repository.getGroupRepository().memberExists(groupId, address);
if (account.getMintingAccount().getEffectiveMintingLevel() == 0) if (account.getMintingAccount().getEffectiveMintingLevel() == 0)
return ValidationResult.ONLINE_ACCOUNTS_INVALID; return ValidationResult.ONLINE_ACCOUNTS_INVALID;
if (this.getBlockData().getHeight() >= BlockChain.getInstance().getFixBatchRewardHeight()) { if (this.getBlockData().getHeight() >= BlockChain.getInstance().getFixBatchRewardHeight()) {
if (!account.isMinterMember) if (!isMinterGroupMember)
return ValidationResult.ONLINE_ACCOUNTS_INVALID; 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 // If block is past a certain age then we simply assume the signatures were correct
long signatureRequirementThreshold = NTP.getTime() - BlockChain.getInstance().getOnlineAccountSignaturesMinLifetime(); long signatureRequirementThreshold = NTP.getTime() - BlockChain.getInstance().getOnlineAccountSignaturesMinLifetime();
@ -1683,17 +1659,7 @@ public class Block {
final List<Integer> cumulativeBlocksByLevel = BlockChain.getInstance().getCumulativeBlocksByLevel(); final List<Integer> cumulativeBlocksByLevel = BlockChain.getInstance().getCumulativeBlocksByLevel();
final int maximumLevel = cumulativeBlocksByLevel.size() - 1; final int maximumLevel = cumulativeBlocksByLevel.size() - 1;
final List<ExpandedAccount> expandedAccounts; final List<ExpandedAccount> expandedAccounts = this.getExpandedAccounts();
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<>(); Set<AccountData> allUniqueExpandedAccounts = new HashSet<>();
for (ExpandedAccount expandedAccount : expandedAccounts) { for (ExpandedAccount expandedAccount : expandedAccounts) {
@ -2093,17 +2059,7 @@ public class Block {
final List<Integer> cumulativeBlocksByLevel = BlockChain.getInstance().getCumulativeBlocksByLevel(); final List<Integer> cumulativeBlocksByLevel = BlockChain.getInstance().getCumulativeBlocksByLevel();
final int maximumLevel = cumulativeBlocksByLevel.size() - 1; final int maximumLevel = cumulativeBlocksByLevel.size() - 1;
final List<ExpandedAccount> expandedAccounts; final List<ExpandedAccount> expandedAccounts = this.getExpandedAccounts();
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<>(); Set<AccountData> allUniqueExpandedAccounts = new HashSet<>();
for (ExpandedAccount expandedAccount : expandedAccounts) { for (ExpandedAccount expandedAccount : expandedAccounts) {
@ -2307,17 +2263,7 @@ public class Block {
List<BlockRewardCandidate> rewardCandidates = new ArrayList<>(); List<BlockRewardCandidate> rewardCandidates = new ArrayList<>();
// All online accounts // All online accounts
final List<ExpandedAccount> expandedAccounts; final List<ExpandedAccount> expandedAccounts = this.getExpandedAccounts();
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: * Distribution rules:
@ -2442,7 +2388,7 @@ public class Block {
final long qoraHoldersShare = BlockChain.getInstance().getQoraHoldersShareAtHeight(this.blockData.getHeight()); final long qoraHoldersShare = BlockChain.getInstance().getQoraHoldersShareAtHeight(this.blockData.getHeight());
// Perform account-level-based reward scaling if appropriate // Perform account-level-based reward scaling if appropriate
if (!haveFounders && this.blockData.getHeight() < BlockChain.getInstance().getAdminsReplaceFoundersHeight() ) { if (!haveFounders) {
// Recalculate distribution ratios based on candidates // Recalculate distribution ratios based on candidates
// Nothing shared? This shouldn't happen // Nothing shared? This shouldn't happen
@ -2478,103 +2424,18 @@ public class Block {
} }
// Add founders as reward candidate if appropriate // Add founders as reward candidate if appropriate
if (haveFounders && this.blockData.getHeight() < BlockChain.getInstance().getAdminsReplaceFoundersHeight()) { if (haveFounders) {
// Yes: add to reward candidates list // Yes: add to reward candidates list
BlockRewardDistributor founderDistributor = (distributionAmount, balanceChanges) -> distributeBlockRewardShare(distributionAmount, onlineFounderAccounts, balanceChanges); BlockRewardDistributor founderDistributor = (distributionAmount, balanceChanges) -> distributeBlockRewardShare(distributionAmount, onlineFounderAccounts, balanceChanges);
final long foundersShare = 1_00000000 - totalShares; final long foundersShare = 1_00000000 - totalShares;
BlockRewardCandidate rewardCandidate = new BlockRewardCandidate("Founders", foundersShare, founderDistributor); BlockRewardCandidate rewardCandidate = new BlockRewardCandidate("Founders", foundersShare, founderDistributor);
rewardCandidates.add(rewardCandidate); 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();
List<Integer> mintingGroupIds = Groups.getGroupIdsToMint(BlockChain.getInstance(), this.blockData.getHeight());
// all minter admins
List<String> minterAdmins = Groups.getAllAdmins(groupRepository, mintingGroupIds);
// 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; 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) { private static long distributeBlockRewardShare(long distributionAmount, List<ExpandedAccount> accounts, Map<String, Long> balanceChanges) {
// Collate all expanded accounts by minting account // Collate all expanded accounts by minting account
Map<String, List<ExpandedAccount>> accountsByMinter = new HashMap<>(); Map<String, List<ExpandedAccount>> accountsByMinter = new HashMap<>();

View File

@ -88,11 +88,7 @@ public class BlockChain {
onlyMintWithNameHeight, onlyMintWithNameHeight,
removeOnlyMintWithNameHeight, removeOnlyMintWithNameHeight,
groupMemberCheckHeight, groupMemberCheckHeight,
fixBatchRewardHeight, fixBatchRewardHeight
adminsReplaceFoundersHeight,
nullGroupMembershipHeight,
ignoreLevelForRewardShareHeight,
adminQueryFixHeight
} }
// Custom transaction fees // Custom transaction fees
@ -212,13 +208,7 @@ public class BlockChain {
private int minAccountLevelToRewardShare; private int minAccountLevelToRewardShare;
private int maxRewardSharesPerFounderMintingAccount; private int maxRewardSharesPerFounderMintingAccount;
private int founderEffectiveMintingLevel; private int founderEffectiveMintingLevel;
private int mintingGroupId;
public static class IdsForHeight {
public int height;
public List<Integer> ids;
}
private List<IdsForHeight> mintingGroupIds;
/** Minimum time to retain online account signatures (ms) for block validity checks. */ /** Minimum time to retain online account signatures (ms) for block validity checks. */
private long onlineAccountSignaturesMinLifetime; private long onlineAccountSignaturesMinLifetime;
@ -550,8 +540,8 @@ public class BlockChain {
return this.onlineAccountSignaturesMaxLifetime; return this.onlineAccountSignaturesMaxLifetime;
} }
public List<IdsForHeight> getMintingGroupIds() { public int getMintingGroupId() {
return mintingGroupIds; return this.mintingGroupId;
} }
public CiyamAtSettings getCiyamAtSettings() { public CiyamAtSettings getCiyamAtSettings() {
@ -672,22 +662,6 @@ public class BlockChain {
return this.featureTriggers.get(FeatureTrigger.fixBatchRewardHeight.name()).intValue(); 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 // More complex getters for aspects that change by height or timestamp
public long getRewardAtHeight(int ourHeight) { public long getRewardAtHeight(int ourHeight) {

View File

@ -423,12 +423,6 @@ public class Controller extends Thread {
LOGGER.info("Db Cache Disabled"); LOGGER.info("Db Cache Disabled");
} }
LOGGER.info("Arbitrary Indexing Starting ...");
ArbitraryIndexUtils.startCaching(
Settings.getInstance().getArbitraryIndexingPriority(),
Settings.getInstance().getArbitraryIndexingFrequency()
);
if( Settings.getInstance().isBalanceRecorderEnabled() ) { if( Settings.getInstance().isBalanceRecorderEnabled() ) {
Optional<HSQLDBBalanceRecorder> recorder = HSQLDBBalanceRecorder.getInstance(); Optional<HSQLDBBalanceRecorder> recorder = HSQLDBBalanceRecorder.getInstance();
@ -547,16 +541,6 @@ public class Controller extends Thread {
ArbitraryDataStorageManager.getInstance().start(); ArbitraryDataStorageManager.getInstance().start();
ArbitraryDataRenderManager.getInstance().start(); ArbitraryDataRenderManager.getInstance().start();
// start rebuild arbitrary resource cache timer task
if( Settings.getInstance().isRebuildArbitraryResourceCacheTaskEnabled() ) {
new Timer().schedule(
new RebuildArbitraryResourceCacheTask(),
Settings.getInstance().getRebuildArbitraryResourceCacheTaskDelay() * RebuildArbitraryResourceCacheTask.MILLIS_IN_MINUTE,
Settings.getInstance().getRebuildArbitraryResourceCacheTaskPeriod() * RebuildArbitraryResourceCacheTask.MILLIS_IN_HOUR
);
}
LOGGER.info("Starting online accounts manager"); LOGGER.info("Starting online accounts manager");
OnlineAccountsManager.getInstance().start(); OnlineAccountsManager.getInstance().start();

View File

@ -25,7 +25,6 @@ import org.qortal.repository.Repository;
import org.qortal.repository.RepositoryManager; import org.qortal.repository.RepositoryManager;
import org.qortal.settings.Settings; import org.qortal.settings.Settings;
import org.qortal.utils.Base58; import org.qortal.utils.Base58;
import org.qortal.utils.Groups;
import org.qortal.utils.NTP; import org.qortal.utils.NTP;
import org.qortal.utils.NamedThreadFactory; import org.qortal.utils.NamedThreadFactory;
@ -226,14 +225,11 @@ public class OnlineAccountsManager {
Set<OnlineAccountData> onlineAccountsToAdd = new HashSet<>(); Set<OnlineAccountData> onlineAccountsToAdd = new HashSet<>();
Set<OnlineAccountData> onlineAccountsToRemove = new HashSet<>(); Set<OnlineAccountData> onlineAccountsToRemove = new HashSet<>();
try (final Repository repository = RepositoryManager.getRepository()) { try (final Repository repository = RepositoryManager.getRepository()) {
int blockHeight = repository.getBlockRepository().getBlockchainHeight();
List<String> mintingGroupMemberAddresses List<String> mintingGroupMemberAddresses
= Groups.getAllMembers( = repository.getGroupRepository()
repository.getGroupRepository(), .getGroupMembers(BlockChain.getInstance().getMintingGroupId()).stream()
Groups.getGroupIdsToMint(BlockChain.getInstance(), blockHeight) .map(GroupMemberData::getMember)
); .collect(Collectors.toList());
for (OnlineAccountData onlineAccountData : this.onlineAccountsImportQueue) { for (OnlineAccountData onlineAccountData : this.onlineAccountsImportQueue) {
if (isStopping) if (isStopping)

View File

@ -2,30 +2,22 @@ package org.qortal.controller.arbitrary;
import org.apache.logging.log4j.LogManager; import org.apache.logging.log4j.LogManager;
import org.apache.logging.log4j.Logger; import org.apache.logging.log4j.Logger;
import org.qortal.api.resource.TransactionsResource;
import org.qortal.controller.Controller; import org.qortal.controller.Controller;
import org.qortal.data.arbitrary.ArbitraryResourceData; import org.qortal.data.arbitrary.ArbitraryResourceData;
import org.qortal.data.transaction.ArbitraryTransactionData; import org.qortal.data.transaction.ArbitraryTransactionData;
import org.qortal.event.DataMonitorEvent;
import org.qortal.event.EventBus;
import org.qortal.gui.SplashFrame; import org.qortal.gui.SplashFrame;
import org.qortal.repository.DataException; import org.qortal.repository.DataException;
import org.qortal.repository.Repository; import org.qortal.repository.Repository;
import org.qortal.repository.RepositoryManager; import org.qortal.repository.RepositoryManager;
import org.qortal.settings.Settings; import org.qortal.settings.Settings;
import org.qortal.transaction.ArbitraryTransaction; import org.qortal.transaction.ArbitraryTransaction;
import org.qortal.transaction.Transaction;
import org.qortal.utils.Base58; import org.qortal.utils.Base58;
import java.text.NumberFormat;
import java.util.ArrayList; import java.util.ArrayList;
import java.util.Collections; import java.util.Collections;
import java.util.HashMap;
import java.util.HashSet;
import java.util.List; import java.util.List;
import java.util.Map;
import java.util.Optional;
import java.util.Set;
import java.util.function.Function;
import java.util.stream.Collectors;
public class ArbitraryDataCacheManager extends Thread { public class ArbitraryDataCacheManager extends Thread {
@ -37,11 +29,6 @@ public class ArbitraryDataCacheManager extends Thread {
/** Queue of arbitrary transactions that require cache updates */ /** Queue of arbitrary transactions that require cache updates */
private final List<ArbitraryTransactionData> updateQueue = Collections.synchronizedList(new ArrayList<>()); private final List<ArbitraryTransactionData> updateQueue = Collections.synchronizedList(new ArrayList<>());
private static final NumberFormat FORMATTER = NumberFormat.getNumberInstance();
static {
FORMATTER.setGroupingUsed(true);
}
public static synchronized ArbitraryDataCacheManager getInstance() { public static synchronized ArbitraryDataCacheManager getInstance() {
if (instance == null) { if (instance == null) {
@ -58,22 +45,17 @@ public class ArbitraryDataCacheManager extends Thread {
try { try {
while (!Controller.isStopping()) { while (!Controller.isStopping()) {
try { Thread.sleep(500L);
Thread.sleep(500L);
// Process queue // Process queue
processResourceQueue(); processResourceQueue();
} catch (Exception e) {
LOGGER.error(e.getMessage(), e);
Thread.sleep(600_000L); // wait 10 minutes to continue
}
} }
} catch (InterruptedException e) {
// Clear queue before terminating thread // Fall through to exit thread
processResourceQueue();
} catch (Exception e) {
LOGGER.error(e.getMessage(), e);
} }
// Clear queue before terminating thread
processResourceQueue();
} }
public void shutdown() { public void shutdown() {
@ -103,25 +85,14 @@ public class ArbitraryDataCacheManager extends Thread {
// Update arbitrary resource caches // Update arbitrary resource caches
try { try {
ArbitraryTransaction arbitraryTransaction = new ArbitraryTransaction(repository, transactionData); ArbitraryTransaction arbitraryTransaction = new ArbitraryTransaction(repository, transactionData);
arbitraryTransaction.updateArbitraryResourceCacheIncludingMetadata(repository, new HashSet<>(0), new HashMap<>(0)); arbitraryTransaction.updateArbitraryResourceCache(repository);
arbitraryTransaction.updateArbitraryMetadataCache(repository);
repository.saveChanges(); repository.saveChanges();
// Update status as separate commit, as this is more prone to failure // Update status as separate commit, as this is more prone to failure
arbitraryTransaction.updateArbitraryResourceStatus(repository); arbitraryTransaction.updateArbitraryResourceStatus(repository);
repository.saveChanges(); repository.saveChanges();
EventBus.INSTANCE.notify(
new DataMonitorEvent(
System.currentTimeMillis(),
transactionData.getIdentifier(),
transactionData.getName(),
transactionData.getService().name(),
"updated resource cache and status, queue",
transactionData.getTimestamp(),
transactionData.getTimestamp()
)
);
LOGGER.debug(() -> String.format("Finished processing transaction %.8s in arbitrary resource queue...", Base58.encode(transactionData.getSignature()))); LOGGER.debug(() -> String.format("Finished processing transaction %.8s in arbitrary resource queue...", Base58.encode(transactionData.getSignature())));
} catch (DataException e) { } catch (DataException e) {
@ -132,9 +103,6 @@ public class ArbitraryDataCacheManager extends Thread {
} catch (DataException e) { } catch (DataException e) {
LOGGER.error("Repository issue while processing arbitrary resource cache updates", e); LOGGER.error("Repository issue while processing arbitrary resource cache updates", e);
} }
catch (Exception e) {
LOGGER.error(e.getMessage(), e);
}
} }
public void addToUpdateQueue(ArbitraryTransactionData transactionData) { public void addToUpdateQueue(ArbitraryTransactionData transactionData) {
@ -180,66 +148,34 @@ public class ArbitraryDataCacheManager extends Thread {
LOGGER.info("Building arbitrary resources cache..."); LOGGER.info("Building arbitrary resources cache...");
SplashFrame.getInstance().updateStatus("Building QDN cache - please wait..."); SplashFrame.getInstance().updateStatus("Building QDN cache - please wait...");
final int batchSize = Settings.getInstance().getBuildArbitraryResourcesBatchSize(); final int batchSize = 100;
int offset = 0; int offset = 0;
List<ArbitraryTransactionData> allArbitraryTransactionsInDescendingOrder
= repository.getArbitraryRepository().getLatestArbitraryTransactions();
LOGGER.info("arbitrary transactions: count = " + allArbitraryTransactionsInDescendingOrder.size());
List<ArbitraryResourceData> resources = repository.getArbitraryRepository().getArbitraryResources(null, null, true);
Map<ArbitraryTransactionDataHashWrapper, ArbitraryResourceData> resourceByWrapper = new HashMap<>(resources.size());
for( ArbitraryResourceData resource : resources ) {
resourceByWrapper.put(
new ArbitraryTransactionDataHashWrapper(resource.service.value, resource.name, resource.identifier),
resource
);
}
LOGGER.info("arbitrary resources: count = " + resourceByWrapper.size());
Set<ArbitraryTransactionDataHashWrapper> latestTransactionsWrapped = new HashSet<>(allArbitraryTransactionsInDescendingOrder.size());
// Loop through all ARBITRARY transactions, and determine latest state // Loop through all ARBITRARY transactions, and determine latest state
while (!Controller.isStopping()) { while (!Controller.isStopping()) {
LOGGER.info( LOGGER.info("Fetching arbitrary transactions {} - {}", offset, offset+batchSize-1);
"Fetching arbitrary transactions {} - {} / {} Total",
FORMATTER.format(offset),
FORMATTER.format(offset+batchSize-1),
FORMATTER.format(allArbitraryTransactionsInDescendingOrder.size())
);
List<ArbitraryTransactionData> transactionsToProcess List<byte[]> signatures = repository.getTransactionRepository().getSignaturesMatchingCriteria(null, null, null, List.of(Transaction.TransactionType.ARBITRARY), null, null, null, TransactionsResource.ConfirmationStatus.BOTH, batchSize, offset, false);
= allArbitraryTransactionsInDescendingOrder.stream() if (signatures.isEmpty()) {
.skip(offset)
.limit(batchSize)
.collect(Collectors.toList());
if (transactionsToProcess.isEmpty()) {
// Complete // Complete
break; break;
} }
try { // Expand signatures to transactions
for( ArbitraryTransactionData transactionData : transactionsToProcess) { for (byte[] signature : signatures) {
if (transactionData.getService() == null) { ArbitraryTransactionData transactionData = (ArbitraryTransactionData) repository
// Unsupported service - ignore this resource .getTransactionRepository().fromSignature(signature);
continue;
}
latestTransactionsWrapped.add(new ArbitraryTransactionDataHashWrapper(transactionData)); if (transactionData.getService() == null) {
// Unsupported service - ignore this resource
// Update arbitrary resource caches continue;
ArbitraryTransaction arbitraryTransaction = new ArbitraryTransaction(repository, transactionData);
arbitraryTransaction.updateArbitraryResourceCacheIncludingMetadata(repository, latestTransactionsWrapped, resourceByWrapper);
} }
repository.saveChanges();
} catch (DataException e) {
repository.discardChanges();
LOGGER.error(e.getMessage(), e); // Update arbitrary resource caches
ArbitraryTransaction arbitraryTransaction = new ArbitraryTransaction(repository, transactionData);
arbitraryTransaction.updateArbitraryResourceCache(repository);
arbitraryTransaction.updateArbitraryMetadataCache(repository);
repository.saveChanges();
} }
offset += batchSize; offset += batchSize;
} }
@ -257,11 +193,6 @@ public class ArbitraryDataCacheManager extends Thread {
repository.discardChanges(); repository.discardChanges();
throw new DataException("Build of arbitrary resources cache failed."); throw new DataException("Build of arbitrary resources cache failed.");
} }
catch (Exception e) {
LOGGER.error(e.getMessage(), e);
return false;
}
} }
private boolean refreshArbitraryStatuses(Repository repository) throws DataException { private boolean refreshArbitraryStatuses(Repository repository) throws DataException {
@ -269,48 +200,27 @@ public class ArbitraryDataCacheManager extends Thread {
LOGGER.info("Refreshing arbitrary resource statuses for locally hosted transactions..."); LOGGER.info("Refreshing arbitrary resource statuses for locally hosted transactions...");
SplashFrame.getInstance().updateStatus("Refreshing statuses - please wait..."); SplashFrame.getInstance().updateStatus("Refreshing statuses - please wait...");
final int batchSize = Settings.getInstance().getBuildArbitraryResourcesBatchSize(); final int batchSize = 100;
int offset = 0; int offset = 0;
List<ArbitraryTransactionData> allHostedTransactions
= ArbitraryDataStorageManager.getInstance()
.listAllHostedTransactions(repository, null, null);
// Loop through all ARBITRARY transactions, and determine latest state // Loop through all ARBITRARY transactions, and determine latest state
while (!Controller.isStopping()) { while (!Controller.isStopping()) {
LOGGER.info( LOGGER.info("Fetching hosted transactions {} - {}", offset, offset+batchSize-1);
"Fetching hosted transactions {} - {} / {} Total",
FORMATTER.format(offset),
FORMATTER.format(offset+batchSize-1),
FORMATTER.format(allHostedTransactions.size())
);
List<ArbitraryTransactionData> hostedTransactions
= allHostedTransactions.stream()
.skip(offset)
.limit(batchSize)
.collect(Collectors.toList());
List<ArbitraryTransactionData> hostedTransactions = ArbitraryDataStorageManager.getInstance().listAllHostedTransactions(repository, batchSize, offset);
if (hostedTransactions.isEmpty()) { if (hostedTransactions.isEmpty()) {
// Complete // Complete
break; break;
} }
try { // Loop through hosted transactions
// Loop through hosted transactions for (ArbitraryTransactionData transactionData : hostedTransactions) {
for (ArbitraryTransactionData transactionData : hostedTransactions) {
// Determine status and update cache // Determine status and update cache
ArbitraryTransaction arbitraryTransaction = new ArbitraryTransaction(repository, transactionData); ArbitraryTransaction arbitraryTransaction = new ArbitraryTransaction(repository, transactionData);
arbitraryTransaction.updateArbitraryResourceStatus(repository); arbitraryTransaction.updateArbitraryResourceStatus(repository);
}
repository.saveChanges(); repository.saveChanges();
} catch (DataException e) {
repository.discardChanges();
LOGGER.error(e.getMessage(), e);
} }
offset += batchSize; offset += batchSize;
} }
@ -324,11 +234,6 @@ public class ArbitraryDataCacheManager extends Thread {
repository.discardChanges(); repository.discardChanges();
throw new DataException("Refresh of arbitrary resource statuses failed."); throw new DataException("Refresh of arbitrary resource statuses failed.");
} }
catch (Exception e) {
LOGGER.error(e.getMessage(), e);
return false;
}
} }
} }

View File

@ -2,10 +2,9 @@ package org.qortal.controller.arbitrary;
import org.apache.logging.log4j.LogManager; import org.apache.logging.log4j.LogManager;
import org.apache.logging.log4j.Logger; import org.apache.logging.log4j.Logger;
import org.qortal.api.resource.TransactionsResource.ConfirmationStatus;
import org.qortal.data.transaction.ArbitraryTransactionData; import org.qortal.data.transaction.ArbitraryTransactionData;
import org.qortal.data.transaction.TransactionData; import org.qortal.data.transaction.TransactionData;
import org.qortal.event.DataMonitorEvent;
import org.qortal.event.EventBus;
import org.qortal.repository.DataException; import org.qortal.repository.DataException;
import org.qortal.repository.Repository; import org.qortal.repository.Repository;
import org.qortal.repository.RepositoryManager; import org.qortal.repository.RepositoryManager;
@ -22,12 +21,8 @@ import java.nio.file.Paths;
import java.security.SecureRandom; import java.security.SecureRandom;
import java.util.ArrayList; import java.util.ArrayList;
import java.util.Arrays; import java.util.Arrays;
import java.util.HashSet;
import java.util.List; import java.util.List;
import java.util.Objects; import java.util.Objects;
import java.util.Optional;
import java.util.Set;
import java.util.stream.Collectors;
import static org.qortal.controller.arbitrary.ArbitraryDataStorageManager.DELETION_THRESHOLD; import static org.qortal.controller.arbitrary.ArbitraryDataStorageManager.DELETION_THRESHOLD;
@ -82,19 +77,6 @@ public class ArbitraryDataCleanupManager extends Thread {
final int limit = 100; final int limit = 100;
int offset = 0; int offset = 0;
List<ArbitraryTransactionData> allArbitraryTransactionsInDescendingOrder;
try (final Repository repository = RepositoryManager.getRepository()) {
allArbitraryTransactionsInDescendingOrder
= repository.getArbitraryRepository()
.getLatestArbitraryTransactions();
} catch( Exception e) {
LOGGER.error(e.getMessage(), e);
allArbitraryTransactionsInDescendingOrder = new ArrayList<>(0);
}
Set<ArbitraryTransactionData> processedTransactions = new HashSet<>();
try { try {
while (!isStopping) { while (!isStopping) {
Thread.sleep(30000); Thread.sleep(30000);
@ -125,31 +107,27 @@ public class ArbitraryDataCleanupManager extends Thread {
// Any arbitrary transactions we want to fetch data for? // Any arbitrary transactions we want to fetch data for?
try (final Repository repository = RepositoryManager.getRepository()) { try (final Repository repository = RepositoryManager.getRepository()) {
List<ArbitraryTransactionData> transactions = allArbitraryTransactionsInDescendingOrder.stream().skip(offset).limit(limit).collect(Collectors.toList()); List<byte[]> signatures = repository.getTransactionRepository().getSignaturesMatchingCriteria(null, null, null, ARBITRARY_TX_TYPE, null, null, null, ConfirmationStatus.BOTH, limit, offset, true);
// LOGGER.info("Found {} arbitrary transactions at offset: {}, limit: {}", signatures.size(), offset, limit);
if (isStopping) { if (isStopping) {
return; return;
} }
if (transactions == null || transactions.isEmpty()) { if (signatures == null || signatures.isEmpty()) {
offset = 0; offset = 0;
allArbitraryTransactionsInDescendingOrder continue;
= repository.getArbitraryRepository()
.getLatestArbitraryTransactions();
transactions = allArbitraryTransactionsInDescendingOrder.stream().limit(limit).collect(Collectors.toList());
processedTransactions.clear();
} }
offset += limit; offset += limit;
now = NTP.getTime(); now = NTP.getTime();
// Loop through the signatures in this batch // Loop through the signatures in this batch
for (int i=0; i<transactions.size(); i++) { for (int i=0; i<signatures.size(); i++) {
if (isStopping) { if (isStopping) {
return; return;
} }
ArbitraryTransactionData arbitraryTransactionData = transactions.get(i); byte[] signature = signatures.get(i);
if (arbitraryTransactionData == null) { if (signature == null) {
continue; continue;
} }
@ -158,7 +136,9 @@ public class ArbitraryDataCleanupManager extends Thread {
Thread.sleep(5000); Thread.sleep(5000);
} }
if (arbitraryTransactionData.getService() == null) { // Fetch the transaction data
ArbitraryTransactionData arbitraryTransactionData = ArbitraryTransactionUtils.fetchTransactionData(repository, signature);
if (arbitraryTransactionData == null || arbitraryTransactionData.getService() == null) {
continue; continue;
} }
@ -167,8 +147,6 @@ public class ArbitraryDataCleanupManager extends Thread {
continue; continue;
} }
boolean mostRecentTransaction = processedTransactions.add(arbitraryTransactionData);
// Check if we have the complete file // Check if we have the complete file
boolean completeFileExists = ArbitraryTransactionUtils.completeFileExists(arbitraryTransactionData); boolean completeFileExists = ArbitraryTransactionUtils.completeFileExists(arbitraryTransactionData);
@ -189,54 +167,20 @@ public class ArbitraryDataCleanupManager extends Thread {
LOGGER.info("Deleting transaction {} because we can't host its data", LOGGER.info("Deleting transaction {} because we can't host its data",
Base58.encode(arbitraryTransactionData.getSignature())); Base58.encode(arbitraryTransactionData.getSignature()));
ArbitraryTransactionUtils.deleteCompleteFileAndChunks(arbitraryTransactionData); ArbitraryTransactionUtils.deleteCompleteFileAndChunks(arbitraryTransactionData);
EventBus.INSTANCE.notify(
new DataMonitorEvent(
System.currentTimeMillis(),
arbitraryTransactionData.getIdentifier(),
arbitraryTransactionData.getName(),
arbitraryTransactionData.getService().name(),
"can't store data, deleting",
arbitraryTransactionData.getTimestamp(),
arbitraryTransactionData.getTimestamp()
)
);
continue; continue;
} }
// Check to see if we have had a more recent PUT // Check to see if we have had a more recent PUT
if (!mostRecentTransaction) { boolean hasMoreRecentPutTransaction = ArbitraryTransactionUtils.hasMoreRecentPutTransaction(repository, arbitraryTransactionData);
if (hasMoreRecentPutTransaction) {
// There is a more recent PUT transaction than the one we are currently processing. // There is a more recent PUT transaction than the one we are currently processing.
// When a PUT is issued, it replaces any layers that would have been there before. // When a PUT is issued, it replaces any layers that would have been there before.
// Therefore any data relating to this older transaction is no longer needed. // Therefore any data relating to this older transaction is no longer needed.
LOGGER.info(String.format("Newer PUT found for %s %s since transaction %s. " + LOGGER.info(String.format("Newer PUT found for %s %s since transaction %s. " +
"Deleting all files associated with the earlier transaction.", arbitraryTransactionData.getService(), "Deleting all files associated with the earlier transaction.", arbitraryTransactionData.getService(),
arbitraryTransactionData.getName(), Base58.encode(arbitraryTransactionData.getSignature()))); arbitraryTransactionData.getName(), Base58.encode(signature)));
ArbitraryTransactionUtils.deleteCompleteFileAndChunks(arbitraryTransactionData); ArbitraryTransactionUtils.deleteCompleteFileAndChunks(arbitraryTransactionData);
Optional<ArbitraryTransactionData> moreRecentPutTransaction
= processedTransactions.stream()
.filter(data -> data.equals(arbitraryTransactionData))
.findAny();
if( moreRecentPutTransaction.isPresent() ) {
EventBus.INSTANCE.notify(
new DataMonitorEvent(
System.currentTimeMillis(),
arbitraryTransactionData.getIdentifier(),
arbitraryTransactionData.getName(),
arbitraryTransactionData.getService().name(),
"deleting data due to replacement",
arbitraryTransactionData.getTimestamp(),
moreRecentPutTransaction.get().getTimestamp()
)
);
}
else {
LOGGER.warn("Something went wrong with the most recent put transaction determination!");
}
continue; continue;
} }
@ -255,21 +199,7 @@ public class ArbitraryDataCleanupManager extends Thread {
LOGGER.debug(String.format("Transaction %s has complete file and all chunks", LOGGER.debug(String.format("Transaction %s has complete file and all chunks",
Base58.encode(arbitraryTransactionData.getSignature()))); Base58.encode(arbitraryTransactionData.getSignature())));
boolean wasDeleted = ArbitraryTransactionUtils.deleteCompleteFile(arbitraryTransactionData, now, STALE_FILE_TIMEOUT); ArbitraryTransactionUtils.deleteCompleteFile(arbitraryTransactionData, now, STALE_FILE_TIMEOUT);
if( wasDeleted ) {
EventBus.INSTANCE.notify(
new DataMonitorEvent(
System.currentTimeMillis(),
arbitraryTransactionData.getIdentifier(),
arbitraryTransactionData.getName(),
arbitraryTransactionData.getService().name(),
"deleting file, retaining chunks",
arbitraryTransactionData.getTimestamp(),
arbitraryTransactionData.getTimestamp()
)
);
}
continue; continue;
} }
@ -307,6 +237,17 @@ public class ArbitraryDataCleanupManager extends Thread {
this.storageLimitReached(repository); this.storageLimitReached(repository);
} }
// Delete random data associated with name if we're over our storage limit for this name
// Use the DELETION_THRESHOLD, for the same reasons as above
for (String followedName : ListUtils.followedNames()) {
if (isStopping) {
return;
}
if (!storageManager.isStorageSpaceAvailableForName(repository, followedName, DELETION_THRESHOLD)) {
this.storageLimitReachedForName(repository, followedName);
}
}
} catch (DataException e) { } catch (DataException e) {
LOGGER.error("Repository issue when cleaning up arbitrary transaction data", e); LOGGER.error("Repository issue when cleaning up arbitrary transaction data", e);
} }
@ -385,6 +326,25 @@ public class ArbitraryDataCleanupManager extends Thread {
// FUTURE: consider reducing the expiry time of the reader cache // FUTURE: consider reducing the expiry time of the reader cache
} }
public void storageLimitReachedForName(Repository repository, String name) throws InterruptedException {
// We think that the storage limit has been reached for supplied name - but we should double check
if (ArbitraryDataStorageManager.getInstance().isStorageSpaceAvailableForName(repository, name, DELETION_THRESHOLD)) {
// We have space available for this name, so don't delete anything
return;
}
// Delete a batch of random chunks associated with this name
// This reduces the chance of too many nodes deleting the same chunk
// when they reach their storage limit
Path dataPath = Paths.get(Settings.getInstance().getDataPath());
for (int i=0; i<CHUNK_DELETION_BATCH_SIZE; i++) {
if (isStopping) {
return;
}
this.deleteRandomFile(repository, dataPath.toFile(), name);
}
}
/** /**
* Iteratively walk through given directory and delete a single random file * Iteratively walk through given directory and delete a single random file
* *
@ -463,7 +423,6 @@ public class ArbitraryDataCleanupManager extends Thread {
} }
LOGGER.info("Deleting random file {} because we have reached max storage capacity...", randomItem.toString()); LOGGER.info("Deleting random file {} because we have reached max storage capacity...", randomItem.toString());
fireRandomItemDeletionNotification(randomItem, repository, "Deleting random file, because we have reached max storage capacity");
boolean success = randomItem.delete(); boolean success = randomItem.delete();
if (success) { if (success) {
try { try {
@ -478,35 +437,6 @@ public class ArbitraryDataCleanupManager extends Thread {
return false; return false;
} }
private void fireRandomItemDeletionNotification(File randomItem, Repository repository, String reason) {
try {
Path parentFileNamePath = randomItem.toPath().toAbsolutePath().getParent().getFileName();
if (parentFileNamePath != null) {
String signature58 = parentFileNamePath.toString();
byte[] signature = Base58.decode(signature58);
TransactionData transactionData = repository.getTransactionRepository().fromSignature(signature);
if (transactionData != null && transactionData.getType() == Transaction.TransactionType.ARBITRARY) {
ArbitraryTransactionData arbitraryTransactionData = (ArbitraryTransactionData) transactionData;
EventBus.INSTANCE.notify(
new DataMonitorEvent(
System.currentTimeMillis(),
arbitraryTransactionData.getIdentifier(),
arbitraryTransactionData.getName(),
arbitraryTransactionData.getService().name(),
reason,
arbitraryTransactionData.getTimestamp(),
arbitraryTransactionData.getTimestamp()
)
);
}
}
} catch (Exception e) {
LOGGER.error(e.getMessage(), e);
}
}
private void cleanupTempDirectory(String folder, long now, long minAge) { private void cleanupTempDirectory(String folder, long now, long minAge) {
String baseDir = Settings.getInstance().getTempDataPath(); String baseDir = Settings.getInstance().getTempDataPath();
Path tempDir = Paths.get(baseDir, folder); Path tempDir = Paths.get(baseDir, folder);

View File

@ -1,21 +0,0 @@
package org.qortal.controller.arbitrary;
public class ArbitraryDataExamination {
private boolean pass;
private String notes;
public ArbitraryDataExamination(boolean pass, String notes) {
this.pass = pass;
this.notes = notes;
}
public boolean isPass() {
return pass;
}
public String getNotes() {
return notes;
}
}

View File

@ -5,8 +5,6 @@ import org.apache.logging.log4j.Logger;
import org.qortal.controller.Controller; import org.qortal.controller.Controller;
import org.qortal.data.arbitrary.ArbitraryFileListResponseInfo; import org.qortal.data.arbitrary.ArbitraryFileListResponseInfo;
import org.qortal.data.transaction.ArbitraryTransactionData; import org.qortal.data.transaction.ArbitraryTransactionData;
import org.qortal.event.DataMonitorEvent;
import org.qortal.event.EventBus;
import org.qortal.network.Peer; import org.qortal.network.Peer;
import org.qortal.repository.DataException; import org.qortal.repository.DataException;
import org.qortal.repository.Repository; import org.qortal.repository.Repository;

View File

@ -10,8 +10,6 @@ import org.qortal.arbitrary.misc.Service;
import org.qortal.controller.Controller; import org.qortal.controller.Controller;
import org.qortal.data.transaction.ArbitraryTransactionData; import org.qortal.data.transaction.ArbitraryTransactionData;
import org.qortal.data.transaction.TransactionData; import org.qortal.data.transaction.TransactionData;
import org.qortal.event.DataMonitorEvent;
import org.qortal.event.EventBus;
import org.qortal.network.Network; import org.qortal.network.Network;
import org.qortal.network.Peer; import org.qortal.network.Peer;
import org.qortal.repository.DataException; import org.qortal.repository.DataException;
@ -30,7 +28,6 @@ import java.nio.file.Files;
import java.nio.file.Path; import java.nio.file.Path;
import java.nio.file.Paths; import java.nio.file.Paths;
import java.util.*; import java.util.*;
import java.util.stream.Collectors;
public class ArbitraryDataManager extends Thread { public class ArbitraryDataManager extends Thread {
@ -198,35 +195,13 @@ public class ArbitraryDataManager extends Thread {
final int limit = 100; final int limit = 100;
int offset = 0; int offset = 0;
List<ArbitraryTransactionData> allArbitraryTransactionsInDescendingOrder;
try (final Repository repository = RepositoryManager.getRepository()) {
if( name == null ) {
allArbitraryTransactionsInDescendingOrder
= repository.getArbitraryRepository()
.getLatestArbitraryTransactions();
}
else {
allArbitraryTransactionsInDescendingOrder
= repository.getArbitraryRepository()
.getLatestArbitraryTransactionsByName(name);
}
} catch( Exception e) {
LOGGER.error(e.getMessage(), e);
allArbitraryTransactionsInDescendingOrder = new ArrayList<>(0);
}
// collect processed transactions in a set to ensure outdated data transactions do not get fetched
Set<ArbitraryTransactionDataHashWrapper> processedTransactions = new HashSet<>();
while (!isStopping) { while (!isStopping) {
Thread.sleep(1000L); Thread.sleep(1000L);
// Any arbitrary transactions we want to fetch data for? // Any arbitrary transactions we want to fetch data for?
try (final Repository repository = RepositoryManager.getRepository()) { try (final Repository repository = RepositoryManager.getRepository()) {
List<byte[]> signatures = processTransactionsForSignatures(limit, offset, allArbitraryTransactionsInDescendingOrder, processedTransactions); List<byte[]> signatures = repository.getTransactionRepository().getSignaturesMatchingCriteria(null, null, null, ARBITRARY_TX_TYPE, null, name, null, ConfirmationStatus.BOTH, limit, offset, true);
// LOGGER.trace("Found {} arbitrary transactions at offset: {}, limit: {}", signatures.size(), offset, limit);
if (signatures == null || signatures.isEmpty()) { if (signatures == null || signatures.isEmpty()) {
offset = 0; offset = 0;
break; break;
@ -248,38 +223,14 @@ public class ArbitraryDataManager extends Thread {
ArbitraryTransactionData arbitraryTransactionData = (ArbitraryTransactionData) arbitraryTransaction.getTransactionData(); ArbitraryTransactionData arbitraryTransactionData = (ArbitraryTransactionData) arbitraryTransaction.getTransactionData();
// Skip transactions that we don't need to proactively store data for // Skip transactions that we don't need to proactively store data for
ArbitraryDataExamination arbitraryDataExamination = storageManager.shouldPreFetchData(repository, arbitraryTransactionData); if (!storageManager.shouldPreFetchData(repository, arbitraryTransactionData)) {
if (!arbitraryDataExamination.isPass()) {
iterator.remove(); iterator.remove();
EventBus.INSTANCE.notify(
new DataMonitorEvent(
System.currentTimeMillis(),
arbitraryTransactionData.getIdentifier(),
arbitraryTransactionData.getName(),
arbitraryTransactionData.getService().name(),
arbitraryDataExamination.getNotes(),
arbitraryTransactionData.getTimestamp(),
arbitraryTransactionData.getTimestamp()
)
);
continue; continue;
} }
// Remove transactions that we already have local data for // Remove transactions that we already have local data for
if (hasLocalData(arbitraryTransaction)) { if (hasLocalData(arbitraryTransaction)) {
iterator.remove(); iterator.remove();
EventBus.INSTANCE.notify(
new DataMonitorEvent(
System.currentTimeMillis(),
arbitraryTransactionData.getIdentifier(),
arbitraryTransactionData.getName(),
arbitraryTransactionData.getService().name(),
"already have local data, skipping",
arbitraryTransactionData.getTimestamp(),
arbitraryTransactionData.getTimestamp()
)
);
} }
} }
@ -297,21 +248,8 @@ public class ArbitraryDataManager extends Thread {
// Check to see if we have had a more recent PUT // Check to see if we have had a more recent PUT
ArbitraryTransactionData arbitraryTransactionData = ArbitraryTransactionUtils.fetchTransactionData(repository, signature); ArbitraryTransactionData arbitraryTransactionData = ArbitraryTransactionUtils.fetchTransactionData(repository, signature);
boolean hasMoreRecentPutTransaction = ArbitraryTransactionUtils.hasMoreRecentPutTransaction(repository, arbitraryTransactionData);
Optional<ArbitraryTransactionData> moreRecentPutTransaction = ArbitraryTransactionUtils.hasMoreRecentPutTransaction(repository, arbitraryTransactionData); if (hasMoreRecentPutTransaction) {
if (moreRecentPutTransaction.isPresent()) {
EventBus.INSTANCE.notify(
new DataMonitorEvent(
System.currentTimeMillis(),
arbitraryTransactionData.getIdentifier(),
arbitraryTransactionData.getName(),
arbitraryTransactionData.getService().name(),
"not fetching old data",
arbitraryTransactionData.getTimestamp(),
moreRecentPutTransaction.get().getTimestamp()
)
);
// There is a more recent PUT transaction than the one we are currently processing. // There is a more recent PUT transaction than the one we are currently processing.
// When a PUT is issued, it replaces any layers that would have been there before. // When a PUT is issued, it replaces any layers that would have been there before.
// Therefore any data relating to this older transaction is no longer needed and we // Therefore any data relating to this older transaction is no longer needed and we
@ -319,34 +257,10 @@ public class ArbitraryDataManager extends Thread {
continue; continue;
} }
EventBus.INSTANCE.notify(
new DataMonitorEvent(
System.currentTimeMillis(),
arbitraryTransactionData.getIdentifier(),
arbitraryTransactionData.getName(),
arbitraryTransactionData.getService().name(),
"fetching data",
arbitraryTransactionData.getTimestamp(),
arbitraryTransactionData.getTimestamp()
)
);
// Ask our connected peers if they have files for this signature // Ask our connected peers if they have files for this signature
// This process automatically then fetches the files themselves if a peer is found // This process automatically then fetches the files themselves if a peer is found
fetchData(arbitraryTransactionData); fetchData(arbitraryTransactionData);
EventBus.INSTANCE.notify(
new DataMonitorEvent(
System.currentTimeMillis(),
arbitraryTransactionData.getIdentifier(),
arbitraryTransactionData.getName(),
arbitraryTransactionData.getService().name(),
"fetched data",
arbitraryTransactionData.getTimestamp(),
arbitraryTransactionData.getTimestamp()
)
);
} catch (DataException e) { } catch (DataException e) {
LOGGER.error("Repository issue when fetching arbitrary transaction data", e); LOGGER.error("Repository issue when fetching arbitrary transaction data", e);
} }
@ -360,20 +274,6 @@ public class ArbitraryDataManager extends Thread {
final int limit = 100; final int limit = 100;
int offset = 0; int offset = 0;
List<ArbitraryTransactionData> allArbitraryTransactionsInDescendingOrder;
try (final Repository repository = RepositoryManager.getRepository()) {
allArbitraryTransactionsInDescendingOrder
= repository.getArbitraryRepository()
.getLatestArbitraryTransactions();
} catch( Exception e) {
LOGGER.error(e.getMessage(), e);
allArbitraryTransactionsInDescendingOrder = new ArrayList<>(0);
}
// collect processed transactions in a set to ensure outdated data transactions do not get fetched
Set<ArbitraryTransactionDataHashWrapper> processedTransactions = new HashSet<>();
while (!isStopping) { while (!isStopping) {
final int minSeconds = 3; final int minSeconds = 3;
final int maxSeconds = 10; final int maxSeconds = 10;
@ -382,8 +282,8 @@ public class ArbitraryDataManager extends Thread {
// Any arbitrary transactions we want to fetch data for? // Any arbitrary transactions we want to fetch data for?
try (final Repository repository = RepositoryManager.getRepository()) { try (final Repository repository = RepositoryManager.getRepository()) {
List<byte[]> signatures = processTransactionsForSignatures(limit, offset, allArbitraryTransactionsInDescendingOrder, processedTransactions); List<byte[]> signatures = repository.getTransactionRepository().getSignaturesMatchingCriteria(null, null, null, ARBITRARY_TX_TYPE, null, null, null, ConfirmationStatus.BOTH, limit, offset, true);
// LOGGER.trace("Found {} arbitrary transactions at offset: {}, limit: {}", signatures.size(), offset, limit);
if (signatures == null || signatures.isEmpty()) { if (signatures == null || signatures.isEmpty()) {
offset = 0; offset = 0;
break; break;
@ -428,74 +328,26 @@ public class ArbitraryDataManager extends Thread {
continue; continue;
} }
// No longer need to see if we have had a more recent PUT since we compared the transactions to process // Check to see if we have had a more recent PUT
// to the transactions previously processed, so we can fetch the transactiondata, notify the event bus,
// fetch the metadata and notify the event bus again
ArbitraryTransactionData arbitraryTransactionData = ArbitraryTransactionUtils.fetchTransactionData(repository, signature); ArbitraryTransactionData arbitraryTransactionData = ArbitraryTransactionUtils.fetchTransactionData(repository, signature);
boolean hasMoreRecentPutTransaction = ArbitraryTransactionUtils.hasMoreRecentPutTransaction(repository, arbitraryTransactionData);
if (hasMoreRecentPutTransaction) {
// There is a more recent PUT transaction than the one we are currently processing.
// When a PUT is issued, it replaces any layers that would have been there before.
// Therefore any data relating to this older transaction is no longer needed and we
// shouldn't fetch it from the network.
continue;
}
// Ask our connected peers if they have metadata for this signature // Ask our connected peers if they have metadata for this signature
fetchMetadata(arbitraryTransactionData); fetchMetadata(arbitraryTransactionData);
EventBus.INSTANCE.notify(
new DataMonitorEvent(
System.currentTimeMillis(),
arbitraryTransactionData.getIdentifier(),
arbitraryTransactionData.getName(),
arbitraryTransactionData.getService().name(),
"fetched metadata",
arbitraryTransactionData.getTimestamp(),
arbitraryTransactionData.getTimestamp()
)
);
} catch (DataException e) { } catch (DataException e) {
LOGGER.error("Repository issue when fetching arbitrary transaction data", e); LOGGER.error("Repository issue when fetching arbitrary transaction data", e);
} catch (Exception e) {
LOGGER.error(e.getMessage(), e);
} }
} }
} }
private static List<byte[]> processTransactionsForSignatures(
int limit,
int offset,
List<ArbitraryTransactionData> transactionsInDescendingOrder,
Set<ArbitraryTransactionDataHashWrapper> processedTransactions) {
// these transactions are in descending order, latest transactions come first
List<ArbitraryTransactionData> transactions
= transactionsInDescendingOrder.stream()
.skip(offset)
.limit(limit)
.collect(Collectors.toList());
// wrap the transactions, so they can be used for hashing and comparing
// Class ArbitraryTransactionDataHashWrapper supports hashCode() and equals(...) for this purpose
List<ArbitraryTransactionDataHashWrapper> wrappedTransactions
= transactions.stream()
.map(transaction -> new ArbitraryTransactionDataHashWrapper(transaction))
.collect(Collectors.toList());
// create a set of wrappers and populate it first to last, so that all outdated transactions get rejected
Set<ArbitraryTransactionDataHashWrapper> transactionsToProcess = new HashSet<>(wrappedTransactions.size());
for(ArbitraryTransactionDataHashWrapper wrappedTransaction : wrappedTransactions) {
transactionsToProcess.add(wrappedTransaction);
}
// remove the matches for previously processed transactions,
// because these transactions have had updates that have already been processed
transactionsToProcess.removeAll(processedTransactions);
// add to processed transactions to compare and remove matches from future processing iterations
processedTransactions.addAll(transactionsToProcess);
List<byte[]> signatures
= transactionsToProcess.stream()
.map(transactionToProcess -> transactionToProcess.getData()
.getSignature())
.collect(Collectors.toList());
return signatures;
}
private ArbitraryTransaction fetchTransaction(final Repository repository, byte[] signature) { private ArbitraryTransaction fetchTransaction(final Repository repository, byte[] signature) {
try { try {
TransactionData transactionData = repository.getTransactionRepository().fromSignature(signature); TransactionData transactionData = repository.getTransactionRepository().fromSignature(signature);

View File

@ -155,24 +155,31 @@ public class ArbitraryDataStorageManager extends Thread {
* @param arbitraryTransactionData - the transaction * @param arbitraryTransactionData - the transaction
* @return boolean - whether to prefetch or not * @return boolean - whether to prefetch or not
*/ */
public ArbitraryDataExamination shouldPreFetchData(Repository repository, ArbitraryTransactionData arbitraryTransactionData) { public boolean shouldPreFetchData(Repository repository, ArbitraryTransactionData arbitraryTransactionData) {
String name = arbitraryTransactionData.getName(); String name = arbitraryTransactionData.getName();
// Only fetch data associated with hashes, as we already have RAW_DATA // Only fetch data associated with hashes, as we already have RAW_DATA
if (arbitraryTransactionData.getDataType() != ArbitraryTransactionData.DataType.DATA_HASH) { if (arbitraryTransactionData.getDataType() != ArbitraryTransactionData.DataType.DATA_HASH) {
return new ArbitraryDataExamination(false, "Only fetch data associated with hashes"); return false;
} }
// Don't fetch anything more if we're (nearly) out of space // Don't fetch anything more if we're (nearly) out of space
// Make sure to keep STORAGE_FULL_THRESHOLD considerably less than 1, to // Make sure to keep STORAGE_FULL_THRESHOLD considerably less than 1, to
// avoid a fetch/delete loop // avoid a fetch/delete loop
if (!this.isStorageSpaceAvailable(STORAGE_FULL_THRESHOLD)) { if (!this.isStorageSpaceAvailable(STORAGE_FULL_THRESHOLD)) {
return new ArbitraryDataExamination(false,"Don't fetch anything more if we're (nearly) out of space"); return false;
}
// Don't fetch anything if we're (nearly) out of space for this name
// Again, make sure to keep STORAGE_FULL_THRESHOLD considerably less than 1, to
// avoid a fetch/delete loop
if (!this.isStorageSpaceAvailableForName(repository, arbitraryTransactionData.getName(), STORAGE_FULL_THRESHOLD)) {
return false;
} }
// Don't store data unless it's an allowed type (public/private) // Don't store data unless it's an allowed type (public/private)
if (!this.isDataTypeAllowed(arbitraryTransactionData)) { if (!this.isDataTypeAllowed(arbitraryTransactionData)) {
return new ArbitraryDataExamination(false, "Don't store data unless it's an allowed type (public/private)"); return false;
} }
// Handle transactions without names differently // Handle transactions without names differently
@ -182,21 +189,21 @@ public class ArbitraryDataStorageManager extends Thread {
// Never fetch data from blocked names, even if they are followed // Never fetch data from blocked names, even if they are followed
if (ListUtils.isNameBlocked(name)) { if (ListUtils.isNameBlocked(name)) {
return new ArbitraryDataExamination(false, "blocked name"); return false;
} }
switch (Settings.getInstance().getStoragePolicy()) { switch (Settings.getInstance().getStoragePolicy()) {
case FOLLOWED: case FOLLOWED:
case FOLLOWED_OR_VIEWED: case FOLLOWED_OR_VIEWED:
return new ArbitraryDataExamination(ListUtils.isFollowingName(name), Settings.getInstance().getStoragePolicy().name()); return ListUtils.isFollowingName(name);
case ALL: case ALL:
return new ArbitraryDataExamination(true, Settings.getInstance().getStoragePolicy().name()); return true;
case NONE: case NONE:
case VIEWED: case VIEWED:
default: default:
return new ArbitraryDataExamination(false, Settings.getInstance().getStoragePolicy().name()); return false;
} }
} }
@ -207,17 +214,17 @@ public class ArbitraryDataStorageManager extends Thread {
* *
* @return boolean - whether the storage policy allows for unnamed data * @return boolean - whether the storage policy allows for unnamed data
*/ */
private ArbitraryDataExamination shouldPreFetchDataWithoutName() { private boolean shouldPreFetchDataWithoutName() {
switch (Settings.getInstance().getStoragePolicy()) { switch (Settings.getInstance().getStoragePolicy()) {
case ALL: case ALL:
return new ArbitraryDataExamination(true, "Fetching all data"); return true;
case NONE: case NONE:
case VIEWED: case VIEWED:
case FOLLOWED: case FOLLOWED:
case FOLLOWED_OR_VIEWED: case FOLLOWED_OR_VIEWED:
default: default:
return new ArbitraryDataExamination(false, Settings.getInstance().getStoragePolicy().name()); return false;
} }
} }
@ -477,6 +484,51 @@ public class ArbitraryDataStorageManager extends Thread {
return true; return true;
} }
public boolean isStorageSpaceAvailableForName(Repository repository, String name, double threshold) {
if (!this.isStorageSpaceAvailable(threshold)) {
// No storage space available at all, so no need to check this name
return false;
}
if (Settings.getInstance().getStoragePolicy() == StoragePolicy.ALL) {
// Using storage policy ALL, so don't limit anything per name
return true;
}
if (name == null) {
// This transaction doesn't have a name, so fall back to total space limitations
return true;
}
int followedNamesCount = ListUtils.followedNamesCount();
if (followedNamesCount == 0) {
// Not following any names, so we have space
return true;
}
long totalSizeForName = 0;
long maxStoragePerName = this.storageCapacityPerName(threshold);
// Fetch all hosted transactions
List<ArbitraryTransactionData> hostedTransactions = this.listAllHostedTransactions(repository, null, null);
for (ArbitraryTransactionData transactionData : hostedTransactions) {
String transactionName = transactionData.getName();
if (!Objects.equals(name, transactionName)) {
// Transaction relates to a different name
continue;
}
totalSizeForName += transactionData.getSize();
}
// Have we reached the limit for this name?
if (totalSizeForName > maxStoragePerName) {
return false;
}
return true;
}
public long storageCapacityPerName(double threshold) { public long storageCapacityPerName(double threshold) {
int followedNamesCount = ListUtils.followedNamesCount(); int followedNamesCount = ListUtils.followedNamesCount();
if (followedNamesCount == 0) { if (followedNamesCount == 0) {

View File

@ -1,48 +0,0 @@
package org.qortal.controller.arbitrary;
import org.qortal.arbitrary.misc.Service;
import org.qortal.data.transaction.ArbitraryTransactionData;
import java.util.Objects;
public class ArbitraryTransactionDataHashWrapper {
private ArbitraryTransactionData data;
private int service;
private String name;
private String identifier;
public ArbitraryTransactionDataHashWrapper(ArbitraryTransactionData data) {
this.data = data;
this.service = data.getService().value;
this.name = data.getName();
this.identifier = data.getIdentifier();
}
public ArbitraryTransactionDataHashWrapper(int service, String name, String identifier) {
this.service = service;
this.name = name;
this.identifier = identifier;
}
public ArbitraryTransactionData getData() {
return data;
}
@Override
public boolean equals(Object o) {
if (this == o) return true;
if (o == null || getClass() != o.getClass()) return false;
ArbitraryTransactionDataHashWrapper that = (ArbitraryTransactionDataHashWrapper) o;
return service == that.service && name.equals(that.name) && Objects.equals(identifier, that.identifier);
}
@Override
public int hashCode() {
return Objects.hash(service, name, identifier);
}
}

View File

@ -1,33 +0,0 @@
package org.qortal.controller.arbitrary;
import org.apache.logging.log4j.LogManager;
import org.apache.logging.log4j.Logger;
import org.qortal.repository.DataException;
import org.qortal.repository.Repository;
import org.qortal.repository.RepositoryManager;
import java.util.TimerTask;
public class RebuildArbitraryResourceCacheTask extends TimerTask {
private static final Logger LOGGER = LogManager.getLogger(RebuildArbitraryResourceCacheTask.class);
public static final long MILLIS_IN_HOUR = 60 * 60 * 1000;
public static final long MILLIS_IN_MINUTE = 60 * 1000;
private static final String REBUILD_ARBITRARY_RESOURCE_CACHE_TASK = "Rebuild Arbitrary Resource Cache Task";
@Override
public void run() {
Thread.currentThread().setName(REBUILD_ARBITRARY_RESOURCE_CACHE_TASK);
try (final Repository repository = RepositoryManager.getRepository()) {
ArbitraryDataCacheManager.getInstance().buildArbitraryResourcesCache(repository, true);
}
catch( DataException e ) {
LOGGER.error(e.getMessage(), e);
}
}
}

View File

@ -2,19 +2,15 @@ package org.qortal.controller.hsqldb;
import org.apache.logging.log4j.LogManager; import org.apache.logging.log4j.LogManager;
import org.apache.logging.log4j.Logger; import org.apache.logging.log4j.Logger;
import org.apache.logging.log4j.util.PropertySource;
import org.qortal.data.account.AccountBalanceData; import org.qortal.data.account.AccountBalanceData;
import org.qortal.data.account.BlockHeightRange;
import org.qortal.data.account.BlockHeightRangeAddressAmounts;
import org.qortal.repository.hsqldb.HSQLDBCacheUtils; import org.qortal.repository.hsqldb.HSQLDBCacheUtils;
import org.qortal.settings.Settings; import org.qortal.settings.Settings;
import org.qortal.utils.BalanceRecorderUtils;
import java.util.ArrayList;
import java.util.Comparator; import java.util.Comparator;
import java.util.List; import java.util.List;
import java.util.Optional; import java.util.Optional;
import java.util.concurrent.ConcurrentHashMap; import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.CopyOnWriteArrayList;
import java.util.stream.Collectors; import java.util.stream.Collectors;
public class HSQLDBBalanceRecorder extends Thread{ public class HSQLDBBalanceRecorder extends Thread{
@ -27,8 +23,6 @@ public class HSQLDBBalanceRecorder extends Thread{
private ConcurrentHashMap<String, List<AccountBalanceData>> balancesByAddress = new ConcurrentHashMap<>(); private ConcurrentHashMap<String, List<AccountBalanceData>> balancesByAddress = new ConcurrentHashMap<>();
private CopyOnWriteArrayList<BlockHeightRangeAddressAmounts> balanceDynamics = new CopyOnWriteArrayList<>();
private int priorityRequested; private int priorityRequested;
private int frequency; private int frequency;
private int capacity; private int capacity;
@ -67,52 +61,36 @@ public class HSQLDBBalanceRecorder extends Thread{
Thread.currentThread().setName("Balance Recorder"); Thread.currentThread().setName("Balance Recorder");
HSQLDBCacheUtils.startRecordingBalances(this.balancesByHeight, this.balanceDynamics, this.priorityRequested, this.frequency, this.capacity); HSQLDBCacheUtils.startRecordingBalances(this.balancesByHeight, this.balancesByAddress, this.priorityRequested, this.frequency, this.capacity);
} }
public List<BlockHeightRangeAddressAmounts> getLatestDynamics(int limit, long offset) { public List<AccountBalanceData> getLatestRecordings(int limit, long offset) {
ArrayList<AccountBalanceData> data;
List<BlockHeightRangeAddressAmounts> latest = this.balanceDynamics.stream() Optional<Integer> lastHeight = getLastHeight();
.sorted(BalanceRecorderUtils.BLOCK_HEIGHT_RANGE_ADDRESS_AMOUNTS_COMPARATOR.reversed())
.skip(offset)
.limit(limit)
.collect(Collectors.toList());
return latest; if(lastHeight.isPresent() ) {
} List<AccountBalanceData> latest = this.balancesByHeight.get(lastHeight.get());
public List<BlockHeightRange> getRanges(Integer offset, Integer limit, Boolean reverse) { if( latest != null ) {
data = new ArrayList<>(latest.size());
if( reverse ) { data.addAll(
return this.balanceDynamics.stream() latest.stream()
.map(BlockHeightRangeAddressAmounts::getRange) .sorted(Comparator.comparingDouble(AccountBalanceData::getBalance).reversed())
.sorted(BalanceRecorderUtils.BLOCK_HEIGHT_RANGE_COMPARATOR.reversed()) .skip(offset)
.skip(offset) .limit(limit)
.limit(limit) .collect(Collectors.toList())
.collect(Collectors.toList()); );
}
else {
data = new ArrayList<>(0);
}
} }
else { else {
return this.balanceDynamics.stream() data = new ArrayList<>(0);
.map(BlockHeightRangeAddressAmounts::getRange)
.sorted(BalanceRecorderUtils.BLOCK_HEIGHT_RANGE_COMPARATOR)
.skip(offset)
.limit(limit)
.collect(Collectors.toList());
} }
}
public Optional<BlockHeightRangeAddressAmounts> getAddressAmounts(BlockHeightRange range) { return data;
return this.balanceDynamics.stream()
.filter( dynamic -> dynamic.getRange().equals(range))
.findAny();
}
public Optional<BlockHeightRange> getRange( int height ) {
return this.balanceDynamics.stream()
.map(BlockHeightRangeAddressAmounts::getRange)
.filter( range -> range.getBegin() < height && range.getEnd() >= height )
.findAny();
} }
private Optional<Integer> getLastHeight() { private Optional<Integer> getLastHeight() {

View File

@ -83,7 +83,6 @@ public abstract class Bitcoiny implements ForeignBlockchain {
return this.bitcoinjContext; return this.bitcoinjContext;
} }
@Override
public String getCurrencyCode() { public String getCurrencyCode() {
return this.currencyCode; return this.currencyCode;
} }

View File

@ -644,10 +644,8 @@ public class ElectrumX extends BitcoinyBlockchainProvider {
} }
/** /**
* <p>Performs RPC call, with automatic reconnection to different server if needed. * Performs RPC call, with automatic reconnection to different server if needed.
* </p> * <p>
* @param method String representation of the RPC call value
* @param params a list of Objects passed to the method of the Remote Server
* @return "result" object from within JSON output * @return "result" object from within JSON output
* @throws ForeignBlockchainException if server returns error or something goes wrong * @throws ForeignBlockchainException if server returns error or something goes wrong
*/ */

View File

@ -2,8 +2,6 @@ package org.qortal.crosschain;
public interface ForeignBlockchain { public interface ForeignBlockchain {
public String getCurrencyCode();
public boolean isValidAddress(String address); public boolean isValidAddress(String address);
public boolean isValidWalletKey(String walletKey); public boolean isValidWalletKey(String walletKey);

View File

@ -1,54 +0,0 @@
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 +
'}';
}
}

View File

@ -1,59 +0,0 @@
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 +
'}';
}
}

View File

@ -1,52 +0,0 @@
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 +
'}';
}
}

View File

@ -1,34 +0,0 @@
package org.qortal.data.arbitrary;
import org.qortal.arbitrary.misc.Service;
import javax.xml.bind.annotation.XmlAccessType;
import javax.xml.bind.annotation.XmlAccessorType;
@XmlAccessorType(XmlAccessType.FIELD)
public class ArbitraryDataIndex {
public String t;
public String n;
public int c;
public String l;
public ArbitraryDataIndex() {}
public ArbitraryDataIndex(String t, String n, int c, String l) {
this.t = t;
this.n = n;
this.c = c;
this.l = l;
}
@Override
public String toString() {
return "ArbitraryDataIndex{" +
"t='" + t + '\'' +
", n='" + n + '\'' +
", c=" + c +
", l='" + l + '\'' +
'}';
}
}

View File

@ -1,41 +0,0 @@
package org.qortal.data.arbitrary;
import javax.xml.bind.annotation.XmlAccessType;
import javax.xml.bind.annotation.XmlAccessorType;
@XmlAccessorType(XmlAccessType.FIELD)
public class ArbitraryDataIndexDetail {
public String issuer;
public int rank;
public String term;
public String name;
public int category;
public String link;
public String indexIdentifer;
public ArbitraryDataIndexDetail() {}
public ArbitraryDataIndexDetail(String issuer, int rank, ArbitraryDataIndex index, String indexIdentifer) {
this.issuer = issuer;
this.rank = rank;
this.term = index.t;
this.name = index.n;
this.category = index.c;
this.link = index.l;
this.indexIdentifer = indexIdentifer;
}
@Override
public String toString() {
return "ArbitraryDataIndexDetail{" +
"issuer='" + issuer + '\'' +
", rank=" + rank +
", term='" + term + '\'' +
", name='" + name + '\'' +
", category=" + category +
", link='" + link + '\'' +
", indexIdentifer='" + indexIdentifer + '\'' +
'}';
}
}

View File

@ -1,38 +0,0 @@
package org.qortal.data.arbitrary;
import org.qortal.arbitrary.misc.Service;
import javax.xml.bind.annotation.XmlAccessType;
import javax.xml.bind.annotation.XmlAccessorType;
import java.util.Objects;
@XmlAccessorType(XmlAccessType.FIELD)
public class ArbitraryDataIndexScoreKey {
public String name;
public int category;
public String link;
public ArbitraryDataIndexScoreKey() {}
public ArbitraryDataIndexScoreKey(String name, int category, String link) {
this.name = name;
this.category = category;
this.link = link;
}
@Override
public boolean equals(Object o) {
if (this == o) return true;
if (o == null || getClass() != o.getClass()) return false;
ArbitraryDataIndexScoreKey that = (ArbitraryDataIndexScoreKey) o;
return category == that.category && Objects.equals(name, that.name) && Objects.equals(link, that.link);
}
@Override
public int hashCode() {
return Objects.hash(name, category, link);
}
}

View File

@ -1,38 +0,0 @@
package org.qortal.data.arbitrary;
import org.qortal.arbitrary.misc.Service;
import javax.xml.bind.annotation.XmlAccessType;
import javax.xml.bind.annotation.XmlAccessorType;
@XmlAccessorType(XmlAccessType.FIELD)
public class ArbitraryDataIndexScorecard {
public double score;
public String name;
public int category;
public String link;
public ArbitraryDataIndexScorecard() {}
public ArbitraryDataIndexScorecard(double score, String name, int category, String link) {
this.score = score;
this.name = name;
this.category = category;
this.link = link;
}
public double getScore() {
return score;
}
@Override
public String toString() {
return "ArbitraryDataIndexScorecard{" +
"score=" + score +
", name='" + name + '\'' +
", category=" + category +
", link='" + link + '\'' +
'}';
}
}

View File

@ -1,57 +0,0 @@
package org.qortal.data.arbitrary;
import javax.xml.bind.annotation.XmlAccessType;
import javax.xml.bind.annotation.XmlAccessorType;
@XmlAccessorType(XmlAccessType.FIELD)
public class DataMonitorInfo {
private long timestamp;
private String identifier;
private String name;
private String service;
private String description;
private long transactionTimestamp;
private long latestPutTimestamp;
public DataMonitorInfo() {
}
public DataMonitorInfo(long timestamp, String identifier, String name, String service, String description, long transactionTimestamp, long latestPutTimestamp) {
this.timestamp = timestamp;
this.identifier = identifier;
this.name = name;
this.service = service;
this.description = description;
this.transactionTimestamp = transactionTimestamp;
this.latestPutTimestamp = latestPutTimestamp;
}
public long getTimestamp() {
return timestamp;
}
public String getIdentifier() {
return identifier;
}
public String getName() {
return name;
}
public String getService() {
return service;
}
public String getDescription() {
return description;
}
public long getTransactionTimestamp() {
return transactionTimestamp;
}
public long getLatestPutTimestamp() {
return latestPutTimestamp;
}
}

View File

@ -1,23 +0,0 @@
package org.qortal.data.arbitrary;
import java.util.List;
import java.util.concurrent.ConcurrentHashMap;
public class IndexCache {
public static final IndexCache SINGLETON = new IndexCache();
private ConcurrentHashMap<String, List<ArbitraryDataIndexDetail>> indicesByTerm = new ConcurrentHashMap<>();
private ConcurrentHashMap<String, List<ArbitraryDataIndexDetail>> indicesByIssuer = new ConcurrentHashMap<>();
public static IndexCache getInstance() {
return SINGLETON;
}
public ConcurrentHashMap<String, List<ArbitraryDataIndexDetail>> getIndicesByTerm() {
return indicesByTerm;
}
public ConcurrentHashMap<String, List<ArbitraryDataIndexDetail>> getIndicesByIssuer() {
return indicesByIssuer;
}
}

View File

@ -1,85 +0,0 @@
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 +
'}';
}
}

View File

@ -1,35 +0,0 @@
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;
}
}

View File

@ -1,49 +0,0 @@
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;
}
}

View File

@ -200,26 +200,4 @@ public class ArbitraryTransactionData extends TransactionData {
return this.payments; return this.payments;
} }
@Override
public String toString() {
return "ArbitraryTransactionData{" +
"version=" + version +
", service=" + service +
", nonce=" + nonce +
", size=" + size +
", name='" + name + '\'' +
", identifier='" + identifier + '\'' +
", method=" + method +
", compression=" + compression +
", dataType=" + dataType +
", type=" + type +
", timestamp=" + timestamp +
", fee=" + fee +
", txGroupId=" + txGroupId +
", blockHeight=" + blockHeight +
", blockSequence=" + blockSequence +
", approvalStatus=" + approvalStatus +
", approvalHeight=" + approvalHeight +
'}';
}
} }

View File

@ -1,57 +0,0 @@
package org.qortal.event;
import javax.xml.bind.annotation.XmlAccessType;
import javax.xml.bind.annotation.XmlAccessorType;
@XmlAccessorType(XmlAccessType.FIELD)
public class DataMonitorEvent implements Event{
private long timestamp;
private String identifier;
private String name;
private String service;
private String description;
private long transactionTimestamp;
private long latestPutTimestamp;
public DataMonitorEvent() {
}
public DataMonitorEvent(long timestamp, String identifier, String name, String service, String description, long transactionTimestamp, long latestPutTimestamp) {
this.timestamp = timestamp;
this.identifier = identifier;
this.name = name;
this.service = service;
this.description = description;
this.transactionTimestamp = transactionTimestamp;
this.latestPutTimestamp = latestPutTimestamp;
}
public long getTimestamp() {
return timestamp;
}
public String getIdentifier() {
return identifier;
}
public String getName() {
return name;
}
public String getService() {
return service;
}
public String getDescription() {
return description;
}
public long getTransactionTimestamp() {
return transactionTimestamp;
}
public long getLatestPutTimestamp() {
return latestPutTimestamp;
}
}

View File

@ -2,7 +2,6 @@ package org.qortal.group;
import org.qortal.account.Account; import org.qortal.account.Account;
import org.qortal.account.PublicKeyAccount; import org.qortal.account.PublicKeyAccount;
import org.qortal.block.BlockChain;
import org.qortal.controller.Controller; import org.qortal.controller.Controller;
import org.qortal.crypto.Crypto; import org.qortal.crypto.Crypto;
import org.qortal.data.group.*; import org.qortal.data.group.*;
@ -151,12 +150,7 @@ public class Group {
// Adminship // Adminship
private GroupAdminData getAdmin(String admin) throws DataException { private GroupAdminData getAdmin(String admin) throws DataException {
if( repository.getBlockRepository().getBlockchainHeight() < BlockChain.getInstance().getAdminQueryFixHeight()) { return groupRepository.getAdmin(this.groupData.getGroupId(), admin);
return groupRepository.getAdminFaulty(this.groupData.getGroupId(), admin);
}
else {
return groupRepository.getAdmin(this.groupData.getGroupId(), admin);
}
} }
private boolean adminExists(String admin) throws DataException { private boolean adminExists(String admin) throws DataException {
@ -674,8 +668,8 @@ public class Group {
public void uninvite(GroupInviteTransactionData groupInviteTransactionData) throws DataException { public void uninvite(GroupInviteTransactionData groupInviteTransactionData) throws DataException {
String invitee = groupInviteTransactionData.getInvitee(); String invitee = groupInviteTransactionData.getInvitee();
// If member exists and the join request is present then they were added when invite matched join request // If member exists then they were added when invite matched join request
if (this.memberExists(invitee) && groupInviteTransactionData.getJoinReference() != null) { if (this.memberExists(invitee)) {
// Rebuild join request using cached reference to transaction that created join request. // Rebuild join request using cached reference to transaction that created join request.
this.rebuildJoinRequest(invitee, groupInviteTransactionData.getJoinReference()); this.rebuildJoinRequest(invitee, groupInviteTransactionData.getJoinReference());

View File

@ -76,9 +76,9 @@ public interface ATRepository {
* Although <tt>expectedValue</tt>, if provided, is natively an unsigned long, * Although <tt>expectedValue</tt>, if provided, is natively an unsigned long,
* the data segment comparison is done via unsigned hex string. * the data segment comparison is done via unsigned hex string.
*/ */
public List<ATStateData> getMatchingFinalATStates(byte[] codeHash, byte[] buyerPublicKey, byte[] sellerPublicKey, Boolean isFinished, public List<ATStateData> getMatchingFinalATStates(byte[] codeHash, Boolean isFinished,
Integer dataByteOffset, Long expectedValue, Integer minimumFinalHeight, Integer dataByteOffset, Long expectedValue, Integer minimumFinalHeight,
Integer limit, Integer offset, Boolean reverse) throws DataException; Integer limit, Integer offset, Boolean reverse) throws DataException;
/** /**
* Returns final ATStateData for ATs matching codeHash (required) * Returns final ATStateData for ATs matching codeHash (required)

View File

@ -27,10 +27,6 @@ public interface ArbitraryRepository {
public List<ArbitraryTransactionData> getArbitraryTransactions(String name, Service service, String identifier, long since) throws DataException; public List<ArbitraryTransactionData> getArbitraryTransactions(String name, Service service, String identifier, long since) throws DataException;
List<ArbitraryTransactionData> getLatestArbitraryTransactions() throws DataException;
List<ArbitraryTransactionData> getLatestArbitraryTransactionsByName(String name) throws DataException;
public ArbitraryTransactionData getInitialTransaction(String name, Service service, Method method, String identifier) throws DataException; public ArbitraryTransactionData getInitialTransaction(String name, Service service, Method method, String identifier) throws DataException;
public ArbitraryTransactionData getLatestTransaction(String name, Service service, Method method, String identifier) throws DataException; public ArbitraryTransactionData getLatestTransaction(String name, Service service, Method method, String identifier) throws DataException;
@ -46,7 +42,7 @@ public interface ArbitraryRepository {
public List<ArbitraryResourceData> getArbitraryResources(Service service, String identifier, List<String> names, boolean defaultResource, Boolean followedOnly, Boolean excludeBlocked, Boolean includeMetadata, Boolean includeStatus, Integer limit, Integer offset, Boolean reverse) throws DataException; public List<ArbitraryResourceData> getArbitraryResources(Service service, String identifier, List<String> names, boolean defaultResource, Boolean followedOnly, Boolean excludeBlocked, Boolean includeMetadata, Boolean includeStatus, Integer limit, Integer offset, Boolean reverse) throws DataException;
public List<ArbitraryResourceData> searchArbitraryResources(Service service, String query, String identifier, List<String> names, String title, String description, List<String> keywords, boolean prefixOnly, List<String> namesFilter, boolean defaultResource, SearchMode mode, Integer minLevel, Boolean followedOnly, Boolean excludeBlocked, Boolean includeMetadata, Boolean includeStatus, Long before, Long after, Integer limit, Integer offset, Boolean reverse) throws DataException; public List<ArbitraryResourceData> searchArbitraryResources(Service service, String query, String identifier, List<String> names, String title, String description, boolean prefixOnly, List<String> namesFilter, boolean defaultResource, SearchMode mode, Integer minLevel, Boolean followedOnly, Boolean excludeBlocked, Boolean includeMetadata, Boolean includeStatus, Long before, Long after, Integer limit, Integer offset, Boolean reverse) throws DataException;
List<ArbitraryResourceData> searchArbitraryResourcesSimple( List<ArbitraryResourceData> searchArbitraryResourcesSimple(
Service service, Service service,

View File

@ -48,8 +48,6 @@ public interface GroupRepository {
// Group Admins // Group Admins
public GroupAdminData getAdminFaulty(int groupId, String address) throws DataException;
public GroupAdminData getAdmin(int groupId, String address) throws DataException; public GroupAdminData getAdmin(int groupId, String address) throws DataException;
public boolean adminExists(int groupId, String address) throws DataException; public boolean adminExists(int groupId, String address) throws DataException;

View File

@ -1,11 +1,9 @@
package org.qortal.repository.hsqldb; package org.qortal.repository.hsqldb;
import com.google.common.primitives.Longs; import com.google.common.primitives.Longs;
import org.apache.logging.log4j.LogManager; import org.apache.logging.log4j.LogManager;
import org.apache.logging.log4j.Logger; import org.apache.logging.log4j.Logger;
import org.qortal.controller.Controller; import org.qortal.controller.Controller;
import org.qortal.crypto.Crypto;
import org.qortal.data.at.ATData; import org.qortal.data.at.ATData;
import org.qortal.data.at.ATStateData; import org.qortal.data.at.ATStateData;
import org.qortal.repository.ATRepository; import org.qortal.repository.ATRepository;
@ -18,8 +16,6 @@ import java.util.ArrayList;
import java.util.List; import java.util.List;
import java.util.Set; import java.util.Set;
import org.qortal.data.account.AccountData;
public class HSQLDBATRepository implements ATRepository { public class HSQLDBATRepository implements ATRepository {
private static final Logger LOGGER = LogManager.getLogger(HSQLDBATRepository.class); private static final Logger LOGGER = LogManager.getLogger(HSQLDBATRepository.class);
@ -404,9 +400,9 @@ public class HSQLDBATRepository implements ATRepository {
} }
@Override @Override
public List<ATStateData> getMatchingFinalATStates(byte[] codeHash, byte[] buyerPublicKey, byte[] sellerPublicKey, Boolean isFinished, public List<ATStateData> getMatchingFinalATStates(byte[] codeHash, Boolean isFinished,
Integer dataByteOffset, Long expectedValue, Integer minimumFinalHeight, Integer dataByteOffset, Long expectedValue, Integer minimumFinalHeight,
Integer limit, Integer offset, Boolean reverse) throws DataException { Integer limit, Integer offset, Boolean reverse) throws DataException {
StringBuilder sql = new StringBuilder(1024); StringBuilder sql = new StringBuilder(1024);
List<Object> bindParams = new ArrayList<>(); List<Object> bindParams = new ArrayList<>();
@ -425,14 +421,10 @@ public class HSQLDBATRepository implements ATRepository {
// Order by AT_address and height to use compound primary key as index // Order by AT_address and height to use compound primary key as index
// Both must be the same direction (DESC) also // Both must be the same direction (DESC) also
sql.append("ORDER BY ATStates.height DESC LIMIT 1) AS FinalATStates "); sql.append("ORDER BY ATStates.AT_address DESC, ATStates.height DESC "
+ "LIMIT 1 "
// Optional JOIN with ATTRANSACTIONS for buyerAddress + ") AS FinalATStates "
if (buyerPublicKey != null && buyerPublicKey.length > 0) { + "WHERE code_hash = ? ");
sql.append("JOIN ATTRANSACTIONS tx ON tx.at_address = ATs.AT_address ");
}
sql.append("WHERE ATs.code_hash = ? ");
bindParams.add(codeHash); bindParams.add(codeHash);
if (isFinished != null) { if (isFinished != null) {
@ -451,20 +443,6 @@ public class HSQLDBATRepository implements ATRepository {
bindParams.add(rawExpectedValue); bindParams.add(rawExpectedValue);
} }
if (buyerPublicKey != null && buyerPublicKey.length > 0 ) {
// the buyer must be the recipient of the transaction and not the creator of the AT
sql.append("AND tx.recipient = ? AND ATs.creator != ? ");
bindParams.add(Crypto.toAddress(buyerPublicKey));
bindParams.add(buyerPublicKey);
}
if (sellerPublicKey != null && sellerPublicKey.length > 0) {
sql.append("AND ATs.creator = ? ");
bindParams.add(sellerPublicKey);
}
sql.append(" ORDER BY FinalATStates.height "); sql.append(" ORDER BY FinalATStates.height ");
if (reverse != null && reverse) if (reverse != null && reverse)
sql.append("DESC"); sql.append("DESC");
@ -505,7 +483,7 @@ public class HSQLDBATRepository implements ATRepository {
Integer dataByteOffset, Long expectedValue, Integer dataByteOffset, Long expectedValue,
int minimumCount, int maximumCount, long minimumPeriod) throws DataException { int minimumCount, int maximumCount, long minimumPeriod) throws DataException {
// We need most recent entry first so we can use its timestamp to slice further results // We need most recent entry first so we can use its timestamp to slice further results
List<ATStateData> mostRecentStates = this.getMatchingFinalATStates(codeHash, null, null, isFinished, List<ATStateData> mostRecentStates = this.getMatchingFinalATStates(codeHash, isFinished,
dataByteOffset, expectedValue, null, dataByteOffset, expectedValue, null,
1, 0, true); 1, 0, true);

View File

@ -7,6 +7,7 @@ import org.qortal.arbitrary.ArbitraryDataFile;
import org.qortal.arbitrary.metadata.ArbitraryDataTransactionMetadata; import org.qortal.arbitrary.metadata.ArbitraryDataTransactionMetadata;
import org.qortal.arbitrary.misc.Category; import org.qortal.arbitrary.misc.Category;
import org.qortal.arbitrary.misc.Service; import org.qortal.arbitrary.misc.Service;
import org.qortal.controller.arbitrary.ArbitraryDataManager;
import org.qortal.data.arbitrary.ArbitraryResourceCache; import org.qortal.data.arbitrary.ArbitraryResourceCache;
import org.qortal.data.arbitrary.ArbitraryResourceData; import org.qortal.data.arbitrary.ArbitraryResourceData;
import org.qortal.data.arbitrary.ArbitraryResourceMetadata; import org.qortal.data.arbitrary.ArbitraryResourceMetadata;
@ -28,7 +29,6 @@ import org.qortal.utils.ListUtils;
import java.sql.ResultSet; import java.sql.ResultSet;
import java.sql.SQLException; import java.sql.SQLException;
import java.util.ArrayList; import java.util.ArrayList;
import java.util.Arrays;
import java.util.List; import java.util.List;
import java.util.Objects; import java.util.Objects;
import java.util.Optional; import java.util.Optional;
@ -227,144 +227,6 @@ public class HSQLDBArbitraryRepository implements ArbitraryRepository {
} }
} }
@Override
public List<ArbitraryTransactionData> getLatestArbitraryTransactions() throws DataException {
String sql = "SELECT type, reference, signature, creator, created_when, fee, " +
"tx_group_id, block_height, approval_status, approval_height, " +
"version, nonce, service, size, is_data_raw, data, metadata_hash, " +
"name, identifier, update_method, secret, compression FROM ArbitraryTransactions " +
"JOIN Transactions USING (signature) " +
"WHERE name IS NOT NULL " +
"ORDER BY created_when DESC";
List<ArbitraryTransactionData> arbitraryTransactionData = new ArrayList<>();
try (ResultSet resultSet = this.repository.checkedExecute(sql)) {
if (resultSet == null)
return new ArrayList<>(0);
do {
byte[] reference = resultSet.getBytes(2);
byte[] signature = resultSet.getBytes(3);
byte[] creatorPublicKey = resultSet.getBytes(4);
long timestamp = resultSet.getLong(5);
Long fee = resultSet.getLong(6);
if (fee == 0 && resultSet.wasNull())
fee = null;
int txGroupId = resultSet.getInt(7);
Integer blockHeight = resultSet.getInt(8);
if (blockHeight == 0 && resultSet.wasNull())
blockHeight = null;
ApprovalStatus approvalStatus = ApprovalStatus.valueOf(resultSet.getInt(9));
Integer approvalHeight = resultSet.getInt(10);
if (approvalHeight == 0 && resultSet.wasNull())
approvalHeight = null;
BaseTransactionData baseTransactionData = new BaseTransactionData(timestamp, txGroupId, reference, creatorPublicKey, fee, approvalStatus, blockHeight, approvalHeight, signature);
int version = resultSet.getInt(11);
int nonce = resultSet.getInt(12);
int serviceInt = resultSet.getInt(13);
int size = resultSet.getInt(14);
boolean isDataRaw = resultSet.getBoolean(15); // NOT NULL, so no null to false
DataType dataType = isDataRaw ? DataType.RAW_DATA : DataType.DATA_HASH;
byte[] data = resultSet.getBytes(16);
byte[] metadataHash = resultSet.getBytes(17);
String nameResult = resultSet.getString(18);
String identifierResult = resultSet.getString(19);
Method method = Method.valueOf(resultSet.getInt(20));
byte[] secret = resultSet.getBytes(21);
Compression compression = Compression.valueOf(resultSet.getInt(22));
// FUTURE: get payments from signature if needed. Avoiding for now to reduce database calls.
ArbitraryTransactionData transactionData = new ArbitraryTransactionData(baseTransactionData,
version, serviceInt, nonce, size, nameResult, identifierResult, method, secret,
compression, data, dataType, metadataHash, null);
arbitraryTransactionData.add(transactionData);
} while (resultSet.next());
return arbitraryTransactionData;
} catch (SQLException e) {
throw new DataException("Unable to fetch arbitrary transactions from repository", e);
} catch (Exception e) {
LOGGER.error(e.getMessage(), e);
return new ArrayList<>(0);
}
}
@Override
public List<ArbitraryTransactionData> getLatestArbitraryTransactionsByName( String name ) throws DataException {
String sql = "SELECT type, reference, signature, creator, created_when, fee, " +
"tx_group_id, block_height, approval_status, approval_height, " +
"version, nonce, service, size, is_data_raw, data, metadata_hash, " +
"name, identifier, update_method, secret, compression FROM ArbitraryTransactions " +
"JOIN Transactions USING (signature) " +
"WHERE name = ? " +
"ORDER BY created_when DESC";
List<ArbitraryTransactionData> arbitraryTransactionData = new ArrayList<>();
try (ResultSet resultSet = this.repository.checkedExecute(sql, name)) {
if (resultSet == null)
return new ArrayList<>(0);
do {
byte[] reference = resultSet.getBytes(2);
byte[] signature = resultSet.getBytes(3);
byte[] creatorPublicKey = resultSet.getBytes(4);
long timestamp = resultSet.getLong(5);
Long fee = resultSet.getLong(6);
if (fee == 0 && resultSet.wasNull())
fee = null;
int txGroupId = resultSet.getInt(7);
Integer blockHeight = resultSet.getInt(8);
if (blockHeight == 0 && resultSet.wasNull())
blockHeight = null;
ApprovalStatus approvalStatus = ApprovalStatus.valueOf(resultSet.getInt(9));
Integer approvalHeight = resultSet.getInt(10);
if (approvalHeight == 0 && resultSet.wasNull())
approvalHeight = null;
BaseTransactionData baseTransactionData = new BaseTransactionData(timestamp, txGroupId, reference, creatorPublicKey, fee, approvalStatus, blockHeight, approvalHeight, signature);
int version = resultSet.getInt(11);
int nonce = resultSet.getInt(12);
int serviceInt = resultSet.getInt(13);
int size = resultSet.getInt(14);
boolean isDataRaw = resultSet.getBoolean(15); // NOT NULL, so no null to false
DataType dataType = isDataRaw ? DataType.RAW_DATA : DataType.DATA_HASH;
byte[] data = resultSet.getBytes(16);
byte[] metadataHash = resultSet.getBytes(17);
String nameResult = resultSet.getString(18);
String identifierResult = resultSet.getString(19);
Method method = Method.valueOf(resultSet.getInt(20));
byte[] secret = resultSet.getBytes(21);
Compression compression = Compression.valueOf(resultSet.getInt(22));
// FUTURE: get payments from signature if needed. Avoiding for now to reduce database calls.
ArbitraryTransactionData transactionData = new ArbitraryTransactionData(baseTransactionData,
version, serviceInt, nonce, size, nameResult, identifierResult, method, secret,
compression, data, dataType, metadataHash, null);
arbitraryTransactionData.add(transactionData);
} while (resultSet.next());
return arbitraryTransactionData;
} catch (SQLException e) {
throw new DataException("Unable to fetch arbitrary transactions from repository", e);
} catch (Exception e) {
LOGGER.error(e.getMessage(), e);
return new ArrayList<>(0);
}
}
private ArbitraryTransactionData getSingleTransaction(String name, Service service, Method method, String identifier, boolean firstNotLast) throws DataException { private ArbitraryTransactionData getSingleTransaction(String name, Service service, Method method, String identifier, boolean firstNotLast) throws DataException {
if (name == null || service == null) { if (name == null || service == null) {
// Required fields // Required fields
@ -862,11 +724,12 @@ public class HSQLDBArbitraryRepository implements ArbitraryRepository {
} }
@Override @Override
public List<ArbitraryResourceData> searchArbitraryResources(Service service, String query, String identifier, List<String> names, String title, String description, List<String> keywords, boolean prefixOnly, public List<ArbitraryResourceData> searchArbitraryResources(Service service, String query, String identifier, List<String> names, String title, String description, boolean prefixOnly,
List<String> exactMatchNames, boolean defaultResource, SearchMode mode, Integer minLevel, Boolean followedOnly, Boolean excludeBlocked, List<String> exactMatchNames, boolean defaultResource, SearchMode mode, Integer minLevel, Boolean followedOnly, Boolean excludeBlocked,
Boolean includeMetadata, Boolean includeStatus, Long before, Long after, Integer limit, Integer offset, Boolean reverse) throws DataException { Boolean includeMetadata, Boolean includeStatus, Long before, Long after, Integer limit, Integer offset, Boolean reverse) throws DataException {
if(Settings.getInstance().isDbCacheEnabled()) { if(Settings.getInstance().isDbCacheEnabled()) {
List<ArbitraryResourceData> list List<ArbitraryResourceData> list
= HSQLDBCacheUtils.callCache( = HSQLDBCacheUtils.callCache(
ArbitraryResourceCache.getInstance(), ArbitraryResourceCache.getInstance(),
@ -888,7 +751,6 @@ public class HSQLDBArbitraryRepository implements ArbitraryRepository {
Optional.ofNullable(description), Optional.ofNullable(description),
prefixOnly, prefixOnly,
Optional.ofNullable(exactMatchNames), Optional.ofNullable(exactMatchNames),
Optional.ofNullable(keywords),
defaultResource, defaultResource,
Optional.ofNullable(minLevel), Optional.ofNullable(minLevel),
Optional.ofNullable(() -> ListUtils.followedNames()), Optional.ofNullable(() -> ListUtils.followedNames()),
@ -909,7 +771,6 @@ public class HSQLDBArbitraryRepository implements ArbitraryRepository {
} }
} }
StringBuilder sql = new StringBuilder(512); StringBuilder sql = new StringBuilder(512);
List<Object> bindParams = new ArrayList<>(); List<Object> bindParams = new ArrayList<>();
@ -996,26 +857,6 @@ public class HSQLDBArbitraryRepository implements ArbitraryRepository {
bindParams.add(queryWildcard); bindParams.add(queryWildcard);
} }
if (keywords != null && !keywords.isEmpty()) {
List<String> searchKeywords = new ArrayList<>(keywords);
List<String> conditions = new ArrayList<>();
List<String> bindValues = new ArrayList<>();
for (int i = 0; i < searchKeywords.size(); i++) {
conditions.add("LOWER(description) LIKE ?");
bindValues.add("%" + searchKeywords.get(i).trim().toLowerCase() + "%");
}
String finalCondition = String.join(" OR ", conditions);
sql.append(" AND (").append(finalCondition).append(")");
bindParams.addAll(bindValues);
}
// Handle name searches // Handle name searches
if (names != null && !names.isEmpty()) { if (names != null && !names.isEmpty()) {
sql.append(" AND ("); sql.append(" AND (");

View File

@ -3,24 +3,15 @@ package org.qortal.repository.hsqldb;
import org.apache.logging.log4j.LogManager; import org.apache.logging.log4j.LogManager;
import org.apache.logging.log4j.Logger; import org.apache.logging.log4j.Logger;
import org.qortal.api.SearchMode; import org.qortal.api.SearchMode;
import org.qortal.api.resource.TransactionsResource;
import org.qortal.arbitrary.misc.Category; import org.qortal.arbitrary.misc.Category;
import org.qortal.arbitrary.misc.Service; import org.qortal.arbitrary.misc.Service;
import org.qortal.controller.Controller; import org.qortal.controller.Controller;
import org.qortal.data.account.AccountBalanceData; import org.qortal.data.account.AccountBalanceData;
import org.qortal.data.account.AddressAmountData;
import org.qortal.data.account.BlockHeightRange;
import org.qortal.data.account.BlockHeightRangeAddressAmounts;
import org.qortal.data.arbitrary.ArbitraryResourceCache; import org.qortal.data.arbitrary.ArbitraryResourceCache;
import org.qortal.data.arbitrary.ArbitraryResourceData; import org.qortal.data.arbitrary.ArbitraryResourceData;
import org.qortal.data.arbitrary.ArbitraryResourceMetadata; import org.qortal.data.arbitrary.ArbitraryResourceMetadata;
import org.qortal.data.arbitrary.ArbitraryResourceStatus; import org.qortal.data.arbitrary.ArbitraryResourceStatus;
import org.qortal.data.transaction.TransactionData;
import org.qortal.repository.DataException; 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.ResultSet;
import java.sql.SQLException; import java.sql.SQLException;
@ -37,7 +28,6 @@ import java.util.Optional;
import java.util.Timer; import java.util.Timer;
import java.util.TimerTask; import java.util.TimerTask;
import java.util.concurrent.ConcurrentHashMap; import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.CopyOnWriteArrayList;
import java.util.function.Function; import java.util.function.Function;
import java.util.function.Predicate; import java.util.function.Predicate;
import java.util.function.Supplier; import java.util.function.Supplier;
@ -167,7 +157,6 @@ public class HSQLDBCacheUtils {
Optional<String> description, Optional<String> description,
boolean prefixOnly, boolean prefixOnly,
Optional<List<String>> exactMatchNames, Optional<List<String>> exactMatchNames,
Optional<List<String>> keywords,
boolean defaultResource, boolean defaultResource,
Optional<Integer> minLevel, Optional<Integer> minLevel,
Optional<Supplier<List<String>>> includeOnly, Optional<Supplier<List<String>>> includeOnly,
@ -181,18 +170,7 @@ public class HSQLDBCacheUtils {
Optional<Boolean> reverse) { Optional<Boolean> reverse) {
// retain only candidates with names // retain only candidates with names
Stream<ArbitraryResourceData> stream = candidates.stream().filter(candidate -> candidate.name != null ); Stream<ArbitraryResourceData> stream = candidates.stream().filter(candidate -> candidate.name != null);
if(after.isPresent()) {
stream = stream.filter( candidate -> candidate.created > after.get().longValue() );
}
if(before.isPresent()) {
stream = stream.filter( candidate -> candidate.created < before.get().longValue() );
}
if(exclude.isPresent())
stream = stream.filter( candidate -> !exclude.get().get().contains( candidate.name ));
// filter by service // filter by service
if( service.isPresent() ) if( service.isPresent() )
@ -216,36 +194,6 @@ public class HSQLDBCacheUtils {
stream = filterTerm(title, data -> data.metadata != null ? data.metadata.getTitle() : null, prefixOnly, stream); stream = filterTerm(title, data -> data.metadata != null ? data.metadata.getTitle() : null, prefixOnly, stream);
stream = filterTerm(description, data -> data.metadata != null ? data.metadata.getDescription() : null, prefixOnly, stream); stream = filterTerm(description, data -> data.metadata != null ? data.metadata.getDescription() : null, prefixOnly, stream);
// New: Filter by keywords if provided
if (keywords.isPresent() && !keywords.get().isEmpty()) {
List<String> searchKeywords = keywords.get().stream()
.map(String::toLowerCase)
.collect(Collectors.toList());
stream = stream.filter(candidate -> {
if (candidate.metadata != null && candidate.metadata.getDescription() != null) {
String descriptionLower = candidate.metadata.getDescription().toLowerCase();
return searchKeywords.stream().anyMatch(descriptionLower::contains);
}
return false;
});
}
if (keywords.isPresent() && !keywords.get().isEmpty()) {
List<String> searchKeywords = keywords.get().stream()
.map(String::toLowerCase)
.collect(Collectors.toList());
stream = stream.filter(candidate -> {
if (candidate.metadata != null && candidate.metadata.getDescription() != null) {
String descriptionLower = candidate.metadata.getDescription().toLowerCase();
return searchKeywords.stream().anyMatch(descriptionLower::contains);
}
return false;
});
}
// if exact names is set, retain resources with exact names // if exact names is set, retain resources with exact names
if( exactMatchNames.isPresent() && !exactMatchNames.get().isEmpty()) { if( exactMatchNames.isPresent() && !exactMatchNames.get().isEmpty()) {
@ -301,58 +249,15 @@ public class HSQLDBCacheUtils {
// truncate to limit // truncate to limit
if( limit.isPresent() && limit.get() > 0 ) stream = stream.limit(limit.get()); if( limit.isPresent() && limit.get() > 0 ) stream = stream.limit(limit.get());
List<ArbitraryResourceData> listCopy1 = stream.collect(Collectors.toList()); // include metadata
if( includeMetadata.isEmpty() || !includeMetadata.get() )
stream = stream.peek( candidate -> candidate.metadata = null );
List<ArbitraryResourceData> listCopy2 = new ArrayList<>(listCopy1.size()); // include status
if( includeStatus.isEmpty() || !includeStatus.get() )
stream = stream.peek( candidate -> candidate.status = null);
// remove metadata from the first copy return stream.collect(Collectors.toList());
if( includeMetadata.isEmpty() || !includeMetadata.get() ) {
for( ArbitraryResourceData data : listCopy1 ) {
ArbitraryResourceData copy = new ArbitraryResourceData();
copy.name = data.name;
copy.service = data.service;
copy.identifier = data.identifier;
copy.status = data.status;
copy.metadata = null;
copy.size = data.size;
copy.created = data.created;
copy.updated = data.updated;
listCopy2.add(copy);
}
}
// put the list copy 1 into the second copy
else {
listCopy2.addAll(listCopy1);
}
// remove status from final copy
if( includeStatus.isEmpty() || !includeStatus.get() ) {
List<ArbitraryResourceData> finalCopy = new ArrayList<>(listCopy2.size());
for( ArbitraryResourceData data : listCopy2 ) {
ArbitraryResourceData copy = new ArbitraryResourceData();
copy.name = data.name;
copy.service = data.service;
copy.identifier = data.identifier;
copy.status = null;
copy.metadata = data.metadata;
copy.size = data.size;
copy.created = data.created;
copy.updated = data.updated;
finalCopy.add(copy);
}
return finalCopy;
}
// keep status included by returning the second copy
else {
return listCopy2;
}
} }
/** /**
@ -484,15 +389,14 @@ public class HSQLDBCacheUtils {
/** /**
* Start Recording Balances * Start Recording Balances
* *
* @param balancesByHeight height -> account balances * @param queue the queue to add to, remove oldest data if necssary
* @param balanceDynamics every balance dynamic * @param repository the db repsoitory
* @param priorityRequested the requested thread priority * @param priorityRequested the requested thread priority
* @param frequency the recording frequencies, in minutes * @param frequency the recording frequencies, in minutes
* @param capacity the maximum size of balanceDynamics
*/ */
public static void startRecordingBalances( public static void startRecordingBalances(
final ConcurrentHashMap<Integer, List<AccountBalanceData>> balancesByHeight, final ConcurrentHashMap<Integer, List<AccountBalanceData>> balancesByHeight,
CopyOnWriteArrayList<BlockHeightRangeAddressAmounts> balanceDynamics, final ConcurrentHashMap<String, List<AccountBalanceData>> balancesByAddress,
int priorityRequested, int priorityRequested,
int frequency, int frequency,
int capacity) { int capacity) {
@ -505,38 +409,55 @@ public class HSQLDBCacheUtils {
Thread.currentThread().setName(BALANCE_RECORDER_TIMER_TASK); Thread.currentThread().setName(BALANCE_RECORDER_TIMER_TASK);
int currentHeight = recordCurrentBalances(balancesByHeight); try (final HSQLDBRepository repository = (HSQLDBRepository) Controller.REPOSITORY_FACTORY.getRepository()) {
while (balancesByHeight.size() > capacity + 1) {
Optional<Integer> firstHeight = balancesByHeight.keySet().stream().sorted().findFirst();
LOGGER.debug("recorded balances: height = " + currentHeight); if (firstHeight.isPresent()) balancesByHeight.remove(firstHeight.get());
}
// remove invalidated recordings, recording after current height // get current balances
BalanceRecorderUtils.removeRecordingsAboveHeight(currentHeight, balancesByHeight); List<AccountBalanceData> accountBalances = getAccountBalances(repository);
// remove invalidated dynamics, on or after current height // get anyone of the balances
BalanceRecorderUtils.removeDynamicsOnOrAboveHeight(currentHeight, balanceDynamics); Optional<AccountBalanceData> data = accountBalances.stream().findAny();
// if there are 2 or more recordings, then produce balance dynamics for the first 2 recordings // if there are any balances, then record them
if( balancesByHeight.size() > 1 ) { if (data.isPresent()) {
// map all new balances to the current height
balancesByHeight.put(data.get().getHeight(), accountBalances);
Optional<Integer> priorHeight = BalanceRecorderUtils.getPriorHeight(currentHeight, balancesByHeight); // for each new balance, map to address
for (AccountBalanceData accountBalance : accountBalances) {
// if there is a prior height // get recorded balances for this address
if(priorHeight.isPresent()) { List<AccountBalanceData> establishedBalances
= balancesByAddress.getOrDefault(accountBalance.getAddress(), new ArrayList<>(0));
boolean isRewardDistribution = BalanceRecorderUtils.isRewardDistributionRange(priorHeight.get(), currentHeight); // 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);
// if this range has a reward recording block or if other blocks are enabled for recording // reset tha balances for this address
if( isRewardDistribution || !Settings.getInstance().isRewardRecordingOnly() ) { balancesByAddress.put(accountBalance.getAddress(), balances);
produceBalanceDynamics(currentHeight, priorHeight, isRewardDistribution, balancesByHeight, balanceDynamics, capacity);
// 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);
} }
} }
else { } catch (DataException e) {
LOGGER.warn("Expecting prior height and nothing was discovered, current height = " + currentHeight); LOGGER.error(e.getMessage(), e);
}
}
// else this should be the first recording
else {
LOGGER.info("first balance recording completed");
} }
} }
}; };
@ -545,98 +466,6 @@ public class HSQLDBCacheUtils {
timer.scheduleAtFixedRate(task, 300_000, frequency * 60_000); 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);
// get anyone of the balances
Optional<AccountBalanceData> data = accountBalances.stream().findAny();
// if there are any balances, then record them
if (data.isPresent()) {
// map all new balances to the current height
balancesByHeight.put(data.get().getHeight(), accountBalances);
currentHeight = data.get().getHeight();
}
else {
currentHeight = Integer.MAX_VALUE;
}
} catch (DataException e) {
LOGGER.error(e.getMessage(), e);
currentHeight = Integer.MAX_VALUE;
}
return currentHeight;
}
/** /**
* Build Timer * Build Timer
* *

View File

@ -454,41 +454,41 @@ public class HSQLDBDatabaseUpdates {
case 12: case 12:
// Groups // Groups
// NOTE: We need to set Groups to `GROUPS` here to avoid SQL Standard Keywords in HSQLDB v2.7.4 // 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, " 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, " + "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, " + "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, " + "reference Signature, creation_group_id GroupID, reduced_group_name GroupName NOT NULL, "
+ "description GenericDescription NOT NULL, PRIMARY KEY (group_id))"); + "description GenericDescription NOT NULL, PRIMARY KEY (group_id))");
// For finding groups by name // 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 // 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 // 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 // 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) " + "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 // Admins
stmt.execute("CREATE TABLE GroupAdmins (group_id GroupID, admin QortalAddress, reference Signature NOT NULL, " 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 // For finding groups by admin address
stmt.execute("CREATE INDEX GroupAdminIndex ON GroupAdmins (admin)"); stmt.execute("CREATE INDEX GroupAdminIndex ON GroupAdmins (admin)");
// Members // Members
stmt.execute("CREATE TABLE GroupMembers (group_id GroupID, address QortalAddress, " stmt.execute("CREATE TABLE GroupMembers (group_id GroupID, address QortalAddress, "
+ "joined_when EpochMillis NOT NULL, reference Signature NOT NULL, " + "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 // For finding groups by member address
stmt.execute("CREATE INDEX GroupMemberIndex ON GroupMembers (address)"); stmt.execute("CREATE INDEX GroupMemberIndex ON GroupMembers (address)");
// Invites // Invites
stmt.execute("CREATE TABLE GroupInvites (group_id GroupID, inviter QortalAddress, invitee QortalAddress, " stmt.execute("CREATE TABLE GroupInvites (group_id GroupID, inviter QortalAddress, invitee QortalAddress, "
+ "expires_when EpochMillis, reference Signature, " + "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 // For finding invites sent by inviter
stmt.execute("CREATE INDEX GroupInviteInviterIndex ON GroupInvites (inviter)"); stmt.execute("CREATE INDEX GroupInviteInviterIndex ON GroupInvites (inviter)");
// For finding invites by group // For finding invites by group
@ -504,7 +504,7 @@ public class HSQLDBDatabaseUpdates {
// NULL expires_when means does not expire! // NULL expires_when means does not expire!
stmt.execute("CREATE TABLE GroupBans (group_id GroupID, offender QortalAddress, admin QortalAddress NOT NULL, " 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, " + "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 // For expiry maintenance
stmt.execute("CREATE INDEX GroupBanExpiryIndex ON GroupBans (expires_when)"); stmt.execute("CREATE INDEX GroupBanExpiryIndex ON GroupBans (expires_when)");
break; break;

View File

@ -350,24 +350,9 @@ public class HSQLDBGroupRepository implements GroupRepository {
// Group Admins // Group Admins
@Override
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;
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 @Override
public GroupAdminData getAdmin(int groupId, String address) throws DataException { 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)) { try (ResultSet resultSet = this.repository.checkedExecute("SELECT admin, reference FROM GroupAdmins WHERE group_id = ?", groupId)) {
if (resultSet == null) if (resultSet == null)
return null; return null;

View File

@ -5,8 +5,6 @@ import org.apache.logging.log4j.Logger;
import org.hsqldb.HsqlException; import org.hsqldb.HsqlException;
import org.hsqldb.error.ErrorCode; import org.hsqldb.error.ErrorCode;
import org.hsqldb.jdbc.HSQLDBPool; 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.DataException;
import org.qortal.repository.Repository; import org.qortal.repository.Repository;
import org.qortal.repository.RepositoryFactory; import org.qortal.repository.RepositoryFactory;
@ -16,8 +14,6 @@ import java.sql.Connection;
import java.sql.DriverManager; import java.sql.DriverManager;
import java.sql.SQLException; import java.sql.SQLException;
import java.sql.Statement; import java.sql.Statement;
import java.util.ArrayList;
import java.util.List;
import java.util.Properties; import java.util.Properties;
public class HSQLDBRepositoryFactory implements RepositoryFactory { public class HSQLDBRepositoryFactory implements RepositoryFactory {
@ -61,13 +57,7 @@ public class HSQLDBRepositoryFactory implements RepositoryFactory {
HSQLDBRepository.attemptRecovery(connectionUrl, "backup"); HSQLDBRepository.attemptRecovery(connectionUrl, "backup");
} }
if(Settings.getInstance().isConnectionPoolMonitorEnabled()) { this.connectionPool = new HSQLDBPool(Settings.getInstance().getRepositoryConnectionPoolSize());
this.connectionPool = new HSQLDBPoolMonitored(Settings.getInstance().getRepositoryConnectionPoolSize());
}
else {
this.connectionPool = new HSQLDBPool(Settings.getInstance().getRepositoryConnectionPoolSize());
}
this.connectionPool.setUrl(this.connectionUrl); this.connectionPool.setUrl(this.connectionUrl);
Properties properties = new Properties(); Properties properties = new Properties();
@ -163,19 +153,4 @@ public class HSQLDBRepositoryFactory implements RepositoryFactory {
return HSQLDBRepository.isDeadlockException(e); 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);
}
}
} }

View File

@ -386,7 +386,7 @@ public class Settings {
/** /**
* DB Cache Enabled? * DB Cache Enabled?
*/ */
private boolean dbCacheEnabled = true; private boolean dbCacheEnabled = false;
/** /**
* DB Cache Thread Priority * DB Cache Thread Priority
@ -444,107 +444,14 @@ public class Settings {
*/ */
private long archivingPause = 3000; private long archivingPause = 3000;
/**
* Enable Balance Recorder?
*
* True for balance recording, otherwise false.
*/
private boolean balanceRecorderEnabled = false; private boolean balanceRecorderEnabled = false;
/**
* Balance Recorder Priority
*
* The thread priority (1 is lowest, 10 is highest) of the balance recorder thread, if enabled.
*/
private int balanceRecorderPriority = 1; private int balanceRecorderPriority = 1;
/** private int balanceRecorderFrequency = 2*60*1000;
* Balance Recorder Frequency
*
* How often the balances will be recorded, if enabled, measured in minutes.
*/
private int balanceRecorderFrequency = 20;
/**
* Balance Recorder Capacity
*
* The number of balance recorder ranges will be held in memory.
*/
private int balanceRecorderCapacity = 1000; private int balanceRecorderCapacity = 1000;
/**
* Minimum Balance Recording
*
* The minimum recored balance change in Qortoshis (1/100000000 QORT)
*/
private long minimumBalanceRecording = 100000000;
/**
* Top Balance Logging Limit
*
* When logging the number limit of top balance changes to show in the logs for any given block range.
*/
private long topBalanceLoggingLimit = 100;
/**
* Balance Recorder Rollback Allowance
*
* If the balance recorder is enabled, it must protect its prior balances by this number of blocks in case of
* a blockchain rollback and reorganization.
*/
private int balanceRecorderRollbackAllowance = 100;
/**
* 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;
/**
* Buiild Arbitrary Resources Batch Size
*
* The number resources to batch per iteration when rebuilding.
*/
private int buildArbitraryResourcesBatchSize = 200;
/**
* Arbitrary Indexing Priority
*
* The thread priority when indexing arbirary resources.
*/
private int arbitraryIndexingPriority = 5;
/**
* Arbitrary Indexing Frequency (In Minutes)
*
* The frequency at which the arbitrary indices are cached.
*/
private int arbitraryIndexingFrequency = 10;
private boolean rebuildArbitraryResourceCacheTaskEnabled = false;
/**
* Rebuild Arbitrary Resource Cache Task Delay (In Minutes)
*
* Waiting period before the first rebuild task is started.
*/
private int rebuildArbitraryResourceCacheTaskDelay = 300;
/**
* Rebuild Arbitrary Resource Cache Task Period (In Hours)
*
* The frequency the arbitrary resource cache is rebuilt.
*/
private int rebuildArbitraryResourceCacheTaskPeriod = 24;
// Domain mapping // Domain mapping
public static class ThreadLimit { public static class ThreadLimit {
private String messageType; private String messageType;
@ -1350,48 +1257,4 @@ public class Settings {
public boolean isBalanceRecorderEnabled() { public boolean isBalanceRecorderEnabled() {
return balanceRecorderEnabled; return balanceRecorderEnabled;
} }
public long getMinimumBalanceRecording() {
return minimumBalanceRecording;
}
public long getTopBalanceLoggingLimit() {
return topBalanceLoggingLimit;
}
public int getBalanceRecorderRollbackAllowance() {
return balanceRecorderRollbackAllowance;
}
public boolean isRewardRecordingOnly() {
return rewardRecordingOnly;
}
public boolean isConnectionPoolMonitorEnabled() {
return connectionPoolMonitorEnabled;
}
public int getBuildArbitraryResourcesBatchSize() {
return buildArbitraryResourcesBatchSize;
}
public int getArbitraryIndexingPriority() {
return arbitraryIndexingPriority;
}
public int getArbitraryIndexingFrequency() {
return arbitraryIndexingFrequency;
}
public boolean isRebuildArbitraryResourceCacheTaskEnabled() {
return rebuildArbitraryResourceCacheTaskEnabled;
}
public int getRebuildArbitraryResourceCacheTaskDelay() {
return rebuildArbitraryResourceCacheTaskDelay;
}
public int getRebuildArbitraryResourceCacheTaskPeriod() {
return rebuildArbitraryResourceCacheTaskPeriod;
}
} }

View File

@ -9,7 +9,6 @@ import org.qortal.arbitrary.metadata.ArbitraryDataTransactionMetadata;
import org.qortal.arbitrary.misc.Service; import org.qortal.arbitrary.misc.Service;
import org.qortal.block.BlockChain; import org.qortal.block.BlockChain;
import org.qortal.controller.arbitrary.ArbitraryDataManager; import org.qortal.controller.arbitrary.ArbitraryDataManager;
import org.qortal.controller.arbitrary.ArbitraryTransactionDataHashWrapper;
import org.qortal.controller.repository.NamesDatabaseIntegrityCheck; import org.qortal.controller.repository.NamesDatabaseIntegrityCheck;
import org.qortal.crypto.Crypto; import org.qortal.crypto.Crypto;
import org.qortal.crypto.MemoryPoW; import org.qortal.crypto.MemoryPoW;
@ -32,12 +31,8 @@ import org.qortal.utils.ArbitraryTransactionUtils;
import org.qortal.utils.NTP; import org.qortal.utils.NTP;
import java.io.IOException; import java.io.IOException;
import java.util.HashMap;
import java.util.HashSet;
import java.util.List; import java.util.List;
import java.util.Map;
import java.util.Objects; import java.util.Objects;
import java.util.Set;
import java.util.stream.Collectors; import java.util.stream.Collectors;
public class ArbitraryTransaction extends Transaction { public class ArbitraryTransaction extends Transaction {
@ -308,13 +303,8 @@ public class ArbitraryTransaction extends Transaction {
// Add/update arbitrary resource caches, but don't update the status as this involves time-consuming // Add/update arbitrary resource caches, but don't update the status as this involves time-consuming
// disk reads, and is more prone to failure. The status will be updated on metadata retrieval, or when // disk reads, and is more prone to failure. The status will be updated on metadata retrieval, or when
// accessing the resource. // accessing the resource.
// Also, must add this transaction as a latest transaction, since the it has not been saved to the this.updateArbitraryResourceCache(repository);
// repository yet. this.updateArbitraryMetadataCache(repository);
this.updateArbitraryResourceCacheIncludingMetadata(
repository,
Set.of(new ArbitraryTransactionDataHashWrapper(arbitraryTransactionData)),
new HashMap<>(0)
);
repository.saveChanges(); repository.saveChanges();
@ -370,10 +360,7 @@ public class ArbitraryTransaction extends Transaction {
* *
* @throws DataException * @throws DataException
*/ */
public void updateArbitraryResourceCacheIncludingMetadata( public void updateArbitraryResourceCache(Repository repository) throws DataException {
Repository repository,
Set<ArbitraryTransactionDataHashWrapper> latestTransactionWrappers,
Map<ArbitraryTransactionDataHashWrapper, ArbitraryResourceData> resourceByWrapper) throws DataException {
// Don't cache resources without a name (such as auto updates) // Don't cache resources without a name (such as auto updates)
if (arbitraryTransactionData.getName() == null) { if (arbitraryTransactionData.getName() == null) {
return; return;
@ -398,33 +385,17 @@ public class ArbitraryTransaction extends Transaction {
arbitraryResourceData.name = name; arbitraryResourceData.name = name;
arbitraryResourceData.identifier = identifier; arbitraryResourceData.identifier = identifier;
final ArbitraryTransactionDataHashWrapper wrapper = new ArbitraryTransactionDataHashWrapper(arbitraryTransactionData); // Get the latest transaction
ArbitraryTransactionData latestTransactionData = repository.getArbitraryRepository().getLatestTransaction(arbitraryTransactionData.getName(), arbitraryTransactionData.getService(), null, arbitraryTransactionData.getIdentifier());
if (latestTransactionData == null) {
// We don't have a latest transaction, so delete from cache
repository.getArbitraryRepository().delete(arbitraryResourceData);
return;
}
ArbitraryTransactionData latestTransactionData; // Get existing cached entry if it exists
if( latestTransactionWrappers.contains(wrapper)) { ArbitraryResourceData existingArbitraryResourceData = repository.getArbitraryRepository()
latestTransactionData .getArbitraryResource(service, name, identifier);
= latestTransactionWrappers.stream()
.filter( latestWrapper -> latestWrapper.equals(wrapper))
.findAny().get()
.getData();
}
else {
// Get the latest transaction
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;
}
}
ArbitraryResourceData existingArbitraryResourceData = resourceByWrapper.get(wrapper);
if( existingArbitraryResourceData == null ) {
// Get existing cached entry if it exists
existingArbitraryResourceData = repository.getArbitraryRepository()
.getArbitraryResource(service, name, identifier);
}
// Check for existing cached data // Check for existing cached data
if (existingArbitraryResourceData == null) { if (existingArbitraryResourceData == null) {
@ -433,7 +404,6 @@ public class ArbitraryTransaction extends Transaction {
arbitraryResourceData.updated = null; arbitraryResourceData.updated = null;
} }
else { else {
resourceByWrapper.put(wrapper, existingArbitraryResourceData);
// An entry already exists - update created time from current transaction if this is older // An entry already exists - update created time from current transaction if this is older
arbitraryResourceData.created = Math.min(existingArbitraryResourceData.created, arbitraryTransactionData.getTimestamp()); arbitraryResourceData.created = Math.min(existingArbitraryResourceData.created, arbitraryTransactionData.getTimestamp());
@ -451,34 +421,6 @@ public class ArbitraryTransaction extends Transaction {
// Save // Save
repository.getArbitraryRepository().save(arbitraryResourceData); repository.getArbitraryRepository().save(arbitraryResourceData);
// Update metadata for latest transaction if it is local
if (latestTransactionData.getMetadataHash() != null) {
ArbitraryDataFile metadataFile = ArbitraryDataFile.fromHash(latestTransactionData.getMetadataHash(), latestTransactionData.getSignature());
if (metadataFile.exists()) {
ArbitraryDataTransactionMetadata transactionMetadata = new ArbitraryDataTransactionMetadata(metadataFile.getFilePath());
try {
transactionMetadata.read();
ArbitraryResourceMetadata metadata = new ArbitraryResourceMetadata();
metadata.setArbitraryResourceData(arbitraryResourceData);
metadata.setTitle(transactionMetadata.getTitle());
metadata.setDescription(transactionMetadata.getDescription());
metadata.setCategory(transactionMetadata.getCategory());
metadata.setTags(transactionMetadata.getTags());
repository.getArbitraryRepository().save(metadata);
} catch (IOException e) {
// Ignore, as we can add it again later
}
} else {
// We don't have a local copy of this metadata file, so delete it from the cache
// It will be re-added if the file later arrives via the network
ArbitraryResourceMetadata metadata = new ArbitraryResourceMetadata();
metadata.setArbitraryResourceData(arbitraryResourceData);
repository.getArbitraryRepository().delete(metadata);
}
}
} }
public void updateArbitraryResourceStatus(Repository repository) throws DataException { public void updateArbitraryResourceStatus(Repository repository) throws DataException {
@ -513,4 +455,60 @@ public class ArbitraryTransaction extends Transaction {
repository.getArbitraryRepository().setStatus(arbitraryResourceData, status); repository.getArbitraryRepository().setStatus(arbitraryResourceData, status);
} }
public void updateArbitraryMetadataCache(Repository repository) throws DataException {
// Get the latest transaction
ArbitraryTransactionData latestTransactionData = repository.getArbitraryRepository().getLatestTransaction(arbitraryTransactionData.getName(), arbitraryTransactionData.getService(), null, arbitraryTransactionData.getIdentifier());
if (latestTransactionData == null) {
// We don't have a latest transaction, so give up
return;
}
Service service = latestTransactionData.getService();
String name = latestTransactionData.getName();
String identifier = latestTransactionData.getIdentifier();
if (service == null) {
// Unsupported service - ignore this resource
return;
}
// In the cache we store null identifiers as "default", as it is part of the primary key
if (identifier == null) {
identifier = "default";
}
ArbitraryResourceData arbitraryResourceData = new ArbitraryResourceData();
arbitraryResourceData.service = service;
arbitraryResourceData.name = name;
arbitraryResourceData.identifier = identifier;
// Update metadata for latest transaction if it is local
if (latestTransactionData.getMetadataHash() != null) {
ArbitraryDataFile metadataFile = ArbitraryDataFile.fromHash(latestTransactionData.getMetadataHash(), latestTransactionData.getSignature());
if (metadataFile.exists()) {
ArbitraryDataTransactionMetadata transactionMetadata = new ArbitraryDataTransactionMetadata(metadataFile.getFilePath());
try {
transactionMetadata.read();
ArbitraryResourceMetadata metadata = new ArbitraryResourceMetadata();
metadata.setArbitraryResourceData(arbitraryResourceData);
metadata.setTitle(transactionMetadata.getTitle());
metadata.setDescription(transactionMetadata.getDescription());
metadata.setCategory(transactionMetadata.getCategory());
metadata.setTags(transactionMetadata.getTags());
repository.getArbitraryRepository().save(metadata);
} catch (IOException e) {
// Ignore, as we can add it again later
}
} else {
// We don't have a local copy of this metadata file, so delete it from the cache
// It will be re-added if the file later arrives via the network
ArbitraryResourceMetadata metadata = new ArbitraryResourceMetadata();
metadata.setArbitraryResourceData(arbitraryResourceData);
repository.getArbitraryRepository().delete(metadata);
}
}
}
} }

View File

@ -2,7 +2,6 @@ package org.qortal.transaction;
import org.qortal.account.Account; import org.qortal.account.Account;
import org.qortal.asset.Asset; import org.qortal.asset.Asset;
import org.qortal.block.BlockChain;
import org.qortal.crypto.Crypto; import org.qortal.crypto.Crypto;
import org.qortal.data.group.GroupData; import org.qortal.data.group.GroupData;
import org.qortal.data.transaction.CancelGroupBanTransactionData; import org.qortal.data.transaction.CancelGroupBanTransactionData;
@ -13,7 +12,6 @@ import org.qortal.repository.Repository;
import java.util.Collections; import java.util.Collections;
import java.util.List; import java.util.List;
import java.util.Objects;
public class CancelGroupBanTransaction extends Transaction { public class CancelGroupBanTransaction extends Transaction {
@ -72,26 +70,9 @@ public class CancelGroupBanTransaction extends Transaction {
if (!this.repository.getGroupRepository().adminExists(groupId, admin.getAddress())) if (!this.repository.getGroupRepository().adminExists(groupId, admin.getAddress()))
return ValidationResult.NOT_GROUP_ADMIN; return ValidationResult.NOT_GROUP_ADMIN;
if( this.repository.getBlockRepository().getBlockchainHeight() < BlockChain.getInstance().getNullGroupMembershipHeight() ) { // Can't unban if not group's current owner
// Can't cancel ban if not group's current owner if (!admin.getAddress().equals(groupData.getOwner()))
if (!admin.getAddress().equals(groupData.getOwner())) return ValidationResult.INVALID_GROUP_OWNER;
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(); Account member = getMember();

View File

@ -2,7 +2,6 @@ package org.qortal.transaction;
import org.qortal.account.Account; import org.qortal.account.Account;
import org.qortal.asset.Asset; import org.qortal.asset.Asset;
import org.qortal.block.BlockChain;
import org.qortal.crypto.Crypto; import org.qortal.crypto.Crypto;
import org.qortal.data.group.GroupData; import org.qortal.data.group.GroupData;
import org.qortal.data.transaction.CancelGroupInviteTransactionData; import org.qortal.data.transaction.CancelGroupInviteTransactionData;
@ -13,7 +12,6 @@ import org.qortal.repository.Repository;
import java.util.Collections; import java.util.Collections;
import java.util.List; import java.util.List;
import java.util.Objects;
public class CancelGroupInviteTransaction extends Transaction { public class CancelGroupInviteTransaction extends Transaction {
@ -82,16 +80,6 @@ public class CancelGroupInviteTransaction extends Transaction {
if (admin.getConfirmedBalance(Asset.QORT) < this.cancelGroupInviteTransactionData.getFee()) if (admin.getConfirmedBalance(Asset.QORT) < this.cancelGroupInviteTransactionData.getFee())
return ValidationResult.NO_BALANCE; 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; return ValidationResult.OK;
} }

View File

@ -22,23 +22,28 @@ public class CancelSellNameTransaction extends Transaction {
private CancelSellNameTransactionData cancelSellNameTransactionData; private CancelSellNameTransactionData cancelSellNameTransactionData;
// Constructors // Constructors
public CancelSellNameTransaction(Repository repository, TransactionData transactionData) { public CancelSellNameTransaction(Repository repository, TransactionData transactionData) {
super(repository, transactionData); super(repository, transactionData);
this.cancelSellNameTransactionData = (CancelSellNameTransactionData) this.transactionData; this.cancelSellNameTransactionData = (CancelSellNameTransactionData) this.transactionData;
} }
// More information // More information
@Override @Override
public List<String> getRecipientAddresses() throws DataException { public List<String> getRecipientAddresses() throws DataException {
return Collections.emptyList(); // No recipient address for this transaction return Collections.emptyList();
} }
// Navigation // Navigation
public Account getOwner() { public Account getOwner() {
return this.getCreator(); // The creator of the transaction is the owner return this.getCreator();
} }
// Processing // Processing
@Override @Override
public ValidationResult isValid() throws DataException { public ValidationResult isValid() throws DataException {
String name = this.cancelSellNameTransactionData.getName(); String name = this.cancelSellNameTransactionData.getName();
@ -52,56 +57,61 @@ public class CancelSellNameTransaction extends Transaction {
if (!name.equals(Unicode.normalize(name))) if (!name.equals(Unicode.normalize(name)))
return ValidationResult.NAME_NOT_NORMALIZED; return ValidationResult.NAME_NOT_NORMALIZED;
// Retrieve name data from repository
NameData nameData = this.repository.getNameRepository().fromName(name); NameData nameData = this.repository.getNameRepository().fromName(name);
// Check if name exists // Check name exists
if (nameData == null) if (nameData == null)
return ValidationResult.NAME_DOES_NOT_EXIST; return ValidationResult.NAME_DOES_NOT_EXIST;
// Check name is currently for sale // Check name is currently for sale
if (!nameData.isForSale()) { if (!nameData.isForSale()) {
// Validate after feature-trigger timestamp, due to potential double cancellations // Only validate after feature-trigger timestamp, due to a small number of double cancelations in the chain history
if (this.cancelSellNameTransactionData.getTimestamp() > BlockChain.getInstance().getCancelSellNameValidationTimestamp()) if (this.cancelSellNameTransactionData.getTimestamp() > BlockChain.getInstance().getCancelSellNameValidationTimestamp())
return ValidationResult.NAME_NOT_FOR_SALE; return ValidationResult.NAME_NOT_FOR_SALE;
} }
// Check if transaction creator matches the name's current owner // Check transaction creator matches name's current owner
Account owner = getOwner(); Account owner = getOwner();
if (!owner.getAddress().equals(nameData.getOwner())) if (!owner.getAddress().equals(nameData.getOwner()))
return ValidationResult.INVALID_NAME_OWNER; return ValidationResult.INVALID_NAME_OWNER;
// Check if issuer has enough balance for the transaction fee // Check issuer has enough funds
if (owner.getConfirmedBalance(Asset.QORT) < cancelSellNameTransactionData.getFee()) if (owner.getConfirmedBalance(Asset.QORT) < cancelSellNameTransactionData.getFee())
return ValidationResult.NO_BALANCE; return ValidationResult.NO_BALANCE;
return ValidationResult.OK; // All validations passed return ValidationResult.OK;
} }
@Override @Override
public void preProcess() throws DataException { public void preProcess() throws DataException {
// Direct access to class field, no need to redeclare CancelSellNameTransactionData cancelSellNameTransactionData = (CancelSellNameTransactionData) transactionData;
// Rebuild this name in the Names table from the transaction history
// This is necessary because in some rare cases names can be missing from the Names table after registration
// but we have been unable to reproduce the issue and track down the root cause
NamesDatabaseIntegrityCheck namesDatabaseIntegrityCheck = new NamesDatabaseIntegrityCheck(); NamesDatabaseIntegrityCheck namesDatabaseIntegrityCheck = new NamesDatabaseIntegrityCheck();
namesDatabaseIntegrityCheck.rebuildName(this.cancelSellNameTransactionData.getName(), this.repository); namesDatabaseIntegrityCheck.rebuildName(cancelSellNameTransactionData.getName(), this.repository);
} }
@Override @Override
public void process() throws DataException { public void process() throws DataException {
// Update the Name to reflect the cancellation of the sale // Update Name
Name name = new Name(this.repository, cancelSellNameTransactionData.getName()); Name name = new Name(this.repository, cancelSellNameTransactionData.getName());
name.cancelSell(cancelSellNameTransactionData); name.cancelSell(cancelSellNameTransactionData);
// Save this transaction with updated "name reference" // Save this transaction, with updated "name reference" to previous transaction that updated name
this.repository.getTransactionRepository().save(cancelSellNameTransactionData); this.repository.getTransactionRepository().save(cancelSellNameTransactionData);
} }
@Override @Override
public void orphan() throws DataException { public void orphan() throws DataException {
// Revert the cancellation of the name sale // Revert name
Name name = new Name(this.repository, cancelSellNameTransactionData.getName()); Name name = new Name(this.repository, cancelSellNameTransactionData.getName());
name.uncancelSell(cancelSellNameTransactionData); name.uncancelSell(cancelSellNameTransactionData);
// Save the transaction with the reverted "name reference" // Save this transaction, with removed "name reference"
this.repository.getTransactionRepository().save(cancelSellNameTransactionData); this.repository.getTransactionRepository().save(cancelSellNameTransactionData);
} }
} }

View File

@ -2,7 +2,6 @@ package org.qortal.transaction;
import org.qortal.account.Account; import org.qortal.account.Account;
import org.qortal.asset.Asset; import org.qortal.asset.Asset;
import org.qortal.block.BlockChain;
import org.qortal.crypto.Crypto; import org.qortal.crypto.Crypto;
import org.qortal.data.group.GroupData; import org.qortal.data.group.GroupData;
import org.qortal.data.transaction.GroupBanTransactionData; import org.qortal.data.transaction.GroupBanTransactionData;
@ -13,7 +12,6 @@ import org.qortal.repository.Repository;
import java.util.Collections; import java.util.Collections;
import java.util.List; import java.util.List;
import java.util.Objects;
public class GroupBanTransaction extends Transaction { public class GroupBanTransaction extends Transaction {
@ -72,25 +70,9 @@ public class GroupBanTransaction extends Transaction {
if (!this.repository.getGroupRepository().adminExists(groupId, admin.getAddress())) if (!this.repository.getGroupRepository().adminExists(groupId, admin.getAddress()))
return ValidationResult.NOT_GROUP_ADMIN; return ValidationResult.NOT_GROUP_ADMIN;
if( this.repository.getBlockRepository().getBlockchainHeight() < BlockChain.getInstance().getNullGroupMembershipHeight() ) { // Can't ban if not group's current owner
// Can't ban if not group's current owner if (!admin.getAddress().equals(groupData.getOwner()))
if (!admin.getAddress().equals(groupData.getOwner())) return ValidationResult.INVALID_GROUP_OWNER;
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(); Account offender = getOffender();

View File

@ -2,7 +2,6 @@ package org.qortal.transaction;
import org.qortal.account.Account; import org.qortal.account.Account;
import org.qortal.asset.Asset; import org.qortal.asset.Asset;
import org.qortal.block.BlockChain;
import org.qortal.crypto.Crypto; import org.qortal.crypto.Crypto;
import org.qortal.data.transaction.GroupInviteTransactionData; import org.qortal.data.transaction.GroupInviteTransactionData;
import org.qortal.data.transaction.TransactionData; import org.qortal.data.transaction.TransactionData;
@ -12,7 +11,6 @@ import org.qortal.repository.Repository;
import java.util.Collections; import java.util.Collections;
import java.util.List; import java.util.List;
import java.util.Objects;
public class GroupInviteTransaction extends Transaction { public class GroupInviteTransaction extends Transaction {
@ -87,16 +85,6 @@ public class GroupInviteTransaction extends Transaction {
if (admin.getConfirmedBalance(Asset.QORT) < this.groupInviteTransactionData.getFee()) if (admin.getConfirmedBalance(Asset.QORT) < this.groupInviteTransactionData.getFee())
return ValidationResult.NO_BALANCE; 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; return ValidationResult.OK;
} }

View File

@ -3,7 +3,6 @@ package org.qortal.transaction;
import org.qortal.account.Account; import org.qortal.account.Account;
import org.qortal.account.PublicKeyAccount; import org.qortal.account.PublicKeyAccount;
import org.qortal.asset.Asset; import org.qortal.asset.Asset;
import org.qortal.block.BlockChain;
import org.qortal.crypto.Crypto; import org.qortal.crypto.Crypto;
import org.qortal.data.group.GroupData; import org.qortal.data.group.GroupData;
import org.qortal.data.transaction.GroupKickTransactionData; import org.qortal.data.transaction.GroupKickTransactionData;
@ -15,7 +14,6 @@ import org.qortal.repository.Repository;
import java.util.Collections; import java.util.Collections;
import java.util.List; import java.util.List;
import java.util.Objects;
public class GroupKickTransaction extends Transaction { public class GroupKickTransaction extends Transaction {
@ -84,26 +82,9 @@ public class GroupKickTransaction extends Transaction {
if (!admin.getAddress().equals(groupData.getOwner()) && groupRepository.adminExists(groupId, member.getAddress())) if (!admin.getAddress().equals(groupData.getOwner()) && groupRepository.adminExists(groupId, member.getAddress()))
return ValidationResult.INVALID_GROUP_OWNER; return ValidationResult.INVALID_GROUP_OWNER;
if( this.repository.getBlockRepository().getBlockchainHeight() < BlockChain.getInstance().getNullGroupMembershipHeight() ) { // Can't kick if not group's current owner
// Can't kick if not group's current owner if (!admin.getAddress().equals(groupData.getOwner()))
if (!admin.getAddress().equals(groupData.getOwner())) return ValidationResult.INVALID_GROUP_OWNER;
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 // Check creator has enough funds
if (admin.getConfirmedBalance(Asset.QORT) < this.groupKickTransactionData.getFee()) if (admin.getConfirmedBalance(Asset.QORT) < this.groupKickTransactionData.getFee())

View File

@ -24,23 +24,28 @@ public class SellNameTransaction extends Transaction {
private SellNameTransactionData sellNameTransactionData; private SellNameTransactionData sellNameTransactionData;
// Constructors // Constructors
public SellNameTransaction(Repository repository, TransactionData transactionData) { public SellNameTransaction(Repository repository, TransactionData transactionData) {
super(repository, transactionData); super(repository, transactionData);
this.sellNameTransactionData = (SellNameTransactionData) this.transactionData; this.sellNameTransactionData = (SellNameTransactionData) this.transactionData;
} }
// More information // More information
@Override @Override
public List<String> getRecipientAddresses() throws DataException { public List<String> getRecipientAddresses() throws DataException {
return Collections.emptyList(); // No direct recipient address for this transaction return Collections.emptyList();
} }
// Navigation // Navigation
public Account getOwner() { public Account getOwner() {
return this.getCreator(); // Owner is the creator of the transaction return this.getCreator();
} }
// Processing // Processing
@Override @Override
public ValidationResult isValid() throws DataException { public ValidationResult isValid() throws DataException {
String name = this.sellNameTransactionData.getName(); String name = this.sellNameTransactionData.getName();
@ -54,54 +59,59 @@ public class SellNameTransaction extends Transaction {
if (!name.equals(Unicode.normalize(name))) if (!name.equals(Unicode.normalize(name)))
return ValidationResult.NAME_NOT_NORMALIZED; return ValidationResult.NAME_NOT_NORMALIZED;
// Retrieve name data from repository
NameData nameData = this.repository.getNameRepository().fromName(name); NameData nameData = this.repository.getNameRepository().fromName(name);
// Check if name exists // Check name exists
if (nameData == null) if (nameData == null)
return ValidationResult.NAME_DOES_NOT_EXIST; return ValidationResult.NAME_DOES_NOT_EXIST;
// Check name is not already for sale // Check name isn't currently for sale
if (nameData.isForSale()) if (nameData.isForSale())
return ValidationResult.NAME_ALREADY_FOR_SALE; return ValidationResult.NAME_ALREADY_FOR_SALE;
// Validate transaction's public key matches name's current owner // Check transaction's public key matches name's current owner
Account owner = getOwner(); Account owner = getOwner();
if (!owner.getAddress().equals(nameData.getOwner())) if (!owner.getAddress().equals(nameData.getOwner()))
return ValidationResult.INVALID_NAME_OWNER; return ValidationResult.INVALID_NAME_OWNER;
// Check amount is positive and within valid range // Check amount is positive
long amount = this.sellNameTransactionData.getAmount(); if (this.sellNameTransactionData.getAmount() <= 0)
if (amount <= 0)
return ValidationResult.NEGATIVE_AMOUNT; return ValidationResult.NEGATIVE_AMOUNT;
if (amount >= MAX_AMOUNT)
// Check amount within bounds
if (this.sellNameTransactionData.getAmount() >= MAX_AMOUNT)
return ValidationResult.INVALID_AMOUNT; return ValidationResult.INVALID_AMOUNT;
// Check if owner has enough balance for the transaction fee // Check issuer has enough funds
if (owner.getConfirmedBalance(Asset.QORT) < this.sellNameTransactionData.getFee()) if (owner.getConfirmedBalance(Asset.QORT) < this.sellNameTransactionData.getFee())
return ValidationResult.NO_BALANCE; return ValidationResult.NO_BALANCE;
return ValidationResult.OK; // All validation checks passed return ValidationResult.OK;
} }
@Override @Override
public void preProcess() throws DataException { public void preProcess() throws DataException {
// Directly access class field rather than local variable for clarity SellNameTransactionData sellNameTransactionData = (SellNameTransactionData) transactionData;
// Rebuild this name in the Names table from the transaction history
// This is necessary because in some rare cases names can be missing from the Names table after registration
// but we have been unable to reproduce the issue and track down the root cause
NamesDatabaseIntegrityCheck namesDatabaseIntegrityCheck = new NamesDatabaseIntegrityCheck(); NamesDatabaseIntegrityCheck namesDatabaseIntegrityCheck = new NamesDatabaseIntegrityCheck();
namesDatabaseIntegrityCheck.rebuildName(this.sellNameTransactionData.getName(), this.repository); namesDatabaseIntegrityCheck.rebuildName(sellNameTransactionData.getName(), this.repository);
} }
@Override @Override
public void process() throws DataException { public void process() throws DataException {
// Sell the name // Sell Name
Name name = new Name(this.repository, this.sellNameTransactionData.getName()); Name name = new Name(this.repository, this.sellNameTransactionData.getName());
name.sell(this.sellNameTransactionData); name.sell(this.sellNameTransactionData);
} }
@Override @Override
public void orphan() throws DataException { public void orphan() throws DataException {
// Revert the name sale in case of orphaning // Revert name
Name name = new Name(this.repository, this.sellNameTransactionData.getName()); Name name = new Name(this.repository, this.sellNameTransactionData.getName());
name.unsell(this.sellNameTransactionData); name.unsell(this.sellNameTransactionData);
} }
} }

View File

@ -65,11 +65,11 @@ public abstract class Transaction {
UPDATE_GROUP(23, true), UPDATE_GROUP(23, true),
ADD_GROUP_ADMIN(24, true), ADD_GROUP_ADMIN(24, true),
REMOVE_GROUP_ADMIN(25, true), REMOVE_GROUP_ADMIN(25, true),
GROUP_BAN(26, true), GROUP_BAN(26, false),
CANCEL_GROUP_BAN(27, true), CANCEL_GROUP_BAN(27, false),
GROUP_KICK(28, true), GROUP_KICK(28, false),
GROUP_INVITE(29, true), GROUP_INVITE(29, false),
CANCEL_GROUP_INVITE(30, true), CANCEL_GROUP_INVITE(30, false),
JOIN_GROUP(31, false), JOIN_GROUP(31, false),
LEAVE_GROUP(32, false), LEAVE_GROUP(32, false),
GROUP_APPROVAL(33, false), GROUP_APPROVAL(33, false),

View File

@ -1,250 +0,0 @@
package org.qortal.utils;
import com.fasterxml.jackson.core.type.TypeReference;
import com.fasterxml.jackson.databind.ObjectMapper;
import com.fasterxml.jackson.databind.exc.InvalidFormatException;
import com.fasterxml.jackson.databind.exc.UnrecognizedPropertyException;
import org.apache.commons.lang3.ArrayUtils;
import org.apache.logging.log4j.LogManager;
import org.apache.logging.log4j.Logger;
import org.qortal.api.SearchMode;
import org.qortal.arbitrary.ArbitraryDataFile;
import org.qortal.arbitrary.ArbitraryDataReader;
import org.qortal.arbitrary.exception.MissingDataException;
import org.qortal.arbitrary.misc.Service;
import org.qortal.controller.Controller;
import org.qortal.data.arbitrary.ArbitraryDataIndex;
import org.qortal.data.arbitrary.ArbitraryDataIndexDetail;
import org.qortal.data.arbitrary.ArbitraryResourceData;
import org.qortal.data.arbitrary.IndexCache;
import org.qortal.repository.DataException;
import org.qortal.repository.Repository;
import org.qortal.repository.RepositoryManager;
import java.io.IOException;
import java.nio.file.Files;
import java.nio.file.Paths;
import java.util.ArrayList;
import java.util.List;
import java.util.Map;
import java.util.Timer;
import java.util.TimerTask;
import java.util.stream.Collectors;
import java.util.stream.Stream;
public class ArbitraryIndexUtils {
public static final ObjectMapper OBJECT_MAPPER = new ObjectMapper();
private static final Logger LOGGER = LogManager.getLogger(ArbitraryIndexUtils.class);
public static final String INDEX_CACHE_TIMER = "Arbitrary Index Cache Timer";
public static final String INDEX_CACHE_TIMER_TASK = "Arbitrary Index Cache Timer Task";
public static void startCaching(int priorityRequested, int frequency) {
Timer timer = buildTimer(INDEX_CACHE_TIMER, priorityRequested);
TimerTask task = new TimerTask() {
@Override
public void run() {
Thread.currentThread().setName(INDEX_CACHE_TIMER_TASK);
try {
fillCache(IndexCache.getInstance());
} catch (IOException | DataException e) {
LOGGER.error(e.getMessage(), e);
}
}
};
// delay 1 second
timer.scheduleAtFixedRate(task, 1_000, frequency * 60_000);
}
private static void fillCache(IndexCache instance) throws DataException, IOException {
try (final Repository repository = RepositoryManager.getRepository()) {
List<ArbitraryResourceData> indexResources
= repository.getArbitraryRepository().searchArbitraryResources(
Service.JSON,
null,
"idx-",
null,
null,
null,
null,
true,
null,
false,
SearchMode.ALL,
0,
null,
null,
null,
null,
null,
null,
null,
null,
true);
List<ArbitraryDataIndexDetail> indexDetails = new ArrayList<>();
LOGGER.debug("processing index resource data: count = " + indexResources.size());
// process all index resources
for( ArbitraryResourceData indexResource : indexResources ) {
try {
LOGGER.debug("processing index resource: name = " + indexResource.name + ", identifier = " + indexResource.identifier);
String json = ArbitraryIndexUtils.getJson(indexResource.name, indexResource.identifier);
// map the JSON string to a list of Java objects
List<ArbitraryDataIndex> indices = OBJECT_MAPPER.readValue(json, new TypeReference<List<ArbitraryDataIndex>>() {});
LOGGER.debug("processed indices = " + indices);
// rank and create index detail for each index in this index resource
for( int rank = 1; rank <= indices.size(); rank++ ) {
indexDetails.add( new ArbitraryDataIndexDetail(indexResource.name, rank, indices.get(rank - 1), indexResource.identifier ));
}
} catch (InvalidFormatException e) {
LOGGER.debug("invalid format, skipping: " + indexResource);
} catch (UnrecognizedPropertyException e) {
LOGGER.debug("unrecognized property, skipping " + indexResource);
}
}
LOGGER.debug("processing indices by term ...");
Map<String, List<ArbitraryDataIndexDetail>> indicesByTerm
= indexDetails.stream().collect(
Collectors.toMap(
detail -> detail.term, // map by term
detail -> List.of(detail), // create list for term
(list1, list2) // merge lists for same term
-> Stream.of(list1, list2)
.flatMap(List::stream)
.collect(Collectors.toList())
)
);
LOGGER.info("processed indices by term: count = " + indicesByTerm.size());
// lock, clear old, load new
synchronized( IndexCache.getInstance().getIndicesByTerm() ) {
IndexCache.getInstance().getIndicesByTerm().clear();
IndexCache.getInstance().getIndicesByTerm().putAll(indicesByTerm);
}
LOGGER.info("loaded indices by term");
LOGGER.debug("processing indices by issuer ...");
Map<String, List<ArbitraryDataIndexDetail>> indicesByIssuer
= indexDetails.stream().collect(
Collectors.toMap(
detail -> detail.issuer, // map by issuer
detail -> List.of(detail), // create list for issuer
(list1, list2) // merge lists for same issuer
-> Stream.of(list1, list2)
.flatMap(List::stream)
.collect(Collectors.toList())
)
);
LOGGER.info("processed indices by issuer: count = " + indicesByIssuer.size());
// lock, clear old, load new
synchronized( IndexCache.getInstance().getIndicesByIssuer() ) {
IndexCache.getInstance().getIndicesByIssuer().clear();
IndexCache.getInstance().getIndicesByIssuer().putAll(indicesByIssuer);
}
LOGGER.info("loaded indices by issuer");
}
}
private static Timer buildTimer( final String name, int priorityRequested) {
// ensure priority is in between 1-10
final int priority = Math.max(0, Math.min(10, priorityRequested));
// Create a custom Timer with updated priority threads
Timer timer = new Timer(true) { // 'true' to make the Timer daemon
@Override
public void schedule(TimerTask task, long delay) {
Thread thread = new Thread(task, name) {
@Override
public void run() {
this.setPriority(priority);
super.run();
}
};
thread.setPriority(priority);
thread.start();
}
};
return timer;
}
public static String getJsonWithExceptionHandling( String name, String identifier ) {
try {
return getJson(name, identifier);
}
catch( Exception e ) {
LOGGER.error(e.getMessage(), e);
return e.getMessage();
}
}
public static String getJson(String name, String identifier) throws IOException {
try {
ArbitraryDataReader arbitraryDataReader
= new ArbitraryDataReader(name, ArbitraryDataFile.ResourceIdType.NAME, Service.JSON, identifier);
int attempts = 0;
Integer maxAttempts = 5;
while (!Controller.isStopping()) {
attempts++;
if (!arbitraryDataReader.isBuilding()) {
try {
arbitraryDataReader.loadSynchronously(false);
break;
} catch (MissingDataException e) {
if (attempts > maxAttempts) {
// Give up after 5 attempts
throw new IOException("Data unavailable. Please try again later.");
}
}
}
Thread.sleep(3000L);
}
java.nio.file.Path outputPath = arbitraryDataReader.getFilePath();
if (outputPath == null) {
// Assume the resource doesn't exist
throw new IOException( "File not found");
}
// No file path supplied - so check if this is a single file resource
String[] files = ArrayUtils.removeElement(outputPath.toFile().list(), ".qortal");
String filepath = files[0];
java.nio.file.Path path = Paths.get(outputPath.toString(), filepath);
if (!Files.exists(path)) {
String message = String.format("No file exists at filepath: %s", filepath);
throw new IOException( message );
}
String data = Files.readString(path);
return data;
} catch (Exception e) {
throw new IOException(String.format("Unable to load %s %s: %s", Service.JSON, name, e.getMessage()));
}
}
}

View File

@ -24,7 +24,6 @@ import java.nio.file.attribute.BasicFileAttributes;
import java.util.ArrayList; import java.util.ArrayList;
import java.util.Arrays; import java.util.Arrays;
import java.util.List; import java.util.List;
import java.util.Optional;
import java.util.stream.Collectors; import java.util.stream.Collectors;
import static java.nio.file.StandardCopyOption.REPLACE_EXISTING; import static java.nio.file.StandardCopyOption.REPLACE_EXISTING;
@ -73,23 +72,23 @@ public class ArbitraryTransactionUtils {
return latestPut; return latestPut;
} }
public static Optional<ArbitraryTransactionData> hasMoreRecentPutTransaction(Repository repository, ArbitraryTransactionData arbitraryTransactionData) { public static boolean hasMoreRecentPutTransaction(Repository repository, ArbitraryTransactionData arbitraryTransactionData) {
byte[] signature = arbitraryTransactionData.getSignature(); byte[] signature = arbitraryTransactionData.getSignature();
if (signature == null) { if (signature == null) {
// We can't make a sensible decision without a signature // We can't make a sensible decision without a signature
// so it's best to assume there is nothing newer // so it's best to assume there is nothing newer
return Optional.empty(); return false;
} }
ArbitraryTransactionData latestPut = ArbitraryTransactionUtils.fetchLatestPut(repository, arbitraryTransactionData); ArbitraryTransactionData latestPut = ArbitraryTransactionUtils.fetchLatestPut(repository, arbitraryTransactionData);
if (latestPut == null) { if (latestPut == null) {
return Optional.empty(); return false;
} }
// If the latest PUT transaction has a newer timestamp, it will override the existing transaction // If the latest PUT transaction has a newer timestamp, it will override the existing transaction
// Any data relating to the older transaction is no longer needed // Any data relating to the older transaction is no longer needed
boolean hasNewerPut = (latestPut.getTimestamp() > arbitraryTransactionData.getTimestamp()); boolean hasNewerPut = (latestPut.getTimestamp() > arbitraryTransactionData.getTimestamp());
return hasNewerPut ? Optional.of(latestPut) : Optional.empty(); return hasNewerPut;
} }
public static boolean completeFileExists(ArbitraryTransactionData transactionData) throws DataException { public static boolean completeFileExists(ArbitraryTransactionData transactionData) throws DataException {
@ -209,15 +208,7 @@ public class ArbitraryTransactionUtils {
return ArbitraryTransactionUtils.isFileRecent(filePath, now, cleanupAfter); return ArbitraryTransactionUtils.isFileRecent(filePath, now, cleanupAfter);
} }
/** public static void deleteCompleteFile(ArbitraryTransactionData arbitraryTransactionData, long now, long cleanupAfter) throws DataException {
*
* @param arbitraryTransactionData
* @param now
* @param cleanupAfter
* @return true if file is deleted, otherwise return false
* @throws DataException
*/
public static boolean deleteCompleteFile(ArbitraryTransactionData arbitraryTransactionData, long now, long cleanupAfter) throws DataException {
byte[] completeHash = arbitraryTransactionData.getData(); byte[] completeHash = arbitraryTransactionData.getData();
byte[] signature = arbitraryTransactionData.getSignature(); byte[] signature = arbitraryTransactionData.getSignature();
@ -228,11 +219,6 @@ public class ArbitraryTransactionUtils {
"if needed", Base58.encode(completeHash)); "if needed", Base58.encode(completeHash));
arbitraryDataFile.delete(); arbitraryDataFile.delete();
return true;
}
else {
return false;
} }
} }

View File

@ -1,319 +0,0 @@
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;
}
}

View File

@ -1,99 +0,0 @@
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
= Groups.getAllMembers(
repository.getGroupRepository(),
Groups.getGroupIdsToMint(BlockChain.getInstance(), blockData.getHeight())
);
// 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);
}
}
}

View File

@ -1,122 +0,0 @@
package org.qortal.utils;
import org.qortal.block.BlockChain;
import org.qortal.data.group.GroupAdminData;
import org.qortal.data.group.GroupMemberData;
import org.qortal.repository.DataException;
import org.qortal.repository.GroupRepository;
import java.util.ArrayList;
import java.util.Comparator;
import java.util.HashSet;
import java.util.List;
import java.util.Optional;
import java.util.Set;
import java.util.stream.Collectors;
/**
* Class Groups
*
* A utility class for group related functionality.
*/
public class Groups {
/**
* Does the member exist in any of these groups?
*
* @param groupRepository the group data repository
* @param groupsIds the group Ids to look for the address
* @param address the address
*
* @return true if the address is in any of the groups listed otherwise false
* @throws DataException
*/
public static boolean memberExistsInAnyGroup(GroupRepository groupRepository, List<Integer> groupsIds, String address) throws DataException {
// if any of the listed groups have the address as a member, then return true
for( Integer groupIdToMint : groupsIds) {
if( groupRepository.memberExists(groupIdToMint, address) ) {
return true;
}
}
// if none of the listed groups have the address as a member, then return false
return false;
}
/**
* Get All Members
*
* Get all the group members from a list of groups.
*
* @param groupRepository the group data repository
* @param groupIds the list of group Ids to look at
*
* @return the list of all members belonging to any of the groups, no duplicates
* @throws DataException
*/
public static List<String> getAllMembers( GroupRepository groupRepository, List<Integer> groupIds ) throws DataException {
// collect all the members in a set, the set keeps out duplicates
Set<String> allMembers = new HashSet<>();
// add all members from each group to the all members set
for( int groupId : groupIds ) {
allMembers.addAll( groupRepository.getGroupMembers(groupId).stream().map(GroupMemberData::getMember).collect(Collectors.toList()));
}
return new ArrayList<>(allMembers);
}
/**
* Get All Admins
*
* Get all the admins from a list of groups.
*
* @param groupRepository the group data repository
* @param groupIds the list of group Ids to look at
*
* @return the list of all admins to any of the groups, no duplicates
* @throws DataException
*/
public static List<String> getAllAdmins( GroupRepository groupRepository, List<Integer> groupIds ) throws DataException {
// collect all the admins in a set, the set keeps out duplicates
Set<String> allAdmins = new HashSet<>();
// collect admins for each group
for( int groupId : groupIds ) {
allAdmins.addAll( groupRepository.getGroupAdmins(groupId).stream().map(GroupAdminData::getAdmin).collect(Collectors.toList()) );
}
return new ArrayList<>(allAdmins);
}
/**
* Get Group Ids To Mint
*
* @param blockchain the blockchain
* @param blockchainHeight the block height to mint
*
* @return the group Ids for the minting groups at the height given
*/
public static List<Integer> getGroupIdsToMint(BlockChain blockchain, int blockchainHeight) {
// sort heights lowest to highest
Comparator<BlockChain.IdsForHeight> compareByHeight = Comparator.comparingInt(entry -> entry.height);
// sort heights highest to lowest
Comparator<BlockChain.IdsForHeight> compareByHeightReversed = compareByHeight.reversed();
// get highest height that is less than the blockchain height
Optional<BlockChain.IdsForHeight> ids = blockchain.getMintingGroupIds().stream()
.filter(entry -> entry.height < blockchainHeight)
.sorted(compareByHeightReversed)
.findFirst();
if( ids.isPresent()) {
return ids.get().ids;
}
else {
return new ArrayList<>(0);
}
}
}

View File

@ -38,9 +38,7 @@
"blockRewardBatchStartHeight": 1508000, "blockRewardBatchStartHeight": 1508000,
"blockRewardBatchSize": 1000, "blockRewardBatchSize": 1000,
"blockRewardBatchAccountsBlockCount": 25, "blockRewardBatchAccountsBlockCount": 25,
"mintingGroupIds": [ "mintingGroupId": 694,
{ "height": 0, "ids": [ 694 ]}
],
"rewardsByHeight": [ "rewardsByHeight": [
{ "height": 1, "reward": 5.00 }, { "height": 1, "reward": 5.00 },
{ "height": 259201, "reward": 4.75 }, { "height": 259201, "reward": 4.75 },
@ -115,11 +113,7 @@
"onlyMintWithNameHeight": 1900300, "onlyMintWithNameHeight": 1900300,
"removeOnlyMintWithNameHeight": 1935500, "removeOnlyMintWithNameHeight": 1935500,
"groupMemberCheckHeight": 1902700, "groupMemberCheckHeight": 1902700,
"fixBatchRewardHeight": 1945900, "fixBatchRewardHeight": 1945900
"adminsReplaceFoundersHeight": 2012800,
"nullGroupMembershipHeight": 2012800,
"ignoreLevelForRewardShareHeight": 2012800,
"adminQueryFixHeight": 2012800
}, },
"checkpoints": [ "checkpoints": [
{ "height": 1136300, "signature": "3BbwawEF2uN8Ni5ofpJXkukoU8ctAPxYoFB7whq9pKfBnjfZcpfEJT4R95NvBDoTP8WDyWvsUvbfHbcr9qSZuYpSKZjUQTvdFf6eqznHGEwhZApWfvXu6zjGCxYCp65F4jsVYYJjkzbjmkCg5WAwN5voudngA23kMK6PpTNygapCzXt" } { "height": 1136300, "signature": "3BbwawEF2uN8Ni5ofpJXkukoU8ctAPxYoFB7whq9pKfBnjfZcpfEJT4R95NvBDoTP8WDyWvsUvbfHbcr9qSZuYpSKZjUQTvdFf6eqznHGEwhZApWfvXu6zjGCxYCp65F4jsVYYJjkzbjmkCg5WAwN5voudngA23kMK6PpTNygapCzXt" }

View File

@ -45,10 +45,10 @@ SYNCHRONIZING_BLOCKCHAIN = Bezig met synchronizeren
SYNCHRONIZING_CLOCK = Klok wordt gesynchronizeerd SYNCHRONIZING_CLOCK = Klok wordt gesynchronizeerd
RESTARTING_NODE = Node opnieuw starten RESTARTING_NODE = Knooppunt opnieuw starten
APPLYING_RESTARTING_NODE = Node wordt herstart. Even geduld alstublieft... APPLYING_RESTARTING_NODE = Herstartknooppunt toepassen. Wees alstublieft geduldig...
BOOTSTRAP_NODE = Opstarten van node (bootstrap) BOOTSTRAP_NODE = Opstartknooppunt
APPLYING_BOOTSTRAP_AND_RESTARTING = Bootstrap toepassen en node opnieuw starten. Even geduld alstublieft... APPLYING_BOOTSTRAP_AND_RESTARTING = Bootstrap toepassen en knooppunt opnieuw starten. Wees alstublieft geduldig...

View File

@ -196,7 +196,6 @@ TX_GROUP_ID_MISMATCH = groep-ID komt niet overeen
TRANSFER_PRIVS_DISABLED = overdrachtsrechten uitgeschakeld TRANSFER_PRIVS_DISABLED = overdrachtsrechten uitgeschakeld
TEMPORARY_DISABLED = naamregistratie tijdelijk uitgeschakeld TEMPORARY_DISABLED = Naamregistratie tijdelijk uitgeschakeld
GENERAL_TEMPORARY_DISABLED = Tijdelijk uitgeschakeld GENERAL_TEMPORARY_DISABLED = Tijdelijk uitgeschakeld

View File

@ -20,21 +20,17 @@
width: 100%; width: 100%;
text-align: center; text-align: center;
z-index: 1000; z-index: 1000;
top: 50%; top: 45%;
-ms-transform: translateY(-50%); -ms-transform: translateY(-50%);
transform: translate(-50% , -50%); transform: translateY(-50%);
left: 50%;
} }
#panel { #panel {
text-align: center; text-align: center;
background: white; background: white;
word-wrap: break-word;
width: 350px; width: 350px;
max-width: 100%;
margin: auto; margin: auto;
padding: 25px; padding: 25px;
border-radius: 30px; border-radius: 30px;
box-sizing: border-box;
} }
#status { #status {
color: #03a9f4; color: #03a9f4;

View File

@ -84,7 +84,6 @@ isDOMContentLoaded: isDOMContentLoaded ? true : false
function handleQDNResourceDisplayed(pathurl, isDOMContentLoaded) { function handleQDNResourceDisplayed(pathurl, isDOMContentLoaded) {
// make sure that an empty string the root path // make sure that an empty string the root path
if(pathurl?.startsWith('/render/hash/')) return;
const path = pathurl || '/' const path = pathurl || '/'
if (!isManualNavigation) { if (!isManualNavigation) {
isManualNavigation = true isManualNavigation = true
@ -285,9 +284,11 @@ window.addEventListener("message", async (event) => {
return; return;
} }
console.log("Core received action: " + JSON.stringify(event.data.action));
let url; let url;
let data = event.data; let data = event.data;
let identifier;
switch (data.action) { switch (data.action) {
case "GET_ACCOUNT_DATA": case "GET_ACCOUNT_DATA":
return httpGetAsyncWithEvent(event, "/addresses/" + data.address); return httpGetAsyncWithEvent(event, "/addresses/" + data.address);
@ -382,7 +383,6 @@ window.addEventListener("message", async (event) => {
if (data.identifier != null) url = url.concat("&identifier=" + data.identifier); if (data.identifier != null) url = url.concat("&identifier=" + data.identifier);
if (data.name != null) url = url.concat("&name=" + data.name); if (data.name != null) url = url.concat("&name=" + data.name);
if (data.names != null) data.names.forEach((x, i) => url = url.concat("&name=" + x)); if (data.names != null) data.names.forEach((x, i) => url = url.concat("&name=" + x));
if (data.keywords != null) data.keywords.forEach((x, i) => url = url.concat("&keywords=" + x));
if (data.title != null) url = url.concat("&title=" + data.title); if (data.title != null) url = url.concat("&title=" + data.title);
if (data.description != null) url = url.concat("&description=" + data.description); if (data.description != null) url = url.concat("&description=" + data.description);
if (data.prefix != null) url = url.concat("&prefix=" + new Boolean(data.prefix).toString()); if (data.prefix != null) url = url.concat("&prefix=" + new Boolean(data.prefix).toString());
@ -419,7 +419,7 @@ window.addEventListener("message", async (event) => {
return httpGetAsyncWithEvent(event, url); return httpGetAsyncWithEvent(event, url);
case "GET_QDN_RESOURCE_PROPERTIES": case "GET_QDN_RESOURCE_PROPERTIES":
identifier = (data.identifier != null) ? data.identifier : "default"; let identifier = (data.identifier != null) ? data.identifier : "default";
url = "/arbitrary/resource/properties/" + data.service + "/" + data.name + "/" + identifier; url = "/arbitrary/resource/properties/" + data.service + "/" + data.name + "/" + identifier;
return httpGetAsyncWithEvent(event, url); return httpGetAsyncWithEvent(event, url);
@ -456,7 +456,7 @@ window.addEventListener("message", async (event) => {
return httpGetAsyncWithEvent(event, url); return httpGetAsyncWithEvent(event, url);
case "GET_AT": case "GET_AT":
url = "/at/" + data.atAddress; url = "/at" + data.atAddress;
return httpGetAsyncWithEvent(event, url); return httpGetAsyncWithEvent(event, url);
case "GET_AT_DATA": case "GET_AT_DATA":
@ -473,7 +473,7 @@ window.addEventListener("message", async (event) => {
case "FETCH_BLOCK": case "FETCH_BLOCK":
if (data.signature != null) { if (data.signature != null) {
url = "/blocks/signature/" + data.signature; url = "/blocks/" + data.signature;
} else if (data.height != null) { } else if (data.height != null) {
url = "/blocks/byheight/" + data.height; url = "/blocks/byheight/" + data.height;
} }
@ -694,7 +694,6 @@ const qortalRequestWithTimeout = (request, timeout) =>
* Send current page details to UI * Send current page details to UI
*/ */
document.addEventListener('DOMContentLoaded', (event) => { document.addEventListener('DOMContentLoaded', (event) => {
resetVariables() resetVariables()
qortalRequest({ qortalRequest({
action: "QDN_RESOURCE_DISPLAYED", action: "QDN_RESOURCE_DISPLAYED",
@ -713,7 +712,6 @@ resetVariables()
* Handle app navigation * Handle app navigation
*/ */
navigation.addEventListener('navigate', (event) => { navigation.addEventListener('navigate', (event) => {
const url = new URL(event.destination.url); const url = new URL(event.destination.url);
let fullpath = url.pathname + url.hash; let fullpath = url.pathname + url.hash;

View File

@ -405,7 +405,7 @@ public class RepositoryTests extends Common {
Integer offset = null; Integer offset = null;
Boolean reverse = null; Boolean reverse = null;
hsqldb.getATRepository().getMatchingFinalATStates(codeHash,null, null, isFinished, dataByteOffset, expectedValue, minimumFinalHeight, limit, offset, reverse); hsqldb.getATRepository().getMatchingFinalATStates(codeHash, isFinished, dataByteOffset, expectedValue, minimumFinalHeight, limit, offset, reverse);
} catch (DataException e) { } catch (DataException e) {
fail("HSQLDB bug #1580"); fail("HSQLDB bug #1580");
} }

View File

@ -15,7 +15,10 @@ import org.qortal.repository.DataException;
import org.qortal.repository.Repository; import org.qortal.repository.Repository;
import org.qortal.repository.RepositoryManager; import org.qortal.repository.RepositoryManager;
import org.qortal.settings.Settings; import org.qortal.settings.Settings;
import org.qortal.test.common.*; import org.qortal.test.common.AccountUtils;
import org.qortal.test.common.BlockUtils;
import org.qortal.test.common.Common;
import org.qortal.test.common.TransactionUtils;
import org.qortal.test.common.transaction.TestTransaction; import org.qortal.test.common.transaction.TestTransaction;
import org.qortal.transaction.Transaction; import org.qortal.transaction.Transaction;
import org.qortal.transaction.TransferPrivsTransaction; import org.qortal.transaction.TransferPrivsTransaction;
@ -25,7 +28,6 @@ import java.util.ArrayList;
import java.util.List; import java.util.List;
import java.util.Random; import java.util.Random;
import static org.hamcrest.core.IsNot.not;
import static org.junit.Assert.*; import static org.junit.Assert.*;
import static org.qortal.test.common.AccountUtils.fee; import static org.qortal.test.common.AccountUtils.fee;
import static org.qortal.transaction.Transaction.ValidationResult.ACCOUNT_NOT_TRANSFERABLE; import static org.qortal.transaction.Transaction.ValidationResult.ACCOUNT_NOT_TRANSFERABLE;
@ -834,10 +836,7 @@ public class SelfSponsorshipAlgoV1Tests extends Common {
assertEquals(0, (int) new Account(repository, aliceAccount.getAddress()).getLevel()); assertEquals(0, (int) new Account(repository, aliceAccount.getAddress()).getLevel());
// Alice can no longer create a reward share transaction // Alice can no longer create a reward share transaction
// Used to Chehck for ACCOUNT_CANNOT_REWARD_SHARE / Updated post canMint() function changes assertEquals(Transaction.ValidationResult.ACCOUNT_CANNOT_REWARD_SHARE, AccountUtils.createRandomRewardShare(repository, aliceAccount));
// canMint returns False because reward penality != 0 (account.java:240)
// Causes trip in RewardShareTransaction.java:126, Returns NOT_MINTING_ACCOUNT
assertThat(Transaction.ValidationResult.OK, not(AccountUtils.createRandomRewardShare(repository, aliceAccount)));
// ... but Bob still can // ... but Bob still can
assertEquals(Transaction.ValidationResult.OK, AccountUtils.createRandomRewardShare(repository, bobAccount)); assertEquals(Transaction.ValidationResult.OK, AccountUtils.createRandomRewardShare(repository, bobAccount));

View File

@ -28,7 +28,6 @@ import java.util.ArrayList;
import java.util.List; import java.util.List;
import java.util.Random; import java.util.Random;
import static org.hamcrest.core.IsNot.not;
import static org.junit.Assert.*; import static org.junit.Assert.*;
import static org.qortal.test.common.AccountUtils.fee; import static org.qortal.test.common.AccountUtils.fee;
import static org.qortal.transaction.Transaction.ValidationResult.ACCOUNT_NOT_TRANSFERABLE; import static org.qortal.transaction.Transaction.ValidationResult.ACCOUNT_NOT_TRANSFERABLE;
@ -841,7 +840,7 @@ public class SelfSponsorshipAlgoV3Tests extends Common {
assertEquals(0, (int) new Account(repository, aliceAccount.getAddress()).getLevel()); assertEquals(0, (int) new Account(repository, aliceAccount.getAddress()).getLevel());
// Alice can no longer create a reward share transaction // Alice can no longer create a reward share transaction
assertThat(Transaction.ValidationResult.OK, not(AccountUtils.createRandomRewardShare(repository, aliceAccount))); assertEquals(Transaction.ValidationResult.ACCOUNT_CANNOT_REWARD_SHARE, AccountUtils.createRandomRewardShare(repository, aliceAccount));
// Bob can no longer create a reward share transaction (disabled at Block 15) // Bob can no longer create a reward share transaction (disabled at Block 15)
assertEquals(Transaction.ValidationResult.OK, AccountUtils.createRandomRewardShare(repository, bobAccount)); assertEquals(Transaction.ValidationResult.OK, AccountUtils.createRandomRewardShare(repository, bobAccount));

View File

@ -25,19 +25,9 @@ public class AddressesApiTests extends ApiCommon {
} }
@Test @Test
@Ignore(value = "No Logic Coded") @Ignore(value = "Doesn't work, to be fixed later")
public void testGetOnlineAccounts() { public void testGetOnlineAccounts() {
// Add and remove users as online checking count after minting
// TODO: Need to construct logic
// this.addressesResource.getOnlineAccounts(), empty Array, Size = 0
assertNotNull(this.addressesResource.getOnlineAccounts()); assertNotNull(this.addressesResource.getOnlineAccounts());
int blocksToMint = 5;
// Add 2 accounts to the online array, mint some blocks
// Assert number of accountsOnline == 2
// Remove an account from onlineStatus, mint some blocks
// Assert number of accountsOnline == 1
// Add two accounts as online, mint some blocks
// Asset number of accountsOnline == 3
} }
@Test @Test

View File

@ -26,7 +26,7 @@ public class CrossChainApiTests extends ApiCommon {
@Test @Test
public void testGetCompletedTrades() { public void testGetCompletedTrades() {
long minimumTimestamp = System.currentTimeMillis(); long minimumTimestamp = System.currentTimeMillis();
assertNoApiError((limit, offset, reverse) -> this.crossChainResource.getCompletedTrades(SPECIFIC_BLOCKCHAIN, minimumTimestamp, null, null, limit, offset, reverse)); assertNoApiError((limit, offset, reverse) -> this.crossChainResource.getCompletedTrades(SPECIFIC_BLOCKCHAIN, minimumTimestamp, limit, offset, reverse));
} }
@Test @Test
@ -35,8 +35,8 @@ public class CrossChainApiTests extends ApiCommon {
Integer offset = null; Integer offset = null;
Boolean reverse = null; Boolean reverse = null;
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, -1L /*minimumTimestamp*/, limit, offset, reverse));
assertApiError(ApiError.INVALID_CRITERIA, () -> this.crossChainResource.getCompletedTrades(SPECIFIC_BLOCKCHAIN, 0L /*minimumTimestamp*/, null, null, limit, offset, reverse)); assertApiError(ApiError.INVALID_CRITERIA, () -> this.crossChainResource.getCompletedTrades(SPECIFIC_BLOCKCHAIN, 0L /*minimumTimestamp*/, limit, offset, reverse));
} }
} }

View File

@ -3,15 +3,10 @@ package org.qortal.test.api;
import org.json.simple.JSONObject; import org.json.simple.JSONObject;
import org.junit.Assert; import org.junit.Assert;
import org.junit.Test; import org.junit.Test;
import org.qortal.api.model.CrossChainTradeLedgerEntry;
import org.qortal.api.resource.CrossChainUtils; import org.qortal.api.resource.CrossChainUtils;
import org.qortal.test.common.ApiCommon; import org.qortal.test.common.ApiCommon;
import java.io.IOException;
import java.io.PrintWriter;
import java.util.ArrayList;
import java.util.HashMap; import java.util.HashMap;
import java.util.List;
import java.util.Map; import java.util.Map;
public class CrossChainUtilsTests extends ApiCommon { public class CrossChainUtilsTests extends ApiCommon {
@ -142,53 +137,4 @@ public class CrossChainUtilsTests extends ApiCommon {
Assert.assertEquals(5, versionDecimal, 0.001); Assert.assertEquals(5, versionDecimal, 0.001);
Assert.assertFalse(thrown); Assert.assertFalse(thrown);
} }
@Test
public void testWriteToLedgerHeaderOnly() throws IOException {
CrossChainUtils.writeToLedger(new PrintWriter(System.out), new ArrayList<>());
}
@Test
public void testWriteToLedgerOneRow() throws IOException {
CrossChainUtils.writeToLedger(
new PrintWriter(System.out),
List.of(
new CrossChainTradeLedgerEntry(
"QORT",
"LTC",
1000,
0,
"LTC",
1,
System.currentTimeMillis())
)
);
}
@Test
public void testWriteToLedgerTwoRows() throws IOException {
CrossChainUtils.writeToLedger(
new PrintWriter(System.out),
List.of(
new CrossChainTradeLedgerEntry(
"QORT",
"LTC",
1000,
0,
"LTC",
1,
System.currentTimeMillis()
),
new CrossChainTradeLedgerEntry(
"LTC",
"QORT",
1,
0,
"LTC",
1000,
System.currentTimeMillis()
)
)
);
}
} }

View File

@ -6,13 +6,11 @@ import org.qortal.arbitrary.ArbitraryDataDigest;
import org.qortal.repository.DataException; import org.qortal.repository.DataException;
import org.qortal.test.common.Common; import org.qortal.test.common.Common;
import java.io.File;
import java.io.FileWriter; import java.io.FileWriter;
import java.io.IOException; import java.io.IOException;
import java.nio.file.Files; import java.nio.file.Files;
import java.nio.file.Path; import java.nio.file.Path;
import java.nio.file.Paths; import java.nio.file.Paths;
import java.util.Arrays;
import java.util.UUID; import java.util.UUID;
import static org.junit.Assert.assertEquals; import static org.junit.Assert.assertEquals;
@ -27,10 +25,7 @@ public class ArbitraryDataDigestTests extends Common {
@Test @Test
public void testDirectoryDigest() throws IOException, DataException { public void testDirectoryDigest() throws IOException, DataException {
String dataPathString = "src" + File.separator + "test" + File.separator + "resources" + File.separator + "arbitrary" + File.separator + "demo1"; Path dataPath = Paths.get("src/test/resources/arbitrary/demo1");
Path dataPath = Paths.get(dataPathString);
// expectedHash is calculated for Linux
String expectedHash58 = "DKyMuonWKoneJqiVHgw26Vk1ytrZG9PGsE9xfBg3GKDp"; String expectedHash58 = "DKyMuonWKoneJqiVHgw26Vk1ytrZG9PGsE9xfBg3GKDp";
// Ensure directory exists // Ensure directory exists
@ -40,13 +35,6 @@ public class ArbitraryDataDigestTests extends Common {
// Compute a hash // Compute a hash
ArbitraryDataDigest digest = new ArbitraryDataDigest(dataPath); ArbitraryDataDigest digest = new ArbitraryDataDigest(dataPath);
digest.compute(); digest.compute();
// Assert Fails, Expected: DKyMuonWKoneJqiVHgw26Vk1ytrZG9PGsE9xfBg3GKDp
// Actual : HbvgC6sfhFtDvmDXfPXVUcCUyZ5rM1NzX488qHmMpMB4
// This might be failing because Windows turns "/" (47) to "\" (92)
// Or maybe a fail due to \n [13] VS \r\n [13,10] in .txt files
// Confirmed the read in windows has \r\n
// Could be a combination of both
System.out.printf("Hash: %s \n", Arrays.toString(digest.getHash()));
assertEquals(expectedHash58, digest.getHash58()); assertEquals(expectedHash58, digest.getHash58());
// Write a random file to .qortal/cache to ensure it isn't being included in the digest function // Write a random file to .qortal/cache to ensure it isn't being included in the digest function

View File

@ -15,7 +15,9 @@ import java.io.File;
import java.io.FileWriter; import java.io.FileWriter;
import java.io.IOException; import java.io.IOException;
import java.nio.ByteBuffer; import java.nio.ByteBuffer;
import java.nio.file.*; import java.nio.file.Files;
import java.nio.file.Path;
import java.nio.file.Paths;
import java.util.Arrays; import java.util.Arrays;
import java.util.Objects; import java.util.Objects;
import java.util.Random; import java.util.Random;
@ -240,7 +242,7 @@ public class ArbitraryDataMergeTests extends Common {
BufferedWriter file1Writer = new BufferedWriter(new FileWriter(file1)); BufferedWriter file1Writer = new BufferedWriter(new FileWriter(file1));
String initialString = ArbitraryUtils.generateRandomString(1024); String initialString = ArbitraryUtils.generateRandomString(1024);
// Add a newline every 50 chars // Add a newline every 50 chars
initialString = initialString.replaceAll("(.{50})", "$1" + System.lineSeparator()); initialString = initialString.replaceAll("(.{50})", "$1\n");
file1Writer.write(initialString); file1Writer.write(initialString);
file1Writer.newLine(); file1Writer.newLine();
file1Writer.close(); file1Writer.close();
@ -305,23 +307,23 @@ public class ArbitraryDataMergeTests extends Common {
file1.deleteOnExit(); file1.deleteOnExit();
file2.deleteOnExit(); file2.deleteOnExit();
// Write a random string to the first file, 1024 characters, \n every 50 chars // Write a random string to the first file
BufferedWriter file1Writer = new BufferedWriter(new FileWriter(file1)); BufferedWriter file1Writer = new BufferedWriter(new FileWriter(file1));
String initialString = ArbitraryUtils.generateRandomString(1024); String initialString = ArbitraryUtils.generateRandomString(1024);
// Add a newline every 50 chars // Add a newline every 50 chars
initialString = initialString.replaceAll("(.{50})", "$1" + System.lineSeparator()); initialString = initialString.replaceAll("(.{50})", "$1\n");
// Remove newline at end of string // Remove newline at end of string
initialString = initialString.stripTrailing(); // Remove Trailing White Space initialString = initialString.stripTrailing();
file1Writer.write(initialString); file1Writer.write(initialString);
// No newline // No newline
file1Writer.close(); file1Writer.close();
byte[] file1Digest = Crypto.digest(file1); byte[] file1Digest = Crypto.digest(file1);
// Write a slightly modified string to the second file, append "-edit" to the string // Write a slightly modified string to the second file
BufferedWriter file2Writer = new BufferedWriter(new FileWriter(file2)); BufferedWriter file2Writer = new BufferedWriter(new FileWriter(file2));
String updatedString = initialString.concat("-edit"); String updatedString = initialString.concat("-edit");
file2Writer.write(updatedString); file2Writer.write(updatedString);
// No newline - This is wrong, both files contain \n every 50 chars // No newline
file2Writer.close(); file2Writer.close();
byte[] file2Digest = Crypto.digest(file2); byte[] file2Digest = Crypto.digest(file2);
@ -340,7 +342,6 @@ public class ArbitraryDataMergeTests extends Common {
ArbitraryDataCreatePatch patch = new ArbitraryDataCreatePatch(tempDir1, tempDir2, signature); ArbitraryDataCreatePatch patch = new ArbitraryDataCreatePatch(tempDir1, tempDir2, signature);
patch.create(); patch.create();
Path patchPath = patch.getFinalPath(); Path patchPath = patch.getFinalPath();
assertTrue(Files.exists(patchPath)); assertTrue(Files.exists(patchPath));
// Check that the patch file exists // Check that the patch file exists
@ -352,7 +353,6 @@ public class ArbitraryDataMergeTests extends Common {
assertFalse(Arrays.equals(patchDigest, file1Digest)); assertFalse(Arrays.equals(patchDigest, file1Digest));
// Make sure that the patch file is different from file2 // Make sure that the patch file is different from file2
// This assert fails, patchFile and file2 have the same contents so the digest will match
assertFalse(Arrays.equals(patchDigest, file2Digest)); assertFalse(Arrays.equals(patchDigest, file2Digest));
// Now merge the patch with the original path // Now merge the patch with the original path
@ -384,7 +384,7 @@ public class ArbitraryDataMergeTests extends Common {
BufferedWriter file1Writer = new BufferedWriter(new FileWriter(file1)); BufferedWriter file1Writer = new BufferedWriter(new FileWriter(file1));
String initialString = ArbitraryUtils.generateRandomString(110 * 1024); String initialString = ArbitraryUtils.generateRandomString(110 * 1024);
// Add a newline every 50 chars // Add a newline every 50 chars
initialString = initialString.replaceAll("(.{50})", "$1" + System.lineSeparator()); initialString = initialString.replaceAll("(.{50})", "$1\n");
file1Writer.write(initialString); file1Writer.write(initialString);
file1Writer.newLine(); file1Writer.newLine();
file1Writer.close(); file1Writer.close();

View File

@ -145,6 +145,56 @@ public class ArbitraryDataStorageCapacityTests extends Common {
} }
} }
@Test
public void testDeleteRandomFilesForName() throws DataException, IOException, InterruptedException, IllegalAccessException {
try (final Repository repository = RepositoryManager.getRepository()) {
String identifier = null; // Not used for this test
Service service = Service.ARBITRARY_DATA;
int chunkSize = 100;
int dataLength = 900; // Actual data length will be longer due to encryption
// Set originalCopyIndicatorFileEnabled to false, otherwise nothing will be deleted as it all originates from this node
FieldUtils.writeField(Settings.getInstance(), "originalCopyIndicatorFileEnabled", false, true);
// Alice hosts some data (with 10 chunks)
PrivateKeyAccount alice = Common.getTestAccount(repository, "alice");
String aliceName = "alice";
RegisterNameTransactionData transactionData = new RegisterNameTransactionData(TestTransaction.generateBase(alice), aliceName, "");
transactionData.setFee(new RegisterNameTransaction(null, null).getUnitFee(transactionData.getTimestamp()));
TransactionUtils.signAndMint(repository, transactionData, alice);
Path alicePath = ArbitraryUtils.generateRandomDataPath(dataLength);
ArbitraryDataFile aliceArbitraryDataFile = ArbitraryUtils.createAndMintTxn(repository, Base58.encode(alice.getPublicKey()), alicePath, aliceName, identifier, ArbitraryTransactionData.Method.PUT, service, alice, chunkSize);
// Bob hosts some data too (also with 10 chunks)
PrivateKeyAccount bob = Common.getTestAccount(repository, "bob");
String bobName = "bob";
transactionData = new RegisterNameTransactionData(TestTransaction.generateBase(bob), bobName, "");
transactionData.setFee(new RegisterNameTransaction(null, null).getUnitFee(transactionData.getTimestamp()));
TransactionUtils.signAndMint(repository, transactionData, bob);
Path bobPath = ArbitraryUtils.generateRandomDataPath(dataLength);
ArbitraryDataFile bobArbitraryDataFile = ArbitraryUtils.createAndMintTxn(repository, Base58.encode(bob.getPublicKey()), bobPath, bobName, identifier, ArbitraryTransactionData.Method.PUT, service, bob, chunkSize);
// All 20 chunks should exist
assertEquals(10, aliceArbitraryDataFile.chunkCount());
assertTrue(aliceArbitraryDataFile.allChunksExist());
assertEquals(10, bobArbitraryDataFile.chunkCount());
assertTrue(bobArbitraryDataFile.allChunksExist());
// Now pretend that Bob has reached his storage limit - this should delete random files
// Run it 10 times to remove the likelihood of the randomizer always picking Alice's files
for (int i=0; i<10; i++) {
ArbitraryDataCleanupManager.getInstance().storageLimitReachedForName(repository, bobName);
}
// Alice should still have all chunks
assertTrue(aliceArbitraryDataFile.allChunksExist());
// Bob should be missing some chunks
assertFalse(bobArbitraryDataFile.allChunksExist());
}
}
private void deleteListsDirectory() { private void deleteListsDirectory() {
// Delete lists directory if exists // Delete lists directory if exists
Path listsPath = Paths.get(Settings.getInstance().getListsPath()); Path listsPath = Paths.get(Settings.getInstance().getListsPath());

View File

@ -73,14 +73,14 @@ public class ArbitraryDataStoragePolicyTests extends Common {
// We should store and pre-fetch data for this transaction // We should store and pre-fetch data for this transaction
assertEquals(StoragePolicy.FOLLOWED_OR_VIEWED, Settings.getInstance().getStoragePolicy()); assertEquals(StoragePolicy.FOLLOWED_OR_VIEWED, Settings.getInstance().getStoragePolicy());
assertTrue(storageManager.canStoreData(arbitraryTransactionData)); assertTrue(storageManager.canStoreData(arbitraryTransactionData));
assertTrue(storageManager.shouldPreFetchData(repository, arbitraryTransactionData).isPass()); assertTrue(storageManager.shouldPreFetchData(repository, arbitraryTransactionData));
// Now unfollow the name // Now unfollow the name
assertTrue(ResourceListManager.getInstance().removeFromList("followedNames", name, false)); assertTrue(ResourceListManager.getInstance().removeFromList("followedNames", name, false));
// We should store but not pre-fetch data for this transaction // We should store but not pre-fetch data for this transaction
assertTrue(storageManager.canStoreData(arbitraryTransactionData)); assertTrue(storageManager.canStoreData(arbitraryTransactionData));
assertFalse(storageManager.shouldPreFetchData(repository, arbitraryTransactionData).isPass()); assertFalse(storageManager.shouldPreFetchData(repository, arbitraryTransactionData));
} }
} }
@ -108,14 +108,14 @@ public class ArbitraryDataStoragePolicyTests extends Common {
// We should store and pre-fetch data for this transaction // We should store and pre-fetch data for this transaction
assertEquals(StoragePolicy.FOLLOWED, Settings.getInstance().getStoragePolicy()); assertEquals(StoragePolicy.FOLLOWED, Settings.getInstance().getStoragePolicy());
assertTrue(storageManager.canStoreData(arbitraryTransactionData)); assertTrue(storageManager.canStoreData(arbitraryTransactionData));
assertTrue(storageManager.shouldPreFetchData(repository, arbitraryTransactionData).isPass()); assertTrue(storageManager.shouldPreFetchData(repository, arbitraryTransactionData));
// Now unfollow the name // Now unfollow the name
assertTrue(ResourceListManager.getInstance().removeFromList("followedNames", name, false)); assertTrue(ResourceListManager.getInstance().removeFromList("followedNames", name, false));
// We shouldn't store or pre-fetch data for this transaction // We shouldn't store or pre-fetch data for this transaction
assertFalse(storageManager.canStoreData(arbitraryTransactionData)); assertFalse(storageManager.canStoreData(arbitraryTransactionData));
assertFalse(storageManager.shouldPreFetchData(repository, arbitraryTransactionData).isPass()); assertFalse(storageManager.shouldPreFetchData(repository, arbitraryTransactionData));
} }
} }
@ -143,14 +143,14 @@ public class ArbitraryDataStoragePolicyTests extends Common {
// We should store but not pre-fetch data for this transaction // We should store but not pre-fetch data for this transaction
assertEquals(StoragePolicy.VIEWED, Settings.getInstance().getStoragePolicy()); assertEquals(StoragePolicy.VIEWED, Settings.getInstance().getStoragePolicy());
assertTrue(storageManager.canStoreData(arbitraryTransactionData)); assertTrue(storageManager.canStoreData(arbitraryTransactionData));
assertFalse(storageManager.shouldPreFetchData(repository, arbitraryTransactionData).isPass()); assertFalse(storageManager.shouldPreFetchData(repository, arbitraryTransactionData));
// Now unfollow the name // Now unfollow the name
assertTrue(ResourceListManager.getInstance().removeFromList("followedNames", name, false)); assertTrue(ResourceListManager.getInstance().removeFromList("followedNames", name, false));
// We should store but not pre-fetch data for this transaction // We should store but not pre-fetch data for this transaction
assertTrue(storageManager.canStoreData(arbitraryTransactionData)); assertTrue(storageManager.canStoreData(arbitraryTransactionData));
assertFalse(storageManager.shouldPreFetchData(repository, arbitraryTransactionData).isPass()); assertFalse(storageManager.shouldPreFetchData(repository, arbitraryTransactionData));
} }
} }
@ -178,14 +178,14 @@ public class ArbitraryDataStoragePolicyTests extends Common {
// We should store and pre-fetch data for this transaction // We should store and pre-fetch data for this transaction
assertEquals(StoragePolicy.ALL, Settings.getInstance().getStoragePolicy()); assertEquals(StoragePolicy.ALL, Settings.getInstance().getStoragePolicy());
assertTrue(storageManager.canStoreData(arbitraryTransactionData)); assertTrue(storageManager.canStoreData(arbitraryTransactionData));
assertTrue(storageManager.shouldPreFetchData(repository, arbitraryTransactionData).isPass()); assertTrue(storageManager.shouldPreFetchData(repository, arbitraryTransactionData));
// Now unfollow the name // Now unfollow the name
assertTrue(ResourceListManager.getInstance().removeFromList("followedNames", name, false)); assertTrue(ResourceListManager.getInstance().removeFromList("followedNames", name, false));
// We should store and pre-fetch data for this transaction // We should store and pre-fetch data for this transaction
assertTrue(storageManager.canStoreData(arbitraryTransactionData)); assertTrue(storageManager.canStoreData(arbitraryTransactionData));
assertTrue(storageManager.shouldPreFetchData(repository, arbitraryTransactionData).isPass()); assertTrue(storageManager.shouldPreFetchData(repository, arbitraryTransactionData));
} }
} }
@ -213,14 +213,14 @@ public class ArbitraryDataStoragePolicyTests extends Common {
// We shouldn't store or pre-fetch data for this transaction // We shouldn't store or pre-fetch data for this transaction
assertEquals(StoragePolicy.NONE, Settings.getInstance().getStoragePolicy()); assertEquals(StoragePolicy.NONE, Settings.getInstance().getStoragePolicy());
assertFalse(storageManager.canStoreData(arbitraryTransactionData)); assertFalse(storageManager.canStoreData(arbitraryTransactionData));
assertFalse(storageManager.shouldPreFetchData(repository, arbitraryTransactionData).isPass()); assertFalse(storageManager.shouldPreFetchData(repository, arbitraryTransactionData));
// Now unfollow the name // Now unfollow the name
assertTrue(ResourceListManager.getInstance().removeFromList("followedNames", name, false)); assertTrue(ResourceListManager.getInstance().removeFromList("followedNames", name, false));
// We shouldn't store or pre-fetch data for this transaction // We shouldn't store or pre-fetch data for this transaction
assertFalse(storageManager.canStoreData(arbitraryTransactionData)); assertFalse(storageManager.canStoreData(arbitraryTransactionData));
assertFalse(storageManager.shouldPreFetchData(repository, arbitraryTransactionData).isPass()); assertFalse(storageManager.shouldPreFetchData(repository, arbitraryTransactionData));
} }
} }
@ -236,7 +236,7 @@ public class ArbitraryDataStoragePolicyTests extends Common {
// We should store but not pre-fetch data for this transaction // We should store but not pre-fetch data for this transaction
assertTrue(storageManager.canStoreData(transactionData)); assertTrue(storageManager.canStoreData(transactionData));
assertFalse(storageManager.shouldPreFetchData(repository, transactionData).isPass()); assertFalse(storageManager.shouldPreFetchData(repository, transactionData));
} }
} }

View File

@ -27,7 +27,6 @@ import org.qortal.transaction.RegisterNameTransaction;
import org.qortal.utils.Base58; import org.qortal.utils.Base58;
import org.qortal.utils.NTP; import org.qortal.utils.NTP;
import java.io.File;
import java.io.IOException; import java.io.IOException;
import java.nio.file.Files; import java.nio.file.Files;
import java.nio.file.Path; import java.nio.file.Path;
@ -411,13 +410,13 @@ public class ArbitraryTransactionMetadataTests extends Common {
assertEquals(3, arbitraryDataFile.getMetadata().getFiles().size()); assertEquals(3, arbitraryDataFile.getMetadata().getFiles().size());
assertTrue(arbitraryDataFile.getMetadata().getFiles().contains("file.txt")); assertTrue(arbitraryDataFile.getMetadata().getFiles().contains("file.txt"));
assertTrue(arbitraryDataFile.getMetadata().getFiles().contains("image1.jpg")); assertTrue(arbitraryDataFile.getMetadata().getFiles().contains("image1.jpg"));
assertTrue(arbitraryDataFile.getMetadata().getFiles().contains("subdirectory" + File.separator + "config.json")); assertTrue(arbitraryDataFile.getMetadata().getFiles().contains("subdirectory/config.json"));
// Ensure the file list can be read back out again, when specified to be included // Ensure the file list can be read back out again, when specified to be included
ArbitraryResourceMetadata resourceMetadata = ArbitraryResourceMetadata.fromTransactionMetadata(arbitraryDataFile.getMetadata(), true); ArbitraryResourceMetadata resourceMetadata = ArbitraryResourceMetadata.fromTransactionMetadata(arbitraryDataFile.getMetadata(), true);
assertTrue(resourceMetadata.getFiles().contains("file.txt")); assertTrue(resourceMetadata.getFiles().contains("file.txt"));
assertTrue(resourceMetadata.getFiles().contains("image1.jpg")); assertTrue(resourceMetadata.getFiles().contains("image1.jpg"));
assertTrue(resourceMetadata.getFiles().contains("subdirectory" + File.separator + "config.json")); assertTrue(resourceMetadata.getFiles().contains("subdirectory/config.json"));
// Ensure it's not returned when specified to be excluded // Ensure it's not returned when specified to be excluded
// The entire object will be null because there is no metadata // The entire object will be null because there is no metadata

View File

@ -440,7 +440,7 @@ public class ArbitraryTransactionTests extends Common {
PrivateKeyAccount alice = Common.getTestAccount(repository, "alice"); PrivateKeyAccount alice = Common.getTestAccount(repository, "alice");
String publicKey58 = Base58.encode(alice.getPublicKey()); String publicKey58 = Base58.encode(alice.getPublicKey());
String name = "TEST-testOnChainData"; // Can be anything for this test String name = "TEST"; // Can be anything for this test
String identifier = null; // Not used for this test String identifier = null; // Not used for this test
Service service = Service.ARBITRARY_DATA; Service service = Service.ARBITRARY_DATA;
int chunkSize = 1000; int chunkSize = 1000;

View File

@ -218,8 +218,6 @@ public class AtRepositoryTests extends Common {
List<ATStateData> atStates = repository.getATRepository().getMatchingFinalATStates( List<ATStateData> atStates = repository.getATRepository().getMatchingFinalATStates(
codeHash, codeHash,
null,
null,
isFinished, isFinished,
dataByteOffset, dataByteOffset,
expectedValue, expectedValue,
@ -266,8 +264,6 @@ public class AtRepositoryTests extends Common {
List<ATStateData> atStates = repository.getATRepository().getMatchingFinalATStates( List<ATStateData> atStates = repository.getATRepository().getMatchingFinalATStates(
codeHash, codeHash,
null,
null,
isFinished, isFinished,
dataByteOffset, dataByteOffset,
expectedValue, expectedValue,

View File

@ -1,58 +0,0 @@
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());
}
}

View File

@ -6,7 +6,6 @@ import org.qortal.group.Group;
import org.qortal.group.Group.ApprovalThreshold; import org.qortal.group.Group.ApprovalThreshold;
import org.qortal.repository.DataException; import org.qortal.repository.DataException;
import org.qortal.repository.Repository; import org.qortal.repository.Repository;
import org.qortal.test.common.transaction.TestTransaction;
import org.qortal.transaction.Transaction.ApprovalStatus; import org.qortal.transaction.Transaction.ApprovalStatus;
import org.qortal.utils.Amounts; import org.qortal.utils.Amounts;
@ -15,18 +14,9 @@ public class GroupUtils {
public static final int txGroupId = Group.NO_GROUP; public static final int txGroupId = Group.NO_GROUP;
public static final long fee = 1L * Amounts.MULTIPLIER; public static final long fee = 1L * Amounts.MULTIPLIER;
public static int createGroup(Repository repository, Object creatorAccountName, String groupName, boolean isOpen, ApprovalThreshold approvalThreshold, public static int createGroup(Repository repository, String creatorAccountName, String groupName, boolean isOpen, ApprovalThreshold approvalThreshold,
int minimumBlockDelay, int maximumBlockDelay) throws DataException { int minimumBlockDelay, int maximumBlockDelay) throws DataException {
PrivateKeyAccount account = Common.getTestAccount(repository, creatorAccountName);
PrivateKeyAccount account;
if (creatorAccountName instanceof java.lang.String) {
account = Common.getTestAccount(repository, (String) creatorAccountName);
}
else if (creatorAccountName instanceof PrivateKeyAccount) {
account = (PrivateKeyAccount) creatorAccountName;
} else {
account = null;
}
byte[] reference = account.getLastReference(); byte[] reference = account.getLastReference();
long timestamp = repository.getTransactionRepository().fromSignature(reference).getTimestamp() + 1; long timestamp = repository.getTransactionRepository().fromSignature(reference).getTimestamp() + 1;
@ -40,49 +30,16 @@ public class GroupUtils {
return repository.getGroupRepository().fromGroupName(groupName).getGroupId(); return repository.getGroupRepository().fromGroupName(groupName).getGroupId();
} }
/**
* <p> Simplified GroupCreation for Testing - less parameters required
* </p>
* @param repository The blockchain database
* @param owner Who will own the group, type PrivateKeyAccount
* @param groupName String representing the published name
* @param isOpen Boolean to allow anyone to join
* @return groupID as an integer
* @throws DataException when error occurs
* @since v4.71
*/
public static int createGroup(Repository repository, PrivateKeyAccount owner, String groupName, boolean isOpen) throws DataException {
String description = groupName + " (description)";
Group.ApprovalThreshold approvalThreshold = Group.ApprovalThreshold.ONE;
int minimumBlockDelay = 10;
int maximumBlockDelay = 1440;
return createGroup(repository, owner, groupName, isOpen, approvalThreshold, minimumBlockDelay, maximumBlockDelay);
} // End Simplified Group Creation
/**
*
* @param repository The block chain database
* @param joinerAccount Account of the person joining the group
* @param groupId Integer of the Group mapping
* @throws DataException
* @since v4.7.1
*/
public static void joinGroup(Repository repository, PrivateKeyAccount joinerAccount, int groupId) throws DataException {
byte[] reference = joinerAccount.getLastReference();
long timestamp = repository.getTransactionRepository().fromSignature(reference).getTimestamp() + 1;
BaseTransactionData baseTransactionData = new BaseTransactionData(timestamp, Group.NO_GROUP, reference, joinerAccount.getPublicKey(), GroupUtils.fee, null);
TransactionData transactionData = new JoinGroupTransactionData(baseTransactionData, groupId);
TransactionUtils.signAndMint(repository, transactionData, joinerAccount);
}
public static void joinGroup(Repository repository, String joinerAccountName, int groupId) throws DataException { public static void joinGroup(Repository repository, String joinerAccountName, int groupId) throws DataException {
PrivateKeyAccount account = Common.getTestAccount(repository, joinerAccountName); PrivateKeyAccount account = Common.getTestAccount(repository, joinerAccountName);
joinGroup(repository, account, groupId); byte[] reference = account.getLastReference();
long timestamp = repository.getTransactionRepository().fromSignature(reference).getTimestamp() + 1;
BaseTransactionData baseTransactionData = new BaseTransactionData(timestamp, Group.NO_GROUP, reference, account.getPublicKey(), GroupUtils.fee, null);
TransactionData transactionData = new JoinGroupTransactionData(baseTransactionData, groupId);
TransactionUtils.signAndMint(repository, transactionData, account);
} }
public static void approveTransaction(Repository repository, String accountName, byte[] pendingSignature, boolean decision) throws DataException { public static void approveTransaction(Repository repository, String accountName, byte[] pendingSignature, boolean decision) throws DataException {

View File

@ -11,7 +11,6 @@ import org.qortal.repository.Repository;
import org.qortal.repository.RepositoryManager; import org.qortal.repository.RepositoryManager;
import org.qortal.test.common.BlockUtils; import org.qortal.test.common.BlockUtils;
import org.qortal.test.common.Common; import org.qortal.test.common.Common;
import org.qortal.test.common.GroupUtils;
import org.qortal.test.common.TransactionUtils; import org.qortal.test.common.TransactionUtils;
import org.qortal.test.common.transaction.TestTransaction; import org.qortal.test.common.transaction.TestTransaction;
import org.qortal.transaction.Transaction.ValidationResult; import org.qortal.transaction.Transaction.ValidationResult;
@ -79,7 +78,7 @@ public class AdminTests extends Common {
int groupId = createGroup(repository, alice, "open-group", true); int groupId = createGroup(repository, alice, "open-group", true);
// Bob to join // Bob to join
GroupUtils.joinGroup(repository, bob, groupId); joinGroup(repository, bob, groupId);
// Promote Bob to admin // Promote Bob to admin
addGroupAdmin(repository, alice, groupId, bob.getAddress()); addGroupAdmin(repository, alice, groupId, bob.getAddress());

View File

@ -4,11 +4,7 @@ import org.junit.After;
import org.junit.Before; import org.junit.Before;
import org.junit.Test; import org.junit.Test;
import org.qortal.account.PrivateKeyAccount; 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.data.transaction.*;
import org.qortal.group.Group;
import org.qortal.repository.DataException; import org.qortal.repository.DataException;
import org.qortal.repository.Repository; import org.qortal.repository.Repository;
import org.qortal.repository.RepositoryManager; import org.qortal.repository.RepositoryManager;
@ -20,8 +16,6 @@ import org.qortal.test.common.transaction.TestTransaction;
import org.qortal.transaction.Transaction; import org.qortal.transaction.Transaction;
import org.qortal.transaction.Transaction.ValidationResult; import org.qortal.transaction.Transaction.ValidationResult;
import java.util.List;
import static org.junit.Assert.*; import static org.junit.Assert.*;
/** /**
@ -46,14 +40,8 @@ import static org.junit.Assert.*;
*/ */
public class DevGroupAdminTests extends Common { public class DevGroupAdminTests extends Common {
public static final int NULL_GROUP_MEMBERSHIP_HEIGHT = BlockChain.getInstance().getNullGroupMembershipHeight();
private static final int DEV_GROUP_ID = 1; 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 @Before
public void beforeTest() throws DataException { public void beforeTest() throws DataException {
Common.useDefaultSettings(); Common.useDefaultSettings();
@ -67,8 +55,8 @@ public class DevGroupAdminTests extends Common {
@Test @Test
public void testGroupKickMember() throws DataException { public void testGroupKickMember() throws DataException {
try (final Repository repository = RepositoryManager.getRepository()) { try (final Repository repository = RepositoryManager.getRepository()) {
PrivateKeyAccount alice = Common.getTestAccount(repository, ALICE); PrivateKeyAccount alice = Common.getTestAccount(repository, "alice");
PrivateKeyAccount bob = Common.getTestAccount(repository, BOB); PrivateKeyAccount bob = Common.getTestAccount(repository, "bob");
// Dev group // Dev group
int groupId = DEV_GROUP_ID; int groupId = DEV_GROUP_ID;
@ -92,10 +80,16 @@ public class DevGroupAdminTests extends Common {
// Attempt to kick Bob // Attempt to kick Bob
result = groupKick(repository, alice, groupId, bob.getAddress()); result = groupKick(repository, alice, groupId, bob.getAddress());
// Should not be OK, cannot kick member out of null owned group // Should be OK
assertNotSame(ValidationResult.OK, result); assertEquals(ValidationResult.OK, result);
// Confirm Bob remains a member // Confirm Bob no longer a member
assertFalse(isMember(repository, bob.getAddress(), groupId));
// Orphan last block
BlockUtils.orphanLastBlock(repository);
// Confirm Bob now a member
assertTrue(isMember(repository, bob.getAddress(), groupId)); assertTrue(isMember(repository, bob.getAddress(), groupId));
} }
} }
@ -103,8 +97,8 @@ public class DevGroupAdminTests extends Common {
@Test @Test
public void testGroupKickAdmin() throws DataException { public void testGroupKickAdmin() throws DataException {
try (final Repository repository = RepositoryManager.getRepository()) { try (final Repository repository = RepositoryManager.getRepository()) {
PrivateKeyAccount alice = Common.getTestAccount(repository, ALICE); PrivateKeyAccount alice = Common.getTestAccount(repository, "alice");
PrivateKeyAccount bob = Common.getTestAccount(repository, BOB); PrivateKeyAccount bob = Common.getTestAccount(repository, "bob");
// Dev group // Dev group
int groupId = DEV_GROUP_ID; int groupId = DEV_GROUP_ID;
@ -129,7 +123,7 @@ public class DevGroupAdminTests extends Common {
assertEquals("incorrect transaction approval status", Transaction.ApprovalStatus.PENDING, approvalStatus); assertEquals("incorrect transaction approval status", Transaction.ApprovalStatus.PENDING, approvalStatus);
// Have Alice approve Bob's approval-needed transaction // 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 // Mint a block so that the transaction becomes approved
BlockUtils.mintBlock(repository); BlockUtils.mintBlock(repository);
@ -173,8 +167,8 @@ public class DevGroupAdminTests extends Common {
@Test @Test
public void testGroupBanMember() throws DataException { public void testGroupBanMember() throws DataException {
try (final Repository repository = RepositoryManager.getRepository()) { try (final Repository repository = RepositoryManager.getRepository()) {
PrivateKeyAccount alice = Common.getTestAccount(repository, ALICE); PrivateKeyAccount alice = Common.getTestAccount(repository, "alice");
PrivateKeyAccount bob = Common.getTestAccount(repository, BOB); PrivateKeyAccount bob = Common.getTestAccount(repository, "bob");
// Dev group // Dev group
int groupId = DEV_GROUP_ID; int groupId = DEV_GROUP_ID;
@ -189,13 +183,18 @@ public class DevGroupAdminTests extends Common {
// Attempt to ban Bob // Attempt to ban Bob
result = groupBan(repository, alice, groupId, bob.getAddress()); result = groupBan(repository, alice, groupId, bob.getAddress());
// Should not be OK, cannot ban someone from a null owned group // Should be OK
assertEquals(ValidationResult.OK, result);
// Bob attempts to rejoin
result = joinGroup(repository, bob, groupId);
// Should NOT be OK
assertNotSame(ValidationResult.OK, result); assertNotSame(ValidationResult.OK, result);
// Bob attempts to join // Orphan last block (Bob ban)
result = joinGroup(repository, bob, groupId); BlockUtils.orphanLastBlock(repository);
// Should be OK, but won't actually get him in the group // Delete unconfirmed group-ban transaction
assertEquals(ValidationResult.OK, result); TransactionUtils.deleteUnconfirmedTransactions(repository);
// Confirm Bob is not a member // Confirm Bob is not a member
assertFalse(isMember(repository, bob.getAddress(), groupId)); assertFalse(isMember(repository, bob.getAddress(), groupId));
@ -205,38 +204,65 @@ public class DevGroupAdminTests extends Common {
// Bob to join // Bob to join
result = joinGroup(repository, bob, groupId); result = joinGroup(repository, bob, groupId);
// Should not be OK, bob should already be a member, he joined before the invite and // Should be OK
// the invite served as an approval assertEquals(ValidationResult.OK, result);
assertEquals(ValidationResult.ALREADY_GROUP_MEMBER, result);
// Confirm Bob now a member, now that he got an invite // Confirm Bob now a member
assertTrue(isMember(repository, bob.getAddress(), groupId)); assertTrue(isMember(repository, bob.getAddress(), groupId));
// Attempt to ban Bob // Attempt to ban Bob
result = groupBan(repository, alice, groupId, bob.getAddress()); result = groupBan(repository, alice, groupId, bob.getAddress());
// Should not be OK, because you can ban a member of a null owned group // Should be OK
assertNotSame(ValidationResult.OK, result); assertEquals(ValidationResult.OK, result);
// Confirm Bob is still a member // Confirm Bob no longer a member
assertTrue(isMember(repository, bob.getAddress(), groupId)); assertFalse(isMember(repository, bob.getAddress(), groupId));
// Bob attempts to rejoin // Bob attempts to rejoin
result = joinGroup(repository, bob, groupId); result = joinGroup(repository, bob, groupId);
// Should NOT be OK, because he is already a member // Should NOT be OK
assertNotSame(ValidationResult.OK, result); assertNotSame(ValidationResult.OK, result);
// Cancel Bob's ban // Cancel Bob's ban
result = cancelGroupBan(repository, alice, groupId, bob.getAddress()); result = cancelGroupBan(repository, alice, groupId, bob.getAddress());
// Should not be OK, because there was no ban to begin with // 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
assertNotSame(ValidationResult.OK, result); 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 @Test
public void testGroupBanAdmin() throws DataException { public void testGroupBanAdmin() throws DataException {
try (final Repository repository = RepositoryManager.getRepository()) { try (final Repository repository = RepositoryManager.getRepository()) {
PrivateKeyAccount alice = Common.getTestAccount(repository, ALICE); PrivateKeyAccount alice = Common.getTestAccount(repository, "alice");
PrivateKeyAccount bob = Common.getTestAccount(repository, BOB); PrivateKeyAccount bob = Common.getTestAccount(repository, "bob");
// Dev group // Dev group
int groupId = DEV_GROUP_ID; int groupId = DEV_GROUP_ID;
@ -260,7 +286,7 @@ public class DevGroupAdminTests extends Common {
assertEquals("incorrect transaction approval status", Transaction.ApprovalStatus.PENDING, approvalStatus); assertEquals("incorrect transaction approval status", Transaction.ApprovalStatus.PENDING, approvalStatus);
// Have Alice approve Bob's approval-needed transaction // 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 // Mint a block so that the transaction becomes approved
BlockUtils.mintBlock(repository); BlockUtils.mintBlock(repository);
@ -295,322 +321,6 @@ 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 { private ValidationResult joinGroup(Repository repository, PrivateKeyAccount joiner, int groupId) throws DataException {
JoinGroupTransactionData transactionData = new JoinGroupTransactionData(TestTransaction.generateBase(joiner), groupId); JoinGroupTransactionData transactionData = new JoinGroupTransactionData(TestTransaction.generateBase(joiner), groupId);
@ -622,31 +332,9 @@ public class DevGroupAdminTests extends Common {
return result; return result;
} }
private ValidationResult groupInvite(Repository repository, PrivateKeyAccount admin, int groupId, String invitee, int timeToLive) throws DataException { private void groupInvite(Repository repository, PrivateKeyAccount admin, int groupId, String invitee, int timeToLive) throws DataException {
GroupInviteTransactionData transactionData = new GroupInviteTransactionData(TestTransaction.generateBase(admin), groupId, invitee, timeToLive); 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); 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 { private ValidationResult groupKick(Repository repository, PrivateKeyAccount admin, int groupId, String member) throws DataException {
@ -659,13 +347,6 @@ public class DevGroupAdminTests extends Common {
return result; 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 { private ValidationResult groupBan(Repository repository, PrivateKeyAccount admin, int groupId, String member) throws DataException {
GroupBanTransactionData transactionData = new GroupBanTransactionData(TestTransaction.generateBase(admin), groupId, member, "testing", 0); GroupBanTransactionData transactionData = new GroupBanTransactionData(TestTransaction.generateBase(admin), groupId, member, "testing", 0);
ValidationResult result = TransactionUtils.signAndImport(repository, transactionData, admin); ValidationResult result = TransactionUtils.signAndImport(repository, transactionData, admin);
@ -676,13 +357,6 @@ public class DevGroupAdminTests extends Common {
return result; 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 { private ValidationResult cancelGroupBan(Repository repository, PrivateKeyAccount admin, int groupId, String member) throws DataException {
CancelGroupBanTransactionData transactionData = new CancelGroupBanTransactionData(TestTransaction.generateBase(admin), groupId, member); CancelGroupBanTransactionData transactionData = new CancelGroupBanTransactionData(TestTransaction.generateBase(admin), groupId, member);
ValidationResult result = TransactionUtils.signAndImport(repository, transactionData, admin); ValidationResult result = TransactionUtils.signAndImport(repository, transactionData, admin);
@ -693,14 +367,6 @@ public class DevGroupAdminTests extends Common {
return result; 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 { private TransactionData addGroupAdmin(Repository repository, PrivateKeyAccount owner, int groupId, String member) throws DataException {
AddGroupAdminTransactionData transactionData = new AddGroupAdminTransactionData(TestTransaction.generateBase(owner), groupId, member); AddGroupAdminTransactionData transactionData = new AddGroupAdminTransactionData(TestTransaction.generateBase(owner), groupId, member);
transactionData.setTxGroupId(groupId); transactionData.setTxGroupId(groupId);

View File

@ -5,14 +5,15 @@ import org.junit.Before;
import org.junit.Test; import org.junit.Test;
import org.qortal.account.PrivateKeyAccount; import org.qortal.account.PrivateKeyAccount;
import org.qortal.data.transaction.AddGroupAdminTransactionData; import org.qortal.data.transaction.AddGroupAdminTransactionData;
import org.qortal.data.transaction.CreateGroupTransactionData;
import org.qortal.data.transaction.JoinGroupTransactionData; import org.qortal.data.transaction.JoinGroupTransactionData;
import org.qortal.data.transaction.RemoveGroupAdminTransactionData; import org.qortal.data.transaction.RemoveGroupAdminTransactionData;
import org.qortal.group.Group.ApprovalThreshold;
import org.qortal.repository.DataException; import org.qortal.repository.DataException;
import org.qortal.repository.Repository; import org.qortal.repository.Repository;
import org.qortal.repository.RepositoryManager; import org.qortal.repository.RepositoryManager;
import org.qortal.test.common.BlockUtils; import org.qortal.test.common.BlockUtils;
import org.qortal.test.common.Common; import org.qortal.test.common.Common;
import org.qortal.test.common.GroupUtils;
import org.qortal.test.common.TransactionUtils; import org.qortal.test.common.TransactionUtils;
import org.qortal.test.common.transaction.TestTransaction; import org.qortal.test.common.transaction.TestTransaction;
import org.qortal.transaction.Transaction.ValidationResult; import org.qortal.transaction.Transaction.ValidationResult;
@ -38,7 +39,7 @@ public class OwnerTests extends Common {
PrivateKeyAccount bob = Common.getTestAccount(repository, "bob"); PrivateKeyAccount bob = Common.getTestAccount(repository, "bob");
// Create group // Create group
int groupId = GroupUtils.createGroup(repository, alice, "open-group", true); int groupId = createGroup(repository, alice, "open-group", true);
// Attempt to promote non-member // Attempt to promote non-member
ValidationResult result = addGroupAdmin(repository, alice, groupId, bob.getAddress()); ValidationResult result = addGroupAdmin(repository, alice, groupId, bob.getAddress());
@ -82,7 +83,7 @@ public class OwnerTests extends Common {
PrivateKeyAccount bob = Common.getTestAccount(repository, "bob"); PrivateKeyAccount bob = Common.getTestAccount(repository, "bob");
// Create group // Create group
int groupId = GroupUtils.createGroup(repository, alice, "open-group", true); int groupId = createGroup(repository, alice, "open-group", true);
// Attempt to demote non-member // Attempt to demote non-member
ValidationResult result = removeGroupAdmin(repository, alice, groupId, bob.getAddress()); ValidationResult result = removeGroupAdmin(repository, alice, groupId, bob.getAddress());
@ -132,6 +133,19 @@ public class OwnerTests extends Common {
} }
} }
private Integer createGroup(Repository repository, PrivateKeyAccount owner, String groupName, boolean isOpen) throws DataException {
String description = groupName + " (description)";
ApprovalThreshold approvalThreshold = ApprovalThreshold.ONE;
int minimumBlockDelay = 10;
int maximumBlockDelay = 1440;
CreateGroupTransactionData transactionData = new CreateGroupTransactionData(TestTransaction.generateBase(owner), groupName, description, isOpen, approvalThreshold, minimumBlockDelay, maximumBlockDelay);
TransactionUtils.signAndMint(repository, transactionData, owner);
return repository.getGroupRepository().fromGroupName(groupName).getGroupId();
}
private ValidationResult joinGroup(Repository repository, PrivateKeyAccount joiner, int groupId) throws DataException { private ValidationResult joinGroup(Repository repository, PrivateKeyAccount joiner, int groupId) throws DataException {
JoinGroupTransactionData transactionData = new JoinGroupTransactionData(TestTransaction.generateBase(joiner), groupId); JoinGroupTransactionData transactionData = new JoinGroupTransactionData(TestTransaction.generateBase(joiner), groupId);
ValidationResult result = TransactionUtils.signAndImport(repository, transactionData, joiner); ValidationResult result = TransactionUtils.signAndImport(repository, transactionData, joiner);

View File

@ -26,7 +26,6 @@ public class HSQLDBCacheUtilsTests {
private static final String DESCRIPTION = "description"; private static final String DESCRIPTION = "description";
private static final String PREFIX_ONLY = "prefixOnly"; private static final String PREFIX_ONLY = "prefixOnly";
private static final String EXACT_MATCH_NAMES = "exactMatchNames"; private static final String EXACT_MATCH_NAMES = "exactMatchNames";
private static final String KEYWORDS = "keywords";
private static final String DEFAULT_RESOURCE = "defaultResource"; private static final String DEFAULT_RESOURCE = "defaultResource";
private static final String MODE = "mode"; private static final String MODE = "mode";
private static final String MIN_LEVEL = "minLevel"; private static final String MIN_LEVEL = "minLevel";
@ -300,19 +299,6 @@ public class HSQLDBCacheUtilsTests {
); );
} }
@Test
public void testAfterNegative() {
ArbitraryResourceData data = new ArbitraryResourceData();
data.created = 10L;
data.name = "Joe";
filterListByMap(
List.of(data),
NAME_LEVEL, new HashMap<>(Map.of(AFTER, 11L)),
0
);
}
@Test @Test
public void testBeforePositive(){ public void testBeforePositive(){
ArbitraryResourceData data = new ArbitraryResourceData(); ArbitraryResourceData data = new ArbitraryResourceData();
@ -326,19 +312,6 @@ public class HSQLDBCacheUtilsTests {
); );
} }
@Test
public void testBeforeNegative(){
ArbitraryResourceData data = new ArbitraryResourceData();
data.created = 10L;
data.name = "Joe";
filterListByMap(
List.of(data),
NAME_LEVEL, new HashMap<>(Map.of(BEFORE, 9L)),
0
);
}
@Test @Test
public void testTitlePositive() { public void testTitlePositive() {
@ -369,25 +342,6 @@ public class HSQLDBCacheUtilsTests {
); );
} }
@Test
public void testMetadataNullificationBugSolution(){
ArbitraryResourceData data = new ArbitraryResourceData();
data.metadata = new ArbitraryResourceMetadata();
data.metadata.setDescription("Once upon a time.");
data.name = "Joe";
List<ArbitraryResourceData> list = List.of(data);
filterListByMap(
List.of(data),
NAME_LEVEL, new HashMap<>(Map.of(DESCRIPTION, "Once upon a time.")),
1
);
Assert.assertNotNull(data.metadata);
}
@Test @Test
public void testMinLevelPositive() { public void testMinLevelPositive() {
@ -445,7 +399,7 @@ public class HSQLDBCacheUtilsTests {
} }
@Test @Test
public void testExcludeBlockedNegative() { public void testExcludeBlockedPositive() {
ArbitraryResourceData data = new ArbitraryResourceData(); ArbitraryResourceData data = new ArbitraryResourceData();
data.name = "Joe"; data.name = "Joe";
@ -459,21 +413,6 @@ 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 @Test
public void testIncludeMetadataPositive() { public void testIncludeMetadataPositive() {
@ -661,7 +600,6 @@ public class HSQLDBCacheUtilsTests {
Optional<String> description = Optional.ofNullable((String) valueByKey.get(DESCRIPTION)); Optional<String> description = Optional.ofNullable((String) valueByKey.get(DESCRIPTION));
boolean prefixOnly = valueByKey.containsKey(PREFIX_ONLY); boolean prefixOnly = valueByKey.containsKey(PREFIX_ONLY);
Optional<List<String>> exactMatchNames = Optional.ofNullable((List<String>) valueByKey.get(EXACT_MATCH_NAMES)); Optional<List<String>> exactMatchNames = Optional.ofNullable((List<String>) valueByKey.get(EXACT_MATCH_NAMES));
Optional<List<String>> keywords = Optional.ofNullable((List<String>) valueByKey.get(KEYWORDS));
boolean defaultResource = valueByKey.containsKey(DEFAULT_RESOURCE); boolean defaultResource = valueByKey.containsKey(DEFAULT_RESOURCE);
Optional<SearchMode> mode = Optional.of((SearchMode) valueByKey.getOrDefault(MODE, SearchMode.ALL)); Optional<SearchMode> mode = Optional.of((SearchMode) valueByKey.getOrDefault(MODE, SearchMode.ALL));
Optional<Integer> minLevel = Optional.ofNullable((Integer) valueByKey.get(MIN_LEVEL)); Optional<Integer> minLevel = Optional.ofNullable((Integer) valueByKey.get(MIN_LEVEL));
@ -688,7 +626,6 @@ public class HSQLDBCacheUtilsTests {
description, description,
prefixOnly, prefixOnly,
exactMatchNames, exactMatchNames,
keywords,
defaultResource, defaultResource,
minLevel, minLevel,
followedOnly, followedOnly,

Some files were not shown because too many files have changed in this diff Show More