diff --git a/pom.xml b/pom.xml
index 71424e58..82e0f42f 100644
--- a/pom.xml
+++ b/pom.xml
@@ -3,7 +3,7 @@
4.0.0
org.qortal
qortal
- 4.7.0
+ 4.7.1
jar
UTF-8
diff --git a/src/main/java/org/qortal/account/Account.java b/src/main/java/org/qortal/account/Account.java
index 856b79ef..722e70da 100644
--- a/src/main/java/org/qortal/account/Account.java
+++ b/src/main/java/org/qortal/account/Account.java
@@ -14,6 +14,7 @@ import org.qortal.repository.NameRepository;
import org.qortal.repository.Repository;
import org.qortal.settings.Settings;
import org.qortal.utils.Base58;
+import org.qortal.utils.Groups;
import javax.xml.bind.annotation.XmlAccessType;
import javax.xml.bind.annotation.XmlAccessorType;
@@ -227,7 +228,7 @@ public class Account {
}
int level = accountData.getLevel();
- int groupIdToMint = BlockChain.getInstance().getMintingGroupId();
+ List groupIdsToMint = Groups.getGroupIdsToMint( BlockChain.getInstance(), blockchainHeight );
int nameCheckHeight = BlockChain.getInstance().getOnlyMintWithNameHeight();
int groupCheckHeight = BlockChain.getInstance().getGroupMemberCheckHeight();
int removeNameCheckHeight = BlockChain.getInstance().getRemoveOnlyMintWithNameHeight();
@@ -261,9 +262,9 @@ public class Account {
if (blockchainHeight >= groupCheckHeight && blockchainHeight < removeNameCheckHeight) {
List myName = nameRepository.getNamesByOwner(myAddress);
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 {
- return level >= levelToMint && !myName.isEmpty() && (isGroupValidated || groupRepository.memberExists(groupIdToMint, myAddress));
+ return level >= levelToMint && !myName.isEmpty() && (isGroupValidated || Groups.memberExistsInAnyGroup(groupRepository, groupIdsToMint, myAddress));
}
}
@@ -272,9 +273,9 @@ public class Account {
// Account's address is a member of the minter group
if (blockchainHeight >= removeNameCheckHeight) {
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 {
- return level >= levelToMint && (isGroupValidated || groupRepository.memberExists(groupIdToMint, myAddress));
+ return level >= levelToMint && (isGroupValidated || Groups.memberExistsInAnyGroup(groupRepository, groupIdsToMint, myAddress));
}
}
diff --git a/src/main/java/org/qortal/api/ApiService.java b/src/main/java/org/qortal/api/ApiService.java
index fbef50d3..2cebe8e5 100644
--- a/src/main/java/org/qortal/api/ApiService.java
+++ b/src/main/java/org/qortal/api/ApiService.java
@@ -194,6 +194,7 @@ public class ApiService {
context.addServlet(AdminStatusWebSocket.class, "/websockets/admin/status");
context.addServlet(BlocksWebSocket.class, "/websockets/blocks");
+ context.addServlet(DataMonitorSocket.class, "/websockets/datamonitor");
context.addServlet(ActiveChatsWebSocket.class, "/websockets/chat/active/*");
context.addServlet(ChatMessagesWebSocket.class, "/websockets/chat/messages");
context.addServlet(TradeOffersWebSocket.class, "/websockets/crosschain/tradeoffers");
diff --git a/src/main/java/org/qortal/api/model/CrossChainTradeLedgerEntry.java b/src/main/java/org/qortal/api/model/CrossChainTradeLedgerEntry.java
new file mode 100644
index 00000000..34f8fc57
--- /dev/null
+++ b/src/main/java/org/qortal/api/model/CrossChainTradeLedgerEntry.java
@@ -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;
+ }
+}
\ No newline at end of file
diff --git a/src/main/java/org/qortal/api/resource/ArbitraryResource.java b/src/main/java/org/qortal/api/resource/ArbitraryResource.java
index 754c3467..a6f44373 100644
--- a/src/main/java/org/qortal/api/resource/ArbitraryResource.java
+++ b/src/main/java/org/qortal/api/resource/ArbitraryResource.java
@@ -33,9 +33,13 @@ import org.qortal.controller.arbitrary.ArbitraryDataStorageManager;
import org.qortal.controller.arbitrary.ArbitraryMetadataManager;
import org.qortal.data.account.AccountData;
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.ArbitraryResourceMetadata;
import org.qortal.data.arbitrary.ArbitraryResourceStatus;
+import org.qortal.data.arbitrary.IndexCache;
import org.qortal.data.naming.NameData;
import org.qortal.data.transaction.ArbitraryTransactionData;
import org.qortal.data.transaction.TransactionData;
@@ -69,8 +73,11 @@ import java.nio.file.Files;
import java.nio.file.Paths;
import java.util.ArrayList;
import java.util.Arrays;
+import java.util.Comparator;
import java.util.List;
+import java.util.Map;
import java.util.Objects;
+import java.util.stream.Collectors;
@Path("/arbitrary")
@Tag(name = "Arbitrary")
@@ -172,6 +179,7 @@ public class ArbitraryResource {
@Parameter(description = "Name (searches name field only)") @QueryParam("name") List names,
@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 = "Keyword (searches description metadata field by keywords)") @QueryParam("keywords") List keywords,
@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 = "Default resources (without identifiers) only") @QueryParam("default") Boolean defaultResource,
@@ -212,7 +220,7 @@ public class ArbitraryResource {
}
List 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,
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 searchIndices(@QueryParam("terms") String[] terms) {
+
+ List indices = new ArrayList<>();
+
+ // get index details for each term
+ for( String term : terms ) {
+ List details = IndexCache.getInstance().getIndicesByTerm().get(term);
+
+ if( details != null ) {
+ indices.addAll(details);
+ }
+ }
+
+ // sum up the scores for each index with identical attributes
+ Map 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 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 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
diff --git a/src/main/java/org/qortal/api/resource/CrossChainResource.java b/src/main/java/org/qortal/api/resource/CrossChainResource.java
index 748dcbe4..3f7acf68 100644
--- a/src/main/java/org/qortal/api/resource/CrossChainResource.java
+++ b/src/main/java/org/qortal/api/resource/CrossChainResource.java
@@ -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.security.SecurityRequirement;
import io.swagger.v3.oas.annotations.tags.Tag;
+import org.glassfish.jersey.media.multipart.ContentDisposition;
import org.qortal.api.ApiError;
import org.qortal.api.ApiErrors;
import org.qortal.api.ApiExceptionFactory;
import org.qortal.api.Security;
import org.qortal.api.model.CrossChainCancelRequest;
+import org.qortal.api.model.CrossChainTradeLedgerEntry;
import org.qortal.api.model.CrossChainTradeSummary;
import org.qortal.controller.tradebot.TradeBot;
import org.qortal.crosschain.ACCT;
@@ -44,10 +46,14 @@ import org.qortal.utils.Base58;
import org.qortal.utils.ByteArray;
import org.qortal.utils.NTP;
+import javax.servlet.ServletContext;
import javax.servlet.http.HttpServletRequest;
+import javax.servlet.http.HttpServletResponse;
import javax.ws.rs.*;
import javax.ws.rs.core.Context;
+import javax.ws.rs.core.HttpHeaders;
import javax.ws.rs.core.MediaType;
+import java.io.IOException;
import java.util.*;
import java.util.function.Supplier;
import java.util.stream.Collectors;
@@ -61,6 +67,13 @@ public class CrossChainResource {
@Context
HttpServletRequest request;
+ @Context
+ HttpServletResponse response;
+
+ @Context
+ ServletContext context;
+
+
@GET
@Path("/tradeoffers")
@Operation(
@@ -258,11 +271,11 @@ public class CrossChainResource {
example = "1597310000000"
) @QueryParam("minimumTimestamp") Long minimumTimestamp,
@Parameter(
- description = "Optionally filter by buyer Qortal address"
- ) @QueryParam("buyerAddress") String buyerAddress,
+ description = "Optionally filter by buyer Qortal public key"
+ ) @QueryParam("buyerPublicKey") String buyerPublicKey58,
@Parameter(
- description = "Optionally filter by seller Qortal address"
- ) @QueryParam("sellerAddress") String sellerAddress,
+ description = "Optionally filter by seller Qortal public key"
+ ) @QueryParam("sellerPublicKey") String sellerPublicKey58,
@Parameter( ref = "limit") @QueryParam("limit") Integer limit,
@Parameter( ref = "offset" ) @QueryParam("offset") Integer offset,
@Parameter( ref = "reverse" ) @QueryParam("reverse") Boolean reverse) {
@@ -274,6 +287,10 @@ public class CrossChainResource {
if (minimumTimestamp != null && minimumTimestamp <= 0)
throw ApiExceptionFactory.INSTANCE.createException(request, ApiError.INVALID_CRITERIA);
+ // Decode public keys
+ byte[] buyerPublicKey = decodePublicKey(buyerPublicKey58);
+ byte[] sellerPublicKey = decodePublicKey(sellerPublicKey58);
+
final Boolean isFinished = Boolean.TRUE;
try (final Repository repository = RepositoryManager.getRepository()) {
@@ -304,7 +321,7 @@ public class CrossChainResource {
byte[] codeHash = acctInfo.getKey().value;
ACCT acct = acctInfo.getValue().get();
- List atStates = repository.getATRepository().getMatchingFinalATStates(codeHash, buyerAddress, sellerAddress,
+ List atStates = repository.getATRepository().getMatchingFinalATStates(codeHash, buyerPublicKey, sellerPublicKey,
isFinished, acct.getModeByteOffset(), (long) AcctMode.REDEEMED.value, minimumFinalHeight,
limit, offset, reverse);
@@ -343,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 crossChainTradeLedgerEntries = new ArrayList<>();
+
+ Map> acctsByCodeHash = SupportedBlockchain.getAcctMap();
+
+ // collect ledger entries for each ACCT
+ for (Map.Entry> 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
@Path("/price/{blockchain}")
@Operation(
diff --git a/src/main/java/org/qortal/api/resource/CrossChainUtils.java b/src/main/java/org/qortal/api/resource/CrossChainUtils.java
index 802faca1..ddd1d2d6 100644
--- a/src/main/java/org/qortal/api/resource/CrossChainUtils.java
+++ b/src/main/java/org/qortal/api/resource/CrossChainUtils.java
@@ -10,21 +10,36 @@ import org.bitcoinj.script.ScriptBuilder;
import org.bouncycastle.util.Strings;
import org.json.simple.JSONObject;
+import org.qortal.api.model.CrossChainTradeLedgerEntry;
import org.qortal.api.model.crosschain.BitcoinyTBDRequest;
import org.qortal.crosschain.*;
import org.qortal.data.at.ATData;
+import org.qortal.data.at.ATStateData;
import org.qortal.data.crosschain.*;
import org.qortal.repository.DataException;
import org.qortal.repository.Repository;
+import org.qortal.utils.Amounts;
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.stream.Collectors;
public class CrossChainUtils {
+ public static final String QORT_CURRENCY_CODE = "QORT";
private static final Logger LOGGER = LogManager.getLogger(CrossChainUtils.class);
public static final String CORE_API_CALL = "Core API Call";
+ public static final String QORTAL_EXCHANGE_LABEL = "Qortal";
public static ServerConfigurationInfo buildServerConfigurationInfo(Bitcoiny blockchain) {
@@ -632,4 +647,128 @@ public class CrossChainUtils {
byte[] lockTimeABytes = BitTwiddling.toBEByteArray((long) lockTimeA);
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 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 entries,
+ byte[] codeHash,
+ ACCT acct,
+ boolean isBuy) throws DataException {
+
+ // get all the final AT states for the code hash (foreign coin)
+ List 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);
+ }
+ }
}
\ No newline at end of file
diff --git a/src/main/java/org/qortal/api/websocket/DataMonitorSocket.java b/src/main/java/org/qortal/api/websocket/DataMonitorSocket.java
new file mode 100644
index 00000000..a93bf2ed
--- /dev/null
+++ b/src/main/java/org/qortal/api/websocket/DataMonitorSocket.java
@@ -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
+ }
+ }
+
+}
diff --git a/src/main/java/org/qortal/arbitrary/ArbitraryDataReader.java b/src/main/java/org/qortal/arbitrary/ArbitraryDataReader.java
index 78a9ee86..6d7e0e23 100644
--- a/src/main/java/org/qortal/arbitrary/ArbitraryDataReader.java
+++ b/src/main/java/org/qortal/arbitrary/ArbitraryDataReader.java
@@ -439,7 +439,15 @@ public class ArbitraryDataReader {
// Ensure the complete hash matches the joined chunks
if (!Arrays.equals(arbitraryDataFile.digest(), transactionData.getData())) {
// 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");
}
}
diff --git a/src/main/java/org/qortal/block/Block.java b/src/main/java/org/qortal/block/Block.java
index 21cbddc4..67e6dd43 100644
--- a/src/main/java/org/qortal/block/Block.java
+++ b/src/main/java/org/qortal/block/Block.java
@@ -39,6 +39,7 @@ import org.qortal.transform.block.BlockTransformer;
import org.qortal.transform.transaction.TransactionTransformer;
import org.qortal.utils.Amounts;
import org.qortal.utils.Base58;
+import org.qortal.utils.Groups;
import org.qortal.utils.NTP;
import java.io.ByteArrayOutputStream;
@@ -150,7 +151,7 @@ public class Block {
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.sharePercent = this.rewardShareData.getSharePercent();
@@ -159,7 +160,12 @@ public class Block {
this.isMinterFounder = Account.isFounder(mintingAccountData.getFlags());
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) {
// Self-share: minter is also recipient
@@ -435,9 +441,9 @@ public class Block {
if (height >= BlockChain.getInstance().getGroupMemberCheckHeight()) {
onlineAccounts.removeIf(a -> {
try {
- int groupId = BlockChain.getInstance().getMintingGroupId();
+ List groupIdsToMint = Groups.getGroupIdsToMint(BlockChain.getInstance(), height);
String address = Account.getRewardShareMintingAddress(repository, a.getPublicKey());
- boolean isMinterGroupMember = repository.getGroupRepository().memberExists(groupId, address);
+ boolean isMinterGroupMember = Groups.memberExistsInAnyGroup(repository.getGroupRepository(), groupIdsToMint, address);
return !isMinterGroupMember;
} catch (DataException e) {
// Something went wrong, so remove the account
@@ -753,7 +759,7 @@ public class Block {
List expandedAccounts = new ArrayList<>();
for (RewardShareData rewardShare : this.cachedOnlineRewardShares) {
- expandedAccounts.add(new ExpandedAccount(repository, rewardShare));
+ expandedAccounts.add(new ExpandedAccount(repository, rewardShare, this.blockData.getHeight()));
}
this.cachedExpandedAccounts = expandedAccounts;
@@ -2485,11 +2491,10 @@ public class Block {
try (final Repository repository = RepositoryManager.getRepository()) {
GroupRepository groupRepository = repository.getGroupRepository();
+ List mintingGroupIds = Groups.getGroupIdsToMint(BlockChain.getInstance(), this.blockData.getHeight());
+
// all minter admins
- List minterAdmins
- = groupRepository.getGroupAdmins(BlockChain.getInstance().getMintingGroupId()).stream()
- .map(GroupAdminData::getAdmin)
- .collect(Collectors.toList());
+ List minterAdmins = Groups.getAllAdmins(groupRepository, mintingGroupIds);
// all minter admins that are online
List onlineMinterAdminAccounts
diff --git a/src/main/java/org/qortal/block/BlockChain.java b/src/main/java/org/qortal/block/BlockChain.java
index 1468fbc3..bce09aed 100644
--- a/src/main/java/org/qortal/block/BlockChain.java
+++ b/src/main/java/org/qortal/block/BlockChain.java
@@ -212,7 +212,13 @@ public class BlockChain {
private int minAccountLevelToRewardShare;
private int maxRewardSharesPerFounderMintingAccount;
private int founderEffectiveMintingLevel;
- private int mintingGroupId;
+
+ public static class IdsForHeight {
+ public int height;
+ public List ids;
+ }
+
+ private List mintingGroupIds;
/** Minimum time to retain online account signatures (ms) for block validity checks. */
private long onlineAccountSignaturesMinLifetime;
@@ -544,8 +550,8 @@ public class BlockChain {
return this.onlineAccountSignaturesMaxLifetime;
}
- public int getMintingGroupId() {
- return this.mintingGroupId;
+ public List getMintingGroupIds() {
+ return mintingGroupIds;
}
public CiyamAtSettings getCiyamAtSettings() {
diff --git a/src/main/java/org/qortal/controller/Controller.java b/src/main/java/org/qortal/controller/Controller.java
index 5697f1ae..4f885e7e 100644
--- a/src/main/java/org/qortal/controller/Controller.java
+++ b/src/main/java/org/qortal/controller/Controller.java
@@ -427,6 +427,12 @@ public class Controller extends Thread {
LOGGER.info("Db Cache Disabled");
}
+ LOGGER.info("Arbitrary Indexing Starting ...");
+ ArbitraryIndexUtils.startCaching(
+ Settings.getInstance().getArbitraryIndexingPriority(),
+ Settings.getInstance().getArbitraryIndexingFrequency()
+ );
+
if( Settings.getInstance().isBalanceRecorderEnabled() ) {
Optional recorder = HSQLDBBalanceRecorder.getInstance();
@@ -554,6 +560,16 @@ public class Controller extends Thread {
ArbitraryDataStorageManager.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");
OnlineAccountsManager.getInstance().start();
diff --git a/src/main/java/org/qortal/controller/OnlineAccountsManager.java b/src/main/java/org/qortal/controller/OnlineAccountsManager.java
index 332bf867..bbca4c7b 100644
--- a/src/main/java/org/qortal/controller/OnlineAccountsManager.java
+++ b/src/main/java/org/qortal/controller/OnlineAccountsManager.java
@@ -25,6 +25,7 @@ import org.qortal.repository.Repository;
import org.qortal.repository.RepositoryManager;
import org.qortal.settings.Settings;
import org.qortal.utils.Base58;
+import org.qortal.utils.Groups;
import org.qortal.utils.NTP;
import org.qortal.utils.NamedThreadFactory;
@@ -225,11 +226,14 @@ public class OnlineAccountsManager {
Set onlineAccountsToAdd = new HashSet<>();
Set onlineAccountsToRemove = new HashSet<>();
try (final Repository repository = RepositoryManager.getRepository()) {
+
+ int blockHeight = repository.getBlockRepository().getBlockchainHeight();
+
List mintingGroupMemberAddresses
- = repository.getGroupRepository()
- .getGroupMembers(BlockChain.getInstance().getMintingGroupId()).stream()
- .map(GroupMemberData::getMember)
- .collect(Collectors.toList());
+ = Groups.getAllMembers(
+ repository.getGroupRepository(),
+ Groups.getGroupIdsToMint(BlockChain.getInstance(), blockHeight)
+ );
for (OnlineAccountData onlineAccountData : this.onlineAccountsImportQueue) {
if (isStopping)
diff --git a/src/main/java/org/qortal/controller/arbitrary/ArbitraryDataCacheManager.java b/src/main/java/org/qortal/controller/arbitrary/ArbitraryDataCacheManager.java
index d6b9303f..9accd9c7 100644
--- a/src/main/java/org/qortal/controller/arbitrary/ArbitraryDataCacheManager.java
+++ b/src/main/java/org/qortal/controller/arbitrary/ArbitraryDataCacheManager.java
@@ -2,22 +2,30 @@ package org.qortal.controller.arbitrary;
import org.apache.logging.log4j.LogManager;
import org.apache.logging.log4j.Logger;
-import org.qortal.api.resource.TransactionsResource;
import org.qortal.controller.Controller;
import org.qortal.data.arbitrary.ArbitraryResourceData;
import org.qortal.data.transaction.ArbitraryTransactionData;
+import org.qortal.event.DataMonitorEvent;
+import org.qortal.event.EventBus;
import org.qortal.gui.SplashFrame;
import org.qortal.repository.DataException;
import org.qortal.repository.Repository;
import org.qortal.repository.RepositoryManager;
import org.qortal.settings.Settings;
import org.qortal.transaction.ArbitraryTransaction;
-import org.qortal.transaction.Transaction;
import org.qortal.utils.Base58;
+import java.text.NumberFormat;
import java.util.ArrayList;
import java.util.Collections;
+import java.util.HashMap;
+import java.util.HashSet;
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 {
@@ -29,6 +37,11 @@ public class ArbitraryDataCacheManager extends Thread {
/** Queue of arbitrary transactions that require cache updates */
private final List updateQueue = Collections.synchronizedList(new ArrayList<>());
+ private static final NumberFormat FORMATTER = NumberFormat.getNumberInstance();
+
+ static {
+ FORMATTER.setGroupingUsed(true);
+ }
public static synchronized ArbitraryDataCacheManager getInstance() {
if (instance == null) {
@@ -45,17 +58,22 @@ public class ArbitraryDataCacheManager extends Thread {
try {
while (!Controller.isStopping()) {
- Thread.sleep(500L);
+ try {
+ Thread.sleep(500L);
- // Process queue
- processResourceQueue();
+ // Process queue
+ 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
- processResourceQueue();
+ // Clear queue before terminating thread
+ processResourceQueue();
+ } catch (Exception e) {
+ LOGGER.error(e.getMessage(), e);
+ }
}
public void shutdown() {
@@ -85,14 +103,25 @@ public class ArbitraryDataCacheManager extends Thread {
// Update arbitrary resource caches
try {
ArbitraryTransaction arbitraryTransaction = new ArbitraryTransaction(repository, transactionData);
- arbitraryTransaction.updateArbitraryResourceCache(repository);
- arbitraryTransaction.updateArbitraryMetadataCache(repository);
+ arbitraryTransaction.updateArbitraryResourceCacheIncludingMetadata(repository, new HashSet<>(0), new HashMap<>(0));
repository.saveChanges();
// Update status as separate commit, as this is more prone to failure
arbitraryTransaction.updateArbitraryResourceStatus(repository);
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())));
} catch (DataException e) {
@@ -103,6 +132,9 @@ public class ArbitraryDataCacheManager extends Thread {
} catch (DataException 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) {
@@ -148,34 +180,66 @@ public class ArbitraryDataCacheManager extends Thread {
LOGGER.info("Building arbitrary resources cache...");
SplashFrame.getInstance().updateStatus("Building QDN cache - please wait...");
- final int batchSize = 100;
+ final int batchSize = Settings.getInstance().getBuildArbitraryResourcesBatchSize();
int offset = 0;
+ List allArbitraryTransactionsInDescendingOrder
+ = repository.getArbitraryRepository().getLatestArbitraryTransactions();
+
+ LOGGER.info("arbitrary transactions: count = " + allArbitraryTransactionsInDescendingOrder.size());
+
+ List resources = repository.getArbitraryRepository().getArbitraryResources(null, null, true);
+
+ Map 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 latestTransactionsWrapped = new HashSet<>(allArbitraryTransactionsInDescendingOrder.size());
+
// Loop through all ARBITRARY transactions, and determine latest state
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 signatures = repository.getTransactionRepository().getSignaturesMatchingCriteria(null, null, null, List.of(Transaction.TransactionType.ARBITRARY), null, null, null, TransactionsResource.ConfirmationStatus.BOTH, batchSize, offset, false);
- if (signatures.isEmpty()) {
+ List transactionsToProcess
+ = allArbitraryTransactionsInDescendingOrder.stream()
+ .skip(offset)
+ .limit(batchSize)
+ .collect(Collectors.toList());
+
+ if (transactionsToProcess.isEmpty()) {
// Complete
break;
}
- // Expand signatures to transactions
- for (byte[] signature : signatures) {
- ArbitraryTransactionData transactionData = (ArbitraryTransactionData) repository
- .getTransactionRepository().fromSignature(signature);
+ try {
+ for( ArbitraryTransactionData transactionData : transactionsToProcess) {
+ if (transactionData.getService() == null) {
+ // Unsupported service - ignore this resource
+ continue;
+ }
- if (transactionData.getService() == null) {
- // Unsupported service - ignore this resource
- continue;
+ latestTransactionsWrapped.add(new ArbitraryTransactionDataHashWrapper(transactionData));
+
+ // Update arbitrary resource caches
+ ArbitraryTransaction arbitraryTransaction = new ArbitraryTransaction(repository, transactionData);
+ arbitraryTransaction.updateArbitraryResourceCacheIncludingMetadata(repository, latestTransactionsWrapped, resourceByWrapper);
}
-
- // Update arbitrary resource caches
- ArbitraryTransaction arbitraryTransaction = new ArbitraryTransaction(repository, transactionData);
- arbitraryTransaction.updateArbitraryResourceCache(repository);
- arbitraryTransaction.updateArbitraryMetadataCache(repository);
repository.saveChanges();
+ } catch (DataException e) {
+ repository.discardChanges();
+
+ LOGGER.error(e.getMessage(), e);
}
offset += batchSize;
}
@@ -193,6 +257,11 @@ public class ArbitraryDataCacheManager extends Thread {
repository.discardChanges();
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 {
@@ -200,27 +269,48 @@ public class ArbitraryDataCacheManager extends Thread {
LOGGER.info("Refreshing arbitrary resource statuses for locally hosted transactions...");
SplashFrame.getInstance().updateStatus("Refreshing statuses - please wait...");
- final int batchSize = 100;
+ final int batchSize = Settings.getInstance().getBuildArbitraryResourcesBatchSize();
int offset = 0;
+ List allHostedTransactions
+ = ArbitraryDataStorageManager.getInstance()
+ .listAllHostedTransactions(repository, null, null);
+
// Loop through all ARBITRARY transactions, and determine latest state
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 hostedTransactions
+ = allHostedTransactions.stream()
+ .skip(offset)
+ .limit(batchSize)
+ .collect(Collectors.toList());
- List hostedTransactions = ArbitraryDataStorageManager.getInstance().listAllHostedTransactions(repository, batchSize, offset);
if (hostedTransactions.isEmpty()) {
// Complete
break;
}
- // Loop through hosted transactions
- for (ArbitraryTransactionData transactionData : hostedTransactions) {
+ try {
+ // Loop through hosted transactions
+ for (ArbitraryTransactionData transactionData : hostedTransactions) {
- // Determine status and update cache
- ArbitraryTransaction arbitraryTransaction = new ArbitraryTransaction(repository, transactionData);
- arbitraryTransaction.updateArbitraryResourceStatus(repository);
+ // Determine status and update cache
+ ArbitraryTransaction arbitraryTransaction = new ArbitraryTransaction(repository, transactionData);
+ arbitraryTransaction.updateArbitraryResourceStatus(repository);
+ }
repository.saveChanges();
+ } catch (DataException e) {
+ repository.discardChanges();
+
+ LOGGER.error(e.getMessage(), e);
}
+
offset += batchSize;
}
@@ -234,6 +324,11 @@ public class ArbitraryDataCacheManager extends Thread {
repository.discardChanges();
throw new DataException("Refresh of arbitrary resource statuses failed.");
}
+ catch (Exception e) {
+ LOGGER.error(e.getMessage(), e);
+
+ return false;
+ }
}
}
diff --git a/src/main/java/org/qortal/controller/arbitrary/ArbitraryDataCleanupManager.java b/src/main/java/org/qortal/controller/arbitrary/ArbitraryDataCleanupManager.java
index aa29a7b8..ce4dd565 100644
--- a/src/main/java/org/qortal/controller/arbitrary/ArbitraryDataCleanupManager.java
+++ b/src/main/java/org/qortal/controller/arbitrary/ArbitraryDataCleanupManager.java
@@ -2,9 +2,10 @@ package org.qortal.controller.arbitrary;
import org.apache.logging.log4j.LogManager;
import org.apache.logging.log4j.Logger;
-import org.qortal.api.resource.TransactionsResource.ConfirmationStatus;
import org.qortal.data.transaction.ArbitraryTransactionData;
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.Repository;
import org.qortal.repository.RepositoryManager;
@@ -21,8 +22,12 @@ import java.nio.file.Paths;
import java.security.SecureRandom;
import java.util.ArrayList;
import java.util.Arrays;
+import java.util.HashSet;
import java.util.List;
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;
@@ -77,6 +82,19 @@ public class ArbitraryDataCleanupManager extends Thread {
final int limit = 100;
int offset = 0;
+ List allArbitraryTransactionsInDescendingOrder;
+
+ try (final Repository repository = RepositoryManager.getRepository()) {
+ allArbitraryTransactionsInDescendingOrder
+ = repository.getArbitraryRepository()
+ .getLatestArbitraryTransactions();
+ } catch( Exception e) {
+ LOGGER.error(e.getMessage(), e);
+ allArbitraryTransactionsInDescendingOrder = new ArrayList<>(0);
+ }
+
+ Set processedTransactions = new HashSet<>();
+
try {
while (!isStopping) {
Thread.sleep(30000);
@@ -107,27 +125,31 @@ public class ArbitraryDataCleanupManager extends Thread {
// Any arbitrary transactions we want to fetch data for?
try (final Repository repository = RepositoryManager.getRepository()) {
- List signatures = repository.getTransactionRepository().getSignaturesMatchingCriteria(null, null, null, ARBITRARY_TX_TYPE, null, null, null, ConfirmationStatus.BOTH, limit, offset, true);
- // LOGGER.info("Found {} arbitrary transactions at offset: {}, limit: {}", signatures.size(), offset, limit);
+ List transactions = allArbitraryTransactionsInDescendingOrder.stream().skip(offset).limit(limit).collect(Collectors.toList());
if (isStopping) {
return;
}
- if (signatures == null || signatures.isEmpty()) {
+ if (transactions == null || transactions.isEmpty()) {
offset = 0;
- continue;
+ allArbitraryTransactionsInDescendingOrder
+ = repository.getArbitraryRepository()
+ .getLatestArbitraryTransactions();
+ transactions = allArbitraryTransactionsInDescendingOrder.stream().limit(limit).collect(Collectors.toList());
+ processedTransactions.clear();
}
+
offset += limit;
now = NTP.getTime();
// Loop through the signatures in this batch
- for (int i=0; i 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;
}
@@ -199,7 +255,21 @@ public class ArbitraryDataCleanupManager extends Thread {
LOGGER.debug(String.format("Transaction %s has complete file and all chunks",
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;
}
@@ -237,17 +307,6 @@ public class ArbitraryDataCleanupManager extends Thread {
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) {
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
}
- 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 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 processedTransactions = new HashSet<>();
+
while (!isStopping) {
Thread.sleep(1000L);
// Any arbitrary transactions we want to fetch data for?
try (final Repository repository = RepositoryManager.getRepository()) {
- List signatures = repository.getTransactionRepository().getSignaturesMatchingCriteria(null, null, null, ARBITRARY_TX_TYPE, null, name, null, ConfirmationStatus.BOTH, limit, offset, true);
- // LOGGER.trace("Found {} arbitrary transactions at offset: {}, limit: {}", signatures.size(), offset, limit);
+ List signatures = processTransactionsForSignatures(limit, offset, allArbitraryTransactionsInDescendingOrder, processedTransactions);
+
if (signatures == null || signatures.isEmpty()) {
offset = 0;
break;
@@ -223,14 +248,38 @@ public class ArbitraryDataManager extends Thread {
ArbitraryTransactionData arbitraryTransactionData = (ArbitraryTransactionData) arbitraryTransaction.getTransactionData();
// 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();
+
+ EventBus.INSTANCE.notify(
+ new DataMonitorEvent(
+ System.currentTimeMillis(),
+ arbitraryTransactionData.getIdentifier(),
+ arbitraryTransactionData.getName(),
+ arbitraryTransactionData.getService().name(),
+ arbitraryDataExamination.getNotes(),
+ arbitraryTransactionData.getTimestamp(),
+ arbitraryTransactionData.getTimestamp()
+ )
+ );
continue;
}
// Remove transactions that we already have local data for
if (hasLocalData(arbitraryTransaction)) {
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
ArbitraryTransactionData arbitraryTransactionData = ArbitraryTransactionUtils.fetchTransactionData(repository, signature);
- boolean hasMoreRecentPutTransaction = ArbitraryTransactionUtils.hasMoreRecentPutTransaction(repository, arbitraryTransactionData);
- if (hasMoreRecentPutTransaction) {
+
+ Optional 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.
// 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
@@ -257,10 +319,34 @@ public class ArbitraryDataManager extends Thread {
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
// This process automatically then fetches the files themselves if a peer is found
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) {
LOGGER.error("Repository issue when fetching arbitrary transaction data", e);
}
@@ -274,6 +360,20 @@ public class ArbitraryDataManager extends Thread {
final int limit = 100;
int offset = 0;
+ List 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 processedTransactions = new HashSet<>();
+
while (!isStopping) {
final int minSeconds = 3;
final int maxSeconds = 10;
@@ -282,8 +382,8 @@ public class ArbitraryDataManager extends Thread {
// Any arbitrary transactions we want to fetch data for?
try (final Repository repository = RepositoryManager.getRepository()) {
- List signatures = repository.getTransactionRepository().getSignaturesMatchingCriteria(null, null, null, ARBITRARY_TX_TYPE, null, null, null, ConfirmationStatus.BOTH, limit, offset, true);
- // LOGGER.trace("Found {} arbitrary transactions at offset: {}, limit: {}", signatures.size(), offset, limit);
+ List signatures = processTransactionsForSignatures(limit, offset, allArbitraryTransactionsInDescendingOrder, processedTransactions);
+
if (signatures == null || signatures.isEmpty()) {
offset = 0;
break;
@@ -328,26 +428,74 @@ public class ArbitraryDataManager extends Thread {
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);
- 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
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) {
LOGGER.error("Repository issue when fetching arbitrary transaction data", e);
+ } catch (Exception e) {
+ LOGGER.error(e.getMessage(), e);
}
}
}
+ private static List processTransactionsForSignatures(
+ int limit,
+ int offset,
+ List transactionsInDescendingOrder,
+ Set processedTransactions) {
+ // these transactions are in descending order, latest transactions come first
+ List 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 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 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 signatures
+ = transactionsToProcess.stream()
+ .map(transactionToProcess -> transactionToProcess.getData()
+ .getSignature())
+ .collect(Collectors.toList());
+
+ return signatures;
+ }
+
private ArbitraryTransaction fetchTransaction(final Repository repository, byte[] signature) {
try {
TransactionData transactionData = repository.getTransactionRepository().fromSignature(signature);
diff --git a/src/main/java/org/qortal/controller/arbitrary/ArbitraryDataStorageManager.java b/src/main/java/org/qortal/controller/arbitrary/ArbitraryDataStorageManager.java
index 62feb8bd..c54a1e12 100644
--- a/src/main/java/org/qortal/controller/arbitrary/ArbitraryDataStorageManager.java
+++ b/src/main/java/org/qortal/controller/arbitrary/ArbitraryDataStorageManager.java
@@ -155,31 +155,24 @@ public class ArbitraryDataStorageManager extends Thread {
* @param arbitraryTransactionData - the transaction
* @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();
// Only fetch data associated with hashes, as we already have RAW_DATA
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
// Make sure to keep STORAGE_FULL_THRESHOLD considerably less than 1, to
// avoid a fetch/delete loop
if (!this.isStorageSpaceAvailable(STORAGE_FULL_THRESHOLD)) {
- return false;
- }
-
- // Don't fetch anything if we're (nearly) out of space for this name
- // Again, make sure to keep STORAGE_FULL_THRESHOLD considerably less than 1, to
- // avoid a fetch/delete loop
- if (!this.isStorageSpaceAvailableForName(repository, arbitraryTransactionData.getName(), STORAGE_FULL_THRESHOLD)) {
- return false;
+ return new ArbitraryDataExamination(false,"Don't fetch anything more if we're (nearly) out of space");
}
// Don't store data unless it's an allowed type (public/private)
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
@@ -189,21 +182,21 @@ public class ArbitraryDataStorageManager extends Thread {
// Never fetch data from blocked names, even if they are followed
if (ListUtils.isNameBlocked(name)) {
- return false;
+ return new ArbitraryDataExamination(false, "blocked name");
}
switch (Settings.getInstance().getStoragePolicy()) {
case FOLLOWED:
case FOLLOWED_OR_VIEWED:
- return ListUtils.isFollowingName(name);
+ return new ArbitraryDataExamination(ListUtils.isFollowingName(name), Settings.getInstance().getStoragePolicy().name());
case ALL:
- return true;
+ return new ArbitraryDataExamination(true, Settings.getInstance().getStoragePolicy().name());
case NONE:
case VIEWED:
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
*/
- private boolean shouldPreFetchDataWithoutName() {
+ private ArbitraryDataExamination shouldPreFetchDataWithoutName() {
switch (Settings.getInstance().getStoragePolicy()) {
case ALL:
- return true;
+ return new ArbitraryDataExamination(true, "Fetching all data");
case NONE:
case VIEWED:
case FOLLOWED:
case FOLLOWED_OR_VIEWED:
default:
- return false;
+ return new ArbitraryDataExamination(false, Settings.getInstance().getStoragePolicy().name());
}
}
@@ -484,51 +477,6 @@ public class ArbitraryDataStorageManager extends Thread {
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 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) {
int followedNamesCount = ListUtils.followedNamesCount();
if (followedNamesCount == 0) {
diff --git a/src/main/java/org/qortal/controller/arbitrary/ArbitraryTransactionDataHashWrapper.java b/src/main/java/org/qortal/controller/arbitrary/ArbitraryTransactionDataHashWrapper.java
new file mode 100644
index 00000000..9ff40771
--- /dev/null
+++ b/src/main/java/org/qortal/controller/arbitrary/ArbitraryTransactionDataHashWrapper.java
@@ -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);
+ }
+}
diff --git a/src/main/java/org/qortal/controller/arbitrary/RebuildArbitraryResourceCacheTask.java b/src/main/java/org/qortal/controller/arbitrary/RebuildArbitraryResourceCacheTask.java
new file mode 100644
index 00000000..d7472325
--- /dev/null
+++ b/src/main/java/org/qortal/controller/arbitrary/RebuildArbitraryResourceCacheTask.java
@@ -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);
+ }
+ }
+}
diff --git a/src/main/java/org/qortal/crosschain/Bitcoiny.java b/src/main/java/org/qortal/crosschain/Bitcoiny.java
index a4f5a2af..d93fa65f 100644
--- a/src/main/java/org/qortal/crosschain/Bitcoiny.java
+++ b/src/main/java/org/qortal/crosschain/Bitcoiny.java
@@ -83,6 +83,7 @@ public abstract class Bitcoiny implements ForeignBlockchain {
return this.bitcoinjContext;
}
+ @Override
public String getCurrencyCode() {
return this.currencyCode;
}
diff --git a/src/main/java/org/qortal/crosschain/ForeignBlockchain.java b/src/main/java/org/qortal/crosschain/ForeignBlockchain.java
index fe64ab83..c66f2719 100644
--- a/src/main/java/org/qortal/crosschain/ForeignBlockchain.java
+++ b/src/main/java/org/qortal/crosschain/ForeignBlockchain.java
@@ -2,6 +2,8 @@ package org.qortal.crosschain;
public interface ForeignBlockchain {
+ public String getCurrencyCode();
+
public boolean isValidAddress(String address);
public boolean isValidWalletKey(String walletKey);
diff --git a/src/main/java/org/qortal/data/arbitrary/ArbitraryDataIndex.java b/src/main/java/org/qortal/data/arbitrary/ArbitraryDataIndex.java
new file mode 100644
index 00000000..9b6bf415
--- /dev/null
+++ b/src/main/java/org/qortal/data/arbitrary/ArbitraryDataIndex.java
@@ -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 + '\'' +
+ '}';
+ }
+}
diff --git a/src/main/java/org/qortal/data/arbitrary/ArbitraryDataIndexDetail.java b/src/main/java/org/qortal/data/arbitrary/ArbitraryDataIndexDetail.java
new file mode 100644
index 00000000..d073c736
--- /dev/null
+++ b/src/main/java/org/qortal/data/arbitrary/ArbitraryDataIndexDetail.java
@@ -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 + '\'' +
+ '}';
+ }
+}
diff --git a/src/main/java/org/qortal/data/arbitrary/ArbitraryDataIndexScoreKey.java b/src/main/java/org/qortal/data/arbitrary/ArbitraryDataIndexScoreKey.java
new file mode 100644
index 00000000..46a661e5
--- /dev/null
+++ b/src/main/java/org/qortal/data/arbitrary/ArbitraryDataIndexScoreKey.java
@@ -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);
+ }
+
+
+}
diff --git a/src/main/java/org/qortal/data/arbitrary/ArbitraryDataIndexScorecard.java b/src/main/java/org/qortal/data/arbitrary/ArbitraryDataIndexScorecard.java
new file mode 100644
index 00000000..1888a4a5
--- /dev/null
+++ b/src/main/java/org/qortal/data/arbitrary/ArbitraryDataIndexScorecard.java
@@ -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 + '\'' +
+ '}';
+ }
+}
diff --git a/src/main/java/org/qortal/data/arbitrary/DataMonitorInfo.java b/src/main/java/org/qortal/data/arbitrary/DataMonitorInfo.java
new file mode 100644
index 00000000..5ee76c29
--- /dev/null
+++ b/src/main/java/org/qortal/data/arbitrary/DataMonitorInfo.java
@@ -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;
+ }
+}
diff --git a/src/main/java/org/qortal/data/arbitrary/IndexCache.java b/src/main/java/org/qortal/data/arbitrary/IndexCache.java
new file mode 100644
index 00000000..dd5c12ab
--- /dev/null
+++ b/src/main/java/org/qortal/data/arbitrary/IndexCache.java
@@ -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> indicesByTerm = new ConcurrentHashMap<>();
+ private ConcurrentHashMap> indicesByIssuer = new ConcurrentHashMap<>();
+
+ public static IndexCache getInstance() {
+ return SINGLETON;
+ }
+
+ public ConcurrentHashMap> getIndicesByTerm() {
+ return indicesByTerm;
+ }
+
+ public ConcurrentHashMap> getIndicesByIssuer() {
+ return indicesByIssuer;
+ }
+}
diff --git a/src/main/java/org/qortal/data/transaction/ArbitraryTransactionData.java b/src/main/java/org/qortal/data/transaction/ArbitraryTransactionData.java
index f3828de8..81cfaa68 100644
--- a/src/main/java/org/qortal/data/transaction/ArbitraryTransactionData.java
+++ b/src/main/java/org/qortal/data/transaction/ArbitraryTransactionData.java
@@ -200,4 +200,26 @@ public class ArbitraryTransactionData extends TransactionData {
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 +
+ '}';
+ }
}
diff --git a/src/main/java/org/qortal/event/DataMonitorEvent.java b/src/main/java/org/qortal/event/DataMonitorEvent.java
new file mode 100644
index 00000000..c62d9acf
--- /dev/null
+++ b/src/main/java/org/qortal/event/DataMonitorEvent.java
@@ -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;
+ }
+}
diff --git a/src/main/java/org/qortal/group/Group.java b/src/main/java/org/qortal/group/Group.java
index 765b86de..a6b4f3a6 100644
--- a/src/main/java/org/qortal/group/Group.java
+++ b/src/main/java/org/qortal/group/Group.java
@@ -674,8 +674,8 @@ public class Group {
public void uninvite(GroupInviteTransactionData groupInviteTransactionData) throws DataException {
String invitee = groupInviteTransactionData.getInvitee();
- // If member exists then they were added when invite matched join request
- if (this.memberExists(invitee)) {
+ // If member exists and the join request is present then they were added when invite matched join request
+ if (this.memberExists(invitee) && groupInviteTransactionData.getJoinReference() != null) {
// Rebuild join request using cached reference to transaction that created join request.
this.rebuildJoinRequest(invitee, groupInviteTransactionData.getJoinReference());
diff --git a/src/main/java/org/qortal/repository/ATRepository.java b/src/main/java/org/qortal/repository/ATRepository.java
index fe001137..2b653ab5 100644
--- a/src/main/java/org/qortal/repository/ATRepository.java
+++ b/src/main/java/org/qortal/repository/ATRepository.java
@@ -76,9 +76,9 @@ public interface ATRepository {
* Although expectedValue, if provided, is natively an unsigned long,
* the data segment comparison is done via unsigned hex string.
*/
- public List getMatchingFinalATStates(byte[] codeHash, String buyerAddress, String sellerAddress, Boolean isFinished,
- Integer dataByteOffset, Long expectedValue, Integer minimumFinalHeight,
- Integer limit, Integer offset, Boolean reverse) throws DataException;
+ public List getMatchingFinalATStates(byte[] codeHash, byte[] buyerPublicKey, byte[] sellerPublicKey, Boolean isFinished,
+ Integer dataByteOffset, Long expectedValue, Integer minimumFinalHeight,
+ Integer limit, Integer offset, Boolean reverse) throws DataException;
/**
* Returns final ATStateData for ATs matching codeHash (required)
diff --git a/src/main/java/org/qortal/repository/ArbitraryRepository.java b/src/main/java/org/qortal/repository/ArbitraryRepository.java
index 1c0e84e2..4770d29b 100644
--- a/src/main/java/org/qortal/repository/ArbitraryRepository.java
+++ b/src/main/java/org/qortal/repository/ArbitraryRepository.java
@@ -27,6 +27,10 @@ public interface ArbitraryRepository {
public List getArbitraryTransactions(String name, Service service, String identifier, long since) throws DataException;
+ List getLatestArbitraryTransactions() throws DataException;
+
+ List getLatestArbitraryTransactionsByName(String name) 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;
@@ -42,7 +46,7 @@ public interface ArbitraryRepository {
public List getArbitraryResources(Service service, String identifier, List names, boolean defaultResource, Boolean followedOnly, Boolean excludeBlocked, Boolean includeMetadata, Boolean includeStatus, Integer limit, Integer offset, Boolean reverse) throws DataException;
- public List searchArbitraryResources(Service service, String query, String identifier, List names, String title, String description, boolean prefixOnly, List 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 searchArbitraryResources(Service service, String query, String identifier, List names, String title, String description, List keywords, boolean prefixOnly, List 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 searchArbitraryResourcesSimple(
Service service,
diff --git a/src/main/java/org/qortal/repository/hsqldb/HSQLDBATRepository.java b/src/main/java/org/qortal/repository/hsqldb/HSQLDBATRepository.java
index 71a95428..6310ec02 100644
--- a/src/main/java/org/qortal/repository/hsqldb/HSQLDBATRepository.java
+++ b/src/main/java/org/qortal/repository/hsqldb/HSQLDBATRepository.java
@@ -5,6 +5,7 @@ import com.google.common.primitives.Longs;
import org.apache.logging.log4j.LogManager;
import org.apache.logging.log4j.Logger;
import org.qortal.controller.Controller;
+import org.qortal.crypto.Crypto;
import org.qortal.data.at.ATData;
import org.qortal.data.at.ATStateData;
import org.qortal.repository.ATRepository;
@@ -403,9 +404,9 @@ public class HSQLDBATRepository implements ATRepository {
}
@Override
- public List getMatchingFinalATStates(byte[] codeHash, String buyerAddress, String sellerAddress, Boolean isFinished,
- Integer dataByteOffset, Long expectedValue, Integer minimumFinalHeight,
- Integer limit, Integer offset, Boolean reverse) throws DataException {
+ public List getMatchingFinalATStates(byte[] codeHash, byte[] buyerPublicKey, byte[] sellerPublicKey, Boolean isFinished,
+ Integer dataByteOffset, Long expectedValue, Integer minimumFinalHeight,
+ Integer limit, Integer offset, Boolean reverse) throws DataException {
StringBuilder sql = new StringBuilder(1024);
List