Compare commits

...

78 Commits

Author SHA1 Message Date
aeab1acbbc Bump version to 4.7.1 2025-04-08 20:13:43 -07:00
crowetic
0b37666d2b
Merge pull request #250 from kennycud/master
Merging current 'test release' from kennycud repo after extensive testing by community.
2025-04-08 08:34:07 -07:00
kennycud
bcf3538d18 dd cache enabled to true by default 2025-04-07 12:19:56 -07:00
kennycud
b2d9d0539e removed cache orphaning, crowetic and I agree it should have never been added to begin with 2025-04-05 11:42:21 -07:00
kennycud
1bd6076e33 forgot IndexCache.java in the last commit
replaced index service attribute with a category attribute and reduced index attribute names to single characters to reduce memory footprint, t is for term, n is for name, c is for category, l if for link

changed default indexing frequency from 1 minute to 10 minutes to reduce memory use

added arbitrary resource endpoint for index search by issuer name and index prefix

added some additional error handling concerning unrecognized properties in the indices
2025-04-03 10:23:58 -07:00
kennycud
a6309e925b replaced index service attribute with a category attribute and reduced index attribute names to single characters to reduce memory footprint, t is for term, n is for name, c is for category, l if for link
changed default indexing frequency from 1 minute to 10 minutes to reduce memory use

added arbitrary resource endpoint for index search by issuer name and index prefix

added some additional error handling concerning unrecognized properties in the indices
2025-04-03 10:18:45 -07:00
kennycud
23de8a98bc removed logging 2025-03-21 18:59:32 -07:00
kennycud
d0a85d4717 QDN bug resolution 2025-03-21 18:44:41 -07:00
kennycud
a893888a2e reduced logging level for invalid formatting 2025-03-21 18:43:06 -07:00
kennycud
bd4472c2c0
Merge pull request #5 from Philreact/feature/search-keywords
added keywords to qortalRequest
2025-03-19 17:37:14 -07:00
kennycud
10dda255e2 added rebuild arbitrary rebuild resource timer task 2025-03-16 18:49:28 -07:00
kennycud
934c23402a added logging, so we can better understand the exception thrown 2025-03-16 18:47:59 -07:00
kennycud
4188f18a9a added error handling 2025-03-16 18:46:38 -07:00
kennycud
e48fd96c1e nullified impossible time constraints 2025-03-14 14:15:18 -07:00
kennycud
e76694e214 implemented before and after filtering 2025-03-13 13:46:17 -07:00
kennycud
dbf49309ec added some critical exception handling for arbitrary data indexing support 2025-03-12 14:24:23 -07:00
kennycud
ab4730cef0 initial implementation of arbitrary data indexing support 2025-03-12 11:21:57 -07:00
kennycud
7f3c1d553f removed name based arbitrary resource storage capacity limits and added arbitrary resource cache rebuild logging verbosity 2025-03-10 15:17:04 -07:00
ab0ef85458 added keywords to SEARCH_QDN_RESOURCES 2025-03-08 20:43:34 +02:00
b64674783a Merge remote-tracking branch 'kenny/master' into feature/search-keywords 2025-03-08 20:19:56 +02:00
kennycud
92fb52220a
Merge pull request #4 from Philreact/feature/search-keywords
Feature/search keywords
2025-03-06 07:10:26 -08:00
kennycud
2d0bdca8dc
Merge pull request #3 from Philreact/bugfix/get-qdn-resource-metadata
fix var bug for GET_QDN_RESOURCE_PROPERTIES
2025-03-06 07:06:43 -08:00
2e9f358d0b changed to list and added to cache 2025-03-06 16:10:30 +02:00
6a6380e9e7 Merge remote-tracking branch 'kenny/master' into feature/search-keywords 2025-03-06 14:02:36 +02:00
kennycud
11c2d1bd14 a solution for the metadata and status members getting nullified in the cache 2025-03-05 18:47:01 -08:00
1d79df078e Merge remote-tracking branch 'kenny/master' into feature/search-keywords 2025-03-05 21:14:47 +02:00
kennycud
4baafd1305 more arbitrary data optimizations, including the arbitrary resources cache rebuild and a setting to support it, added and removed notifications, added method to the arbitrary repository, also removed an unnecessary setting that was added in the last commit 2025-03-03 10:37:39 -08:00
f8cee2e0b7 Merge remote-tracking branch 'kenny/master' into feature/search-keywords 2025-02-27 18:36:00 +02:00
kennycud
676885ea2d optimized arbitrary metadata fetching, added arbitrary data cache manager notifications, removed redundant notifications, added method to arbitrary repository and a setting to support the optimization 2025-02-24 16:36:13 -08:00
kennycud
1f4ca6263f data monitor initial implementation 2025-02-19 17:18:05 -08:00
kennycud
df37372180 trade ledger export implementation, completed trades bug fix 2025-02-11 18:45:57 -08:00
086b0809d6 remove log 2025-02-08 22:20:03 +02:00
33650cc432 when the path is render/hash do not save path for nav history 2025-02-05 15:11:48 +02:00
c22abc440b change label 2025-02-04 18:02:56 +02:00
258eb3a0f9 added keywords query for arbitrary research search 2025-02-04 15:42:25 +02:00
kennycud
91ceafe0e3 supporting multiple minting groups instead of supporting one and only one minting group 2025-02-03 18:19:56 -08:00
kennycud
9017db725e Merge remote-tracking branch 'origin/master' 2025-02-01 18:44:17 -08:00
kennycud
a42f214358 invite orphan vulnerability patch, detailed test case coming in a commit soon 2025-02-01 18:43:48 -08:00
ecd4233dd0 fix fetch block qortalRequest 2025-01-26 00:13:58 +02:00
e5b6e893cd GET_AT missing a slash 2025-01-24 21:30:55 +02:00
9e45d640bc fix var bug 2025-01-23 23:52:39 +02:00
crowetic
faee7c8f6a
Merge pull request #247 from crowetic/master
push featureTrigger blocks back a bit to give more time to prepare+sign auto-update
2025-01-21 19:26:47 -08:00
ca238c995e push featureTrigger blocks back a bit to give more time for auto-update. 2025-01-21 19:12:17 -08:00
e434a28d00 Merge remote-tracking branch 'origin/master' 2025-01-21 19:11:19 -08:00
996d5e0e86 push featureTrigger blocks back a bit to give more time for auto-update. 2025-01-21 19:10:06 -08:00
8b797b5bd5 push featureTrigger blocks back a bit to give more time for auto-update. 2025-01-21 19:05:57 -08:00
crowetic
999cfafe00
Merge pull request #246 from crowetic/master
updates/fixes to publish-auto-update.pl
2025-01-21 18:24:13 -08:00
4991618f19 updates/fixes to publish-auto-update.pl 2025-01-21 18:22:25 -08:00
crowetic
4c35239bb1
Merge pull request #245 from crowetic/master
bump version to 4.7.0 and set featureTrigger block heights
2025-01-21 18:10:13 -08:00
d6cf45b311 bump version to 4.7.0 and set featureTrigger block heights 2025-01-21 18:07:25 -08:00
crowetic
ea9a24dca2
Merge pull request #244 from kennycud/master
Balance Recorder & Hard Forks
2025-01-21 17:35:26 -08:00
kennycud
72f0194487 get admin query fix and hardfork 2025-01-17 19:31:13 -08:00
kennycud
b2dbcbb603 made adjustments to support the ignore level feature trigger and removed the fail-safe feature trigger since the ignore level feature trigger now satisfies it implicitly 2025-01-13 13:52:17 -08:00
kennycud
69cba78d94 exclude blocked implementation completion 2025-01-11 19:01:13 -08:00
kennycud
70f4ff4fb3 ignore level for reward share feature hard fork 2025-01-11 18:20:28 -08:00
kennycud
a8a8904ebf removed the NULL account from the dev admin reward distribution and added some fail safes in case the admin groups are empty 2025-01-08 16:19:38 -08:00
kennycud
2805bb8364 corrected an arithmetic error 2025-01-07 13:20:39 -08:00
kennycud
d9a7648d36 access to decoded online accounts by block 2025-01-05 15:59:09 -08:00
kennycud
2392b7b155 system info and database connection status access 2025-01-05 13:49:31 -08:00
kennycud
f5d338435a Since the Groups table is now named Groups with back ticks, it is now case-sensitive. Since it is now case-sensitive it needs to be in all caps, so when other SQL statements call on this table using the Groups without backticks it will be compatible. When Groups is used in a statement without back ticks or quotes it automatically gets converted into capital letters. 2025-01-02 18:10:25 -08:00
kennycud
8f6b55a98b rollback the Groups table back quotes, because this only works with my testing environment and causes problems in production 2024-12-31 13:57:39 -08:00
kennycud
278243f01c rollback the negation of founder effective minting level, because I made it under the assumption that it was used for reward distributions when it is used for block signatures only 2024-12-31 13:54:20 -08:00
kennycud
756f3a243d negate founder effective minting level for admins replace founders hardfork 2024-12-30 18:36:44 -08:00
kennycud
950c4a5b35 Merge remote-tracking branch 'origin/master' 2024-12-30 16:06:24 -08:00
kennycud
ebc58c5c5c qualified Groups table name, so it will be compatible with HSQLDB updated release which uses Groups for as a reserved word 2024-12-30 16:01:53 -08:00
kennycud
8bbb994876
Merge pull request #2 from Philreact/master
added seller/buyer to filter completed trades
2024-12-30 12:19:01 -08:00
kennycud
c2ba9d142c crowetic's logging suggestions for the new reward distribution update 2024-12-30 12:15:27 -08:00
kennycud
a300ac2393 added capabilities for groups with null ownership including banning and kicking members and member ban cancellations; enforcing group approval thresholds to invites and invite cancellations; the established add and remove admin capabilities were used as guidance for this implementation; this was added as a hardfork to preserve group transactions from previous blocks 2024-12-29 18:08:04 -08:00
kennycud
bdbbd0152f updated the hard fork heights for the test chain 2024-12-28 14:01:01 -08:00
kennycud
45d88c1bac Admin share typo fix and new test case submission. 2024-12-26 14:40:44 -08:00
kennycud
3952705edd Admin replace founders hardfork and online validation fail-safe hardfork. 2024-12-26 13:53:00 -08:00
kennycud
4f0aabfb36 For Balance Recorder, reward recordings only, that is the default. 2024-12-25 13:24:24 -08:00
5ac0027b5a fix css for qdn resource loading 2024-12-25 09:16:35 +02:00
e9b75b051b added seller/buyer to filter completed trades 2024-12-24 14:39:31 +02:00
kennycud
c71f5fa8bf added another logging line to troubleshoot QDN problem 2024-12-13 15:21:51 -08:00
kennycud
5e145de52b Balance Recorder initial implementation. 2024-12-12 13:46:18 -08:00
kennycud
543d0a7d22 Merge remote-tracking branch 'origin/master' 2024-12-10 14:07:32 -08:00
kennycud
5346c97922 added logging to help solve the updated field problem, the problem is the updated field is not getting updated 2024-12-10 14:07:11 -08:00
85 changed files with 5854 additions and 553 deletions

View File

@ -3,7 +3,7 @@
<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.6.6</version> <version>4.7.1</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>

View File

@ -0,0 +1,173 @@
package org.hsqldb.jdbc;
import org.apache.logging.log4j.LogManager;
import org.apache.logging.log4j.Logger;
import org.hsqldb.jdbc.pool.JDBCPooledConnection;
import org.qortal.data.system.DbConnectionInfo;
import org.qortal.repository.hsqldb.HSQLDBRepositoryFactory;
import javax.sql.ConnectionEvent;
import javax.sql.PooledConnection;
import java.sql.Connection;
import java.sql.SQLException;
import java.util.Comparator;
import java.util.List;
import java.util.concurrent.ConcurrentHashMap;
import java.util.stream.Collectors;
/**
* Class HSQLDBPoolMonitored
*
* This class uses the same logic as HSQLDBPool. The only difference is it monitors the state of every connection
* to the database. This is used for debugging purposes only.
*/
public class HSQLDBPoolMonitored extends HSQLDBPool {
private static final Logger LOGGER = LogManager.getLogger(HSQLDBRepositoryFactory.class);
private static final String EMPTY = "Empty";
private static final String AVAILABLE = "Available";
private static final String ALLOCATED = "Allocated";
private ConcurrentHashMap<Integer, DbConnectionInfo> infoByIndex;
public HSQLDBPoolMonitored(int poolSize) {
super(poolSize);
this.infoByIndex = new ConcurrentHashMap<>(poolSize);
}
/**
* Tries to retrieve a new connection using the properties that have already been
* set.
*
* @return a connection to the data source, or null if no spare connections in pool
* @exception SQLException if a database access error occurs
*/
public Connection tryConnection() throws SQLException {
for (int i = 0; i < states.length(); i++) {
if (states.compareAndSet(i, RefState.available, RefState.allocated)) {
JDBCPooledConnection pooledConnection = connections[i];
if (pooledConnection == null)
// Probably shutdown situation
return null;
infoByIndex.put(i, new DbConnectionInfo(System.currentTimeMillis(), Thread.currentThread().getName(), ALLOCATED));
return pooledConnection.getConnection();
}
if (states.compareAndSet(i, RefState.empty, RefState.allocated)) {
try {
JDBCPooledConnection pooledConnection = (JDBCPooledConnection) source.getPooledConnection();
if (pooledConnection == null)
// Probably shutdown situation
return null;
pooledConnection.addConnectionEventListener(this);
pooledConnection.addStatementEventListener(this);
connections[i] = pooledConnection;
infoByIndex.put(i, new DbConnectionInfo(System.currentTimeMillis(), Thread.currentThread().getName(), ALLOCATED));
return pooledConnection.getConnection();
} catch (SQLException e) {
states.set(i, RefState.empty);
infoByIndex.put(i, new DbConnectionInfo(System.currentTimeMillis(), Thread.currentThread().getName(), EMPTY));
}
}
}
return null;
}
public Connection getConnection() throws SQLException {
int var1 = 300;
if (this.source.loginTimeout != 0) {
var1 = this.source.loginTimeout * 10;
}
if (this.closed) {
throw new SQLException("connection pool is closed");
} else {
for(int var2 = 0; var2 < var1; ++var2) {
for(int var3 = 0; var3 < this.states.length(); ++var3) {
if (this.states.compareAndSet(var3, 1, 2)) {
infoByIndex.put(var3, new DbConnectionInfo(System.currentTimeMillis(), Thread.currentThread().getName(), ALLOCATED));
return this.connections[var3].getConnection();
}
if (this.states.compareAndSet(var3, 0, 2)) {
try {
JDBCPooledConnection var4 = (JDBCPooledConnection)this.source.getPooledConnection();
var4.addConnectionEventListener(this);
var4.addStatementEventListener(this);
this.connections[var3] = var4;
infoByIndex.put(var3, new DbConnectionInfo(System.currentTimeMillis(), Thread.currentThread().getName(), ALLOCATED));
return this.connections[var3].getConnection();
} catch (SQLException var6) {
this.states.set(var3, 0);
infoByIndex.put(var3, new DbConnectionInfo(System.currentTimeMillis(), Thread.currentThread().getName(), EMPTY));
}
}
}
try {
Thread.sleep(100L);
} catch (InterruptedException var5) {
}
}
throw JDBCUtil.invalidArgument();
}
}
public void connectionClosed(ConnectionEvent event) {
PooledConnection connection = (PooledConnection) event.getSource();
for (int i = 0; i < connections.length; i++) {
if (connections[i] == connection) {
states.set(i, RefState.available);
infoByIndex.put(i, new DbConnectionInfo(System.currentTimeMillis(), Thread.currentThread().getName(), AVAILABLE));
break;
}
}
}
public void connectionErrorOccurred(ConnectionEvent event) {
PooledConnection connection = (PooledConnection) event.getSource();
for (int i = 0; i < connections.length; i++) {
if (connections[i] == connection) {
states.set(i, RefState.allocated);
connections[i] = null;
states.set(i, RefState.empty);
infoByIndex.put(i, new DbConnectionInfo(System.currentTimeMillis(), Thread.currentThread().getName(), EMPTY));
break;
}
}
}
public List<DbConnectionInfo> getDbConnectionsStates() {
return infoByIndex.values().stream()
.sorted(Comparator.comparingLong(DbConnectionInfo::getUpdated))
.collect(Collectors.toList());
}
private int findConnectionIndex(ConnectionEvent connectionEvent) {
PooledConnection pooledConnection = (PooledConnection) connectionEvent.getSource();
for(int i = 0; i < this.connections.length; ++i) {
if (this.connections[i] == pooledConnection) {
return i;
}
}
return -1;
}
}

View File

@ -14,6 +14,7 @@ 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;
@ -216,9 +217,18 @@ 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();
int groupIdToMint = BlockChain.getInstance().getMintingGroupId(); List<Integer> groupIdsToMint = Groups.getGroupIdsToMint( BlockChain.getInstance(), blockchainHeight );
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();
@ -252,9 +262,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 || groupRepository.memberExists(groupIdToMint, myAddress)); return accountData.getBlocksMintedPenalty() == 0 && !myName.isEmpty() && (isGroupValidated || Groups.memberExistsInAnyGroup(groupRepository, groupIdsToMint, myAddress));
} else { } else {
return level >= levelToMint && !myName.isEmpty() && (isGroupValidated || groupRepository.memberExists(groupIdToMint, myAddress)); return level >= levelToMint && !myName.isEmpty() && (isGroupValidated || Groups.memberExistsInAnyGroup(groupRepository, groupIdsToMint, myAddress));
} }
} }
@ -263,9 +273,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 || groupRepository.memberExists(groupIdToMint, myAddress)); return accountData.getBlocksMintedPenalty() == 0 && (isGroupValidated || Groups.memberExistsInAnyGroup(groupRepository, groupIdsToMint, myAddress));
} else { } else {
return level >= levelToMint && (isGroupValidated || groupRepository.memberExists(groupIdToMint, myAddress)); return level >= levelToMint && (isGroupValidated || Groups.memberExistsInAnyGroup(groupRepository, groupIdsToMint, myAddress));
} }
} }
@ -306,6 +316,9 @@ 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,6 +194,7 @@ 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

@ -0,0 +1,72 @@
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

@ -0,0 +1,50 @@
package org.qortal.api.model;
import javax.xml.bind.annotation.XmlAccessType;
import javax.xml.bind.annotation.XmlAccessorType;
import java.util.Objects;
// All properties to be converted to JSON via JAXB
@XmlAccessorType(XmlAccessType.FIELD)
public class DatasetStatus {
private String name;
private long count;
public DatasetStatus() {}
public DatasetStatus(String name, long count) {
this.name = name;
this.count = count;
}
public String getName() {
return name;
}
public long getCount() {
return count;
}
@Override
public boolean equals(Object o) {
if (this == o) return true;
if (o == null || getClass() != o.getClass()) return false;
DatasetStatus that = (DatasetStatus) o;
return count == that.count && Objects.equals(name, that.name);
}
@Override
public int hashCode() {
return Objects.hash(name, count);
}
@Override
public String toString() {
return "DatasetStatus{" +
"name='" + name + '\'' +
", count=" + count +
'}';
}
}

View File

@ -33,9 +33,13 @@ 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;
@ -69,8 +73,11 @@ 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")
@ -172,6 +179,7 @@ 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,
@ -212,7 +220,7 @@ public class ArbitraryResource {
} }
List<ArbitraryResourceData> resources = repository.getArbitraryRepository() List<ArbitraryResourceData> resources = repository.getArbitraryRepository()
.searchArbitraryResources(service, query, identifier, names, title, description, usePrefixOnly, .searchArbitraryResources(service, query, identifier, names, title, description, keywords, 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);
@ -1185,6 +1193,90 @@ 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,9 +16,13 @@ import org.qortal.api.model.AggregatedOrder;
import org.qortal.api.model.TradeWithOrderInfo; import org.qortal.api.model.TradeWithOrderInfo;
import org.qortal.api.resource.TransactionsResource.ConfirmationStatus; import org.qortal.api.resource.TransactionsResource.ConfirmationStatus;
import org.qortal.asset.Asset; import org.qortal.asset.Asset;
import org.qortal.controller.hsqldb.HSQLDBBalanceRecorder;
import org.qortal.crypto.Crypto; import org.qortal.crypto.Crypto;
import org.qortal.data.account.AccountBalanceData; import org.qortal.data.account.AccountBalanceData;
import org.qortal.data.account.AccountData; import org.qortal.data.account.AccountData;
import org.qortal.data.account.AddressAmountData;
import org.qortal.data.account.BlockHeightRange;
import org.qortal.data.account.BlockHeightRangeAddressAmounts;
import org.qortal.data.asset.AssetData; import org.qortal.data.asset.AssetData;
import org.qortal.data.asset.OrderData; import org.qortal.data.asset.OrderData;
import org.qortal.data.asset.RecentTradeData; import org.qortal.data.asset.RecentTradeData;
@ -33,6 +37,7 @@ import org.qortal.transaction.Transaction;
import org.qortal.transaction.Transaction.ValidationResult; import org.qortal.transaction.Transaction.ValidationResult;
import org.qortal.transform.TransformationException; import org.qortal.transform.TransformationException;
import org.qortal.transform.transaction.*; import org.qortal.transform.transaction.*;
import org.qortal.utils.BalanceRecorderUtils;
import org.qortal.utils.Base58; import org.qortal.utils.Base58;
import javax.servlet.http.HttpServletRequest; import javax.servlet.http.HttpServletRequest;
@ -42,6 +47,7 @@ import javax.ws.rs.core.MediaType;
import java.util.ArrayList; import java.util.ArrayList;
import java.util.Collections; import java.util.Collections;
import java.util.List; import java.util.List;
import java.util.Optional;
import java.util.stream.Collectors; import java.util.stream.Collectors;
@Path("/assets") @Path("/assets")
@ -179,6 +185,122 @@ public class AssetsResource {
} }
} }
@GET
@Path("/balancedynamicranges")
@Operation(
summary = "Get balance dynamic ranges listed.",
description = ".",
responses = {
@ApiResponse(
content = @Content(
array = @ArraySchema(
schema = @Schema(
implementation = BlockHeightRange.class
)
)
)
)
}
)
public List<BlockHeightRange> getBalanceDynamicRanges(
@Parameter(ref = "offset") @QueryParam("offset") Integer offset,
@Parameter(ref = "limit") @QueryParam("limit") Integer limit,
@Parameter(ref = "reverse") @QueryParam("reverse") Boolean reverse) {
Optional<HSQLDBBalanceRecorder> recorder = HSQLDBBalanceRecorder.getInstance();
if( recorder.isPresent()) {
return recorder.get().getRanges(offset, limit, reverse);
}
else {
return new ArrayList<>(0);
}
}
@GET
@Path("/balancedynamicrange/{height}")
@Operation(
summary = "Get balance dynamic range for a given height.",
description = ".",
responses = {
@ApiResponse(
content = @Content(
schema = @Schema(
implementation = BlockHeightRange.class
)
)
)
}
)
@ApiErrors({
ApiError.INVALID_CRITERIA, ApiError.INVALID_DATA
})
public BlockHeightRange getBalanceDynamicRange(@PathParam("height") int height) {
Optional<HSQLDBBalanceRecorder> recorder = HSQLDBBalanceRecorder.getInstance();
if( recorder.isPresent()) {
Optional<BlockHeightRange> range = recorder.get().getRange(height);
if( range.isPresent() ) {
return range.get();
}
else {
throw ApiExceptionFactory.INSTANCE.createException(request, ApiError.INVALID_CRITERIA);
}
}
else {
throw ApiExceptionFactory.INSTANCE.createException(request, ApiError.INVALID_DATA);
}
}
@GET
@Path("/balancedynamicamounts/{begin}/{end}")
@Operation(
summary = "Get balance dynamic ranges address amounts listed.",
description = ".",
responses = {
@ApiResponse(
content = @Content(
array = @ArraySchema(
schema = @Schema(
implementation = AddressAmountData.class
)
)
)
)
}
)
@ApiErrors({
ApiError.INVALID_CRITERIA, ApiError.INVALID_DATA
})
public List<AddressAmountData> getBalanceDynamicAddressAmounts(
@PathParam("begin") int begin,
@PathParam("end") int end,
@Parameter(ref = "offset") @QueryParam("offset") Integer offset,
@Parameter(ref = "limit") @QueryParam("limit") Integer limit) {
Optional<HSQLDBBalanceRecorder> recorder = HSQLDBBalanceRecorder.getInstance();
if( recorder.isPresent()) {
Optional<BlockHeightRangeAddressAmounts> addressAmounts = recorder.get().getAddressAmounts(new BlockHeightRange(begin, end, 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,6 +19,8 @@ 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;
@ -27,6 +29,7 @@ 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;
@ -45,6 +48,7 @@ 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")
@ -889,4 +893,50 @@ 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,11 +10,13 @@ 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;
@ -44,14 +46,20 @@ 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 {
@ -59,6 +67,13 @@ public class CrossChainResource {
@Context @Context
HttpServletRequest request; HttpServletRequest request;
@Context
HttpServletResponse response;
@Context
ServletContext context;
@GET @GET
@Path("/tradeoffers") @Path("/tradeoffers")
@Operation( @Operation(
@ -255,6 +270,12 @@ 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) {
@ -266,6 +287,10 @@ 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()) {
@ -296,7 +321,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, List<ATStateData> atStates = repository.getATRepository().getMatchingFinalATStates(codeHash, buyerPublicKey, sellerPublicKey,
isFinished, acct.getModeByteOffset(), (long) AcctMode.REDEEMED.value, minimumFinalHeight, isFinished, acct.getModeByteOffset(), (long) AcctMode.REDEEMED.value, minimumFinalHeight,
limit, offset, reverse); limit, offset, reverse);
@ -335,6 +360,120 @@ 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,21 +10,36 @@ 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) {
@ -632,4 +647,128 @@ 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,6 +32,7 @@ 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;
@ -40,6 +41,7 @@ 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;
@ -52,6 +54,7 @@ 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;
@ -1064,4 +1067,50 @@ 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

@ -0,0 +1,102 @@
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, List<ATStateData> atStates = repository.getATRepository().getMatchingFinalATStates(codeHash, null, null,
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, List<ATStateData> initialAtStates = repository.getATRepository().getMatchingFinalATStates(codeHash, null, null,
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, List<ATStateData> historicAtStates = repository.getATRepository().getMatchingFinalATStates(codeHash, null, null,
isFinished, dataByteOffset, expectedValue, minimumFinalHeight, isFinished, dataByteOffset, expectedValue, minimumFinalHeight,
null, null, null); null, null, null);

View File

@ -439,7 +439,15 @@ 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
arbitraryDataFile.delete(); LOGGER.info("Deleting invalid file: path = " + arbitraryDataFile.getFilePath());
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

@ -23,8 +23,10 @@ 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;
@ -37,6 +39,7 @@ 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;
@ -148,7 +151,7 @@ public class Block {
final BlockChain blockChain = BlockChain.getInstance(); final BlockChain blockChain = BlockChain.getInstance();
ExpandedAccount(Repository repository, RewardShareData rewardShareData) throws DataException { ExpandedAccount(Repository repository, RewardShareData rewardShareData, int blockHeight) throws DataException {
this.rewardShareData = rewardShareData; this.rewardShareData = rewardShareData;
this.sharePercent = this.rewardShareData.getSharePercent(); this.sharePercent = this.rewardShareData.getSharePercent();
@ -157,7 +160,12 @@ 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 = repository.getGroupRepository().memberExists(BlockChain.getInstance().getMintingGroupId(), this.mintingAccount.getAddress()); this.isMinterMember
= 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
@ -170,6 +178,19 @@ public class Block {
} }
} }
/**
* Get Effective Minting Level
*
* @return the effective minting level, if a data exception is thrown, it catches the exception and returns a zero
*/
public int getEffectiveMintingLevel() {
try {
return this.mintingAccount.getEffectiveMintingLevel();
} catch (DataException e) {
return 0;
}
}
public Account getMintingAccount() { public Account getMintingAccount() {
return this.mintingAccount; return this.mintingAccount;
} }
@ -186,7 +207,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) if (this.isMinterFounder && blockHeight < BlockChain.getInstance().getAdminsReplaceFoundersHeight())
return null; return null;
final int accountLevel = this.mintingAccountData.getLevel(); final int accountLevel = this.mintingAccountData.getLevel();
@ -403,7 +424,9 @@ 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
if (height >= BlockChain.getInstance().getOnlineAccountMinterLevelValidationHeight()) { // but only if they are before the ignore level feature trigger
if (height < BlockChain.getInstance().getIgnoreLevelForRewardShareHeight() &&
height >= BlockChain.getInstance().getOnlineAccountMinterLevelValidationHeight()) {
onlineAccounts.removeIf(a -> { onlineAccounts.removeIf(a -> {
try { try {
return Account.getRewardShareEffectiveMintingLevel(repository, a.getPublicKey()) == 0; return Account.getRewardShareEffectiveMintingLevel(repository, a.getPublicKey()) == 0;
@ -418,9 +441,9 @@ public class Block {
if (height >= BlockChain.getInstance().getGroupMemberCheckHeight()) { if (height >= BlockChain.getInstance().getGroupMemberCheckHeight()) {
onlineAccounts.removeIf(a -> { onlineAccounts.removeIf(a -> {
try { try {
int groupId = BlockChain.getInstance().getMintingGroupId(); List<Integer> groupIdsToMint = Groups.getGroupIdsToMint(BlockChain.getInstance(), height);
String address = Account.getRewardShareMintingAddress(repository, a.getPublicKey()); String address = Account.getRewardShareMintingAddress(repository, a.getPublicKey());
boolean isMinterGroupMember = repository.getGroupRepository().memberExists(groupId, address); boolean isMinterGroupMember = Groups.memberExistsInAnyGroup(repository.getGroupRepository(), groupIdsToMint, 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
@ -736,15 +759,7 @@ public class Block {
List<ExpandedAccount> expandedAccounts = new ArrayList<>(); List<ExpandedAccount> expandedAccounts = new ArrayList<>();
for (RewardShareData rewardShare : this.cachedOnlineRewardShares) { for (RewardShareData rewardShare : this.cachedOnlineRewardShares) {
int groupId = BlockChain.getInstance().getMintingGroupId(); expandedAccounts.add(new ExpandedAccount(repository, rewardShare, this.blockData.getHeight()));
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;
@ -1154,23 +1169,32 @@ 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,
if (this.getBlockData().getHeight() >= BlockChain.getInstance().getOnlineAccountMinterLevelValidationHeight()) { // but only if it is before the feature trigger where we ignore level again
List<ExpandedAccount> expandedAccounts = this.getExpandedAccounts(); if (this.blockData.getHeight() < BlockChain.getInstance().getIgnoreLevelForRewardShareHeight() &&
for (ExpandedAccount account : expandedAccounts) { this.getBlockData().getHeight() >= BlockChain.getInstance().getOnlineAccountMinterLevelValidationHeight()) {
int groupId = BlockChain.getInstance().getMintingGroupId(); List<ExpandedAccount> expandedAccounts
String address = account.getMintingAccount().getAddress(); = this.getExpandedAccounts().stream()
boolean isMinterGroupMember = repository.getGroupRepository().memberExists(groupId, address); .filter(expandedAccount -> expandedAccount.isMinterMember)
.collect(Collectors.toList());
for (ExpandedAccount account : expandedAccounts) {
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 (!isMinterGroupMember) if (!account.isMinterMember)
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();
@ -1659,7 +1683,17 @@ 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 = this.getExpandedAccounts(); final List<ExpandedAccount> expandedAccounts;
if (this.getBlockData().getHeight() < BlockChain.getInstance().getFixBatchRewardHeight()) {
expandedAccounts = this.getExpandedAccounts().stream().collect(Collectors.toList());
}
else {
expandedAccounts
= this.getExpandedAccounts().stream()
.filter(expandedAccount -> expandedAccount.isMinterMember)
.collect(Collectors.toList());
}
Set<AccountData> allUniqueExpandedAccounts = new HashSet<>(); Set<AccountData> allUniqueExpandedAccounts = new HashSet<>();
for (ExpandedAccount expandedAccount : expandedAccounts) { for (ExpandedAccount expandedAccount : expandedAccounts) {
@ -2059,7 +2093,17 @@ 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 = this.getExpandedAccounts(); final List<ExpandedAccount> expandedAccounts;
if (this.getBlockData().getHeight() < BlockChain.getInstance().getFixBatchRewardHeight()) {
expandedAccounts = this.getExpandedAccounts().stream().collect(Collectors.toList());
}
else {
expandedAccounts
= this.getExpandedAccounts().stream()
.filter(expandedAccount -> expandedAccount.isMinterMember)
.collect(Collectors.toList());
}
Set<AccountData> allUniqueExpandedAccounts = new HashSet<>(); Set<AccountData> allUniqueExpandedAccounts = new HashSet<>();
for (ExpandedAccount expandedAccount : expandedAccounts) { for (ExpandedAccount expandedAccount : expandedAccounts) {
@ -2263,7 +2307,17 @@ public class Block {
List<BlockRewardCandidate> rewardCandidates = new ArrayList<>(); List<BlockRewardCandidate> rewardCandidates = new ArrayList<>();
// All online accounts // All online accounts
final List<ExpandedAccount> expandedAccounts = this.getExpandedAccounts(); final List<ExpandedAccount> expandedAccounts;
if (this.getBlockData().getHeight() < BlockChain.getInstance().getFixBatchRewardHeight()) {
expandedAccounts = this.getExpandedAccounts().stream().collect(Collectors.toList());
}
else {
expandedAccounts
= this.getExpandedAccounts().stream()
.filter(expandedAccount -> expandedAccount.isMinterMember)
.collect(Collectors.toList());
}
/* /*
* Distribution rules: * Distribution rules:
@ -2388,7 +2442,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) { if (!haveFounders && this.blockData.getHeight() < BlockChain.getInstance().getAdminsReplaceFoundersHeight() ) {
// Recalculate distribution ratios based on candidates // Recalculate distribution ratios based on candidates
// Nothing shared? This shouldn't happen // Nothing shared? This shouldn't happen
@ -2424,18 +2478,103 @@ public class Block {
} }
// Add founders as reward candidate if appropriate // Add founders as reward candidate if appropriate
if (haveFounders) { if (haveFounders && this.blockData.getHeight() < BlockChain.getInstance().getAdminsReplaceFoundersHeight()) {
// 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,7 +88,11 @@ public class BlockChain {
onlyMintWithNameHeight, onlyMintWithNameHeight,
removeOnlyMintWithNameHeight, removeOnlyMintWithNameHeight,
groupMemberCheckHeight, groupMemberCheckHeight,
fixBatchRewardHeight fixBatchRewardHeight,
adminsReplaceFoundersHeight,
nullGroupMembershipHeight,
ignoreLevelForRewardShareHeight,
adminQueryFixHeight
} }
// Custom transaction fees // Custom transaction fees
@ -208,7 +212,13 @@ 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;
@ -540,8 +550,8 @@ public class BlockChain {
return this.onlineAccountSignaturesMaxLifetime; return this.onlineAccountSignaturesMaxLifetime;
} }
public int getMintingGroupId() { public List<IdsForHeight> getMintingGroupIds() {
return this.mintingGroupId; return mintingGroupIds;
} }
public CiyamAtSettings getCiyamAtSettings() { public CiyamAtSettings getCiyamAtSettings() {
@ -662,6 +672,22 @@ 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,6 +423,12 @@ 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();
@ -541,6 +547,16 @@ 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,6 +25,7 @@ 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;
@ -225,11 +226,14 @@ 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
= repository.getGroupRepository() = Groups.getAllMembers(
.getGroupMembers(BlockChain.getInstance().getMintingGroupId()).stream() repository.getGroupRepository(),
.map(GroupMemberData::getMember) Groups.getGroupIdsToMint(BlockChain.getInstance(), blockHeight)
.collect(Collectors.toList()); );
for (OnlineAccountData onlineAccountData : this.onlineAccountsImportQueue) { for (OnlineAccountData onlineAccountData : this.onlineAccountsImportQueue) {
if (isStopping) if (isStopping)

View File

@ -2,22 +2,30 @@ 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 {
@ -29,6 +37,11 @@ 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) {
@ -45,17 +58,22 @@ 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) {
// Fall through to exit thread
} }
// Clear queue before terminating thread // Clear queue before terminating thread
processResourceQueue(); processResourceQueue();
} catch (Exception e) {
LOGGER.error(e.getMessage(), e);
}
} }
public void shutdown() { public void shutdown() {
@ -85,14 +103,25 @@ 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.updateArbitraryResourceCache(repository); arbitraryTransaction.updateArbitraryResourceCacheIncludingMetadata(repository, new HashSet<>(0), new HashMap<>(0));
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) {
@ -103,6 +132,9 @@ 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) {
@ -148,34 +180,66 @@ 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 = 100; final int batchSize = Settings.getInstance().getBuildArbitraryResourcesBatchSize();
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("Fetching arbitrary transactions {} - {}", offset, offset+batchSize-1); LOGGER.info(
"Fetching arbitrary transactions {} - {} / {} Total",
FORMATTER.format(offset),
FORMATTER.format(offset+batchSize-1),
FORMATTER.format(allArbitraryTransactionsInDescendingOrder.size())
);
List<byte[]> signatures = repository.getTransactionRepository().getSignaturesMatchingCriteria(null, null, null, List.of(Transaction.TransactionType.ARBITRARY), null, null, null, TransactionsResource.ConfirmationStatus.BOTH, batchSize, offset, false); List<ArbitraryTransactionData> transactionsToProcess
if (signatures.isEmpty()) { = allArbitraryTransactionsInDescendingOrder.stream()
.skip(offset)
.limit(batchSize)
.collect(Collectors.toList());
if (transactionsToProcess.isEmpty()) {
// Complete // Complete
break; break;
} }
// Expand signatures to transactions try {
for (byte[] signature : signatures) { for( ArbitraryTransactionData transactionData : transactionsToProcess) {
ArbitraryTransactionData transactionData = (ArbitraryTransactionData) repository
.getTransactionRepository().fromSignature(signature);
if (transactionData.getService() == null) { if (transactionData.getService() == null) {
// Unsupported service - ignore this resource // Unsupported service - ignore this resource
continue; continue;
} }
latestTransactionsWrapped.add(new ArbitraryTransactionDataHashWrapper(transactionData));
// Update arbitrary resource caches // Update arbitrary resource caches
ArbitraryTransaction arbitraryTransaction = new ArbitraryTransaction(repository, transactionData); ArbitraryTransaction arbitraryTransaction = new ArbitraryTransaction(repository, transactionData);
arbitraryTransaction.updateArbitraryResourceCache(repository); arbitraryTransaction.updateArbitraryResourceCacheIncludingMetadata(repository, latestTransactionsWrapped, resourceByWrapper);
arbitraryTransaction.updateArbitraryMetadataCache(repository); }
repository.saveChanges(); repository.saveChanges();
} catch (DataException e) {
repository.discardChanges();
LOGGER.error(e.getMessage(), e);
} }
offset += batchSize; offset += batchSize;
} }
@ -193,6 +257,11 @@ 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 {
@ -200,27 +269,48 @@ 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 = 100; final int batchSize = Settings.getInstance().getBuildArbitraryResourcesBatchSize();
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("Fetching hosted transactions {} - {}", offset, offset+batchSize-1); LOGGER.info(
"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;
} }
@ -234,6 +324,11 @@ 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,9 +2,10 @@ 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;
@ -21,8 +22,12 @@ 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;
@ -77,6 +82,19 @@ 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);
@ -107,27 +125,31 @@ 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<byte[]> signatures = repository.getTransactionRepository().getSignaturesMatchingCriteria(null, null, null, ARBITRARY_TX_TYPE, null, null, null, ConfirmationStatus.BOTH, limit, offset, true); List<ArbitraryTransactionData> transactions = allArbitraryTransactionsInDescendingOrder.stream().skip(offset).limit(limit).collect(Collectors.toList());
// LOGGER.info("Found {} arbitrary transactions at offset: {}, limit: {}", signatures.size(), offset, limit);
if (isStopping) { if (isStopping) {
return; return;
} }
if (signatures == null || signatures.isEmpty()) { if (transactions == null || transactions.isEmpty()) {
offset = 0; offset = 0;
continue; allArbitraryTransactionsInDescendingOrder
= 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<signatures.size(); i++) { for (int i=0; i<transactions.size(); i++) {
if (isStopping) { if (isStopping) {
return; return;
} }
byte[] signature = signatures.get(i); ArbitraryTransactionData arbitraryTransactionData = transactions.get(i);
if (signature == null) { if (arbitraryTransactionData == null) {
continue; continue;
} }
@ -136,9 +158,7 @@ public class ArbitraryDataCleanupManager extends Thread {
Thread.sleep(5000); Thread.sleep(5000);
} }
// Fetch the transaction data if (arbitraryTransactionData.getService() == null) {
ArbitraryTransactionData arbitraryTransactionData = ArbitraryTransactionUtils.fetchTransactionData(repository, signature);
if (arbitraryTransactionData == null || arbitraryTransactionData.getService() == null) {
continue; continue;
} }
@ -147,6 +167,8 @@ 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);
@ -167,20 +189,54 @@ 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
boolean hasMoreRecentPutTransaction = ArbitraryTransactionUtils.hasMoreRecentPutTransaction(repository, arbitraryTransactionData); if (!mostRecentTransaction) {
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(signature))); arbitraryTransactionData.getName(), Base58.encode(arbitraryTransactionData.getSignature())));
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;
} }
@ -199,7 +255,21 @@ 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())));
ArbitraryTransactionUtils.deleteCompleteFile(arbitraryTransactionData, now, STALE_FILE_TIMEOUT); boolean wasDeleted = 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;
} }
@ -237,17 +307,6 @@ 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);
} }
@ -326,25 +385,6 @@ 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
* *
@ -423,6 +463,7 @@ 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 {
@ -437,6 +478,35 @@ 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

@ -0,0 +1,21 @@
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,6 +5,8 @@ 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,6 +10,8 @@ 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;
@ -28,6 +30,7 @@ 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 {
@ -195,13 +198,35 @@ 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 = repository.getTransactionRepository().getSignaturesMatchingCriteria(null, null, null, ARBITRARY_TX_TYPE, null, name, null, ConfirmationStatus.BOTH, limit, offset, true); List<byte[]> signatures = processTransactionsForSignatures(limit, offset, allArbitraryTransactionsInDescendingOrder, processedTransactions);
// 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;
@ -223,14 +248,38 @@ 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
if (!storageManager.shouldPreFetchData(repository, arbitraryTransactionData)) { ArbitraryDataExamination arbitraryDataExamination = 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()
)
);
} }
} }
@ -248,8 +297,21 @@ 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);
if (hasMoreRecentPutTransaction) { Optional<ArbitraryTransactionData> moreRecentPutTransaction = ArbitraryTransactionUtils.hasMoreRecentPutTransaction(repository, arbitraryTransactionData);
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
@ -257,10 +319,34 @@ 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);
} }
@ -274,6 +360,20 @@ 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;
@ -282,8 +382,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 = repository.getTransactionRepository().getSignaturesMatchingCriteria(null, null, null, ARBITRARY_TX_TYPE, null, null, null, ConfirmationStatus.BOTH, limit, offset, true); List<byte[]> signatures = processTransactionsForSignatures(limit, offset, allArbitraryTransactionsInDescendingOrder, processedTransactions);
// 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;
@ -328,26 +428,74 @@ public class ArbitraryDataManager extends Thread {
continue; continue;
} }
// Check to see if we have had a more recent PUT // No longer need to see if we have had a more recent PUT since we compared the transactions to process
// 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,31 +155,24 @@ 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 boolean shouldPreFetchData(Repository repository, ArbitraryTransactionData arbitraryTransactionData) { public ArbitraryDataExamination 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 false; return new ArbitraryDataExamination(false, "Only fetch data associated with hashes");
} }
// 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 false; return new ArbitraryDataExamination(false,"Don't fetch anything more if we're (nearly) out of space");
}
// 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 false; return new ArbitraryDataExamination(false, "Don't store data unless it's an allowed type (public/private)");
} }
// Handle transactions without names differently // Handle transactions without names differently
@ -189,21 +182,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 false; return new ArbitraryDataExamination(false, "blocked name");
} }
switch (Settings.getInstance().getStoragePolicy()) { switch (Settings.getInstance().getStoragePolicy()) {
case FOLLOWED: case FOLLOWED:
case FOLLOWED_OR_VIEWED: case FOLLOWED_OR_VIEWED:
return ListUtils.isFollowingName(name); return new ArbitraryDataExamination(ListUtils.isFollowingName(name), Settings.getInstance().getStoragePolicy().name());
case ALL: case ALL:
return true; return new ArbitraryDataExamination(true, Settings.getInstance().getStoragePolicy().name());
case NONE: case NONE:
case VIEWED: case VIEWED:
default: default:
return false; return new ArbitraryDataExamination(false, Settings.getInstance().getStoragePolicy().name());
} }
} }
@ -214,17 +207,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 boolean shouldPreFetchDataWithoutName() { private ArbitraryDataExamination shouldPreFetchDataWithoutName() {
switch (Settings.getInstance().getStoragePolicy()) { switch (Settings.getInstance().getStoragePolicy()) {
case ALL: case ALL:
return true; return new ArbitraryDataExamination(true, "Fetching all data");
case NONE: case NONE:
case VIEWED: case VIEWED:
case FOLLOWED: case FOLLOWED:
case FOLLOWED_OR_VIEWED: case FOLLOWED_OR_VIEWED:
default: default:
return false; return new ArbitraryDataExamination(false, Settings.getInstance().getStoragePolicy().name());
} }
} }
@ -484,51 +477,6 @@ 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

@ -0,0 +1,48 @@
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

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

View File

@ -83,6 +83,7 @@ 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

@ -2,6 +2,8 @@ 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

@ -0,0 +1,54 @@
package org.qortal.data.account;
import javax.xml.bind.annotation.XmlAccessType;
import javax.xml.bind.annotation.XmlAccessorType;
import javax.xml.bind.annotation.adapters.XmlJavaTypeAdapter;
import java.util.Objects;
// All properties to be converted to JSON via JAXB
@XmlAccessorType(XmlAccessType.FIELD)
public class AddressAmountData {
private String address;
@XmlJavaTypeAdapter(value = org.qortal.api.AmountTypeAdapter.class)
private long amount;
public AddressAmountData() {
}
public AddressAmountData(String address, long amount) {
this.address = address;
this.amount = amount;
}
public String getAddress() {
return address;
}
public long getAmount() {
return amount;
}
@Override
public boolean equals(Object o) {
if (this == o) return true;
if (o == null || getClass() != o.getClass()) return false;
AddressAmountData that = (AddressAmountData) o;
return amount == that.amount && Objects.equals(address, that.address);
}
@Override
public int hashCode() {
return Objects.hash(address, amount);
}
@Override
public String toString() {
return "AddressAmountData{" +
"address='" + address + '\'' +
", amount=" + amount +
'}';
}
}

View File

@ -0,0 +1,59 @@
package org.qortal.data.account;
import javax.xml.bind.annotation.XmlAccessType;
import javax.xml.bind.annotation.XmlAccessorType;
import java.util.Objects;
// All properties to be converted to JSON via JAXB
@XmlAccessorType(XmlAccessType.FIELD)
public class BlockHeightRange {
private int begin;
private int end;
private boolean isRewardDistribution;
public BlockHeightRange() {
}
public BlockHeightRange(int begin, int end, boolean isRewardDistribution) {
this.begin = begin;
this.end = end;
this.isRewardDistribution = isRewardDistribution;
}
public int getBegin() {
return begin;
}
public int getEnd() {
return end;
}
public boolean isRewardDistribution() {
return isRewardDistribution;
}
@Override
public boolean equals(Object o) {
if (this == o) return true;
if (o == null || getClass() != o.getClass()) return false;
BlockHeightRange that = (BlockHeightRange) o;
return begin == that.begin && end == that.end;
}
@Override
public int hashCode() {
return Objects.hash(begin, end);
}
@Override
public String toString() {
return "BlockHeightRange{" +
"begin=" + begin +
", end=" + end +
", isRewardDistribution=" + isRewardDistribution +
'}';
}
}

View File

@ -0,0 +1,52 @@
package org.qortal.data.account;
import javax.xml.bind.annotation.XmlAccessType;
import javax.xml.bind.annotation.XmlAccessorType;
import java.util.List;
import java.util.Objects;
// All properties to be converted to JSON via JAXB
@XmlAccessorType(XmlAccessType.FIELD)
public class BlockHeightRangeAddressAmounts {
private BlockHeightRange range;
private List<AddressAmountData> amounts;
public BlockHeightRangeAddressAmounts() {
}
public BlockHeightRangeAddressAmounts(BlockHeightRange range, List<AddressAmountData> amounts) {
this.range = range;
this.amounts = amounts;
}
public BlockHeightRange getRange() {
return range;
}
public List<AddressAmountData> getAmounts() {
return amounts;
}
@Override
public boolean equals(Object o) {
if (this == o) return true;
if (o == null || getClass() != o.getClass()) return false;
BlockHeightRangeAddressAmounts that = (BlockHeightRangeAddressAmounts) o;
return Objects.equals(range, that.range) && Objects.equals(amounts, that.amounts);
}
@Override
public int hashCode() {
return Objects.hash(range, amounts);
}
@Override
public String toString() {
return "BlockHeightRangeAddressAmounts{" +
"range=" + range +
", amounts=" + amounts +
'}';
}
}

View File

@ -0,0 +1,34 @@
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

@ -0,0 +1,41 @@
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

@ -0,0 +1,38 @@
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

@ -0,0 +1,38 @@
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

@ -0,0 +1,57 @@
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

@ -0,0 +1,23 @@
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

@ -0,0 +1,85 @@
package org.qortal.data.block;
import javax.xml.bind.annotation.XmlAccessType;
import javax.xml.bind.annotation.XmlAccessorType;
import java.util.Objects;
// All properties to be converted to JSON via JAX-RS
@XmlAccessorType(XmlAccessType.FIELD)
public class DecodedOnlineAccountData {
private long onlineTimestamp;
private String minter;
private String recipient;
private int sharePercent;
private boolean minterGroupMember;
private String name;
private int level;
public DecodedOnlineAccountData() {
}
public DecodedOnlineAccountData(long onlineTimestamp, String minter, String recipient, int sharePercent, boolean minterGroupMember, String name, int level) {
this.onlineTimestamp = onlineTimestamp;
this.minter = minter;
this.recipient = recipient;
this.sharePercent = sharePercent;
this.minterGroupMember = minterGroupMember;
this.name = name;
this.level = level;
}
public long getOnlineTimestamp() {
return onlineTimestamp;
}
public String getMinter() {
return minter;
}
public String getRecipient() {
return recipient;
}
public int getSharePercent() {
return sharePercent;
}
public boolean isMinterGroupMember() {
return minterGroupMember;
}
public String getName() {
return name;
}
public int getLevel() {
return level;
}
@Override
public boolean equals(Object o) {
if (this == o) return true;
if (o == null || getClass() != o.getClass()) return false;
DecodedOnlineAccountData that = (DecodedOnlineAccountData) o;
return onlineTimestamp == that.onlineTimestamp && sharePercent == that.sharePercent && minterGroupMember == that.minterGroupMember && level == that.level && Objects.equals(minter, that.minter) && Objects.equals(recipient, that.recipient) && Objects.equals(name, that.name);
}
@Override
public int hashCode() {
return Objects.hash(onlineTimestamp, minter, recipient, sharePercent, minterGroupMember, name, level);
}
@Override
public String toString() {
return "DecodedOnlineAccountData{" +
"onlineTimestamp=" + onlineTimestamp +
", minter='" + minter + '\'' +
", recipient='" + recipient + '\'' +
", sharePercent=" + sharePercent +
", minterGroupMember=" + minterGroupMember +
", name='" + name + '\'' +
", level=" + level +
'}';
}
}

View File

@ -0,0 +1,35 @@
package org.qortal.data.system;
import javax.xml.bind.annotation.XmlAccessType;
import javax.xml.bind.annotation.XmlAccessorType;
@XmlAccessorType(XmlAccessType.FIELD)
public class DbConnectionInfo {
private long updated;
private String owner;
private String state;
public DbConnectionInfo() {
}
public DbConnectionInfo(long timeOpened, String owner, String state) {
this.updated = timeOpened;
this.owner = owner;
this.state = state;
}
public long getUpdated() {
return updated;
}
public String getOwner() {
return owner;
}
public String getState() {
return state;
}
}

View File

@ -0,0 +1,49 @@
package org.qortal.data.system;
import javax.xml.bind.annotation.XmlAccessType;
import javax.xml.bind.annotation.XmlAccessorType;
@XmlAccessorType(XmlAccessType.FIELD)
public class SystemInfo {
private long freeMemory;
private long memoryInUse;
private long totalMemory;
private long maxMemory;
private int availableProcessors;
public SystemInfo() {
}
public SystemInfo(long freeMemory, long memoryInUse, long totalMemory, long maxMemory, int availableProcessors) {
this.freeMemory = freeMemory;
this.memoryInUse = memoryInUse;
this.totalMemory = totalMemory;
this.maxMemory = maxMemory;
this.availableProcessors = availableProcessors;
}
public long getFreeMemory() {
return freeMemory;
}
public long getMemoryInUse() {
return memoryInUse;
}
public long getTotalMemory() {
return totalMemory;
}
public long getMaxMemory() {
return maxMemory;
}
public int getAvailableProcessors() {
return availableProcessors;
}
}

View File

@ -200,4 +200,26 @@ 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

@ -0,0 +1,57 @@
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,6 +2,7 @@ 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.*;
@ -150,8 +151,13 @@ 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.getAdminFaulty(this.groupData.getGroupId(), admin);
}
else {
return groupRepository.getAdmin(this.groupData.getGroupId(), admin); return groupRepository.getAdmin(this.groupData.getGroupId(), admin);
} }
}
private boolean adminExists(String admin) throws DataException { private boolean adminExists(String admin) throws DataException {
return groupRepository.adminExists(this.groupData.getGroupId(), admin); return groupRepository.adminExists(this.groupData.getGroupId(), admin);
@ -668,8 +674,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 then they were added when invite matched join request // If member exists and the join request is present then they were added when invite matched join request
if (this.memberExists(invitee)) { if (this.memberExists(invitee) && groupInviteTransactionData.getJoinReference() != null) {
// 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,7 +76,7 @@ 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, Boolean isFinished, public List<ATStateData> getMatchingFinalATStates(byte[] codeHash, byte[] buyerPublicKey, byte[] sellerPublicKey, 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;

View File

@ -27,6 +27,10 @@ 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;
@ -42,7 +46,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, 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, 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;
List<ArbitraryResourceData> searchArbitraryResourcesSimple( List<ArbitraryResourceData> searchArbitraryResourcesSimple(
Service service, Service service,

View File

@ -48,6 +48,8 @@ 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,9 +1,11 @@
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;
@ -16,6 +18,8 @@ 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);
@ -400,7 +404,7 @@ public class HSQLDBATRepository implements ATRepository {
} }
@Override @Override
public List<ATStateData> getMatchingFinalATStates(byte[] codeHash, Boolean isFinished, public List<ATStateData> getMatchingFinalATStates(byte[] codeHash, byte[] buyerPublicKey, byte[] sellerPublicKey, 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);
@ -421,10 +425,14 @@ 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.AT_address DESC, ATStates.height DESC " sql.append("ORDER BY ATStates.height DESC LIMIT 1) AS FinalATStates ");
+ "LIMIT 1 "
+ ") AS FinalATStates " // Optional JOIN with ATTRANSACTIONS for buyerAddress
+ "WHERE code_hash = ? "); if (buyerPublicKey != null && buyerPublicKey.length > 0) {
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) {
@ -443,6 +451,20 @@ 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");
@ -483,7 +505,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, isFinished, List<ATStateData> mostRecentStates = this.getMatchingFinalATStates(codeHash, null, null, isFinished,
dataByteOffset, expectedValue, null, dataByteOffset, expectedValue, null,
1, 0, true); 1, 0, true);

View File

@ -7,7 +7,6 @@ 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;
@ -29,6 +28,7 @@ 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,6 +227,144 @@ 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
@ -724,12 +862,11 @@ public class HSQLDBArbitraryRepository implements ArbitraryRepository {
} }
@Override @Override
public List<ArbitraryResourceData> searchArbitraryResources(Service service, String query, String identifier, List<String> names, String title, String description, boolean prefixOnly, public List<ArbitraryResourceData> searchArbitraryResources(Service service, String query, String identifier, List<String> names, String title, String description, List<String> keywords, 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(),
@ -751,6 +888,7 @@ 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()),
@ -771,6 +909,7 @@ 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<>();
@ -857,6 +996,26 @@ 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,15 +3,24 @@ 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;
@ -28,6 +37,7 @@ import java.util.Optional;
import java.util.Timer; import java.util.Timer;
import java.util.TimerTask; import java.util.TimerTask;
import java.util.concurrent.ConcurrentHashMap; import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.CopyOnWriteArrayList;
import java.util.function.Function; import java.util.function.Function;
import java.util.function.Predicate; import java.util.function.Predicate;
import java.util.function.Supplier; import java.util.function.Supplier;
@ -157,6 +167,7 @@ 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,
@ -172,6 +183,17 @@ public class HSQLDBCacheUtils {
// 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() )
stream = stream.filter(candidate -> candidate.service.equals(service.get())); stream = stream.filter(candidate -> candidate.service.equals(service.get()));
@ -194,6 +216,36 @@ 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()) {
@ -249,15 +301,58 @@ 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());
// include metadata List<ArbitraryResourceData> listCopy1 = stream.collect(Collectors.toList());
if( includeMetadata.isEmpty() || !includeMetadata.get() )
stream = stream.peek( candidate -> candidate.metadata = null );
// include status List<ArbitraryResourceData> listCopy2 = new ArrayList<>(listCopy1.size());
if( includeStatus.isEmpty() || !includeStatus.get() )
stream = stream.peek( candidate -> candidate.status = null);
return stream.collect(Collectors.toList()); // remove metadata from the first copy
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;
}
} }
/** /**
@ -389,14 +484,15 @@ public class HSQLDBCacheUtils {
/** /**
* Start Recording Balances * Start Recording Balances
* *
* @param queue the queue to add to, remove oldest data if necssary * @param balancesByHeight height -> account balances
* @param repository the db repsoitory * @param balanceDynamics every balance dynamic
* @param priorityRequested the requested thread priority * @param priorityRequested the requested thread priority
* @param frequency the recording frequencies, in minutes * @param frequency the recording frequencies, in minutes
* @param capacity the maximum size of balanceDynamics
*/ */
public static void startRecordingBalances( public static void startRecordingBalances(
final ConcurrentHashMap<Integer, List<AccountBalanceData>> balancesByHeight, final ConcurrentHashMap<Integer, List<AccountBalanceData>> balancesByHeight,
final ConcurrentHashMap<String, List<AccountBalanceData>> balancesByAddress, CopyOnWriteArrayList<BlockHeightRangeAddressAmounts> balanceDynamics,
int priorityRequested, int priorityRequested,
int frequency, int frequency,
int capacity) { int capacity) {
@ -409,12 +505,113 @@ public class HSQLDBCacheUtils {
Thread.currentThread().setName(BALANCE_RECORDER_TIMER_TASK); Thread.currentThread().setName(BALANCE_RECORDER_TIMER_TASK);
try (final HSQLDBRepository repository = (HSQLDBRepository) Controller.REPOSITORY_FACTORY.getRepository()) { int currentHeight = recordCurrentBalances(balancesByHeight);
while (balancesByHeight.size() > capacity + 1) {
Optional<Integer> firstHeight = balancesByHeight.keySet().stream().sorted().findFirst();
if (firstHeight.isPresent()) balancesByHeight.remove(firstHeight.get()); LOGGER.debug("recorded balances: height = " + currentHeight);
// remove invalidated recordings, recording after current height
BalanceRecorderUtils.removeRecordingsAboveHeight(currentHeight, balancesByHeight);
// remove invalidated dynamics, on or after current height
BalanceRecorderUtils.removeDynamicsOnOrAboveHeight(currentHeight, balanceDynamics);
// if there are 2 or more recordings, then produce balance dynamics for the first 2 recordings
if( balancesByHeight.size() > 1 ) {
Optional<Integer> priorHeight = BalanceRecorderUtils.getPriorHeight(currentHeight, balancesByHeight);
// if there is a prior height
if(priorHeight.isPresent()) {
boolean isRewardDistribution = BalanceRecorderUtils.isRewardDistributionRange(priorHeight.get(), currentHeight);
// if this range has a reward recording block or if other blocks are enabled for recording
if( isRewardDistribution || !Settings.getInstance().isRewardRecordingOnly() ) {
produceBalanceDynamics(currentHeight, priorHeight, isRewardDistribution, balancesByHeight, balanceDynamics, capacity);
} }
}
else {
LOGGER.warn("Expecting prior height and nothing was discovered, current height = " + currentHeight);
}
}
// else this should be the first recording
else {
LOGGER.info("first balance recording completed");
}
}
};
// wait 5 minutes
timer.scheduleAtFixedRate(task, 300_000, frequency * 60_000);
}
private static void produceBalanceDynamics(int currentHeight, Optional<Integer> priorHeight, boolean isRewardDistribution, ConcurrentHashMap<Integer, List<AccountBalanceData>> balancesByHeight, CopyOnWriteArrayList<BlockHeightRangeAddressAmounts> balanceDynamics, int capacity) {
BlockHeightRange blockHeightRange = new BlockHeightRange(priorHeight.get(), currentHeight, isRewardDistribution);
LOGGER.debug("building dynamics for block heights: range = " + blockHeightRange);
List<AccountBalanceData> currentBalances = balancesByHeight.get(currentHeight);
ArrayList<TransactionData> transactions = getTransactionDataForBlocks(blockHeightRange);
LOGGER.info("transactions counted for balance adjustments: count = " + transactions.size());
List<AddressAmountData> currentDynamics
= BalanceRecorderUtils.buildBalanceDynamics(
currentBalances,
balancesByHeight.get(priorHeight.get()),
Settings.getInstance().getMinimumBalanceRecording(),
transactions);
LOGGER.debug("dynamics built: count = " + currentDynamics.size());
if(LOGGER.isDebugEnabled())
currentDynamics.stream()
.sorted(Comparator.comparingLong(AddressAmountData::getAmount).reversed())
.limit(Settings.getInstance().getTopBalanceLoggingLimit())
.forEach(top5Dynamic -> LOGGER.debug("Top Dynamics = " + top5Dynamic));
BlockHeightRangeAddressAmounts amounts
= new BlockHeightRangeAddressAmounts( blockHeightRange, currentDynamics );
balanceDynamics.add(amounts);
BalanceRecorderUtils.removeRecordingsBelowHeight(currentHeight - Settings.getInstance().getBalanceRecorderRollbackAllowance(), balancesByHeight);
while(balanceDynamics.size() > capacity) {
BlockHeightRangeAddressAmounts oldestDynamics = BalanceRecorderUtils.removeOldestDynamics(balanceDynamics);
LOGGER.debug("removing oldest dynamics: range " + oldestDynamics.getRange());
}
}
private static ArrayList<TransactionData> getTransactionDataForBlocks(BlockHeightRange blockHeightRange) {
ArrayList<TransactionData> transactions;
try (final Repository repository = RepositoryManager.getRepository()) {
List<byte[]> signatures
= repository.getTransactionRepository().getSignaturesMatchingCriteria(
blockHeightRange.getBegin() + 1, blockHeightRange.getEnd() - blockHeightRange.getBegin(),
null, null,null, null, null,
TransactionsResource.ConfirmationStatus.CONFIRMED,
null, null, null);
transactions = new ArrayList<>(signatures.size());
for (byte[] signature : signatures) {
transactions.add(repository.getTransactionRepository().fromSignature(signature));
}
LOGGER.debug(String.format("Found %s transactions for " + blockHeightRange, transactions.size()));
} catch (Exception e) {
transactions = new ArrayList<>(0);
LOGGER.warn("Problems getting transactions for balance recording: " + e.getMessage());
}
return transactions;
}
private static int recordCurrentBalances(ConcurrentHashMap<Integer, List<AccountBalanceData>> balancesByHeight) {
int currentHeight;
try (final HSQLDBRepository repository = (HSQLDBRepository) Controller.REPOSITORY_FACTORY.getRepository()) {
// get current balances // get current balances
List<AccountBalanceData> accountBalances = getAccountBalances(repository); List<AccountBalanceData> accountBalances = getAccountBalances(repository);
@ -427,43 +624,17 @@ public class HSQLDBCacheUtils {
// map all new balances to the current height // map all new balances to the current height
balancesByHeight.put(data.get().getHeight(), accountBalances); balancesByHeight.put(data.get().getHeight(), accountBalances);
// for each new balance, map to address currentHeight = data.get().getHeight();
for (AccountBalanceData accountBalance : accountBalances) {
// get recorded balances for this address
List<AccountBalanceData> establishedBalances
= balancesByAddress.getOrDefault(accountBalance.getAddress(), new ArrayList<>(0));
// start a new list of recordings for this address, add the new balance and add the established
// balances
List<AccountBalanceData> balances = new ArrayList<>(establishedBalances.size() + 1);
balances.add(accountBalance);
balances.addAll(establishedBalances);
// reset tha balances for this address
balancesByAddress.put(accountBalance.getAddress(), balances);
// TODO: reduce account balances to capacity
}
// reduce height balances to capacity
while( balancesByHeight.size() > capacity ) {
Optional<Integer> lowestHeight
= balancesByHeight.entrySet().stream()
.min(Comparator.comparingInt(Map.Entry::getKey))
.map(Map.Entry::getKey);
if (lowestHeight.isPresent()) balancesByHeight.entrySet().remove(lowestHeight);
} }
else {
currentHeight = Integer.MAX_VALUE;
} }
} catch (DataException e) { } catch (DataException e) {
LOGGER.error(e.getMessage(), e); LOGGER.error(e.getMessage(), e);
currentHeight = Integer.MAX_VALUE;
} }
}
};
// wait 5 minutes return currentHeight;
timer.scheduleAtFixedRate(task, 300_000, frequency * 60_000);
} }
/** /**

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

@ -351,7 +351,7 @@ public class HSQLDBGroupRepository implements GroupRepository {
// Group Admins // Group Admins
@Override @Override
public GroupAdminData getAdmin(int groupId, String address) throws DataException { public GroupAdminData getAdminFaulty(int groupId, String address) throws DataException {
try (ResultSet resultSet = this.repository.checkedExecute("SELECT admin, reference FROM GroupAdmins WHERE group_id = ?", groupId)) { 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;
@ -365,6 +365,21 @@ public class HSQLDBGroupRepository implements GroupRepository {
} }
} }
@Override
public GroupAdminData getAdmin(int groupId, String address) throws DataException {
try (ResultSet resultSet = this.repository.checkedExecute("SELECT admin, reference FROM GroupAdmins WHERE group_id = ? AND admin = ?", groupId, address)) {
if (resultSet == null)
return null;
String admin = resultSet.getString(1);
byte[] reference = resultSet.getBytes(2);
return new GroupAdminData(groupId, admin, reference);
} catch (SQLException e) {
throw new DataException("Unable to fetch group admin from repository", e);
}
}
@Override @Override
public boolean adminExists(int groupId, String address) throws DataException { public boolean adminExists(int groupId, String address) throws DataException {
try { try {

View File

@ -5,6 +5,8 @@ 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;
@ -14,6 +16,8 @@ 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 {
@ -57,7 +61,13 @@ public class HSQLDBRepositoryFactory implements RepositoryFactory {
HSQLDBRepository.attemptRecovery(connectionUrl, "backup"); HSQLDBRepository.attemptRecovery(connectionUrl, "backup");
} }
if(Settings.getInstance().isConnectionPoolMonitorEnabled()) {
this.connectionPool = new HSQLDBPoolMonitored(Settings.getInstance().getRepositoryConnectionPoolSize());
}
else {
this.connectionPool = new HSQLDBPool(Settings.getInstance().getRepositoryConnectionPoolSize()); this.connectionPool = new HSQLDBPool(Settings.getInstance().getRepositoryConnectionPoolSize());
}
this.connectionPool.setUrl(this.connectionUrl); this.connectionPool.setUrl(this.connectionUrl);
Properties properties = new Properties(); Properties properties = new Properties();
@ -153,4 +163,19 @@ 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 = false; private boolean dbCacheEnabled = true;
/** /**
* DB Cache Thread Priority * DB Cache Thread Priority
@ -444,14 +444,107 @@ 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;
@ -1257,4 +1350,48 @@ 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,6 +9,7 @@ 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;
@ -31,8 +32,12 @@ 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 {
@ -303,8 +308,13 @@ 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.
this.updateArbitraryResourceCache(repository); // Also, must add this transaction as a latest transaction, since the it has not been saved to the
this.updateArbitraryMetadataCache(repository); // repository yet.
this.updateArbitraryResourceCacheIncludingMetadata(
repository,
Set.of(new ArbitraryTransactionDataHashWrapper(arbitraryTransactionData)),
new HashMap<>(0)
);
repository.saveChanges(); repository.saveChanges();
@ -360,7 +370,10 @@ public class ArbitraryTransaction extends Transaction {
* *
* @throws DataException * @throws DataException
*/ */
public void updateArbitraryResourceCache(Repository repository) throws DataException { public void updateArbitraryResourceCacheIncludingMetadata(
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;
@ -385,17 +398,33 @@ public class ArbitraryTransaction extends Transaction {
arbitraryResourceData.name = name; arbitraryResourceData.name = name;
arbitraryResourceData.identifier = identifier; arbitraryResourceData.identifier = identifier;
final ArbitraryTransactionDataHashWrapper wrapper = new ArbitraryTransactionDataHashWrapper(arbitraryTransactionData);
ArbitraryTransactionData latestTransactionData;
if( latestTransactionWrappers.contains(wrapper)) {
latestTransactionData
= latestTransactionWrappers.stream()
.filter( latestWrapper -> latestWrapper.equals(wrapper))
.findAny().get()
.getData();
}
else {
// Get the latest transaction // Get the latest transaction
ArbitraryTransactionData latestTransactionData = repository.getArbitraryRepository().getLatestTransaction(arbitraryTransactionData.getName(), arbitraryTransactionData.getService(), null, arbitraryTransactionData.getIdentifier()); latestTransactionData = repository.getArbitraryRepository().getLatestTransaction(arbitraryTransactionData.getName(), arbitraryTransactionData.getService(), null, arbitraryTransactionData.getIdentifier());
if (latestTransactionData == null) { 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 // We don't have a latest transaction, so delete from cache
repository.getArbitraryRepository().delete(arbitraryResourceData); repository.getArbitraryRepository().delete(arbitraryResourceData);
return; return;
} }
}
ArbitraryResourceData existingArbitraryResourceData = resourceByWrapper.get(wrapper);
if( existingArbitraryResourceData == null ) {
// Get existing cached entry if it exists // Get existing cached entry if it exists
ArbitraryResourceData existingArbitraryResourceData = repository.getArbitraryRepository() existingArbitraryResourceData = repository.getArbitraryRepository()
.getArbitraryResource(service, name, identifier); .getArbitraryResource(service, name, identifier);
}
// Check for existing cached data // Check for existing cached data
if (existingArbitraryResourceData == null) { if (existingArbitraryResourceData == null) {
@ -404,6 +433,7 @@ 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());
@ -421,6 +451,34 @@ 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 {
@ -455,60 +513,4 @@ 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,6 +2,7 @@ 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;
@ -12,6 +13,7 @@ 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 {
@ -70,9 +72,26 @@ 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;
// Can't unban if not group's current owner if( this.repository.getBlockRepository().getBlockchainHeight() < BlockChain.getInstance().getNullGroupMembershipHeight() ) {
// Can't cancel ban if not group's current owner
if (!admin.getAddress().equals(groupData.getOwner())) 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,6 +2,7 @@ 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;
@ -12,6 +13,7 @@ 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 {
@ -80,6 +82,16 @@ 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

@ -2,6 +2,7 @@ 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;
@ -12,6 +13,7 @@ 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 {
@ -70,9 +72,25 @@ 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,6 +2,7 @@ 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;
@ -11,6 +12,7 @@ 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 {
@ -85,6 +87,16 @@ 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,6 +3,7 @@ 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;
@ -14,6 +15,7 @@ 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 {
@ -82,9 +84,26 @@ 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

@ -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, false), GROUP_BAN(26, true),
CANCEL_GROUP_BAN(27, false), CANCEL_GROUP_BAN(27, true),
GROUP_KICK(28, false), GROUP_KICK(28, true),
GROUP_INVITE(29, false), GROUP_INVITE(29, true),
CANCEL_GROUP_INVITE(30, false), CANCEL_GROUP_INVITE(30, true),
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

@ -0,0 +1,250 @@
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,6 +24,7 @@ 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;
@ -72,23 +73,23 @@ public class ArbitraryTransactionUtils {
return latestPut; return latestPut;
} }
public static boolean hasMoreRecentPutTransaction(Repository repository, ArbitraryTransactionData arbitraryTransactionData) { public static Optional<ArbitraryTransactionData> 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 false; return Optional.empty();
} }
ArbitraryTransactionData latestPut = ArbitraryTransactionUtils.fetchLatestPut(repository, arbitraryTransactionData); ArbitraryTransactionData latestPut = ArbitraryTransactionUtils.fetchLatestPut(repository, arbitraryTransactionData);
if (latestPut == null) { if (latestPut == null) {
return false; return Optional.empty();
} }
// 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; return hasNewerPut ? Optional.of(latestPut) : Optional.empty();
} }
public static boolean completeFileExists(ArbitraryTransactionData transactionData) throws DataException { public static boolean completeFileExists(ArbitraryTransactionData transactionData) throws DataException {
@ -208,7 +209,15 @@ 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();
@ -219,6 +228,11 @@ public class ArbitraryTransactionUtils {
"if needed", Base58.encode(completeHash)); "if needed", Base58.encode(completeHash));
arbitraryDataFile.delete(); arbitraryDataFile.delete();
return true;
}
else {
return false;
} }
} }

View File

@ -0,0 +1,319 @@
package org.qortal.utils;
import org.qortal.block.Block;
import org.qortal.crypto.Crypto;
import org.qortal.data.PaymentData;
import org.qortal.data.account.AccountBalanceData;
import org.qortal.data.account.AddressAmountData;
import org.qortal.data.account.BlockHeightRange;
import org.qortal.data.account.BlockHeightRangeAddressAmounts;
import org.qortal.data.transaction.ATTransactionData;
import org.qortal.data.transaction.BaseTransactionData;
import org.qortal.data.transaction.BuyNameTransactionData;
import org.qortal.data.transaction.CreateAssetOrderTransactionData;
import org.qortal.data.transaction.DeployAtTransactionData;
import org.qortal.data.transaction.MultiPaymentTransactionData;
import org.qortal.data.transaction.PaymentTransactionData;
import org.qortal.data.transaction.TransactionData;
import org.qortal.data.transaction.TransferAssetTransactionData;
import java.util.Comparator;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.Optional;
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.CopyOnWriteArrayList;
import java.util.function.Predicate;
import java.util.stream.Collectors;
public class BalanceRecorderUtils {
public static final Predicate<AddressAmountData> ADDRESS_AMOUNT_DATA_NOT_ZERO = addressAmount -> addressAmount.getAmount() != 0;
public static final Comparator<BlockHeightRangeAddressAmounts> BLOCK_HEIGHT_RANGE_ADDRESS_AMOUNTS_COMPARATOR = new Comparator<BlockHeightRangeAddressAmounts>() {
@Override
public int compare(BlockHeightRangeAddressAmounts amounts1, BlockHeightRangeAddressAmounts amounts2) {
return amounts1.getRange().getEnd() - amounts2.getRange().getEnd();
}
};
public static final Comparator<AddressAmountData> ADDRESS_AMOUNT_DATA_COMPARATOR = new Comparator<AddressAmountData>() {
@Override
public int compare(AddressAmountData addressAmountData, AddressAmountData t1) {
if( addressAmountData.getAmount() > t1.getAmount() ) {
return 1;
}
else if( addressAmountData.getAmount() < t1.getAmount() ) {
return -1;
}
else {
return 0;
}
}
};
public static final Comparator<BlockHeightRange> BLOCK_HEIGHT_RANGE_COMPARATOR = new Comparator<BlockHeightRange>() {
@Override
public int compare(BlockHeightRange range1, BlockHeightRange range2) {
return range1.getEnd() - range2.getEnd();
}
};
/**
* Build Balance Dynmaics For Account
*
* @param priorBalances the balances prior to the current height, assuming only one balance per address
* @param accountBalance the current balance
*
* @return the difference between the current balance and the prior balance for the current balance address
*/
public static AddressAmountData buildBalanceDynamicsForAccount(List<AccountBalanceData> priorBalances, AccountBalanceData accountBalance) {
Optional<AccountBalanceData> matchingAccountPriorBalance
= priorBalances.stream()
.filter(priorBalance -> accountBalance.getAddress().equals(priorBalance.getAddress()))
.findFirst();
if(matchingAccountPriorBalance.isPresent()) {
return new AddressAmountData(accountBalance.getAddress(), accountBalance.getBalance() - matchingAccountPriorBalance.get().getBalance());
}
else {
return new AddressAmountData(accountBalance.getAddress(), accountBalance.getBalance());
}
}
public static List<AddressAmountData> buildBalanceDynamics(
final List<AccountBalanceData> balances,
final List<AccountBalanceData> priorBalances,
long minimum,
List<TransactionData> transactions) {
Map<String, Long> amountsByAddress = new HashMap<>(transactions.size());
for( TransactionData transactionData : transactions ) {
mapBalanceModificationsForTransaction(amountsByAddress, transactionData);
}
List<AddressAmountData> addressAmounts
= balances.stream()
.map(balance -> buildBalanceDynamicsForAccount(priorBalances, balance))
.map( data -> adjustAddressAmount(amountsByAddress.getOrDefault(data.getAddress(), 0L), data))
.filter(ADDRESS_AMOUNT_DATA_NOT_ZERO)
.filter(data -> data.getAmount() >= minimum)
.collect(Collectors.toList());
return addressAmounts;
}
public static AddressAmountData adjustAddressAmount(long adjustment, AddressAmountData data) {
return new AddressAmountData(data.getAddress(), data.getAmount() - adjustment);
}
public static void mapBalanceModificationsForTransaction(Map<String, Long> amountsByAddress, TransactionData transactionData) {
String creatorAddress;
// AT Transaction
if( transactionData instanceof ATTransactionData) {
creatorAddress = mapBalanceModificationsForAtTransaction(amountsByAddress, (ATTransactionData) transactionData);
}
// Buy Name Transaction
else if( transactionData instanceof BuyNameTransactionData) {
creatorAddress = mapBalanceModificationsForBuyNameTransaction(amountsByAddress, (BuyNameTransactionData) transactionData);
}
// Create Asset Order Transaction
else if( transactionData instanceof CreateAssetOrderTransactionData) {
//TODO I'm not sure how to handle this one. This hasn't been used at this point in the blockchain.
creatorAddress = Crypto.toAddress(transactionData.getCreatorPublicKey());
}
// Deploy AT Transaction
else if( transactionData instanceof DeployAtTransactionData ) {
creatorAddress = mapBalanceModificationsForDeployAtTransaction(amountsByAddress, (DeployAtTransactionData) transactionData);
}
// Multi Payment Transaction
else if( transactionData instanceof MultiPaymentTransactionData) {
creatorAddress = mapBalanceModificationsForMultiPaymentTransaction(amountsByAddress, (MultiPaymentTransactionData) transactionData);
}
// Payment Transaction
else if( transactionData instanceof PaymentTransactionData ) {
creatorAddress = mapBalanceModicationsForPaymentTransaction(amountsByAddress, (PaymentTransactionData) transactionData);
}
// Transfer Asset Transaction
else if( transactionData instanceof TransferAssetTransactionData) {
creatorAddress = mapBalanceModificationsForTransferAssetTransaction(amountsByAddress, (TransferAssetTransactionData) transactionData);
}
// Other Transactions
else {
creatorAddress = Crypto.toAddress(transactionData.getCreatorPublicKey());
}
// all transactions modify the balance for fees
mapBalanceModifications(amountsByAddress, transactionData.getFee(), creatorAddress, Optional.empty());
}
public static String mapBalanceModificationsForTransferAssetTransaction(Map<String, Long> amountsByAddress, TransferAssetTransactionData transferAssetData) {
String creatorAddress = Crypto.toAddress(transferAssetData.getSenderPublicKey());
if( transferAssetData.getAssetId() == 0) {
mapBalanceModifications(
amountsByAddress,
transferAssetData.getAmount(),
creatorAddress,
Optional.of(transferAssetData.getRecipient())
);
}
return creatorAddress;
}
public static String mapBalanceModicationsForPaymentTransaction(Map<String, Long> amountsByAddress, PaymentTransactionData paymentData) {
String creatorAddress = Crypto.toAddress(paymentData.getCreatorPublicKey());
mapBalanceModifications(amountsByAddress,
paymentData.getAmount(),
creatorAddress,
Optional.of(paymentData.getRecipient())
);
return creatorAddress;
}
public static String mapBalanceModificationsForMultiPaymentTransaction(Map<String, Long> amountsByAddress, MultiPaymentTransactionData multiPaymentData) {
String creatorAddress = Crypto.toAddress(multiPaymentData.getCreatorPublicKey());
for(PaymentData payment : multiPaymentData.getPayments() ) {
mapBalanceModificationsForTransaction(
amountsByAddress,
getPaymentTransactionData(multiPaymentData, payment)
);
}
return creatorAddress;
}
public static String mapBalanceModificationsForDeployAtTransaction(Map<String, Long> amountsByAddress, DeployAtTransactionData transactionData) {
String creatorAddress;
DeployAtTransactionData deployAtData = transactionData;
creatorAddress = Crypto.toAddress(deployAtData.getCreatorPublicKey());
if( deployAtData.getAssetId() == 0 ) {
mapBalanceModifications(
amountsByAddress,
deployAtData.getAmount(),
creatorAddress,
Optional.of(deployAtData.getAtAddress())
);
}
return creatorAddress;
}
public static String mapBalanceModificationsForBuyNameTransaction(Map<String, Long> amountsByAddress, BuyNameTransactionData transactionData) {
String creatorAddress;
BuyNameTransactionData buyNameData = transactionData;
creatorAddress = Crypto.toAddress(buyNameData.getCreatorPublicKey());
mapBalanceModifications(
amountsByAddress,
buyNameData.getAmount(),
creatorAddress,
Optional.of(buyNameData.getSeller())
);
return creatorAddress;
}
public static String mapBalanceModificationsForAtTransaction(Map<String, Long> amountsByAddress, ATTransactionData transactionData) {
String creatorAddress;
ATTransactionData atData = transactionData;
creatorAddress = atData.getATAddress();
if( atData.getAssetId() != null && atData.getAssetId() == 0) {
mapBalanceModifications(
amountsByAddress,
atData.getAmount(),
creatorAddress,
Optional.of(atData.getRecipient())
);
}
return creatorAddress;
}
public static PaymentTransactionData getPaymentTransactionData(MultiPaymentTransactionData multiPaymentData, PaymentData payment) {
return new PaymentTransactionData(
new BaseTransactionData(
multiPaymentData.getTimestamp(),
multiPaymentData.getTxGroupId(),
multiPaymentData.getReference(),
multiPaymentData.getCreatorPublicKey(),
0L,
multiPaymentData.getSignature()
),
payment.getRecipient(),
payment.getAmount()
);
}
public static void mapBalanceModifications(Map<String, Long> amountsByAddress, Long amount, String sender, Optional<String> recipient) {
amountsByAddress.put(
sender,
amountsByAddress.getOrDefault(sender, 0L) - amount
);
if( recipient.isPresent() )
amountsByAddress.put(
recipient.get(),
amountsByAddress.getOrDefault(recipient.get(), 0L) + amount
);
}
public static void removeRecordingsAboveHeight(int currentHeight, ConcurrentHashMap<Integer, List<AccountBalanceData>> balancesByHeight) {
balancesByHeight.entrySet().stream()
.filter(heightWithBalances -> heightWithBalances.getKey() > currentHeight)
.forEach(heightWithBalances -> balancesByHeight.remove(heightWithBalances.getKey()));
}
public static void removeRecordingsBelowHeight(int currentHeight, ConcurrentHashMap<Integer, List<AccountBalanceData>> balancesByHeight) {
balancesByHeight.entrySet().stream()
.filter(heightWithBalances -> heightWithBalances.getKey() < currentHeight)
.forEach(heightWithBalances -> balancesByHeight.remove(heightWithBalances.getKey()));
}
public static void removeDynamicsOnOrAboveHeight(int currentHeight, CopyOnWriteArrayList<BlockHeightRangeAddressAmounts> balanceDynamics) {
balanceDynamics.stream()
.filter(addressAmounts -> addressAmounts.getRange().getEnd() >= currentHeight)
.forEach(addressAmounts -> balanceDynamics.remove(addressAmounts));
}
public static BlockHeightRangeAddressAmounts removeOldestDynamics(CopyOnWriteArrayList<BlockHeightRangeAddressAmounts> balanceDynamics) {
BlockHeightRangeAddressAmounts oldestDynamics
= balanceDynamics.stream().sorted(BLOCK_HEIGHT_RANGE_ADDRESS_AMOUNTS_COMPARATOR).findFirst().get();
balanceDynamics.remove(oldestDynamics);
return oldestDynamics;
}
public static Optional<Integer> getPriorHeight(int currentHeight, ConcurrentHashMap<Integer, List<AccountBalanceData>> balancesByHeight) {
Optional<Integer> priorHeight
= balancesByHeight.keySet().stream()
.filter(height -> height < currentHeight)
.sorted(Comparator.reverseOrder()).findFirst();
return priorHeight;
}
/**
* Is Reward Distribution Range?
*
* @param start start height, exclusive
* @param end end height, inclusive
*
* @return true there is a reward distribution block within this block range
*/
public static boolean isRewardDistributionRange(int start, int end) {
// iterate through the block height until a reward distribution block or the end of the range
for( int i = start + 1; i <= end; i++) {
if( Block.isRewardDistributionBlock(i) ) return true;
}
// no reward distribution blocks found within range
return false;
}
}

View File

@ -0,0 +1,99 @@
package org.qortal.utils;
import io.druid.extendedset.intset.ConciseSet;
import org.apache.logging.log4j.LogManager;
import org.apache.logging.log4j.Logger;
import org.qortal.block.BlockChain;
import org.qortal.data.account.AddressLevelPairing;
import org.qortal.data.account.RewardShareData;
import org.qortal.data.block.BlockData;
import org.qortal.data.block.DecodedOnlineAccountData;
import org.qortal.data.group.GroupMemberData;
import org.qortal.data.naming.NameData;
import org.qortal.repository.DataException;
import org.qortal.repository.Repository;
import org.qortal.transform.block.BlockTransformer;
import java.util.HashSet;
import java.util.List;
import java.util.Map;
import java.util.Set;
import java.util.stream.Collectors;
/**
* Class Blocks
*
* Methods for block related logic.
*/
public class Blocks {
private static final Logger LOGGER = LogManager.getLogger(Blocks.class);
/**
* Get Decode Online Accounts For Block
*
* @param repository the data repository
* @param blockData the block data
*
* @return the online accounts set to the block
*
* @throws DataException
*/
public static Set<DecodedOnlineAccountData> getDecodedOnlineAccountsForBlock(Repository repository, BlockData blockData) throws DataException {
try {
// get all online account indices from block
ConciseSet onlineAccountIndices = BlockTransformer.decodeOnlineAccounts(blockData.getEncodedOnlineAccounts());
// get online reward shares from the online accounts on the block
List<RewardShareData> onlineRewardShares = repository.getAccountRepository().getRewardSharesByIndexes(onlineAccountIndices.toArray());
// online timestamp for block
long onlineTimestamp = blockData.getOnlineAccountsTimestamp();
Set<DecodedOnlineAccountData> onlineAccounts = new HashSet<>();
// all minting group member addresses
List<String> mintingGroupAddresses
= 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

@ -0,0 +1,122 @@
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,7 +38,9 @@
"blockRewardBatchStartHeight": 1508000, "blockRewardBatchStartHeight": 1508000,
"blockRewardBatchSize": 1000, "blockRewardBatchSize": 1000,
"blockRewardBatchAccountsBlockCount": 25, "blockRewardBatchAccountsBlockCount": 25,
"mintingGroupId": 694, "mintingGroupIds": [
{ "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 },
@ -113,7 +115,11 @@
"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

@ -20,17 +20,21 @@
width: 100%; width: 100%;
text-align: center; text-align: center;
z-index: 1000; z-index: 1000;
top: 45%; top: 50%;
-ms-transform: translateY(-50%); -ms-transform: translateY(-50%);
transform: translateY(-50%); transform: translate(-50% , -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,6 +84,7 @@ 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
@ -284,11 +285,9 @@ 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);
@ -383,6 +382,7 @@ 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":
let identifier = (data.identifier != null) ? data.identifier : "default"; 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/" + data.signature; url = "/blocks/signature/" + data.signature;
} else if (data.height != null) { } else if (data.height != null) {
url = "/blocks/byheight/" + data.height; url = "/blocks/byheight/" + data.height;
} }
@ -694,6 +694,7 @@ 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",
@ -712,6 +713,7 @@ 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, isFinished, dataByteOffset, expectedValue, minimumFinalHeight, limit, offset, reverse); hsqldb.getATRepository().getMatchingFinalATStates(codeHash,null, null, isFinished, dataByteOffset, expectedValue, minimumFinalHeight, limit, offset, reverse);
} catch (DataException e) { } catch (DataException e) {
fail("HSQLDB bug #1580"); fail("HSQLDB bug #1580");
} }

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, limit, offset, reverse)); assertNoApiError((limit, offset, reverse) -> this.crossChainResource.getCompletedTrades(SPECIFIC_BLOCKCHAIN, minimumTimestamp, null, null, 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*/, limit, offset, reverse)); assertApiError(ApiError.INVALID_CRITERIA, () -> this.crossChainResource.getCompletedTrades(SPECIFIC_BLOCKCHAIN, -1L /*minimumTimestamp*/, null, null, limit, offset, reverse));
assertApiError(ApiError.INVALID_CRITERIA, () -> this.crossChainResource.getCompletedTrades(SPECIFIC_BLOCKCHAIN, 0L /*minimumTimestamp*/, limit, offset, reverse)); assertApiError(ApiError.INVALID_CRITERIA, () -> this.crossChainResource.getCompletedTrades(SPECIFIC_BLOCKCHAIN, 0L /*minimumTimestamp*/, null, null, limit, offset, reverse));
} }
} }

View File

@ -3,10 +3,15 @@ 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 {
@ -137,4 +142,53 @@ 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

@ -145,56 +145,6 @@ 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)); assertTrue(storageManager.shouldPreFetchData(repository, arbitraryTransactionData).isPass());
// 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)); assertFalse(storageManager.shouldPreFetchData(repository, arbitraryTransactionData).isPass());
} }
} }
@ -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)); assertTrue(storageManager.shouldPreFetchData(repository, arbitraryTransactionData).isPass());
// 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)); assertFalse(storageManager.shouldPreFetchData(repository, arbitraryTransactionData).isPass());
} }
} }
@ -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)); assertFalse(storageManager.shouldPreFetchData(repository, arbitraryTransactionData).isPass());
// 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)); assertFalse(storageManager.shouldPreFetchData(repository, arbitraryTransactionData).isPass());
} }
} }
@ -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)); assertTrue(storageManager.shouldPreFetchData(repository, arbitraryTransactionData).isPass());
// 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)); assertTrue(storageManager.shouldPreFetchData(repository, arbitraryTransactionData).isPass());
} }
} }
@ -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)); assertFalse(storageManager.shouldPreFetchData(repository, arbitraryTransactionData).isPass());
// 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)); assertFalse(storageManager.shouldPreFetchData(repository, arbitraryTransactionData).isPass());
} }
} }
@ -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)); assertFalse(storageManager.shouldPreFetchData(repository, transactionData).isPass());
} }
} }

View File

@ -218,6 +218,8 @@ 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,
@ -264,6 +266,8 @@ 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

@ -0,0 +1,58 @@
package org.qortal.test.block;
import org.checkerframework.checker.units.qual.K;
import org.junit.Assert;
import org.junit.Test;
import org.qortal.block.Block;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Optional;
public class BlockTests {
@Test
public void testDistributeToAccountsOneDistribution(){
List<String> addresses = new ArrayList<>();
addresses.add("a");
addresses.add("b");
addresses.add("c");
HashMap<String, Long> balanceByAddress = new HashMap<>();
long total = Block.distributeToAccounts( 10L, addresses, balanceByAddress);
Assert.assertEquals(9, total);
Assert.assertEquals(3, balanceByAddress.size());
Assert.assertTrue(balanceByAddress.containsKey("a"));
Assert.assertTrue(balanceByAddress.containsKey("b"));
Assert.assertTrue(balanceByAddress.containsKey("c"));
Assert.assertEquals(3L, balanceByAddress.getOrDefault("a", 0L).longValue());
Assert.assertEquals(3L, balanceByAddress.getOrDefault("b", 0L).longValue());
Assert.assertEquals(3L, balanceByAddress.getOrDefault("c", 0L).longValue());
}
@Test
public void testDistributeToAccountsTwoDistributions(){
List<String> addresses = new ArrayList<>();
addresses.add("a");
addresses.add("b");
addresses.add("c");
HashMap<String, Long> balanceByAddress = new HashMap<>();
long total1 = Block.distributeToAccounts( 10L, addresses, balanceByAddress);
long total2 = Block.distributeToAccounts( 20L, addresses, balanceByAddress);
Assert.assertEquals(9, total1);
Assert.assertEquals(18, total2);
Assert.assertEquals(3, balanceByAddress.size());
Assert.assertTrue(balanceByAddress.containsKey("a"));
Assert.assertTrue(balanceByAddress.containsKey("b"));
Assert.assertTrue(balanceByAddress.containsKey("c"));
Assert.assertEquals(9L, balanceByAddress.getOrDefault("a", 0L).longValue());
Assert.assertEquals(9L, balanceByAddress.getOrDefault("b", 0L).longValue());
Assert.assertEquals(9L, balanceByAddress.getOrDefault("c", 0L).longValue());
}
}

View File

@ -4,7 +4,11 @@ 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;
@ -16,6 +20,8 @@ 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.*;
/** /**
@ -40,8 +46,14 @@ 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();
@ -55,8 +67,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;
@ -80,16 +92,10 @@ 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 be OK // Should not be OK, cannot kick member out of null owned group
assertEquals(ValidationResult.OK, result); assertNotSame(ValidationResult.OK, result);
// Confirm Bob no longer a member // Confirm Bob remains 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));
} }
} }
@ -97,8 +103,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;
@ -123,7 +129,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);
@ -167,8 +173,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;
@ -183,18 +189,13 @@ 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 be OK // Should not be OK, cannot ban someone from a null owned group
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);
// Orphan last block (Bob ban) // Bob attempts to join
BlockUtils.orphanLastBlock(repository); result = joinGroup(repository, bob, groupId);
// Delete unconfirmed group-ban transaction // Should be OK, but won't actually get him in the group
TransactionUtils.deleteUnconfirmedTransactions(repository); assertEquals(ValidationResult.OK, result);
// Confirm Bob is not a member // Confirm Bob is not a member
assertFalse(isMember(repository, bob.getAddress(), groupId)); assertFalse(isMember(repository, bob.getAddress(), groupId));
@ -204,65 +205,38 @@ public class DevGroupAdminTests extends Common {
// Bob to join // Bob to join
result = joinGroup(repository, bob, groupId); result = joinGroup(repository, bob, groupId);
// Should be OK // Should not be OK, bob should already be a member, he joined before the invite and
assertEquals(ValidationResult.OK, result); // the invite served as an approval
assertEquals(ValidationResult.ALREADY_GROUP_MEMBER, result);
// Confirm Bob now a member // Confirm Bob now a member, now that he got an invite
assertTrue(isMember(repository, bob.getAddress(), groupId)); 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 be OK // Should not be OK, because you can ban a member of a null owned group
assertEquals(ValidationResult.OK, result); assertNotSame(ValidationResult.OK, result);
// Confirm Bob no longer a member // Confirm Bob is still a member
assertFalse(isMember(repository, bob.getAddress(), groupId)); assertTrue(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 // Should NOT be OK, because he is already a member
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 be OK // Should not be OK, because there was no ban to begin with
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;
@ -286,7 +260,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);
@ -321,6 +295,322 @@ public class DevGroupAdminTests extends Common {
} }
} }
@Test
public void testAddAdmin2of3() throws DataException {
try (final Repository repository = RepositoryManager.getRepository()) {
// establish accounts
PrivateKeyAccount alice = Common.getTestAccount(repository, ALICE);
PrivateKeyAccount bob = Common.getTestAccount(repository, BOB);
PrivateKeyAccount chloe = Common.getTestAccount(repository, CHLOE);
PrivateKeyAccount dilbert = Common.getTestAccount(repository, DILBERT);
// assert admin statuses
assertEquals(2, repository.getGroupRepository().countGroupAdmins(DEV_GROUP_ID).intValue());
assertTrue(isAdmin(repository, Group.NULL_OWNER_ADDRESS, DEV_GROUP_ID));
assertTrue(isAdmin(repository, alice.getAddress(), DEV_GROUP_ID));
assertFalse(isAdmin(repository, bob.getAddress(), DEV_GROUP_ID));
assertFalse(isAdmin(repository, chloe.getAddress(), DEV_GROUP_ID));
assertFalse(isAdmin(repository, dilbert.getAddress(), DEV_GROUP_ID));
// confirm Bob is not a member
assertFalse(isMember(repository, bob.getAddress(), DEV_GROUP_ID));
// alice invites bob
ValidationResult result = groupInvite(repository, alice, DEV_GROUP_ID, bob.getAddress(), 3600);
assertSame(ValidationResult.OK, result);
// bob joins
joinGroup(repository, bob, DEV_GROUP_ID);
// confirm Bob is a member now, but still not an admin
assertTrue(isMember(repository, bob.getAddress(), DEV_GROUP_ID));
assertFalse(isAdmin(repository, bob.getAddress(), DEV_GROUP_ID));
// bob creates transaction to add himself as an admin
TransactionData addGroupAdminTransactionData1 = addGroupAdmin(repository, bob, DEV_GROUP_ID, bob.getAddress());
// bob creates add admin transaction for himself, alice signs which is 50% approval while 40% is needed
signForGroupApproval(repository, addGroupAdminTransactionData1, List.of(alice));
// assert 3 admins in group and bob is an admin now
assertEquals(3, repository.getGroupRepository().countGroupAdmins(DEV_GROUP_ID).intValue() );
assertTrue(isAdmin(repository, bob.getAddress(), DEV_GROUP_ID));
// bob invites chloe
result = groupInvite(repository, bob, DEV_GROUP_ID, chloe.getAddress(), 3600);
assertSame(ValidationResult.OK, result);
// chloe joins
joinGroup(repository, chloe, DEV_GROUP_ID);
// confirm Chloe is a member now, but still not an admin
assertTrue(isMember(repository, chloe.getAddress(), DEV_GROUP_ID));
assertFalse(isAdmin(repository, chloe.getAddress(), DEV_GROUP_ID));
// chloe creates transaction to add herself as an admin
TransactionData addChloeAsGroupAdmin = addGroupAdmin(repository, chloe, DEV_GROUP_ID, chloe.getAddress());
// no one has signed, so it should be pending
Transaction.ApprovalStatus addChloeAsGroupAdminStatus1 = GroupUtils.getApprovalStatus(repository, addChloeAsGroupAdmin.getSignature());
assertEquals( Transaction.ApprovalStatus.PENDING, addChloeAsGroupAdminStatus1);
// signer 1
Transaction.ApprovalStatus addChloeAsGroupAdminStatus2 = signForGroupApproval(repository, addChloeAsGroupAdmin, List.of(alice));
// 1 out of 3 has signed, so it should be pending, because it is less than 40%
assertEquals( Transaction.ApprovalStatus.PENDING, addChloeAsGroupAdminStatus2);
// signer 2
Transaction.ApprovalStatus addChloeAsGroupAdminStatus3 = signForGroupApproval(repository, addChloeAsGroupAdmin, List.of(bob));
// 2 out of 3 has signed, so it should be approved, because it is more than 40%
assertEquals( Transaction.ApprovalStatus.APPROVED, addChloeAsGroupAdminStatus3);
}
}
@Test
public void testNullOwnershipMembership() throws DataException{
try (final Repository repository = RepositoryManager.getRepository()) {
Block block = BlockUtils.mintBlocks(repository, NULL_GROUP_MEMBERSHIP_HEIGHT);
assertEquals(NULL_GROUP_MEMBERSHIP_HEIGHT + 1, block.getBlockData().getHeight().intValue());
// establish accounts
PrivateKeyAccount alice = Common.getTestAccount(repository, ALICE);
PrivateKeyAccount bob = Common.getTestAccount(repository, BOB);
PrivateKeyAccount chloe = Common.getTestAccount(repository, CHLOE);
PrivateKeyAccount dilbert = Common.getTestAccount(repository, DILBERT);
// assert admin statuses
assertEquals(2, repository.getGroupRepository().countGroupAdmins(DEV_GROUP_ID).intValue());
assertTrue(isAdmin(repository, Group.NULL_OWNER_ADDRESS, DEV_GROUP_ID));
assertTrue(isAdmin(repository, alice.getAddress(), DEV_GROUP_ID));
assertFalse(isAdmin(repository, bob.getAddress(), DEV_GROUP_ID));
assertFalse(isAdmin(repository, chloe.getAddress(), DEV_GROUP_ID));
assertFalse(isAdmin(repository, dilbert.getAddress(), DEV_GROUP_ID));
// confirm Bob is not a member
assertFalse(isMember(repository, bob.getAddress(), DEV_GROUP_ID));
// alice invites bob, alice signs which is 50% approval while 40% is needed
TransactionData createInviteTransactionData = createGroupInviteForGroupApproval(repository, alice, DEV_GROUP_ID, bob.getAddress(), 3600);
Transaction.ApprovalStatus bobsInviteStatus = signForGroupApproval(repository, createInviteTransactionData, List.of(alice));
// assert approval
assertEquals(Transaction.ApprovalStatus.APPROVED, bobsInviteStatus);
// bob joins
joinGroup(repository, bob, DEV_GROUP_ID);
// confirm Bob is a member now, but still not an admin
assertTrue(isMember(repository, bob.getAddress(), DEV_GROUP_ID));
assertFalse(isAdmin(repository, bob.getAddress(), DEV_GROUP_ID));
// bob creates transaction to add himself as an admin
TransactionData addGroupAdminTransactionData1 = addGroupAdmin(repository, bob, DEV_GROUP_ID, bob.getAddress());
// bob creates add admin transaction for himself, alice signs which is 50% approval while 40% is needed
signForGroupApproval(repository, addGroupAdminTransactionData1, List.of(alice));
// assert 3 admins in group and bob is an admin now
assertEquals(3, repository.getGroupRepository().countGroupAdmins(DEV_GROUP_ID).intValue());
assertTrue(isAdmin(repository, bob.getAddress(), DEV_GROUP_ID));
// bob invites chloe, bob signs which is 33% approval while 40% is needed
TransactionData chloeInvite = createGroupInviteForGroupApproval(repository, bob, DEV_GROUP_ID, chloe.getAddress(), 3600);
Transaction.ApprovalStatus chloeInviteStatus = signForGroupApproval(repository, chloeInvite, List.of(bob));
// assert pending
assertEquals(Transaction.ApprovalStatus.PENDING, chloeInviteStatus);
// alice signs which is 66% approval while 40% is needed
chloeInviteStatus = signForGroupApproval(repository, chloeInvite, List.of(alice));
// assert approval
assertEquals(Transaction.ApprovalStatus.APPROVED, chloeInviteStatus);
// chloe joins
joinGroup(repository, chloe, DEV_GROUP_ID);
// assert chloe is in the group
assertTrue(isMember(repository, bob.getAddress(), DEV_GROUP_ID));
// alice kicks chloe, alice signs which is 33% approval while 40% is needed
TransactionData chloeKick = createGroupKickForGroupApproval(repository, alice, DEV_GROUP_ID, chloe.getAddress(),"testing chloe kick");
Transaction.ApprovalStatus chloeKickStatus = signForGroupApproval(repository, chloeKick, List.of(alice));
// assert pending
assertEquals(Transaction.ApprovalStatus.PENDING, chloeKickStatus);
// assert chloe is still in the group
assertTrue(isMember(repository, bob.getAddress(), DEV_GROUP_ID));
// bob signs which is 66% approval while 40% is needed
chloeKickStatus = signForGroupApproval(repository, chloeKick, List.of(bob));
// assert approval
assertEquals(Transaction.ApprovalStatus.APPROVED, chloeKickStatus);
// assert chloe is not in the group
assertFalse(isMember(repository, chloe.getAddress(), DEV_GROUP_ID));
// bob invites chloe, alice and bob signs which is 66% approval while 40% is needed
TransactionData chloeInviteAgain = createGroupInviteForGroupApproval(repository, bob, DEV_GROUP_ID, chloe.getAddress(), 3600);
Transaction.ApprovalStatus chloeInviteAgainStatus = signForGroupApproval(repository, chloeInviteAgain, List.of(alice, bob));
// assert approved
assertEquals(Transaction.ApprovalStatus.APPROVED, chloeInviteAgainStatus);
// chloe joins again
joinGroup(repository, chloe, DEV_GROUP_ID);
// assert chloe is in the group
assertTrue(isMember(repository, bob.getAddress(), DEV_GROUP_ID));
// alice bans chloe, alice signs which is 33% approval while 40% is needed
TransactionData chloeBan = createGroupBanForGroupApproval(repository, alice, DEV_GROUP_ID, chloe.getAddress(), "testing group ban", 3600);
Transaction.ApprovalStatus chloeBanStatus1 = signForGroupApproval(repository, chloeBan, List.of(alice));
// assert pending
assertEquals(Transaction.ApprovalStatus.PENDING, chloeBanStatus1);
// bob signs which 66% approval while 40% is needed
Transaction.ApprovalStatus chloeBanStatus2 = signForGroupApproval(repository, chloeBan, List.of(bob));
// assert approved
assertEquals(Transaction.ApprovalStatus.APPROVED, chloeBanStatus2);
// assert chloe is not in the group
assertFalse(isMember(repository, chloe.getAddress(), DEV_GROUP_ID));
// bob invites chloe, alice and bob signs which is 66% approval while 40% is needed
ValidationResult chloeInviteValidation = signAndImportGroupInvite(repository, bob, DEV_GROUP_ID, chloe.getAddress(), 3600);
// assert banned status on invite attempt
assertEquals(ValidationResult.BANNED_FROM_GROUP, chloeInviteValidation);
// bob cancel ban on chloe, bob signs which is 33% approval while 40% is needed
TransactionData chloeCancelBan = createCancelGroupBanForGroupApproval( repository, bob, DEV_GROUP_ID, chloe.getAddress());
Transaction.ApprovalStatus chloeCancelBanStatus1 = signForGroupApproval(repository, chloeCancelBan, List.of(bob));
// assert pending
assertEquals(Transaction.ApprovalStatus.PENDING, chloeCancelBanStatus1);
// alice signs which is 66% approval while 40% is needed
Transaction.ApprovalStatus chloeCancelBanStatus2 = signForGroupApproval(repository, chloeCancelBan, List.of(alice));
// assert approved
assertEquals(Transaction.ApprovalStatus.APPROVED, chloeCancelBanStatus2);
// bob invites chloe, alice and bob signs which is 66% approval while 40% is needed
TransactionData chloeInvite4 = createGroupInviteForGroupApproval(repository, bob, DEV_GROUP_ID, chloe.getAddress(), 3600);
Transaction.ApprovalStatus chloeInvite4Status = signForGroupApproval(repository, chloeInvite4, List.of(alice, bob));
// assert approved
assertEquals(Transaction.ApprovalStatus.APPROVED, chloeInvite4Status);
// chloe joins again
joinGroup(repository, chloe, DEV_GROUP_ID);
// assert chloe is in the group
assertTrue(isMember(repository, chloe.getAddress(), DEV_GROUP_ID));
// bob invites dilbert, alice and bob signs which is 66% approval while 40% is needed
TransactionData dilbertInvite1 = createGroupInviteForGroupApproval(repository, bob, DEV_GROUP_ID, dilbert.getAddress(), 3600);
Transaction.ApprovalStatus dibertInviteStatus1 = signForGroupApproval(repository, dilbertInvite1, List.of(alice, bob));
// assert approved
assertEquals(Transaction.ApprovalStatus.APPROVED, dibertInviteStatus1);
// alice cancels dilbert's invite, alice signs which is 33% approval while 40% is needed
TransactionData cancelDilbertInvite = createCancelInviteForGroupApproval(repository, alice, DEV_GROUP_ID, dilbert.getAddress());
Transaction.ApprovalStatus cancelDilbertInviteStatus1 = signForGroupApproval(repository, cancelDilbertInvite, List.of(alice));
// assert pending
assertEquals(Transaction.ApprovalStatus.PENDING, cancelDilbertInviteStatus1);
// dilbert joins before the group approves cancellation
joinGroup(repository, dilbert, DEV_GROUP_ID);
// assert dilbert is in the group
assertTrue(isMember(repository, dilbert.getAddress(), DEV_GROUP_ID));
// alice kicks out dilbert, alice and bob sign which is 66% approval while 40% is needed
TransactionData kickDilbert = createGroupKickForGroupApproval(repository, alice, DEV_GROUP_ID, dilbert.getAddress(), "he is sneaky");
Transaction.ApprovalStatus kickDilbertStatus = signForGroupApproval(repository, kickDilbert, List.of(alice, bob));
// assert approved
assertEquals(Transaction.ApprovalStatus.APPROVED, kickDilbertStatus);
// assert dilbert is out of the group
assertFalse(isMember(repository, dilbert.getAddress(), DEV_GROUP_ID));
// bob invites dilbert again, alice and bob signs which is 66% approval while 40% is needed
TransactionData dilbertInvite2 = createGroupInviteForGroupApproval(repository, bob, DEV_GROUP_ID, dilbert.getAddress(), 3600);
Transaction.ApprovalStatus dibertInviteStatus2 = signForGroupApproval(repository, dilbertInvite2, List.of(alice, bob));
// assert approved
assertEquals(Transaction.ApprovalStatus.APPROVED, dibertInviteStatus2);
// alice cancels dilbert's invite, alice and bob signs which is 66% approval while 40% is needed
TransactionData cancelDilbertInvite2 = createCancelInviteForGroupApproval(repository, alice, DEV_GROUP_ID, dilbert.getAddress());
Transaction.ApprovalStatus cancelDilbertInviteStatus2 = signForGroupApproval(repository, cancelDilbertInvite2, List.of(alice, bob));
// assert approved
assertEquals(Transaction.ApprovalStatus.APPROVED, cancelDilbertInviteStatus2);
// dilbert tries to join after the group approves cancellation
joinGroup(repository, dilbert, DEV_GROUP_ID);
// assert dilbert is not in the group
assertFalse(isMember(repository, dilbert.getAddress(), DEV_GROUP_ID));
}
}
@Test
public void testGetAdmin() throws DataException{
try (final Repository repository = RepositoryManager.getRepository()) {
// establish accounts
PrivateKeyAccount alice = Common.getTestAccount(repository, ALICE);
PrivateKeyAccount bob = Common.getTestAccount(repository, BOB);
GroupAdminData aliceAdminData = repository.getGroupRepository().getAdmin(DEV_GROUP_ID, alice.getAddress());
assertNotNull(aliceAdminData);
assertEquals( alice.getAddress(), aliceAdminData.getAdmin() );
assertEquals( DEV_GROUP_ID, aliceAdminData.getGroupId());
GroupAdminData bobAdminData = repository.getGroupRepository().getAdmin(DEV_GROUP_ID, bob.getAddress());
assertNull(bobAdminData);
}
}
private Transaction.ApprovalStatus signForGroupApproval(Repository repository, TransactionData data, List<PrivateKeyAccount> signers) throws DataException {
for (PrivateKeyAccount signer : signers) {
signTransactionDataForGroupApproval(repository, signer, data);
}
BlockUtils.mintBlocks(repository, 2);
// return approval status
return GroupUtils.getApprovalStatus(repository, data.getSignature());
}
private static void signTransactionDataForGroupApproval(Repository repository, PrivateKeyAccount signer, TransactionData transactionData) throws DataException {
byte[] reference = signer.getLastReference();
long timestamp = repository.getTransactionRepository().fromSignature(reference).getTimestamp() + 1;
BaseTransactionData baseTransactionData
= new BaseTransactionData(timestamp, Group.NO_GROUP, reference, signer.getPublicKey(), GroupUtils.fee, null);
TransactionData groupApprovalTransactionData
= new GroupApprovalTransactionData(baseTransactionData, transactionData.getSignature(), true);
TransactionUtils.signAndImportValid(repository, groupApprovalTransactionData, signer);
}
private ValidationResult joinGroup(Repository repository, PrivateKeyAccount joiner, int groupId) throws DataException { 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);
@ -332,9 +622,31 @@ public class DevGroupAdminTests extends Common {
return result; return result;
} }
private void groupInvite(Repository repository, PrivateKeyAccount admin, int groupId, String invitee, int timeToLive) throws DataException { private ValidationResult groupInvite(Repository repository, PrivateKeyAccount admin, int groupId, String invitee, int timeToLive) throws DataException {
GroupInviteTransactionData transactionData = new GroupInviteTransactionData(TestTransaction.generateBase(admin), groupId, invitee, timeToLive); 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 {
@ -347,6 +659,13 @@ 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);
@ -357,6 +676,13 @@ 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);
@ -367,6 +693,14 @@ 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

@ -26,6 +26,7 @@ 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";
@ -299,6 +300,19 @@ 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();
@ -312,6 +326,19 @@ 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() {
@ -342,6 +369,25 @@ 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() {
@ -399,7 +445,7 @@ public class HSQLDBCacheUtilsTests {
} }
@Test @Test
public void testExcludeBlockedPositive() { public void testExcludeBlockedNegative() {
ArbitraryResourceData data = new ArbitraryResourceData(); ArbitraryResourceData data = new ArbitraryResourceData();
data.name = "Joe"; data.name = "Joe";
@ -413,6 +459,21 @@ public class HSQLDBCacheUtilsTests {
); );
} }
@Test
public void testExcludeBlockedPositive() {
ArbitraryResourceData data = new ArbitraryResourceData();
data.name = "Joe";
Supplier<List<String>> supplier = () -> List.of("Joe");
filterListByMap(
List.of(data),
NAME_LEVEL, new HashMap<>(Map.of(EXCLUDE_BLOCKED, supplier)),
0
);
}
@Test @Test
public void testIncludeMetadataPositive() { public void testIncludeMetadataPositive() {
@ -600,6 +661,7 @@ 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));
@ -626,6 +688,7 @@ public class HSQLDBCacheUtilsTests {
description, description,
prefixOnly, prefixOnly,
exactMatchNames, exactMatchNames,
keywords,
defaultResource, defaultResource,
minLevel, minLevel,
followedOnly, followedOnly,

View File

@ -0,0 +1,763 @@
package org.qortal.test.utils;
import org.junit.Assert;
import org.junit.Test;
import org.qortal.asset.Asset;
import org.qortal.block.Block;
import org.qortal.crypto.Crypto;
import org.qortal.data.PaymentData;
import org.qortal.data.account.AccountBalanceData;
import org.qortal.data.account.AddressAmountData;
import org.qortal.data.account.BlockHeightRange;
import org.qortal.data.account.BlockHeightRangeAddressAmounts;
import org.qortal.data.transaction.ATTransactionData;
import org.qortal.data.transaction.BaseTransactionData;
import org.qortal.data.transaction.BuyNameTransactionData;
import org.qortal.data.transaction.DeployAtTransactionData;
import org.qortal.data.transaction.MultiPaymentTransactionData;
import org.qortal.data.transaction.PaymentTransactionData;
import org.qortal.data.transaction.RegisterNameTransactionData;
import org.qortal.data.transaction.TransactionData;
import org.qortal.data.transaction.TransferAssetTransactionData;
import org.qortal.utils.BalanceRecorderUtils;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.Optional;
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.CopyOnWriteArrayList;
import java.util.stream.Collectors;
public class BalanceRecorderUtilsTests {
public static final String RECIPIENT_ADDRESS = "recipient";
public static final String AT_ADDRESS = "atAddress";
public static final String OTHER = "Other";
@Test
public void testNotZeroForZero() {
boolean test = BalanceRecorderUtils.ADDRESS_AMOUNT_DATA_NOT_ZERO.test( new AddressAmountData("", 0));
Assert.assertFalse(test);
}
@Test
public void testNotZeroForPositive() {
boolean test = BalanceRecorderUtils.ADDRESS_AMOUNT_DATA_NOT_ZERO.test(new AddressAmountData("", 1));
Assert.assertTrue(test);
}
@Test
public void testNotZeroForNegative() {
boolean test = BalanceRecorderUtils.ADDRESS_AMOUNT_DATA_NOT_ZERO.test( new AddressAmountData("", -10));
Assert.assertTrue(test);
}
@Test
public void testAddressAmountComparatorReverseOrder() {
BlockHeightRangeAddressAmounts addressAmounts1 = new BlockHeightRangeAddressAmounts(new BlockHeightRange(2, 3, false), new ArrayList<>(0));
BlockHeightRangeAddressAmounts addressAmounts2 = new BlockHeightRangeAddressAmounts(new BlockHeightRange(1, 2, false), new ArrayList<>(0));
int compare = BalanceRecorderUtils.BLOCK_HEIGHT_RANGE_ADDRESS_AMOUNTS_COMPARATOR.compare(addressAmounts1, addressAmounts2);
Assert.assertTrue( compare > 0);
}
@Test
public void testAddressAmountComparatorForwardOrder() {
BlockHeightRangeAddressAmounts addressAmounts1 = new BlockHeightRangeAddressAmounts(new BlockHeightRange(1, 2, false), new ArrayList<>(0));
BlockHeightRangeAddressAmounts addressAmounts2 = new BlockHeightRangeAddressAmounts(new BlockHeightRange(2, 3, false), new ArrayList<>(0));
int compare = BalanceRecorderUtils.BLOCK_HEIGHT_RANGE_ADDRESS_AMOUNTS_COMPARATOR.compare(addressAmounts1, addressAmounts2);
Assert.assertTrue( compare < 0 );
}
@Test
public void testAddressAmountDataComparator() {
AddressAmountData addressAmount1 = new AddressAmountData("a", 10);
AddressAmountData addressAmount2 = new AddressAmountData("b", 20);
int compare = BalanceRecorderUtils.ADDRESS_AMOUNT_DATA_COMPARATOR.compare(addressAmount1, addressAmount2);
Assert.assertTrue( compare < 0);
}
@Test
public void testRemoveRecordingsBelowHeightNoBalances() {
int currentHeight = 5;
ConcurrentHashMap<Integer, List<AccountBalanceData>> balancesByHeight = new ConcurrentHashMap<>();
BalanceRecorderUtils.removeRecordingsBelowHeight(currentHeight, balancesByHeight);
Assert.assertEquals(0, balancesByHeight.size());
}
@Test
public void testRemoveRecordingsBelowHeightOneBalanceBelow() {
int currentHeight = 5;
ConcurrentHashMap<Integer, List<AccountBalanceData>> balancesByHeight = new ConcurrentHashMap<>(1);
balancesByHeight.put(1, new ArrayList<>(0));
Assert.assertEquals(1, balancesByHeight.size());
BalanceRecorderUtils.removeRecordingsBelowHeight(currentHeight, balancesByHeight);
Assert.assertEquals(0, balancesByHeight.size());
}
@Test
public void testRemoveRecordingsBelowHeightOneBalanceAbove() {
int currentHeight = 5;
ConcurrentHashMap<Integer, List<AccountBalanceData>> balancesByHeight = new ConcurrentHashMap<>(1);
balancesByHeight.put(10, new ArrayList<>(0));
Assert.assertEquals(1, balancesByHeight.size());
BalanceRecorderUtils.removeRecordingsBelowHeight(currentHeight, balancesByHeight);
Assert.assertEquals(1, balancesByHeight.size());
}
@Test
public void testBuildBalanceDynamicsOneAccountOneChange() {
String address = "a";
List<AccountBalanceData> balances = new ArrayList<>(1);
balances.add(new AccountBalanceData(address, 0, 2));
List<AccountBalanceData> priorBalances = new ArrayList<>(1);
priorBalances.add(new AccountBalanceData(address, 0, 1));
List<AddressAmountData> dynamics = BalanceRecorderUtils.buildBalanceDynamics(balances, priorBalances, 0, new ArrayList<>(0));
Assert.assertNotNull(dynamics);
Assert.assertEquals(1, dynamics.size());
AddressAmountData addressAmountData = dynamics.get(0);
Assert.assertNotNull(addressAmountData);
Assert.assertEquals(address, addressAmountData.getAddress());
Assert.assertEquals(1, addressAmountData.getAmount());
}
@Test
public void testBuildBalanceDynamicsOneAccountNoPrior() {
String address = "a";
List<AccountBalanceData> balances = new ArrayList<>(1);
balances.add(new AccountBalanceData(address, 0, 2));
List<AccountBalanceData> priorBalances = new ArrayList<>(0);
List<AddressAmountData> dynamics = BalanceRecorderUtils.buildBalanceDynamics(balances, priorBalances, 0, new ArrayList<>(0));
Assert.assertNotNull(dynamics);
Assert.assertEquals(1, dynamics.size());
AddressAmountData addressAmountData = dynamics.get(0);
Assert.assertNotNull(addressAmountData);
Assert.assertEquals(address, addressAmountData.getAddress());
Assert.assertEquals(2, addressAmountData.getAmount());
}
@Test
public void testBuildBalanceDynamicOneAccountAdjustment() {
List<AccountBalanceData> balances = new ArrayList<>(1);
balances.add(new AccountBalanceData(RECIPIENT_ADDRESS, 0, 20));
List<AccountBalanceData> priorBalances = new ArrayList<>(0);
priorBalances.add(new AccountBalanceData(RECIPIENT_ADDRESS, 0, 12));
List<TransactionData> transactions = new ArrayList<>();
final long amount = 5L;
final long fee = 1L;
boolean exceptionThrown = false;
try {
byte[] creatorPublicKey = TestUtils.generatePublicKey();
PaymentTransactionData paymentData
= new PaymentTransactionData(
new BaseTransactionData(0L, 0, null, creatorPublicKey, fee, null),
RECIPIENT_ADDRESS,
amount
);
transactions.add(paymentData);
List<AddressAmountData> dynamics
= BalanceRecorderUtils.buildBalanceDynamics(
balances,
priorBalances,
0,
transactions
);
Assert.assertNotNull(dynamics);
Assert.assertEquals(1, dynamics.size());
AddressAmountData addressAmountData = dynamics.get(0);
Assert.assertNotNull(addressAmountData);
Assert.assertEquals(RECIPIENT_ADDRESS, addressAmountData.getAddress());
Assert.assertEquals(3, addressAmountData.getAmount());
} catch( Exception e ) {
exceptionThrown = true;
}
Assert.assertFalse(exceptionThrown);
}
@Test
public void testBuildBalanceDynamicsTwoAccountsNegativeValues() {
String address1 = "a";
String address2 = "b";
List<AccountBalanceData> balances = new ArrayList<>(2);
balances.add(new AccountBalanceData(address1, 0, 10_000));
balances.add(new AccountBalanceData(address2, 0, 100));
List<AccountBalanceData> priorBalances = new ArrayList<>(2);
priorBalances.add(new AccountBalanceData(address2, 0, 200));
priorBalances.add(new AccountBalanceData(address1, 0, 5000));
List<AddressAmountData> dynamics = BalanceRecorderUtils.buildBalanceDynamics(balances, priorBalances, -100L, new ArrayList<>(0));
Assert.assertNotNull(dynamics);
Assert.assertEquals(2, dynamics.size());
Map<String, Long> amountByAddress
= dynamics.stream()
.collect(Collectors.toMap(dynamic -> dynamic.getAddress(), dynamic -> dynamic.getAmount()));
Assert.assertTrue(amountByAddress.containsKey(address1));
long amount1 = amountByAddress.get(address1);
Assert.assertNotNull(amount1);
Assert.assertEquals(5000L, amount1 );
Assert.assertTrue(amountByAddress.containsKey(address2));
long amount2 = amountByAddress.get(address2);
Assert.assertNotNull(amount2);
Assert.assertEquals(-100L, amount2);
}
@Test
public void testBuildBalanceDynamicsForAccountNoPriorAnyAccount() {
List<AccountBalanceData> priorBalances = new ArrayList<>(0);
AccountBalanceData accountBalance = new AccountBalanceData("a", 0, 10);
AddressAmountData dynamic = BalanceRecorderUtils.buildBalanceDynamicsForAccount(priorBalances, accountBalance);
Assert.assertNotNull(dynamic);
Assert.assertEquals(10, dynamic.getAmount());
Assert.assertEquals("a", dynamic.getAddress());
}
@Test
public void testBuildBalanceDynamicsForAccountNoPriorThisAccount() {
List<AccountBalanceData> priorBalances = new ArrayList<>(2);
priorBalances.add(new AccountBalanceData("b", 0, 100));
AccountBalanceData accountBalanceData = new AccountBalanceData("a", 0, 10);
AddressAmountData dynamic = BalanceRecorderUtils.buildBalanceDynamicsForAccount(priorBalances, accountBalanceData);
Assert.assertNotNull(dynamic);
Assert.assertEquals(10, dynamic.getAmount());
Assert.assertEquals("a", dynamic.getAddress());
}
@Test
public void testBuildBalanceDynamicsForAccountPriorForThisAndOthers() {
List<AccountBalanceData> priorBalances = new ArrayList<>(2);
priorBalances.add(new AccountBalanceData("a", 0, 100));
priorBalances.add(new AccountBalanceData("b", 0, 200));
priorBalances.add(new AccountBalanceData("c", 0, 300));
AccountBalanceData accountBalance = new AccountBalanceData("b", 0, 1000);
AddressAmountData dynamic = BalanceRecorderUtils.buildBalanceDynamicsForAccount(priorBalances, accountBalance);
Assert.assertNotNull(dynamic);
Assert.assertEquals(800, dynamic.getAmount());
Assert.assertEquals("b", dynamic.getAddress());
}
@Test
public void testRemoveRecordingAboveHeightOneOfTwo() {
int currentHeight = 10;
ConcurrentHashMap<Integer, List<AccountBalanceData>> balancesByHeight = new ConcurrentHashMap<>();
balancesByHeight.put(3, new ArrayList<>());
balancesByHeight.put(20, new ArrayList<>());
Assert.assertEquals(2, balancesByHeight.size());
BalanceRecorderUtils.removeRecordingsAboveHeight(currentHeight, balancesByHeight);
Assert.assertEquals(1, balancesByHeight.size());
Assert.assertTrue( balancesByHeight.containsKey(3));
}
@Test
public void testPriorHeightBeforeAfter() {
int currentHeight = 10;
ConcurrentHashMap<Integer, List<AccountBalanceData>> balancesByHeight = new ConcurrentHashMap<>();
balancesByHeight.put( 2, new ArrayList<>());
balancesByHeight.put(7, new ArrayList<>());
balancesByHeight.put(12, new ArrayList<>());
Optional<Integer> priorHeight = BalanceRecorderUtils.getPriorHeight(currentHeight, balancesByHeight);
Assert.assertNotNull(priorHeight);
Assert.assertTrue(priorHeight.isPresent());
Assert.assertEquals( 7, priorHeight.get().intValue());
}
@Test
public void testPriorHeightNoPriorAfterOnly() {
int currentHeight = 10;
ConcurrentHashMap<Integer, List<AccountBalanceData>> balancesByHeight = new ConcurrentHashMap<>();
balancesByHeight.put(12, new ArrayList<>());
Optional<Integer> priorHeight = BalanceRecorderUtils.getPriorHeight(currentHeight, balancesByHeight);
Assert.assertNotNull(priorHeight);
Assert.assertTrue(priorHeight.isEmpty());
}
@Test
public void testPriorHeightPriorOnly() {
int currentHeight = 10;
ConcurrentHashMap<Integer, List<AccountBalanceData>> balancesByHeight = new ConcurrentHashMap<>();
balancesByHeight.put(7, new ArrayList<>());
Optional<Integer> priorHeight = BalanceRecorderUtils.getPriorHeight(currentHeight, balancesByHeight);
Assert.assertNotNull(priorHeight);
Assert.assertTrue(priorHeight.isPresent());
Assert.assertEquals(7, priorHeight.get().intValue());
}
@Test
public void testRemoveDynamicsOnOrAboveHeightOneAbove() {
int currentHeight = 10;
CopyOnWriteArrayList<BlockHeightRangeAddressAmounts> dynamics = new CopyOnWriteArrayList<>();
BlockHeightRange range1 = new BlockHeightRange(10, 20, false);
dynamics.add(new BlockHeightRangeAddressAmounts(range1, new ArrayList<>()));
BlockHeightRange range2 = new BlockHeightRange(1, 4, false);
dynamics.add(new BlockHeightRangeAddressAmounts(range2, new ArrayList<>()));
Assert.assertEquals(2, dynamics.size());
BalanceRecorderUtils.removeDynamicsOnOrAboveHeight(currentHeight, dynamics);
Assert.assertEquals(1, dynamics.size());
Assert.assertEquals(range2, dynamics.get(0).getRange());
}
@Test
public void testRemoveDynamicsOnOrAboveOneOnOneAbove() {
int currentHeight = 11;
CopyOnWriteArrayList<BlockHeightRangeAddressAmounts> dynamics = new CopyOnWriteArrayList<>();
BlockHeightRange range1 = new BlockHeightRange(1,5, false);
dynamics.add(new BlockHeightRangeAddressAmounts(range1, new ArrayList<>()));
BlockHeightRange range2 = new BlockHeightRange(6, 11, false);
dynamics.add((new BlockHeightRangeAddressAmounts(range2, new ArrayList<>())));
BlockHeightRange range3 = new BlockHeightRange(22, 16, false);
dynamics.add(new BlockHeightRangeAddressAmounts(range3, new ArrayList<>()));
Assert.assertEquals(3, dynamics.size());
BalanceRecorderUtils.removeDynamicsOnOrAboveHeight(currentHeight, dynamics);
Assert.assertEquals(1, dynamics.size());
Assert.assertTrue( dynamics.get(0).getRange().equals(range1));
}
@Test
public void testRemoveOldestDynamicsTwice() {
CopyOnWriteArrayList<BlockHeightRangeAddressAmounts> dynamics = new CopyOnWriteArrayList<>();
dynamics.add(new BlockHeightRangeAddressAmounts(new BlockHeightRange(1, 5, false), new ArrayList<>()));
dynamics.add(new BlockHeightRangeAddressAmounts(new BlockHeightRange(5, 9, false), new ArrayList<>()));
Assert.assertEquals(2, dynamics.size());
BalanceRecorderUtils.removeOldestDynamics(dynamics);
Assert.assertEquals(1, dynamics.size());
Assert.assertTrue(dynamics.get(0).getRange().equals(new BlockHeightRange(5, 9, false)));
BalanceRecorderUtils.removeOldestDynamics(dynamics);
Assert.assertEquals(0, dynamics.size());
}
@Test
public void testMapBalanceModificationsForPaymentTransaction() {
boolean exceptionThrown = false;
try {
final long amount = 1L;
final long fee = 1L;
byte[] creatorPublicKey = TestUtils.generatePublicKey();
PaymentTransactionData paymentData
= new PaymentTransactionData(
new BaseTransactionData(0L, 0, null, creatorPublicKey, fee, null),
RECIPIENT_ADDRESS,
amount
);
// map balance modifications for addresses in the transaction
Map<String, Long> amountsByAddress = new HashMap<>();
BalanceRecorderUtils.mapBalanceModicationsForPaymentTransaction(amountsByAddress, paymentData);
// this will not add the fee, that is done in a different place
assertAmountsByAddress(amountsByAddress, amount, creatorPublicKey, RECIPIENT_ADDRESS);
} catch (Exception e) {
exceptionThrown = true;
}
Assert.assertFalse(exceptionThrown);
}
@Test
public void testMapBalanceModificationsForAssetOrderTransaction() {
boolean exceptionThrown = false;
try{
final long amount = 1L;
final long fee = 1L;
byte[] creatorPublicKey = TestUtils.generatePublicKey();
TransferAssetTransactionData transferAssetData
= new TransferAssetTransactionData(
new BaseTransactionData(0L, 0, null, creatorPublicKey, fee, null),
RECIPIENT_ADDRESS,
amount,
0
);
// map balance modifications for addresses in the transaction
Map<String, Long> amountsByAddress = new HashMap<>();
BalanceRecorderUtils.mapBalanceModificationsForTransferAssetTransaction(amountsByAddress, transferAssetData);
assertAmountsByAddress(amountsByAddress, amount, creatorPublicKey, RECIPIENT_ADDRESS);
} catch( Exception e) {
exceptionThrown = true;
}
Assert.assertFalse(exceptionThrown);
}
@Test
public void testMapBalanceModificationsForATTransactionMessageType() {
boolean exceptionThrown = false;
try {
final long fee = 1L;
byte[] creatorPublicKey = TestUtils.generatePublicKey();
Map<String, Long> amountsByAddress = new HashMap<>();
ATTransactionData atTransactionData = new ATTransactionData(new BaseTransactionData(0L, 0, null, creatorPublicKey, fee, null),
AT_ADDRESS,
RECIPIENT_ADDRESS,
new byte[0]);
BalanceRecorderUtils.mapBalanceModificationsForAtTransaction( amountsByAddress, atTransactionData);
// no balance changes for AT message
Assert.assertTrue(amountsByAddress.size() == 0);
} catch( Exception e) {
exceptionThrown = true;
}
Assert.assertFalse(exceptionThrown);
}
@Test
public void testMapBalanceModificationsForATTransactionPaymentType() {
boolean exceptionThrown = false;
try{
final long amount = 1L;
final long fee = 1L;
byte[] creatorPublicKey = TestUtils.generatePublicKey();
Map<String, Long> amountsByAddress = new HashMap<>();
ATTransactionData atTransactionData
= new ATTransactionData(
new BaseTransactionData(0L, 0, null, creatorPublicKey, fee, null),
AT_ADDRESS,
RECIPIENT_ADDRESS,
amount,
0
);
BalanceRecorderUtils.mapBalanceModificationsForAtTransaction( amountsByAddress, atTransactionData);
assertAmountByAddress(amountsByAddress, amount, RECIPIENT_ADDRESS);
assertAmountByAddress(amountsByAddress, -amount, AT_ADDRESS);
} catch( Exception e) {
exceptionThrown = true;
}
Assert.assertFalse(exceptionThrown);
}
@Test
public void testMapBalanceModificationsForBuyNameTransaction() {
boolean exceptionThrown = false;
try{
final long amount = 100L;
final long fee = 1L;
byte[] creatorPublicKey = TestUtils.generatePublicKey();
Map<String, Long> amountsByAddress = new HashMap<>();
BuyNameTransactionData buyNameData
= new BuyNameTransactionData(
new BaseTransactionData(0L, 0, null, creatorPublicKey, fee, null),
"null",
amount,
RECIPIENT_ADDRESS
);
BalanceRecorderUtils.mapBalanceModificationsForBuyNameTransaction(amountsByAddress, buyNameData);
assertAmountsByAddress(amountsByAddress, amount, creatorPublicKey, RECIPIENT_ADDRESS);
} catch( Exception e) {
exceptionThrown = true;
}
Assert.assertFalse(exceptionThrown);
}
@Test
public void testMapBalanceModificationsForMultiPaymentTransaction() {
boolean exceptionThrown = false;
try{
final long amount = 100L;
final long fee = 1L;
byte[] creatorPublicKey = TestUtils.generatePublicKey();
Map<String, Long> amountsByAddress = new HashMap<>();
List<PaymentData> payments = new ArrayList<>();
payments.add(new PaymentData(RECIPIENT_ADDRESS, 0, amount));
MultiPaymentTransactionData multiPayment
= new MultiPaymentTransactionData(new BaseTransactionData(0L, 0, null, creatorPublicKey, fee, null),
payments);
BalanceRecorderUtils.mapBalanceModificationsForMultiPaymentTransaction(amountsByAddress,multiPayment);
assertAmountsByAddress(amountsByAddress, amount, creatorPublicKey, RECIPIENT_ADDRESS);
} catch( Exception e ) {
exceptionThrown = true;
}
Assert.assertFalse(exceptionThrown);
}
@Test
public void testMapBalanceModificationsForMultiPaymentTransaction2PaymentsOneAddress() {
boolean exceptionThrown = false;
try{
final long amount = 100L;
final long fee = 1L;
byte[] creatorPublicKey = TestUtils.generatePublicKey();
Map<String, Long> amountsByAddress = new HashMap<>();
List<PaymentData> payments = new ArrayList<>();
payments.add(new PaymentData(RECIPIENT_ADDRESS, 0, amount));
payments.add(new PaymentData(RECIPIENT_ADDRESS, 0, amount));
MultiPaymentTransactionData multiPayment
= new MultiPaymentTransactionData(new BaseTransactionData(0L, 0, null, creatorPublicKey, fee, null),
payments);
BalanceRecorderUtils.mapBalanceModificationsForMultiPaymentTransaction(amountsByAddress,multiPayment);
assertAmountsByAddress(amountsByAddress, 2*amount, creatorPublicKey, RECIPIENT_ADDRESS);
} catch( Exception e ) {
exceptionThrown = true;
}
Assert.assertFalse(exceptionThrown);
}
@Test
public void testMapBalanceModificationsForMultiPaymentTransaction2PaymentsTwoAddresses() {
boolean exceptionThrown = false;
try{
final long amount = 100L;
final long fee = 1L;
byte[] creatorPublicKey = TestUtils.generatePublicKey();
Map<String, Long> amountsByAddress = new HashMap<>();
List<PaymentData> payments = new ArrayList<>();
payments.add(new PaymentData(RECIPIENT_ADDRESS, 0, amount));
payments.add(new PaymentData(OTHER, 0, amount));
MultiPaymentTransactionData multiPayment
= new MultiPaymentTransactionData(new BaseTransactionData(0L, 0, null, creatorPublicKey, fee, null),
payments);
BalanceRecorderUtils.mapBalanceModificationsForMultiPaymentTransaction(amountsByAddress,multiPayment);
assertAmountByAddress(amountsByAddress, amount, RECIPIENT_ADDRESS);
assertAmountByAddress(amountsByAddress, amount, OTHER);
String creatorAddress = Crypto.toAddress(creatorPublicKey);
assertAmountByAddress(amountsByAddress, 2*-amount, creatorAddress);
} catch( Exception e ) {
exceptionThrown = true;
}
Assert.assertFalse(exceptionThrown);
}
@Test
public void testMapBalanceModificationsForDeployAtTransaction() {
boolean exceptionThrown = false;
try{
final long amount = 3L;
final long fee = 1L;
byte[] creatorPublicKey = TestUtils.generatePublicKey();
Map<String, Long> amountsByAddress = new HashMap<>();
DeployAtTransactionData deployAt
= new DeployAtTransactionData(
new BaseTransactionData(0L, 0, null, creatorPublicKey, fee, null),
AT_ADDRESS, "name", "description", "type", "tags", new byte[0], amount, Asset.QORT
);
BalanceRecorderUtils.mapBalanceModificationsForDeployAtTransaction(amountsByAddress,deployAt);
assertAmountsByAddress(amountsByAddress, amount, creatorPublicKey, AT_ADDRESS);
} catch( Exception e) {
exceptionThrown = true;
e.printStackTrace();
}
Assert.assertFalse(exceptionThrown);
}
@Test
public void testMapBalanceModificationsForTransaction() {
boolean exceptionThrown = false;
try {
final long fee = 2;
byte[] creatorPublicKey = TestUtils.generatePublicKey();
Map<String, Long> amountsByAddress = new HashMap<>();
BalanceRecorderUtils.mapBalanceModificationsForTransaction(
amountsByAddress,
new RegisterNameTransactionData(
new BaseTransactionData(0L, 0, null, creatorPublicKey, fee, null),
"aaa", "data", "aaa")
);
String creatorAddress = Crypto.toAddress(creatorPublicKey);
assertAmountByAddress(amountsByAddress, -fee, creatorAddress);
} catch(Exception e) {
exceptionThrown = true;
}
Assert.assertFalse(exceptionThrown);
}
@Test
public void testBlockHeightRangeEqualityTrue() {
BlockHeightRange range1 = new BlockHeightRange(2, 4, false);
BlockHeightRange range2 = new BlockHeightRange(2, 4, true);
Assert.assertTrue(range1.equals(range2));
Assert.assertEquals(range1, range2);
}
@Test
public void testBloHeightRangeEqualityFalse() {
BlockHeightRange range1 = new BlockHeightRange(2, 3, true);
BlockHeightRange range2 = new BlockHeightRange(2, 4, true);
Assert.assertFalse(range1.equals(range2));
}
private static void assertAmountsByAddress(Map<String, Long> amountsByAddress, long amount, byte[] creatorPublicKey, String recipientAddress) {
assertAmountByAddress(amountsByAddress, amount, recipientAddress);
String creatorAddress = Crypto.toAddress(creatorPublicKey);
assertAmountByAddress(amountsByAddress, -amount, creatorAddress);
}
private static void assertAmountByAddress(Map<String, Long> amountsByAddress, long amount, String address) {
Long amountForAddress = amountsByAddress.get(address);
Assert.assertTrue(amountsByAddress.containsKey(address));
Assert.assertNotNull(amountForAddress);
Assert.assertEquals(amount, amountForAddress.longValue());
}
}

View File

@ -0,0 +1,102 @@
package org.qortal.test.utils;
import org.qortal.account.PrivateKeyAccount;
import org.qortal.data.transaction.CreateGroupTransactionData;
import org.qortal.data.transaction.GroupInviteTransactionData;
import org.qortal.data.transaction.JoinGroupTransactionData;
import org.qortal.data.transaction.LeaveGroupTransactionData;
import org.qortal.group.Group;
import org.qortal.repository.DataException;
import org.qortal.repository.Repository;
import org.qortal.test.common.TransactionUtils;
import org.qortal.test.common.transaction.TestTransaction;
/**
* Class GroupsTestUtils
*
* Utility methods for testing the Groups class.
*/
public class GroupsTestUtils {
/**
* Create Group
*
* @param repository the data repository
* @param owner the group owner
* @param groupName the group name
* @param isOpen true if the group is public, false for private
*
* @return the group Id
* @throws DataException
*/
public static Integer 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;
CreateGroupTransactionData transactionData = new CreateGroupTransactionData(TestTransaction.generateBase(owner), groupName, description, isOpen, approvalThreshold, minimumBlockDelay, maximumBlockDelay);
TransactionUtils.signAndMint(repository, transactionData, owner);
return repository.getGroupRepository().fromGroupName(groupName).getGroupId();
}
/**
* Join Group
*
* @param repository the data repository
* @param joiner the address for the account joining the group
* @param groupId the Id for the group to join
*
* @throws DataException
*/
public static void joinGroup(Repository repository, PrivateKeyAccount joiner, int groupId) throws DataException {
JoinGroupTransactionData transactionData = new JoinGroupTransactionData(TestTransaction.generateBase(joiner), groupId);
TransactionUtils.signAndMint(repository, transactionData, joiner);
}
/**
* Group Invite
*
* @param repository the data repository
* @param admin the admin account to sign the invite
* @param groupId the Id of the group to invite to
* @param invitee the recipient address for the invite
* @param timeToLive the time length of the invite
*
* @throws DataException
*/
public static void groupInvite(Repository repository, PrivateKeyAccount admin, int groupId, String invitee, int timeToLive) throws DataException {
GroupInviteTransactionData transactionData = new GroupInviteTransactionData(TestTransaction.generateBase(admin), groupId, invitee, timeToLive);
TransactionUtils.signAndMint(repository, transactionData, admin);
}
/**
* Leave Group
*
* @param repository the data repository
* @param leaver the account leaving
* @param groupId the Id of the group being left
*
* @throws DataException
*/
public static void leaveGroup(Repository repository, PrivateKeyAccount leaver, int groupId) throws DataException {
LeaveGroupTransactionData transactionData = new LeaveGroupTransactionData(TestTransaction.generateBase(leaver), groupId);
TransactionUtils.signAndMint(repository, transactionData, leaver);
}
/**
* Is Member?
*
* @param repository the data repository
* @param address the account address
* @param groupId the group Id
*
* @return true if the account is a member of the group, otherwise false
* @throws DataException
*/
public static boolean isMember(Repository repository, String address, int groupId) throws DataException {
return repository.getGroupRepository().memberExists(groupId, address);
}
}

View File

@ -0,0 +1,199 @@
package org.qortal.test.utils;
import org.junit.After;
import org.junit.Assert;
import org.junit.Before;
import org.junit.Test;
import org.qortal.account.PrivateKeyAccount;
import org.qortal.block.Block;
import org.qortal.block.BlockChain;
import org.qortal.repository.DataException;
import org.qortal.repository.Repository;
import org.qortal.repository.RepositoryManager;
import org.qortal.test.common.BlockUtils;
import org.qortal.test.common.Common;
import org.qortal.utils.Groups;
import java.util.List;
import static org.junit.Assert.*;
public class GroupsTests extends Common {
public static final String ALICE = "alice";
public static final String BOB = "bob";
public static final String CHLOE = "chloe";
public static final String DILBERT = "dilbert";
private static final int HEIGHT_1 = 5;
private static final int HEIGHT_2 = 8;
private static final int HEIGHT_3 = 12;
@Before
public void beforeTest() throws DataException {
Common.useDefaultSettings();
}
@After
public void afterTest() throws DataException {
Common.orphanCheck();
}
@Test
public void testGetGroupIdsToMintSimple() {
List<Integer> ids = Groups.getGroupIdsToMint(BlockChain.getInstance(), 0);
Assert.assertNotNull(ids);
Assert.assertEquals(0, ids.size());
}
@Test
public void testGetGroupIdsToMintComplex() throws DataException {
try (final Repository repository = RepositoryManager.getRepository()) {
Block block1 = BlockUtils.mintBlocks(repository, HEIGHT_1);
int height1 = block1.getBlockData().getHeight().intValue();
assertEquals(HEIGHT_1 + 1, height1);
List<Integer> ids1 = Groups.getGroupIdsToMint(BlockChain.getInstance(), height1);
Assert.assertEquals(1, ids1.size() );
Assert.assertTrue( ids1.contains( 694 ) );
Block block2 = BlockUtils.mintBlocks(repository, HEIGHT_2 - HEIGHT_1);
int height2 = block2.getBlockData().getHeight().intValue();
assertEquals( HEIGHT_2 + 1, height2);
List<Integer> ids2 = Groups.getGroupIdsToMint(BlockChain.getInstance(), height2);
Assert.assertEquals(2, ids2.size() );
Assert.assertTrue( ids2.contains( 694 ) );
Assert.assertTrue( ids2.contains( 800 ) );
Block block3 = BlockUtils.mintBlocks(repository, HEIGHT_3 - HEIGHT_2);
int height3 = block3.getBlockData().getHeight().intValue();
assertEquals( HEIGHT_3 + 1, height3);
List<Integer> ids3 = Groups.getGroupIdsToMint(BlockChain.getInstance(), height3);
Assert.assertEquals( 1, ids3.size() );
Assert.assertTrue( ids3.contains( 800 ) );
}
}
@Test
public void testMemberExistsInAnyGroupSimple() throws DataException {
try (final Repository repository = RepositoryManager.getRepository()) {
PrivateKeyAccount alice = Common.getTestAccount(repository, "alice");
PrivateKeyAccount bob = Common.getTestAccount(repository, "bob");
// Create group
int groupId = GroupsTestUtils.createGroup(repository, alice, "closed-group", false);
// Confirm Bob is not a member
Assert.assertFalse( Groups.memberExistsInAnyGroup(repository.getGroupRepository(), List.of(groupId), bob.getAddress()) );
// Bob to join
GroupsTestUtils.joinGroup(repository, bob, groupId);
// Confirm Bob still not a member
assertFalse(GroupsTestUtils.isMember(repository, bob.getAddress(), groupId));
// Have Alice 'invite' Bob to confirm membership
GroupsTestUtils.groupInvite(repository, alice, groupId, bob.getAddress(), 0); // non-expiring invite
// Confirm Bob now a member
Assert.assertTrue( Groups.memberExistsInAnyGroup(repository.getGroupRepository(), List.of(groupId), bob.getAddress()) );
}
}
@Test
public void testGroupsListedFunctionality() throws DataException {
try (final Repository repository = RepositoryManager.getRepository()) {
PrivateKeyAccount alice = Common.getTestAccount(repository, ALICE);
PrivateKeyAccount bob = Common.getTestAccount(repository, BOB);
PrivateKeyAccount chloe = Common.getTestAccount(repository, CHLOE);
PrivateKeyAccount dilbert = Common.getTestAccount(repository, DILBERT);
// Create groups
int group1Id = GroupsTestUtils.createGroup(repository, alice, "group-1", false);
int group2Id = GroupsTestUtils.createGroup(repository, bob, "group-2", false);
// test memberExistsInAnyGroup
Assert.assertTrue(Groups.memberExistsInAnyGroup(repository.getGroupRepository(), List.of(group1Id, group2Id), alice.getAddress()));
Assert.assertFalse(Groups.memberExistsInAnyGroup(repository.getGroupRepository(), List.of(group1Id, group2Id), chloe.getAddress()));
// alice is a member
Assert.assertTrue(GroupsTestUtils.isMember(repository, alice.getAddress(), group1Id));
List<String> allMembersBeforeJoin = Groups.getAllMembers(repository.getGroupRepository(), List.of(group1Id));
// assert one member
Assert.assertNotNull(allMembersBeforeJoin);
Assert.assertEquals(1, allMembersBeforeJoin.size());
List<String> allAdminsBeforeJoin = Groups.getAllAdmins(repository.getGroupRepository(), List.of(group1Id));
// assert one admin
Assert.assertNotNull(allAdminsBeforeJoin);
Assert.assertEquals( 1, allAdminsBeforeJoin.size());
// Bob to join
GroupsTestUtils.joinGroup(repository, bob, group1Id);
// Have Alice 'invite' Bob to confirm membership
GroupsTestUtils.groupInvite(repository, alice, group1Id, bob.getAddress(), 0); // non-expiring invite
List<String> allMembersAfterJoin = Groups.getAllMembers(repository.getGroupRepository(), List.of(group1Id));
// alice and bob are members
Assert.assertNotNull(allMembersAfterJoin);
Assert.assertEquals(2, allMembersAfterJoin.size());
List<String> allAdminsAfterJoin = Groups.getAllAdmins(repository.getGroupRepository(), List.of(group1Id));
// assert still one admin
Assert.assertNotNull(allAdminsAfterJoin);
Assert.assertEquals(1, allAdminsAfterJoin.size());
List<String> allAdminsFor2Groups = Groups.getAllAdmins(repository.getGroupRepository(), List.of(group1Id, group2Id));
// assert 2 admins when including the second group
Assert.assertNotNull(allAdminsFor2Groups);
Assert.assertEquals(2, allAdminsFor2Groups.size());
List<String> allMembersFor2Groups = Groups.getAllMembers(repository.getGroupRepository(), List.of(group1Id, group2Id));
// assert 2 members when including the seconds group
Assert.assertNotNull(allMembersFor2Groups);
Assert.assertEquals(2, allMembersFor2Groups.size());
GroupsTestUtils.leaveGroup(repository, bob, group1Id);
List<String> allMembersForAfterBobLeavesGroup1InAllGroups = Groups.getAllMembers(repository.getGroupRepository(), List.of(group1Id, group2Id));
// alice and bob are members of one group still
Assert.assertNotNull(allMembersForAfterBobLeavesGroup1InAllGroups);
Assert.assertEquals(2, allMembersForAfterBobLeavesGroup1InAllGroups.size());
GroupsTestUtils.groupInvite(repository, alice, group1Id, chloe.getAddress(), 3600);
GroupsTestUtils.groupInvite(repository, bob, group2Id, chloe.getAddress(), 3600);
GroupsTestUtils.joinGroup(repository, chloe, group1Id);
GroupsTestUtils.joinGroup(repository, chloe, group2Id);
List<String> allMembersAfterDilbert = Groups.getAllMembers((repository.getGroupRepository()), List.of(group1Id, group2Id));
// 3 accounts are now members of one group or another
Assert.assertNotNull(allMembersAfterDilbert);
Assert.assertEquals(3, allMembersAfterDilbert.size());
}
}
}

View File

@ -0,0 +1,48 @@
package org.qortal.test.utils;
import org.bouncycastle.jce.provider.BouncyCastleProvider;
import java.security.KeyPair;
import java.security.KeyPairGenerator;
import java.security.MessageDigest;
import java.security.PublicKey;
import java.security.Security;
public class TestUtils {
public static byte[] generatePublicKey() throws Exception {
// Add the Bouncy Castle provider
Security.addProvider(new BouncyCastleProvider());
// Generate a key pair
KeyPair keyPair = generateKeyPair();
// Get the public key
PublicKey publicKey = keyPair.getPublic();
// Get the public key as a byte array
byte[] publicKeyBytes = publicKey.getEncoded();
// Generate a RIPEMD160 message digest from the public key
byte[] ripeMd160Digest = generateRipeMd160Digest(publicKeyBytes);
return ripeMd160Digest;
}
public static KeyPair generateKeyPair() throws Exception {
// Generate a key pair using the RSA algorithm
KeyPairGenerator keyGen = KeyPairGenerator.getInstance("RSA");
keyGen.initialize(2048); // Key size (bits)
return keyGen.generateKeyPair();
}
public static byte[] generateRipeMd160Digest(byte[] input) throws Exception {
// Create a RIPEMD160 message digest instance
MessageDigest ripeMd160 = MessageDigest.getInstance("RIPEMD160", new BouncyCastleProvider());
// Update the message digest with the input bytes
ripeMd160.update(input);
// Get the message digest bytes
return ripeMd160.digest();
}
}

View File

@ -31,6 +31,12 @@
"blockRewardBatchStartHeight": 999999000, "blockRewardBatchStartHeight": 999999000,
"blockRewardBatchSize": 10, "blockRewardBatchSize": 10,
"blockRewardBatchAccountsBlockCount": 3, "blockRewardBatchAccountsBlockCount": 3,
"mintingGroupIds": [
{ "height": 0, "ids": []},
{ "height": 5, "ids": [694]},
{ "height": 8, "ids": [694, 800]},
{ "height": 12, "ids": [800]}
],
"rewardsByHeight": [ "rewardsByHeight": [
{ "height": 1, "reward": 100 }, { "height": 1, "reward": 100 },
{ "height": 11, "reward": 10 }, { "height": 11, "reward": 10 },
@ -103,7 +109,12 @@
"onlyMintWithNameHeight": 9999999999990, "onlyMintWithNameHeight": 9999999999990,
"groupMemberCheckHeight": 9999999999999, "groupMemberCheckHeight": 9999999999999,
"decreaseOnlineAccountsDifficultyTimestamp": 9999999999999, "decreaseOnlineAccountsDifficultyTimestamp": 9999999999999,
"removeOnlyMintWithNameHeight": 9999999999999 "removeOnlyMintWithNameHeight": 9999999999999,
"fixBatchRewardHeight": 9999999999999,
"adminsReplaceFoundersHeight": 9999999999999,
"ignoreLevelForRewardShareHeight": 9999999999999,
"nullGroupMembershipHeight": 20,
"adminQueryFixHeight": 9999999999999
}, },
"genesisInfo": { "genesisInfo": {
"version": 4, "version": 4,

View File

@ -69,7 +69,8 @@ die("Can't calculate SHA256 of ${project}.update\n") unless $sha256 =~ m/(\S{64}
chomp $sha256; chomp $sha256;
# long-form commit hash of HEAD on auto-update branch # long-form commit hash of HEAD on auto-update branch
my $update_hash = `git rev-parse refs/heads/auto-update-${commit_hash}`; #my $update_hash = `git rev-parse refs/heads/auto-update-${commit_hash}`;
my $update_hash = `git rev-parse origin/auto-update-${commit_hash}`;
die("Can't find commit hash for HEAD on auto-update-${commit_hash} branch\n") if ! defined $update_hash; die("Can't find commit hash for HEAD on auto-update-${commit_hash} branch\n") if ! defined $update_hash;
chomp $update_hash; chomp $update_hash;
@ -132,11 +133,57 @@ my $signed_tx = `curl --silent -H "accept: text/plain" -H "Content-Type: applica
die("Can't sign raw transaction:\n$signed_tx\n") unless $signed_tx =~ m/^\w{390,410}$/; # +90ish longer than $raw_tx die("Can't sign raw transaction:\n$signed_tx\n") unless $signed_tx =~ m/^\w{390,410}$/; # +90ish longer than $raw_tx
printf "\nSigned transaction:\n%s\n", $signed_tx; printf "\nSigned transaction:\n%s\n", $signed_tx;
# Check we can actually fetch update # Get the origin URL - So that we will be able to TEST the obtaining of the qortal.update...
my $origin = `git remote get-url origin`; my $origin = `git remote get-url origin`;
die("Unable to get github url for 'origin'?\n") unless $origin && $origin =~ m/:(.*)\.git$/; chomp $origin; # Remove any trailing newlines
my $repo = $1; die("Unable to get github url for 'origin'?\n") unless $origin;
# Debug: Print the origin URL
print "Full Origin URL: $origin\n";
# Extract the repository path (e.g., Qortal/qortal) NOTE - github is case-sensitive with repo names
my $repo;
if ($origin =~ m/[:\/]([\w\-]+\/[\w\-]+)\.git$/) {
$repo = $1;
print "Extracted direct repository path: $repo\n";
if ($repo =~ m/^qortal\//i) {
$repo =~ s/^qortal\//Qortal\//;
print "Corrected repository path capitalization: $repo\n";
}
print "Please verify the direct repository path. Current: '$repo'\n";
print "If incorrect, input the correct direct repository path (e.g., 'Qortal/qortal' or 'bob/qortal').NOTE - github is CASE SENSITIVE for repository urls... Press Enter to keep the extracted version: ";
my $input = <STDIN>;
if ($input =~ m/^qortal\//i) {
$input =~ s/^qortal\//Qortal\//;
print "Corrected repository path capitalization: $repo\n";
}
chomp $input;
$repo = $input if $input; # Update repo if user provides input
} else {
# Default to qortal/qortal if extraction fails
$repo = "Qortal/qortal";
print "Failed to extract repository path from origin URL. Using default: $repo\n";
# Prompt the user for confirmation or input
print "Please verify the repository path. Current: '$repo'\n";
print "If incorrect, input the correct repository path (e.g., 'Qortal/qortal' or 'BobsCodeburgers/qortal'). NOTE - GitHub is CASE SENSITIVE for repository urls... Press Enter to keep the default: ";
my $input = <STDIN>;
if ($input =~ m/^qortal\//i) {
$input =~ s/^qortal\//Qortal\//;
print "Corrected repository path capitalization: $repo\n";
}
chomp $input;
$repo = $input if $input; # Update repo if user provides input
}
# Debug: Print the final repository path
print "Final direct repository path: $repo\n";
# Construct the update URL
my $update_url = "https://github.com/${repo}/raw/${update_hash}/${project}.update"; my $update_url = "https://github.com/${repo}/raw/${update_hash}/${project}.update";
print "Final update URL: $update_url\n";
my $fetch_result = `curl --silent -o /dev/null --location --range 0-1 --head --write-out '%{http_code}' --url ${update_url}`; my $fetch_result = `curl --silent -o /dev/null --location --range 0-1 --head --write-out '%{http_code}' --url ${update_url}`;
die("\nUnable to fetch update from ${update_url}\n") if $fetch_result ne '200'; die("\nUnable to fetch update from ${update_url}\n") if $fetch_result ne '200';