mirror of
https://github.com/Qortal/qortal.git
synced 2025-07-19 10:51:23 +00:00
Merge pull request #256 from kennycud/master
Foreign Fees Manager, Multiple Names, QDN Oprtimaizations - Tested for minimum 1 week, most longer.
This commit is contained in:
commit
4f3b4e4a58
5
pom.xml
5
pom.xml
@ -795,6 +795,11 @@
|
|||||||
<groupId>org.glassfish.jaxb</groupId>
|
<groupId>org.glassfish.jaxb</groupId>
|
||||||
<artifactId>jaxb-runtime</artifactId>
|
<artifactId>jaxb-runtime</artifactId>
|
||||||
<version>${jaxb-runtime.version}</version>
|
<version>${jaxb-runtime.version}</version>
|
||||||
|
</dependency>
|
||||||
|
<dependency>
|
||||||
|
<groupId>org.apache.tika</groupId>
|
||||||
|
<artifactId>tika-core</artifactId>
|
||||||
|
<version>3.1.0</version>
|
||||||
</dependency>
|
</dependency>
|
||||||
</dependencies>
|
</dependencies>
|
||||||
</project>
|
</project>
|
||||||
|
@ -2,12 +2,14 @@ package org.qortal.account;
|
|||||||
|
|
||||||
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.block.BlockChain;
|
import org.qortal.block.BlockChain;
|
||||||
import org.qortal.controller.LiteNode;
|
import org.qortal.controller.LiteNode;
|
||||||
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.RewardShareData;
|
import org.qortal.data.account.RewardShareData;
|
||||||
import org.qortal.data.naming.NameData;
|
import org.qortal.data.naming.NameData;
|
||||||
|
import org.qortal.data.transaction.TransactionData;
|
||||||
import org.qortal.repository.DataException;
|
import org.qortal.repository.DataException;
|
||||||
import org.qortal.repository.GroupRepository;
|
import org.qortal.repository.GroupRepository;
|
||||||
import org.qortal.repository.NameRepository;
|
import org.qortal.repository.NameRepository;
|
||||||
@ -19,7 +21,11 @@ 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;
|
||||||
|
|
||||||
|
import java.util.Comparator;
|
||||||
|
import java.util.HashMap;
|
||||||
import java.util.List;
|
import java.util.List;
|
||||||
|
import java.util.Map;
|
||||||
|
import java.util.Optional;
|
||||||
|
|
||||||
import static org.qortal.utils.Amounts.prettyAmount;
|
import static org.qortal.utils.Amounts.prettyAmount;
|
||||||
|
|
||||||
@ -361,6 +367,142 @@ public class Account {
|
|||||||
return accountData.getLevel();
|
return accountData.getLevel();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get Primary Name
|
||||||
|
*
|
||||||
|
* @return the primary name for this address if present, otherwise empty
|
||||||
|
*
|
||||||
|
* @throws DataException
|
||||||
|
*/
|
||||||
|
public Optional<String> getPrimaryName() throws DataException {
|
||||||
|
|
||||||
|
return this.repository.getNameRepository().getPrimaryName(this.address);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Remove Primary Name
|
||||||
|
*
|
||||||
|
* @throws DataException
|
||||||
|
*/
|
||||||
|
public void removePrimaryName() throws DataException {
|
||||||
|
this.repository.getNameRepository().removePrimaryName(this.address);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Reset Primary Name
|
||||||
|
*
|
||||||
|
* Set primary name based on the names (and their history) this account owns.
|
||||||
|
*
|
||||||
|
* @param confirmationStatus the status of the transactions for the determining the primary name
|
||||||
|
*
|
||||||
|
* @return the primary name, empty if their isn't one
|
||||||
|
*
|
||||||
|
* @throws DataException
|
||||||
|
*/
|
||||||
|
public Optional<String> resetPrimaryName(TransactionsResource.ConfirmationStatus confirmationStatus) throws DataException {
|
||||||
|
Optional<String> primaryName = determinePrimaryName(confirmationStatus);
|
||||||
|
|
||||||
|
if(primaryName.isPresent()) {
|
||||||
|
return setPrimaryName(primaryName.get());
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
return primaryName;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Determine Primary Name
|
||||||
|
*
|
||||||
|
* Determine primary name based on a list of registered names.
|
||||||
|
*
|
||||||
|
* @param confirmationStatus the status of the transactions for this determination
|
||||||
|
*
|
||||||
|
* @return the primary name, empty if there is no primary name
|
||||||
|
*
|
||||||
|
* @throws DataException
|
||||||
|
*/
|
||||||
|
public Optional<String> determinePrimaryName(TransactionsResource.ConfirmationStatus confirmationStatus) throws DataException {
|
||||||
|
|
||||||
|
// all registered names for the owner
|
||||||
|
List<NameData> names = this.repository.getNameRepository().getNamesByOwner(this.address);
|
||||||
|
|
||||||
|
Optional<String> primaryName;
|
||||||
|
|
||||||
|
// if no registered names, the no primary name possible
|
||||||
|
if (names.isEmpty()) {
|
||||||
|
primaryName = Optional.empty();
|
||||||
|
}
|
||||||
|
// if names
|
||||||
|
else {
|
||||||
|
// if one name, then that is the primary name
|
||||||
|
if (names.size() == 1) {
|
||||||
|
primaryName = Optional.of( names.get(0).getName() );
|
||||||
|
}
|
||||||
|
// if more than one name, then seek the earliest name acquisition that was never released
|
||||||
|
else {
|
||||||
|
Map<String, TransactionData> txByName = new HashMap<>(names.size());
|
||||||
|
|
||||||
|
// for each name, get the latest transaction
|
||||||
|
for (NameData nameData : names) {
|
||||||
|
|
||||||
|
// since the name is currently registered to the owner,
|
||||||
|
// we assume the latest transaction involving this name was the transaction that the acquired
|
||||||
|
// name through registration, purchase or update
|
||||||
|
Optional<TransactionData> latestTransaction
|
||||||
|
= this.repository
|
||||||
|
.getTransactionRepository()
|
||||||
|
.getTransactionsInvolvingName(
|
||||||
|
nameData.getName(),
|
||||||
|
confirmationStatus
|
||||||
|
)
|
||||||
|
.stream()
|
||||||
|
.sorted(Comparator.comparing(
|
||||||
|
TransactionData::getTimestamp).reversed()
|
||||||
|
)
|
||||||
|
.findFirst(); // first is the last, since it was reversed
|
||||||
|
|
||||||
|
// if there is a latest transaction, expected for all registered names
|
||||||
|
if (latestTransaction.isPresent()) {
|
||||||
|
txByName.put(nameData.getName(), latestTransaction.get());
|
||||||
|
}
|
||||||
|
// if there is no latest transaction, then
|
||||||
|
else {
|
||||||
|
LOGGER.warn("No matching transaction for name: " + nameData.getName());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// get the first name aqcuistion for this address
|
||||||
|
Optional<Map.Entry<String, TransactionData>> firstNameEntry
|
||||||
|
= txByName.entrySet().stream().sorted(Comparator.comparing(entry -> entry.getValue().getTimestamp())).findFirst();
|
||||||
|
|
||||||
|
// if their is a name acquisition, then the first one is the primary name
|
||||||
|
if (firstNameEntry.isPresent()) {
|
||||||
|
primaryName = Optional.of( firstNameEntry.get().getKey() );
|
||||||
|
}
|
||||||
|
// if there is no nameacquistion, then there is no primary name
|
||||||
|
else {
|
||||||
|
primaryName = Optional.empty();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return primaryName;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Set Primary Name
|
||||||
|
*
|
||||||
|
* @param primaryName the primary to set to this address
|
||||||
|
*
|
||||||
|
* @return the primary name if successful, empty if unsuccessful
|
||||||
|
*
|
||||||
|
* @throws DataException
|
||||||
|
*/
|
||||||
|
public Optional<String> setPrimaryName( String primaryName ) throws DataException {
|
||||||
|
int changed = this.repository.getNameRepository().setPrimaryName(this.address, primaryName);
|
||||||
|
|
||||||
|
return changed > 0 ? Optional.of(primaryName) : Optional.empty();
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Returns reward-share minting address, or unknown if reward-share does not exist.
|
* Returns reward-share minting address, or unknown if reward-share does not exist.
|
||||||
*
|
*
|
||||||
|
@ -46,6 +46,7 @@ public class ApiService {
|
|||||||
private ApiService() {
|
private ApiService() {
|
||||||
this.config = new ResourceConfig();
|
this.config = new ResourceConfig();
|
||||||
this.config.packages("org.qortal.api.resource", "org.qortal.api.restricted.resource");
|
this.config.packages("org.qortal.api.resource", "org.qortal.api.restricted.resource");
|
||||||
|
this.config.register(org.glassfish.jersey.media.multipart.MultiPartFeature.class);
|
||||||
this.config.register(OpenApiResource.class);
|
this.config.register(OpenApiResource.class);
|
||||||
this.config.register(ApiDefinition.class);
|
this.config.register(ApiDefinition.class);
|
||||||
this.config.register(AnnotationPostProcessor.class);
|
this.config.register(AnnotationPostProcessor.class);
|
||||||
@ -197,6 +198,7 @@ public class ApiService {
|
|||||||
context.addServlet(DataMonitorSocket.class, "/websockets/datamonitor");
|
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(UnsignedFeesSocket.class, "/websockets/crosschain/unsignedfees");
|
||||||
context.addServlet(TradeOffersWebSocket.class, "/websockets/crosschain/tradeoffers");
|
context.addServlet(TradeOffersWebSocket.class, "/websockets/crosschain/tradeoffers");
|
||||||
context.addServlet(TradeBotWebSocket.class, "/websockets/crosschain/tradebot");
|
context.addServlet(TradeBotWebSocket.class, "/websockets/crosschain/tradebot");
|
||||||
context.addServlet(TradePresenceWebSocket.class, "/websockets/crosschain/tradepresence");
|
context.addServlet(TradePresenceWebSocket.class, "/websockets/crosschain/tradepresence");
|
||||||
|
@ -40,6 +40,7 @@ public class DevProxyService {
|
|||||||
private DevProxyService() {
|
private DevProxyService() {
|
||||||
this.config = new ResourceConfig();
|
this.config = new ResourceConfig();
|
||||||
this.config.packages("org.qortal.api.proxy.resource", "org.qortal.api.resource");
|
this.config.packages("org.qortal.api.proxy.resource", "org.qortal.api.resource");
|
||||||
|
this.config.register(org.glassfish.jersey.media.multipart.MultiPartFeature.class);
|
||||||
this.config.register(OpenApiResource.class);
|
this.config.register(OpenApiResource.class);
|
||||||
this.config.register(ApiDefinition.class);
|
this.config.register(ApiDefinition.class);
|
||||||
this.config.register(AnnotationPostProcessor.class);
|
this.config.register(AnnotationPostProcessor.class);
|
||||||
|
@ -39,6 +39,7 @@ public class DomainMapService {
|
|||||||
private DomainMapService() {
|
private DomainMapService() {
|
||||||
this.config = new ResourceConfig();
|
this.config = new ResourceConfig();
|
||||||
this.config.packages("org.qortal.api.resource", "org.qortal.api.domainmap.resource");
|
this.config.packages("org.qortal.api.resource", "org.qortal.api.domainmap.resource");
|
||||||
|
this.config.register(org.glassfish.jersey.media.multipart.MultiPartFeature.class);
|
||||||
this.config.register(OpenApiResource.class);
|
this.config.register(OpenApiResource.class);
|
||||||
this.config.register(ApiDefinition.class);
|
this.config.register(ApiDefinition.class);
|
||||||
this.config.register(AnnotationPostProcessor.class);
|
this.config.register(AnnotationPostProcessor.class);
|
||||||
|
@ -39,6 +39,7 @@ public class GatewayService {
|
|||||||
private GatewayService() {
|
private GatewayService() {
|
||||||
this.config = new ResourceConfig();
|
this.config = new ResourceConfig();
|
||||||
this.config.packages("org.qortal.api.resource", "org.qortal.api.gateway.resource");
|
this.config.packages("org.qortal.api.resource", "org.qortal.api.gateway.resource");
|
||||||
|
this.config.register(org.glassfish.jersey.media.multipart.MultiPartFeature.class);
|
||||||
this.config.register(OpenApiResource.class);
|
this.config.register(OpenApiResource.class);
|
||||||
this.config.register(ApiDefinition.class);
|
this.config.register(ApiDefinition.class);
|
||||||
this.config.register(AnnotationPostProcessor.class);
|
this.config.register(AnnotationPostProcessor.class);
|
||||||
|
@ -1,14 +1,13 @@
|
|||||||
package org.qortal.api;
|
package org.qortal.api;
|
||||||
|
|
||||||
|
|
||||||
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.jsoup.Jsoup;
|
import org.jsoup.Jsoup;
|
||||||
import org.jsoup.nodes.Document;
|
import org.jsoup.nodes.Document;
|
||||||
import org.jsoup.select.Elements;
|
import org.jsoup.select.Elements;
|
||||||
import org.qortal.arbitrary.misc.Service;
|
import org.qortal.arbitrary.misc.Service;
|
||||||
|
|
||||||
import java.util.Objects;
|
import java.util.Objects;
|
||||||
|
|
||||||
public class HTMLParser {
|
public class HTMLParser {
|
||||||
|
|
||||||
private static final Logger LOGGER = LogManager.getLogger(HTMLParser.class);
|
private static final Logger LOGGER = LogManager.getLogger(HTMLParser.class);
|
||||||
@ -22,10 +21,11 @@ public class HTMLParser {
|
|||||||
private String identifier;
|
private String identifier;
|
||||||
private String path;
|
private String path;
|
||||||
private String theme;
|
private String theme;
|
||||||
|
private String lang;
|
||||||
private boolean usingCustomRouting;
|
private boolean usingCustomRouting;
|
||||||
|
|
||||||
public HTMLParser(String resourceId, String inPath, String prefix, boolean includeResourceIdInPrefix, byte[] data,
|
public HTMLParser(String resourceId, String inPath, String prefix, boolean includeResourceIdInPrefix, byte[] data,
|
||||||
String qdnContext, Service service, String identifier, String theme, boolean usingCustomRouting) {
|
String qdnContext, Service service, String identifier, String theme, boolean usingCustomRouting, String lang) {
|
||||||
String inPathWithoutFilename = inPath.contains("/") ? inPath.substring(0, inPath.lastIndexOf('/')) : String.format("/%s",inPath);
|
String inPathWithoutFilename = inPath.contains("/") ? inPath.substring(0, inPath.lastIndexOf('/')) : String.format("/%s",inPath);
|
||||||
this.qdnBase = includeResourceIdInPrefix ? String.format("%s/%s", prefix, resourceId) : prefix;
|
this.qdnBase = includeResourceIdInPrefix ? String.format("%s/%s", prefix, resourceId) : prefix;
|
||||||
this.qdnBaseWithPath = includeResourceIdInPrefix ? String.format("%s/%s%s", prefix, resourceId, inPathWithoutFilename) : String.format("%s%s", prefix, inPathWithoutFilename);
|
this.qdnBaseWithPath = includeResourceIdInPrefix ? String.format("%s/%s%s", prefix, resourceId, inPathWithoutFilename) : String.format("%s%s", prefix, inPathWithoutFilename);
|
||||||
@ -36,6 +36,7 @@ public class HTMLParser {
|
|||||||
this.identifier = identifier;
|
this.identifier = identifier;
|
||||||
this.path = inPath;
|
this.path = inPath;
|
||||||
this.theme = theme;
|
this.theme = theme;
|
||||||
|
this.lang = lang;
|
||||||
this.usingCustomRouting = usingCustomRouting;
|
this.usingCustomRouting = usingCustomRouting;
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -61,9 +62,13 @@ public class HTMLParser {
|
|||||||
String identifier = this.identifier != null ? this.identifier.replace("\\", "").replace("\"","\\\"") : "";
|
String identifier = this.identifier != null ? this.identifier.replace("\\", "").replace("\"","\\\"") : "";
|
||||||
String path = this.path != null ? this.path.replace("\\", "").replace("\"","\\\"") : "";
|
String path = this.path != null ? this.path.replace("\\", "").replace("\"","\\\"") : "";
|
||||||
String theme = this.theme != null ? this.theme.replace("\\", "").replace("\"","\\\"") : "";
|
String theme = this.theme != null ? this.theme.replace("\\", "").replace("\"","\\\"") : "";
|
||||||
|
String lang = this.lang != null ? this.lang.replace("\\", "").replace("\"", "\\\"") : "";
|
||||||
String qdnBase = this.qdnBase != null ? this.qdnBase.replace("\\", "").replace("\"","\\\"") : "";
|
String qdnBase = this.qdnBase != null ? this.qdnBase.replace("\\", "").replace("\"","\\\"") : "";
|
||||||
String qdnBaseWithPath = this.qdnBaseWithPath != null ? this.qdnBaseWithPath.replace("\\", "").replace("\"","\\\"") : "";
|
String qdnBaseWithPath = this.qdnBaseWithPath != null ? this.qdnBaseWithPath.replace("\\", "").replace("\"","\\\"") : "";
|
||||||
String qdnContextVar = String.format("<script>var _qdnContext=\"%s\"; var _qdnTheme=\"%s\"; var _qdnService=\"%s\"; var _qdnName=\"%s\"; var _qdnIdentifier=\"%s\"; var _qdnPath=\"%s\"; var _qdnBase=\"%s\"; var _qdnBaseWithPath=\"%s\";</script>", qdnContext, theme, service, name, identifier, path, qdnBase, qdnBaseWithPath);
|
String qdnContextVar = String.format(
|
||||||
|
"<script>var _qdnContext=\"%s\"; var _qdnTheme=\"%s\"; var _qdnLang=\"%s\"; var _qdnService=\"%s\"; var _qdnName=\"%s\"; var _qdnIdentifier=\"%s\"; var _qdnPath=\"%s\"; var _qdnBase=\"%s\"; var _qdnBaseWithPath=\"%s\";</script>",
|
||||||
|
qdnContext, theme, lang, service, name, identifier, path, qdnBase, qdnBaseWithPath
|
||||||
|
);
|
||||||
head.get(0).prepend(qdnContextVar);
|
head.get(0).prepend(qdnContextVar);
|
||||||
|
|
||||||
// Add base href tag
|
// Add base href tag
|
||||||
|
@ -304,11 +304,11 @@ public class BitcoinyTBDRequest {
|
|||||||
private String networkName;
|
private String networkName;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Fee Ceiling
|
* Fee Required
|
||||||
*
|
*
|
||||||
* web search, LTC fee ceiling = 1000L
|
* web search, LTC fee required = 1000L
|
||||||
*/
|
*/
|
||||||
private long feeCeiling;
|
private long feeRequired;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Extended Public Key
|
* Extended Public Key
|
||||||
@ -570,8 +570,8 @@ public class BitcoinyTBDRequest {
|
|||||||
return this.networkName;
|
return this.networkName;
|
||||||
}
|
}
|
||||||
|
|
||||||
public long getFeeCeiling() {
|
public long getFeeRequired() {
|
||||||
return this.feeCeiling;
|
return this.feeRequired;
|
||||||
}
|
}
|
||||||
|
|
||||||
public String getExtendedPublicKey() {
|
public String getExtendedPublicKey() {
|
||||||
@ -671,7 +671,7 @@ public class BitcoinyTBDRequest {
|
|||||||
", minimumOrderAmount=" + minimumOrderAmount +
|
", minimumOrderAmount=" + minimumOrderAmount +
|
||||||
", feePerKb=" + feePerKb +
|
", feePerKb=" + feePerKb +
|
||||||
", networkName='" + networkName + '\'' +
|
", networkName='" + networkName + '\'' +
|
||||||
", feeCeiling=" + feeCeiling +
|
", feeRequired=" + feeRequired +
|
||||||
", extendedPublicKey='" + extendedPublicKey + '\'' +
|
", extendedPublicKey='" + extendedPublicKey + '\'' +
|
||||||
", sendAmount=" + sendAmount +
|
", sendAmount=" + sendAmount +
|
||||||
", sendingFeePerByte=" + sendingFeePerByte +
|
", sendingFeePerByte=" + sendingFeePerByte +
|
||||||
|
@ -142,10 +142,20 @@ public class DevProxyServerResource {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
String lang = request.getParameter("lang");
|
||||||
|
if (lang == null || lang.isBlank()) {
|
||||||
|
lang = "en"; // fallback
|
||||||
|
}
|
||||||
|
|
||||||
|
String theme = request.getParameter("theme");
|
||||||
|
if (theme == null || theme.isBlank()) {
|
||||||
|
theme = "light";
|
||||||
|
}
|
||||||
|
|
||||||
// Parse and modify output if needed
|
// Parse and modify output if needed
|
||||||
if (HTMLParser.isHtmlFile(filename)) {
|
if (HTMLParser.isHtmlFile(filename)) {
|
||||||
// HTML file - needs to be parsed
|
// HTML file - needs to be parsed
|
||||||
HTMLParser htmlParser = new HTMLParser("", inPath, "", false, data, "proxy", Service.APP, null, "light", true);
|
HTMLParser htmlParser = new HTMLParser("", inPath, "", false, data, "proxy", Service.APP, null, theme , true, lang);
|
||||||
htmlParser.addAdditionalHeaderTags();
|
htmlParser.addAdditionalHeaderTags();
|
||||||
response.addHeader("Content-Security-Policy", "default-src 'self' 'unsafe-inline' 'unsafe-eval'; media-src 'self' data: blob:; img-src 'self' data: blob:; connect-src 'self' ws:; font-src 'self' data:;");
|
response.addHeader("Content-Security-Policy", "default-src 'self' 'unsafe-inline' 'unsafe-eval'; media-src 'self' data: blob:; img-src 'self' data: blob:; connect-src 'self' ws:; font-src 'self' data:;");
|
||||||
response.setContentType(con.getContentType());
|
response.setContentType(con.getContentType());
|
||||||
|
@ -3,6 +3,7 @@ package org.qortal.api.resource;
|
|||||||
import com.google.common.primitives.Bytes;
|
import com.google.common.primitives.Bytes;
|
||||||
import com.j256.simplemagic.ContentInfo;
|
import com.j256.simplemagic.ContentInfo;
|
||||||
import com.j256.simplemagic.ContentInfoUtil;
|
import com.j256.simplemagic.ContentInfoUtil;
|
||||||
|
|
||||||
import io.swagger.v3.oas.annotations.Operation;
|
import io.swagger.v3.oas.annotations.Operation;
|
||||||
import io.swagger.v3.oas.annotations.Parameter;
|
import io.swagger.v3.oas.annotations.Parameter;
|
||||||
import io.swagger.v3.oas.annotations.media.ArraySchema;
|
import io.swagger.v3.oas.annotations.media.ArraySchema;
|
||||||
@ -12,6 +13,7 @@ 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.apache.commons.io.FileUtils;
|
import org.apache.commons.io.FileUtils;
|
||||||
import org.apache.commons.lang3.ArrayUtils;
|
import org.apache.commons.lang3.ArrayUtils;
|
||||||
import org.apache.logging.log4j.LogManager;
|
import org.apache.logging.log4j.LogManager;
|
||||||
@ -63,14 +65,19 @@ 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.MediaType;
|
import javax.ws.rs.core.MediaType;
|
||||||
|
|
||||||
import java.io.BufferedWriter;
|
import java.io.BufferedWriter;
|
||||||
import java.io.File;
|
import java.io.File;
|
||||||
import java.io.FileWriter;
|
import java.io.FileWriter;
|
||||||
import java.io.IOException;
|
import java.io.IOException;
|
||||||
|
import java.io.InputStream;
|
||||||
|
import java.io.OutputStream;
|
||||||
import java.net.FileNameMap;
|
import java.net.FileNameMap;
|
||||||
import java.net.URLConnection;
|
import java.net.URLConnection;
|
||||||
import java.nio.file.Files;
|
import java.nio.file.Files;
|
||||||
import java.nio.file.Paths;
|
import java.nio.file.Paths;
|
||||||
|
import java.nio.file.StandardCopyOption;
|
||||||
|
import java.nio.file.StandardOpenOption;
|
||||||
import java.util.ArrayList;
|
import java.util.ArrayList;
|
||||||
import java.util.Arrays;
|
import java.util.Arrays;
|
||||||
import java.util.Comparator;
|
import java.util.Comparator;
|
||||||
@ -78,6 +85,16 @@ import java.util.List;
|
|||||||
import java.util.Map;
|
import java.util.Map;
|
||||||
import java.util.Objects;
|
import java.util.Objects;
|
||||||
import java.util.stream.Collectors;
|
import java.util.stream.Collectors;
|
||||||
|
import java.util.zip.GZIPOutputStream;
|
||||||
|
|
||||||
|
import org.apache.tika.Tika;
|
||||||
|
import org.apache.tika.mime.MimeTypeException;
|
||||||
|
import org.apache.tika.mime.MimeTypes;
|
||||||
|
|
||||||
|
import javax.ws.rs.core.Response;
|
||||||
|
|
||||||
|
import org.glassfish.jersey.media.multipart.FormDataParam;
|
||||||
|
import static org.qortal.api.ApiError.REPOSITORY_ISSUE;
|
||||||
|
|
||||||
@Path("/arbitrary")
|
@Path("/arbitrary")
|
||||||
@Tag(name = "Arbitrary")
|
@Tag(name = "Arbitrary")
|
||||||
@ -686,20 +703,20 @@ public class ArbitraryResource {
|
|||||||
)
|
)
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
public HttpServletResponse get(@PathParam("service") Service service,
|
public void get(@PathParam("service") Service service,
|
||||||
@PathParam("name") String name,
|
@PathParam("name") String name,
|
||||||
@QueryParam("filepath") String filepath,
|
@QueryParam("filepath") String filepath,
|
||||||
@QueryParam("encoding") String encoding,
|
@QueryParam("encoding") String encoding,
|
||||||
@QueryParam("rebuild") boolean rebuild,
|
@QueryParam("rebuild") boolean rebuild,
|
||||||
@QueryParam("async") boolean async,
|
@QueryParam("async") boolean async,
|
||||||
@QueryParam("attempts") Integer attempts) {
|
@QueryParam("attempts") Integer attempts, @QueryParam("attachment") boolean attachment, @QueryParam("attachmentFilename") String attachmentFilename) {
|
||||||
|
|
||||||
// Authentication can be bypassed in the settings, for those running public QDN nodes
|
// Authentication can be bypassed in the settings, for those running public QDN nodes
|
||||||
if (!Settings.getInstance().isQDNAuthBypassEnabled()) {
|
if (!Settings.getInstance().isQDNAuthBypassEnabled()) {
|
||||||
Security.checkApiCallAllowed(request);
|
Security.checkApiCallAllowed(request);
|
||||||
}
|
}
|
||||||
|
|
||||||
return this.download(service, name, null, filepath, encoding, rebuild, async, attempts);
|
this.download(service, name, null, filepath, encoding, rebuild, async, attempts, attachment, attachmentFilename);
|
||||||
}
|
}
|
||||||
|
|
||||||
@GET
|
@GET
|
||||||
@ -719,21 +736,21 @@ public class ArbitraryResource {
|
|||||||
)
|
)
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
public HttpServletResponse get(@PathParam("service") Service service,
|
public void get(@PathParam("service") Service service,
|
||||||
@PathParam("name") String name,
|
@PathParam("name") String name,
|
||||||
@PathParam("identifier") String identifier,
|
@PathParam("identifier") String identifier,
|
||||||
@QueryParam("filepath") String filepath,
|
@QueryParam("filepath") String filepath,
|
||||||
@QueryParam("encoding") String encoding,
|
@QueryParam("encoding") String encoding,
|
||||||
@QueryParam("rebuild") boolean rebuild,
|
@QueryParam("rebuild") boolean rebuild,
|
||||||
@QueryParam("async") boolean async,
|
@QueryParam("async") boolean async,
|
||||||
@QueryParam("attempts") Integer attempts) {
|
@QueryParam("attempts") Integer attempts, @QueryParam("attachment") boolean attachment, @QueryParam("attachmentFilename") String attachmentFilename) {
|
||||||
|
|
||||||
// Authentication can be bypassed in the settings, for those running public QDN nodes
|
// Authentication can be bypassed in the settings, for those running public QDN nodes
|
||||||
if (!Settings.getInstance().isQDNAuthBypassEnabled()) {
|
if (!Settings.getInstance().isQDNAuthBypassEnabled()) {
|
||||||
Security.checkApiCallAllowed(request, null);
|
Security.checkApiCallAllowed(request, null);
|
||||||
}
|
}
|
||||||
|
|
||||||
return this.download(service, name, identifier, filepath, encoding, rebuild, async, attempts);
|
this.download(service, name, identifier, filepath, encoding, rebuild, async, attempts, attachment, attachmentFilename);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
@ -878,6 +895,464 @@ public class ArbitraryResource {
|
|||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
@GET
|
||||||
|
@Path("/check/tmp")
|
||||||
|
@Produces(MediaType.TEXT_PLAIN)
|
||||||
|
@Operation(
|
||||||
|
summary = "Check if the disk has enough disk space for an upcoming upload",
|
||||||
|
responses = {
|
||||||
|
@ApiResponse(description = "OK if sufficient space", responseCode = "200"),
|
||||||
|
@ApiResponse(description = "Insufficient space", responseCode = "507") // 507 = Insufficient Storage
|
||||||
|
}
|
||||||
|
)
|
||||||
|
@SecurityRequirement(name = "apiKey")
|
||||||
|
public Response checkUploadSpace(@HeaderParam(Security.API_KEY_HEADER) String apiKey,
|
||||||
|
@QueryParam("totalSize") Long totalSize) {
|
||||||
|
Security.checkApiCallAllowed(request);
|
||||||
|
|
||||||
|
if (totalSize == null || totalSize <= 0) {
|
||||||
|
return Response.status(Response.Status.BAD_REQUEST)
|
||||||
|
.entity("Missing or invalid totalSize parameter").build();
|
||||||
|
}
|
||||||
|
|
||||||
|
File uploadDir = new File("uploads-temp");
|
||||||
|
if (!uploadDir.exists()) {
|
||||||
|
uploadDir.mkdirs(); // ensure the folder exists
|
||||||
|
}
|
||||||
|
|
||||||
|
long usableSpace = uploadDir.getUsableSpace();
|
||||||
|
long requiredSpace = (long)(((double)totalSize) * 2.2); // estimate for chunks + merge
|
||||||
|
|
||||||
|
if (usableSpace < requiredSpace) {
|
||||||
|
return Response.status(507).entity("Insufficient disk space").build();
|
||||||
|
}
|
||||||
|
|
||||||
|
return Response.ok("Sufficient disk space").build();
|
||||||
|
}
|
||||||
|
|
||||||
|
@POST
|
||||||
|
@Path("/{service}/{name}/chunk")
|
||||||
|
@Consumes(MediaType.MULTIPART_FORM_DATA)
|
||||||
|
@Operation(
|
||||||
|
summary = "Upload a single file chunk to be later assembled into a complete arbitrary resource (no identifier)",
|
||||||
|
requestBody = @RequestBody(
|
||||||
|
required = true,
|
||||||
|
content = @Content(
|
||||||
|
mediaType = MediaType.MULTIPART_FORM_DATA,
|
||||||
|
schema = @Schema(
|
||||||
|
implementation = Object.class
|
||||||
|
)
|
||||||
|
)
|
||||||
|
),
|
||||||
|
responses = {
|
||||||
|
@ApiResponse(
|
||||||
|
description = "Chunk uploaded successfully",
|
||||||
|
responseCode = "200"
|
||||||
|
),
|
||||||
|
@ApiResponse(
|
||||||
|
description = "Error writing chunk",
|
||||||
|
responseCode = "500"
|
||||||
|
)
|
||||||
|
}
|
||||||
|
)
|
||||||
|
@SecurityRequirement(name = "apiKey")
|
||||||
|
public Response uploadChunkNoIdentifier(@HeaderParam(Security.API_KEY_HEADER) String apiKey,
|
||||||
|
@PathParam("service") String serviceString,
|
||||||
|
@PathParam("name") String name,
|
||||||
|
@FormDataParam("chunk") InputStream chunkStream,
|
||||||
|
@FormDataParam("index") int index) {
|
||||||
|
Security.checkApiCallAllowed(request);
|
||||||
|
|
||||||
|
try {
|
||||||
|
String safeService = Paths.get(serviceString).getFileName().toString();
|
||||||
|
String safeName = Paths.get(name).getFileName().toString();
|
||||||
|
|
||||||
|
java.nio.file.Path tempDir = Paths.get("uploads-temp", safeService, safeName);
|
||||||
|
Files.createDirectories(tempDir);
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
java.nio.file.Path chunkFile = tempDir.resolve("chunk_" + index);
|
||||||
|
Files.copy(chunkStream, chunkFile, StandardCopyOption.REPLACE_EXISTING);
|
||||||
|
|
||||||
|
return Response.ok("Chunk " + index + " received").build();
|
||||||
|
} catch (IOException e) {
|
||||||
|
LOGGER.error("Failed to write chunk {} for service '{}' and name '{}'", index, serviceString, name, e);
|
||||||
|
return Response.serverError().entity("Failed to write chunk: " + e.getMessage()).build();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@POST
|
||||||
|
@Path("/{service}/{name}/finalize")
|
||||||
|
@Produces(MediaType.TEXT_PLAIN)
|
||||||
|
@Operation(
|
||||||
|
summary = "Finalize a chunked upload (no identifier) and build a raw, unsigned, ARBITRARY transaction",
|
||||||
|
responses = {
|
||||||
|
@ApiResponse(
|
||||||
|
description = "raw, unsigned, ARBITRARY transaction encoded in Base58",
|
||||||
|
content = @Content(mediaType = MediaType.TEXT_PLAIN)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
)
|
||||||
|
@SecurityRequirement(name = "apiKey")
|
||||||
|
public String finalizeUploadNoIdentifier(
|
||||||
|
@HeaderParam(Security.API_KEY_HEADER) String apiKey,
|
||||||
|
@PathParam("service") String serviceString,
|
||||||
|
@PathParam("name") String name,
|
||||||
|
@QueryParam("title") String title,
|
||||||
|
@QueryParam("description") String description,
|
||||||
|
@QueryParam("tags") List<String> tags,
|
||||||
|
@QueryParam("category") Category category,
|
||||||
|
@QueryParam("filename") String filename,
|
||||||
|
@QueryParam("fee") Long fee,
|
||||||
|
@QueryParam("preview") Boolean preview,
|
||||||
|
@QueryParam("isZip") Boolean isZip
|
||||||
|
) {
|
||||||
|
Security.checkApiCallAllowed(request);
|
||||||
|
java.nio.file.Path tempFile = null;
|
||||||
|
java.nio.file.Path tempDir = null;
|
||||||
|
java.nio.file.Path chunkDir = null;
|
||||||
|
String safeService = Paths.get(serviceString).getFileName().toString();
|
||||||
|
String safeName = Paths.get(name).getFileName().toString();
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
try {
|
||||||
|
chunkDir = Paths.get("uploads-temp", safeService, safeName);
|
||||||
|
|
||||||
|
if (!Files.exists(chunkDir) || !Files.isDirectory(chunkDir)) {
|
||||||
|
throw ApiExceptionFactory.INSTANCE.createCustomException(request, ApiError.INVALID_CRITERIA, "No chunks found for upload");
|
||||||
|
}
|
||||||
|
|
||||||
|
String safeFilename = (filename == null || filename.isBlank()) ? "qortal-" + NTP.getTime() : filename;
|
||||||
|
tempDir = Files.createTempDirectory("qortal-");
|
||||||
|
String sanitizedFilename = Paths.get(safeFilename).getFileName().toString();
|
||||||
|
tempFile = tempDir.resolve(sanitizedFilename);
|
||||||
|
|
||||||
|
try (OutputStream out = Files.newOutputStream(tempFile, StandardOpenOption.CREATE, StandardOpenOption.TRUNCATE_EXISTING)) {
|
||||||
|
byte[] buffer = new byte[65536];
|
||||||
|
for (java.nio.file.Path chunk : Files.list(chunkDir)
|
||||||
|
.filter(path -> path.getFileName().toString().startsWith("chunk_"))
|
||||||
|
.sorted(Comparator.comparingInt(path -> {
|
||||||
|
String name2 = path.getFileName().toString();
|
||||||
|
String numberPart = name2.substring("chunk_".length());
|
||||||
|
return Integer.parseInt(numberPart);
|
||||||
|
})).collect(Collectors.toList())) {
|
||||||
|
try (InputStream in = Files.newInputStream(chunk)) {
|
||||||
|
int bytesRead;
|
||||||
|
while ((bytesRead = in.read(buffer)) != -1) {
|
||||||
|
out.write(buffer, 0, bytesRead);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
String detectedExtension = "";
|
||||||
|
String uploadFilename = null;
|
||||||
|
boolean extensionIsValid = false;
|
||||||
|
|
||||||
|
if (filename != null && !filename.isBlank()) {
|
||||||
|
int lastDot = filename.lastIndexOf('.');
|
||||||
|
if (lastDot > 0 && lastDot < filename.length() - 1) {
|
||||||
|
extensionIsValid = true;
|
||||||
|
uploadFilename = filename;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!extensionIsValid) {
|
||||||
|
Tika tika = new Tika();
|
||||||
|
String mimeType = tika.detect(tempFile.toFile());
|
||||||
|
try {
|
||||||
|
MimeTypes allTypes = MimeTypes.getDefaultMimeTypes();
|
||||||
|
org.apache.tika.mime.MimeType mime = allTypes.forName(mimeType);
|
||||||
|
detectedExtension = mime.getExtension();
|
||||||
|
} catch (MimeTypeException e) {
|
||||||
|
LOGGER.warn("Could not determine file extension for MIME type: {}", mimeType, e);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (filename != null && !filename.isBlank()) {
|
||||||
|
int lastDot = filename.lastIndexOf('.');
|
||||||
|
String baseName = (lastDot > 0) ? filename.substring(0, lastDot) : filename;
|
||||||
|
uploadFilename = baseName + (detectedExtension != null ? detectedExtension : "");
|
||||||
|
} else {
|
||||||
|
uploadFilename = "qortal-" + NTP.getTime() + (detectedExtension != null ? detectedExtension : "");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Boolean isZipBoolean = false;
|
||||||
|
|
||||||
|
if (isZip != null && isZip) {
|
||||||
|
isZipBoolean = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
// ✅ Call upload with `null` as identifier
|
||||||
|
return this.upload(
|
||||||
|
Service.valueOf(serviceString),
|
||||||
|
name,
|
||||||
|
null, // no identifier
|
||||||
|
tempFile.toString(),
|
||||||
|
null,
|
||||||
|
null,
|
||||||
|
isZipBoolean,
|
||||||
|
fee,
|
||||||
|
uploadFilename,
|
||||||
|
title,
|
||||||
|
description,
|
||||||
|
tags,
|
||||||
|
category,
|
||||||
|
preview
|
||||||
|
);
|
||||||
|
|
||||||
|
} catch (IOException e) {
|
||||||
|
LOGGER.error("Failed to merge chunks for service='{}', name='{}'", serviceString, name, e);
|
||||||
|
|
||||||
|
throw ApiExceptionFactory.INSTANCE.createCustomException(request, ApiError.REPOSITORY_ISSUE, "Failed to merge chunks: " + e.getMessage());
|
||||||
|
} finally {
|
||||||
|
if (tempDir != null) {
|
||||||
|
try {
|
||||||
|
Files.walk(tempDir)
|
||||||
|
.sorted(Comparator.reverseOrder())
|
||||||
|
.map(java.nio.file.Path::toFile)
|
||||||
|
.forEach(File::delete);
|
||||||
|
} catch (IOException e) {
|
||||||
|
LOGGER.warn("Failed to delete temp directory: {}", tempDir, e);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
Files.walk(chunkDir)
|
||||||
|
.sorted(Comparator.reverseOrder())
|
||||||
|
.map(java.nio.file.Path::toFile)
|
||||||
|
.forEach(File::delete);
|
||||||
|
} catch (IOException e) {
|
||||||
|
LOGGER.warn("Failed to delete chunk directory: {}", chunkDir, e);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
@POST
|
||||||
|
@Path("/{service}/{name}/{identifier}/chunk")
|
||||||
|
@Consumes(MediaType.MULTIPART_FORM_DATA)
|
||||||
|
@Operation(
|
||||||
|
summary = "Upload a single file chunk to be later assembled into a complete arbitrary resource",
|
||||||
|
requestBody = @RequestBody(
|
||||||
|
required = true,
|
||||||
|
content = @Content(
|
||||||
|
mediaType = MediaType.MULTIPART_FORM_DATA,
|
||||||
|
schema = @Schema(
|
||||||
|
implementation = Object.class
|
||||||
|
)
|
||||||
|
)
|
||||||
|
),
|
||||||
|
responses = {
|
||||||
|
@ApiResponse(
|
||||||
|
description = "Chunk uploaded successfully",
|
||||||
|
responseCode = "200"
|
||||||
|
),
|
||||||
|
@ApiResponse(
|
||||||
|
description = "Error writing chunk",
|
||||||
|
responseCode = "500"
|
||||||
|
)
|
||||||
|
}
|
||||||
|
)
|
||||||
|
@SecurityRequirement(name = "apiKey")
|
||||||
|
public Response uploadChunk(@HeaderParam(Security.API_KEY_HEADER) String apiKey,
|
||||||
|
@PathParam("service") String serviceString,
|
||||||
|
@PathParam("name") String name,
|
||||||
|
@PathParam("identifier") String identifier,
|
||||||
|
@FormDataParam("chunk") InputStream chunkStream,
|
||||||
|
@FormDataParam("index") int index) {
|
||||||
|
Security.checkApiCallAllowed(request);
|
||||||
|
|
||||||
|
try {
|
||||||
|
String safeService = Paths.get(serviceString).getFileName().toString();
|
||||||
|
String safeName = Paths.get(name).getFileName().toString();
|
||||||
|
String safeIdentifier = Paths.get(identifier).getFileName().toString();
|
||||||
|
|
||||||
|
java.nio.file.Path tempDir = Paths.get("uploads-temp", safeService, safeName, safeIdentifier);
|
||||||
|
|
||||||
|
Files.createDirectories(tempDir);
|
||||||
|
|
||||||
|
java.nio.file.Path chunkFile = tempDir.resolve("chunk_" + index);
|
||||||
|
Files.copy(chunkStream, chunkFile, StandardCopyOption.REPLACE_EXISTING);
|
||||||
|
|
||||||
|
return Response.ok("Chunk " + index + " received").build();
|
||||||
|
} catch (IOException e) {
|
||||||
|
LOGGER.error("Failed to write chunk {} for service='{}', name='{}', identifier='{}'", index, serviceString, name, identifier, e);
|
||||||
|
return Response.serverError().entity("Failed to write chunk: " + e.getMessage()).build();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@POST
|
||||||
|
@Path("/{service}/{name}/{identifier}/finalize")
|
||||||
|
@Produces(MediaType.TEXT_PLAIN)
|
||||||
|
@Operation(
|
||||||
|
summary = "Finalize a chunked upload and build a raw, unsigned, ARBITRARY transaction",
|
||||||
|
responses = {
|
||||||
|
@ApiResponse(
|
||||||
|
description = "raw, unsigned, ARBITRARY transaction encoded in Base58",
|
||||||
|
content = @Content(mediaType = MediaType.TEXT_PLAIN)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
)
|
||||||
|
@SecurityRequirement(name = "apiKey")
|
||||||
|
public String finalizeUpload(
|
||||||
|
@HeaderParam(Security.API_KEY_HEADER) String apiKey,
|
||||||
|
@PathParam("service") String serviceString,
|
||||||
|
@PathParam("name") String name,
|
||||||
|
@PathParam("identifier") String identifier,
|
||||||
|
@QueryParam("title") String title,
|
||||||
|
@QueryParam("description") String description,
|
||||||
|
@QueryParam("tags") List<String> tags,
|
||||||
|
@QueryParam("category") Category category,
|
||||||
|
@QueryParam("filename") String filename,
|
||||||
|
@QueryParam("fee") Long fee,
|
||||||
|
@QueryParam("preview") Boolean preview,
|
||||||
|
@QueryParam("isZip") Boolean isZip
|
||||||
|
) {
|
||||||
|
Security.checkApiCallAllowed(request);
|
||||||
|
java.nio.file.Path tempFile = null;
|
||||||
|
java.nio.file.Path tempDir = null;
|
||||||
|
java.nio.file.Path chunkDir = null;
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
try {
|
||||||
|
String safeService = Paths.get(serviceString).getFileName().toString();
|
||||||
|
String safeName = Paths.get(name).getFileName().toString();
|
||||||
|
String safeIdentifier = Paths.get(identifier).getFileName().toString();
|
||||||
|
java.nio.file.Path baseUploadsDir = Paths.get("uploads-temp"); // relative to Qortal working dir
|
||||||
|
chunkDir = baseUploadsDir.resolve(safeService).resolve(safeName).resolve(safeIdentifier);
|
||||||
|
|
||||||
|
if (!Files.exists(chunkDir) || !Files.isDirectory(chunkDir)) {
|
||||||
|
throw ApiExceptionFactory.INSTANCE.createCustomException(request, ApiError.INVALID_CRITERIA, "No chunks found for upload");
|
||||||
|
}
|
||||||
|
|
||||||
|
// Step 1: Determine a safe filename for disk temp file (regardless of extension correctness)
|
||||||
|
String safeFilename = filename;
|
||||||
|
if (filename == null || filename.isBlank()) {
|
||||||
|
safeFilename = "qortal-" + NTP.getTime();
|
||||||
|
}
|
||||||
|
|
||||||
|
tempDir = Files.createTempDirectory("qortal-");
|
||||||
|
String sanitizedFilename = Paths.get(safeFilename).getFileName().toString();
|
||||||
|
tempFile = tempDir.resolve(sanitizedFilename);
|
||||||
|
|
||||||
|
|
||||||
|
// Step 2: Merge chunks
|
||||||
|
|
||||||
|
try (OutputStream out = Files.newOutputStream(tempFile, StandardOpenOption.CREATE, StandardOpenOption.TRUNCATE_EXISTING)) {
|
||||||
|
byte[] buffer = new byte[65536];
|
||||||
|
for (java.nio.file.Path chunk : Files.list(chunkDir)
|
||||||
|
.filter(path -> path.getFileName().toString().startsWith("chunk_"))
|
||||||
|
.sorted(Comparator.comparingInt(path -> {
|
||||||
|
String name2 = path.getFileName().toString();
|
||||||
|
String numberPart = name2.substring("chunk_".length());
|
||||||
|
return Integer.parseInt(numberPart);
|
||||||
|
})).collect(Collectors.toList())) {
|
||||||
|
try (InputStream in = Files.newInputStream(chunk)) {
|
||||||
|
int bytesRead;
|
||||||
|
while ((bytesRead = in.read(buffer)) != -1) {
|
||||||
|
out.write(buffer, 0, bytesRead);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
// Step 3: Determine correct extension
|
||||||
|
String detectedExtension = "";
|
||||||
|
String uploadFilename = null;
|
||||||
|
boolean extensionIsValid = false;
|
||||||
|
|
||||||
|
if (filename != null && !filename.isBlank()) {
|
||||||
|
int lastDot = filename.lastIndexOf('.');
|
||||||
|
if (lastDot > 0 && lastDot < filename.length() - 1) {
|
||||||
|
extensionIsValid = true;
|
||||||
|
uploadFilename = filename;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!extensionIsValid) {
|
||||||
|
Tika tika = new Tika();
|
||||||
|
String mimeType = tika.detect(tempFile.toFile());
|
||||||
|
try {
|
||||||
|
MimeTypes allTypes = MimeTypes.getDefaultMimeTypes();
|
||||||
|
org.apache.tika.mime.MimeType mime = allTypes.forName(mimeType);
|
||||||
|
detectedExtension = mime.getExtension();
|
||||||
|
} catch (MimeTypeException e) {
|
||||||
|
LOGGER.warn("Could not determine file extension for MIME type: {}", mimeType, e);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (filename != null && !filename.isBlank()) {
|
||||||
|
int lastDot = filename.lastIndexOf('.');
|
||||||
|
String baseName = (lastDot > 0) ? filename.substring(0, lastDot) : filename;
|
||||||
|
uploadFilename = baseName + (detectedExtension != null ? detectedExtension : "");
|
||||||
|
} else {
|
||||||
|
uploadFilename = "qortal-" + NTP.getTime() + (detectedExtension != null ? detectedExtension : "");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
Boolean isZipBoolean = false;
|
||||||
|
|
||||||
|
if (isZip != null && isZip) {
|
||||||
|
isZipBoolean = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
return this.upload(
|
||||||
|
Service.valueOf(serviceString),
|
||||||
|
name,
|
||||||
|
identifier,
|
||||||
|
tempFile.toString(),
|
||||||
|
null,
|
||||||
|
null,
|
||||||
|
isZipBoolean,
|
||||||
|
fee,
|
||||||
|
uploadFilename,
|
||||||
|
title,
|
||||||
|
description,
|
||||||
|
tags,
|
||||||
|
category,
|
||||||
|
preview
|
||||||
|
);
|
||||||
|
|
||||||
|
} catch (IOException e) {
|
||||||
|
LOGGER.error("Unexpected error in finalizeUpload for service='{}', name='{}', name='{}'", serviceString, name, identifier, e);
|
||||||
|
|
||||||
|
throw ApiExceptionFactory.INSTANCE.createCustomException(request, ApiError.REPOSITORY_ISSUE, "Failed to merge chunks: " + e.getMessage());
|
||||||
|
} finally {
|
||||||
|
if (tempDir != null) {
|
||||||
|
try {
|
||||||
|
Files.walk(tempDir)
|
||||||
|
.sorted(Comparator.reverseOrder())
|
||||||
|
.map(java.nio.file.Path::toFile)
|
||||||
|
.forEach(File::delete);
|
||||||
|
} catch (IOException e) {
|
||||||
|
LOGGER.warn("Failed to delete temp directory: {}", tempDir, e);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
Files.walk(chunkDir)
|
||||||
|
.sorted(Comparator.reverseOrder())
|
||||||
|
.map(java.nio.file.Path::toFile)
|
||||||
|
.forEach(File::delete);
|
||||||
|
} catch (IOException e) {
|
||||||
|
LOGGER.warn("Failed to delete chunk directory: {}", chunkDir, e);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
// Upload base64-encoded data
|
// Upload base64-encoded data
|
||||||
|
|
||||||
@ -1343,7 +1818,7 @@ public class ArbitraryResource {
|
|||||||
if (path == null) {
|
if (path == null) {
|
||||||
// See if we have a string instead
|
// See if we have a string instead
|
||||||
if (string != null) {
|
if (string != null) {
|
||||||
if (filename == null) {
|
if (filename == null || filename.isBlank()) {
|
||||||
// Use current time as filename
|
// Use current time as filename
|
||||||
filename = String.format("qortal-%d", NTP.getTime());
|
filename = String.format("qortal-%d", NTP.getTime());
|
||||||
}
|
}
|
||||||
@ -1358,7 +1833,7 @@ public class ArbitraryResource {
|
|||||||
}
|
}
|
||||||
// ... or base64 encoded raw data
|
// ... or base64 encoded raw data
|
||||||
else if (base64 != null) {
|
else if (base64 != null) {
|
||||||
if (filename == null) {
|
if (filename == null || filename.isBlank()) {
|
||||||
// Use current time as filename
|
// Use current time as filename
|
||||||
filename = String.format("qortal-%d", NTP.getTime());
|
filename = String.format("qortal-%d", NTP.getTime());
|
||||||
}
|
}
|
||||||
@ -1409,6 +1884,7 @@ public class ArbitraryResource {
|
|||||||
);
|
);
|
||||||
|
|
||||||
transactionBuilder.build();
|
transactionBuilder.build();
|
||||||
|
|
||||||
// Don't compute nonce - this is done by the client (or via POST /arbitrary/compute)
|
// Don't compute nonce - this is done by the client (or via POST /arbitrary/compute)
|
||||||
ArbitraryTransactionData transactionData = transactionBuilder.getArbitraryTransactionData();
|
ArbitraryTransactionData transactionData = transactionBuilder.getArbitraryTransactionData();
|
||||||
return Base58.encode(ArbitraryTransactionTransformer.toBytes(transactionData));
|
return Base58.encode(ArbitraryTransactionTransformer.toBytes(transactionData));
|
||||||
@ -1424,8 +1900,7 @@ public class ArbitraryResource {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private HttpServletResponse download(Service service, String name, String identifier, String filepath, String encoding, boolean rebuild, boolean async, Integer maxAttempts) {
|
private void download(Service service, String name, String identifier, String filepath, String encoding, boolean rebuild, boolean async, Integer maxAttempts, boolean attachment, String attachmentFilename) {
|
||||||
|
|
||||||
try {
|
try {
|
||||||
ArbitraryDataReader arbitraryDataReader = new ArbitraryDataReader(name, ArbitraryDataFile.ResourceIdType.NAME, service, identifier);
|
ArbitraryDataReader arbitraryDataReader = new ArbitraryDataReader(name, ArbitraryDataFile.ResourceIdType.NAME, service, identifier);
|
||||||
|
|
||||||
@ -1438,8 +1913,7 @@ public class ArbitraryResource {
|
|||||||
if (async) {
|
if (async) {
|
||||||
// Asynchronous
|
// Asynchronous
|
||||||
arbitraryDataReader.loadAsynchronously(false, 1);
|
arbitraryDataReader.loadAsynchronously(false, 1);
|
||||||
}
|
} else {
|
||||||
else {
|
|
||||||
// Synchronous
|
// Synchronous
|
||||||
while (!Controller.isStopping()) {
|
while (!Controller.isStopping()) {
|
||||||
attempts++;
|
attempts++;
|
||||||
@ -1449,12 +1923,10 @@ public class ArbitraryResource {
|
|||||||
break;
|
break;
|
||||||
} catch (MissingDataException e) {
|
} catch (MissingDataException e) {
|
||||||
if (attempts > maxAttempts) {
|
if (attempts > maxAttempts) {
|
||||||
// Give up after 5 attempts
|
|
||||||
throw ApiExceptionFactory.INSTANCE.createCustomException(request, ApiError.INVALID_CRITERIA, "Data unavailable. Please try again later.");
|
throw ApiExceptionFactory.INSTANCE.createCustomException(request, ApiError.INVALID_CRITERIA, "Data unavailable. Please try again later.");
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
Thread.sleep(3000L);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -1470,67 +1942,170 @@ public class ArbitraryResource {
|
|||||||
if (files != null && files.length == 1) {
|
if (files != null && files.length == 1) {
|
||||||
// This is a single file resource
|
// This is a single file resource
|
||||||
filepath = files[0];
|
filepath = files[0];
|
||||||
}
|
} else {
|
||||||
else {
|
throw ApiExceptionFactory.INSTANCE.createCustomException(request, ApiError.INVALID_CRITERIA, "filepath is required for resources containing more than one file");
|
||||||
throw ApiExceptionFactory.INSTANCE.createCustomException(request, ApiError.INVALID_CRITERIA,
|
|
||||||
"filepath is required for resources containing more than one file");
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
java.nio.file.Path path = Paths.get(outputPath.toString(), filepath);
|
java.nio.file.Path path = Paths.get(outputPath.toString(), filepath);
|
||||||
if (!Files.exists(path)) {
|
if (!Files.exists(path)) {
|
||||||
String message = String.format("No file exists at filepath: %s", filepath);
|
throw ApiExceptionFactory.INSTANCE.createCustomException(request, ApiError.INVALID_CRITERIA, "No file exists at filepath: " + filepath);
|
||||||
throw ApiExceptionFactory.INSTANCE.createCustomException(request, ApiError.INVALID_CRITERIA, message);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
byte[] data;
|
if (attachment) {
|
||||||
int fileSize = (int)path.toFile().length();
|
String rawFilename;
|
||||||
int length = fileSize;
|
|
||||||
|
|
||||||
// Parse "Range" header
|
if (attachmentFilename != null && !attachmentFilename.isEmpty()) {
|
||||||
Integer rangeStart = null;
|
// 1. Sanitize first
|
||||||
Integer rangeEnd = null;
|
String safeAttachmentFilename = attachmentFilename.replaceAll("[\\\\/:*?\"<>|]", "_");
|
||||||
|
|
||||||
|
// 2. Check for a valid extension (3–5 alphanumeric chars)
|
||||||
|
if (!safeAttachmentFilename.matches(".*\\.[a-zA-Z0-9]{2,5}$")) {
|
||||||
|
safeAttachmentFilename += ".bin";
|
||||||
|
}
|
||||||
|
|
||||||
|
rawFilename = safeAttachmentFilename;
|
||||||
|
} else {
|
||||||
|
// Fallback if no filename is provided
|
||||||
|
String baseFilename = (identifier != null && !identifier.isEmpty())
|
||||||
|
? name + "-" + identifier
|
||||||
|
: name;
|
||||||
|
rawFilename = baseFilename.replaceAll("[\\\\/:*?\"<>|]", "_") + ".bin";
|
||||||
|
}
|
||||||
|
|
||||||
|
// Optional: trim length
|
||||||
|
rawFilename = rawFilename.length() > 100 ? rawFilename.substring(0, 100) : rawFilename;
|
||||||
|
|
||||||
|
// 3. Set Content-Disposition header
|
||||||
|
response.setHeader("Content-Disposition", "attachment; filename=\"" + rawFilename + "\"");
|
||||||
|
}
|
||||||
|
|
||||||
|
// Determine the total size of the requested file
|
||||||
|
long fileSize = Files.size(path);
|
||||||
|
String mimeType = context.getMimeType(path.toString());
|
||||||
|
|
||||||
|
// Attempt to read the "Range" header from the request to support partial content delivery (e.g., for video streaming or resumable downloads)
|
||||||
String range = request.getHeader("Range");
|
String range = request.getHeader("Range");
|
||||||
if (range != null) {
|
|
||||||
range = range.replace("bytes=", "");
|
long rangeStart = 0;
|
||||||
String[] parts = range.split("-");
|
long rangeEnd = fileSize - 1;
|
||||||
rangeStart = (parts != null && parts.length > 0) ? Integer.parseInt(parts[0]) : null;
|
boolean isPartial = false;
|
||||||
rangeEnd = (parts != null && parts.length > 1) ? Integer.parseInt(parts[1]) : fileSize;
|
|
||||||
|
// If a Range header is present and no base64 encoding is requested, parse the range values
|
||||||
|
if (range != null && encoding == null) {
|
||||||
|
range = range.replace("bytes=", ""); // Remove the "bytes=" prefix
|
||||||
|
String[] parts = range.split("-"); // Split the range into start and end
|
||||||
|
|
||||||
|
// Parse range start
|
||||||
|
if (parts.length > 0 && !parts[0].isEmpty()) {
|
||||||
|
rangeStart = Long.parseLong(parts[0]);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (rangeStart != null && rangeEnd != null) {
|
// Parse range end, if present
|
||||||
// We have a range, so update the requested length
|
if (parts.length > 1 && !parts[1].isEmpty()) {
|
||||||
length = rangeEnd - rangeStart;
|
rangeEnd = Long.parseLong(parts[1]);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (length < fileSize && encoding == null) {
|
isPartial = true; // Indicate that this is a partial content request
|
||||||
// Partial content requested, and not encoding the data
|
|
||||||
response.setStatus(206);
|
|
||||||
response.addHeader("Content-Range", String.format("bytes %d-%d/%d", rangeStart, rangeEnd-1, fileSize));
|
|
||||||
data = FilesystemUtils.readFromFile(path.toString(), rangeStart, length);
|
|
||||||
}
|
|
||||||
else {
|
|
||||||
// Full content requested (or encoded data)
|
|
||||||
response.setStatus(200);
|
|
||||||
data = Files.readAllBytes(path); // TODO: limit file size that can be read into memory
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// Encode the data if requested
|
// Calculate how many bytes should be sent in the response
|
||||||
if (encoding != null && Objects.equals(encoding.toLowerCase(), "base64")) {
|
long contentLength = rangeEnd - rangeStart + 1;
|
||||||
data = Base64.encode(data);
|
|
||||||
|
// Inform the client that byte ranges are supported
|
||||||
|
response.setHeader("Accept-Ranges", "bytes");
|
||||||
|
|
||||||
|
if (isPartial) {
|
||||||
|
// If partial content was requested, return 206 Partial Content with appropriate headers
|
||||||
|
response.setStatus(HttpServletResponse.SC_PARTIAL_CONTENT);
|
||||||
|
response.setHeader("Content-Range", String.format("bytes %d-%d/%d", rangeStart, rangeEnd, fileSize));
|
||||||
|
} else {
|
||||||
|
// Otherwise, return the entire file with status 200 OK
|
||||||
|
response.setStatus(HttpServletResponse.SC_OK);
|
||||||
}
|
}
|
||||||
|
|
||||||
response.addHeader("Accept-Ranges", "bytes");
|
// Initialize output streams for writing the file to the response
|
||||||
response.setContentType(context.getMimeType(path.toString()));
|
OutputStream rawOut = null;
|
||||||
response.setContentLength(data.length);
|
OutputStream base64Out = null;
|
||||||
response.getOutputStream().write(data);
|
OutputStream gzipOut = null;
|
||||||
|
|
||||||
return response;
|
try {
|
||||||
} catch (Exception e) {
|
rawOut = response.getOutputStream();
|
||||||
LOGGER.debug(String.format("Unable to load %s %s: %s", service, name, e.getMessage()));
|
|
||||||
|
if (encoding != null && "base64".equalsIgnoreCase(encoding)) {
|
||||||
|
// If base64 encoding is requested, override content type
|
||||||
|
response.setContentType("text/plain");
|
||||||
|
|
||||||
|
// Check if the client accepts gzip encoding
|
||||||
|
String acceptEncoding = request.getHeader("Accept-Encoding");
|
||||||
|
boolean wantsGzip = acceptEncoding != null && acceptEncoding.contains("gzip");
|
||||||
|
|
||||||
|
if (wantsGzip) {
|
||||||
|
// Wrap output in GZIP and Base64 streams if gzip is accepted
|
||||||
|
response.setHeader("Content-Encoding", "gzip");
|
||||||
|
gzipOut = new GZIPOutputStream(rawOut);
|
||||||
|
base64Out = java.util.Base64.getEncoder().wrap(gzipOut);
|
||||||
|
} else {
|
||||||
|
// Wrap output in Base64 only
|
||||||
|
base64Out = java.util.Base64.getEncoder().wrap(rawOut);
|
||||||
|
}
|
||||||
|
|
||||||
|
rawOut = base64Out; // Use the wrapped stream for writing
|
||||||
|
} else {
|
||||||
|
// For raw binary output, set the content type and length
|
||||||
|
response.setContentType(mimeType != null ? mimeType : "application/octet-stream");
|
||||||
|
response.setContentLength((int) contentLength);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Stream file content
|
||||||
|
try (InputStream inputStream = Files.newInputStream(path)) {
|
||||||
|
if (rangeStart > 0) {
|
||||||
|
inputStream.skip(rangeStart);
|
||||||
|
}
|
||||||
|
|
||||||
|
byte[] buffer = new byte[65536];
|
||||||
|
long bytesRemaining = contentLength;
|
||||||
|
int bytesRead;
|
||||||
|
|
||||||
|
while (bytesRemaining > 0 && (bytesRead = inputStream.read(buffer, 0, (int) Math.min(buffer.length, bytesRemaining))) != -1) {
|
||||||
|
rawOut.write(buffer, 0, bytesRead);
|
||||||
|
bytesRemaining -= bytesRead;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Stream finished
|
||||||
|
if (base64Out != null) {
|
||||||
|
base64Out.close(); // Also flushes and closes the wrapped gzipOut
|
||||||
|
} else if (gzipOut != null) {
|
||||||
|
gzipOut.close(); // Only close gzipOut if it wasn't wrapped by base64Out
|
||||||
|
} else {
|
||||||
|
rawOut.flush(); // Flush only the base output stream if nothing was wrapped
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!response.isCommitted()) {
|
||||||
|
response.setStatus(HttpServletResponse.SC_OK);
|
||||||
|
response.getWriter().write(" ");
|
||||||
|
}
|
||||||
|
|
||||||
|
} catch (IOException e) {
|
||||||
|
// Streaming errors should not rethrow — just log
|
||||||
|
LOGGER.warn(String.format("Streaming error for %s %s: %s", service, name, e.getMessage()));
|
||||||
|
}
|
||||||
|
|
||||||
|
} catch (IOException | ApiException | DataException e) {
|
||||||
|
LOGGER.warn(String.format("Unable to load %s %s: %s", service, name, e.getMessage()));
|
||||||
|
if (!response.isCommitted()) {
|
||||||
throw ApiExceptionFactory.INSTANCE.createCustomException(request, ApiError.FILE_NOT_FOUND, e.getMessage());
|
throw ApiExceptionFactory.INSTANCE.createCustomException(request, ApiError.FILE_NOT_FOUND, e.getMessage());
|
||||||
}
|
}
|
||||||
|
} catch (NumberFormatException e) {
|
||||||
|
LOGGER.warn(String.format("Invalid range for %s %s: %s", service, name, e.getMessage()));
|
||||||
|
if (!response.isCommitted()) {
|
||||||
|
throw ApiExceptionFactory.INSTANCE.createCustomException(request, ApiError.INVALID_DATA, e.getMessage());
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
private FileProperties getFileProperties(Service service, String name, String identifier) {
|
private FileProperties getFileProperties(Service service, String name, String identifier) {
|
||||||
try {
|
try {
|
||||||
|
@ -502,10 +502,10 @@ public class CrossChainBitcoinResource {
|
|||||||
}
|
}
|
||||||
|
|
||||||
@GET
|
@GET
|
||||||
@Path("/feeceiling")
|
@Path("/feerequired")
|
||||||
@Operation(
|
@Operation(
|
||||||
summary = "Returns Bitcoin fee per Kb.",
|
summary = "The total fee required for unlocking BTC to the trade offer creator.",
|
||||||
description = "Returns Bitcoin fee per Kb.",
|
description = "This is in sats for a transaction that is approximately 300 kB in size.",
|
||||||
responses = {
|
responses = {
|
||||||
@ApiResponse(
|
@ApiResponse(
|
||||||
content = @Content(
|
content = @Content(
|
||||||
@ -516,17 +516,17 @@ public class CrossChainBitcoinResource {
|
|||||||
)
|
)
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
public String getBitcoinFeeCeiling() {
|
public String getBitcoinFeeRequired() {
|
||||||
Bitcoin bitcoin = Bitcoin.getInstance();
|
Bitcoin bitcoin = Bitcoin.getInstance();
|
||||||
|
|
||||||
return String.valueOf(bitcoin.getFeeCeiling());
|
return String.valueOf(bitcoin.getFeeRequired());
|
||||||
}
|
}
|
||||||
|
|
||||||
@POST
|
@POST
|
||||||
@Path("/updatefeeceiling")
|
@Path("/updatefeerequired")
|
||||||
@Operation(
|
@Operation(
|
||||||
summary = "Sets Bitcoin fee ceiling.",
|
summary = "The total fee required for unlocking BTC to the trade offer creator.",
|
||||||
description = "Sets Bitcoin fee ceiling.",
|
description = "This is in sats for a transaction that is approximately 300 kB in size.",
|
||||||
requestBody = @RequestBody(
|
requestBody = @RequestBody(
|
||||||
required = true,
|
required = true,
|
||||||
content = @Content(
|
content = @Content(
|
||||||
@ -545,13 +545,13 @@ public class CrossChainBitcoinResource {
|
|||||||
}
|
}
|
||||||
)
|
)
|
||||||
@ApiErrors({ApiError.INVALID_PRIVATE_KEY, ApiError.INVALID_CRITERIA})
|
@ApiErrors({ApiError.INVALID_PRIVATE_KEY, ApiError.INVALID_CRITERIA})
|
||||||
public String setBitcoinFeeCeiling(@HeaderParam(Security.API_KEY_HEADER) String apiKey, String fee) {
|
public String setBitcoinFeeRequired(@HeaderParam(Security.API_KEY_HEADER) String apiKey, String fee) {
|
||||||
Security.checkApiCallAllowed(request);
|
Security.checkApiCallAllowed(request);
|
||||||
|
|
||||||
Bitcoin bitcoin = Bitcoin.getInstance();
|
Bitcoin bitcoin = Bitcoin.getInstance();
|
||||||
|
|
||||||
try {
|
try {
|
||||||
return CrossChainUtils.setFeeCeiling(bitcoin, fee);
|
return CrossChainUtils.setFeeRequired(bitcoin, fee);
|
||||||
}
|
}
|
||||||
catch (IllegalArgumentException e) {
|
catch (IllegalArgumentException e) {
|
||||||
throw ApiExceptionFactory.INSTANCE.createException(request, ApiError.INVALID_CRITERIA);
|
throw ApiExceptionFactory.INSTANCE.createException(request, ApiError.INVALID_CRITERIA);
|
||||||
|
@ -502,10 +502,10 @@ public class CrossChainDigibyteResource {
|
|||||||
}
|
}
|
||||||
|
|
||||||
@GET
|
@GET
|
||||||
@Path("/feeceiling")
|
@Path("/feerequired")
|
||||||
@Operation(
|
@Operation(
|
||||||
summary = "Returns Digibyte fee per Kb.",
|
summary = "The total fee required for unlocking DGB to the trade offer creator.",
|
||||||
description = "Returns Digibyte fee per Kb.",
|
description = "This is in sats for a transaction that is approximately 300 kB in size.",
|
||||||
responses = {
|
responses = {
|
||||||
@ApiResponse(
|
@ApiResponse(
|
||||||
content = @Content(
|
content = @Content(
|
||||||
@ -516,17 +516,17 @@ public class CrossChainDigibyteResource {
|
|||||||
)
|
)
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
public String getDigibyteFeeCeiling() {
|
public String getDigibyteFeeRequired() {
|
||||||
Digibyte digibyte = Digibyte.getInstance();
|
Digibyte digibyte = Digibyte.getInstance();
|
||||||
|
|
||||||
return String.valueOf(digibyte.getFeeCeiling());
|
return String.valueOf(digibyte.getFeeRequired());
|
||||||
}
|
}
|
||||||
|
|
||||||
@POST
|
@POST
|
||||||
@Path("/updatefeeceiling")
|
@Path("/updatefeerequired")
|
||||||
@Operation(
|
@Operation(
|
||||||
summary = "Sets Digibyte fee ceiling.",
|
summary = "The total fee required for unlocking DGB to the trade offer creator.",
|
||||||
description = "Sets Digibyte fee ceiling.",
|
description = "This is in sats for a transaction that is approximately 300 kB in size.",
|
||||||
requestBody = @RequestBody(
|
requestBody = @RequestBody(
|
||||||
required = true,
|
required = true,
|
||||||
content = @Content(
|
content = @Content(
|
||||||
@ -545,13 +545,13 @@ public class CrossChainDigibyteResource {
|
|||||||
}
|
}
|
||||||
)
|
)
|
||||||
@ApiErrors({ApiError.INVALID_PRIVATE_KEY, ApiError.INVALID_CRITERIA})
|
@ApiErrors({ApiError.INVALID_PRIVATE_KEY, ApiError.INVALID_CRITERIA})
|
||||||
public String setDigibyteFeeCeiling(@HeaderParam(Security.API_KEY_HEADER) String apiKey, String fee) {
|
public String setDigibyteFeeRequired(@HeaderParam(Security.API_KEY_HEADER) String apiKey, String fee) {
|
||||||
Security.checkApiCallAllowed(request);
|
Security.checkApiCallAllowed(request);
|
||||||
|
|
||||||
Digibyte digibyte = Digibyte.getInstance();
|
Digibyte digibyte = Digibyte.getInstance();
|
||||||
|
|
||||||
try {
|
try {
|
||||||
return CrossChainUtils.setFeeCeiling(digibyte, fee);
|
return CrossChainUtils.setFeeRequired(digibyte, fee);
|
||||||
}
|
}
|
||||||
catch (IllegalArgumentException e) {
|
catch (IllegalArgumentException e) {
|
||||||
throw ApiExceptionFactory.INSTANCE.createException(request, ApiError.INVALID_CRITERIA);
|
throw ApiExceptionFactory.INSTANCE.createException(request, ApiError.INVALID_CRITERIA);
|
||||||
|
@ -502,10 +502,10 @@ public class CrossChainDogecoinResource {
|
|||||||
}
|
}
|
||||||
|
|
||||||
@GET
|
@GET
|
||||||
@Path("/feeceiling")
|
@Path("/feerequired")
|
||||||
@Operation(
|
@Operation(
|
||||||
summary = "Returns Dogecoin fee per Kb.",
|
summary = "The total fee required for unlocking DOGE to the trade offer creator.",
|
||||||
description = "Returns Dogecoin fee per Kb.",
|
description = "This is in sats for a transaction that is approximately 300 kB in size.",
|
||||||
responses = {
|
responses = {
|
||||||
@ApiResponse(
|
@ApiResponse(
|
||||||
content = @Content(
|
content = @Content(
|
||||||
@ -516,17 +516,17 @@ public class CrossChainDogecoinResource {
|
|||||||
)
|
)
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
public String getDogecoinFeeCeiling() {
|
public String getDogecoinFeeRequired() {
|
||||||
Dogecoin dogecoin = Dogecoin.getInstance();
|
Dogecoin dogecoin = Dogecoin.getInstance();
|
||||||
|
|
||||||
return String.valueOf(dogecoin.getFeeCeiling());
|
return String.valueOf(dogecoin.getFeeRequired());
|
||||||
}
|
}
|
||||||
|
|
||||||
@POST
|
@POST
|
||||||
@Path("/updatefeeceiling")
|
@Path("/updatefeerequired")
|
||||||
@Operation(
|
@Operation(
|
||||||
summary = "Sets Dogecoin fee ceiling.",
|
summary = "The total fee required for unlocking DOGE to the trade offer creator.",
|
||||||
description = "Sets Dogecoin fee ceiling.",
|
description = "This is in sats for a transaction that is approximately 300 kB in size.",
|
||||||
requestBody = @RequestBody(
|
requestBody = @RequestBody(
|
||||||
required = true,
|
required = true,
|
||||||
content = @Content(
|
content = @Content(
|
||||||
@ -545,13 +545,13 @@ public class CrossChainDogecoinResource {
|
|||||||
}
|
}
|
||||||
)
|
)
|
||||||
@ApiErrors({ApiError.INVALID_PRIVATE_KEY, ApiError.INVALID_CRITERIA})
|
@ApiErrors({ApiError.INVALID_PRIVATE_KEY, ApiError.INVALID_CRITERIA})
|
||||||
public String setDogecoinFeeCeiling(@HeaderParam(Security.API_KEY_HEADER) String apiKey, String fee) {
|
public String setDogecoinFeeRequired(@HeaderParam(Security.API_KEY_HEADER) String apiKey, String fee) {
|
||||||
Security.checkApiCallAllowed(request);
|
Security.checkApiCallAllowed(request);
|
||||||
|
|
||||||
Dogecoin dogecoin = Dogecoin.getInstance();
|
Dogecoin dogecoin = Dogecoin.getInstance();
|
||||||
|
|
||||||
try {
|
try {
|
||||||
return CrossChainUtils.setFeeCeiling(dogecoin, fee);
|
return CrossChainUtils.setFeeRequired(dogecoin, fee);
|
||||||
}
|
}
|
||||||
catch (IllegalArgumentException e) {
|
catch (IllegalArgumentException e) {
|
||||||
throw ApiExceptionFactory.INSTANCE.createException(request, ApiError.INVALID_CRITERIA);
|
throw ApiExceptionFactory.INSTANCE.createException(request, ApiError.INVALID_CRITERIA);
|
||||||
|
@ -540,10 +540,10 @@ public class CrossChainLitecoinResource {
|
|||||||
}
|
}
|
||||||
|
|
||||||
@GET
|
@GET
|
||||||
@Path("/feeceiling")
|
@Path("/feerequired")
|
||||||
@Operation(
|
@Operation(
|
||||||
summary = "Returns Litecoin fee per Kb.",
|
summary = "The total fee required for unlocking LTC to the trade offer creator.",
|
||||||
description = "Returns Litecoin fee per Kb.",
|
description = "This is in sats for a transaction that is approximately 300 kB in size.",
|
||||||
responses = {
|
responses = {
|
||||||
@ApiResponse(
|
@ApiResponse(
|
||||||
content = @Content(
|
content = @Content(
|
||||||
@ -554,17 +554,17 @@ public class CrossChainLitecoinResource {
|
|||||||
)
|
)
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
public String getLitecoinFeeCeiling() {
|
public String getLitecoinFeeRequired() {
|
||||||
Litecoin litecoin = Litecoin.getInstance();
|
Litecoin litecoin = Litecoin.getInstance();
|
||||||
|
|
||||||
return String.valueOf(litecoin.getFeeCeiling());
|
return String.valueOf(litecoin.getFeeRequired());
|
||||||
}
|
}
|
||||||
|
|
||||||
@POST
|
@POST
|
||||||
@Path("/updatefeeceiling")
|
@Path("/updatefeerequired")
|
||||||
@Operation(
|
@Operation(
|
||||||
summary = "Sets Litecoin fee ceiling.",
|
summary = "The total fee required for unlocking LTC to the trade offer creator.",
|
||||||
description = "Sets Litecoin fee ceiling.",
|
description = "This is in sats for a transaction that is approximately 300 kB in size.",
|
||||||
requestBody = @RequestBody(
|
requestBody = @RequestBody(
|
||||||
required = true,
|
required = true,
|
||||||
content = @Content(
|
content = @Content(
|
||||||
@ -583,13 +583,13 @@ public class CrossChainLitecoinResource {
|
|||||||
}
|
}
|
||||||
)
|
)
|
||||||
@ApiErrors({ApiError.INVALID_PRIVATE_KEY, ApiError.INVALID_CRITERIA})
|
@ApiErrors({ApiError.INVALID_PRIVATE_KEY, ApiError.INVALID_CRITERIA})
|
||||||
public String setLitecoinFeeCeiling(@HeaderParam(Security.API_KEY_HEADER) String apiKey, String fee) {
|
public String setLitecoinFeeRequired(@HeaderParam(Security.API_KEY_HEADER) String apiKey, String fee) {
|
||||||
Security.checkApiCallAllowed(request);
|
Security.checkApiCallAllowed(request);
|
||||||
|
|
||||||
Litecoin litecoin = Litecoin.getInstance();
|
Litecoin litecoin = Litecoin.getInstance();
|
||||||
|
|
||||||
try {
|
try {
|
||||||
return CrossChainUtils.setFeeCeiling(litecoin, fee);
|
return CrossChainUtils.setFeeRequired(litecoin, fee);
|
||||||
}
|
}
|
||||||
catch (IllegalArgumentException e) {
|
catch (IllegalArgumentException e) {
|
||||||
throw ApiExceptionFactory.INSTANCE.createException(request, ApiError.INVALID_CRITERIA);
|
throw ApiExceptionFactory.INSTANCE.createException(request, ApiError.INVALID_CRITERIA);
|
||||||
|
@ -587,10 +587,10 @@ public class CrossChainPirateChainResource {
|
|||||||
}
|
}
|
||||||
|
|
||||||
@GET
|
@GET
|
||||||
@Path("/feeceiling")
|
@Path("/feerequired")
|
||||||
@Operation(
|
@Operation(
|
||||||
summary = "Returns PirateChain fee per Kb.",
|
summary = "The total fee required for unlocking ARRR to the trade offer creator.",
|
||||||
description = "Returns PirateChain fee per Kb.",
|
description = "The total fee required for unlocking ARRR to the trade offer creator.",
|
||||||
responses = {
|
responses = {
|
||||||
@ApiResponse(
|
@ApiResponse(
|
||||||
content = @Content(
|
content = @Content(
|
||||||
@ -601,17 +601,17 @@ public class CrossChainPirateChainResource {
|
|||||||
)
|
)
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
public String getPirateChainFeeCeiling() {
|
public String getPirateChainFeeRequired() {
|
||||||
PirateChain pirateChain = PirateChain.getInstance();
|
PirateChain pirateChain = PirateChain.getInstance();
|
||||||
|
|
||||||
return String.valueOf(pirateChain.getFeeCeiling());
|
return String.valueOf(pirateChain.getFeeRequired());
|
||||||
}
|
}
|
||||||
|
|
||||||
@POST
|
@POST
|
||||||
@Path("/updatefeeceiling")
|
@Path("/updatefeerequired")
|
||||||
@Operation(
|
@Operation(
|
||||||
summary = "Sets PirateChain fee ceiling.",
|
summary = "The total fee required for unlocking ARRR to the trade offer creator.",
|
||||||
description = "Sets PirateChain fee ceiling.",
|
description = "This is in sats for a transaction that is approximately 300 kB in size.",
|
||||||
requestBody = @RequestBody(
|
requestBody = @RequestBody(
|
||||||
required = true,
|
required = true,
|
||||||
content = @Content(
|
content = @Content(
|
||||||
@ -630,13 +630,13 @@ public class CrossChainPirateChainResource {
|
|||||||
}
|
}
|
||||||
)
|
)
|
||||||
@ApiErrors({ApiError.INVALID_PRIVATE_KEY, ApiError.INVALID_CRITERIA})
|
@ApiErrors({ApiError.INVALID_PRIVATE_KEY, ApiError.INVALID_CRITERIA})
|
||||||
public String setPirateChainFeeCeiling(@HeaderParam(Security.API_KEY_HEADER) String apiKey, String fee) {
|
public String setPirateChainFeeRequired(@HeaderParam(Security.API_KEY_HEADER) String apiKey, String fee) {
|
||||||
Security.checkApiCallAllowed(request);
|
Security.checkApiCallAllowed(request);
|
||||||
|
|
||||||
PirateChain pirateChain = PirateChain.getInstance();
|
PirateChain pirateChain = PirateChain.getInstance();
|
||||||
|
|
||||||
try {
|
try {
|
||||||
return CrossChainUtils.setFeeCeiling(pirateChain, fee);
|
return CrossChainUtils.setFeeRequired(pirateChain, fee);
|
||||||
}
|
}
|
||||||
catch (IllegalArgumentException e) {
|
catch (IllegalArgumentException e) {
|
||||||
throw ApiExceptionFactory.INSTANCE.createException(request, ApiError.INVALID_CRITERIA);
|
throw ApiExceptionFactory.INSTANCE.createException(request, ApiError.INVALID_CRITERIA);
|
||||||
|
@ -502,10 +502,10 @@ public class CrossChainRavencoinResource {
|
|||||||
}
|
}
|
||||||
|
|
||||||
@GET
|
@GET
|
||||||
@Path("/feeceiling")
|
@Path("/feerequired")
|
||||||
@Operation(
|
@Operation(
|
||||||
summary = "Returns Ravencoin fee per Kb.",
|
summary = "The total fee required for unlocking RVN to the trade offer creator.",
|
||||||
description = "Returns Ravencoin fee per Kb.",
|
description = "This is in sats for a transaction that is approximately 300 kB in size.",
|
||||||
responses = {
|
responses = {
|
||||||
@ApiResponse(
|
@ApiResponse(
|
||||||
content = @Content(
|
content = @Content(
|
||||||
@ -516,17 +516,17 @@ public class CrossChainRavencoinResource {
|
|||||||
)
|
)
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
public String getRavencoinFeeCeiling() {
|
public String getRavencoinFeeRequired() {
|
||||||
Ravencoin ravencoin = Ravencoin.getInstance();
|
Ravencoin ravencoin = Ravencoin.getInstance();
|
||||||
|
|
||||||
return String.valueOf(ravencoin.getFeeCeiling());
|
return String.valueOf(ravencoin.getFeeRequired());
|
||||||
}
|
}
|
||||||
|
|
||||||
@POST
|
@POST
|
||||||
@Path("/updatefeeceiling")
|
@Path("/updatefeerequired")
|
||||||
@Operation(
|
@Operation(
|
||||||
summary = "Sets Ravencoin fee ceiling.",
|
summary = "The total fee required for unlocking RVN to the trade offer creator.",
|
||||||
description = "Sets Ravencoin fee ceiling.",
|
description = "This is in sats for a transaction that is approximately 300 kB in size.",
|
||||||
requestBody = @RequestBody(
|
requestBody = @RequestBody(
|
||||||
required = true,
|
required = true,
|
||||||
content = @Content(
|
content = @Content(
|
||||||
@ -545,13 +545,13 @@ public class CrossChainRavencoinResource {
|
|||||||
}
|
}
|
||||||
)
|
)
|
||||||
@ApiErrors({ApiError.INVALID_PRIVATE_KEY, ApiError.INVALID_CRITERIA})
|
@ApiErrors({ApiError.INVALID_PRIVATE_KEY, ApiError.INVALID_CRITERIA})
|
||||||
public String setRavencoinFeeCeiling(@HeaderParam(Security.API_KEY_HEADER) String apiKey, String fee) {
|
public String setRavencoinFeeRequired(@HeaderParam(Security.API_KEY_HEADER) String apiKey, String fee) {
|
||||||
Security.checkApiCallAllowed(request);
|
Security.checkApiCallAllowed(request);
|
||||||
|
|
||||||
Ravencoin ravencoin = Ravencoin.getInstance();
|
Ravencoin ravencoin = Ravencoin.getInstance();
|
||||||
|
|
||||||
try {
|
try {
|
||||||
return CrossChainUtils.setFeeCeiling(ravencoin, fee);
|
return CrossChainUtils.setFeeRequired(ravencoin, fee);
|
||||||
}
|
}
|
||||||
catch (IllegalArgumentException e) {
|
catch (IllegalArgumentException e) {
|
||||||
throw ApiExceptionFactory.INSTANCE.createException(request, ApiError.INVALID_CRITERIA);
|
throw ApiExceptionFactory.INSTANCE.createException(request, ApiError.INVALID_CRITERIA);
|
||||||
|
@ -10,6 +10,8 @@ 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.apache.logging.log4j.LogManager;
|
||||||
|
import org.apache.logging.log4j.Logger;
|
||||||
import org.glassfish.jersey.media.multipart.ContentDisposition;
|
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;
|
||||||
@ -18,6 +20,7 @@ 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.CrossChainTradeLedgerEntry;
|
||||||
import org.qortal.api.model.CrossChainTradeSummary;
|
import org.qortal.api.model.CrossChainTradeSummary;
|
||||||
|
import org.qortal.controller.ForeignFeesManager;
|
||||||
import org.qortal.controller.tradebot.TradeBot;
|
import org.qortal.controller.tradebot.TradeBot;
|
||||||
import org.qortal.crosschain.ACCT;
|
import org.qortal.crosschain.ACCT;
|
||||||
import org.qortal.crosschain.AcctMode;
|
import org.qortal.crosschain.AcctMode;
|
||||||
@ -29,6 +32,8 @@ import org.qortal.data.at.ATData;
|
|||||||
import org.qortal.data.at.ATStateData;
|
import org.qortal.data.at.ATStateData;
|
||||||
import org.qortal.data.crosschain.CrossChainTradeData;
|
import org.qortal.data.crosschain.CrossChainTradeData;
|
||||||
import org.qortal.data.crosschain.TransactionSummary;
|
import org.qortal.data.crosschain.TransactionSummary;
|
||||||
|
import org.qortal.data.crosschain.ForeignFeeDecodedData;
|
||||||
|
import org.qortal.data.crosschain.ForeignFeeEncodedData;
|
||||||
import org.qortal.data.transaction.BaseTransactionData;
|
import org.qortal.data.transaction.BaseTransactionData;
|
||||||
import org.qortal.data.transaction.MessageTransactionData;
|
import org.qortal.data.transaction.MessageTransactionData;
|
||||||
import org.qortal.data.transaction.TransactionData;
|
import org.qortal.data.transaction.TransactionData;
|
||||||
@ -64,6 +69,8 @@ import java.util.stream.Collectors;
|
|||||||
@Tag(name = "Cross-Chain")
|
@Tag(name = "Cross-Chain")
|
||||||
public class CrossChainResource {
|
public class CrossChainResource {
|
||||||
|
|
||||||
|
private static final Logger LOGGER = LogManager.getLogger(CrossChainResource.class);
|
||||||
|
|
||||||
@Context
|
@Context
|
||||||
HttpServletRequest request;
|
HttpServletRequest request;
|
||||||
|
|
||||||
@ -360,6 +367,101 @@ public class CrossChainResource {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@POST
|
||||||
|
@Path("/signedfees")
|
||||||
|
@Operation(
|
||||||
|
summary = "",
|
||||||
|
description = "",
|
||||||
|
requestBody = @RequestBody(
|
||||||
|
required = true,
|
||||||
|
content = @Content(
|
||||||
|
mediaType = MediaType.APPLICATION_JSON,
|
||||||
|
array = @ArraySchema(
|
||||||
|
schema = @Schema(
|
||||||
|
implementation = ForeignFeeEncodedData.class
|
||||||
|
)
|
||||||
|
)
|
||||||
|
)
|
||||||
|
),
|
||||||
|
responses = {
|
||||||
|
@ApiResponse(
|
||||||
|
description = "true on success",
|
||||||
|
content = @Content(
|
||||||
|
mediaType = MediaType.TEXT_PLAIN,
|
||||||
|
schema = @Schema(
|
||||||
|
type = "boolean"
|
||||||
|
)
|
||||||
|
)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
)
|
||||||
|
public String postSignedForeignFees(List<ForeignFeeEncodedData> signedFees) {
|
||||||
|
|
||||||
|
LOGGER.info("signedFees = " + signedFees);
|
||||||
|
|
||||||
|
try {
|
||||||
|
ForeignFeesManager.getInstance().addSignedFees(signedFees);
|
||||||
|
|
||||||
|
return "true";
|
||||||
|
}
|
||||||
|
catch( Exception e ) {
|
||||||
|
|
||||||
|
LOGGER.error(e.getMessage(), e);
|
||||||
|
|
||||||
|
return "false";
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@GET
|
||||||
|
@Path("/unsignedfees/{address}")
|
||||||
|
@Operation(
|
||||||
|
summary = "",
|
||||||
|
description = "",
|
||||||
|
responses = {
|
||||||
|
@ApiResponse(
|
||||||
|
content = @Content(
|
||||||
|
array = @ArraySchema(
|
||||||
|
schema = @Schema(
|
||||||
|
implementation = ForeignFeeEncodedData.class
|
||||||
|
)
|
||||||
|
)
|
||||||
|
)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
)
|
||||||
|
@ApiErrors({ApiError.INVALID_CRITERIA, ApiError.REPOSITORY_ISSUE})
|
||||||
|
public List<ForeignFeeEncodedData> getUnsignedFees(@PathParam("address") String address) {
|
||||||
|
|
||||||
|
List<ForeignFeeEncodedData> unsignedFeesForAddress = ForeignFeesManager.getInstance().getUnsignedFeesForAddress(address);
|
||||||
|
|
||||||
|
LOGGER.info("address = " + address);
|
||||||
|
LOGGER.info("returning unsigned = " + unsignedFeesForAddress);
|
||||||
|
return unsignedFeesForAddress;
|
||||||
|
}
|
||||||
|
|
||||||
|
@GET
|
||||||
|
@Path("/signedfees")
|
||||||
|
@Operation(
|
||||||
|
summary = "",
|
||||||
|
description = "",
|
||||||
|
responses = {
|
||||||
|
@ApiResponse(
|
||||||
|
content = @Content(
|
||||||
|
array = @ArraySchema(
|
||||||
|
schema = @Schema(
|
||||||
|
implementation = ForeignFeeDecodedData.class
|
||||||
|
)
|
||||||
|
)
|
||||||
|
)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
)
|
||||||
|
@ApiErrors({ApiError.INVALID_CRITERIA, ApiError.REPOSITORY_ISSUE})
|
||||||
|
public List<ForeignFeeDecodedData> getSignedFees() {
|
||||||
|
|
||||||
|
return ForeignFeesManager.getInstance().getSignedFees();
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Decode Public Key
|
* Decode Public Key
|
||||||
*
|
*
|
||||||
|
@ -12,10 +12,15 @@ 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.CrossChainTradeLedgerEntry;
|
||||||
import org.qortal.api.model.crosschain.BitcoinyTBDRequest;
|
import org.qortal.api.model.crosschain.BitcoinyTBDRequest;
|
||||||
|
import org.qortal.asset.Asset;
|
||||||
import org.qortal.crosschain.*;
|
import org.qortal.crosschain.*;
|
||||||
|
import org.qortal.data.account.AccountBalanceData;
|
||||||
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.data.crosschain.*;
|
import org.qortal.data.crosschain.*;
|
||||||
|
import org.qortal.event.EventBus;
|
||||||
|
import org.qortal.event.LockingFeeUpdateEvent;
|
||||||
|
import org.qortal.event.RequiredFeeUpdateEvent;
|
||||||
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.Amounts;
|
||||||
@ -23,15 +28,11 @@ import org.qortal.utils.BitTwiddling;
|
|||||||
|
|
||||||
import java.io.BufferedWriter;
|
import java.io.BufferedWriter;
|
||||||
import java.io.IOException;
|
import java.io.IOException;
|
||||||
import java.io.OutputStreamWriter;
|
|
||||||
import java.io.PrintWriter;
|
|
||||||
import java.io.Writer;
|
import java.io.Writer;
|
||||||
import java.text.DateFormat;
|
import java.text.DateFormat;
|
||||||
import java.text.SimpleDateFormat;
|
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.function.Function;
|
||||||
import java.util.stream.Collectors;
|
import java.util.stream.Collectors;
|
||||||
|
|
||||||
|
|
||||||
@ -103,11 +104,13 @@ public class CrossChainUtils {
|
|||||||
|
|
||||||
bitcoiny.setFeePerKb(Coin.valueOf(satoshis) );
|
bitcoiny.setFeePerKb(Coin.valueOf(satoshis) );
|
||||||
|
|
||||||
|
EventBus.INSTANCE.notify(new LockingFeeUpdateEvent());
|
||||||
|
|
||||||
return String.valueOf(bitcoiny.getFeePerKb().value);
|
return String.valueOf(bitcoiny.getFeePerKb().value);
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Set Fee Ceiling
|
* Set Fee Required
|
||||||
*
|
*
|
||||||
* @param bitcoiny the blockchain support
|
* @param bitcoiny the blockchain support
|
||||||
* @param fee the fee in satoshis
|
* @param fee the fee in satoshis
|
||||||
@ -116,14 +119,16 @@ public class CrossChainUtils {
|
|||||||
*
|
*
|
||||||
* @throws IllegalArgumentException if invalid
|
* @throws IllegalArgumentException if invalid
|
||||||
*/
|
*/
|
||||||
public static String setFeeCeiling(Bitcoiny bitcoiny, String fee) throws IllegalArgumentException{
|
public static String setFeeRequired(Bitcoiny bitcoiny, String fee) throws IllegalArgumentException{
|
||||||
|
|
||||||
long satoshis = Long.parseLong(fee);
|
long satoshis = Long.parseLong(fee);
|
||||||
if( satoshis < 0 ) throw new IllegalArgumentException("can't set fee to negative number");
|
if( satoshis < 0 ) throw new IllegalArgumentException("can't set fee to negative number");
|
||||||
|
|
||||||
bitcoiny.setFeeCeiling( Long.parseLong(fee));
|
bitcoiny.setFeeRequired( Long.parseLong(fee));
|
||||||
|
|
||||||
return String.valueOf(bitcoiny.getFeeCeiling());
|
EventBus.INSTANCE.notify(new RequiredFeeUpdateEvent(bitcoiny));
|
||||||
|
|
||||||
|
return String.valueOf(bitcoiny.getFeeRequired());
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@ -232,6 +237,9 @@ public class CrossChainUtils {
|
|||||||
return bitcoiny.getBlockchainProvider().removeServer(server);
|
return bitcoiny.getBlockchainProvider().removeServer(server);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public static ChainableServer getCurrentServer( Bitcoiny bitcoiny ) {
|
||||||
|
return bitcoiny.getBlockchainProvider().getCurrentServer();
|
||||||
|
}
|
||||||
/**
|
/**
|
||||||
* Set Current Server
|
* Set Current Server
|
||||||
*
|
*
|
||||||
@ -771,4 +779,46 @@ public class CrossChainUtils {
|
|||||||
entries.add(ledgerEntry);
|
entries.add(ledgerEntry);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public static List<CrossChainTradeData> populateTradeDataList(Repository repository, ACCT acct, List<ATData> atDataList) throws DataException {
|
||||||
|
|
||||||
|
if(atDataList.isEmpty()) return new ArrayList<>(0);
|
||||||
|
|
||||||
|
List<ATStateData> latestATStates
|
||||||
|
= repository.getATRepository()
|
||||||
|
.getLatestATStates(
|
||||||
|
atDataList.stream()
|
||||||
|
.map(ATData::getATAddress)
|
||||||
|
.collect(Collectors.toList())
|
||||||
|
);
|
||||||
|
|
||||||
|
Map<String, ATStateData> atStateDataByAtAddress
|
||||||
|
= latestATStates.stream().collect(Collectors.toMap(ATStateData::getATAddress, Function.identity()));
|
||||||
|
|
||||||
|
Map<String, ATData> atDataByAtAddress
|
||||||
|
= atDataList.stream().collect(Collectors.toMap(ATData::getATAddress, Function.identity()));
|
||||||
|
|
||||||
|
Map<String, Long> balanceByAtAddress
|
||||||
|
= repository
|
||||||
|
.getAccountRepository()
|
||||||
|
.getBalances(new ArrayList<>(atDataByAtAddress.keySet()), Asset.QORT)
|
||||||
|
.stream().collect(Collectors.toMap(AccountBalanceData::getAddress, AccountBalanceData::getBalance));
|
||||||
|
|
||||||
|
List<CrossChainTradeData> crossChainTradeDataList = new ArrayList<>(latestATStates.size());
|
||||||
|
|
||||||
|
for( ATStateData atStateData : latestATStates ) {
|
||||||
|
ATData atData = atDataByAtAddress.get(atStateData.getATAddress());
|
||||||
|
crossChainTradeDataList.add(
|
||||||
|
acct.populateTradeData(
|
||||||
|
repository,
|
||||||
|
atData.getCreatorPublicKey(),
|
||||||
|
atData.getCreation(),
|
||||||
|
atStateData,
|
||||||
|
OptionalLong.of(balanceByAtAddress.get(atStateData.getATAddress()))
|
||||||
|
)
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
return crossChainTradeDataList;
|
||||||
|
}
|
||||||
}
|
}
|
@ -33,6 +33,7 @@ import javax.ws.rs.*;
|
|||||||
import javax.ws.rs.core.Context;
|
import javax.ws.rs.core.Context;
|
||||||
import javax.ws.rs.core.MediaType;
|
import javax.ws.rs.core.MediaType;
|
||||||
import java.util.List;
|
import java.util.List;
|
||||||
|
import java.util.Optional;
|
||||||
import java.util.stream.Collectors;
|
import java.util.stream.Collectors;
|
||||||
|
|
||||||
@Path("/names")
|
@Path("/names")
|
||||||
@ -104,6 +105,45 @@ public class NamesResource {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@GET
|
||||||
|
@Path("/primary/{address}")
|
||||||
|
@Operation(
|
||||||
|
summary = "primary name owned by address",
|
||||||
|
responses = {
|
||||||
|
@ApiResponse(
|
||||||
|
description = "registered primary name info",
|
||||||
|
content = @Content(
|
||||||
|
mediaType = MediaType.APPLICATION_JSON,
|
||||||
|
schema = @Schema(implementation = NameSummary.class)
|
||||||
|
)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
)
|
||||||
|
@ApiErrors({ApiError.INVALID_ADDRESS, ApiError.REPOSITORY_ISSUE, ApiError.UNAUTHORIZED})
|
||||||
|
public NameSummary getPrimaryNameByAddress(@PathParam("address") String address) {
|
||||||
|
if (!Crypto.isValidAddress(address))
|
||||||
|
throw ApiExceptionFactory.INSTANCE.createException(request, ApiError.INVALID_ADDRESS);
|
||||||
|
|
||||||
|
try (final Repository repository = RepositoryManager.getRepository()) {
|
||||||
|
|
||||||
|
if (Settings.getInstance().isLite()) {
|
||||||
|
throw ApiExceptionFactory.INSTANCE.createException(request, ApiError.UNAUTHORIZED);
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
Optional<String> primaryName = repository.getNameRepository().getPrimaryName(address);
|
||||||
|
|
||||||
|
if(primaryName.isPresent()) {
|
||||||
|
return new NameSummary(new NameData(primaryName.get(), address));
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
return new NameSummary((new NameData(null, address)));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} catch (DataException e) {
|
||||||
|
throw ApiExceptionFactory.INSTANCE.createException(request, ApiError.REPOSITORY_ISSUE, e);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
@GET
|
@GET
|
||||||
@Path("/{name}")
|
@Path("/{name}")
|
||||||
@Operation(
|
@Operation(
|
||||||
|
@ -1092,25 +1092,4 @@ public class AdminResource {
|
|||||||
|
|
||||||
return info;
|
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);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
@ -71,33 +71,33 @@ public class RenderResource {
|
|||||||
@Path("/signature/{signature}")
|
@Path("/signature/{signature}")
|
||||||
@SecurityRequirement(name = "apiKey")
|
@SecurityRequirement(name = "apiKey")
|
||||||
public HttpServletResponse getIndexBySignature(@PathParam("signature") String signature,
|
public HttpServletResponse getIndexBySignature(@PathParam("signature") String signature,
|
||||||
@QueryParam("theme") String theme) {
|
@QueryParam("theme") String theme, @QueryParam("lang") String lang) {
|
||||||
if (!Settings.getInstance().isQDNAuthBypassEnabled())
|
if (!Settings.getInstance().isQDNAuthBypassEnabled())
|
||||||
Security.requirePriorAuthorization(request, signature, Service.WEBSITE, null);
|
Security.requirePriorAuthorization(request, signature, Service.WEBSITE, null);
|
||||||
|
|
||||||
return this.get(signature, ResourceIdType.SIGNATURE, null, null, "/", null, "/render/signature", true, true, theme);
|
return this.get(signature, ResourceIdType.SIGNATURE, null, null, "/", null, "/render/signature", true, true, theme, lang);
|
||||||
}
|
}
|
||||||
|
|
||||||
@GET
|
@GET
|
||||||
@Path("/signature/{signature}/{path:.*}")
|
@Path("/signature/{signature}/{path:.*}")
|
||||||
@SecurityRequirement(name = "apiKey")
|
@SecurityRequirement(name = "apiKey")
|
||||||
public HttpServletResponse getPathBySignature(@PathParam("signature") String signature, @PathParam("path") String inPath,
|
public HttpServletResponse getPathBySignature(@PathParam("signature") String signature, @PathParam("path") String inPath,
|
||||||
@QueryParam("theme") String theme) {
|
@QueryParam("theme") String theme, @QueryParam("lang") String lang) {
|
||||||
if (!Settings.getInstance().isQDNAuthBypassEnabled())
|
if (!Settings.getInstance().isQDNAuthBypassEnabled())
|
||||||
Security.requirePriorAuthorization(request, signature, Service.WEBSITE, null);
|
Security.requirePriorAuthorization(request, signature, Service.WEBSITE, null);
|
||||||
|
|
||||||
return this.get(signature, ResourceIdType.SIGNATURE, null, null, inPath,null, "/render/signature", true, true, theme);
|
return this.get(signature, ResourceIdType.SIGNATURE, null, null, inPath,null, "/render/signature", true, true, theme, lang);
|
||||||
}
|
}
|
||||||
|
|
||||||
@GET
|
@GET
|
||||||
@Path("/hash/{hash}")
|
@Path("/hash/{hash}")
|
||||||
@SecurityRequirement(name = "apiKey")
|
@SecurityRequirement(name = "apiKey")
|
||||||
public HttpServletResponse getIndexByHash(@PathParam("hash") String hash58, @QueryParam("secret") String secret58,
|
public HttpServletResponse getIndexByHash(@PathParam("hash") String hash58, @QueryParam("secret") String secret58,
|
||||||
@QueryParam("theme") String theme) {
|
@QueryParam("theme") String theme, @QueryParam("lang") String lang) {
|
||||||
if (!Settings.getInstance().isQDNAuthBypassEnabled())
|
if (!Settings.getInstance().isQDNAuthBypassEnabled())
|
||||||
Security.requirePriorAuthorization(request, hash58, Service.WEBSITE, null);
|
Security.requirePriorAuthorization(request, hash58, Service.WEBSITE, null);
|
||||||
|
|
||||||
return this.get(hash58, ResourceIdType.FILE_HASH, Service.ARBITRARY_DATA, null, "/", secret58, "/render/hash", true, false, theme);
|
return this.get(hash58, ResourceIdType.FILE_HASH, Service.ARBITRARY_DATA, null, "/", secret58, "/render/hash", true, false, theme, lang);
|
||||||
}
|
}
|
||||||
|
|
||||||
@GET
|
@GET
|
||||||
@ -105,11 +105,11 @@ public class RenderResource {
|
|||||||
@SecurityRequirement(name = "apiKey")
|
@SecurityRequirement(name = "apiKey")
|
||||||
public HttpServletResponse getPathByHash(@PathParam("hash") String hash58, @PathParam("path") String inPath,
|
public HttpServletResponse getPathByHash(@PathParam("hash") String hash58, @PathParam("path") String inPath,
|
||||||
@QueryParam("secret") String secret58,
|
@QueryParam("secret") String secret58,
|
||||||
@QueryParam("theme") String theme) {
|
@QueryParam("theme") String theme, @QueryParam("lang") String lang) {
|
||||||
if (!Settings.getInstance().isQDNAuthBypassEnabled())
|
if (!Settings.getInstance().isQDNAuthBypassEnabled())
|
||||||
Security.requirePriorAuthorization(request, hash58, Service.WEBSITE, null);
|
Security.requirePriorAuthorization(request, hash58, Service.WEBSITE, null);
|
||||||
|
|
||||||
return this.get(hash58, ResourceIdType.FILE_HASH, Service.ARBITRARY_DATA, null, inPath, secret58, "/render/hash", true, false, theme);
|
return this.get(hash58, ResourceIdType.FILE_HASH, Service.ARBITRARY_DATA, null, inPath, secret58, "/render/hash", true, false, theme, lang);
|
||||||
}
|
}
|
||||||
|
|
||||||
@GET
|
@GET
|
||||||
@ -119,12 +119,12 @@ public class RenderResource {
|
|||||||
@PathParam("name") String name,
|
@PathParam("name") String name,
|
||||||
@PathParam("path") String inPath,
|
@PathParam("path") String inPath,
|
||||||
@QueryParam("identifier") String identifier,
|
@QueryParam("identifier") String identifier,
|
||||||
@QueryParam("theme") String theme) {
|
@QueryParam("theme") String theme, @QueryParam("lang") String lang) {
|
||||||
if (!Settings.getInstance().isQDNAuthBypassEnabled())
|
if (!Settings.getInstance().isQDNAuthBypassEnabled())
|
||||||
Security.requirePriorAuthorization(request, name, service, null);
|
Security.requirePriorAuthorization(request, name, service, null);
|
||||||
|
|
||||||
String prefix = String.format("/render/%s", service);
|
String prefix = String.format("/render/%s", service);
|
||||||
return this.get(name, ResourceIdType.NAME, service, identifier, inPath, null, prefix, true, true, theme);
|
return this.get(name, ResourceIdType.NAME, service, identifier, inPath, null, prefix, true, true, theme, lang);
|
||||||
}
|
}
|
||||||
|
|
||||||
@GET
|
@GET
|
||||||
@ -133,18 +133,18 @@ public class RenderResource {
|
|||||||
public HttpServletResponse getIndexByName(@PathParam("service") Service service,
|
public HttpServletResponse getIndexByName(@PathParam("service") Service service,
|
||||||
@PathParam("name") String name,
|
@PathParam("name") String name,
|
||||||
@QueryParam("identifier") String identifier,
|
@QueryParam("identifier") String identifier,
|
||||||
@QueryParam("theme") String theme) {
|
@QueryParam("theme") String theme, @QueryParam("lang") String lang) {
|
||||||
if (!Settings.getInstance().isQDNAuthBypassEnabled())
|
if (!Settings.getInstance().isQDNAuthBypassEnabled())
|
||||||
Security.requirePriorAuthorization(request, name, service, null);
|
Security.requirePriorAuthorization(request, name, service, null);
|
||||||
|
|
||||||
String prefix = String.format("/render/%s", service);
|
String prefix = String.format("/render/%s", service);
|
||||||
return this.get(name, ResourceIdType.NAME, service, identifier, "/", null, prefix, true, true, theme);
|
return this.get(name, ResourceIdType.NAME, service, identifier, "/", null, prefix, true, true, theme, lang);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
private HttpServletResponse get(String resourceId, ResourceIdType resourceIdType, Service service, String identifier,
|
private HttpServletResponse get(String resourceId, ResourceIdType resourceIdType, Service service, String identifier,
|
||||||
String inPath, String secret58, String prefix, boolean includeResourceIdInPrefix, boolean async, String theme) {
|
String inPath, String secret58, String prefix, boolean includeResourceIdInPrefix, boolean async, String theme, String lang) {
|
||||||
|
|
||||||
ArbitraryDataRenderer renderer = new ArbitraryDataRenderer(resourceId, resourceIdType, service, identifier, inPath,
|
ArbitraryDataRenderer renderer = new ArbitraryDataRenderer(resourceId, resourceIdType, service, identifier, inPath,
|
||||||
secret58, prefix, includeResourceIdInPrefix, async, "render", request, response, context);
|
secret58, prefix, includeResourceIdInPrefix, async, "render", request, response, context);
|
||||||
@ -152,6 +152,9 @@ public class RenderResource {
|
|||||||
if (theme != null) {
|
if (theme != null) {
|
||||||
renderer.setTheme(theme);
|
renderer.setTheme(theme);
|
||||||
}
|
}
|
||||||
|
if (lang != null) {
|
||||||
|
renderer.setLang(lang);
|
||||||
|
}
|
||||||
return renderer.render();
|
return renderer.render();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -0,0 +1,83 @@
|
|||||||
|
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.data.crosschain.UnsignedFeeEvent;
|
||||||
|
import org.qortal.event.Event;
|
||||||
|
import org.qortal.event.EventBus;
|
||||||
|
import org.qortal.event.FeeWaitingEvent;
|
||||||
|
import org.qortal.event.Listener;
|
||||||
|
|
||||||
|
import java.io.IOException;
|
||||||
|
import java.io.StringWriter;
|
||||||
|
|
||||||
|
@WebSocket
|
||||||
|
@SuppressWarnings("serial")
|
||||||
|
public class UnsignedFeesSocket extends ApiWebSocket implements Listener {
|
||||||
|
|
||||||
|
private static final Logger LOGGER = LogManager.getLogger(UnsignedFeesSocket.class);
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void configure(WebSocketServletFactory factory) {
|
||||||
|
LOGGER.info("configure");
|
||||||
|
|
||||||
|
factory.register(UnsignedFeesSocket.class);
|
||||||
|
|
||||||
|
EventBus.INSTANCE.addListener(this);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void listen(Event event) {
|
||||||
|
if (!(event instanceof FeeWaitingEvent))
|
||||||
|
return;
|
||||||
|
|
||||||
|
for (Session session : getSessions()) {
|
||||||
|
FeeWaitingEvent feeWaitingEvent = (FeeWaitingEvent) event;
|
||||||
|
sendUnsignedFeeEvent(session, new UnsignedFeeEvent(feeWaitingEvent.isPositive(), feeWaitingEvent.getAddress()));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
@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 sendUnsignedFeeEvent(Session session, UnsignedFeeEvent unsignedFeeEvent) {
|
||||||
|
StringWriter stringWriter = new StringWriter();
|
||||||
|
|
||||||
|
try {
|
||||||
|
marshall(stringWriter, unsignedFeeEvent);
|
||||||
|
|
||||||
|
session.getRemote().sendStringByFuture(stringWriter.toString());
|
||||||
|
} catch (IOException | WebSocketException e) {
|
||||||
|
// No output this time
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
@ -4,6 +4,7 @@ import org.qortal.repository.DataException;
|
|||||||
import org.qortal.utils.Base58;
|
import org.qortal.utils.Base58;
|
||||||
|
|
||||||
import java.io.IOException;
|
import java.io.IOException;
|
||||||
|
import java.io.InputStream;
|
||||||
import java.nio.charset.StandardCharsets;
|
import java.nio.charset.StandardCharsets;
|
||||||
import java.nio.file.FileSystem;
|
import java.nio.file.FileSystem;
|
||||||
import java.nio.file.Files;
|
import java.nio.file.Files;
|
||||||
@ -25,7 +26,11 @@ public class ArbitraryDataDigest {
|
|||||||
}
|
}
|
||||||
|
|
||||||
public void compute() throws IOException, DataException {
|
public void compute() throws IOException, DataException {
|
||||||
List<Path> allPaths = Files.walk(path).filter(Files::isRegularFile).sorted().collect(Collectors.toList());
|
List<Path> allPaths = Files.walk(path)
|
||||||
|
.filter(Files::isRegularFile)
|
||||||
|
.sorted()
|
||||||
|
.collect(Collectors.toList());
|
||||||
|
|
||||||
Path basePathAbsolute = this.path.toAbsolutePath();
|
Path basePathAbsolute = this.path.toAbsolutePath();
|
||||||
|
|
||||||
MessageDigest sha256;
|
MessageDigest sha256;
|
||||||
@ -48,25 +53,27 @@ public class ArbitraryDataDigest {
|
|||||||
|
|
||||||
// Account for \ VS / : Linux VS Windows
|
// Account for \ VS / : Linux VS Windows
|
||||||
String pathString = relativePath.toString();
|
String pathString = relativePath.toString();
|
||||||
|
|
||||||
if (relativePath.getFileSystem().toString().contains("Windows")) {
|
if (relativePath.getFileSystem().toString().contains("Windows")) {
|
||||||
pathString = pathString.replace("\\", "/");
|
pathString = pathString.replace("\\", "/");
|
||||||
}
|
}
|
||||||
|
|
||||||
// Hash path
|
// Hash path
|
||||||
byte[] filePathBytes = pathString.getBytes(StandardCharsets.UTF_8);
|
byte[] filePathBytes = pathString.getBytes(StandardCharsets.UTF_8);
|
||||||
System.out.printf("Path: %s \n", pathString);
|
|
||||||
System.out.printf("Path Byte Array: %s \n", Arrays.toString(filePathBytes));
|
|
||||||
sha256.update(filePathBytes);
|
sha256.update(filePathBytes);
|
||||||
|
|
||||||
// Hash contents
|
try (InputStream in = Files.newInputStream(path)) {
|
||||||
byte[] fileContent = Files.readAllBytes(path);
|
byte[] buffer = new byte[65536]; // 64 KB
|
||||||
System.out.printf("File Content: %s \n", Arrays.toString(fileContent));
|
int bytesRead;
|
||||||
sha256.update(fileContent);
|
while ((bytesRead = in.read(buffer)) != -1) {
|
||||||
|
sha256.update(buffer, 0, bytesRead);
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
this.hash = sha256.digest();
|
this.hash = sha256.digest();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
public boolean isHashValid(byte[] hash) {
|
public boolean isHashValid(byte[] hash) {
|
||||||
return Arrays.equals(hash, this.hash);
|
return Arrays.equals(hash, this.hash);
|
||||||
}
|
}
|
||||||
|
@ -52,7 +52,7 @@ public class ArbitraryDataFile {
|
|||||||
|
|
||||||
private static final Logger LOGGER = LogManager.getLogger(ArbitraryDataFile.class);
|
private static final Logger LOGGER = LogManager.getLogger(ArbitraryDataFile.class);
|
||||||
|
|
||||||
public static final long MAX_FILE_SIZE = 500 * 1024 * 1024; // 500MiB
|
public static final long MAX_FILE_SIZE = 2L * 1024 * 1024 * 1024; // 2 GiB
|
||||||
protected static final int MAX_CHUNK_SIZE = 1 * 1024 * 1024; // 1MiB
|
protected static final int MAX_CHUNK_SIZE = 1 * 1024 * 1024; // 1MiB
|
||||||
public static final int CHUNK_SIZE = 512 * 1024; // 0.5MiB
|
public static final int CHUNK_SIZE = 512 * 1024; // 0.5MiB
|
||||||
public static int SHORT_DIGEST_LENGTH = 8;
|
public static int SHORT_DIGEST_LENGTH = 8;
|
||||||
|
@ -37,6 +37,7 @@ public class ArbitraryDataRenderer {
|
|||||||
private final Service service;
|
private final Service service;
|
||||||
private final String identifier;
|
private final String identifier;
|
||||||
private String theme = "light";
|
private String theme = "light";
|
||||||
|
private String lang = "en";
|
||||||
private String inPath;
|
private String inPath;
|
||||||
private final String secret58;
|
private final String secret58;
|
||||||
private final String prefix;
|
private final String prefix;
|
||||||
@ -166,9 +167,9 @@ public class ArbitraryDataRenderer {
|
|||||||
if (HTMLParser.isHtmlFile(filename)) {
|
if (HTMLParser.isHtmlFile(filename)) {
|
||||||
// HTML file - needs to be parsed
|
// HTML file - needs to be parsed
|
||||||
byte[] data = Files.readAllBytes(filePath); // TODO: limit file size that can be read into memory
|
byte[] data = Files.readAllBytes(filePath); // TODO: limit file size that can be read into memory
|
||||||
HTMLParser htmlParser = new HTMLParser(resourceId, inPath, prefix, includeResourceIdInPrefix, data, qdnContext, service, identifier, theme, usingCustomRouting);
|
HTMLParser htmlParser = new HTMLParser(resourceId, inPath, prefix, includeResourceIdInPrefix, data, qdnContext, service, identifier, theme, usingCustomRouting, lang);
|
||||||
htmlParser.addAdditionalHeaderTags();
|
htmlParser.addAdditionalHeaderTags();
|
||||||
response.addHeader("Content-Security-Policy", "default-src 'self' 'unsafe-inline' 'unsafe-eval'; font-src 'self' data:; media-src 'self' data: blob:; img-src 'self' data: blob:; connect-src 'self' wss:;");
|
response.addHeader("Content-Security-Policy", "default-src 'self' 'unsafe-inline' 'unsafe-eval'; font-src 'self' data:; media-src 'self' data: blob:; img-src 'self' data: blob:; connect-src 'self' wss: blob:;");
|
||||||
response.setContentType(context.getMimeType(filename));
|
response.setContentType(context.getMimeType(filename));
|
||||||
response.setContentLength(htmlParser.getData().length);
|
response.setContentLength(htmlParser.getData().length);
|
||||||
response.getOutputStream().write(htmlParser.getData());
|
response.getOutputStream().write(htmlParser.getData());
|
||||||
@ -256,5 +257,8 @@ public class ArbitraryDataRenderer {
|
|||||||
public void setTheme(String theme) {
|
public void setTheme(String theme) {
|
||||||
this.theme = theme;
|
this.theme = theme;
|
||||||
}
|
}
|
||||||
|
public void setLang(String lang) {
|
||||||
|
this.lang = lang;
|
||||||
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
@ -29,6 +29,7 @@ import org.qortal.utils.FilesystemUtils;
|
|||||||
import org.qortal.utils.NTP;
|
import org.qortal.utils.NTP;
|
||||||
|
|
||||||
import java.io.IOException;
|
import java.io.IOException;
|
||||||
|
import java.nio.file.Files;
|
||||||
import java.nio.file.Path;
|
import java.nio.file.Path;
|
||||||
import java.util.ArrayList;
|
import java.util.ArrayList;
|
||||||
import java.util.List;
|
import java.util.List;
|
||||||
@ -197,7 +198,7 @@ public class ArbitraryDataTransactionBuilder {
|
|||||||
|
|
||||||
// We can't use PATCH for on-chain data because this requires the .qortal directory, which can't be put on chain
|
// We can't use PATCH for on-chain data because this requires the .qortal directory, which can't be put on chain
|
||||||
final boolean isSingleFileResource = FilesystemUtils.isSingleFileResource(this.path, false);
|
final boolean isSingleFileResource = FilesystemUtils.isSingleFileResource(this.path, false);
|
||||||
final boolean shouldUseOnChainData = (isSingleFileResource && AES.getEncryptedFileSize(FilesystemUtils.getSingleFileContents(path).length) <= ArbitraryTransaction.MAX_DATA_SIZE);
|
final boolean shouldUseOnChainData = (isSingleFileResource && AES.getEncryptedFileSize(Files.size(path)) <= ArbitraryTransaction.MAX_DATA_SIZE);
|
||||||
if (shouldUseOnChainData) {
|
if (shouldUseOnChainData) {
|
||||||
LOGGER.info("Data size is small enough to go on chain - using PUT");
|
LOGGER.info("Data size is small enough to go on chain - using PUT");
|
||||||
return Method.PUT;
|
return Method.PUT;
|
||||||
@ -245,7 +246,7 @@ public class ArbitraryDataTransactionBuilder {
|
|||||||
|
|
||||||
// Single file resources are handled differently, especially for very small data payloads, as these go on chain
|
// Single file resources are handled differently, especially for very small data payloads, as these go on chain
|
||||||
final boolean isSingleFileResource = FilesystemUtils.isSingleFileResource(path, false);
|
final boolean isSingleFileResource = FilesystemUtils.isSingleFileResource(path, false);
|
||||||
final boolean shouldUseOnChainData = (isSingleFileResource && AES.getEncryptedFileSize(FilesystemUtils.getSingleFileContents(path).length) <= ArbitraryTransaction.MAX_DATA_SIZE);
|
final boolean shouldUseOnChainData = (isSingleFileResource && AES.getEncryptedFileSize(Files.size(path)) <= ArbitraryTransaction.MAX_DATA_SIZE);
|
||||||
|
|
||||||
// Use zip compression if data isn't going on chain
|
// Use zip compression if data isn't going on chain
|
||||||
Compression compression = shouldUseOnChainData ? Compression.NONE : Compression.ZIP;
|
Compression compression = shouldUseOnChainData ? Compression.NONE : Compression.ZIP;
|
||||||
|
@ -62,7 +62,17 @@ public enum Service {
|
|||||||
|
|
||||||
// Custom validation function to require an index HTML file in the root directory
|
// Custom validation function to require an index HTML file in the root directory
|
||||||
List<String> fileNames = ArbitraryDataRenderer.indexFiles();
|
List<String> fileNames = ArbitraryDataRenderer.indexFiles();
|
||||||
String[] files = path.toFile().list();
|
List<String> files;
|
||||||
|
|
||||||
|
// single files are paackaged differently
|
||||||
|
if( path.toFile().isFile() ) {
|
||||||
|
files = new ArrayList<>(1);
|
||||||
|
files.add(path.getFileName().toString());
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
files = new ArrayList<>(Arrays.asList(path.toFile().list()));
|
||||||
|
}
|
||||||
|
|
||||||
if (files != null) {
|
if (files != null) {
|
||||||
for (String file : files) {
|
for (String file : files) {
|
||||||
Path fileName = Paths.get(file).getFileName();
|
Path fileName = Paths.get(file).getFileName();
|
||||||
|
@ -1640,6 +1640,8 @@ public class Block {
|
|||||||
SelfSponsorshipAlgoV2Block.processAccountPenalties(this);
|
SelfSponsorshipAlgoV2Block.processAccountPenalties(this);
|
||||||
} else if (this.blockData.getHeight() == BlockChain.getInstance().getSelfSponsorshipAlgoV3Height()) {
|
} else if (this.blockData.getHeight() == BlockChain.getInstance().getSelfSponsorshipAlgoV3Height()) {
|
||||||
SelfSponsorshipAlgoV3Block.processAccountPenalties(this);
|
SelfSponsorshipAlgoV3Block.processAccountPenalties(this);
|
||||||
|
} else if (this.blockData.getHeight() == BlockChain.getInstance().getMultipleNamesPerAccountHeight()) {
|
||||||
|
PrimaryNamesBlock.processNames(this.repository);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -1721,11 +1723,19 @@ public class Block {
|
|||||||
accountData.setBlocksMinted(accountData.getBlocksMinted() + 1);
|
accountData.setBlocksMinted(accountData.getBlocksMinted() + 1);
|
||||||
LOGGER.trace(() -> String.format("Block minter %s up to %d minted block%s", accountData.getAddress(), accountData.getBlocksMinted(), (accountData.getBlocksMinted() != 1 ? "s" : "")));
|
LOGGER.trace(() -> String.format("Block minter %s up to %d minted block%s", accountData.getAddress(), accountData.getBlocksMinted(), (accountData.getBlocksMinted() != 1 ? "s" : "")));
|
||||||
|
|
||||||
final int effectiveBlocksMinted = accountData.getBlocksMinted() + accountData.getBlocksMintedAdjustment() + accountData.getBlocksMintedPenalty();
|
int blocksMintedAdjustment
|
||||||
|
=
|
||||||
|
(this.blockData.getHeight() > BlockChain.getInstance().getMintedBlocksAdjustmentRemovalHeight())
|
||||||
|
?
|
||||||
|
0
|
||||||
|
:
|
||||||
|
accountData.getBlocksMintedAdjustment();
|
||||||
|
|
||||||
|
final int effectiveBlocksMinted = accountData.getBlocksMinted() + blocksMintedAdjustment + accountData.getBlocksMintedPenalty();
|
||||||
|
|
||||||
for (int newLevel = maximumLevel; newLevel >= 0; --newLevel)
|
for (int newLevel = maximumLevel; newLevel >= 0; --newLevel)
|
||||||
if (effectiveBlocksMinted >= cumulativeBlocksByLevel.get(newLevel)) {
|
if (effectiveBlocksMinted >= cumulativeBlocksByLevel.get(newLevel)) {
|
||||||
if (newLevel > accountData.getLevel()) {
|
if (newLevel != accountData.getLevel()) {
|
||||||
// Account has increased in level!
|
// Account has increased in level!
|
||||||
accountData.setLevel(newLevel);
|
accountData.setLevel(newLevel);
|
||||||
bumpedAccounts.put(accountData.getAddress(), newLevel);
|
bumpedAccounts.put(accountData.getAddress(), newLevel);
|
||||||
@ -1952,6 +1962,8 @@ public class Block {
|
|||||||
SelfSponsorshipAlgoV2Block.orphanAccountPenalties(this);
|
SelfSponsorshipAlgoV2Block.orphanAccountPenalties(this);
|
||||||
} else if (this.blockData.getHeight() == BlockChain.getInstance().getSelfSponsorshipAlgoV3Height()) {
|
} else if (this.blockData.getHeight() == BlockChain.getInstance().getSelfSponsorshipAlgoV3Height()) {
|
||||||
SelfSponsorshipAlgoV3Block.orphanAccountPenalties(this);
|
SelfSponsorshipAlgoV3Block.orphanAccountPenalties(this);
|
||||||
|
} else if (this.blockData.getHeight() == BlockChain.getInstance().getMultipleNamesPerAccountHeight()) {
|
||||||
|
PrimaryNamesBlock.orphanNames( this.repository );
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -2127,11 +2139,19 @@ public class Block {
|
|||||||
accountData.setBlocksMinted(accountData.getBlocksMinted() - 1);
|
accountData.setBlocksMinted(accountData.getBlocksMinted() - 1);
|
||||||
LOGGER.trace(() -> String.format("Block minter %s down to %d minted block%s", accountData.getAddress(), accountData.getBlocksMinted(), (accountData.getBlocksMinted() != 1 ? "s" : "")));
|
LOGGER.trace(() -> String.format("Block minter %s down to %d minted block%s", accountData.getAddress(), accountData.getBlocksMinted(), (accountData.getBlocksMinted() != 1 ? "s" : "")));
|
||||||
|
|
||||||
final int effectiveBlocksMinted = accountData.getBlocksMinted() + accountData.getBlocksMintedAdjustment() + accountData.getBlocksMintedPenalty();
|
int blocksMintedAdjustment
|
||||||
|
=
|
||||||
|
(this.blockData.getHeight() -1 > BlockChain.getInstance().getMintedBlocksAdjustmentRemovalHeight())
|
||||||
|
?
|
||||||
|
0
|
||||||
|
:
|
||||||
|
accountData.getBlocksMintedAdjustment();
|
||||||
|
|
||||||
|
final int effectiveBlocksMinted = accountData.getBlocksMinted() + blocksMintedAdjustment + accountData.getBlocksMintedPenalty();
|
||||||
|
|
||||||
for (int newLevel = maximumLevel; newLevel >= 0; --newLevel)
|
for (int newLevel = maximumLevel; newLevel >= 0; --newLevel)
|
||||||
if (effectiveBlocksMinted >= cumulativeBlocksByLevel.get(newLevel)) {
|
if (effectiveBlocksMinted >= cumulativeBlocksByLevel.get(newLevel)) {
|
||||||
if (newLevel < accountData.getLevel()) {
|
if (newLevel != accountData.getLevel()) {
|
||||||
// Account has decreased in level!
|
// Account has decreased in level!
|
||||||
accountData.setLevel(newLevel);
|
accountData.setLevel(newLevel);
|
||||||
repository.getAccountRepository().setLevel(accountData);
|
repository.getAccountRepository().setLevel(accountData);
|
||||||
|
@ -92,7 +92,9 @@ public class BlockChain {
|
|||||||
adminsReplaceFoundersHeight,
|
adminsReplaceFoundersHeight,
|
||||||
nullGroupMembershipHeight,
|
nullGroupMembershipHeight,
|
||||||
ignoreLevelForRewardShareHeight,
|
ignoreLevelForRewardShareHeight,
|
||||||
adminQueryFixHeight
|
adminQueryFixHeight,
|
||||||
|
multipleNamesPerAccountHeight,
|
||||||
|
mintedBlocksAdjustmentRemovalHeight
|
||||||
}
|
}
|
||||||
|
|
||||||
// Custom transaction fees
|
// Custom transaction fees
|
||||||
@ -112,7 +114,8 @@ public class BlockChain {
|
|||||||
/** Whether to use legacy, broken RIPEMD160 implementation when converting public keys to addresses. */
|
/** Whether to use legacy, broken RIPEMD160 implementation when converting public keys to addresses. */
|
||||||
private boolean useBrokenMD160ForAddresses = false;
|
private boolean useBrokenMD160ForAddresses = false;
|
||||||
|
|
||||||
/** Whether only one registered name is allowed per account. */
|
/** This should get ignored and overwritten in the oneNamePerAccount(int blockchainHeight) method,
|
||||||
|
* because it is based on block height, not based on the genesis block.*/
|
||||||
private boolean oneNamePerAccount = false;
|
private boolean oneNamePerAccount = false;
|
||||||
|
|
||||||
/** Checkpoints */
|
/** Checkpoints */
|
||||||
@ -474,8 +477,9 @@ public class BlockChain {
|
|||||||
return this.useBrokenMD160ForAddresses;
|
return this.useBrokenMD160ForAddresses;
|
||||||
}
|
}
|
||||||
|
|
||||||
public boolean oneNamePerAccount() {
|
public boolean oneNamePerAccount(int blockchainHeight) {
|
||||||
return this.oneNamePerAccount;
|
// this is not set on a simple blockchain setting, it is based on a feature trigger height
|
||||||
|
return blockchainHeight < this.getMultipleNamesPerAccountHeight();
|
||||||
}
|
}
|
||||||
|
|
||||||
public List<Checkpoint> getCheckpoints() {
|
public List<Checkpoint> getCheckpoints() {
|
||||||
@ -688,6 +692,14 @@ public class BlockChain {
|
|||||||
return this.featureTriggers.get(FeatureTrigger.adminQueryFixHeight.name()).intValue();
|
return this.featureTriggers.get(FeatureTrigger.adminQueryFixHeight.name()).intValue();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public int getMultipleNamesPerAccountHeight() {
|
||||||
|
return this.featureTriggers.get(FeatureTrigger.multipleNamesPerAccountHeight.name()).intValue();
|
||||||
|
}
|
||||||
|
|
||||||
|
public int getMintedBlocksAdjustmentRemovalHeight() {
|
||||||
|
return this.featureTriggers.get(FeatureTrigger.mintedBlocksAdjustmentRemovalHeight.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) {
|
||||||
|
47
src/main/java/org/qortal/block/PrimaryNamesBlock.java
Normal file
47
src/main/java/org/qortal/block/PrimaryNamesBlock.java
Normal file
@ -0,0 +1,47 @@
|
|||||||
|
package org.qortal.block;
|
||||||
|
|
||||||
|
import org.qortal.account.Account;
|
||||||
|
import org.qortal.api.resource.TransactionsResource;
|
||||||
|
import org.qortal.data.naming.NameData;
|
||||||
|
import org.qortal.repository.DataException;
|
||||||
|
import org.qortal.repository.Repository;
|
||||||
|
|
||||||
|
import java.util.Set;
|
||||||
|
import java.util.stream.Collectors;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Class PrimaryNamesBlock
|
||||||
|
*/
|
||||||
|
public class PrimaryNamesBlock {
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Process Primary Names
|
||||||
|
*
|
||||||
|
* @param repository
|
||||||
|
* @throws DataException
|
||||||
|
*/
|
||||||
|
public static void processNames(Repository repository) throws DataException {
|
||||||
|
|
||||||
|
Set<String> addressesWithNames
|
||||||
|
= repository.getNameRepository().getAllNames().stream()
|
||||||
|
.map(NameData::getOwner).collect(Collectors.toSet());
|
||||||
|
|
||||||
|
// for each address with a name, set primary name to the address
|
||||||
|
for( String address : addressesWithNames ) {
|
||||||
|
|
||||||
|
Account account = new Account(repository, address);
|
||||||
|
account.resetPrimaryName(TransactionsResource.ConfirmationStatus.CONFIRMED);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Orphan the Primary Names Block
|
||||||
|
*
|
||||||
|
* @param repository
|
||||||
|
* @throws DataException
|
||||||
|
*/
|
||||||
|
public static void orphanNames(Repository repository) throws DataException {
|
||||||
|
|
||||||
|
repository.getNameRepository().clearPrimaryNames();
|
||||||
|
}
|
||||||
|
}
|
@ -46,6 +46,7 @@ import org.qortal.utils.*;
|
|||||||
|
|
||||||
import javax.xml.bind.annotation.XmlAccessType;
|
import javax.xml.bind.annotation.XmlAccessType;
|
||||||
import javax.xml.bind.annotation.XmlAccessorType;
|
import javax.xml.bind.annotation.XmlAccessorType;
|
||||||
|
|
||||||
import java.awt.TrayIcon.MessageType;
|
import java.awt.TrayIcon.MessageType;
|
||||||
import java.io.File;
|
import java.io.File;
|
||||||
import java.io.FileNotFoundException;
|
import java.io.FileNotFoundException;
|
||||||
@ -53,6 +54,7 @@ import java.io.IOException;
|
|||||||
import java.io.InputStream;
|
import java.io.InputStream;
|
||||||
import java.net.InetSocketAddress;
|
import java.net.InetSocketAddress;
|
||||||
import java.net.UnknownHostException;
|
import java.net.UnknownHostException;
|
||||||
|
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.security.SecureRandom;
|
import java.security.SecureRandom;
|
||||||
@ -70,11 +72,10 @@ import java.util.concurrent.locks.ReentrantLock;
|
|||||||
import java.util.function.Function;
|
import java.util.function.Function;
|
||||||
import java.util.function.Predicate;
|
import java.util.function.Predicate;
|
||||||
import java.util.stream.Collectors;
|
import java.util.stream.Collectors;
|
||||||
|
import java.util.stream.Stream;
|
||||||
|
|
||||||
public class Controller extends Thread {
|
public class Controller extends Thread {
|
||||||
|
|
||||||
public static HSQLDBRepositoryFactory REPOSITORY_FACTORY;
|
|
||||||
|
|
||||||
static {
|
static {
|
||||||
// This must go before any calls to LogManager/Logger
|
// This must go before any calls to LogManager/Logger
|
||||||
System.setProperty("log4j2.formatMsgNoLookups", "true");
|
System.setProperty("log4j2.formatMsgNoLookups", "true");
|
||||||
@ -396,6 +397,9 @@ public class Controller extends Thread {
|
|||||||
|
|
||||||
Controller.newInstance(args);
|
Controller.newInstance(args);
|
||||||
|
|
||||||
|
|
||||||
|
cleanChunkUploadTempDir(); // cleanup leftover chunks from streaming to disk
|
||||||
|
|
||||||
LOGGER.info("Starting NTP");
|
LOGGER.info("Starting NTP");
|
||||||
Long ntpOffset = Settings.getInstance().getTestNtpOffset();
|
Long ntpOffset = Settings.getInstance().getTestNtpOffset();
|
||||||
if (ntpOffset != null)
|
if (ntpOffset != null)
|
||||||
@ -405,8 +409,8 @@ public class Controller extends Thread {
|
|||||||
|
|
||||||
LOGGER.info("Starting repository");
|
LOGGER.info("Starting repository");
|
||||||
try {
|
try {
|
||||||
REPOSITORY_FACTORY = new HSQLDBRepositoryFactory(getRepositoryUrl());
|
HSQLDBRepositoryFactory repositoryFactory = new HSQLDBRepositoryFactory(getRepositoryUrl());
|
||||||
RepositoryManager.setRepositoryFactory(REPOSITORY_FACTORY);
|
RepositoryManager.setRepositoryFactory(repositoryFactory);
|
||||||
RepositoryManager.setRequestedCheckpoint(Boolean.TRUE);
|
RepositoryManager.setRequestedCheckpoint(Boolean.TRUE);
|
||||||
|
|
||||||
try (final Repository repository = RepositoryManager.getRepository()) {
|
try (final Repository repository = RepositoryManager.getRepository()) {
|
||||||
@ -560,6 +564,9 @@ public class Controller extends Thread {
|
|||||||
LOGGER.info("Starting online accounts manager");
|
LOGGER.info("Starting online accounts manager");
|
||||||
OnlineAccountsManager.getInstance().start();
|
OnlineAccountsManager.getInstance().start();
|
||||||
|
|
||||||
|
LOGGER.info("Starting foreign fees manager");
|
||||||
|
ForeignFeesManager.getInstance().start();
|
||||||
|
|
||||||
LOGGER.info("Starting transaction importer");
|
LOGGER.info("Starting transaction importer");
|
||||||
TransactionImporter.getInstance().start();
|
TransactionImporter.getInstance().start();
|
||||||
|
|
||||||
@ -1130,6 +1137,9 @@ public class Controller extends Thread {
|
|||||||
LOGGER.info("Shutting down online accounts manager");
|
LOGGER.info("Shutting down online accounts manager");
|
||||||
OnlineAccountsManager.getInstance().shutdown();
|
OnlineAccountsManager.getInstance().shutdown();
|
||||||
|
|
||||||
|
LOGGER.info("Shutting down foreign fees manager");
|
||||||
|
ForeignFeesManager.getInstance().shutdown();
|
||||||
|
|
||||||
LOGGER.info("Shutting down transaction importer");
|
LOGGER.info("Shutting down transaction importer");
|
||||||
TransactionImporter.getInstance().shutdown();
|
TransactionImporter.getInstance().shutdown();
|
||||||
|
|
||||||
@ -1474,6 +1484,14 @@ public class Controller extends Thread {
|
|||||||
OnlineAccountsManager.getInstance().onNetworkOnlineAccountsV3Message(peer, message);
|
OnlineAccountsManager.getInstance().onNetworkOnlineAccountsV3Message(peer, message);
|
||||||
break;
|
break;
|
||||||
|
|
||||||
|
case GET_FOREIGN_FEES:
|
||||||
|
ForeignFeesManager.getInstance().onNetworkGetForeignFeesMessage(peer, message);
|
||||||
|
break;
|
||||||
|
|
||||||
|
case FOREIGN_FEES:
|
||||||
|
ForeignFeesManager.getInstance().onNetworkForeignFeesMessage(peer, message);
|
||||||
|
break;
|
||||||
|
|
||||||
case GET_ARBITRARY_DATA:
|
case GET_ARBITRARY_DATA:
|
||||||
// Not currently supported
|
// Not currently supported
|
||||||
break;
|
break;
|
||||||
@ -2160,6 +2178,24 @@ public class Controller extends Thread {
|
|||||||
return now - offset;
|
return now - offset;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private static void cleanChunkUploadTempDir() {
|
||||||
|
Path uploadsTemp = Paths.get("uploads-temp");
|
||||||
|
if (!Files.exists(uploadsTemp)) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
try (Stream<Path> paths = Files.walk(uploadsTemp)) {
|
||||||
|
paths.sorted(Comparator.reverseOrder())
|
||||||
|
.map(Path::toFile)
|
||||||
|
.forEach(File::delete);
|
||||||
|
|
||||||
|
LOGGER.info("Cleaned up all temporary uploads in {}", uploadsTemp);
|
||||||
|
} catch (IOException e) {
|
||||||
|
LOGGER.warn("Failed to clean up uploads-temp directory", e);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
public StatsSnapshot getStatsSnapshot() {
|
public StatsSnapshot getStatsSnapshot() {
|
||||||
return this.stats;
|
return this.stats;
|
||||||
}
|
}
|
||||||
|
1202
src/main/java/org/qortal/controller/ForeignFeesManager.java
Normal file
1202
src/main/java/org/qortal/controller/ForeignFeesManager.java
Normal file
File diff suppressed because it is too large
Load Diff
@ -2,6 +2,7 @@ package org.qortal.controller;
|
|||||||
|
|
||||||
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.arbitrary.PeerMessage;
|
||||||
import org.qortal.data.block.BlockData;
|
import org.qortal.data.block.BlockData;
|
||||||
import org.qortal.data.transaction.TransactionData;
|
import org.qortal.data.transaction.TransactionData;
|
||||||
import org.qortal.network.Network;
|
import org.qortal.network.Network;
|
||||||
@ -20,7 +21,11 @@ import org.qortal.utils.Base58;
|
|||||||
import org.qortal.utils.NTP;
|
import org.qortal.utils.NTP;
|
||||||
|
|
||||||
import java.util.*;
|
import java.util.*;
|
||||||
|
import java.util.concurrent.Executors;
|
||||||
|
import java.util.concurrent.ScheduledExecutorService;
|
||||||
|
import java.util.concurrent.TimeUnit;
|
||||||
import java.util.concurrent.locks.ReentrantLock;
|
import java.util.concurrent.locks.ReentrantLock;
|
||||||
|
import java.util.function.Function;
|
||||||
import java.util.stream.Collectors;
|
import java.util.stream.Collectors;
|
||||||
|
|
||||||
public class TransactionImporter extends Thread {
|
public class TransactionImporter extends Thread {
|
||||||
@ -50,6 +55,10 @@ public class TransactionImporter extends Thread {
|
|||||||
/** Cached list of unconfirmed transactions, used when counting per creator. This is replaced regularly */
|
/** Cached list of unconfirmed transactions, used when counting per creator. This is replaced regularly */
|
||||||
public static List<TransactionData> unconfirmedTransactionsCache = null;
|
public static List<TransactionData> unconfirmedTransactionsCache = null;
|
||||||
|
|
||||||
|
public TransactionImporter() {
|
||||||
|
signatureMessageScheduler.scheduleAtFixedRate(this::processNetworkTransactionSignaturesMessage, 60, 1, TimeUnit.SECONDS);
|
||||||
|
getTransactionMessageScheduler.scheduleAtFixedRate(this::processNetworkGetTransactionMessages, 60, 1, TimeUnit.SECONDS);
|
||||||
|
}
|
||||||
|
|
||||||
public static synchronized TransactionImporter getInstance() {
|
public static synchronized TransactionImporter getInstance() {
|
||||||
if (instance == null) {
|
if (instance == null) {
|
||||||
@ -371,36 +380,104 @@ public class TransactionImporter extends Thread {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// List to collect messages
|
||||||
|
private final List<PeerMessage> getTransactionMessageList = new ArrayList<>();
|
||||||
|
// Lock to synchronize access to the list
|
||||||
|
private final Object getTransactionMessageLock = new Object();
|
||||||
|
|
||||||
|
// Scheduled executor service to process messages every second
|
||||||
|
private final ScheduledExecutorService getTransactionMessageScheduler = Executors.newScheduledThreadPool(1);
|
||||||
|
|
||||||
public void onNetworkGetTransactionMessage(Peer peer, Message message) {
|
public void onNetworkGetTransactionMessage(Peer peer, Message message) {
|
||||||
GetTransactionMessage getTransactionMessage = (GetTransactionMessage) message;
|
|
||||||
|
synchronized (getTransactionMessageLock) {
|
||||||
|
getTransactionMessageList.add(new PeerMessage(peer, message));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private void processNetworkGetTransactionMessages() {
|
||||||
|
|
||||||
|
try {
|
||||||
|
List<PeerMessage> messagesToProcess;
|
||||||
|
synchronized (getTransactionMessageLock) {
|
||||||
|
messagesToProcess = new ArrayList<>(getTransactionMessageList);
|
||||||
|
getTransactionMessageList.clear();
|
||||||
|
}
|
||||||
|
|
||||||
|
if( messagesToProcess.isEmpty() ) return;
|
||||||
|
|
||||||
|
Map<String, PeerMessage> peerMessageBySignature58 = new HashMap<>(messagesToProcess.size());
|
||||||
|
|
||||||
|
for( PeerMessage peerMessage : messagesToProcess ) {
|
||||||
|
GetTransactionMessage getTransactionMessage = (GetTransactionMessage) peerMessage.getMessage();
|
||||||
byte[] signature = getTransactionMessage.getSignature();
|
byte[] signature = getTransactionMessage.getSignature();
|
||||||
|
|
||||||
try (final Repository repository = RepositoryManager.getRepository()) {
|
peerMessageBySignature58.put(Base58.encode(signature), peerMessage);
|
||||||
|
}
|
||||||
|
|
||||||
// Firstly check the sig-valid transactions that are currently queued for import
|
// Firstly check the sig-valid transactions that are currently queued for import
|
||||||
TransactionData transactionData = this.getCachedSigValidTransactions().stream()
|
Map<String, TransactionData> transactionsCachedBySignature58
|
||||||
.filter(t -> Arrays.equals(signature, t.getSignature()))
|
= this.getCachedSigValidTransactions().stream()
|
||||||
.findFirst().orElse(null);
|
.collect(Collectors.toMap(t -> Base58.encode(t.getSignature()), Function.identity()));
|
||||||
|
|
||||||
if (transactionData == null) {
|
Map<Boolean, List<Map.Entry<String, PeerMessage>>> transactionsCachedBySignature58Partition
|
||||||
|
= peerMessageBySignature58.entrySet().stream()
|
||||||
|
.collect(Collectors.partitioningBy(entry -> transactionsCachedBySignature58.containsKey(entry.getKey())));
|
||||||
|
|
||||||
|
List<byte[]> signaturesNeeded
|
||||||
|
= transactionsCachedBySignature58Partition.get(false).stream()
|
||||||
|
.map(Map.Entry::getValue)
|
||||||
|
.map(PeerMessage::getMessage)
|
||||||
|
.map(message -> (GetTransactionMessage) message)
|
||||||
|
.map(GetTransactionMessage::getSignature)
|
||||||
|
.collect(Collectors.toList());
|
||||||
|
|
||||||
|
// transaction found in the import queue
|
||||||
|
Map<String, TransactionData> transactionsToSendBySignature58 = new HashMap<>(messagesToProcess.size());
|
||||||
|
for( Map.Entry<String, PeerMessage> entry : transactionsCachedBySignature58Partition.get(true)) {
|
||||||
|
transactionsToSendBySignature58.put(entry.getKey(), transactionsCachedBySignature58.get(entry.getKey()));
|
||||||
|
}
|
||||||
|
|
||||||
|
if( !signaturesNeeded.isEmpty() ) {
|
||||||
// Not found in import queue, so try the database
|
// Not found in import queue, so try the database
|
||||||
transactionData = repository.getTransactionRepository().fromSignature(signature);
|
try (final Repository repository = RepositoryManager.getRepository()) {
|
||||||
|
transactionsToSendBySignature58.putAll(
|
||||||
|
repository.getTransactionRepository().fromSignatures(signaturesNeeded).stream()
|
||||||
|
.collect(Collectors.toMap(transactionData -> Base58.encode(transactionData.getSignature()), Function.identity()))
|
||||||
|
);
|
||||||
|
} catch (DataException e) {
|
||||||
|
LOGGER.error(e.getMessage(), e);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if (transactionData == null) {
|
for( final Map.Entry<String, TransactionData> entry : transactionsToSendBySignature58.entrySet() ) {
|
||||||
// Still not found - so we don't have this transaction
|
|
||||||
LOGGER.debug(() -> String.format("Ignoring GET_TRANSACTION request from peer %s for unknown transaction %s", peer, Base58.encode(signature)));
|
PeerMessage peerMessage = peerMessageBySignature58.get(entry.getKey());
|
||||||
// Send no response at all???
|
final Message message = peerMessage.getMessage();
|
||||||
return;
|
final Peer peer = peerMessage.getPeer();
|
||||||
|
|
||||||
|
Runnable sendTransactionMessageRunner = () -> sendTransactionMessage(entry.getKey(), entry.getValue(), message, peer);
|
||||||
|
Thread sendTransactionMessageThread = new Thread(sendTransactionMessageRunner);
|
||||||
|
sendTransactionMessageThread.start();
|
||||||
|
}
|
||||||
|
} catch (Exception e) {
|
||||||
|
LOGGER.error(e.getMessage(),e);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
Message transactionMessage = new TransactionMessage(transactionData);
|
private static void sendTransactionMessage(String signature58, TransactionData data, Message message, Peer peer) {
|
||||||
|
try {
|
||||||
|
Message transactionMessage = new TransactionMessage(data);
|
||||||
transactionMessage.setId(message.getId());
|
transactionMessage.setId(message.getId());
|
||||||
|
|
||||||
if (!peer.sendMessage(transactionMessage))
|
if (!peer.sendMessage(transactionMessage))
|
||||||
peer.disconnect("failed to send transaction");
|
peer.disconnect("failed to send transaction");
|
||||||
} catch (DataException e) {
|
}
|
||||||
LOGGER.error(String.format("Repository issue while sending transaction %s to peer %s", Base58.encode(signature), peer), e);
|
catch (TransformationException e) {
|
||||||
} catch (TransformationException e) {
|
LOGGER.error(String.format("Serialization issue while sending transaction %s to peer %s", signature58, peer), e);
|
||||||
LOGGER.error(String.format("Serialization issue while sending transaction %s to peer %s", Base58.encode(signature), peer), e);
|
}
|
||||||
|
catch (Exception e) {
|
||||||
|
LOGGER.error(e.getMessage(), e);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -421,11 +498,37 @@ public class TransactionImporter extends Thread {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// List to collect messages
|
||||||
|
private final List<PeerMessage> signatureMessageList = new ArrayList<>();
|
||||||
|
// Lock to synchronize access to the list
|
||||||
|
private final Object signatureMessageLock = new Object();
|
||||||
|
|
||||||
|
// Scheduled executor service to process messages every second
|
||||||
|
private final ScheduledExecutorService signatureMessageScheduler = Executors.newScheduledThreadPool(1);
|
||||||
|
|
||||||
public void onNetworkTransactionSignaturesMessage(Peer peer, Message message) {
|
public void onNetworkTransactionSignaturesMessage(Peer peer, Message message) {
|
||||||
TransactionSignaturesMessage transactionSignaturesMessage = (TransactionSignaturesMessage) message;
|
synchronized (signatureMessageLock) {
|
||||||
|
signatureMessageList.add(new PeerMessage(peer, message));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public void processNetworkTransactionSignaturesMessage() {
|
||||||
|
|
||||||
|
try {
|
||||||
|
List<PeerMessage> messagesToProcess;
|
||||||
|
synchronized (signatureMessageLock) {
|
||||||
|
messagesToProcess = new ArrayList<>(signatureMessageList);
|
||||||
|
signatureMessageList.clear();
|
||||||
|
}
|
||||||
|
|
||||||
|
Map<String, byte[]> signatureBySignature58 = new HashMap<>(messagesToProcess.size() * 10);
|
||||||
|
Map<String, Peer> peerBySignature58 = new HashMap<>( messagesToProcess.size() * 10 );
|
||||||
|
|
||||||
|
for( PeerMessage peerMessage : messagesToProcess ) {
|
||||||
|
|
||||||
|
TransactionSignaturesMessage transactionSignaturesMessage = (TransactionSignaturesMessage) peerMessage.getMessage();
|
||||||
List<byte[]> signatures = transactionSignaturesMessage.getSignatures();
|
List<byte[]> signatures = transactionSignaturesMessage.getSignatures();
|
||||||
|
|
||||||
try (final Repository repository = RepositoryManager.getRepository()) {
|
|
||||||
for (byte[] signature : signatures) {
|
for (byte[] signature : signatures) {
|
||||||
String signature58 = Base58.encode(signature);
|
String signature58 = Base58.encode(signature);
|
||||||
if (invalidUnconfirmedTransactions.containsKey(signature58)) {
|
if (invalidUnconfirmedTransactions.containsKey(signature58)) {
|
||||||
@ -436,29 +539,45 @@ public class TransactionImporter extends Thread {
|
|||||||
|
|
||||||
// Ignore if this transaction is in the queue
|
// Ignore if this transaction is in the queue
|
||||||
if (incomingTransactionQueueContains(signature)) {
|
if (incomingTransactionQueueContains(signature)) {
|
||||||
LOGGER.trace(() -> String.format("Ignoring existing queued transaction %s from peer %s", Base58.encode(signature), peer));
|
LOGGER.trace(() -> String.format("Ignoring existing queued transaction %s from peer %s", Base58.encode(signature), peerMessage.getPeer()));
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Do we have it already? (Before requesting transaction data itself)
|
signatureBySignature58.put(signature58, signature);
|
||||||
if (repository.getTransactionRepository().exists(signature)) {
|
peerBySignature58.put(signature58, peerMessage.getPeer());
|
||||||
LOGGER.trace(() -> String.format("Ignoring existing transaction %s from peer %s", Base58.encode(signature), peer));
|
}
|
||||||
continue;
|
}
|
||||||
|
|
||||||
|
if( !signatureBySignature58.isEmpty() ) {
|
||||||
|
try (final Repository repository = RepositoryManager.getRepository()) {
|
||||||
|
|
||||||
|
// remove signatures in db already
|
||||||
|
repository.getTransactionRepository()
|
||||||
|
.fromSignatures(new ArrayList<>(signatureBySignature58.values())).stream()
|
||||||
|
.map(TransactionData::getSignature)
|
||||||
|
.map(signature -> Base58.encode(signature))
|
||||||
|
.forEach(signature58 -> signatureBySignature58.remove(signature58));
|
||||||
|
} catch (DataException e) {
|
||||||
|
LOGGER.error(String.format("Repository issue while processing unconfirmed transactions from peer"), e);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Check isInterrupted() here and exit fast
|
// Check isInterrupted() here and exit fast
|
||||||
if (Thread.currentThread().isInterrupted())
|
if (Thread.currentThread().isInterrupted())
|
||||||
return;
|
return;
|
||||||
|
|
||||||
|
for (Map.Entry<String, byte[]> entry : signatureBySignature58.entrySet()) {
|
||||||
|
|
||||||
|
Peer peer = peerBySignature58.get(entry.getKey());
|
||||||
|
|
||||||
// Fetch actual transaction data from peer
|
// Fetch actual transaction data from peer
|
||||||
Message getTransactionMessage = new GetTransactionMessage(signature);
|
Message getTransactionMessage = new GetTransactionMessage(entry.getValue());
|
||||||
if (!peer.sendMessage(getTransactionMessage)) {
|
if (peer != null && !peer.sendMessage(getTransactionMessage)) {
|
||||||
peer.disconnect("failed to request transaction");
|
peer.disconnect("failed to request transaction");
|
||||||
return;
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
} catch (DataException e) {
|
} catch (Exception e) {
|
||||||
LOGGER.error(String.format("Repository issue while processing unconfirmed transactions from peer %s", peer), e);
|
LOGGER.error(e.getMessage(), e);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -25,6 +25,10 @@ import org.qortal.utils.NTP;
|
|||||||
import org.qortal.utils.Triple;
|
import org.qortal.utils.Triple;
|
||||||
|
|
||||||
import java.util.*;
|
import java.util.*;
|
||||||
|
import java.util.concurrent.Executors;
|
||||||
|
import java.util.concurrent.ScheduledExecutorService;
|
||||||
|
import java.util.concurrent.TimeUnit;
|
||||||
|
import java.util.stream.Collectors;
|
||||||
|
|
||||||
import static org.qortal.controller.arbitrary.ArbitraryDataFileManager.MAX_FILE_HASH_RESPONSES;
|
import static org.qortal.controller.arbitrary.ArbitraryDataFileManager.MAX_FILE_HASH_RESPONSES;
|
||||||
|
|
||||||
@ -73,6 +77,8 @@ public class ArbitraryDataFileListManager {
|
|||||||
|
|
||||||
|
|
||||||
private ArbitraryDataFileListManager() {
|
private ArbitraryDataFileListManager() {
|
||||||
|
getArbitraryDataFileListMessageScheduler.scheduleAtFixedRate(this::processNetworkGetArbitraryDataFileListMessage, 60, 1, TimeUnit.SECONDS);
|
||||||
|
arbitraryDataFileListMessageScheduler.scheduleAtFixedRate(this::processNetworkArbitraryDataFileListMessage, 60, 1, TimeUnit.SECONDS);
|
||||||
}
|
}
|
||||||
|
|
||||||
public static ArbitraryDataFileListManager getInstance() {
|
public static ArbitraryDataFileListManager getInstance() {
|
||||||
@ -400,7 +406,7 @@ public class ArbitraryDataFileListManager {
|
|||||||
String signature58 = Base58.encode(signature);
|
String signature58 = Base58.encode(signature);
|
||||||
for (Iterator<Map.Entry<Integer, Triple<String, Peer, Long>>> it = arbitraryDataFileListRequests.entrySet().iterator(); it.hasNext();) {
|
for (Iterator<Map.Entry<Integer, Triple<String, Peer, Long>>> it = arbitraryDataFileListRequests.entrySet().iterator(); it.hasNext();) {
|
||||||
Map.Entry<Integer, Triple<String, Peer, Long>> entry = it.next();
|
Map.Entry<Integer, Triple<String, Peer, Long>> entry = it.next();
|
||||||
if (entry == null || entry.getKey() == null || entry.getValue() != null) {
|
if (entry == null || entry.getKey() == null || entry.getValue() == null) {
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
if (Objects.equals(entry.getValue().getA(), signature58)) {
|
if (Objects.equals(entry.getValue().getA(), signature58)) {
|
||||||
@ -413,12 +419,46 @@ public class ArbitraryDataFileListManager {
|
|||||||
|
|
||||||
// Network handlers
|
// Network handlers
|
||||||
|
|
||||||
|
// List to collect messages
|
||||||
|
private final List<PeerMessage> arbitraryDataFileListMessageList = new ArrayList<>();
|
||||||
|
// Lock to synchronize access to the list
|
||||||
|
private final Object arbitraryDataFileListMessageLock = new Object();
|
||||||
|
|
||||||
|
// Scheduled executor service to process messages every second
|
||||||
|
private final ScheduledExecutorService arbitraryDataFileListMessageScheduler = Executors.newScheduledThreadPool(1);
|
||||||
|
|
||||||
public void onNetworkArbitraryDataFileListMessage(Peer peer, Message message) {
|
public void onNetworkArbitraryDataFileListMessage(Peer peer, Message message) {
|
||||||
// Don't process if QDN is disabled
|
// Don't process if QDN is disabled
|
||||||
if (!Settings.getInstance().isQdnEnabled()) {
|
if (!Settings.getInstance().isQdnEnabled()) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
synchronized (arbitraryDataFileListMessageLock) {
|
||||||
|
arbitraryDataFileListMessageList.add(new PeerMessage(peer, message));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private void processNetworkArbitraryDataFileListMessage() {
|
||||||
|
|
||||||
|
try {
|
||||||
|
List<PeerMessage> messagesToProcess;
|
||||||
|
synchronized (arbitraryDataFileListMessageLock) {
|
||||||
|
messagesToProcess = new ArrayList<>(arbitraryDataFileListMessageList);
|
||||||
|
arbitraryDataFileListMessageList.clear();
|
||||||
|
}
|
||||||
|
|
||||||
|
if (messagesToProcess.isEmpty()) return;
|
||||||
|
|
||||||
|
Map<String, PeerMessage> peerMessageBySignature58 = new HashMap<>(messagesToProcess.size());
|
||||||
|
Map<String, byte[]> signatureBySignature58 = new HashMap<>(messagesToProcess.size());
|
||||||
|
Map<String, Boolean> isRelayRequestBySignature58 = new HashMap<>(messagesToProcess.size());
|
||||||
|
Map<String, List<byte[]>> hashesBySignature58 = new HashMap<>(messagesToProcess.size());
|
||||||
|
Map<String, Triple<String, Peer, Long>> requestBySignature58 = new HashMap<>(messagesToProcess.size());
|
||||||
|
|
||||||
|
for (PeerMessage peerMessage : messagesToProcess) {
|
||||||
|
Peer peer = peerMessage.getPeer();
|
||||||
|
Message message = peerMessage.getMessage();
|
||||||
|
|
||||||
ArbitraryDataFileListMessage arbitraryDataFileListMessage = (ArbitraryDataFileListMessage) message;
|
ArbitraryDataFileListMessage arbitraryDataFileListMessage = (ArbitraryDataFileListMessage) message;
|
||||||
LOGGER.debug("Received hash list from peer {} with {} hashes", peer, arbitraryDataFileListMessage.getHashes().size());
|
LOGGER.debug("Received hash list from peer {} with {} hashes", peer, arbitraryDataFileListMessage.getHashes().size());
|
||||||
|
|
||||||
@ -432,7 +472,7 @@ public class ArbitraryDataFileListManager {
|
|||||||
// Do we have a pending request for this data?
|
// Do we have a pending request for this data?
|
||||||
Triple<String, Peer, Long> request = arbitraryDataFileListRequests.get(message.getId());
|
Triple<String, Peer, Long> request = arbitraryDataFileListRequests.get(message.getId());
|
||||||
if (request == null || request.getA() == null) {
|
if (request == null || request.getA() == null) {
|
||||||
return;
|
continue;
|
||||||
}
|
}
|
||||||
boolean isRelayRequest = (request.getB() != null);
|
boolean isRelayRequest = (request.getB() != null);
|
||||||
|
|
||||||
@ -440,43 +480,55 @@ public class ArbitraryDataFileListManager {
|
|||||||
byte[] signature = arbitraryDataFileListMessage.getSignature();
|
byte[] signature = arbitraryDataFileListMessage.getSignature();
|
||||||
String signature58 = Base58.encode(signature);
|
String signature58 = Base58.encode(signature);
|
||||||
if (!request.getA().equals(signature58)) {
|
if (!request.getA().equals(signature58)) {
|
||||||
return;
|
continue;
|
||||||
}
|
}
|
||||||
|
|
||||||
List<byte[]> hashes = arbitraryDataFileListMessage.getHashes();
|
List<byte[]> hashes = arbitraryDataFileListMessage.getHashes();
|
||||||
if (hashes == null || hashes.isEmpty()) {
|
if (hashes == null || hashes.isEmpty()) {
|
||||||
return;
|
continue;
|
||||||
}
|
}
|
||||||
|
|
||||||
ArbitraryTransactionData arbitraryTransactionData = null;
|
peerMessageBySignature58.put(signature58, peerMessage);
|
||||||
|
signatureBySignature58.put(signature58, signature);
|
||||||
|
isRelayRequestBySignature58.put(signature58, isRelayRequest);
|
||||||
|
hashesBySignature58.put(signature58, hashes);
|
||||||
|
requestBySignature58.put(signature58, request);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (signatureBySignature58.isEmpty()) return;
|
||||||
|
|
||||||
|
List<ArbitraryTransactionData> arbitraryTransactionDataList;
|
||||||
|
|
||||||
// Check transaction exists and hashes are correct
|
// Check transaction exists and hashes are correct
|
||||||
try (final Repository repository = RepositoryManager.getRepository()) {
|
try (final Repository repository = RepositoryManager.getRepository()) {
|
||||||
TransactionData transactionData = repository.getTransactionRepository().fromSignature(signature);
|
arbitraryTransactionDataList
|
||||||
if (!(transactionData instanceof ArbitraryTransactionData))
|
= repository.getTransactionRepository()
|
||||||
|
.fromSignatures(new ArrayList<>(signatureBySignature58.values())).stream()
|
||||||
|
.filter(data -> data instanceof ArbitraryTransactionData)
|
||||||
|
.map(data -> (ArbitraryTransactionData) data)
|
||||||
|
.collect(Collectors.toList());
|
||||||
|
} catch (DataException e) {
|
||||||
|
LOGGER.error(String.format("Repository issue while finding arbitrary transaction data list"), e);
|
||||||
return;
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
arbitraryTransactionData = (ArbitraryTransactionData) transactionData;
|
for (ArbitraryTransactionData arbitraryTransactionData : arbitraryTransactionDataList) {
|
||||||
|
|
||||||
// // Load data file(s)
|
byte[] signature = arbitraryTransactionData.getSignature();
|
||||||
// ArbitraryDataFile arbitraryDataFile = ArbitraryDataFile.fromTransactionData(arbitraryTransactionData);
|
String signature58 = Base58.encode(signature);
|
||||||
//
|
|
||||||
// // Check all hashes exist
|
|
||||||
// for (byte[] hash : hashes) {
|
|
||||||
// //LOGGER.debug("Received hash {}", Base58.encode(hash));
|
|
||||||
// if (!arbitraryDataFile.containsChunk(hash)) {
|
|
||||||
// // Check the hash against the complete file
|
|
||||||
// if (!Arrays.equals(arbitraryDataFile.getHash(), hash)) {
|
|
||||||
// LOGGER.info("Received non-matching chunk hash {} for signature {}. This could happen if we haven't obtained the metadata file yet.", Base58.encode(hash), signature58);
|
|
||||||
// return;
|
|
||||||
// }
|
|
||||||
// }
|
|
||||||
// }
|
|
||||||
|
|
||||||
|
List<byte[]> hashes = hashesBySignature58.get(signature58);
|
||||||
|
|
||||||
|
PeerMessage peerMessage = peerMessageBySignature58.get(signature58);
|
||||||
|
Peer peer = peerMessage.getPeer();
|
||||||
|
Message message = peerMessage.getMessage();
|
||||||
|
|
||||||
|
ArbitraryDataFileListMessage arbitraryDataFileListMessage = (ArbitraryDataFileListMessage) message;
|
||||||
|
|
||||||
|
Boolean isRelayRequest = isRelayRequestBySignature58.get(signature58);
|
||||||
if (!isRelayRequest || !Settings.getInstance().isRelayModeEnabled()) {
|
if (!isRelayRequest || !Settings.getInstance().isRelayModeEnabled()) {
|
||||||
Long now = NTP.getTime();
|
Long now = NTP.getTime();
|
||||||
|
|
||||||
if (ArbitraryDataFileManager.getInstance().arbitraryDataFileHashResponses.size() < MAX_FILE_HASH_RESPONSES) {
|
|
||||||
// Keep track of the hashes this peer reports to have access to
|
// Keep track of the hashes this peer reports to have access to
|
||||||
for (byte[] hash : hashes) {
|
for (byte[] hash : hashes) {
|
||||||
String hash58 = Base58.encode(hash);
|
String hash58 = Base58.encode(hash);
|
||||||
@ -487,8 +539,7 @@ public class ArbitraryDataFileListManager {
|
|||||||
ArbitraryFileListResponseInfo responseInfo = new ArbitraryFileListResponseInfo(hash58, signature58,
|
ArbitraryFileListResponseInfo responseInfo = new ArbitraryFileListResponseInfo(hash58, signature58,
|
||||||
peer, now, arbitraryDataFileListMessage.getRequestTime(), requestHops);
|
peer, now, arbitraryDataFileListMessage.getRequestTime(), requestHops);
|
||||||
|
|
||||||
ArbitraryDataFileManager.getInstance().arbitraryDataFileHashResponses.add(responseInfo);
|
ArbitraryDataFileManager.getInstance().addResponse(responseInfo);
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// Keep track of the source peer, for direct connections
|
// Keep track of the source peer, for direct connections
|
||||||
@ -498,14 +549,12 @@ public class ArbitraryDataFileListManager {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
} catch (DataException e) {
|
|
||||||
LOGGER.error(String.format("Repository issue while finding arbitrary transaction data list for peer %s", peer), e);
|
|
||||||
}
|
|
||||||
|
|
||||||
// Forwarding
|
// Forwarding
|
||||||
if (isRelayRequest && Settings.getInstance().isRelayModeEnabled()) {
|
if (isRelayRequest && Settings.getInstance().isRelayModeEnabled()) {
|
||||||
|
|
||||||
boolean isBlocked = (arbitraryTransactionData == null || ListUtils.isNameBlocked(arbitraryTransactionData.getName()));
|
boolean isBlocked = (arbitraryTransactionData == null || ListUtils.isNameBlocked(arbitraryTransactionData.getName()));
|
||||||
if (!isBlocked) {
|
if (!isBlocked) {
|
||||||
|
Triple<String, Peer, Long> request = requestBySignature58.get(signature58);
|
||||||
Peer requestingPeer = request.getB();
|
Peer requestingPeer = request.getB();
|
||||||
if (requestingPeer != null) {
|
if (requestingPeer != null) {
|
||||||
Long requestTime = arbitraryDataFileListMessage.getRequestTime();
|
Long requestTime = arbitraryDataFileListMessage.getRequestTime();
|
||||||
@ -545,6 +594,18 @@ public class ArbitraryDataFileListManager {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
} catch (Exception e) {
|
||||||
|
LOGGER.error(e.getMessage(), e);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// List to collect messages
|
||||||
|
private final List<PeerMessage> getArbitraryDataFileListMessageList = new ArrayList<>();
|
||||||
|
// Lock to synchronize access to the list
|
||||||
|
private final Object getArbitraryDataFileListMessageLock = new Object();
|
||||||
|
|
||||||
|
// Scheduled executor service to process messages every second
|
||||||
|
private final ScheduledExecutorService getArbitraryDataFileListMessageScheduler = Executors.newScheduledThreadPool(1);
|
||||||
|
|
||||||
public void onNetworkGetArbitraryDataFileListMessage(Peer peer, Message message) {
|
public void onNetworkGetArbitraryDataFileListMessage(Peer peer, Message message) {
|
||||||
// Don't respond if QDN is disabled
|
// Don't respond if QDN is disabled
|
||||||
@ -552,8 +613,34 @@ public class ArbitraryDataFileListManager {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
synchronized (getArbitraryDataFileListMessageLock) {
|
||||||
|
getArbitraryDataFileListMessageList.add(new PeerMessage(peer, message));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private void processNetworkGetArbitraryDataFileListMessage() {
|
||||||
|
|
||||||
|
try {
|
||||||
|
List<PeerMessage> messagesToProcess;
|
||||||
|
synchronized (getArbitraryDataFileListMessageLock) {
|
||||||
|
messagesToProcess = new ArrayList<>(getArbitraryDataFileListMessageList);
|
||||||
|
getArbitraryDataFileListMessageList.clear();
|
||||||
|
}
|
||||||
|
|
||||||
|
if (messagesToProcess.isEmpty()) return;
|
||||||
|
|
||||||
|
Map<String, byte[]> signatureBySignature58 = new HashMap<>(messagesToProcess.size());
|
||||||
|
Map<String, List<byte[]>> requestedHashesBySignature58 = new HashMap<>(messagesToProcess.size());
|
||||||
|
Map<String, String> requestingPeerBySignature58 = new HashMap<>(messagesToProcess.size());
|
||||||
|
Map<String, Long> nowBySignature58 = new HashMap<>((messagesToProcess.size()));
|
||||||
|
Map<String, PeerMessage> peerMessageBySignature58 = new HashMap<>(messagesToProcess.size());
|
||||||
|
|
||||||
|
for (PeerMessage messagePeer : messagesToProcess) {
|
||||||
Controller.getInstance().stats.getArbitraryDataFileListMessageStats.requests.incrementAndGet();
|
Controller.getInstance().stats.getArbitraryDataFileListMessageStats.requests.incrementAndGet();
|
||||||
|
|
||||||
|
Message message = messagePeer.message;
|
||||||
|
Peer peer = messagePeer.peer;
|
||||||
|
|
||||||
GetArbitraryDataFileListMessage getArbitraryDataFileListMessage = (GetArbitraryDataFileListMessage) message;
|
GetArbitraryDataFileListMessage getArbitraryDataFileListMessage = (GetArbitraryDataFileListMessage) message;
|
||||||
byte[] signature = getArbitraryDataFileListMessage.getSignature();
|
byte[] signature = getArbitraryDataFileListMessage.getSignature();
|
||||||
String signature58 = Base58.encode(signature);
|
String signature58 = Base58.encode(signature);
|
||||||
@ -563,7 +650,7 @@ public class ArbitraryDataFileListManager {
|
|||||||
// If we've seen this request recently, then ignore
|
// If we've seen this request recently, then ignore
|
||||||
if (arbitraryDataFileListRequests.putIfAbsent(message.getId(), newEntry) != null) {
|
if (arbitraryDataFileListRequests.putIfAbsent(message.getId(), newEntry) != null) {
|
||||||
LOGGER.trace("Ignoring hash list request from peer {} for signature {}", peer, signature58);
|
LOGGER.trace("Ignoring hash list request from peer {} for signature {}", peer, signature58);
|
||||||
return;
|
continue;
|
||||||
}
|
}
|
||||||
|
|
||||||
List<byte[]> requestedHashes = getArbitraryDataFileListMessage.getHashes();
|
List<byte[]> requestedHashes = getArbitraryDataFileListMessage.getHashes();
|
||||||
@ -572,25 +659,50 @@ public class ArbitraryDataFileListManager {
|
|||||||
|
|
||||||
if (requestingPeer != null) {
|
if (requestingPeer != null) {
|
||||||
LOGGER.debug("Received hash list request with {} hashes from peer {} (requesting peer {}) for signature {}", hashCount, peer, requestingPeer, signature58);
|
LOGGER.debug("Received hash list request with {} hashes from peer {} (requesting peer {}) for signature {}", hashCount, peer, requestingPeer, signature58);
|
||||||
}
|
} else {
|
||||||
else {
|
|
||||||
LOGGER.debug("Received hash list request with {} hashes from peer {} for signature {}", hashCount, peer, signature58);
|
LOGGER.debug("Received hash list request with {} hashes from peer {} for signature {}", hashCount, peer, signature58);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
signatureBySignature58.put(signature58, signature);
|
||||||
|
requestedHashesBySignature58.put(signature58, requestedHashes);
|
||||||
|
requestingPeerBySignature58.put(signature58, requestingPeer);
|
||||||
|
nowBySignature58.put(signature58, now);
|
||||||
|
peerMessageBySignature58.put(signature58, messagePeer);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (signatureBySignature58.isEmpty()) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
List<byte[]> hashes = new ArrayList<>();
|
List<byte[]> hashes = new ArrayList<>();
|
||||||
ArbitraryTransactionData transactionData = null;
|
|
||||||
boolean allChunksExist = false;
|
boolean allChunksExist = false;
|
||||||
boolean hasMetadata = false;
|
boolean hasMetadata = false;
|
||||||
|
|
||||||
|
List<ArbitraryTransactionData> transactionDataList;
|
||||||
try (final Repository repository = RepositoryManager.getRepository()) {
|
try (final Repository repository = RepositoryManager.getRepository()) {
|
||||||
|
|
||||||
// Firstly we need to lookup this file on chain to get a list of its hashes
|
// Firstly we need to lookup this file on chain to get a list of its hashes
|
||||||
transactionData = (ArbitraryTransactionData)repository.getTransactionRepository().fromSignature(signature);
|
transactionDataList
|
||||||
if (transactionData instanceof ArbitraryTransactionData) {
|
= repository.getTransactionRepository()
|
||||||
|
.fromSignatures(new ArrayList<>(signatureBySignature58.values())).stream()
|
||||||
|
.filter(data -> data instanceof ArbitraryTransactionData)
|
||||||
|
.map(data -> (ArbitraryTransactionData) data)
|
||||||
|
.collect(Collectors.toList());
|
||||||
|
|
||||||
|
} catch (DataException e) {
|
||||||
|
LOGGER.error(String.format("Repository issue while fetching arbitrary file list for peer"), e);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
for (ArbitraryTransactionData transactionData : transactionDataList) {
|
||||||
|
byte[] signature = transactionData.getSignature();
|
||||||
|
String signature58 = Base58.encode(signature);
|
||||||
|
List<byte[]> requestedHashes = requestedHashesBySignature58.get(signature58);
|
||||||
|
|
||||||
// Check if we're even allowed to serve data for this transaction
|
// Check if we're even allowed to serve data for this transaction
|
||||||
if (ArbitraryDataStorageManager.getInstance().canStoreData(transactionData)) {
|
if (ArbitraryDataStorageManager.getInstance().canStoreData(transactionData)) {
|
||||||
|
|
||||||
|
try {
|
||||||
// Load file(s) and add any that exist to the list of hashes
|
// Load file(s) and add any that exist to the list of hashes
|
||||||
ArbitraryDataFile arbitraryDataFile = ArbitraryDataFile.fromTransactionData(transactionData);
|
ArbitraryDataFile arbitraryDataFile = ArbitraryDataFile.fromTransactionData(transactionData);
|
||||||
|
|
||||||
@ -614,6 +726,7 @@ public class ArbitraryDataFileListManager {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
// Assume all chunks exists, unless one can't be found below
|
// Assume all chunks exists, unless one can't be found below
|
||||||
allChunksExist = true;
|
allChunksExist = true;
|
||||||
|
|
||||||
@ -627,11 +740,9 @@ public class ArbitraryDataFileListManager {
|
|||||||
allChunksExist = false;
|
allChunksExist = false;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
} catch (DataException e) {
|
} catch (DataException e) {
|
||||||
LOGGER.error(String.format("Repository issue while fetching arbitrary file list for peer %s", peer), e);
|
LOGGER.error(e.getMessage(), e);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// If the only file we have is the metadata then we shouldn't respond. Most nodes will already have that,
|
// If the only file we have is the metadata then we shouldn't respond. Most nodes will already have that,
|
||||||
@ -640,7 +751,14 @@ public class ArbitraryDataFileListManager {
|
|||||||
hashes.clear();
|
hashes.clear();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
PeerMessage peerMessage = peerMessageBySignature58.get(signature58);
|
||||||
|
Peer peer = peerMessage.getPeer();
|
||||||
|
Message message = peerMessage.getMessage();
|
||||||
|
|
||||||
|
Long now = nowBySignature58.get(signature58);
|
||||||
|
|
||||||
// We should only respond if we have at least one hash
|
// We should only respond if we have at least one hash
|
||||||
|
String requestingPeer = requestingPeerBySignature58.get(signature58);
|
||||||
if (!hashes.isEmpty()) {
|
if (!hashes.isEmpty()) {
|
||||||
|
|
||||||
// Firstly we should keep track of the requesting peer, to allow for potential direct connections later
|
// Firstly we should keep track of the requesting peer, to allow for potential direct connections later
|
||||||
@ -649,7 +767,7 @@ public class ArbitraryDataFileListManager {
|
|||||||
// We have all the chunks, so update requests map to reflect that we've sent it
|
// We have all the chunks, so update requests map to reflect that we've sent it
|
||||||
// There is no need to keep track of the request, as we can serve all the chunks
|
// There is no need to keep track of the request, as we can serve all the chunks
|
||||||
if (allChunksExist) {
|
if (allChunksExist) {
|
||||||
newEntry = new Triple<>(null, null, now);
|
Triple<String, Peer, Long> newEntry = new Triple<>(null, null, now);
|
||||||
arbitraryDataFileListRequests.put(message.getId(), newEntry);
|
arbitraryDataFileListRequests.put(message.getId(), newEntry);
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -670,14 +788,13 @@ public class ArbitraryDataFileListManager {
|
|||||||
if (!peer.sendMessage(arbitraryDataFileListMessage)) {
|
if (!peer.sendMessage(arbitraryDataFileListMessage)) {
|
||||||
LOGGER.debug("Couldn't send list of hashes");
|
LOGGER.debug("Couldn't send list of hashes");
|
||||||
peer.disconnect("failed to send list of hashes");
|
peer.disconnect("failed to send list of hashes");
|
||||||
return;
|
continue;
|
||||||
}
|
}
|
||||||
LOGGER.debug("Sent list of hashes (count: {})", hashes.size());
|
|
||||||
|
|
||||||
if (allChunksExist) {
|
if (allChunksExist) {
|
||||||
// Nothing left to do, so return to prevent any unnecessary forwarding from occurring
|
// Nothing left to do, so return to prevent any unnecessary forwarding from occurring
|
||||||
LOGGER.debug("No need for any forwarding because file list request is fully served");
|
LOGGER.debug("No need for any forwarding because file list request is fully served");
|
||||||
return;
|
continue;
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
@ -687,6 +804,9 @@ public class ArbitraryDataFileListManager {
|
|||||||
if (Settings.getInstance().isRelayModeEnabled() && !isBlocked) {
|
if (Settings.getInstance().isRelayModeEnabled() && !isBlocked) {
|
||||||
// In relay mode - so ask our other peers if they have it
|
// In relay mode - so ask our other peers if they have it
|
||||||
|
|
||||||
|
|
||||||
|
GetArbitraryDataFileListMessage getArbitraryDataFileListMessage = (GetArbitraryDataFileListMessage) message;
|
||||||
|
|
||||||
long requestTime = getArbitraryDataFileListMessage.getRequestTime();
|
long requestTime = getArbitraryDataFileListMessage.getRequestTime();
|
||||||
int requestHops = getArbitraryDataFileListMessage.getRequestHops() + 1;
|
int requestHops = getArbitraryDataFileListMessage.getRequestHops() + 1;
|
||||||
long totalRequestTime = now - requestTime;
|
long totalRequestTime = now - requestTime;
|
||||||
@ -706,15 +826,17 @@ public class ArbitraryDataFileListManager {
|
|||||||
broadcastPeer == peer || Objects.equals(broadcastPeer.getPeerData().getAddress().getHost(), peer.getPeerData().getAddress().getHost()) ? null : relayGetArbitraryDataFileListMessage
|
broadcastPeer == peer || Objects.equals(broadcastPeer.getPeerData().getAddress().getHost(), peer.getPeerData().getAddress().getHost()) ? null : relayGetArbitraryDataFileListMessage
|
||||||
);
|
);
|
||||||
|
|
||||||
}
|
} else {
|
||||||
else {
|
|
||||||
// This relay request has reached the maximum number of allowed hops
|
// This relay request has reached the maximum number of allowed hops
|
||||||
}
|
}
|
||||||
}
|
} else {
|
||||||
else {
|
|
||||||
// This relay request has timed out
|
// This relay request has timed out
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
} catch (Exception e) {
|
||||||
|
LOGGER.error(e.getMessage(), e);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
@ -25,6 +25,8 @@ import java.security.SecureRandom;
|
|||||||
import java.util.*;
|
import java.util.*;
|
||||||
import java.util.concurrent.ExecutorService;
|
import java.util.concurrent.ExecutorService;
|
||||||
import java.util.concurrent.Executors;
|
import java.util.concurrent.Executors;
|
||||||
|
import java.util.concurrent.ScheduledExecutorService;
|
||||||
|
import java.util.concurrent.TimeUnit;
|
||||||
import java.util.stream.Collectors;
|
import java.util.stream.Collectors;
|
||||||
|
|
||||||
public class ArbitraryDataFileManager extends Thread {
|
public class ArbitraryDataFileManager extends Thread {
|
||||||
@ -48,7 +50,7 @@ public class ArbitraryDataFileManager extends Thread {
|
|||||||
/**
|
/**
|
||||||
* List to keep track of any arbitrary data file hash responses
|
* List to keep track of any arbitrary data file hash responses
|
||||||
*/
|
*/
|
||||||
public final List<ArbitraryFileListResponseInfo> arbitraryDataFileHashResponses = Collections.synchronizedList(new ArrayList<>());
|
private final List<ArbitraryFileListResponseInfo> arbitraryDataFileHashResponses = Collections.synchronizedList(new ArrayList<>());
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* List to keep track of peers potentially available for direct connections, based on recent requests
|
* List to keep track of peers potentially available for direct connections, based on recent requests
|
||||||
@ -67,6 +69,7 @@ public class ArbitraryDataFileManager extends Thread {
|
|||||||
|
|
||||||
|
|
||||||
private ArbitraryDataFileManager() {
|
private ArbitraryDataFileManager() {
|
||||||
|
this.arbitraryDataFileHashResponseScheduler.scheduleAtFixedRate( this::processResponses, 60, 1, TimeUnit.SECONDS);
|
||||||
}
|
}
|
||||||
|
|
||||||
public static ArbitraryDataFileManager getInstance() {
|
public static ArbitraryDataFileManager getInstance() {
|
||||||
@ -81,13 +84,6 @@ public class ArbitraryDataFileManager extends Thread {
|
|||||||
Thread.currentThread().setName("Arbitrary Data File Manager");
|
Thread.currentThread().setName("Arbitrary Data File Manager");
|
||||||
|
|
||||||
try {
|
try {
|
||||||
// Use a fixed thread pool to execute the arbitrary data file requests
|
|
||||||
int threadCount = 5;
|
|
||||||
ExecutorService arbitraryDataFileRequestExecutor = Executors.newFixedThreadPool(threadCount);
|
|
||||||
for (int i = 0; i < threadCount; i++) {
|
|
||||||
arbitraryDataFileRequestExecutor.execute(new ArbitraryDataFileRequestThread());
|
|
||||||
}
|
|
||||||
|
|
||||||
while (!isStopping) {
|
while (!isStopping) {
|
||||||
// Nothing to do yet
|
// Nothing to do yet
|
||||||
Thread.sleep(1000);
|
Thread.sleep(1000);
|
||||||
@ -112,7 +108,6 @@ public class ArbitraryDataFileManager extends Thread {
|
|||||||
|
|
||||||
final long relayMinimumTimestamp = now - ArbitraryDataManager.getInstance().ARBITRARY_RELAY_TIMEOUT;
|
final long relayMinimumTimestamp = now - ArbitraryDataManager.getInstance().ARBITRARY_RELAY_TIMEOUT;
|
||||||
arbitraryRelayMap.removeIf(entry -> entry == null || entry.getTimestamp() == null || entry.getTimestamp() < relayMinimumTimestamp);
|
arbitraryRelayMap.removeIf(entry -> entry == null || entry.getTimestamp() == null || entry.getTimestamp() < relayMinimumTimestamp);
|
||||||
arbitraryDataFileHashResponses.removeIf(entry -> entry.getTimestamp() < relayMinimumTimestamp);
|
|
||||||
|
|
||||||
final long directConnectionInfoMinimumTimestamp = now - ArbitraryDataManager.getInstance().ARBITRARY_DIRECT_CONNECTION_INFO_TIMEOUT;
|
final long directConnectionInfoMinimumTimestamp = now - ArbitraryDataManager.getInstance().ARBITRARY_DIRECT_CONNECTION_INFO_TIMEOUT;
|
||||||
directConnectionInfo.removeIf(entry -> entry.getTimestamp() < directConnectionInfoMinimumTimestamp);
|
directConnectionInfo.removeIf(entry -> entry.getTimestamp() < directConnectionInfoMinimumTimestamp);
|
||||||
@ -125,8 +120,7 @@ public class ArbitraryDataFileManager extends Thread {
|
|||||||
|
|
||||||
// Fetch data files by hash
|
// Fetch data files by hash
|
||||||
|
|
||||||
public boolean fetchArbitraryDataFiles(Repository repository,
|
public boolean fetchArbitraryDataFiles(Peer peer,
|
||||||
Peer peer,
|
|
||||||
byte[] signature,
|
byte[] signature,
|
||||||
ArbitraryTransactionData arbitraryTransactionData,
|
ArbitraryTransactionData arbitraryTransactionData,
|
||||||
List<byte[]> hashes) throws DataException {
|
List<byte[]> hashes) throws DataException {
|
||||||
@ -151,16 +145,10 @@ public class ArbitraryDataFileManager extends Thread {
|
|||||||
if (receivedArbitraryDataFile != null) {
|
if (receivedArbitraryDataFile != null) {
|
||||||
LOGGER.debug("Received data file {} from peer {}. Time taken: {} ms", receivedArbitraryDataFile.getHash58(), peer, (endTime-startTime));
|
LOGGER.debug("Received data file {} from peer {}. Time taken: {} ms", receivedArbitraryDataFile.getHash58(), peer, (endTime-startTime));
|
||||||
receivedAtLeastOneFile = true;
|
receivedAtLeastOneFile = true;
|
||||||
|
|
||||||
// Remove this hash from arbitraryDataFileHashResponses now that we have received it
|
|
||||||
arbitraryDataFileHashResponses.remove(hash58);
|
|
||||||
}
|
}
|
||||||
else {
|
else {
|
||||||
LOGGER.debug("Peer {} didn't respond with data file {} for signature {}. Time taken: {} ms", peer, Base58.encode(hash), Base58.encode(signature), (endTime-startTime));
|
LOGGER.debug("Peer {} didn't respond with data file {} for signature {}. Time taken: {} ms", peer, Base58.encode(hash), Base58.encode(signature), (endTime-startTime));
|
||||||
|
|
||||||
// Remove this hash from arbitraryDataFileHashResponses now that we have failed to receive it
|
|
||||||
arbitraryDataFileHashResponses.remove(hash58);
|
|
||||||
|
|
||||||
// Stop asking for files from this peer
|
// Stop asking for files from this peer
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
@ -169,10 +157,6 @@ public class ArbitraryDataFileManager extends Thread {
|
|||||||
LOGGER.trace("Already requesting data file {} for signature {} from peer {}", arbitraryDataFile, Base58.encode(signature), peer);
|
LOGGER.trace("Already requesting data file {} for signature {} from peer {}", arbitraryDataFile, Base58.encode(signature), peer);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
else {
|
|
||||||
// Remove this hash from arbitraryDataFileHashResponses because we have a local copy
|
|
||||||
arbitraryDataFileHashResponses.remove(hash58);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
if (receivedAtLeastOneFile) {
|
if (receivedAtLeastOneFile) {
|
||||||
@ -191,6 +175,38 @@ public class ArbitraryDataFileManager extends Thread {
|
|||||||
return receivedAtLeastOneFile;
|
return receivedAtLeastOneFile;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Lock to synchronize access to the list
|
||||||
|
private final Object arbitraryDataFileHashResponseLock = new Object();
|
||||||
|
|
||||||
|
// Scheduled executor service to process messages every second
|
||||||
|
private final ScheduledExecutorService arbitraryDataFileHashResponseScheduler = Executors.newScheduledThreadPool(1);
|
||||||
|
|
||||||
|
|
||||||
|
public void addResponse( ArbitraryFileListResponseInfo responseInfo ) {
|
||||||
|
|
||||||
|
synchronized (arbitraryDataFileHashResponseLock) {
|
||||||
|
this.arbitraryDataFileHashResponses.add(responseInfo);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private void processResponses() {
|
||||||
|
try {
|
||||||
|
List<ArbitraryFileListResponseInfo> responsesToProcess;
|
||||||
|
synchronized (arbitraryDataFileHashResponseLock) {
|
||||||
|
responsesToProcess = new ArrayList<>(arbitraryDataFileHashResponses);
|
||||||
|
arbitraryDataFileHashResponses.clear();
|
||||||
|
}
|
||||||
|
|
||||||
|
if (responsesToProcess.isEmpty()) return;
|
||||||
|
|
||||||
|
Long now = NTP.getTime();
|
||||||
|
|
||||||
|
ArbitraryDataFileRequestThread.getInstance().processFileHashes(now, responsesToProcess, this);
|
||||||
|
} catch (Exception e) {
|
||||||
|
LOGGER.error(e.getMessage(), e);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
private ArbitraryDataFile fetchArbitraryDataFile(Peer peer, Peer requestingPeer, ArbitraryTransactionData arbitraryTransactionData, byte[] signature, byte[] hash, Message originalMessage) throws DataException {
|
private ArbitraryDataFile fetchArbitraryDataFile(Peer peer, Peer requestingPeer, ArbitraryTransactionData arbitraryTransactionData, byte[] signature, byte[] hash, Message originalMessage) throws DataException {
|
||||||
ArbitraryDataFile existingFile = ArbitraryDataFile.fromHash(hash, signature);
|
ArbitraryDataFile existingFile = ArbitraryDataFile.fromHash(hash, signature);
|
||||||
boolean fileAlreadyExists = existingFile.exists();
|
boolean fileAlreadyExists = existingFile.exists();
|
||||||
@ -212,8 +228,7 @@ public class ArbitraryDataFileManager extends Thread {
|
|||||||
arbitraryDataFileRequests.remove(hash58);
|
arbitraryDataFileRequests.remove(hash58);
|
||||||
LOGGER.trace(String.format("Removed hash %.8s from arbitraryDataFileRequests", hash58));
|
LOGGER.trace(String.format("Removed hash %.8s from arbitraryDataFileRequests", hash58));
|
||||||
|
|
||||||
// We may need to remove the file list request, if we have all the files for this transaction
|
|
||||||
this.handleFileListRequests(signature);
|
|
||||||
|
|
||||||
if (response == null) {
|
if (response == null) {
|
||||||
LOGGER.debug("Received null response from peer {}", peer);
|
LOGGER.debug("Received null response from peer {}", peer);
|
||||||
@ -258,6 +273,9 @@ public class ArbitraryDataFileManager extends Thread {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// We may need to remove the file list request, if we have all the files for this transaction
|
||||||
|
this.handleFileListRequests(signature);
|
||||||
|
|
||||||
return arbitraryDataFile;
|
return arbitraryDataFile;
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -270,10 +288,12 @@ public class ArbitraryDataFileManager extends Thread {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
boolean allChunksExist = ArbitraryTransactionUtils.allChunksExist(arbitraryTransactionData);
|
boolean completeFileExists = ArbitraryTransactionUtils.completeFileExists(arbitraryTransactionData);
|
||||||
|
|
||||||
|
if (completeFileExists) {
|
||||||
|
String signature58 = Base58.encode(arbitraryTransactionData.getSignature());
|
||||||
|
LOGGER.info("All chunks or complete file exist for transaction {}", signature58);
|
||||||
|
|
||||||
if (allChunksExist) {
|
|
||||||
// Update requests map to reflect that we've received all chunks
|
|
||||||
ArbitraryDataFileListManager.getInstance().deleteFileListRequestsForSignature(signature);
|
ArbitraryDataFileListManager.getInstance().deleteFileListRequestsForSignature(signature);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -4,127 +4,179 @@ 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.data.arbitrary.ArbitraryFileListResponseInfo;
|
import org.qortal.data.arbitrary.ArbitraryFileListResponseInfo;
|
||||||
|
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.network.Peer;
|
import org.qortal.network.Peer;
|
||||||
|
import org.qortal.network.message.MessageType;
|
||||||
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.utils.ArbitraryTransactionUtils;
|
import org.qortal.utils.ArbitraryTransactionUtils;
|
||||||
import org.qortal.utils.Base58;
|
import org.qortal.utils.Base58;
|
||||||
import org.qortal.utils.NTP;
|
import org.qortal.utils.NTP;
|
||||||
|
|
||||||
|
import java.net.http.HttpResponse;
|
||||||
|
import java.util.ArrayList;
|
||||||
import java.util.Arrays;
|
import java.util.Arrays;
|
||||||
import java.util.Comparator;
|
import java.util.Comparator;
|
||||||
|
import java.util.HashMap;
|
||||||
import java.util.Iterator;
|
import java.util.Iterator;
|
||||||
|
import java.util.List;
|
||||||
|
import java.util.Map;
|
||||||
|
import java.util.concurrent.ConcurrentHashMap;
|
||||||
|
import java.util.concurrent.Executor;
|
||||||
|
import java.util.concurrent.ExecutorService;
|
||||||
|
import java.util.concurrent.Executors;
|
||||||
|
import java.util.concurrent.ScheduledExecutorService;
|
||||||
|
import java.util.concurrent.ThreadPoolExecutor;
|
||||||
|
import java.util.concurrent.TimeUnit;
|
||||||
|
|
||||||
import static java.lang.Thread.NORM_PRIORITY;
|
import static java.lang.Thread.NORM_PRIORITY;
|
||||||
|
|
||||||
public class ArbitraryDataFileRequestThread implements Runnable {
|
public class ArbitraryDataFileRequestThread {
|
||||||
|
|
||||||
private static final Logger LOGGER = LogManager.getLogger(ArbitraryDataFileRequestThread.class);
|
private static final Logger LOGGER = LogManager.getLogger(ArbitraryDataFileRequestThread.class);
|
||||||
|
|
||||||
public ArbitraryDataFileRequestThread() {
|
private ConcurrentHashMap<String, ExecutorService> executorByPeer = new ConcurrentHashMap<>();
|
||||||
|
|
||||||
|
private ArbitraryDataFileRequestThread() {
|
||||||
|
cleanupExecutorByPeerScheduler.scheduleAtFixedRate(this::cleanupExecutorsByPeer, 1, 1, TimeUnit.MINUTES);
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
private static ArbitraryDataFileRequestThread instance = null;
|
||||||
public void run() {
|
|
||||||
Thread.currentThread().setName("Arbitrary Data File Request Thread");
|
public static ArbitraryDataFileRequestThread getInstance() {
|
||||||
Thread.currentThread().setPriority(NORM_PRIORITY);
|
|
||||||
|
if( instance == null ) {
|
||||||
|
instance = new ArbitraryDataFileRequestThread();
|
||||||
|
}
|
||||||
|
|
||||||
|
return instance;
|
||||||
|
}
|
||||||
|
|
||||||
|
private final ScheduledExecutorService cleanupExecutorByPeerScheduler = Executors.newScheduledThreadPool(1);
|
||||||
|
|
||||||
|
private void cleanupExecutorsByPeer() {
|
||||||
|
|
||||||
try {
|
try {
|
||||||
while (!Controller.isStopping()) {
|
this.executorByPeer.forEach((key, value) -> {
|
||||||
Long now = NTP.getTime();
|
if (value instanceof ThreadPoolExecutor) {
|
||||||
this.processFileHashes(now);
|
ThreadPoolExecutor threadPoolExecutor = (ThreadPoolExecutor) value;
|
||||||
|
if (threadPoolExecutor.getActiveCount() == 0) {
|
||||||
|
if (this.executorByPeer.computeIfPresent(key, (k, v) -> null) == null) {
|
||||||
|
LOGGER.info("removed executor: peer = " + key);
|
||||||
}
|
}
|
||||||
} catch (InterruptedException e) {
|
}
|
||||||
// Fall-through to exit thread...
|
} else {
|
||||||
|
LOGGER.warn("casting issue in cleanup");
|
||||||
|
}
|
||||||
|
});
|
||||||
|
} catch (Exception e) {
|
||||||
|
LOGGER.error(e.getMessage(), e);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private void processFileHashes(Long now) throws InterruptedException {
|
public void processFileHashes(Long now, List<ArbitraryFileListResponseInfo> responseInfos, ArbitraryDataFileManager arbitraryDataFileManager) {
|
||||||
if (Controller.isStopping()) {
|
if (Controller.isStopping()) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
ArbitraryDataFileManager arbitraryDataFileManager = ArbitraryDataFileManager.getInstance();
|
Map<String, byte[]> signatureBySignature58 = new HashMap<>(responseInfos.size());
|
||||||
String signature58 = null;
|
Map<String, List<ArbitraryFileListResponseInfo>> responseInfoBySignature58 = new HashMap<>();
|
||||||
String hash58 = null;
|
|
||||||
Peer peer = null;
|
|
||||||
boolean shouldProcess = false;
|
|
||||||
|
|
||||||
synchronized (arbitraryDataFileManager.arbitraryDataFileHashResponses) {
|
for( ArbitraryFileListResponseInfo responseInfo : responseInfos) {
|
||||||
if (!arbitraryDataFileManager.arbitraryDataFileHashResponses.isEmpty()) {
|
|
||||||
|
|
||||||
// Sort by lowest number of node hops first
|
if( responseInfo == null ) continue;
|
||||||
Comparator<ArbitraryFileListResponseInfo> lowestHopsFirstComparator =
|
|
||||||
Comparator.comparingInt(ArbitraryFileListResponseInfo::getRequestHops);
|
|
||||||
arbitraryDataFileManager.arbitraryDataFileHashResponses.sort(lowestHopsFirstComparator);
|
|
||||||
|
|
||||||
Iterator iterator = arbitraryDataFileManager.arbitraryDataFileHashResponses.iterator();
|
|
||||||
while (iterator.hasNext()) {
|
|
||||||
if (Controller.isStopping()) {
|
if (Controller.isStopping()) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
ArbitraryFileListResponseInfo responseInfo = (ArbitraryFileListResponseInfo) iterator.next();
|
Peer peer = responseInfo.getPeer();
|
||||||
if (responseInfo == null) {
|
|
||||||
iterator.remove();
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
|
|
||||||
hash58 = responseInfo.getHash58();
|
// if relay timeout, then move on
|
||||||
peer = responseInfo.getPeer();
|
if (now - responseInfo.getTimestamp() >= ArbitraryDataManager.ARBITRARY_RELAY_TIMEOUT || responseInfo.getSignature58() == null || peer == null) {
|
||||||
signature58 = responseInfo.getSignature58();
|
|
||||||
Long timestamp = responseInfo.getTimestamp();
|
|
||||||
|
|
||||||
if (now - timestamp >= ArbitraryDataManager.ARBITRARY_RELAY_TIMEOUT || signature58 == null || peer == null) {
|
|
||||||
// Ignore - to be deleted
|
|
||||||
iterator.remove();
|
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Skip if already requesting, but don't remove, as we might want to retry later
|
// Skip if already requesting, but don't remove, as we might want to retry later
|
||||||
if (arbitraryDataFileManager.arbitraryDataFileRequests.containsKey(hash58)) {
|
if (arbitraryDataFileManager.arbitraryDataFileRequests.containsKey(responseInfo.getHash58())) {
|
||||||
// Already requesting - leave this attempt for later
|
// Already requesting - leave this attempt for later
|
||||||
|
arbitraryDataFileManager.addResponse(responseInfo); // don't remove -> adding back, beacause it was removed already above
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
|
|
||||||
// We want to process this file
|
|
||||||
shouldProcess = true;
|
byte[] hash = Base58.decode(responseInfo.getHash58());
|
||||||
iterator.remove();
|
byte[] signature = Base58.decode(responseInfo.getSignature58());
|
||||||
break;
|
|
||||||
}
|
// check for null
|
||||||
}
|
if (signature == null || hash == null || peer == null) {
|
||||||
|
continue;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!shouldProcess) {
|
// We want to process this file, store and map data to process later
|
||||||
// Nothing to do
|
signatureBySignature58.put(responseInfo.getSignature58(), signature);
|
||||||
Thread.sleep(1000L);
|
responseInfoBySignature58
|
||||||
return;
|
.computeIfAbsent(responseInfo.getSignature58(), signature58 -> new ArrayList<>())
|
||||||
|
.add(responseInfo);
|
||||||
}
|
}
|
||||||
|
|
||||||
byte[] hash = Base58.decode(hash58);
|
// if there are no signatures, then there is nothing to process and nothing query the database
|
||||||
byte[] signature = Base58.decode(signature58);
|
if( signatureBySignature58.isEmpty() ) return;
|
||||||
|
|
||||||
|
List<ArbitraryTransactionData> arbitraryTransactionDataList = new ArrayList<>();
|
||||||
|
|
||||||
// Fetch the transaction data
|
// Fetch the transaction data
|
||||||
try (final Repository repository = RepositoryManager.getRepository()) {
|
try (final Repository repository = RepositoryManager.getRepository()) {
|
||||||
ArbitraryTransactionData arbitraryTransactionData = ArbitraryTransactionUtils.fetchTransactionData(repository, signature);
|
arbitraryTransactionDataList.addAll(
|
||||||
if (arbitraryTransactionData == null) {
|
ArbitraryTransactionUtils.fetchTransactionDataList(repository, new ArrayList<>(signatureBySignature58.values())));
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (signature == null || hash == null || peer == null || arbitraryTransactionData == null) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
LOGGER.trace("Fetching file {} from peer {} via request thread...", hash58, peer);
|
|
||||||
arbitraryDataFileManager.fetchArbitraryDataFiles(repository, peer, signature, arbitraryTransactionData, Arrays.asList(hash));
|
|
||||||
|
|
||||||
} catch (DataException e) {
|
} catch (DataException e) {
|
||||||
LOGGER.debug("Unable to process file hashes: {}", e.getMessage());
|
LOGGER.warn("Unable to fetch transaction data: {}", e.getMessage());
|
||||||
|
}
|
||||||
|
|
||||||
|
if( !arbitraryTransactionDataList.isEmpty() ) {
|
||||||
|
long start = System.currentTimeMillis();
|
||||||
|
|
||||||
|
for(ArbitraryTransactionData data : arbitraryTransactionDataList ) {
|
||||||
|
String signature58 = Base58.encode(data.getSignature());
|
||||||
|
for( ArbitraryFileListResponseInfo responseInfo : responseInfoBySignature58.get(signature58)) {
|
||||||
|
Runnable fetcher = () -> arbitraryDataFileFetcher(arbitraryDataFileManager, responseInfo, data);
|
||||||
|
this.executorByPeer
|
||||||
|
.computeIfAbsent(
|
||||||
|
responseInfo.getPeer().toString(),
|
||||||
|
peer -> Executors.newFixedThreadPool(
|
||||||
|
Settings.getInstance().getMaxThreadsForMessageType(MessageType.GET_ARBITRARY_DATA_FILE))
|
||||||
|
)
|
||||||
|
.execute(fetcher);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
long timeLapse = System.currentTimeMillis() - start;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private void arbitraryDataFileFetcher(ArbitraryDataFileManager arbitraryDataFileManager, ArbitraryFileListResponseInfo responseInfo, ArbitraryTransactionData arbitraryTransactionData) {
|
||||||
|
try {
|
||||||
|
Long now = NTP.getTime();
|
||||||
|
|
||||||
|
if (now - responseInfo.getTimestamp() >= ArbitraryDataManager.ARBITRARY_RELAY_TIMEOUT ) {
|
||||||
|
|
||||||
|
Peer peer = responseInfo.getPeer();
|
||||||
|
String hash58 = responseInfo.getHash58();
|
||||||
|
String signature58 = responseInfo.getSignature58();
|
||||||
|
LOGGER.debug("Peer {} version {} didn't fetch data file {} for signature {} due to relay timeout.", peer, peer.getPeersVersionString(), hash58, signature58);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
arbitraryDataFileManager.fetchArbitraryDataFiles(
|
||||||
|
responseInfo.getPeer(),
|
||||||
|
arbitraryTransactionData.getSignature(),
|
||||||
|
arbitraryTransactionData,
|
||||||
|
Arrays.asList(Base58.decode(responseInfo.getHash58()))
|
||||||
|
);
|
||||||
|
} catch (DataException e) {
|
||||||
|
LOGGER.warn("Unable to process file hashes: {}", e.getMessage());
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
@ -47,15 +47,15 @@ public class ArbitraryDataStorageManager extends Thread {
|
|||||||
|
|
||||||
private static final long DIRECTORY_SIZE_CHECK_INTERVAL = 10 * 60 * 1000L; // 10 minutes
|
private static final long DIRECTORY_SIZE_CHECK_INTERVAL = 10 * 60 * 1000L; // 10 minutes
|
||||||
|
|
||||||
/** Treat storage as full at 90% usage, to reduce risk of going over the limit.
|
/** Treat storage as full at 80% usage, to reduce risk of going over the limit.
|
||||||
* This is necessary because we don't calculate total storage values before every write.
|
* This is necessary because we don't calculate total storage values before every write.
|
||||||
* It also helps avoid a fetch/delete loop, as we will stop fetching before the hard limit.
|
* It also helps avoid a fetch/delete loop, as we will stop fetching before the hard limit.
|
||||||
* This must be lower than DELETION_THRESHOLD. */
|
* This must be lower than DELETION_THRESHOLD. */
|
||||||
private static final double STORAGE_FULL_THRESHOLD = 0.90f; // 90%
|
private static final double STORAGE_FULL_THRESHOLD = 0.8f; // 80%
|
||||||
|
|
||||||
/** Start deleting files once we reach 98% usage.
|
/** Start deleting files once we reach 90% usage.
|
||||||
* This must be higher than STORAGE_FULL_THRESHOLD in order to avoid a fetch/delete loop. */
|
* This must be higher than STORAGE_FULL_THRESHOLD in order to avoid a fetch/delete loop. */
|
||||||
public static final double DELETION_THRESHOLD = 0.98f; // 98%
|
public static final double DELETION_THRESHOLD = 0.9f; // 90%
|
||||||
|
|
||||||
private static final long PER_NAME_STORAGE_MULTIPLIER = 4L;
|
private static final long PER_NAME_STORAGE_MULTIPLIER = 4L;
|
||||||
|
|
||||||
|
@ -24,6 +24,11 @@ import org.qortal.utils.Triple;
|
|||||||
|
|
||||||
import java.io.IOException;
|
import java.io.IOException;
|
||||||
import java.util.*;
|
import java.util.*;
|
||||||
|
import java.util.concurrent.Executors;
|
||||||
|
import java.util.concurrent.ScheduledExecutorService;
|
||||||
|
import java.util.concurrent.TimeUnit;
|
||||||
|
import java.util.function.Function;
|
||||||
|
import java.util.stream.Collectors;
|
||||||
|
|
||||||
import static org.qortal.controller.arbitrary.ArbitraryDataFileListManager.*;
|
import static org.qortal.controller.arbitrary.ArbitraryDataFileListManager.*;
|
||||||
|
|
||||||
@ -61,6 +66,7 @@ public class ArbitraryMetadataManager {
|
|||||||
|
|
||||||
|
|
||||||
private ArbitraryMetadataManager() {
|
private ArbitraryMetadataManager() {
|
||||||
|
scheduler.scheduleAtFixedRate(this::processNetworkGetArbitraryMetadataMessage, 60, 1, TimeUnit.SECONDS);
|
||||||
}
|
}
|
||||||
|
|
||||||
public static ArbitraryMetadataManager getInstance() {
|
public static ArbitraryMetadataManager getInstance() {
|
||||||
@ -371,36 +377,84 @@ public class ArbitraryMetadataManager {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// List to collect messages
|
||||||
|
private final List<PeerMessage> messageList = new ArrayList<>();
|
||||||
|
// Lock to synchronize access to the list
|
||||||
|
private final Object lock = new Object();
|
||||||
|
|
||||||
|
// Scheduled executor service to process messages every second
|
||||||
|
private final ScheduledExecutorService scheduler = Executors.newScheduledThreadPool(1);
|
||||||
|
|
||||||
public void onNetworkGetArbitraryMetadataMessage(Peer peer, Message message) {
|
public void onNetworkGetArbitraryMetadataMessage(Peer peer, Message message) {
|
||||||
|
|
||||||
// Don't respond if QDN is disabled
|
// Don't respond if QDN is disabled
|
||||||
if (!Settings.getInstance().isQdnEnabled()) {
|
if (!Settings.getInstance().isQdnEnabled()) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
synchronized (lock) {
|
||||||
|
messageList.add(new PeerMessage(peer, message));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private void processNetworkGetArbitraryMetadataMessage() {
|
||||||
|
|
||||||
|
try {
|
||||||
|
List<PeerMessage> messagesToProcess;
|
||||||
|
synchronized (lock) {
|
||||||
|
messagesToProcess = new ArrayList<>(messageList);
|
||||||
|
messageList.clear();
|
||||||
|
}
|
||||||
|
|
||||||
|
Map<String, byte[]> signatureBySignature58 = new HashMap<>((messagesToProcess.size()));
|
||||||
|
Map<String, Long> nowBySignature58 = new HashMap<>(messagesToProcess.size());
|
||||||
|
Map<String,PeerMessage> peerMessageBySignature58 = new HashMap<>(messagesToProcess.size());
|
||||||
|
|
||||||
|
for( PeerMessage peerMessage : messagesToProcess) {
|
||||||
Controller.getInstance().stats.getArbitraryMetadataMessageStats.requests.incrementAndGet();
|
Controller.getInstance().stats.getArbitraryMetadataMessageStats.requests.incrementAndGet();
|
||||||
|
|
||||||
GetArbitraryMetadataMessage getArbitraryMetadataMessage = (GetArbitraryMetadataMessage) message;
|
GetArbitraryMetadataMessage getArbitraryMetadataMessage = (GetArbitraryMetadataMessage) peerMessage.message;
|
||||||
byte[] signature = getArbitraryMetadataMessage.getSignature();
|
byte[] signature = getArbitraryMetadataMessage.getSignature();
|
||||||
String signature58 = Base58.encode(signature);
|
String signature58 = Base58.encode(signature);
|
||||||
Long now = NTP.getTime();
|
Long now = NTP.getTime();
|
||||||
Triple<String, Peer, Long> newEntry = new Triple<>(signature58, peer, now);
|
Triple<String, Peer, Long> newEntry = new Triple<>(signature58, peerMessage.peer, now);
|
||||||
|
|
||||||
// If we've seen this request recently, then ignore
|
// If we've seen this request recently, then ignore
|
||||||
if (arbitraryMetadataRequests.putIfAbsent(message.getId(), newEntry) != null) {
|
if (arbitraryMetadataRequests.putIfAbsent(peerMessage.message.getId(), newEntry) != null) {
|
||||||
LOGGER.debug("Ignoring metadata request from peer {} for signature {}", peer, signature58);
|
LOGGER.debug("Ignoring metadata request from peer {} for signature {}", peerMessage.peer, signature58);
|
||||||
return;
|
continue;
|
||||||
}
|
}
|
||||||
|
|
||||||
LOGGER.debug("Received metadata request from peer {} for signature {}", peer, signature58);
|
LOGGER.debug("Received metadata request from peer {} for signature {}", peerMessage.peer, signature58);
|
||||||
|
|
||||||
ArbitraryTransactionData transactionData = null;
|
signatureBySignature58.put(signature58, signature);
|
||||||
ArbitraryDataFile metadataFile = null;
|
nowBySignature58.put(signature58, now);
|
||||||
|
peerMessageBySignature58.put(signature58, peerMessage);
|
||||||
|
}
|
||||||
|
|
||||||
|
if( signatureBySignature58.isEmpty() ) return;
|
||||||
|
|
||||||
|
List<TransactionData> transactionDataList;
|
||||||
try (final Repository repository = RepositoryManager.getRepository()) {
|
try (final Repository repository = RepositoryManager.getRepository()) {
|
||||||
|
|
||||||
// Firstly we need to lookup this file on chain to get its metadata hash
|
// Firstly we need to lookup this file on chain to get its metadata hash
|
||||||
transactionData = (ArbitraryTransactionData)repository.getTransactionRepository().fromSignature(signature);
|
transactionDataList = repository.getTransactionRepository().fromSignatures(new ArrayList(signatureBySignature58.values()));
|
||||||
if (transactionData instanceof ArbitraryTransactionData) {
|
} catch (DataException e) {
|
||||||
|
LOGGER.error(String.format("Repository issue while fetching arbitrary transactions"), e);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
Map<String, ArbitraryTransactionData> dataBySignature58
|
||||||
|
= transactionDataList.stream()
|
||||||
|
.filter(data -> data instanceof ArbitraryTransactionData)
|
||||||
|
.map(ArbitraryTransactionData.class::cast)
|
||||||
|
.collect(Collectors.toMap(data -> Base58.encode(data.getSignature()), Function.identity()));
|
||||||
|
|
||||||
|
for(Map.Entry<String, ArbitraryTransactionData> entry : dataBySignature58.entrySet()) {
|
||||||
|
String signature58 = entry.getKey();
|
||||||
|
ArbitraryTransactionData transactionData = entry.getValue();
|
||||||
|
|
||||||
|
try {
|
||||||
|
|
||||||
// Check if we're even allowed to serve metadata for this transaction
|
// Check if we're even allowed to serve metadata for this transaction
|
||||||
if (ArbitraryDataStorageManager.getInstance().canStoreData(transactionData)) {
|
if (ArbitraryDataStorageManager.getInstance().canStoreData(transactionData)) {
|
||||||
@ -409,69 +463,74 @@ public class ArbitraryMetadataManager {
|
|||||||
if (metadataHash != null) {
|
if (metadataHash != null) {
|
||||||
|
|
||||||
// Load metadata file
|
// Load metadata file
|
||||||
metadataFile = ArbitraryDataFile.fromHash(metadataHash, signature);
|
ArbitraryDataFile metadataFile = ArbitraryDataFile.fromHash(metadataHash, transactionData.getSignature());
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
} catch (DataException e) {
|
|
||||||
LOGGER.error(String.format("Repository issue while fetching arbitrary metadata for peer %s", peer), e);
|
|
||||||
}
|
|
||||||
|
|
||||||
// We should only respond if we have the metadata file
|
// We should only respond if we have the metadata file
|
||||||
if (metadataFile != null && metadataFile.exists()) {
|
if (metadataFile != null && metadataFile.exists()) {
|
||||||
|
|
||||||
|
PeerMessage peerMessage = peerMessageBySignature58.get(signature58);
|
||||||
|
Message message = peerMessage.message;
|
||||||
|
Peer peer = peerMessage.peer;
|
||||||
|
|
||||||
// We have the metadata file, so update requests map to reflect that we've sent it
|
// We have the metadata file, so update requests map to reflect that we've sent it
|
||||||
newEntry = new Triple<>(null, null, now);
|
Triple newEntry = new Triple<>(null, null, nowBySignature58.get(signature58));
|
||||||
arbitraryMetadataRequests.put(message.getId(), newEntry);
|
arbitraryMetadataRequests.put(message.getId(), newEntry);
|
||||||
|
|
||||||
ArbitraryMetadataMessage arbitraryMetadataMessage = new ArbitraryMetadataMessage(signature, metadataFile);
|
ArbitraryMetadataMessage arbitraryMetadataMessage = new ArbitraryMetadataMessage(entry.getValue().getSignature(), metadataFile);
|
||||||
arbitraryMetadataMessage.setId(message.getId());
|
arbitraryMetadataMessage.setId(message.getId());
|
||||||
if (!peer.sendMessage(arbitraryMetadataMessage)) {
|
if (!peer.sendMessage(arbitraryMetadataMessage)) {
|
||||||
LOGGER.debug("Couldn't send metadata");
|
LOGGER.debug("Couldn't send metadata");
|
||||||
peer.disconnect("failed to send metadata");
|
peer.disconnect("failed to send metadata");
|
||||||
return;
|
continue;
|
||||||
}
|
}
|
||||||
LOGGER.debug("Sent metadata");
|
LOGGER.debug("Sent metadata");
|
||||||
|
|
||||||
// Nothing left to do, so return to prevent any unnecessary forwarding from occurring
|
// Nothing left to do, so return to prevent any unnecessary forwarding from occurring
|
||||||
LOGGER.debug("No need for any forwarding because metadata request is fully served");
|
LOGGER.debug("No need for any forwarding because metadata request is fully served");
|
||||||
return;
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
} catch (DataException e) {
|
||||||
|
LOGGER.error(String.format("Repository issue while fetching arbitrary metadata"), e);
|
||||||
|
}
|
||||||
|
|
||||||
// We may need to forward this request on
|
// We may need to forward this request on
|
||||||
boolean isBlocked = (transactionData == null || ListUtils.isNameBlocked(transactionData.getName()));
|
boolean isBlocked = (transactionDataList == null || ListUtils.isNameBlocked(transactionData.getName()));
|
||||||
if (Settings.getInstance().isRelayModeEnabled() && !isBlocked) {
|
if (Settings.getInstance().isRelayModeEnabled() && !isBlocked) {
|
||||||
// In relay mode - so ask our other peers if they have it
|
// In relay mode - so ask our other peers if they have it
|
||||||
|
|
||||||
|
PeerMessage peerMessage = peerMessageBySignature58.get(signature58);
|
||||||
|
GetArbitraryMetadataMessage getArbitraryMetadataMessage = (GetArbitraryMetadataMessage) peerMessage.message;
|
||||||
long requestTime = getArbitraryMetadataMessage.getRequestTime();
|
long requestTime = getArbitraryMetadataMessage.getRequestTime();
|
||||||
int requestHops = getArbitraryMetadataMessage.getRequestHops() + 1;
|
int requestHops = getArbitraryMetadataMessage.getRequestHops() + 1;
|
||||||
long totalRequestTime = now - requestTime;
|
long totalRequestTime = nowBySignature58.get(signature58) - requestTime;
|
||||||
|
|
||||||
if (totalRequestTime < RELAY_REQUEST_MAX_DURATION) {
|
if (totalRequestTime < RELAY_REQUEST_MAX_DURATION) {
|
||||||
// Relay request hasn't timed out yet, so can potentially be rebroadcast
|
// Relay request hasn't timed out yet, so can potentially be rebroadcast
|
||||||
if (requestHops < RELAY_REQUEST_MAX_HOPS) {
|
if (requestHops < RELAY_REQUEST_MAX_HOPS) {
|
||||||
// Relay request hasn't reached the maximum number of hops yet, so can be rebroadcast
|
// Relay request hasn't reached the maximum number of hops yet, so can be rebroadcast
|
||||||
|
|
||||||
|
byte[] signature = signatureBySignature58.get(signature58);
|
||||||
Message relayGetArbitraryMetadataMessage = new GetArbitraryMetadataMessage(signature, requestTime, requestHops);
|
Message relayGetArbitraryMetadataMessage = new GetArbitraryMetadataMessage(signature, requestTime, requestHops);
|
||||||
relayGetArbitraryMetadataMessage.setId(message.getId());
|
relayGetArbitraryMetadataMessage.setId(getArbitraryMetadataMessage.getId());
|
||||||
|
|
||||||
|
Peer peer = peerMessage.peer;
|
||||||
LOGGER.debug("Rebroadcasting metadata request from peer {} for signature {} to our other peers... totalRequestTime: {}, requestHops: {}", peer, Base58.encode(signature), totalRequestTime, requestHops);
|
LOGGER.debug("Rebroadcasting metadata request from peer {} for signature {} to our other peers... totalRequestTime: {}, requestHops: {}", peer, Base58.encode(signature), totalRequestTime, requestHops);
|
||||||
Network.getInstance().broadcast(
|
Network.getInstance().broadcast(
|
||||||
broadcastPeer ->
|
broadcastPeer ->
|
||||||
!broadcastPeer.isAtLeastVersion(RELAY_MIN_PEER_VERSION) ? null :
|
!broadcastPeer.isAtLeastVersion(RELAY_MIN_PEER_VERSION) ? null :
|
||||||
broadcastPeer == peer || Objects.equals(broadcastPeer.getPeerData().getAddress().getHost(), peer.getPeerData().getAddress().getHost()) ? null : relayGetArbitraryMetadataMessage);
|
broadcastPeer == peer || Objects.equals(broadcastPeer.getPeerData().getAddress().getHost(), peer.getPeerData().getAddress().getHost()) ? null : relayGetArbitraryMetadataMessage);
|
||||||
|
|
||||||
}
|
} else {
|
||||||
else {
|
|
||||||
// This relay request has reached the maximum number of allowed hops
|
// This relay request has reached the maximum number of allowed hops
|
||||||
}
|
}
|
||||||
}
|
} else {
|
||||||
else {
|
|
||||||
// This relay request has timed out
|
// This relay request has timed out
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
} catch (Exception e) {
|
||||||
|
LOGGER.error(e.getMessage(), e);
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
@ -0,0 +1,22 @@
|
|||||||
|
package org.qortal.controller.arbitrary;
|
||||||
|
|
||||||
|
import org.qortal.network.Peer;
|
||||||
|
import org.qortal.network.message.Message;
|
||||||
|
|
||||||
|
public class PeerMessage {
|
||||||
|
Peer peer;
|
||||||
|
Message message;
|
||||||
|
|
||||||
|
public PeerMessage(Peer peer, Message message) {
|
||||||
|
this.peer = peer;
|
||||||
|
this.message = message;
|
||||||
|
}
|
||||||
|
|
||||||
|
public Peer getPeer() {
|
||||||
|
return peer;
|
||||||
|
}
|
||||||
|
|
||||||
|
public Message getMessage() {
|
||||||
|
return message;
|
||||||
|
}
|
||||||
|
}
|
@ -8,6 +8,7 @@ import org.qortal.account.PrivateKeyAccount;
|
|||||||
import org.qortal.api.model.crosschain.TradeBotCreateRequest;
|
import org.qortal.api.model.crosschain.TradeBotCreateRequest;
|
||||||
import org.qortal.controller.Controller;
|
import org.qortal.controller.Controller;
|
||||||
import org.qortal.controller.Synchronizer;
|
import org.qortal.controller.Synchronizer;
|
||||||
|
import org.qortal.controller.arbitrary.PeerMessage;
|
||||||
import org.qortal.controller.tradebot.AcctTradeBot.ResponseResult;
|
import org.qortal.controller.tradebot.AcctTradeBot.ResponseResult;
|
||||||
import org.qortal.crosschain.*;
|
import org.qortal.crosschain.*;
|
||||||
import org.qortal.crypto.Crypto;
|
import org.qortal.crypto.Crypto;
|
||||||
@ -37,7 +38,12 @@ import org.qortal.utils.NTP;
|
|||||||
import java.awt.TrayIcon.MessageType;
|
import java.awt.TrayIcon.MessageType;
|
||||||
import java.security.SecureRandom;
|
import java.security.SecureRandom;
|
||||||
import java.util.*;
|
import java.util.*;
|
||||||
|
import java.util.concurrent.Executors;
|
||||||
|
import java.util.concurrent.ScheduledExecutorService;
|
||||||
|
import java.util.concurrent.TimeUnit;
|
||||||
|
import java.util.function.Function;
|
||||||
import java.util.function.Supplier;
|
import java.util.function.Supplier;
|
||||||
|
import java.util.stream.Collectors;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Performing cross-chain trading steps on behalf of user.
|
* Performing cross-chain trading steps on behalf of user.
|
||||||
@ -118,6 +124,9 @@ public class TradeBot implements Listener {
|
|||||||
private Map<String, Long> validTrades = new HashMap<>();
|
private Map<String, Long> validTrades = new HashMap<>();
|
||||||
|
|
||||||
private TradeBot() {
|
private TradeBot() {
|
||||||
|
|
||||||
|
tradePresenceMessageScheduler.scheduleAtFixedRate( this::processTradePresencesMessages, 60, 1, TimeUnit.SECONDS);
|
||||||
|
|
||||||
EventBus.INSTANCE.addListener(event -> TradeBot.getInstance().listen(event));
|
EventBus.INSTANCE.addListener(event -> TradeBot.getInstance().listen(event));
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -551,11 +560,43 @@ public class TradeBot implements Listener {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// List to collect messages
|
||||||
|
private final List<PeerMessage> tradePresenceMessageList = new ArrayList<>();
|
||||||
|
// Lock to synchronize access to the list
|
||||||
|
private final Object tradePresenceMessageLock = new Object();
|
||||||
|
|
||||||
|
// Scheduled executor service to process messages every second
|
||||||
|
private final ScheduledExecutorService tradePresenceMessageScheduler = Executors.newScheduledThreadPool(1);
|
||||||
|
|
||||||
public void onTradePresencesMessage(Peer peer, Message message) {
|
public void onTradePresencesMessage(Peer peer, Message message) {
|
||||||
TradePresencesMessage tradePresencesMessage = (TradePresencesMessage) message;
|
|
||||||
|
synchronized (tradePresenceMessageLock) {
|
||||||
|
tradePresenceMessageList.add(new PeerMessage(peer, message));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public void processTradePresencesMessages() {
|
||||||
|
|
||||||
|
try {
|
||||||
|
List<PeerMessage> messagesToProcess;
|
||||||
|
synchronized (tradePresenceMessageLock) {
|
||||||
|
messagesToProcess = new ArrayList<>(tradePresenceMessageList);
|
||||||
|
tradePresenceMessageList.clear();
|
||||||
|
}
|
||||||
|
|
||||||
|
if( messagesToProcess.isEmpty() ) return;
|
||||||
|
|
||||||
|
Map<Peer, List<TradePresenceData>> tradePresencesByPeer = new HashMap<>(messagesToProcess.size());
|
||||||
|
|
||||||
|
// map all trade presences from the messages to their peer
|
||||||
|
for( PeerMessage peerMessage : messagesToProcess ) {
|
||||||
|
TradePresencesMessage tradePresencesMessage = (TradePresencesMessage) peerMessage.getMessage();
|
||||||
|
|
||||||
List<TradePresenceData> peersTradePresences = tradePresencesMessage.getTradePresences();
|
List<TradePresenceData> peersTradePresences = tradePresencesMessage.getTradePresences();
|
||||||
|
|
||||||
|
tradePresencesByPeer.put(peerMessage.getPeer(), peersTradePresences);
|
||||||
|
}
|
||||||
|
|
||||||
long now = NTP.getTime();
|
long now = NTP.getTime();
|
||||||
// Timestamps before this are too far into the past
|
// Timestamps before this are too far into the past
|
||||||
long pastThreshold = now;
|
long pastThreshold = now;
|
||||||
@ -566,8 +607,16 @@ public class TradeBot implements Listener {
|
|||||||
|
|
||||||
int newCount = 0;
|
int newCount = 0;
|
||||||
|
|
||||||
try (final Repository repository = RepositoryManager.getRepository()) {
|
Map<String, List<Peer>> peersByAtAddress = new HashMap<>(tradePresencesByPeer.size());
|
||||||
for (TradePresenceData peersTradePresence : peersTradePresences) {
|
Map<String, TradePresenceData> tradePresenceByAtAddress = new HashMap<>(tradePresencesByPeer.size());
|
||||||
|
|
||||||
|
// for each batch of trade presence data from a peer, validate and populate the maps declared above
|
||||||
|
for ( Map.Entry<Peer, List<TradePresenceData>> entry: tradePresencesByPeer.entrySet()) {
|
||||||
|
|
||||||
|
Peer peer = entry.getKey();
|
||||||
|
|
||||||
|
for( TradePresenceData peersTradePresence : entry.getValue() ) {
|
||||||
|
// TradePresenceData peersTradePresence
|
||||||
long timestamp = peersTradePresence.getTimestamp();
|
long timestamp = peersTradePresence.getTimestamp();
|
||||||
|
|
||||||
// Ignore if timestamp is out of bounds
|
// Ignore if timestamp is out of bounds
|
||||||
@ -613,15 +662,37 @@ public class TradeBot implements Listener {
|
|||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
|
|
||||||
ATData atData = repository.getATRepository().fromATAddress(peersTradePresence.getAtAddress());
|
peersByAtAddress.computeIfAbsent(peersTradePresence.getAtAddress(), address -> new ArrayList<>()).add(peer);
|
||||||
|
tradePresenceByAtAddress.put(peersTradePresence.getAtAddress(), peersTradePresence);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if( tradePresenceByAtAddress.isEmpty() ) return;
|
||||||
|
|
||||||
|
List<ATData> atDataList;
|
||||||
|
try (final Repository repository = RepositoryManager.getRepository()) {
|
||||||
|
atDataList = repository.getATRepository().fromATAddresses( new ArrayList<>(tradePresenceByAtAddress.keySet()) );
|
||||||
|
} catch (DataException e) {
|
||||||
|
LOGGER.error("Couldn't process TRADE_PRESENCES message due to repository issue", e);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
Map<String, Supplier<ACCT>> supplierByAtAddress = new HashMap<>(atDataList.size());
|
||||||
|
|
||||||
|
List<ATData> validatedAtDataList = new ArrayList<>(atDataList.size());
|
||||||
|
|
||||||
|
// for each trade
|
||||||
|
for( ATData atData : atDataList ) {
|
||||||
|
|
||||||
|
TradePresenceData peersTradePresence = tradePresenceByAtAddress.get(atData.getATAddress());
|
||||||
if (atData == null || atData.getIsFrozen() || atData.getIsFinished()) {
|
if (atData == null || atData.getIsFrozen() || atData.getIsFinished()) {
|
||||||
if (atData == null)
|
if (atData == null)
|
||||||
LOGGER.trace("Ignoring trade presence {} from peer {} as AT doesn't exist",
|
LOGGER.trace("Ignoring trade presence {} from peer as AT doesn't exist",
|
||||||
peersTradePresence.getAtAddress(), peer
|
peersTradePresence.getAtAddress()
|
||||||
);
|
);
|
||||||
else
|
else
|
||||||
LOGGER.trace("Ignoring trade presence {} from peer {} as AT is frozen or finished",
|
LOGGER.trace("Ignoring trade presence {} from peer as AT is frozen or finished",
|
||||||
peersTradePresence.getAtAddress(), peer
|
peersTradePresence.getAtAddress()
|
||||||
);
|
);
|
||||||
|
|
||||||
continue;
|
continue;
|
||||||
@ -630,22 +701,55 @@ public class TradeBot implements Listener {
|
|||||||
ByteArray atCodeHash = ByteArray.wrap(atData.getCodeHash());
|
ByteArray atCodeHash = ByteArray.wrap(atData.getCodeHash());
|
||||||
Supplier<ACCT> acctSupplier = acctSuppliersByCodeHash.get(atCodeHash);
|
Supplier<ACCT> acctSupplier = acctSuppliersByCodeHash.get(atCodeHash);
|
||||||
if (acctSupplier == null) {
|
if (acctSupplier == null) {
|
||||||
LOGGER.trace("Ignoring trade presence {} from peer {} as AT isn't a known ACCT?",
|
LOGGER.trace("Ignoring trade presence {} from peer as AT isn't a known ACCT?",
|
||||||
peersTradePresence.getAtAddress(), peer
|
peersTradePresence.getAtAddress()
|
||||||
);
|
);
|
||||||
|
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
|
validatedAtDataList.add(atData);
|
||||||
CrossChainTradeData tradeData = acctSupplier.get().populateTradeData(repository, atData);
|
|
||||||
if (tradeData == null) {
|
|
||||||
LOGGER.trace("Ignoring trade presence {} from peer {} as trade data not found?",
|
|
||||||
peersTradePresence.getAtAddress(), peer
|
|
||||||
);
|
|
||||||
|
|
||||||
continue;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// populated data for each trade
|
||||||
|
List<CrossChainTradeData> crossChainTradeDataList;
|
||||||
|
|
||||||
|
// validated trade data grouped by code (cross chain coin)
|
||||||
|
Map<ByteArray, List<ATData>> atDataByCodeHash
|
||||||
|
= validatedAtDataList.stream().collect(
|
||||||
|
Collectors.groupingBy(data -> ByteArray.wrap(data.getCodeHash())));
|
||||||
|
|
||||||
|
try (final Repository repository = RepositoryManager.getRepository()) {
|
||||||
|
|
||||||
|
crossChainTradeDataList = new ArrayList<>();
|
||||||
|
|
||||||
|
// for each code (cross chain coin), get each trade, then populate trade data
|
||||||
|
for( Map.Entry<ByteArray, List<ATData>> entry : atDataByCodeHash.entrySet() ) {
|
||||||
|
|
||||||
|
Supplier<ACCT> acctSupplier = acctSuppliersByCodeHash.get(entry.getKey());
|
||||||
|
|
||||||
|
crossChainTradeDataList.addAll(
|
||||||
|
acctSupplier.get().populateTradeDataList(
|
||||||
|
repository,
|
||||||
|
entry.getValue()
|
||||||
|
)
|
||||||
|
.stream().filter( data -> data != null )
|
||||||
|
.collect(Collectors.toList())
|
||||||
|
);
|
||||||
|
}
|
||||||
|
} catch (DataException e) {
|
||||||
|
LOGGER.error("Couldn't process TRADE_PRESENCES message due to repository issue", e);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// for each populated trade data, validate and fire event
|
||||||
|
for( CrossChainTradeData tradeData : crossChainTradeDataList ) {
|
||||||
|
|
||||||
|
List<Peer> peers = peersByAtAddress.get(tradeData.qortalAtAddress);
|
||||||
|
|
||||||
|
for( Peer peer : peers ) {
|
||||||
|
|
||||||
|
TradePresenceData peersTradePresence = tradePresenceByAtAddress.get(tradeData.qortalAtAddress);
|
||||||
|
|
||||||
// Convert signer's public key to address form
|
// Convert signer's public key to address form
|
||||||
String signerAddress = peersTradePresence.getTradeAddress();
|
String signerAddress = peersTradePresence.getTradeAddress();
|
||||||
|
|
||||||
@ -658,24 +762,27 @@ public class TradeBot implements Listener {
|
|||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
ByteArray pubkeyByteArray = ByteArray.wrap(peersTradePresence.getPublicKey());
|
||||||
|
|
||||||
// This is new to us
|
// This is new to us
|
||||||
this.allTradePresencesByPubkey.put(pubkeyByteArray, peersTradePresence);
|
this.allTradePresencesByPubkey.put(pubkeyByteArray, peersTradePresence);
|
||||||
++newCount;
|
++newCount;
|
||||||
|
|
||||||
LOGGER.trace("Added trade presence {} from peer {} with timestamp {}",
|
LOGGER.trace("Added trade presence {} from peer {} with timestamp {}",
|
||||||
peersTradePresence.getAtAddress(), peer, timestamp
|
peersTradePresence.getAtAddress(), peer, tradeData.creationTimestamp
|
||||||
);
|
);
|
||||||
|
|
||||||
EventBus.INSTANCE.notify(new TradePresenceEvent(peersTradePresence));
|
EventBus.INSTANCE.notify(new TradePresenceEvent(peersTradePresence));
|
||||||
}
|
}
|
||||||
} catch (DataException e) {
|
|
||||||
LOGGER.error("Couldn't process TRADE_PRESENCES message due to repository issue", e);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
if (newCount > 0) {
|
if (newCount > 0) {
|
||||||
LOGGER.debug("New trade presences: {}, all trade presences: {}", newCount, allTradePresencesByPubkey.size());
|
LOGGER.info("New trade presences: {}, all trade presences: {}", newCount, allTradePresencesByPubkey.size());
|
||||||
rebuildSafeAllTradePresences();
|
rebuildSafeAllTradePresences();
|
||||||
}
|
}
|
||||||
|
} catch (Exception e) {
|
||||||
|
LOGGER.error(e.getMessage(), e);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
public void bridgePresence(long timestamp, byte[] publicKey, byte[] signature, String atAddress) {
|
public void bridgePresence(long timestamp, byte[] publicKey, byte[] signature, String atAddress) {
|
||||||
|
@ -6,6 +6,9 @@ import org.qortal.data.crosschain.CrossChainTradeData;
|
|||||||
import org.qortal.repository.DataException;
|
import org.qortal.repository.DataException;
|
||||||
import org.qortal.repository.Repository;
|
import org.qortal.repository.Repository;
|
||||||
|
|
||||||
|
import java.util.List;
|
||||||
|
import java.util.OptionalLong;
|
||||||
|
|
||||||
public interface ACCT {
|
public interface ACCT {
|
||||||
|
|
||||||
public byte[] getCodeBytesHash();
|
public byte[] getCodeBytesHash();
|
||||||
@ -16,8 +19,12 @@ public interface ACCT {
|
|||||||
|
|
||||||
public CrossChainTradeData populateTradeData(Repository repository, ATData atData) throws DataException;
|
public CrossChainTradeData populateTradeData(Repository repository, ATData atData) throws DataException;
|
||||||
|
|
||||||
|
public List<CrossChainTradeData> populateTradeDataList(Repository respository, List<ATData> atDataList) throws DataException;
|
||||||
|
|
||||||
public CrossChainTradeData populateTradeData(Repository repository, ATStateData atStateData) throws DataException;
|
public CrossChainTradeData populateTradeData(Repository repository, ATStateData atStateData) throws DataException;
|
||||||
|
|
||||||
|
CrossChainTradeData populateTradeData(Repository repository, byte[] creatorPublicKey, long creationTimestamp, ATStateData atStateData, OptionalLong optionalBalance) throws DataException;
|
||||||
|
|
||||||
public byte[] buildCancelMessage(String creatorQortalAddress);
|
public byte[] buildCancelMessage(String creatorQortalAddress);
|
||||||
|
|
||||||
public byte[] findSecretA(Repository repository, CrossChainTradeData crossChainTradeData) throws DataException;
|
public byte[] findSecretA(Repository repository, CrossChainTradeData crossChainTradeData) throws DataException;
|
||||||
|
@ -1,5 +1,6 @@
|
|||||||
package org.qortal.crosschain;
|
package org.qortal.crosschain;
|
||||||
|
|
||||||
|
import org.bitcoinj.core.Coin;
|
||||||
import org.bitcoinj.core.Context;
|
import org.bitcoinj.core.Context;
|
||||||
import org.bitcoinj.core.NetworkParameters;
|
import org.bitcoinj.core.NetworkParameters;
|
||||||
import org.bitcoinj.core.Transaction;
|
import org.bitcoinj.core.Transaction;
|
||||||
@ -14,15 +15,21 @@ import java.util.Arrays;
|
|||||||
import java.util.Collection;
|
import java.util.Collection;
|
||||||
import java.util.EnumMap;
|
import java.util.EnumMap;
|
||||||
import java.util.Map;
|
import java.util.Map;
|
||||||
|
import java.util.concurrent.atomic.AtomicLong;
|
||||||
|
|
||||||
public class Bitcoin extends Bitcoiny {
|
public class Bitcoin extends Bitcoiny {
|
||||||
|
|
||||||
public static final String CURRENCY_CODE = "BTC";
|
public static final String CURRENCY_CODE = "BTC";
|
||||||
|
|
||||||
private static final long MINIMUM_ORDER_AMOUNT = 100000; // 0.001 BTC minimum order, due to high fees
|
// Locking fee to lock in a QORT for BTC. This is the default value that the user should reset to
|
||||||
|
// a value inline with the BTC fee market. This is 5 sats per kB.
|
||||||
|
private static final Coin DEFAULT_FEE_PER_KB = Coin.valueOf(5_000); // 0.00005 BTC per 1000 bytes
|
||||||
|
|
||||||
// Temporary values until a dynamic fee system is written.
|
private static final long MINIMUM_ORDER_AMOUNT = 100_000; // 0.001 BTC minimum order, due to high fees
|
||||||
private static final long NEW_FEE_AMOUNT = 6_000L;
|
|
||||||
|
// Default value until user resets fee to compete with the current market. This is a total value for a
|
||||||
|
// p2sh transaction, size 300 kB, 5 sats per kB
|
||||||
|
private static final long NEW_FEE_AMOUNT = 1_500L;
|
||||||
|
|
||||||
private static final long NON_MAINNET_FEE = 1000L; // enough for TESTNET3 and should be OK for REGTEST
|
private static final long NON_MAINNET_FEE = 1000L; // enough for TESTNET3 and should be OK for REGTEST
|
||||||
|
|
||||||
@ -111,7 +118,7 @@ public class Bitcoin extends Bitcoiny {
|
|||||||
|
|
||||||
@Override
|
@Override
|
||||||
public long getP2shFee(Long timestamp) {
|
public long getP2shFee(Long timestamp) {
|
||||||
return this.getFeeCeiling();
|
return this.getFeeRequired();
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
TEST3 {
|
TEST3 {
|
||||||
@ -173,14 +180,14 @@ public class Bitcoin extends Bitcoiny {
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
private long feeCeiling = NEW_FEE_AMOUNT;
|
private AtomicLong feeRequired = new AtomicLong(NEW_FEE_AMOUNT);
|
||||||
|
|
||||||
public long getFeeCeiling() {
|
public long getFeeRequired() {
|
||||||
return feeCeiling;
|
return feeRequired.get();
|
||||||
}
|
}
|
||||||
|
|
||||||
public void setFeeCeiling(long feeCeiling) {
|
public void setFeeRequired(long feeRequired) {
|
||||||
this.feeCeiling = feeCeiling;
|
this.feeRequired.set(feeRequired);
|
||||||
}
|
}
|
||||||
|
|
||||||
public abstract NetworkParameters getParams();
|
public abstract NetworkParameters getParams();
|
||||||
@ -196,7 +203,7 @@ public class Bitcoin extends Bitcoiny {
|
|||||||
// Constructors and instance
|
// Constructors and instance
|
||||||
|
|
||||||
private Bitcoin(BitcoinNet bitcoinNet, BitcoinyBlockchainProvider blockchain, Context bitcoinjContext, String currencyCode) {
|
private Bitcoin(BitcoinNet bitcoinNet, BitcoinyBlockchainProvider blockchain, Context bitcoinjContext, String currencyCode) {
|
||||||
super(blockchain, bitcoinjContext, currencyCode, bitcoinjContext.getFeePerKb());
|
super(blockchain, bitcoinjContext, currencyCode, DEFAULT_FEE_PER_KB);
|
||||||
this.bitcoinNet = bitcoinNet;
|
this.bitcoinNet = bitcoinNet;
|
||||||
|
|
||||||
LOGGER.info(() -> String.format("Starting Bitcoin support using %s", this.bitcoinNet.name()));
|
LOGGER.info(() -> String.format("Starting Bitcoin support using %s", this.bitcoinNet.name()));
|
||||||
@ -242,14 +249,14 @@ public class Bitcoin extends Bitcoiny {
|
|||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public long getFeeCeiling() {
|
public long getFeeRequired() {
|
||||||
return this.bitcoinNet.getFeeCeiling();
|
return this.bitcoinNet.getFeeRequired();
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public void setFeeCeiling(long fee) {
|
public void setFeeRequired(long fee) {
|
||||||
|
|
||||||
this.bitcoinNet.setFeeCeiling( fee );
|
this.bitcoinNet.setFeeRequired( fee );
|
||||||
}
|
}
|
||||||
/**
|
/**
|
||||||
* Returns bitcoinj transaction sending <tt>amount</tt> to <tt>recipient</tt> using 20 sat/byte fee.
|
* Returns bitcoinj transaction sending <tt>amount</tt> to <tt>recipient</tt> using 20 sat/byte fee.
|
||||||
|
@ -4,6 +4,7 @@ import com.google.common.hash.HashCode;
|
|||||||
import com.google.common.primitives.Bytes;
|
import com.google.common.primitives.Bytes;
|
||||||
import org.ciyam.at.*;
|
import org.ciyam.at.*;
|
||||||
import org.qortal.account.Account;
|
import org.qortal.account.Account;
|
||||||
|
import org.qortal.api.resource.CrossChainUtils;
|
||||||
import org.qortal.asset.Asset;
|
import org.qortal.asset.Asset;
|
||||||
import org.qortal.at.QortalFunctionCode;
|
import org.qortal.at.QortalFunctionCode;
|
||||||
import org.qortal.crypto.Crypto;
|
import org.qortal.crypto.Crypto;
|
||||||
@ -19,6 +20,7 @@ import org.qortal.utils.BitTwiddling;
|
|||||||
import java.nio.ByteBuffer;
|
import java.nio.ByteBuffer;
|
||||||
import java.util.Arrays;
|
import java.util.Arrays;
|
||||||
import java.util.List;
|
import java.util.List;
|
||||||
|
import java.util.OptionalLong;
|
||||||
|
|
||||||
import static org.ciyam.at.OpCode.calcOffset;
|
import static org.ciyam.at.OpCode.calcOffset;
|
||||||
|
|
||||||
@ -608,7 +610,14 @@ public class BitcoinACCTv1 implements ACCT {
|
|||||||
@Override
|
@Override
|
||||||
public CrossChainTradeData populateTradeData(Repository repository, ATData atData) throws DataException {
|
public CrossChainTradeData populateTradeData(Repository repository, ATData atData) throws DataException {
|
||||||
ATStateData atStateData = repository.getATRepository().getLatestATState(atData.getATAddress());
|
ATStateData atStateData = repository.getATRepository().getLatestATState(atData.getATAddress());
|
||||||
return populateTradeData(repository, atData.getCreatorPublicKey(), atData.getCreation(), atStateData);
|
return populateTradeData(repository, atData.getCreatorPublicKey(), atData.getCreation(), atStateData, OptionalLong.empty());
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public List<CrossChainTradeData> populateTradeDataList(Repository repository, List<ATData> atDataList) throws DataException {
|
||||||
|
List<CrossChainTradeData> crossChainTradeDataList = CrossChainUtils.populateTradeDataList(repository, this, atDataList);
|
||||||
|
|
||||||
|
return crossChainTradeDataList;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@ -617,13 +626,14 @@ public class BitcoinACCTv1 implements ACCT {
|
|||||||
@Override
|
@Override
|
||||||
public CrossChainTradeData populateTradeData(Repository repository, ATStateData atStateData) throws DataException {
|
public CrossChainTradeData populateTradeData(Repository repository, ATStateData atStateData) throws DataException {
|
||||||
ATData atData = repository.getATRepository().fromATAddress(atStateData.getATAddress());
|
ATData atData = repository.getATRepository().fromATAddress(atStateData.getATAddress());
|
||||||
return populateTradeData(repository, atData.getCreatorPublicKey(), atData.getCreation(), atStateData);
|
return populateTradeData(repository, atData.getCreatorPublicKey(), atData.getCreation(), atStateData, OptionalLong.empty());
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Returns CrossChainTradeData with useful info extracted from AT.
|
* Returns CrossChainTradeData with useful info extracted from AT.
|
||||||
*/
|
*/
|
||||||
public CrossChainTradeData populateTradeData(Repository repository, byte[] creatorPublicKey, long creationTimestamp, ATStateData atStateData) throws DataException {
|
@Override
|
||||||
|
public CrossChainTradeData populateTradeData(Repository repository, byte[] creatorPublicKey, long creationTimestamp, ATStateData atStateData, OptionalLong optionalBalance) throws DataException {
|
||||||
byte[] addressBytes = new byte[25]; // for general use
|
byte[] addressBytes = new byte[25]; // for general use
|
||||||
String atAddress = atStateData.getATAddress();
|
String atAddress = atStateData.getATAddress();
|
||||||
|
|
||||||
@ -636,8 +646,13 @@ public class BitcoinACCTv1 implements ACCT {
|
|||||||
tradeData.qortalCreator = Crypto.toAddress(creatorPublicKey);
|
tradeData.qortalCreator = Crypto.toAddress(creatorPublicKey);
|
||||||
tradeData.creationTimestamp = creationTimestamp;
|
tradeData.creationTimestamp = creationTimestamp;
|
||||||
|
|
||||||
|
if(optionalBalance.isPresent()) {
|
||||||
|
tradeData.qortBalance = optionalBalance.getAsLong();
|
||||||
|
}
|
||||||
|
else {
|
||||||
Account atAccount = new Account(repository, atAddress);
|
Account atAccount = new Account(repository, atAddress);
|
||||||
tradeData.qortBalance = atAccount.getConfirmedBalance(Asset.QORT);
|
tradeData.qortBalance = atAccount.getConfirmedBalance(Asset.QORT);
|
||||||
|
}
|
||||||
|
|
||||||
byte[] stateData = atStateData.getStateData();
|
byte[] stateData = atStateData.getStateData();
|
||||||
ByteBuffer dataByteBuffer = ByteBuffer.wrap(stateData);
|
ByteBuffer dataByteBuffer = ByteBuffer.wrap(stateData);
|
||||||
|
@ -6,6 +6,7 @@ import org.apache.logging.log4j.LogManager;
|
|||||||
import org.apache.logging.log4j.Logger;
|
import org.apache.logging.log4j.Logger;
|
||||||
import org.ciyam.at.*;
|
import org.ciyam.at.*;
|
||||||
import org.qortal.account.Account;
|
import org.qortal.account.Account;
|
||||||
|
import org.qortal.api.resource.CrossChainUtils;
|
||||||
import org.qortal.asset.Asset;
|
import org.qortal.asset.Asset;
|
||||||
import org.qortal.at.QortalFunctionCode;
|
import org.qortal.at.QortalFunctionCode;
|
||||||
import org.qortal.crypto.Crypto;
|
import org.qortal.crypto.Crypto;
|
||||||
@ -21,6 +22,7 @@ import org.qortal.utils.BitTwiddling;
|
|||||||
import java.nio.ByteBuffer;
|
import java.nio.ByteBuffer;
|
||||||
import java.util.Arrays;
|
import java.util.Arrays;
|
||||||
import java.util.List;
|
import java.util.List;
|
||||||
|
import java.util.OptionalLong;
|
||||||
|
|
||||||
import static org.ciyam.at.OpCode.calcOffset;
|
import static org.ciyam.at.OpCode.calcOffset;
|
||||||
|
|
||||||
@ -569,7 +571,14 @@ public class BitcoinACCTv3 implements ACCT {
|
|||||||
@Override
|
@Override
|
||||||
public CrossChainTradeData populateTradeData(Repository repository, ATData atData) throws DataException {
|
public CrossChainTradeData populateTradeData(Repository repository, ATData atData) throws DataException {
|
||||||
ATStateData atStateData = repository.getATRepository().getLatestATState(atData.getATAddress());
|
ATStateData atStateData = repository.getATRepository().getLatestATState(atData.getATAddress());
|
||||||
return populateTradeData(repository, atData.getCreatorPublicKey(), atData.getCreation(), atStateData);
|
return populateTradeData(repository, atData.getCreatorPublicKey(), atData.getCreation(), atStateData, OptionalLong.empty());
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public List<CrossChainTradeData> populateTradeDataList(Repository repository, List<ATData> atDataList) throws DataException {
|
||||||
|
List<CrossChainTradeData> crossChainTradeDataList = CrossChainUtils.populateTradeDataList(repository, this, atDataList);
|
||||||
|
|
||||||
|
return crossChainTradeDataList;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@ -578,13 +587,14 @@ public class BitcoinACCTv3 implements ACCT {
|
|||||||
@Override
|
@Override
|
||||||
public CrossChainTradeData populateTradeData(Repository repository, ATStateData atStateData) throws DataException {
|
public CrossChainTradeData populateTradeData(Repository repository, ATStateData atStateData) throws DataException {
|
||||||
ATData atData = repository.getATRepository().fromATAddress(atStateData.getATAddress());
|
ATData atData = repository.getATRepository().fromATAddress(atStateData.getATAddress());
|
||||||
return populateTradeData(repository, atData.getCreatorPublicKey(), atData.getCreation(), atStateData);
|
return populateTradeData(repository, atData.getCreatorPublicKey(), atData.getCreation(), atStateData, OptionalLong.empty());
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Returns CrossChainTradeData with useful info extracted from AT.
|
* Returns CrossChainTradeData with useful info extracted from AT.
|
||||||
*/
|
*/
|
||||||
public CrossChainTradeData populateTradeData(Repository repository, byte[] creatorPublicKey, long creationTimestamp, ATStateData atStateData) throws DataException {
|
@Override
|
||||||
|
public CrossChainTradeData populateTradeData(Repository repository, byte[] creatorPublicKey, long creationTimestamp, ATStateData atStateData, OptionalLong optionalBalance) throws DataException {
|
||||||
byte[] addressBytes = new byte[25]; // for general use
|
byte[] addressBytes = new byte[25]; // for general use
|
||||||
String atAddress = atStateData.getATAddress();
|
String atAddress = atStateData.getATAddress();
|
||||||
|
|
||||||
@ -597,8 +607,13 @@ public class BitcoinACCTv3 implements ACCT {
|
|||||||
tradeData.qortalCreator = Crypto.toAddress(creatorPublicKey);
|
tradeData.qortalCreator = Crypto.toAddress(creatorPublicKey);
|
||||||
tradeData.creationTimestamp = creationTimestamp;
|
tradeData.creationTimestamp = creationTimestamp;
|
||||||
|
|
||||||
|
if(optionalBalance.isPresent()) {
|
||||||
|
tradeData.qortBalance = optionalBalance.getAsLong();
|
||||||
|
}
|
||||||
|
else {
|
||||||
Account atAccount = new Account(repository, atAddress);
|
Account atAccount = new Account(repository, atAddress);
|
||||||
tradeData.qortBalance = atAccount.getConfirmedBalance(Asset.QORT);
|
tradeData.qortBalance = atAccount.getConfirmedBalance(Asset.QORT);
|
||||||
|
}
|
||||||
|
|
||||||
byte[] stateData = atStateData.getStateData();
|
byte[] stateData = atStateData.getStateData();
|
||||||
ByteBuffer dataByteBuffer = ByteBuffer.wrap(stateData);
|
ByteBuffer dataByteBuffer = ByteBuffer.wrap(stateData);
|
||||||
|
@ -840,9 +840,9 @@ public abstract class Bitcoiny implements ForeignBlockchain {
|
|||||||
} while (true);
|
} while (true);
|
||||||
}
|
}
|
||||||
|
|
||||||
public abstract long getFeeCeiling();
|
public abstract long getFeeRequired();
|
||||||
|
|
||||||
public abstract void setFeeCeiling(long fee);
|
public abstract void setFeeRequired(long fee);
|
||||||
|
|
||||||
// UTXOProvider support
|
// UTXOProvider support
|
||||||
|
|
||||||
|
@ -89,7 +89,7 @@ public class BitcoinyTBD extends Bitcoiny {
|
|||||||
NetTBD netTBD
|
NetTBD netTBD
|
||||||
= new NetTBD(
|
= new NetTBD(
|
||||||
bitcoinyTBDRequest.getNetworkName(),
|
bitcoinyTBDRequest.getNetworkName(),
|
||||||
bitcoinyTBDRequest.getFeeCeiling(),
|
bitcoinyTBDRequest.getFeeRequired(),
|
||||||
networkParams,
|
networkParams,
|
||||||
Collections.emptyList(),
|
Collections.emptyList(),
|
||||||
bitcoinyTBDRequest.getExpectedGenesisHash()
|
bitcoinyTBDRequest.getExpectedGenesisHash()
|
||||||
@ -134,18 +134,18 @@ public class BitcoinyTBD extends Bitcoiny {
|
|||||||
@Override
|
@Override
|
||||||
public long getP2shFee(Long timestamp) throws ForeignBlockchainException {
|
public long getP2shFee(Long timestamp) throws ForeignBlockchainException {
|
||||||
|
|
||||||
return this.netTBD.getFeeCeiling();
|
return this.netTBD.getFeeRequired();
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public long getFeeCeiling() {
|
public long getFeeRequired() {
|
||||||
|
|
||||||
return this.netTBD.getFeeCeiling();
|
return this.netTBD.getFeeRequired();
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public void setFeeCeiling(long fee) {
|
public void setFeeRequired(long fee) {
|
||||||
|
|
||||||
this.netTBD.setFeeCeiling( fee );
|
this.netTBD.setFeeRequired( fee );
|
||||||
}
|
}
|
||||||
}
|
}
|
@ -14,6 +14,7 @@ import java.util.Arrays;
|
|||||||
import java.util.Collection;
|
import java.util.Collection;
|
||||||
import java.util.EnumMap;
|
import java.util.EnumMap;
|
||||||
import java.util.Map;
|
import java.util.Map;
|
||||||
|
import java.util.concurrent.atomic.AtomicLong;
|
||||||
|
|
||||||
public class Digibyte extends Bitcoiny {
|
public class Digibyte extends Bitcoiny {
|
||||||
|
|
||||||
@ -59,7 +60,7 @@ public class Digibyte extends Bitcoiny {
|
|||||||
|
|
||||||
@Override
|
@Override
|
||||||
public long getP2shFee(Long timestamp) {
|
public long getP2shFee(Long timestamp) {
|
||||||
return this.getFeeCeiling();
|
return this.getFeeRequired();
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
TEST3 {
|
TEST3 {
|
||||||
@ -109,14 +110,14 @@ public class Digibyte extends Bitcoiny {
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
private long feeCeiling = MAINNET_FEE;
|
private AtomicLong feeRequired = new AtomicLong(MAINNET_FEE);
|
||||||
|
|
||||||
public long getFeeCeiling() {
|
public long getFeeRequired() {
|
||||||
return feeCeiling;
|
return feeRequired.get();
|
||||||
}
|
}
|
||||||
|
|
||||||
public void setFeeCeiling(long feeCeiling) {
|
public void setFeeRequired(long feeRequired) {
|
||||||
this.feeCeiling = feeCeiling;
|
this.feeRequired.set(feeRequired);
|
||||||
}
|
}
|
||||||
|
|
||||||
public abstract NetworkParameters getParams();
|
public abstract NetworkParameters getParams();
|
||||||
@ -178,13 +179,13 @@ public class Digibyte extends Bitcoiny {
|
|||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public long getFeeCeiling() {
|
public long getFeeRequired() {
|
||||||
return this.digibyteNet.getFeeCeiling();
|
return this.digibyteNet.getFeeRequired();
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public void setFeeCeiling(long fee) {
|
public void setFeeRequired(long fee) {
|
||||||
|
|
||||||
this.digibyteNet.setFeeCeiling( fee );
|
this.digibyteNet.setFeeRequired( fee );
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -6,6 +6,7 @@ import org.apache.logging.log4j.LogManager;
|
|||||||
import org.apache.logging.log4j.Logger;
|
import org.apache.logging.log4j.Logger;
|
||||||
import org.ciyam.at.*;
|
import org.ciyam.at.*;
|
||||||
import org.qortal.account.Account;
|
import org.qortal.account.Account;
|
||||||
|
import org.qortal.api.resource.CrossChainUtils;
|
||||||
import org.qortal.asset.Asset;
|
import org.qortal.asset.Asset;
|
||||||
import org.qortal.at.QortalFunctionCode;
|
import org.qortal.at.QortalFunctionCode;
|
||||||
import org.qortal.crypto.Crypto;
|
import org.qortal.crypto.Crypto;
|
||||||
@ -21,6 +22,7 @@ import org.qortal.utils.BitTwiddling;
|
|||||||
import java.nio.ByteBuffer;
|
import java.nio.ByteBuffer;
|
||||||
import java.util.Arrays;
|
import java.util.Arrays;
|
||||||
import java.util.List;
|
import java.util.List;
|
||||||
|
import java.util.OptionalLong;
|
||||||
|
|
||||||
import static org.ciyam.at.OpCode.calcOffset;
|
import static org.ciyam.at.OpCode.calcOffset;
|
||||||
|
|
||||||
@ -569,7 +571,14 @@ public class DigibyteACCTv3 implements ACCT {
|
|||||||
@Override
|
@Override
|
||||||
public CrossChainTradeData populateTradeData(Repository repository, ATData atData) throws DataException {
|
public CrossChainTradeData populateTradeData(Repository repository, ATData atData) throws DataException {
|
||||||
ATStateData atStateData = repository.getATRepository().getLatestATState(atData.getATAddress());
|
ATStateData atStateData = repository.getATRepository().getLatestATState(atData.getATAddress());
|
||||||
return populateTradeData(repository, atData.getCreatorPublicKey(), atData.getCreation(), atStateData);
|
return populateTradeData(repository, atData.getCreatorPublicKey(), atData.getCreation(), atStateData, OptionalLong.empty());
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public List<CrossChainTradeData> populateTradeDataList(Repository repository, List<ATData> atDataList) throws DataException {
|
||||||
|
List<CrossChainTradeData> crossChainTradeDataList = CrossChainUtils.populateTradeDataList(repository, this, atDataList);
|
||||||
|
|
||||||
|
return crossChainTradeDataList;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@ -578,13 +587,14 @@ public class DigibyteACCTv3 implements ACCT {
|
|||||||
@Override
|
@Override
|
||||||
public CrossChainTradeData populateTradeData(Repository repository, ATStateData atStateData) throws DataException {
|
public CrossChainTradeData populateTradeData(Repository repository, ATStateData atStateData) throws DataException {
|
||||||
ATData atData = repository.getATRepository().fromATAddress(atStateData.getATAddress());
|
ATData atData = repository.getATRepository().fromATAddress(atStateData.getATAddress());
|
||||||
return populateTradeData(repository, atData.getCreatorPublicKey(), atData.getCreation(), atStateData);
|
return populateTradeData(repository, atData.getCreatorPublicKey(), atData.getCreation(), atStateData, OptionalLong.empty());
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Returns CrossChainTradeData with useful info extracted from AT.
|
* Returns CrossChainTradeData with useful info extracted from AT.
|
||||||
*/
|
*/
|
||||||
public CrossChainTradeData populateTradeData(Repository repository, byte[] creatorPublicKey, long creationTimestamp, ATStateData atStateData) throws DataException {
|
@Override
|
||||||
|
public CrossChainTradeData populateTradeData(Repository repository, byte[] creatorPublicKey, long creationTimestamp, ATStateData atStateData, OptionalLong optionalBalance) throws DataException {
|
||||||
byte[] addressBytes = new byte[25]; // for general use
|
byte[] addressBytes = new byte[25]; // for general use
|
||||||
String atAddress = atStateData.getATAddress();
|
String atAddress = atStateData.getATAddress();
|
||||||
|
|
||||||
@ -597,8 +607,13 @@ public class DigibyteACCTv3 implements ACCT {
|
|||||||
tradeData.qortalCreator = Crypto.toAddress(creatorPublicKey);
|
tradeData.qortalCreator = Crypto.toAddress(creatorPublicKey);
|
||||||
tradeData.creationTimestamp = creationTimestamp;
|
tradeData.creationTimestamp = creationTimestamp;
|
||||||
|
|
||||||
|
if(optionalBalance.isPresent()) {
|
||||||
|
tradeData.qortBalance = optionalBalance.getAsLong();
|
||||||
|
}
|
||||||
|
else {
|
||||||
Account atAccount = new Account(repository, atAddress);
|
Account atAccount = new Account(repository, atAddress);
|
||||||
tradeData.qortBalance = atAccount.getConfirmedBalance(Asset.QORT);
|
tradeData.qortBalance = atAccount.getConfirmedBalance(Asset.QORT);
|
||||||
|
}
|
||||||
|
|
||||||
byte[] stateData = atStateData.getStateData();
|
byte[] stateData = atStateData.getStateData();
|
||||||
ByteBuffer dataByteBuffer = ByteBuffer.wrap(stateData);
|
ByteBuffer dataByteBuffer = ByteBuffer.wrap(stateData);
|
||||||
|
@ -13,6 +13,7 @@ import java.util.Arrays;
|
|||||||
import java.util.Collection;
|
import java.util.Collection;
|
||||||
import java.util.EnumMap;
|
import java.util.EnumMap;
|
||||||
import java.util.Map;
|
import java.util.Map;
|
||||||
|
import java.util.concurrent.atomic.AtomicLong;
|
||||||
|
|
||||||
public class Dogecoin extends Bitcoiny {
|
public class Dogecoin extends Bitcoiny {
|
||||||
|
|
||||||
@ -60,7 +61,7 @@ public class Dogecoin extends Bitcoiny {
|
|||||||
|
|
||||||
@Override
|
@Override
|
||||||
public long getP2shFee(Long timestamp) {
|
public long getP2shFee(Long timestamp) {
|
||||||
return this.getFeeCeiling();
|
return this.getFeeRequired();
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
TEST3 {
|
TEST3 {
|
||||||
@ -110,14 +111,14 @@ public class Dogecoin extends Bitcoiny {
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
private long feeCeiling = MAINNET_FEE;
|
private AtomicLong feeRequired = new AtomicLong(MAINNET_FEE);
|
||||||
|
|
||||||
public long getFeeCeiling() {
|
public long getFeeRequired() {
|
||||||
return feeCeiling;
|
return feeRequired.get();
|
||||||
}
|
}
|
||||||
|
|
||||||
public void setFeeCeiling(long feeCeiling) {
|
public void setFeeRequired(long feeRequired) {
|
||||||
this.feeCeiling = feeCeiling;
|
this.feeRequired.set(feeRequired);
|
||||||
}
|
}
|
||||||
|
|
||||||
public abstract NetworkParameters getParams();
|
public abstract NetworkParameters getParams();
|
||||||
@ -179,13 +180,13 @@ public class Dogecoin extends Bitcoiny {
|
|||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public long getFeeCeiling() {
|
public long getFeeRequired() {
|
||||||
return this.dogecoinNet.getFeeCeiling();
|
return this.dogecoinNet.getFeeRequired();
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public void setFeeCeiling(long fee) {
|
public void setFeeRequired(long fee) {
|
||||||
|
|
||||||
this.dogecoinNet.setFeeCeiling( fee );
|
this.dogecoinNet.setFeeRequired( fee );
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -6,6 +6,7 @@ import org.apache.logging.log4j.LogManager;
|
|||||||
import org.apache.logging.log4j.Logger;
|
import org.apache.logging.log4j.Logger;
|
||||||
import org.ciyam.at.*;
|
import org.ciyam.at.*;
|
||||||
import org.qortal.account.Account;
|
import org.qortal.account.Account;
|
||||||
|
import org.qortal.api.resource.CrossChainUtils;
|
||||||
import org.qortal.asset.Asset;
|
import org.qortal.asset.Asset;
|
||||||
import org.qortal.at.QortalFunctionCode;
|
import org.qortal.at.QortalFunctionCode;
|
||||||
import org.qortal.crypto.Crypto;
|
import org.qortal.crypto.Crypto;
|
||||||
@ -21,6 +22,7 @@ import org.qortal.utils.BitTwiddling;
|
|||||||
import java.nio.ByteBuffer;
|
import java.nio.ByteBuffer;
|
||||||
import java.util.Arrays;
|
import java.util.Arrays;
|
||||||
import java.util.List;
|
import java.util.List;
|
||||||
|
import java.util.OptionalLong;
|
||||||
|
|
||||||
import static org.ciyam.at.OpCode.calcOffset;
|
import static org.ciyam.at.OpCode.calcOffset;
|
||||||
|
|
||||||
@ -566,7 +568,14 @@ public class DogecoinACCTv1 implements ACCT {
|
|||||||
@Override
|
@Override
|
||||||
public CrossChainTradeData populateTradeData(Repository repository, ATData atData) throws DataException {
|
public CrossChainTradeData populateTradeData(Repository repository, ATData atData) throws DataException {
|
||||||
ATStateData atStateData = repository.getATRepository().getLatestATState(atData.getATAddress());
|
ATStateData atStateData = repository.getATRepository().getLatestATState(atData.getATAddress());
|
||||||
return populateTradeData(repository, atData.getCreatorPublicKey(), atData.getCreation(), atStateData);
|
return populateTradeData(repository, atData.getCreatorPublicKey(), atData.getCreation(), atStateData, OptionalLong.empty());
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public List<CrossChainTradeData> populateTradeDataList(Repository repository, List<ATData> atDataList) throws DataException {
|
||||||
|
List<CrossChainTradeData> crossChainTradeDataList = CrossChainUtils.populateTradeDataList(repository, this, atDataList);
|
||||||
|
|
||||||
|
return crossChainTradeDataList;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@ -575,13 +584,14 @@ public class DogecoinACCTv1 implements ACCT {
|
|||||||
@Override
|
@Override
|
||||||
public CrossChainTradeData populateTradeData(Repository repository, ATStateData atStateData) throws DataException {
|
public CrossChainTradeData populateTradeData(Repository repository, ATStateData atStateData) throws DataException {
|
||||||
ATData atData = repository.getATRepository().fromATAddress(atStateData.getATAddress());
|
ATData atData = repository.getATRepository().fromATAddress(atStateData.getATAddress());
|
||||||
return populateTradeData(repository, atData.getCreatorPublicKey(), atData.getCreation(), atStateData);
|
return populateTradeData(repository, atData.getCreatorPublicKey(), atData.getCreation(), atStateData, OptionalLong.empty());
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Returns CrossChainTradeData with useful info extracted from AT.
|
* Returns CrossChainTradeData with useful info extracted from AT.
|
||||||
*/
|
*/
|
||||||
public CrossChainTradeData populateTradeData(Repository repository, byte[] creatorPublicKey, long creationTimestamp, ATStateData atStateData) throws DataException {
|
@Override
|
||||||
|
public CrossChainTradeData populateTradeData(Repository repository, byte[] creatorPublicKey, long creationTimestamp, ATStateData atStateData, OptionalLong optionalBalance) throws DataException {
|
||||||
byte[] addressBytes = new byte[25]; // for general use
|
byte[] addressBytes = new byte[25]; // for general use
|
||||||
String atAddress = atStateData.getATAddress();
|
String atAddress = atStateData.getATAddress();
|
||||||
|
|
||||||
@ -594,8 +604,13 @@ public class DogecoinACCTv1 implements ACCT {
|
|||||||
tradeData.qortalCreator = Crypto.toAddress(creatorPublicKey);
|
tradeData.qortalCreator = Crypto.toAddress(creatorPublicKey);
|
||||||
tradeData.creationTimestamp = creationTimestamp;
|
tradeData.creationTimestamp = creationTimestamp;
|
||||||
|
|
||||||
|
if(optionalBalance.isPresent()) {
|
||||||
|
tradeData.qortBalance = optionalBalance.getAsLong();
|
||||||
|
}
|
||||||
|
else {
|
||||||
Account atAccount = new Account(repository, atAddress);
|
Account atAccount = new Account(repository, atAddress);
|
||||||
tradeData.qortBalance = atAccount.getConfirmedBalance(Asset.QORT);
|
tradeData.qortBalance = atAccount.getConfirmedBalance(Asset.QORT);
|
||||||
|
}
|
||||||
|
|
||||||
byte[] stateData = atStateData.getStateData();
|
byte[] stateData = atStateData.getStateData();
|
||||||
ByteBuffer dataByteBuffer = ByteBuffer.wrap(stateData);
|
ByteBuffer dataByteBuffer = ByteBuffer.wrap(stateData);
|
||||||
|
@ -6,6 +6,7 @@ import org.apache.logging.log4j.LogManager;
|
|||||||
import org.apache.logging.log4j.Logger;
|
import org.apache.logging.log4j.Logger;
|
||||||
import org.ciyam.at.*;
|
import org.ciyam.at.*;
|
||||||
import org.qortal.account.Account;
|
import org.qortal.account.Account;
|
||||||
|
import org.qortal.api.resource.CrossChainUtils;
|
||||||
import org.qortal.asset.Asset;
|
import org.qortal.asset.Asset;
|
||||||
import org.qortal.at.QortalFunctionCode;
|
import org.qortal.at.QortalFunctionCode;
|
||||||
import org.qortal.crypto.Crypto;
|
import org.qortal.crypto.Crypto;
|
||||||
@ -21,6 +22,7 @@ import org.qortal.utils.BitTwiddling;
|
|||||||
import java.nio.ByteBuffer;
|
import java.nio.ByteBuffer;
|
||||||
import java.util.Arrays;
|
import java.util.Arrays;
|
||||||
import java.util.List;
|
import java.util.List;
|
||||||
|
import java.util.OptionalLong;
|
||||||
|
|
||||||
import static org.ciyam.at.OpCode.calcOffset;
|
import static org.ciyam.at.OpCode.calcOffset;
|
||||||
|
|
||||||
@ -569,7 +571,14 @@ public class DogecoinACCTv3 implements ACCT {
|
|||||||
@Override
|
@Override
|
||||||
public CrossChainTradeData populateTradeData(Repository repository, ATData atData) throws DataException {
|
public CrossChainTradeData populateTradeData(Repository repository, ATData atData) throws DataException {
|
||||||
ATStateData atStateData = repository.getATRepository().getLatestATState(atData.getATAddress());
|
ATStateData atStateData = repository.getATRepository().getLatestATState(atData.getATAddress());
|
||||||
return populateTradeData(repository, atData.getCreatorPublicKey(), atData.getCreation(), atStateData);
|
return populateTradeData(repository, atData.getCreatorPublicKey(), atData.getCreation(), atStateData, OptionalLong.empty());
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public List<CrossChainTradeData> populateTradeDataList(Repository repository, List<ATData> atDataList) throws DataException {
|
||||||
|
List<CrossChainTradeData> crossChainTradeDataList = CrossChainUtils.populateTradeDataList(repository, this, atDataList);
|
||||||
|
|
||||||
|
return crossChainTradeDataList;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@ -578,13 +587,14 @@ public class DogecoinACCTv3 implements ACCT {
|
|||||||
@Override
|
@Override
|
||||||
public CrossChainTradeData populateTradeData(Repository repository, ATStateData atStateData) throws DataException {
|
public CrossChainTradeData populateTradeData(Repository repository, ATStateData atStateData) throws DataException {
|
||||||
ATData atData = repository.getATRepository().fromATAddress(atStateData.getATAddress());
|
ATData atData = repository.getATRepository().fromATAddress(atStateData.getATAddress());
|
||||||
return populateTradeData(repository, atData.getCreatorPublicKey(), atData.getCreation(), atStateData);
|
return populateTradeData(repository, atData.getCreatorPublicKey(), atData.getCreation(), atStateData, OptionalLong.empty());
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Returns CrossChainTradeData with useful info extracted from AT.
|
* Returns CrossChainTradeData with useful info extracted from AT.
|
||||||
*/
|
*/
|
||||||
public CrossChainTradeData populateTradeData(Repository repository, byte[] creatorPublicKey, long creationTimestamp, ATStateData atStateData) throws DataException {
|
@Override
|
||||||
|
public CrossChainTradeData populateTradeData(Repository repository, byte[] creatorPublicKey, long creationTimestamp, ATStateData atStateData, OptionalLong optionalBalance) throws DataException {
|
||||||
byte[] addressBytes = new byte[25]; // for general use
|
byte[] addressBytes = new byte[25]; // for general use
|
||||||
String atAddress = atStateData.getATAddress();
|
String atAddress = atStateData.getATAddress();
|
||||||
|
|
||||||
@ -597,8 +607,13 @@ public class DogecoinACCTv3 implements ACCT {
|
|||||||
tradeData.qortalCreator = Crypto.toAddress(creatorPublicKey);
|
tradeData.qortalCreator = Crypto.toAddress(creatorPublicKey);
|
||||||
tradeData.creationTimestamp = creationTimestamp;
|
tradeData.creationTimestamp = creationTimestamp;
|
||||||
|
|
||||||
|
if(optionalBalance.isPresent()) {
|
||||||
|
tradeData.qortBalance = optionalBalance.getAsLong();
|
||||||
|
}
|
||||||
|
else {
|
||||||
Account atAccount = new Account(repository, atAddress);
|
Account atAccount = new Account(repository, atAddress);
|
||||||
tradeData.qortBalance = atAccount.getConfirmedBalance(Asset.QORT);
|
tradeData.qortBalance = atAccount.getConfirmedBalance(Asset.QORT);
|
||||||
|
}
|
||||||
|
|
||||||
byte[] stateData = atStateData.getStateData();
|
byte[] stateData = atStateData.getStateData();
|
||||||
ByteBuffer dataByteBuffer = ByteBuffer.wrap(stateData);
|
ByteBuffer dataByteBuffer = ByteBuffer.wrap(stateData);
|
||||||
|
@ -14,6 +14,7 @@ import java.util.Arrays;
|
|||||||
import java.util.Collection;
|
import java.util.Collection;
|
||||||
import java.util.EnumMap;
|
import java.util.EnumMap;
|
||||||
import java.util.Map;
|
import java.util.Map;
|
||||||
|
import java.util.concurrent.atomic.AtomicLong;
|
||||||
|
|
||||||
public class Litecoin extends Bitcoiny {
|
public class Litecoin extends Bitcoiny {
|
||||||
|
|
||||||
@ -63,7 +64,7 @@ public class Litecoin extends Bitcoiny {
|
|||||||
|
|
||||||
@Override
|
@Override
|
||||||
public long getP2shFee(Long timestamp) {
|
public long getP2shFee(Long timestamp) {
|
||||||
return this.getFeeCeiling();
|
return this.getFeeRequired();
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
TEST3 {
|
TEST3 {
|
||||||
@ -116,14 +117,14 @@ public class Litecoin extends Bitcoiny {
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
private long feeCeiling = MAINNET_FEE;
|
private AtomicLong feeRequired = new AtomicLong(MAINNET_FEE);
|
||||||
|
|
||||||
public long getFeeCeiling() {
|
public long getFeeRequired() {
|
||||||
return feeCeiling;
|
return feeRequired.get();
|
||||||
}
|
}
|
||||||
|
|
||||||
public void setFeeCeiling(long feeCeiling) {
|
public void setFeeRequired(long feeRequired) {
|
||||||
this.feeCeiling = feeCeiling;
|
this.feeRequired.set(feeRequired);
|
||||||
}
|
}
|
||||||
|
|
||||||
public abstract NetworkParameters getParams();
|
public abstract NetworkParameters getParams();
|
||||||
@ -185,13 +186,13 @@ public class Litecoin extends Bitcoiny {
|
|||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public long getFeeCeiling() {
|
public long getFeeRequired() {
|
||||||
return this.litecoinNet.getFeeCeiling();
|
return this.litecoinNet.getFeeRequired();
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public void setFeeCeiling(long fee) {
|
public void setFeeRequired(long fee) {
|
||||||
|
|
||||||
this.litecoinNet.setFeeCeiling( fee );
|
this.litecoinNet.setFeeRequired( fee );
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -4,6 +4,7 @@ import com.google.common.hash.HashCode;
|
|||||||
import com.google.common.primitives.Bytes;
|
import com.google.common.primitives.Bytes;
|
||||||
import org.ciyam.at.*;
|
import org.ciyam.at.*;
|
||||||
import org.qortal.account.Account;
|
import org.qortal.account.Account;
|
||||||
|
import org.qortal.api.resource.CrossChainUtils;
|
||||||
import org.qortal.asset.Asset;
|
import org.qortal.asset.Asset;
|
||||||
import org.qortal.at.QortalFunctionCode;
|
import org.qortal.at.QortalFunctionCode;
|
||||||
import org.qortal.crypto.Crypto;
|
import org.qortal.crypto.Crypto;
|
||||||
@ -19,6 +20,7 @@ import org.qortal.utils.BitTwiddling;
|
|||||||
import java.nio.ByteBuffer;
|
import java.nio.ByteBuffer;
|
||||||
import java.util.Arrays;
|
import java.util.Arrays;
|
||||||
import java.util.List;
|
import java.util.List;
|
||||||
|
import java.util.OptionalLong;
|
||||||
|
|
||||||
import static org.ciyam.at.OpCode.calcOffset;
|
import static org.ciyam.at.OpCode.calcOffset;
|
||||||
|
|
||||||
@ -559,7 +561,14 @@ public class LitecoinACCTv1 implements ACCT {
|
|||||||
@Override
|
@Override
|
||||||
public CrossChainTradeData populateTradeData(Repository repository, ATData atData) throws DataException {
|
public CrossChainTradeData populateTradeData(Repository repository, ATData atData) throws DataException {
|
||||||
ATStateData atStateData = repository.getATRepository().getLatestATState(atData.getATAddress());
|
ATStateData atStateData = repository.getATRepository().getLatestATState(atData.getATAddress());
|
||||||
return populateTradeData(repository, atData.getCreatorPublicKey(), atData.getCreation(), atStateData);
|
return populateTradeData(repository, atData.getCreatorPublicKey(), atData.getCreation(), atStateData, OptionalLong.empty());
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public List<CrossChainTradeData> populateTradeDataList(Repository repository, List<ATData> atDataList) throws DataException {
|
||||||
|
List<CrossChainTradeData> crossChainTradeDataList = CrossChainUtils.populateTradeDataList(repository, this, atDataList);
|
||||||
|
|
||||||
|
return crossChainTradeDataList;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@ -568,13 +577,14 @@ public class LitecoinACCTv1 implements ACCT {
|
|||||||
@Override
|
@Override
|
||||||
public CrossChainTradeData populateTradeData(Repository repository, ATStateData atStateData) throws DataException {
|
public CrossChainTradeData populateTradeData(Repository repository, ATStateData atStateData) throws DataException {
|
||||||
ATData atData = repository.getATRepository().fromATAddress(atStateData.getATAddress());
|
ATData atData = repository.getATRepository().fromATAddress(atStateData.getATAddress());
|
||||||
return populateTradeData(repository, atData.getCreatorPublicKey(), atData.getCreation(), atStateData);
|
return populateTradeData(repository, atData.getCreatorPublicKey(), atData.getCreation(), atStateData, OptionalLong.empty());
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Returns CrossChainTradeData with useful info extracted from AT.
|
* Returns CrossChainTradeData with useful info extracted from AT.
|
||||||
*/
|
*/
|
||||||
public CrossChainTradeData populateTradeData(Repository repository, byte[] creatorPublicKey, long creationTimestamp, ATStateData atStateData) throws DataException {
|
@Override
|
||||||
|
public CrossChainTradeData populateTradeData(Repository repository, byte[] creatorPublicKey, long creationTimestamp, ATStateData atStateData, OptionalLong optionalBalance) throws DataException {
|
||||||
byte[] addressBytes = new byte[25]; // for general use
|
byte[] addressBytes = new byte[25]; // for general use
|
||||||
String atAddress = atStateData.getATAddress();
|
String atAddress = atStateData.getATAddress();
|
||||||
|
|
||||||
@ -587,8 +597,13 @@ public class LitecoinACCTv1 implements ACCT {
|
|||||||
tradeData.qortalCreator = Crypto.toAddress(creatorPublicKey);
|
tradeData.qortalCreator = Crypto.toAddress(creatorPublicKey);
|
||||||
tradeData.creationTimestamp = creationTimestamp;
|
tradeData.creationTimestamp = creationTimestamp;
|
||||||
|
|
||||||
|
if(optionalBalance.isPresent()) {
|
||||||
|
tradeData.qortBalance = optionalBalance.getAsLong();
|
||||||
|
}
|
||||||
|
else {
|
||||||
Account atAccount = new Account(repository, atAddress);
|
Account atAccount = new Account(repository, atAddress);
|
||||||
tradeData.qortBalance = atAccount.getConfirmedBalance(Asset.QORT);
|
tradeData.qortBalance = atAccount.getConfirmedBalance(Asset.QORT);
|
||||||
|
}
|
||||||
|
|
||||||
byte[] stateData = atStateData.getStateData();
|
byte[] stateData = atStateData.getStateData();
|
||||||
ByteBuffer dataByteBuffer = ByteBuffer.wrap(stateData);
|
ByteBuffer dataByteBuffer = ByteBuffer.wrap(stateData);
|
||||||
|
@ -4,6 +4,7 @@ import com.google.common.hash.HashCode;
|
|||||||
import com.google.common.primitives.Bytes;
|
import com.google.common.primitives.Bytes;
|
||||||
import org.ciyam.at.*;
|
import org.ciyam.at.*;
|
||||||
import org.qortal.account.Account;
|
import org.qortal.account.Account;
|
||||||
|
import org.qortal.api.resource.CrossChainUtils;
|
||||||
import org.qortal.asset.Asset;
|
import org.qortal.asset.Asset;
|
||||||
import org.qortal.at.QortalFunctionCode;
|
import org.qortal.at.QortalFunctionCode;
|
||||||
import org.qortal.crypto.Crypto;
|
import org.qortal.crypto.Crypto;
|
||||||
@ -19,6 +20,7 @@ import org.qortal.utils.BitTwiddling;
|
|||||||
import java.nio.ByteBuffer;
|
import java.nio.ByteBuffer;
|
||||||
import java.util.Arrays;
|
import java.util.Arrays;
|
||||||
import java.util.List;
|
import java.util.List;
|
||||||
|
import java.util.OptionalLong;
|
||||||
|
|
||||||
import static org.ciyam.at.OpCode.calcOffset;
|
import static org.ciyam.at.OpCode.calcOffset;
|
||||||
|
|
||||||
@ -562,7 +564,14 @@ public class LitecoinACCTv3 implements ACCT {
|
|||||||
@Override
|
@Override
|
||||||
public CrossChainTradeData populateTradeData(Repository repository, ATData atData) throws DataException {
|
public CrossChainTradeData populateTradeData(Repository repository, ATData atData) throws DataException {
|
||||||
ATStateData atStateData = repository.getATRepository().getLatestATState(atData.getATAddress());
|
ATStateData atStateData = repository.getATRepository().getLatestATState(atData.getATAddress());
|
||||||
return populateTradeData(repository, atData.getCreatorPublicKey(), atData.getCreation(), atStateData);
|
return populateTradeData(repository, atData.getCreatorPublicKey(), atData.getCreation(), atStateData, OptionalLong.empty());
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public List<CrossChainTradeData> populateTradeDataList(Repository repository, List<ATData> atDataList) throws DataException {
|
||||||
|
List<CrossChainTradeData> crossChainTradeDataList = CrossChainUtils.populateTradeDataList(repository, this, atDataList);
|
||||||
|
|
||||||
|
return crossChainTradeDataList;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@ -571,13 +580,14 @@ public class LitecoinACCTv3 implements ACCT {
|
|||||||
@Override
|
@Override
|
||||||
public CrossChainTradeData populateTradeData(Repository repository, ATStateData atStateData) throws DataException {
|
public CrossChainTradeData populateTradeData(Repository repository, ATStateData atStateData) throws DataException {
|
||||||
ATData atData = repository.getATRepository().fromATAddress(atStateData.getATAddress());
|
ATData atData = repository.getATRepository().fromATAddress(atStateData.getATAddress());
|
||||||
return populateTradeData(repository, atData.getCreatorPublicKey(), atData.getCreation(), atStateData);
|
return populateTradeData(repository, atData.getCreatorPublicKey(), atData.getCreation(), atStateData, OptionalLong.empty());
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Returns CrossChainTradeData with useful info extracted from AT.
|
* Returns CrossChainTradeData with useful info extracted from AT.
|
||||||
*/
|
*/
|
||||||
public CrossChainTradeData populateTradeData(Repository repository, byte[] creatorPublicKey, long creationTimestamp, ATStateData atStateData) throws DataException {
|
@Override
|
||||||
|
public CrossChainTradeData populateTradeData(Repository repository, byte[] creatorPublicKey, long creationTimestamp, ATStateData atStateData, OptionalLong optionalBalance) throws DataException {
|
||||||
byte[] addressBytes = new byte[25]; // for general use
|
byte[] addressBytes = new byte[25]; // for general use
|
||||||
String atAddress = atStateData.getATAddress();
|
String atAddress = atStateData.getATAddress();
|
||||||
|
|
||||||
@ -590,8 +600,13 @@ public class LitecoinACCTv3 implements ACCT {
|
|||||||
tradeData.qortalCreator = Crypto.toAddress(creatorPublicKey);
|
tradeData.qortalCreator = Crypto.toAddress(creatorPublicKey);
|
||||||
tradeData.creationTimestamp = creationTimestamp;
|
tradeData.creationTimestamp = creationTimestamp;
|
||||||
|
|
||||||
|
if(optionalBalance.isPresent()) {
|
||||||
|
tradeData.qortBalance = optionalBalance.getAsLong();
|
||||||
|
}
|
||||||
|
else {
|
||||||
Account atAccount = new Account(repository, atAddress);
|
Account atAccount = new Account(repository, atAddress);
|
||||||
tradeData.qortBalance = atAccount.getConfirmedBalance(Asset.QORT);
|
tradeData.qortBalance = atAccount.getConfirmedBalance(Asset.QORT);
|
||||||
|
}
|
||||||
|
|
||||||
byte[] stateData = atStateData.getStateData();
|
byte[] stateData = atStateData.getStateData();
|
||||||
ByteBuffer dataByteBuffer = ByteBuffer.wrap(stateData);
|
ByteBuffer dataByteBuffer = ByteBuffer.wrap(stateData);
|
||||||
|
@ -3,18 +3,19 @@ package org.qortal.crosschain;
|
|||||||
import org.bitcoinj.core.NetworkParameters;
|
import org.bitcoinj.core.NetworkParameters;
|
||||||
|
|
||||||
import java.util.Collection;
|
import java.util.Collection;
|
||||||
|
import java.util.concurrent.atomic.AtomicLong;
|
||||||
|
|
||||||
public class NetTBD {
|
public class NetTBD {
|
||||||
|
|
||||||
private String name;
|
private String name;
|
||||||
private long feeCeiling;
|
private AtomicLong feeRequired;
|
||||||
private NetworkParameters params;
|
private NetworkParameters params;
|
||||||
private Collection<ElectrumX.Server> servers;
|
private Collection<ElectrumX.Server> servers;
|
||||||
private String genesisHash;
|
private String genesisHash;
|
||||||
|
|
||||||
public NetTBD(String name, long feeCeiling, NetworkParameters params, Collection<ElectrumX.Server> servers, String genesisHash) {
|
public NetTBD(String name, long feeRequired, NetworkParameters params, Collection<ElectrumX.Server> servers, String genesisHash) {
|
||||||
this.name = name;
|
this.name = name;
|
||||||
this.feeCeiling = feeCeiling;
|
this.feeRequired = new AtomicLong(feeRequired);
|
||||||
this.params = params;
|
this.params = params;
|
||||||
this.servers = servers;
|
this.servers = servers;
|
||||||
this.genesisHash = genesisHash;
|
this.genesisHash = genesisHash;
|
||||||
@ -25,14 +26,14 @@ public class NetTBD {
|
|||||||
return this.name;
|
return this.name;
|
||||||
}
|
}
|
||||||
|
|
||||||
public long getFeeCeiling() {
|
public long getFeeRequired() {
|
||||||
|
|
||||||
return feeCeiling;
|
return feeRequired.get();
|
||||||
}
|
}
|
||||||
|
|
||||||
public void setFeeCeiling(long feeCeiling) {
|
public void setFeeRequired(long feeRequired) {
|
||||||
|
|
||||||
this.feeCeiling = feeCeiling;
|
this.feeRequired.set(feeRequired);
|
||||||
}
|
}
|
||||||
|
|
||||||
public NetworkParameters getParams() {
|
public NetworkParameters getParams() {
|
||||||
|
@ -21,6 +21,7 @@ import org.qortal.utils.BitTwiddling;
|
|||||||
|
|
||||||
import java.nio.ByteBuffer;
|
import java.nio.ByteBuffer;
|
||||||
import java.util.*;
|
import java.util.*;
|
||||||
|
import java.util.concurrent.atomic.AtomicLong;
|
||||||
|
|
||||||
public class PirateChain extends Bitcoiny {
|
public class PirateChain extends Bitcoiny {
|
||||||
|
|
||||||
@ -51,12 +52,7 @@ public class PirateChain extends Bitcoiny {
|
|||||||
public Collection<Server> getServers() {
|
public Collection<Server> getServers() {
|
||||||
return Arrays.asList(
|
return Arrays.asList(
|
||||||
// Servers chosen on NO BASIS WHATSOEVER from various sources!
|
// Servers chosen on NO BASIS WHATSOEVER from various sources!
|
||||||
new Server("lightd.pirate.black", Server.ConnectionType.SSL, 443),
|
new Server("lightd.pirate.black", Server.ConnectionType.SSL, 443)
|
||||||
new Server("wallet-arrr1.qortal.online", Server.ConnectionType.SSL, 443),
|
|
||||||
new Server("wallet-arrr2.qortal.online", Server.ConnectionType.SSL, 443),
|
|
||||||
new Server("wallet-arrr3.qortal.online", Server.ConnectionType.SSL, 443),
|
|
||||||
new Server("wallet-arrr4.qortal.online", Server.ConnectionType.SSL, 443),
|
|
||||||
new Server("wallet-arrr5.qortal.online", Server.ConnectionType.SSL, 443)
|
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -67,7 +63,7 @@ public class PirateChain extends Bitcoiny {
|
|||||||
|
|
||||||
@Override
|
@Override
|
||||||
public long getP2shFee(Long timestamp) {
|
public long getP2shFee(Long timestamp) {
|
||||||
return this.getFeeCeiling();
|
return this.getFeeRequired();
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
TEST3 {
|
TEST3 {
|
||||||
@ -117,14 +113,14 @@ public class PirateChain extends Bitcoiny {
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
private long feeCeiling = MAINNET_FEE;
|
private AtomicLong feeRequired = new AtomicLong(MAINNET_FEE);
|
||||||
|
|
||||||
public long getFeeCeiling() {
|
public long getFeeRequired() {
|
||||||
return feeCeiling;
|
return feeRequired.get();
|
||||||
}
|
}
|
||||||
|
|
||||||
public void setFeeCeiling(long feeCeiling) {
|
public void setFeeRequired(long feeRequired) {
|
||||||
this.feeCeiling = feeCeiling;
|
this.feeRequired.set(feeRequired);
|
||||||
}
|
}
|
||||||
|
|
||||||
public abstract NetworkParameters getParams();
|
public abstract NetworkParameters getParams();
|
||||||
@ -186,14 +182,14 @@ public class PirateChain extends Bitcoiny {
|
|||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public long getFeeCeiling() {
|
public long getFeeRequired() {
|
||||||
return this.pirateChainNet.getFeeCeiling();
|
return this.pirateChainNet.getFeeRequired();
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public void setFeeCeiling(long fee) {
|
public void setFeeRequired(long fee) {
|
||||||
|
|
||||||
this.pirateChainNet.setFeeCeiling( fee );
|
this.pirateChainNet.setFeeRequired( fee );
|
||||||
}
|
}
|
||||||
/**
|
/**
|
||||||
* Returns confirmed balance, based on passed payment script.
|
* Returns confirmed balance, based on passed payment script.
|
||||||
|
@ -4,6 +4,7 @@ import com.google.common.hash.HashCode;
|
|||||||
import com.google.common.primitives.Bytes;
|
import com.google.common.primitives.Bytes;
|
||||||
import org.ciyam.at.*;
|
import org.ciyam.at.*;
|
||||||
import org.qortal.account.Account;
|
import org.qortal.account.Account;
|
||||||
|
import org.qortal.api.resource.CrossChainUtils;
|
||||||
import org.qortal.asset.Asset;
|
import org.qortal.asset.Asset;
|
||||||
import org.qortal.at.QortalFunctionCode;
|
import org.qortal.at.QortalFunctionCode;
|
||||||
import org.qortal.crypto.Crypto;
|
import org.qortal.crypto.Crypto;
|
||||||
@ -19,6 +20,7 @@ import org.qortal.utils.BitTwiddling;
|
|||||||
import java.nio.ByteBuffer;
|
import java.nio.ByteBuffer;
|
||||||
import java.util.Arrays;
|
import java.util.Arrays;
|
||||||
import java.util.List;
|
import java.util.List;
|
||||||
|
import java.util.OptionalLong;
|
||||||
|
|
||||||
import static org.ciyam.at.OpCode.calcOffset;
|
import static org.ciyam.at.OpCode.calcOffset;
|
||||||
|
|
||||||
@ -580,7 +582,14 @@ public class PirateChainACCTv3 implements ACCT {
|
|||||||
@Override
|
@Override
|
||||||
public CrossChainTradeData populateTradeData(Repository repository, ATData atData) throws DataException {
|
public CrossChainTradeData populateTradeData(Repository repository, ATData atData) throws DataException {
|
||||||
ATStateData atStateData = repository.getATRepository().getLatestATState(atData.getATAddress());
|
ATStateData atStateData = repository.getATRepository().getLatestATState(atData.getATAddress());
|
||||||
return populateTradeData(repository, atData.getCreatorPublicKey(), atData.getCreation(), atStateData);
|
return populateTradeData(repository, atData.getCreatorPublicKey(), atData.getCreation(), atStateData, OptionalLong.empty());
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public List<CrossChainTradeData> populateTradeDataList(Repository repository, List<ATData> atDataList) throws DataException {
|
||||||
|
List<CrossChainTradeData> crossChainTradeDataList = CrossChainUtils.populateTradeDataList(repository, this, atDataList);
|
||||||
|
|
||||||
|
return crossChainTradeDataList;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@ -589,13 +598,14 @@ public class PirateChainACCTv3 implements ACCT {
|
|||||||
@Override
|
@Override
|
||||||
public CrossChainTradeData populateTradeData(Repository repository, ATStateData atStateData) throws DataException {
|
public CrossChainTradeData populateTradeData(Repository repository, ATStateData atStateData) throws DataException {
|
||||||
ATData atData = repository.getATRepository().fromATAddress(atStateData.getATAddress());
|
ATData atData = repository.getATRepository().fromATAddress(atStateData.getATAddress());
|
||||||
return populateTradeData(repository, atData.getCreatorPublicKey(), atData.getCreation(), atStateData);
|
return populateTradeData(repository, atData.getCreatorPublicKey(), atData.getCreation(), atStateData, OptionalLong.empty());
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Returns CrossChainTradeData with useful info extracted from AT.
|
* Returns CrossChainTradeData with useful info extracted from AT.
|
||||||
*/
|
*/
|
||||||
public CrossChainTradeData populateTradeData(Repository repository, byte[] creatorPublicKey, long creationTimestamp, ATStateData atStateData) throws DataException {
|
@Override
|
||||||
|
public CrossChainTradeData populateTradeData(Repository repository, byte[] creatorPublicKey, long creationTimestamp, ATStateData atStateData, OptionalLong optionalBalance) throws DataException {
|
||||||
byte[] addressBytes = new byte[25]; // for general use
|
byte[] addressBytes = new byte[25]; // for general use
|
||||||
String atAddress = atStateData.getATAddress();
|
String atAddress = atStateData.getATAddress();
|
||||||
|
|
||||||
@ -608,8 +618,13 @@ public class PirateChainACCTv3 implements ACCT {
|
|||||||
tradeData.qortalCreator = Crypto.toAddress(creatorPublicKey);
|
tradeData.qortalCreator = Crypto.toAddress(creatorPublicKey);
|
||||||
tradeData.creationTimestamp = creationTimestamp;
|
tradeData.creationTimestamp = creationTimestamp;
|
||||||
|
|
||||||
|
if(optionalBalance.isPresent()) {
|
||||||
|
tradeData.qortBalance = optionalBalance.getAsLong();
|
||||||
|
}
|
||||||
|
else {
|
||||||
Account atAccount = new Account(repository, atAddress);
|
Account atAccount = new Account(repository, atAddress);
|
||||||
tradeData.qortBalance = atAccount.getConfirmedBalance(Asset.QORT);
|
tradeData.qortBalance = atAccount.getConfirmedBalance(Asset.QORT);
|
||||||
|
}
|
||||||
|
|
||||||
byte[] stateData = atStateData.getStateData();
|
byte[] stateData = atStateData.getStateData();
|
||||||
ByteBuffer dataByteBuffer = ByteBuffer.wrap(stateData);
|
ByteBuffer dataByteBuffer = ByteBuffer.wrap(stateData);
|
||||||
|
@ -8,6 +8,7 @@ import org.bouncycastle.util.encoders.DecoderException;
|
|||||||
import org.json.JSONArray;
|
import org.json.JSONArray;
|
||||||
import org.json.JSONException;
|
import org.json.JSONException;
|
||||||
import org.json.JSONObject;
|
import org.json.JSONObject;
|
||||||
|
import org.qortal.api.resource.CrossChainUtils;
|
||||||
import org.qortal.controller.PirateChainWalletController;
|
import org.qortal.controller.PirateChainWalletController;
|
||||||
import org.qortal.crypto.Crypto;
|
import org.qortal.crypto.Crypto;
|
||||||
import org.qortal.settings.Settings;
|
import org.qortal.settings.Settings;
|
||||||
@ -67,8 +68,8 @@ public class PirateWallet {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Pick a random server
|
// Pick a random server
|
||||||
PirateLightClient.Server server = this.getRandomServer();
|
ChainableServer server = PirateChain.getInstance().blockchainProvider.getCurrentServer();
|
||||||
String serverUri = String.format("https://%s:%d/", server.hostname, server.port);
|
String serverUri = String.format("https://%s:%d/", server.getHostName(), server.getPort());
|
||||||
|
|
||||||
// Pirate library uses base64 encoding
|
// Pirate library uses base64 encoding
|
||||||
String entropy64 = Base64.toBase64String(this.entropyBytes);
|
String entropy64 = Base64.toBase64String(this.entropyBytes);
|
||||||
|
@ -14,6 +14,7 @@ import java.util.Arrays;
|
|||||||
import java.util.Collection;
|
import java.util.Collection;
|
||||||
import java.util.EnumMap;
|
import java.util.EnumMap;
|
||||||
import java.util.Map;
|
import java.util.Map;
|
||||||
|
import java.util.concurrent.atomic.AtomicLong;
|
||||||
|
|
||||||
public class Ravencoin extends Bitcoiny {
|
public class Ravencoin extends Bitcoiny {
|
||||||
|
|
||||||
@ -61,7 +62,7 @@ public class Ravencoin extends Bitcoiny {
|
|||||||
|
|
||||||
@Override
|
@Override
|
||||||
public long getP2shFee(Long timestamp) {
|
public long getP2shFee(Long timestamp) {
|
||||||
return this.getFeeCeiling();
|
return this.getFeeRequired();
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
TEST3 {
|
TEST3 {
|
||||||
@ -111,14 +112,14 @@ public class Ravencoin extends Bitcoiny {
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
private long feeCeiling = MAINNET_FEE;
|
private AtomicLong feeRequired = new AtomicLong( MAINNET_FEE );
|
||||||
|
|
||||||
public long getFeeCeiling() {
|
public long getFeeRequired() {
|
||||||
return feeCeiling;
|
return feeRequired.get();
|
||||||
}
|
}
|
||||||
|
|
||||||
public void setFeeCeiling(long feeCeiling) {
|
public void setFeeRequired(long feeRequired) {
|
||||||
this.feeCeiling = feeCeiling;
|
this.feeRequired.set(feeRequired);
|
||||||
}
|
}
|
||||||
|
|
||||||
public abstract NetworkParameters getParams();
|
public abstract NetworkParameters getParams();
|
||||||
@ -180,13 +181,13 @@ public class Ravencoin extends Bitcoiny {
|
|||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public long getFeeCeiling() {
|
public long getFeeRequired() {
|
||||||
return this.ravencoinNet.getFeeCeiling();
|
return this.ravencoinNet.getFeeRequired();
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public void setFeeCeiling(long fee) {
|
public void setFeeRequired(long fee) {
|
||||||
|
|
||||||
this.ravencoinNet.setFeeCeiling( fee );
|
this.ravencoinNet.setFeeRequired( fee );
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -6,6 +6,7 @@ import org.apache.logging.log4j.LogManager;
|
|||||||
import org.apache.logging.log4j.Logger;
|
import org.apache.logging.log4j.Logger;
|
||||||
import org.ciyam.at.*;
|
import org.ciyam.at.*;
|
||||||
import org.qortal.account.Account;
|
import org.qortal.account.Account;
|
||||||
|
import org.qortal.api.resource.CrossChainUtils;
|
||||||
import org.qortal.asset.Asset;
|
import org.qortal.asset.Asset;
|
||||||
import org.qortal.at.QortalFunctionCode;
|
import org.qortal.at.QortalFunctionCode;
|
||||||
import org.qortal.crypto.Crypto;
|
import org.qortal.crypto.Crypto;
|
||||||
@ -21,6 +22,7 @@ import org.qortal.utils.BitTwiddling;
|
|||||||
import java.nio.ByteBuffer;
|
import java.nio.ByteBuffer;
|
||||||
import java.util.Arrays;
|
import java.util.Arrays;
|
||||||
import java.util.List;
|
import java.util.List;
|
||||||
|
import java.util.OptionalLong;
|
||||||
|
|
||||||
import static org.ciyam.at.OpCode.calcOffset;
|
import static org.ciyam.at.OpCode.calcOffset;
|
||||||
|
|
||||||
@ -569,7 +571,14 @@ public class RavencoinACCTv3 implements ACCT {
|
|||||||
@Override
|
@Override
|
||||||
public CrossChainTradeData populateTradeData(Repository repository, ATData atData) throws DataException {
|
public CrossChainTradeData populateTradeData(Repository repository, ATData atData) throws DataException {
|
||||||
ATStateData atStateData = repository.getATRepository().getLatestATState(atData.getATAddress());
|
ATStateData atStateData = repository.getATRepository().getLatestATState(atData.getATAddress());
|
||||||
return populateTradeData(repository, atData.getCreatorPublicKey(), atData.getCreation(), atStateData);
|
return populateTradeData(repository, atData.getCreatorPublicKey(), atData.getCreation(), atStateData, OptionalLong.empty());
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public List<CrossChainTradeData> populateTradeDataList(Repository repository, List<ATData> atDataList) throws DataException {
|
||||||
|
List<CrossChainTradeData> crossChainTradeDataList = CrossChainUtils.populateTradeDataList(repository, this, atDataList);
|
||||||
|
|
||||||
|
return crossChainTradeDataList;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@ -578,13 +587,14 @@ public class RavencoinACCTv3 implements ACCT {
|
|||||||
@Override
|
@Override
|
||||||
public CrossChainTradeData populateTradeData(Repository repository, ATStateData atStateData) throws DataException {
|
public CrossChainTradeData populateTradeData(Repository repository, ATStateData atStateData) throws DataException {
|
||||||
ATData atData = repository.getATRepository().fromATAddress(atStateData.getATAddress());
|
ATData atData = repository.getATRepository().fromATAddress(atStateData.getATAddress());
|
||||||
return populateTradeData(repository, atData.getCreatorPublicKey(), atData.getCreation(), atStateData);
|
return populateTradeData(repository, atData.getCreatorPublicKey(), atData.getCreation(), atStateData, OptionalLong.empty());
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Returns CrossChainTradeData with useful info extracted from AT.
|
* Returns CrossChainTradeData with useful info extracted from AT.
|
||||||
*/
|
*/
|
||||||
public CrossChainTradeData populateTradeData(Repository repository, byte[] creatorPublicKey, long creationTimestamp, ATStateData atStateData) throws DataException {
|
@Override
|
||||||
|
public CrossChainTradeData populateTradeData(Repository repository, byte[] creatorPublicKey, long creationTimestamp, ATStateData atStateData, OptionalLong optionalBalance) throws DataException {
|
||||||
byte[] addressBytes = new byte[25]; // for general use
|
byte[] addressBytes = new byte[25]; // for general use
|
||||||
String atAddress = atStateData.getATAddress();
|
String atAddress = atStateData.getATAddress();
|
||||||
|
|
||||||
@ -597,8 +607,13 @@ public class RavencoinACCTv3 implements ACCT {
|
|||||||
tradeData.qortalCreator = Crypto.toAddress(creatorPublicKey);
|
tradeData.qortalCreator = Crypto.toAddress(creatorPublicKey);
|
||||||
tradeData.creationTimestamp = creationTimestamp;
|
tradeData.creationTimestamp = creationTimestamp;
|
||||||
|
|
||||||
|
if(optionalBalance.isPresent()) {
|
||||||
|
tradeData.qortBalance = optionalBalance.getAsLong();
|
||||||
|
}
|
||||||
|
else {
|
||||||
Account atAccount = new Account(repository, atAddress);
|
Account atAccount = new Account(repository, atAddress);
|
||||||
tradeData.qortBalance = atAccount.getConfirmedBalance(Asset.QORT);
|
tradeData.qortBalance = atAccount.getConfirmedBalance(Asset.QORT);
|
||||||
|
}
|
||||||
|
|
||||||
byte[] stateData = atStateData.getStateData();
|
byte[] stateData = atStateData.getStateData();
|
||||||
ByteBuffer dataByteBuffer = ByteBuffer.wrap(stateData);
|
ByteBuffer dataByteBuffer = ByteBuffer.wrap(stateData);
|
||||||
|
@ -100,7 +100,7 @@ public class AES {
|
|||||||
// Prepend the output stream with the 16 byte initialization vector
|
// Prepend the output stream with the 16 byte initialization vector
|
||||||
outputStream.write(iv.getIV());
|
outputStream.write(iv.getIV());
|
||||||
|
|
||||||
byte[] buffer = new byte[1024];
|
byte[] buffer = new byte[65536];
|
||||||
int bytesRead;
|
int bytesRead;
|
||||||
while ((bytesRead = inputStream.read(buffer)) != -1) {
|
while ((bytesRead = inputStream.read(buffer)) != -1) {
|
||||||
byte[] output = cipher.update(buffer, 0, bytesRead);
|
byte[] output = cipher.update(buffer, 0, bytesRead);
|
||||||
@ -138,7 +138,7 @@ public class AES {
|
|||||||
Cipher cipher = Cipher.getInstance(algorithm);
|
Cipher cipher = Cipher.getInstance(algorithm);
|
||||||
cipher.init(Cipher.DECRYPT_MODE, key, new IvParameterSpec(iv));
|
cipher.init(Cipher.DECRYPT_MODE, key, new IvParameterSpec(iv));
|
||||||
|
|
||||||
byte[] buffer = new byte[64];
|
byte[] buffer = new byte[65536];
|
||||||
int bytesRead;
|
int bytesRead;
|
||||||
while ((bytesRead = inputStream.read(buffer)) != -1) {
|
while ((bytesRead = inputStream.read(buffer)) != -1) {
|
||||||
byte[] output = cipher.update(buffer, 0, bytesRead);
|
byte[] output = cipher.update(buffer, 0, bytesRead);
|
||||||
|
57
src/main/java/org/qortal/data/crosschain/ForeignFeeData.java
Normal file
57
src/main/java/org/qortal/data/crosschain/ForeignFeeData.java
Normal file
@ -0,0 +1,57 @@
|
|||||||
|
package org.qortal.data.crosschain;
|
||||||
|
|
||||||
|
import org.json.JSONObject;
|
||||||
|
|
||||||
|
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 ForeignFeeData {
|
||||||
|
|
||||||
|
private String blockchain;
|
||||||
|
|
||||||
|
@XmlJavaTypeAdapter(value = org.qortal.api.AmountTypeAdapter.class)
|
||||||
|
private long fee;
|
||||||
|
|
||||||
|
protected ForeignFeeData() {
|
||||||
|
/* JAXB */
|
||||||
|
}
|
||||||
|
|
||||||
|
public ForeignFeeData(String blockchain,
|
||||||
|
long fee) {
|
||||||
|
this.blockchain = blockchain;
|
||||||
|
this.fee = fee;
|
||||||
|
}
|
||||||
|
|
||||||
|
public String getBlockchain() {
|
||||||
|
return this.blockchain;
|
||||||
|
}
|
||||||
|
|
||||||
|
public long getFee() {
|
||||||
|
return this.fee;
|
||||||
|
}
|
||||||
|
|
||||||
|
public JSONObject toJson() {
|
||||||
|
JSONObject jsonObject = new JSONObject();
|
||||||
|
jsonObject.put("blockchain", this.getBlockchain());
|
||||||
|
jsonObject.put("fee", this.getFee());
|
||||||
|
return jsonObject;
|
||||||
|
}
|
||||||
|
|
||||||
|
public static ForeignFeeData fromJson(JSONObject json) {
|
||||||
|
return new ForeignFeeData(
|
||||||
|
json.isNull("blockchain") ? null : json.getString("blockchain"),
|
||||||
|
json.isNull("fee") ? null : json.getLong("fee")
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public String toString() {
|
||||||
|
return "ForeignFeeData{" +
|
||||||
|
"blockchain='" + blockchain + '\'' +
|
||||||
|
", fee=" + fee +
|
||||||
|
'}';
|
||||||
|
}
|
||||||
|
}
|
@ -0,0 +1,90 @@
|
|||||||
|
package org.qortal.data.crosschain;
|
||||||
|
|
||||||
|
import org.json.JSONObject;
|
||||||
|
import org.qortal.data.account.MintingAccountData;
|
||||||
|
import org.qortal.utils.Base58;
|
||||||
|
|
||||||
|
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 ForeignFeeDecodedData {
|
||||||
|
|
||||||
|
protected long timestamp;
|
||||||
|
protected byte[] data;
|
||||||
|
protected String atAddress;
|
||||||
|
protected Integer fee;
|
||||||
|
|
||||||
|
// Constructors
|
||||||
|
|
||||||
|
// necessary for JAXB serialization
|
||||||
|
protected ForeignFeeDecodedData() {
|
||||||
|
}
|
||||||
|
|
||||||
|
public ForeignFeeDecodedData(long timestamp, byte[] data, String atAddress, Integer fee) {
|
||||||
|
this.timestamp = timestamp;
|
||||||
|
this.data = data;
|
||||||
|
this.atAddress = atAddress;
|
||||||
|
this.fee = fee;
|
||||||
|
}
|
||||||
|
|
||||||
|
public long getTimestamp() {
|
||||||
|
return this.timestamp;
|
||||||
|
}
|
||||||
|
|
||||||
|
public byte[] getData() {
|
||||||
|
return this.data;
|
||||||
|
}
|
||||||
|
|
||||||
|
public String getAtAddress() {
|
||||||
|
return atAddress;
|
||||||
|
}
|
||||||
|
|
||||||
|
public Integer getFee() {
|
||||||
|
return this.fee;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Comparison
|
||||||
|
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public boolean equals(Object o) {
|
||||||
|
if (this == o) return true;
|
||||||
|
if (o == null || getClass() != o.getClass()) return false;
|
||||||
|
ForeignFeeDecodedData that = (ForeignFeeDecodedData) o;
|
||||||
|
return timestamp == that.timestamp && Objects.equals(atAddress, that.atAddress) && Objects.equals(fee, that.fee);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public int hashCode() {
|
||||||
|
return Objects.hash(timestamp, atAddress, fee);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public String toString() {
|
||||||
|
return "ForeignFeeDecodedData{" +
|
||||||
|
"timestamp=" + timestamp +
|
||||||
|
", atAddress='" + atAddress + '\'' +
|
||||||
|
", fee=" + fee +
|
||||||
|
'}';
|
||||||
|
}
|
||||||
|
|
||||||
|
public JSONObject toJson() {
|
||||||
|
JSONObject jsonObject = new JSONObject();
|
||||||
|
jsonObject.put("data", Base58.encode(this.data));
|
||||||
|
jsonObject.put("atAddress", this.atAddress);
|
||||||
|
jsonObject.put("timestamp", this.timestamp);
|
||||||
|
jsonObject.put("fee", this.fee);
|
||||||
|
return jsonObject;
|
||||||
|
}
|
||||||
|
|
||||||
|
public static ForeignFeeDecodedData fromJson(JSONObject json) {
|
||||||
|
return new ForeignFeeDecodedData(
|
||||||
|
json.isNull("timestamp") ? null : json.getLong("timestamp"),
|
||||||
|
json.isNull("data") ? null : Base58.decode(json.getString("data")),
|
||||||
|
json.isNull("atAddress") ? null : json.getString("atAddress"),
|
||||||
|
json.isNull("fee") ? null : json.getInt("fee"));
|
||||||
|
}
|
||||||
|
}
|
@ -0,0 +1,69 @@
|
|||||||
|
package org.qortal.data.crosschain;
|
||||||
|
|
||||||
|
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 ForeignFeeEncodedData {
|
||||||
|
|
||||||
|
protected long timestamp;
|
||||||
|
protected String data;
|
||||||
|
protected String atAddress;
|
||||||
|
protected Integer fee;
|
||||||
|
|
||||||
|
// Constructors
|
||||||
|
|
||||||
|
// necessary for JAXB serialization
|
||||||
|
protected ForeignFeeEncodedData() {
|
||||||
|
}
|
||||||
|
|
||||||
|
public ForeignFeeEncodedData(long timestamp, String data, String atAddress, Integer fee) {
|
||||||
|
this.timestamp = timestamp;
|
||||||
|
this.data = data;
|
||||||
|
this.atAddress = atAddress;
|
||||||
|
this.fee = fee;
|
||||||
|
}
|
||||||
|
|
||||||
|
public long getTimestamp() {
|
||||||
|
return this.timestamp;
|
||||||
|
}
|
||||||
|
|
||||||
|
public String getData() {
|
||||||
|
return this.data;
|
||||||
|
}
|
||||||
|
|
||||||
|
public String getAtAddress() {
|
||||||
|
return atAddress;
|
||||||
|
}
|
||||||
|
|
||||||
|
public Integer getFee() {
|
||||||
|
return this.fee;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Comparison
|
||||||
|
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public boolean equals(Object o) {
|
||||||
|
if (this == o) return true;
|
||||||
|
if (o == null || getClass() != o.getClass()) return false;
|
||||||
|
ForeignFeeEncodedData that = (ForeignFeeEncodedData) o;
|
||||||
|
return timestamp == that.timestamp && Objects.equals(atAddress, that.atAddress) && Objects.equals(fee, that.fee);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public int hashCode() {
|
||||||
|
return Objects.hash(timestamp, atAddress, fee);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public String toString() {
|
||||||
|
return "ForeignFeeDecodedData{" +
|
||||||
|
"timestamp=" + timestamp +
|
||||||
|
", atAddress='" + atAddress + '\'' +
|
||||||
|
", fee=" + fee +
|
||||||
|
'}';
|
||||||
|
}
|
||||||
|
}
|
@ -0,0 +1,29 @@
|
|||||||
|
package org.qortal.data.crosschain;
|
||||||
|
|
||||||
|
import javax.xml.bind.annotation.XmlAccessType;
|
||||||
|
import javax.xml.bind.annotation.XmlAccessorType;
|
||||||
|
|
||||||
|
@XmlAccessorType(XmlAccessType.FIELD)
|
||||||
|
public class UnsignedFeeEvent {
|
||||||
|
|
||||||
|
private boolean positive;
|
||||||
|
|
||||||
|
private String address;
|
||||||
|
|
||||||
|
public UnsignedFeeEvent() {
|
||||||
|
}
|
||||||
|
|
||||||
|
public UnsignedFeeEvent(boolean positive, String address) {
|
||||||
|
|
||||||
|
this.positive = positive;
|
||||||
|
this.address = address;
|
||||||
|
}
|
||||||
|
|
||||||
|
public boolean isPositive() {
|
||||||
|
return positive;
|
||||||
|
}
|
||||||
|
|
||||||
|
public String getAddress() {
|
||||||
|
return address;
|
||||||
|
}
|
||||||
|
}
|
@ -67,6 +67,11 @@ public class NameData {
|
|||||||
this(name, reducedName, owner, data, registered, null, false, null, reference, creationGroupId);
|
this(name, reducedName, owner, data, registered, null, false, null, reference, creationGroupId);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Typically used for name summsry
|
||||||
|
public NameData(String name, String owner) {
|
||||||
|
this(name, null, owner, null, 0L, null, false, null, null, 0);
|
||||||
|
}
|
||||||
|
|
||||||
// Getters / setters
|
// Getters / setters
|
||||||
|
|
||||||
public String getName() {
|
public String getName() {
|
||||||
|
30
src/main/java/org/qortal/event/FeeWaitingEvent.java
Normal file
30
src/main/java/org/qortal/event/FeeWaitingEvent.java
Normal file
@ -0,0 +1,30 @@
|
|||||||
|
package org.qortal.event;
|
||||||
|
|
||||||
|
import javax.xml.bind.annotation.XmlAccessType;
|
||||||
|
import javax.xml.bind.annotation.XmlAccessorType;
|
||||||
|
|
||||||
|
@XmlAccessorType(XmlAccessType.FIELD)
|
||||||
|
public class FeeWaitingEvent implements Event{
|
||||||
|
|
||||||
|
private boolean positive;
|
||||||
|
|
||||||
|
private String address;
|
||||||
|
|
||||||
|
public FeeWaitingEvent() {
|
||||||
|
}
|
||||||
|
|
||||||
|
public FeeWaitingEvent(boolean positive, String address) {
|
||||||
|
|
||||||
|
this.positive = positive;
|
||||||
|
this.address = address;
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
public boolean isPositive() {
|
||||||
|
return positive;
|
||||||
|
}
|
||||||
|
|
||||||
|
public String getAddress() {
|
||||||
|
return address;
|
||||||
|
}
|
||||||
|
}
|
@ -0,0 +1,4 @@
|
|||||||
|
package org.qortal.event;
|
||||||
|
|
||||||
|
public class LockingFeeUpdateEvent implements Event{
|
||||||
|
}
|
15
src/main/java/org/qortal/event/RequiredFeeUpdateEvent.java
Normal file
15
src/main/java/org/qortal/event/RequiredFeeUpdateEvent.java
Normal file
@ -0,0 +1,15 @@
|
|||||||
|
package org.qortal.event;
|
||||||
|
|
||||||
|
import org.qortal.crosschain.Bitcoiny;
|
||||||
|
|
||||||
|
public class RequiredFeeUpdateEvent implements Event{
|
||||||
|
private final Bitcoiny bitcoiny;
|
||||||
|
|
||||||
|
public RequiredFeeUpdateEvent(Bitcoiny bitcoiny) {
|
||||||
|
this.bitcoiny = bitcoiny;
|
||||||
|
}
|
||||||
|
|
||||||
|
public Bitcoiny getBitcoiny() {
|
||||||
|
return bitcoiny;
|
||||||
|
}
|
||||||
|
}
|
@ -714,6 +714,7 @@ public class Network {
|
|||||||
// We can't block here so use tryRepository(). We don't NEED to connect a new peer.
|
// We can't block here so use tryRepository(). We don't NEED to connect a new peer.
|
||||||
try (Repository repository = RepositoryManager.tryRepository()) {
|
try (Repository repository = RepositoryManager.tryRepository()) {
|
||||||
if (repository == null) {
|
if (repository == null) {
|
||||||
|
LOGGER.warn("Unable to get repository connection : Network.getConnectablePeer()");
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -982,7 +983,7 @@ public class Network {
|
|||||||
if (maxThreadsForMessageType != null) {
|
if (maxThreadsForMessageType != null) {
|
||||||
Integer threadCount = threadsPerMessageType.get(message.getType());
|
Integer threadCount = threadsPerMessageType.get(message.getType());
|
||||||
if (threadCount != null && threadCount >= maxThreadsForMessageType) {
|
if (threadCount != null && threadCount >= maxThreadsForMessageType) {
|
||||||
LOGGER.trace("Discarding {} message as there are already {} active threads", message.getType().name(), threadCount);
|
LOGGER.warn("Discarding {} message as there are already {} active threads", message.getType().name(), threadCount);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -1499,6 +1500,7 @@ public class Network {
|
|||||||
// Pruning peers isn't critical so no need to block for a repository instance.
|
// Pruning peers isn't critical so no need to block for a repository instance.
|
||||||
try (Repository repository = RepositoryManager.tryRepository()) {
|
try (Repository repository = RepositoryManager.tryRepository()) {
|
||||||
if (repository == null) {
|
if (repository == null) {
|
||||||
|
LOGGER.warn("Unable to get repository connection : Network.prunePeers()");
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -1567,6 +1569,7 @@ public class Network {
|
|||||||
// Merging peers isn't critical so don't block for a repository instance.
|
// Merging peers isn't critical so don't block for a repository instance.
|
||||||
try (Repository repository = RepositoryManager.tryRepository()) {
|
try (Repository repository = RepositoryManager.tryRepository()) {
|
||||||
if (repository == null) {
|
if (repository == null) {
|
||||||
|
LOGGER.warn("Unable to get repository connection : Network.opportunisticMergePeers()");
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -0,0 +1,43 @@
|
|||||||
|
package org.qortal.network.message;
|
||||||
|
|
||||||
|
import org.qortal.data.crosschain.ForeignFeeDecodedData;
|
||||||
|
import org.qortal.utils.ForeignFeesMessageUtils;
|
||||||
|
|
||||||
|
import java.nio.ByteBuffer;
|
||||||
|
import java.util.List;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* For sending online accounts info to remote peer.
|
||||||
|
*
|
||||||
|
* Same format as V2, but with added support for a mempow nonce.
|
||||||
|
*/
|
||||||
|
public class ForeignFeesMessage extends Message {
|
||||||
|
|
||||||
|
public static final long MIN_PEER_VERSION = 0x300060000L; // 3.6.0
|
||||||
|
|
||||||
|
private List<ForeignFeeDecodedData> foreignFees;
|
||||||
|
|
||||||
|
public ForeignFeesMessage(List<ForeignFeeDecodedData> foreignFeeDecodedData) {
|
||||||
|
super(MessageType.FOREIGN_FEES);
|
||||||
|
|
||||||
|
this.dataBytes = ForeignFeesMessageUtils.fromDataToSendBytes(foreignFeeDecodedData);
|
||||||
|
this.checksumBytes = Message.generateChecksum(this.dataBytes);
|
||||||
|
}
|
||||||
|
|
||||||
|
private ForeignFeesMessage(int id, List<ForeignFeeDecodedData> foreignFees) {
|
||||||
|
super(id, MessageType.FOREIGN_FEES);
|
||||||
|
|
||||||
|
this.foreignFees = foreignFees;
|
||||||
|
}
|
||||||
|
|
||||||
|
public List<ForeignFeeDecodedData> getForeignFees() {
|
||||||
|
return this.foreignFees;
|
||||||
|
}
|
||||||
|
|
||||||
|
public static Message fromByteBuffer(int id, ByteBuffer bytes) throws MessageException {
|
||||||
|
List<ForeignFeeDecodedData> foreignFeeDecodedData = ForeignFeesMessageUtils.fromSendBytesToData(bytes);
|
||||||
|
|
||||||
|
return new ForeignFeesMessage(id, foreignFeeDecodedData);
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
@ -0,0 +1,46 @@
|
|||||||
|
package org.qortal.network.message;
|
||||||
|
|
||||||
|
import org.qortal.data.crosschain.ForeignFeeDecodedData;
|
||||||
|
import org.qortal.utils.ForeignFeesMessageUtils;
|
||||||
|
|
||||||
|
import java.nio.ByteBuffer;
|
||||||
|
import java.util.Collections;
|
||||||
|
import java.util.List;
|
||||||
|
import java.util.Map;
|
||||||
|
|
||||||
|
public class GetForeignFeesMessage extends Message {
|
||||||
|
|
||||||
|
private static final Map<Long, Map<Byte, byte[]>> EMPTY_ONLINE_ACCOUNTS = Collections.emptyMap();
|
||||||
|
private final List<ForeignFeeDecodedData> foreignFeeDecodedData;
|
||||||
|
|
||||||
|
public GetForeignFeesMessage(List<ForeignFeeDecodedData> foreignFeeDecodedData) {
|
||||||
|
super(MessageType.GET_FOREIGN_FEES);
|
||||||
|
|
||||||
|
this.foreignFeeDecodedData = foreignFeeDecodedData;
|
||||||
|
|
||||||
|
// If we don't have ANY online accounts then it's an easier construction...
|
||||||
|
if (foreignFeeDecodedData.isEmpty()) {
|
||||||
|
this.dataBytes = EMPTY_DATA_BYTES;
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
this.dataBytes = ForeignFeesMessageUtils.fromDataToGetBytes(foreignFeeDecodedData);
|
||||||
|
this.checksumBytes = Message.generateChecksum(this.dataBytes);
|
||||||
|
}
|
||||||
|
|
||||||
|
private GetForeignFeesMessage(int id, List<ForeignFeeDecodedData> foreignFeeDecodedData) {
|
||||||
|
super(id, MessageType.GET_FOREIGN_FEES);
|
||||||
|
|
||||||
|
this.foreignFeeDecodedData = foreignFeeDecodedData;
|
||||||
|
}
|
||||||
|
|
||||||
|
public List<ForeignFeeDecodedData> getForeignFeeData() {
|
||||||
|
return foreignFeeDecodedData;
|
||||||
|
}
|
||||||
|
|
||||||
|
public static Message fromByteBuffer(int id, ByteBuffer bytes) {
|
||||||
|
|
||||||
|
return new GetForeignFeesMessage(id, ForeignFeesMessageUtils.fromGetBytesToData(bytes));
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
@ -79,7 +79,10 @@ public enum MessageType {
|
|||||||
GET_NAME(182, GetNameMessage::fromByteBuffer),
|
GET_NAME(182, GetNameMessage::fromByteBuffer),
|
||||||
|
|
||||||
TRANSACTIONS(190, TransactionsMessage::fromByteBuffer),
|
TRANSACTIONS(190, TransactionsMessage::fromByteBuffer),
|
||||||
GET_ACCOUNT_TRANSACTIONS(191, GetAccountTransactionsMessage::fromByteBuffer);
|
GET_ACCOUNT_TRANSACTIONS(191, GetAccountTransactionsMessage::fromByteBuffer),
|
||||||
|
|
||||||
|
FOREIGN_FEES( 200, ForeignFeesMessage::fromByteBuffer),
|
||||||
|
GET_FOREIGN_FEES( 201, GetForeignFeesMessage::fromByteBuffer);
|
||||||
|
|
||||||
public final int value;
|
public final int value;
|
||||||
public final MessageProducer fromByteBufferMethod;
|
public final MessageProducer fromByteBufferMethod;
|
||||||
|
@ -14,6 +14,8 @@ public interface ATRepository {
|
|||||||
/** Returns ATData using AT's address or null if none found */
|
/** Returns ATData using AT's address or null if none found */
|
||||||
public ATData fromATAddress(String atAddress) throws DataException;
|
public ATData fromATAddress(String atAddress) throws DataException;
|
||||||
|
|
||||||
|
public List<ATData> fromATAddresses(List<String> atAddresses) throws DataException;
|
||||||
|
|
||||||
/** Returns where AT with passed address exists in repository */
|
/** Returns where AT with passed address exists in repository */
|
||||||
public boolean exists(String atAddress) throws DataException;
|
public boolean exists(String atAddress) throws DataException;
|
||||||
|
|
||||||
@ -62,6 +64,8 @@ public interface ATRepository {
|
|||||||
*/
|
*/
|
||||||
public ATStateData getLatestATState(String atAddress) throws DataException;
|
public ATStateData getLatestATState(String atAddress) throws DataException;
|
||||||
|
|
||||||
|
public List<ATStateData> getLatestATStates(List<String> collect) throws DataException;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Returns final ATStateData for ATs matching codeHash (required)
|
* Returns final ATStateData for ATs matching codeHash (required)
|
||||||
* and specific data segment value (optional).
|
* and specific data segment value (optional).
|
||||||
|
@ -130,6 +130,8 @@ public interface AccountRepository {
|
|||||||
*/
|
*/
|
||||||
public AccountBalanceData getBalance(String address, long assetId) throws DataException;
|
public AccountBalanceData getBalance(String address, long assetId) throws DataException;
|
||||||
|
|
||||||
|
public List<AccountBalanceData> getBalances(List<String> addresses, long assetId) throws DataException;
|
||||||
|
|
||||||
/** Returns all account balances for given assetID, optionally excluding zero balances. */
|
/** Returns all account balances for given assetID, optionally excluding zero balances. */
|
||||||
public List<AccountBalanceData> getAssetBalances(long assetId, Boolean excludeZero) throws DataException;
|
public List<AccountBalanceData> getAssetBalances(long assetId, Boolean excludeZero) throws DataException;
|
||||||
|
|
||||||
|
@ -3,6 +3,7 @@ package org.qortal.repository;
|
|||||||
import org.qortal.data.naming.NameData;
|
import org.qortal.data.naming.NameData;
|
||||||
|
|
||||||
import java.util.List;
|
import java.util.List;
|
||||||
|
import java.util.Optional;
|
||||||
|
|
||||||
public interface NameRepository {
|
public interface NameRepository {
|
||||||
|
|
||||||
@ -34,10 +35,17 @@ public interface NameRepository {
|
|||||||
return getNamesByOwner(address, null, null, null);
|
return getNamesByOwner(address, null, null, null);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public int setPrimaryName(String address, String primaryName) throws DataException;
|
||||||
|
|
||||||
|
public void removePrimaryName(String address) throws DataException;
|
||||||
|
|
||||||
|
public Optional<String> getPrimaryName(String address) throws DataException;
|
||||||
|
|
||||||
|
public int clearPrimaryNames() throws DataException;
|
||||||
|
|
||||||
public List<String> getRecentNames(long startTimestamp) throws DataException;
|
public List<String> getRecentNames(long startTimestamp) throws DataException;
|
||||||
|
|
||||||
public void save(NameData nameData) throws DataException;
|
public void save(NameData nameData) throws DataException;
|
||||||
|
|
||||||
public void delete(String name) throws DataException;
|
public void delete(String name) throws DataException;
|
||||||
|
|
||||||
}
|
}
|
||||||
|
@ -1,6 +1,7 @@
|
|||||||
package org.qortal.repository;
|
package org.qortal.repository;
|
||||||
|
|
||||||
import java.io.IOException;
|
import java.io.IOException;
|
||||||
|
import java.sql.Connection;
|
||||||
import java.util.concurrent.TimeoutException;
|
import java.util.concurrent.TimeoutException;
|
||||||
|
|
||||||
public interface Repository extends AutoCloseable {
|
public interface Repository extends AutoCloseable {
|
||||||
@ -62,4 +63,5 @@ public interface Repository extends AutoCloseable {
|
|||||||
|
|
||||||
public static void attemptRecovery(String connectionUrl, String name) throws DataException {}
|
public static void attemptRecovery(String connectionUrl, String name) throws DataException {}
|
||||||
|
|
||||||
|
public Connection getConnection();
|
||||||
}
|
}
|
||||||
|
@ -18,6 +18,8 @@ public interface TransactionRepository {
|
|||||||
|
|
||||||
public TransactionData fromSignature(byte[] signature) throws DataException;
|
public TransactionData fromSignature(byte[] signature) throws DataException;
|
||||||
|
|
||||||
|
public List<TransactionData> fromSignatures(List<byte[]> signatures) throws DataException;
|
||||||
|
|
||||||
public TransactionData fromReference(byte[] reference) throws DataException;
|
public TransactionData fromReference(byte[] reference) throws DataException;
|
||||||
|
|
||||||
public TransactionData fromHeightAndSequence(int height, int sequence) throws DataException;
|
public TransactionData fromHeightAndSequence(int height, int sequence) throws DataException;
|
||||||
@ -351,4 +353,5 @@ public interface TransactionRepository {
|
|||||||
|
|
||||||
public void delete(TransactionData transactionData) throws DataException;
|
public void delete(TransactionData transactionData) throws DataException;
|
||||||
|
|
||||||
|
|
||||||
}
|
}
|
||||||
|
@ -15,8 +15,12 @@ import org.qortal.utils.ByteArray;
|
|||||||
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.Collections;
|
||||||
|
import java.util.Comparator;
|
||||||
import java.util.List;
|
import java.util.List;
|
||||||
|
import java.util.Map;
|
||||||
import java.util.Set;
|
import java.util.Set;
|
||||||
|
import java.util.stream.Collectors;
|
||||||
|
|
||||||
import org.qortal.data.account.AccountData;
|
import org.qortal.data.account.AccountData;
|
||||||
|
|
||||||
@ -76,6 +80,63 @@ public class HSQLDBATRepository implements ATRepository {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public List<ATData> fromATAddresses(List<String> atAddresses) throws DataException {
|
||||||
|
String sql = "SELECT creator, created_when, version, asset_id, code_bytes, code_hash, "
|
||||||
|
+ "is_sleeping, sleep_until_height, is_finished, had_fatal_error, "
|
||||||
|
+ "is_frozen, frozen_balance, sleep_until_message_timestamp, AT_address "
|
||||||
|
+ "FROM ATs "
|
||||||
|
+ "WHERE AT_address IN ("
|
||||||
|
+ String.join(", ", Collections.nCopies(atAddresses.size(), "?"))
|
||||||
|
+ ")"
|
||||||
|
;
|
||||||
|
|
||||||
|
List<ATData> list;
|
||||||
|
try (ResultSet resultSet = this.repository.checkedExecute(sql, atAddresses.toArray(new String[atAddresses.size()]))) {
|
||||||
|
if (resultSet == null) {
|
||||||
|
return new ArrayList<>(0);
|
||||||
|
}
|
||||||
|
|
||||||
|
list = new ArrayList<>(atAddresses.size());
|
||||||
|
|
||||||
|
do {
|
||||||
|
byte[] creatorPublicKey = resultSet.getBytes(1);
|
||||||
|
long created = resultSet.getLong(2);
|
||||||
|
int version = resultSet.getInt(3);
|
||||||
|
long assetId = resultSet.getLong(4);
|
||||||
|
byte[] codeBytes = resultSet.getBytes(5); // Actually BLOB
|
||||||
|
byte[] codeHash = resultSet.getBytes(6);
|
||||||
|
boolean isSleeping = resultSet.getBoolean(7);
|
||||||
|
|
||||||
|
Integer sleepUntilHeight = resultSet.getInt(8);
|
||||||
|
if (sleepUntilHeight == 0 && resultSet.wasNull())
|
||||||
|
sleepUntilHeight = null;
|
||||||
|
|
||||||
|
boolean isFinished = resultSet.getBoolean(9);
|
||||||
|
boolean hadFatalError = resultSet.getBoolean(10);
|
||||||
|
boolean isFrozen = resultSet.getBoolean(11);
|
||||||
|
|
||||||
|
Long frozenBalance = resultSet.getLong(12);
|
||||||
|
if (frozenBalance == 0 && resultSet.wasNull())
|
||||||
|
frozenBalance = null;
|
||||||
|
|
||||||
|
Long sleepUntilMessageTimestamp = resultSet.getLong(13);
|
||||||
|
if (sleepUntilMessageTimestamp == 0 && resultSet.wasNull())
|
||||||
|
sleepUntilMessageTimestamp = null;
|
||||||
|
|
||||||
|
String atAddress = resultSet.getString(14);
|
||||||
|
|
||||||
|
list.add(new ATData(atAddress, creatorPublicKey, created, version, assetId, codeBytes, codeHash,
|
||||||
|
isSleeping, sleepUntilHeight, isFinished, hadFatalError, isFrozen, frozenBalance,
|
||||||
|
sleepUntilMessageTimestamp));
|
||||||
|
} while ( resultSet.next());
|
||||||
|
|
||||||
|
return list;
|
||||||
|
} catch (SQLException e) {
|
||||||
|
throw new DataException("Unable to fetch AT from repository", e);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public boolean exists(String atAddress) throws DataException {
|
public boolean exists(String atAddress) throws DataException {
|
||||||
try {
|
try {
|
||||||
@ -403,6 +464,56 @@ public class HSQLDBATRepository implements ATRepository {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public List<ATStateData> getLatestATStates(List<String> atAddresses) throws DataException{
|
||||||
|
String sql = "SELECT height, state_data, state_hash, fees, is_initial, sleep_until_message_timestamp, AT_address "
|
||||||
|
+ "FROM ATStates "
|
||||||
|
+ "JOIN ATStatesData USING (AT_address, height) "
|
||||||
|
+ "WHERE ATStates.AT_address IN ("
|
||||||
|
+ String.join(", ", Collections.nCopies(atAddresses.size(), "?"))
|
||||||
|
+ ")";
|
||||||
|
|
||||||
|
List<ATStateData> stateDataList;
|
||||||
|
|
||||||
|
try (ResultSet resultSet = this.repository.checkedExecute(sql, atAddresses.toArray(new String[atAddresses.size()]))) {
|
||||||
|
if (resultSet == null)
|
||||||
|
return new ArrayList<>(0);
|
||||||
|
|
||||||
|
stateDataList = new ArrayList<>();
|
||||||
|
|
||||||
|
do {
|
||||||
|
int height = resultSet.getInt(1);
|
||||||
|
byte[] stateData = resultSet.getBytes(2); // Actually BLOB
|
||||||
|
byte[] stateHash = resultSet.getBytes(3);
|
||||||
|
long fees = resultSet.getLong(4);
|
||||||
|
boolean isInitial = resultSet.getBoolean(5);
|
||||||
|
|
||||||
|
Long sleepUntilMessageTimestamp = resultSet.getLong(6);
|
||||||
|
if (sleepUntilMessageTimestamp == 0 && resultSet.wasNull())
|
||||||
|
sleepUntilMessageTimestamp = null;
|
||||||
|
|
||||||
|
String atAddress = resultSet.getString(7);
|
||||||
|
stateDataList.add(new ATStateData(atAddress, height, stateData, stateHash, fees, isInitial, sleepUntilMessageTimestamp));
|
||||||
|
} while( resultSet.next());
|
||||||
|
} catch (SQLException e) {
|
||||||
|
throw new DataException("Unable to fetch latest AT state from repository", e);
|
||||||
|
}
|
||||||
|
|
||||||
|
Map<String, List<ATStateData>> stateDataByAtAddress
|
||||||
|
= stateDataList.stream()
|
||||||
|
.collect(Collectors.groupingBy(ATStateData::getATAddress));
|
||||||
|
|
||||||
|
List<ATStateData> latestForEachAtAddress
|
||||||
|
= stateDataByAtAddress.values().stream()
|
||||||
|
.map(list -> list.stream()
|
||||||
|
.max(Comparator.comparing(ATStateData::getHeight))
|
||||||
|
.orElse(null))
|
||||||
|
.filter(obj -> obj != null)
|
||||||
|
.collect(Collectors.toList());
|
||||||
|
|
||||||
|
return latestForEachAtAddress;
|
||||||
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public List<ATStateData> getMatchingFinalATStates(byte[] codeHash, byte[] buyerPublicKey, byte[] sellerPublicKey, 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,
|
||||||
|
@ -407,6 +407,39 @@ public class HSQLDBAccountRepository implements AccountRepository {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public List<AccountBalanceData> getBalances(List<String> addresses, long assetId) throws DataException {
|
||||||
|
|
||||||
|
StringBuffer sql = new StringBuffer();
|
||||||
|
sql.append("SELECT balance, account, asset_id FROM AccountBalances ");
|
||||||
|
sql.append("WHERE account IN (");
|
||||||
|
sql.append(String.join(", ", Collections.nCopies(addresses.size(), "?")));
|
||||||
|
sql.append(")");
|
||||||
|
|
||||||
|
try (ResultSet resultSet = this.repository.checkedExecute(sql.toString(), addresses.toArray(new String[addresses.size()]))) {
|
||||||
|
if (resultSet == null)
|
||||||
|
return new ArrayList<>(0);
|
||||||
|
|
||||||
|
List<AccountBalanceData> balances = new ArrayList<>(addresses.size());
|
||||||
|
do {
|
||||||
|
long balance = resultSet.getLong(1);
|
||||||
|
String address = resultSet.getString(2);
|
||||||
|
Long assetIdResult = resultSet.getLong(3);
|
||||||
|
|
||||||
|
if( assetIdResult != assetId ) {
|
||||||
|
LOGGER.warn("assetIdResult = " + assetIdResult);
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
balances.add(new AccountBalanceData(address, assetId, balance) );
|
||||||
|
} while( resultSet.next());
|
||||||
|
|
||||||
|
return balances;
|
||||||
|
} catch (SQLException e) {
|
||||||
|
throw new DataException("Unable to fetch account balance from repository", e);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public List<AccountBalanceData> getAssetBalances(long assetId, Boolean excludeZero) throws DataException {
|
public List<AccountBalanceData> getAssetBalances(long assetId, Boolean excludeZero) throws DataException {
|
||||||
StringBuilder sql = new StringBuilder(1024);
|
StringBuilder sql = new StringBuilder(1024);
|
||||||
|
@ -468,7 +468,7 @@ public class HSQLDBCacheUtils {
|
|||||||
|
|
||||||
Thread.currentThread().setName(DB_CACHE_TIMER_TASK);
|
Thread.currentThread().setName(DB_CACHE_TIMER_TASK);
|
||||||
|
|
||||||
try (final HSQLDBRepository respository = (HSQLDBRepository) Controller.REPOSITORY_FACTORY.getRepository()) {
|
try (final Repository respository = RepositoryManager.getRepository()) {
|
||||||
fillCache(ArbitraryResourceCache.getInstance(), respository);
|
fillCache(ArbitraryResourceCache.getInstance(), respository);
|
||||||
}
|
}
|
||||||
catch( DataException e ) {
|
catch( DataException e ) {
|
||||||
@ -611,7 +611,7 @@ public class HSQLDBCacheUtils {
|
|||||||
private static int recordCurrentBalances(ConcurrentHashMap<Integer, List<AccountBalanceData>> balancesByHeight) {
|
private static int recordCurrentBalances(ConcurrentHashMap<Integer, List<AccountBalanceData>> balancesByHeight) {
|
||||||
int currentHeight;
|
int currentHeight;
|
||||||
|
|
||||||
try (final HSQLDBRepository repository = (HSQLDBRepository) Controller.REPOSITORY_FACTORY.getRepository()) {
|
try (final Repository repository = RepositoryManager.getRepository()) {
|
||||||
|
|
||||||
// get current balances
|
// get current balances
|
||||||
List<AccountBalanceData> accountBalances = getAccountBalances(repository);
|
List<AccountBalanceData> accountBalances = getAccountBalances(repository);
|
||||||
@ -675,7 +675,7 @@ public class HSQLDBCacheUtils {
|
|||||||
* @param cache the cache to fill
|
* @param cache the cache to fill
|
||||||
* @param repository the data source to fill the cache with
|
* @param repository the data source to fill the cache with
|
||||||
*/
|
*/
|
||||||
public static void fillCache(ArbitraryResourceCache cache, HSQLDBRepository repository) {
|
public static void fillCache(ArbitraryResourceCache cache, Repository repository) {
|
||||||
|
|
||||||
try {
|
try {
|
||||||
// ensure all data is committed in, before we query it
|
// ensure all data is committed in, before we query it
|
||||||
@ -713,7 +713,7 @@ public class HSQLDBCacheUtils {
|
|||||||
*
|
*
|
||||||
* @throws SQLException
|
* @throws SQLException
|
||||||
*/
|
*/
|
||||||
private static void fillNamepMap(ConcurrentHashMap<String, Integer> levelByName, HSQLDBRepository repository ) throws SQLException {
|
private static void fillNamepMap(ConcurrentHashMap<String, Integer> levelByName, Repository repository ) throws SQLException {
|
||||||
|
|
||||||
StringBuilder sql = new StringBuilder(512);
|
StringBuilder sql = new StringBuilder(512);
|
||||||
|
|
||||||
@ -721,7 +721,7 @@ public class HSQLDBCacheUtils {
|
|||||||
sql.append("FROM NAMES ");
|
sql.append("FROM NAMES ");
|
||||||
sql.append("INNER JOIN ACCOUNTS on owner = account ");
|
sql.append("INNER JOIN ACCOUNTS on owner = account ");
|
||||||
|
|
||||||
Statement statement = repository.connection.createStatement();
|
Statement statement = repository.getConnection().createStatement();
|
||||||
|
|
||||||
ResultSet resultSet = statement.executeQuery(sql.toString());
|
ResultSet resultSet = statement.executeQuery(sql.toString());
|
||||||
|
|
||||||
@ -744,7 +744,7 @@ public class HSQLDBCacheUtils {
|
|||||||
* @return the resources
|
* @return the resources
|
||||||
* @throws SQLException
|
* @throws SQLException
|
||||||
*/
|
*/
|
||||||
private static List<ArbitraryResourceData> getResources( HSQLDBRepository repository) throws SQLException {
|
private static List<ArbitraryResourceData> getResources( Repository repository) throws SQLException {
|
||||||
|
|
||||||
List<ArbitraryResourceData> resources = new ArrayList<>();
|
List<ArbitraryResourceData> resources = new ArrayList<>();
|
||||||
|
|
||||||
@ -756,7 +756,7 @@ public class HSQLDBCacheUtils {
|
|||||||
sql.append("LEFT JOIN ArbitraryMetadataCache USING (service, name, identifier) WHERE name IS NOT NULL");
|
sql.append("LEFT JOIN ArbitraryMetadataCache USING (service, name, identifier) WHERE name IS NOT NULL");
|
||||||
|
|
||||||
List<ArbitraryResourceData> arbitraryResources = new ArrayList<>();
|
List<ArbitraryResourceData> arbitraryResources = new ArrayList<>();
|
||||||
Statement statement = repository.connection.createStatement();
|
Statement statement = repository.getConnection().createStatement();
|
||||||
|
|
||||||
ResultSet resultSet = statement.executeQuery(sql.toString());
|
ResultSet resultSet = statement.executeQuery(sql.toString());
|
||||||
|
|
||||||
@ -822,7 +822,7 @@ public class HSQLDBCacheUtils {
|
|||||||
return resources;
|
return resources;
|
||||||
}
|
}
|
||||||
|
|
||||||
public static List<AccountBalanceData> getAccountBalances(HSQLDBRepository repository) {
|
public static List<AccountBalanceData> getAccountBalances(Repository repository) {
|
||||||
|
|
||||||
StringBuilder sql = new StringBuilder();
|
StringBuilder sql = new StringBuilder();
|
||||||
|
|
||||||
@ -836,7 +836,7 @@ public class HSQLDBCacheUtils {
|
|||||||
LOGGER.info( "Getting account balances ...");
|
LOGGER.info( "Getting account balances ...");
|
||||||
|
|
||||||
try {
|
try {
|
||||||
Statement statement = repository.connection.createStatement();
|
Statement statement = repository.getConnection().createStatement();
|
||||||
|
|
||||||
ResultSet resultSet = statement.executeQuery(sql.toString());
|
ResultSet resultSet = statement.executeQuery(sql.toString());
|
||||||
|
|
||||||
|
@ -1,5 +1,8 @@
|
|||||||
package org.qortal.repository.hsqldb;
|
package org.qortal.repository.hsqldb;
|
||||||
|
|
||||||
|
import org.apache.logging.log4j.LogManager;
|
||||||
|
import org.apache.logging.log4j.Logger;
|
||||||
|
import org.qortal.block.BlockChain;
|
||||||
import org.qortal.data.chat.ActiveChats;
|
import org.qortal.data.chat.ActiveChats;
|
||||||
import org.qortal.data.chat.ActiveChats.DirectChat;
|
import org.qortal.data.chat.ActiveChats.DirectChat;
|
||||||
import org.qortal.data.chat.ActiveChats.GroupChat;
|
import org.qortal.data.chat.ActiveChats.GroupChat;
|
||||||
@ -18,6 +21,8 @@ import static org.qortal.data.chat.ChatMessage.Encoding;
|
|||||||
|
|
||||||
public class HSQLDBChatRepository implements ChatRepository {
|
public class HSQLDBChatRepository implements ChatRepository {
|
||||||
|
|
||||||
|
private static final Logger LOGGER = LogManager.getLogger(HSQLDBChatRepository.class);
|
||||||
|
|
||||||
protected HSQLDBRepository repository;
|
protected HSQLDBRepository repository;
|
||||||
|
|
||||||
public HSQLDBChatRepository(HSQLDBRepository repository) {
|
public HSQLDBChatRepository(HSQLDBRepository repository) {
|
||||||
@ -35,13 +40,25 @@ public class HSQLDBChatRepository implements ChatRepository {
|
|||||||
|
|
||||||
StringBuilder sql = new StringBuilder(1024);
|
StringBuilder sql = new StringBuilder(1024);
|
||||||
|
|
||||||
|
String tableName;
|
||||||
|
|
||||||
|
// if the PrimaryTable is available, then use it
|
||||||
|
if( this.repository.getBlockRepository().getBlockchainHeight() > BlockChain.getInstance().getMultipleNamesPerAccountHeight()) {
|
||||||
|
LOGGER.debug("using PrimaryNames for chat transactions");
|
||||||
|
tableName = "PrimaryNames";
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
LOGGER.debug("using Names for chat transactions");
|
||||||
|
tableName = "Names";
|
||||||
|
}
|
||||||
|
|
||||||
sql.append("SELECT created_when, tx_group_id, Transactions.reference, creator, "
|
sql.append("SELECT created_when, tx_group_id, Transactions.reference, creator, "
|
||||||
+ "sender, SenderNames.name, recipient, RecipientNames.name, "
|
+ "sender, SenderNames.name, recipient, RecipientNames.name, "
|
||||||
+ "chat_reference, data, is_text, is_encrypted, signature "
|
+ "chat_reference, data, is_text, is_encrypted, signature "
|
||||||
+ "FROM ChatTransactions "
|
+ "FROM ChatTransactions "
|
||||||
+ "JOIN Transactions USING (signature) "
|
+ "JOIN Transactions USING (signature) "
|
||||||
+ "LEFT OUTER JOIN Names AS SenderNames ON SenderNames.owner = sender "
|
+ "LEFT OUTER JOIN " + tableName + " AS SenderNames ON SenderNames.owner = sender "
|
||||||
+ "LEFT OUTER JOIN Names AS RecipientNames ON RecipientNames.owner = recipient ");
|
+ "LEFT OUTER JOIN " + tableName + " AS RecipientNames ON RecipientNames.owner = recipient ");
|
||||||
|
|
||||||
// WHERE clauses
|
// WHERE clauses
|
||||||
|
|
||||||
@ -142,10 +159,23 @@ public class HSQLDBChatRepository implements ChatRepository {
|
|||||||
|
|
||||||
@Override
|
@Override
|
||||||
public ChatMessage toChatMessage(ChatTransactionData chatTransactionData, Encoding encoding) throws DataException {
|
public ChatMessage toChatMessage(ChatTransactionData chatTransactionData, Encoding encoding) throws DataException {
|
||||||
|
|
||||||
|
String tableName;
|
||||||
|
|
||||||
|
// if the PrimaryTable is available, then use it
|
||||||
|
if( this.repository.getBlockRepository().getBlockchainHeight() > BlockChain.getInstance().getMultipleNamesPerAccountHeight()) {
|
||||||
|
LOGGER.debug("using PrimaryNames for chat transactions");
|
||||||
|
tableName = "PrimaryNames";
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
LOGGER.debug("using Names for chat transactions");
|
||||||
|
tableName = "Names";
|
||||||
|
}
|
||||||
|
|
||||||
String sql = "SELECT SenderNames.name, RecipientNames.name "
|
String sql = "SELECT SenderNames.name, RecipientNames.name "
|
||||||
+ "FROM ChatTransactions "
|
+ "FROM ChatTransactions "
|
||||||
+ "LEFT OUTER JOIN Names AS SenderNames ON SenderNames.owner = sender "
|
+ "LEFT OUTER JOIN " + tableName + " AS SenderNames ON SenderNames.owner = sender "
|
||||||
+ "LEFT OUTER JOIN Names AS RecipientNames ON RecipientNames.owner = recipient "
|
+ "LEFT OUTER JOIN " + tableName + " AS RecipientNames ON RecipientNames.owner = recipient "
|
||||||
+ "WHERE signature = ?";
|
+ "WHERE signature = ?";
|
||||||
|
|
||||||
try (ResultSet resultSet = this.repository.checkedExecute(sql, chatTransactionData.getSignature())) {
|
try (ResultSet resultSet = this.repository.checkedExecute(sql, chatTransactionData.getSignature())) {
|
||||||
@ -184,6 +214,18 @@ public class HSQLDBChatRepository implements ChatRepository {
|
|||||||
}
|
}
|
||||||
|
|
||||||
private List<GroupChat> getActiveGroupChats(String address, Encoding encoding, Boolean hasChatReference) throws DataException {
|
private List<GroupChat> getActiveGroupChats(String address, Encoding encoding, Boolean hasChatReference) throws DataException {
|
||||||
|
String tableName;
|
||||||
|
|
||||||
|
// if the PrimaryTable is available, then use it
|
||||||
|
if( this.repository.getBlockRepository().getBlockchainHeight() > BlockChain.getInstance().getMultipleNamesPerAccountHeight()) {
|
||||||
|
LOGGER.debug("using PrimaryNames for chat transactions");
|
||||||
|
tableName = "PrimaryNames";
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
LOGGER.debug("using Names for chat transactions");
|
||||||
|
tableName = "Names";
|
||||||
|
}
|
||||||
|
|
||||||
// Find groups where address is a member and potential latest message details
|
// Find groups where address is a member and potential latest message details
|
||||||
String groupsSql = "SELECT group_id, group_name, latest_timestamp, sender, sender_name, signature, data "
|
String groupsSql = "SELECT group_id, group_name, latest_timestamp, sender, sender_name, signature, data "
|
||||||
+ "FROM GroupMembers "
|
+ "FROM GroupMembers "
|
||||||
@ -192,7 +234,7 @@ public class HSQLDBChatRepository implements ChatRepository {
|
|||||||
+ "SELECT created_when AS latest_timestamp, sender, name AS sender_name, signature, data "
|
+ "SELECT created_when AS latest_timestamp, sender, name AS sender_name, signature, data "
|
||||||
+ "FROM ChatTransactions "
|
+ "FROM ChatTransactions "
|
||||||
+ "JOIN Transactions USING (signature) "
|
+ "JOIN Transactions USING (signature) "
|
||||||
+ "LEFT OUTER JOIN Names AS SenderNames ON SenderNames.owner = sender "
|
+ "LEFT OUTER JOIN " + tableName + " AS SenderNames ON SenderNames.owner = sender "
|
||||||
// NOTE: We need to qualify "Groups.group_id" here to avoid "General error" bug in HSQLDB v2.5.0
|
// NOTE: We need to qualify "Groups.group_id" here to avoid "General error" bug in HSQLDB v2.5.0
|
||||||
+ "WHERE tx_group_id = Groups.group_id AND type = " + TransactionType.CHAT.value + " ";
|
+ "WHERE tx_group_id = Groups.group_id AND type = " + TransactionType.CHAT.value + " ";
|
||||||
|
|
||||||
@ -236,7 +278,7 @@ public class HSQLDBChatRepository implements ChatRepository {
|
|||||||
String grouplessSql = "SELECT created_when, sender, SenderNames.name, signature, data "
|
String grouplessSql = "SELECT created_when, sender, SenderNames.name, signature, data "
|
||||||
+ "FROM ChatTransactions "
|
+ "FROM ChatTransactions "
|
||||||
+ "JOIN Transactions USING (signature) "
|
+ "JOIN Transactions USING (signature) "
|
||||||
+ "LEFT OUTER JOIN Names AS SenderNames ON SenderNames.owner = sender "
|
+ "LEFT OUTER JOIN " + tableName + " AS SenderNames ON SenderNames.owner = sender "
|
||||||
+ "WHERE tx_group_id = 0 "
|
+ "WHERE tx_group_id = 0 "
|
||||||
+ "AND recipient IS NULL ";
|
+ "AND recipient IS NULL ";
|
||||||
|
|
||||||
@ -276,6 +318,18 @@ public class HSQLDBChatRepository implements ChatRepository {
|
|||||||
}
|
}
|
||||||
|
|
||||||
private List<DirectChat> getActiveDirectChats(String address, Boolean hasChatReference) throws DataException {
|
private List<DirectChat> getActiveDirectChats(String address, Boolean hasChatReference) throws DataException {
|
||||||
|
String tableName;
|
||||||
|
|
||||||
|
// if the PrimaryTable is available, then use it
|
||||||
|
if( this.repository.getBlockRepository().getBlockchainHeight() > BlockChain.getInstance().getMultipleNamesPerAccountHeight()) {
|
||||||
|
LOGGER.debug("using PrimaryNames for chat transactions");
|
||||||
|
tableName = "PrimaryNames";
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
LOGGER.debug("using Names for chat transactions");
|
||||||
|
tableName = "Names";
|
||||||
|
}
|
||||||
|
|
||||||
// Find chat messages involving address
|
// Find chat messages involving address
|
||||||
String directSql = "SELECT other_address, name, latest_timestamp, sender, sender_name "
|
String directSql = "SELECT other_address, name, latest_timestamp, sender, sender_name "
|
||||||
+ "FROM ("
|
+ "FROM ("
|
||||||
@ -289,7 +343,7 @@ public class HSQLDBChatRepository implements ChatRepository {
|
|||||||
+ "SELECT created_when AS latest_timestamp, sender, name AS sender_name "
|
+ "SELECT created_when AS latest_timestamp, sender, name AS sender_name "
|
||||||
+ "FROM ChatTransactions "
|
+ "FROM ChatTransactions "
|
||||||
+ "NATURAL JOIN Transactions "
|
+ "NATURAL JOIN Transactions "
|
||||||
+ "LEFT OUTER JOIN Names AS SenderNames ON SenderNames.owner = sender "
|
+ "LEFT OUTER JOIN " + tableName + " AS SenderNames ON SenderNames.owner = sender "
|
||||||
+ "WHERE (sender = other_address AND recipient = ?) "
|
+ "WHERE (sender = other_address AND recipient = ?) "
|
||||||
+ "OR (sender = ? AND recipient = other_address) ";
|
+ "OR (sender = ? AND recipient = other_address) ";
|
||||||
|
|
||||||
@ -305,7 +359,7 @@ public class HSQLDBChatRepository implements ChatRepository {
|
|||||||
directSql += "ORDER BY created_when DESC "
|
directSql += "ORDER BY created_when DESC "
|
||||||
+ "LIMIT 1"
|
+ "LIMIT 1"
|
||||||
+ ") AS LatestMessages "
|
+ ") AS LatestMessages "
|
||||||
+ "LEFT OUTER JOIN Names ON owner = other_address";
|
+ "LEFT OUTER JOIN " + tableName + " ON owner = other_address";
|
||||||
|
|
||||||
Object[] bindParams = new Object[] { address, address, address, address };
|
Object[] bindParams = new Object[] { address, address, address, address };
|
||||||
|
|
||||||
|
@ -1053,6 +1053,12 @@ public class HSQLDBDatabaseUpdates {
|
|||||||
stmt.execute("UPDATE Accounts SET blocks_minted_penalty = -5000000 WHERE blocks_minted_penalty < 0");
|
stmt.execute("UPDATE Accounts SET blocks_minted_penalty = -5000000 WHERE blocks_minted_penalty < 0");
|
||||||
break;
|
break;
|
||||||
|
|
||||||
|
case 50:
|
||||||
|
// Primary name for a Qortal Address, 0-1 for any address
|
||||||
|
stmt.execute("CREATE TABLE PrimaryNames (owner QortalAddress, name RegisteredName, "
|
||||||
|
+ "PRIMARY KEY (owner), FOREIGN KEY (name) REFERENCES Names (name) ON DELETE CASCADE)");
|
||||||
|
break;
|
||||||
|
|
||||||
default:
|
default:
|
||||||
// nothing to do
|
// nothing to do
|
||||||
return false;
|
return false;
|
||||||
|
@ -8,6 +8,7 @@ import java.sql.ResultSet;
|
|||||||
import java.sql.SQLException;
|
import java.sql.SQLException;
|
||||||
import java.util.ArrayList;
|
import java.util.ArrayList;
|
||||||
import java.util.List;
|
import java.util.List;
|
||||||
|
import java.util.Optional;
|
||||||
|
|
||||||
public class HSQLDBNameRepository implements NameRepository {
|
public class HSQLDBNameRepository implements NameRepository {
|
||||||
|
|
||||||
@ -267,7 +268,7 @@ public class HSQLDBNameRepository implements NameRepository {
|
|||||||
StringBuilder sql = new StringBuilder(512);
|
StringBuilder sql = new StringBuilder(512);
|
||||||
|
|
||||||
sql.append("SELECT name, reduced_name, data, registered_when, updated_when, "
|
sql.append("SELECT name, reduced_name, data, registered_when, updated_when, "
|
||||||
+ "is_for_sale, sale_price, reference, creation_group_id FROM Names WHERE owner = ? ORDER BY name");
|
+ "is_for_sale, sale_price, reference, creation_group_id FROM Names WHERE owner = ? ORDER BY registered_when");
|
||||||
|
|
||||||
if (reverse != null && reverse)
|
if (reverse != null && reverse)
|
||||||
sql.append(" DESC");
|
sql.append(" DESC");
|
||||||
@ -333,6 +334,55 @@ public class HSQLDBNameRepository implements NameRepository {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void removePrimaryName(String address) throws DataException {
|
||||||
|
try {
|
||||||
|
this.repository.delete("PrimaryNames", "owner = ?", address);
|
||||||
|
} catch (SQLException e) {
|
||||||
|
throw new DataException("Unable to delete primary name from repository", e);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public Optional<String> getPrimaryName(String address) throws DataException {
|
||||||
|
String sql = "SELECT name FROM PrimaryNames WHERE owner = ?";
|
||||||
|
|
||||||
|
List<String> names = new ArrayList<>();
|
||||||
|
|
||||||
|
try (ResultSet resultSet = this.repository.checkedExecute(sql, address)) {
|
||||||
|
if (resultSet == null)
|
||||||
|
return Optional.empty();
|
||||||
|
|
||||||
|
String name = resultSet.getString(1);
|
||||||
|
|
||||||
|
return Optional.of(name);
|
||||||
|
} catch (SQLException e) {
|
||||||
|
throw new DataException("Unable to fetch recent names from repository", e);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public int setPrimaryName(String address, String primaryName) throws DataException {
|
||||||
|
|
||||||
|
String sql = "INSERT INTO PrimaryNames (owner, name) VALUES (?, ?) ON DUPLICATE KEY UPDATE name = ?";
|
||||||
|
|
||||||
|
try{
|
||||||
|
return this.repository.executeCheckedUpdate(sql, address, primaryName, primaryName);
|
||||||
|
} catch (SQLException e) {
|
||||||
|
throw new DataException("Unable to set primary name", e);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public int clearPrimaryNames() throws DataException {
|
||||||
|
|
||||||
|
try {
|
||||||
|
return this.repository.delete("PrimaryNames");
|
||||||
|
} catch (SQLException e) {
|
||||||
|
throw new DataException("Unable to clear primary names from repository", e);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public void save(NameData nameData) throws DataException {
|
public void save(NameData nameData) throws DataException {
|
||||||
HSQLDBSaver saveHelper = new HSQLDBSaver("Names");
|
HSQLDBSaver saveHelper = new HSQLDBSaver("Names");
|
||||||
|
@ -174,6 +174,11 @@ public class HSQLDBRepository implements Repository {
|
|||||||
|
|
||||||
// Transaction COMMIT / ROLLBACK / savepoints
|
// Transaction COMMIT / ROLLBACK / savepoints
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public Connection getConnection() {
|
||||||
|
return this.connection;
|
||||||
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public void saveChanges() throws DataException {
|
public void saveChanges() throws DataException {
|
||||||
long beforeQuery = this.slowQueryThreshold == null ? 0 : System.currentTimeMillis();
|
long beforeQuery = this.slowQueryThreshold == null ? 0 : System.currentTimeMillis();
|
||||||
|
@ -155,6 +155,58 @@ public class HSQLDBTransactionRepository implements TransactionRepository {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public List<TransactionData> fromSignatures(List<byte[]> signatures) throws DataException {
|
||||||
|
StringBuffer sql = new StringBuffer();
|
||||||
|
|
||||||
|
sql.append("SELECT type, reference, creator, created_when, fee, tx_group_id, block_height, approval_status, approval_height, signature ");
|
||||||
|
sql.append("FROM Transactions WHERE signature IN (");
|
||||||
|
sql.append(String.join(", ", Collections.nCopies(signatures.size(), "?")));
|
||||||
|
sql.append(")");
|
||||||
|
|
||||||
|
List<TransactionData> list;
|
||||||
|
try (ResultSet resultSet = this.repository.checkedExecute(sql.toString(), signatures.toArray(new byte[0][]))) {
|
||||||
|
if (resultSet == null) {
|
||||||
|
return new ArrayList<>(0);
|
||||||
|
}
|
||||||
|
|
||||||
|
list = new ArrayList<>(signatures.size());
|
||||||
|
|
||||||
|
do {
|
||||||
|
TransactionType type = TransactionType.valueOf(resultSet.getInt(1));
|
||||||
|
|
||||||
|
byte[] reference = resultSet.getBytes(2);
|
||||||
|
byte[] creatorPublicKey = resultSet.getBytes(3);
|
||||||
|
long timestamp = resultSet.getLong(4);
|
||||||
|
|
||||||
|
Long fee = resultSet.getLong(5);
|
||||||
|
if (fee == 0 && resultSet.wasNull())
|
||||||
|
fee = null;
|
||||||
|
|
||||||
|
int txGroupId = resultSet.getInt(6);
|
||||||
|
|
||||||
|
Integer blockHeight = resultSet.getInt(7);
|
||||||
|
if (blockHeight == 0 && resultSet.wasNull())
|
||||||
|
blockHeight = null;
|
||||||
|
|
||||||
|
ApprovalStatus approvalStatus = ApprovalStatus.valueOf(resultSet.getInt(8));
|
||||||
|
Integer approvalHeight = resultSet.getInt(9);
|
||||||
|
if (approvalHeight == 0 && resultSet.wasNull())
|
||||||
|
approvalHeight = null;
|
||||||
|
|
||||||
|
byte[] signature = resultSet.getBytes(10);
|
||||||
|
|
||||||
|
BaseTransactionData baseTransactionData = new BaseTransactionData(timestamp, txGroupId, reference, creatorPublicKey, fee, approvalStatus, blockHeight, approvalHeight, signature);
|
||||||
|
|
||||||
|
list.add( fromBase(type, baseTransactionData) );
|
||||||
|
} while( resultSet.next());
|
||||||
|
|
||||||
|
return list;
|
||||||
|
} catch (SQLException e) {
|
||||||
|
throw new DataException("Unable to fetch transactions from repository", e);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public TransactionData fromReference(byte[] reference) throws DataException {
|
public TransactionData fromReference(byte[] reference) throws DataException {
|
||||||
String sql = "SELECT type, signature, creator, created_when, fee, tx_group_id, block_height, approval_status, approval_height "
|
String sql = "SELECT type, signature, creator, created_when, fee, tx_group_id, block_height, approval_status, approval_height "
|
||||||
|
@ -759,14 +759,14 @@ public class Settings {
|
|||||||
maxThreadsPerMessageType.add(new ThreadLimit("GET_ARBITRARY_DATA_FILE", 5));
|
maxThreadsPerMessageType.add(new ThreadLimit("GET_ARBITRARY_DATA_FILE", 5));
|
||||||
maxThreadsPerMessageType.add(new ThreadLimit("ARBITRARY_DATA", 5));
|
maxThreadsPerMessageType.add(new ThreadLimit("ARBITRARY_DATA", 5));
|
||||||
maxThreadsPerMessageType.add(new ThreadLimit("GET_ARBITRARY_DATA", 5));
|
maxThreadsPerMessageType.add(new ThreadLimit("GET_ARBITRARY_DATA", 5));
|
||||||
maxThreadsPerMessageType.add(new ThreadLimit("ARBITRARY_DATA_FILE_LIST", 5));
|
maxThreadsPerMessageType.add(new ThreadLimit("ARBITRARY_DATA_FILE_LIST", 50));
|
||||||
maxThreadsPerMessageType.add(new ThreadLimit("GET_ARBITRARY_DATA_FILE_LIST", 5));
|
maxThreadsPerMessageType.add(new ThreadLimit("GET_ARBITRARY_DATA_FILE_LIST", 50));
|
||||||
maxThreadsPerMessageType.add(new ThreadLimit("ARBITRARY_SIGNATURES", 5));
|
maxThreadsPerMessageType.add(new ThreadLimit("ARBITRARY_SIGNATURES", 5));
|
||||||
maxThreadsPerMessageType.add(new ThreadLimit("ARBITRARY_METADATA", 5));
|
maxThreadsPerMessageType.add(new ThreadLimit("ARBITRARY_METADATA", 5));
|
||||||
maxThreadsPerMessageType.add(new ThreadLimit("GET_ARBITRARY_METADATA", 5));
|
maxThreadsPerMessageType.add(new ThreadLimit("GET_ARBITRARY_METADATA", 100));
|
||||||
maxThreadsPerMessageType.add(new ThreadLimit("GET_TRANSACTION", 10));
|
maxThreadsPerMessageType.add(new ThreadLimit("GET_TRANSACTION", 50));
|
||||||
maxThreadsPerMessageType.add(new ThreadLimit("TRANSACTION_SIGNATURES", 5));
|
maxThreadsPerMessageType.add(new ThreadLimit("TRANSACTION_SIGNATURES", 50));
|
||||||
maxThreadsPerMessageType.add(new ThreadLimit("TRADE_PRESENCES", 5));
|
maxThreadsPerMessageType.add(new ThreadLimit("TRADE_PRESENCES", 50));
|
||||||
}
|
}
|
||||||
|
|
||||||
// Getters / setters
|
// Getters / setters
|
||||||
|
@ -16,6 +16,7 @@ import org.qortal.utils.Unicode;
|
|||||||
|
|
||||||
import java.util.Collections;
|
import java.util.Collections;
|
||||||
import java.util.List;
|
import java.util.List;
|
||||||
|
import java.util.Optional;
|
||||||
|
|
||||||
public class BuyNameTransaction extends Transaction {
|
public class BuyNameTransaction extends Transaction {
|
||||||
|
|
||||||
@ -48,6 +49,15 @@ public class BuyNameTransaction extends Transaction {
|
|||||||
|
|
||||||
@Override
|
@Override
|
||||||
public ValidationResult isValid() throws DataException {
|
public ValidationResult isValid() throws DataException {
|
||||||
|
Optional<String> buyerPrimaryName = this.getBuyer().getPrimaryName();
|
||||||
|
if( buyerPrimaryName.isPresent() ) {
|
||||||
|
|
||||||
|
NameData nameData = repository.getNameRepository().fromName(buyerPrimaryName.get());
|
||||||
|
if (nameData.isForSale()) {
|
||||||
|
return ValidationResult.NOT_SUPPORTED;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
String name = this.buyNameTransactionData.getName();
|
String name = this.buyNameTransactionData.getName();
|
||||||
|
|
||||||
// Check seller address is valid
|
// Check seller address is valid
|
||||||
@ -79,7 +89,7 @@ public class BuyNameTransaction extends Transaction {
|
|||||||
return ValidationResult.BUYER_ALREADY_OWNER;
|
return ValidationResult.BUYER_ALREADY_OWNER;
|
||||||
|
|
||||||
// If accounts are only allowed one registered name then check for this
|
// If accounts are only allowed one registered name then check for this
|
||||||
if (BlockChain.getInstance().oneNamePerAccount()
|
if (BlockChain.getInstance().oneNamePerAccount(this.repository.getBlockRepository().getBlockchainHeight())
|
||||||
&& !this.repository.getNameRepository().getNamesByOwner(buyer.getAddress()).isEmpty())
|
&& !this.repository.getNameRepository().getNamesByOwner(buyer.getAddress()).isEmpty())
|
||||||
return ValidationResult.MULTIPLE_NAMES_FORBIDDEN;
|
return ValidationResult.MULTIPLE_NAMES_FORBIDDEN;
|
||||||
|
|
||||||
@ -92,7 +102,7 @@ public class BuyNameTransaction extends Transaction {
|
|||||||
return ValidationResult.INVALID_AMOUNT;
|
return ValidationResult.INVALID_AMOUNT;
|
||||||
|
|
||||||
// Check buyer has enough funds
|
// Check buyer has enough funds
|
||||||
if (buyer.getConfirmedBalance(Asset.QORT) < this.buyNameTransactionData.getFee())
|
if (buyer.getConfirmedBalance(Asset.QORT) < this.buyNameTransactionData.getFee() + this.buyNameTransactionData.getAmount())
|
||||||
return ValidationResult.NO_BALANCE;
|
return ValidationResult.NO_BALANCE;
|
||||||
|
|
||||||
return ValidationResult.OK;
|
return ValidationResult.OK;
|
||||||
@ -117,6 +127,25 @@ public class BuyNameTransaction extends Transaction {
|
|||||||
|
|
||||||
// Save transaction with updated "name reference" pointing to previous transaction that changed name
|
// Save transaction with updated "name reference" pointing to previous transaction that changed name
|
||||||
this.repository.getTransactionRepository().save(this.buyNameTransactionData);
|
this.repository.getTransactionRepository().save(this.buyNameTransactionData);
|
||||||
|
|
||||||
|
// if multiple names feature is activated, then check the buyer and seller's primary name status
|
||||||
|
if( this.repository.getBlockRepository().getBlockchainHeight() > BlockChain.getInstance().getMultipleNamesPerAccountHeight()) {
|
||||||
|
|
||||||
|
Account seller = new Account(this.repository, this.buyNameTransactionData.getSeller());
|
||||||
|
Optional<String> sellerPrimaryName = seller.getPrimaryName();
|
||||||
|
|
||||||
|
// if the seller sold their primary name, then remove their primary name
|
||||||
|
if (sellerPrimaryName.isPresent() && sellerPrimaryName.get().equals(buyNameTransactionData.getName())) {
|
||||||
|
seller.removePrimaryName();
|
||||||
|
}
|
||||||
|
|
||||||
|
Account buyer = new Account(this.repository, this.getBuyer().getAddress());
|
||||||
|
|
||||||
|
// if the buyer had no primary name, then set the primary name to the name bought
|
||||||
|
if( buyer.getPrimaryName().isEmpty() ) {
|
||||||
|
buyer.setPrimaryName(this.buyNameTransactionData.getName());
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
@ -127,6 +156,24 @@ public class BuyNameTransaction extends Transaction {
|
|||||||
|
|
||||||
// Save this transaction, with previous "name reference"
|
// Save this transaction, with previous "name reference"
|
||||||
this.repository.getTransactionRepository().save(this.buyNameTransactionData);
|
this.repository.getTransactionRepository().save(this.buyNameTransactionData);
|
||||||
|
|
||||||
|
// if multiple names feature is activated, then check the buyer and seller's primary name status
|
||||||
|
if( this.repository.getBlockRepository().getBlockchainHeight() > BlockChain.getInstance().getMultipleNamesPerAccountHeight()) {
|
||||||
|
|
||||||
|
Account seller = new Account(this.repository, this.buyNameTransactionData.getSeller());
|
||||||
|
|
||||||
|
// if the seller lost their primary name, then set their primary name back
|
||||||
|
if (seller.getPrimaryName().isEmpty()) {
|
||||||
|
seller.setPrimaryName(this.buyNameTransactionData.getName());
|
||||||
}
|
}
|
||||||
|
|
||||||
|
Account buyer = new Account(this.repository, this.getBuyer().getAddress());
|
||||||
|
Optional<String> buyerPrimaryName = buyer.getPrimaryName();
|
||||||
|
|
||||||
|
// if the buyer bought their primary, then remove it
|
||||||
|
if( buyerPrimaryName.isPresent() && this.buyNameTransactionData.getName().equals(buyerPrimaryName.get()) ) {
|
||||||
|
buyer.removePrimaryName();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
@ -2,10 +2,12 @@ package org.qortal.transaction;
|
|||||||
|
|
||||||
import com.google.common.base.Utf8;
|
import com.google.common.base.Utf8;
|
||||||
import org.qortal.account.Account;
|
import org.qortal.account.Account;
|
||||||
|
import org.qortal.api.resource.TransactionsResource;
|
||||||
import org.qortal.asset.Asset;
|
import org.qortal.asset.Asset;
|
||||||
import org.qortal.block.BlockChain;
|
import org.qortal.block.BlockChain;
|
||||||
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.data.naming.NameData;
|
||||||
import org.qortal.data.transaction.RegisterNameTransactionData;
|
import org.qortal.data.transaction.RegisterNameTransactionData;
|
||||||
import org.qortal.data.transaction.TransactionData;
|
import org.qortal.data.transaction.TransactionData;
|
||||||
import org.qortal.naming.Name;
|
import org.qortal.naming.Name;
|
||||||
@ -15,6 +17,7 @@ import org.qortal.utils.Unicode;
|
|||||||
|
|
||||||
import java.util.Collections;
|
import java.util.Collections;
|
||||||
import java.util.List;
|
import java.util.List;
|
||||||
|
import java.util.Optional;
|
||||||
|
|
||||||
public class RegisterNameTransaction extends Transaction {
|
public class RegisterNameTransaction extends Transaction {
|
||||||
|
|
||||||
@ -54,6 +57,15 @@ public class RegisterNameTransaction extends Transaction {
|
|||||||
Account registrant = getRegistrant();
|
Account registrant = getRegistrant();
|
||||||
String name = this.registerNameTransactionData.getName();
|
String name = this.registerNameTransactionData.getName();
|
||||||
|
|
||||||
|
Optional<String> registrantPrimaryName = registrant.getPrimaryName();
|
||||||
|
if( registrantPrimaryName.isPresent() ) {
|
||||||
|
|
||||||
|
NameData nameData = repository.getNameRepository().fromName(registrantPrimaryName.get());
|
||||||
|
if (nameData.isForSale()) {
|
||||||
|
return ValidationResult.NOT_SUPPORTED;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
int blockchainHeight = this.repository.getBlockRepository().getBlockchainHeight();
|
int blockchainHeight = this.repository.getBlockRepository().getBlockchainHeight();
|
||||||
final int start = BlockChain.getInstance().getSelfSponsorshipAlgoV2Height() - 1180;
|
final int start = BlockChain.getInstance().getSelfSponsorshipAlgoV2Height() - 1180;
|
||||||
final int end = BlockChain.getInstance().getSelfSponsorshipAlgoV3Height();
|
final int end = BlockChain.getInstance().getSelfSponsorshipAlgoV3Height();
|
||||||
@ -94,7 +106,7 @@ public class RegisterNameTransaction extends Transaction {
|
|||||||
return ValidationResult.NAME_ALREADY_REGISTERED;
|
return ValidationResult.NAME_ALREADY_REGISTERED;
|
||||||
|
|
||||||
// If accounts are only allowed one registered name then check for this
|
// If accounts are only allowed one registered name then check for this
|
||||||
if (BlockChain.getInstance().oneNamePerAccount()
|
if (BlockChain.getInstance().oneNamePerAccount(this.repository.getBlockRepository().getBlockchainHeight())
|
||||||
&& !this.repository.getNameRepository().getNamesByOwner(getRegistrant().getAddress()).isEmpty())
|
&& !this.repository.getNameRepository().getNamesByOwner(getRegistrant().getAddress()).isEmpty())
|
||||||
return ValidationResult.MULTIPLE_NAMES_FORBIDDEN;
|
return ValidationResult.MULTIPLE_NAMES_FORBIDDEN;
|
||||||
|
|
||||||
@ -117,6 +129,16 @@ public class RegisterNameTransaction extends Transaction {
|
|||||||
// Register Name
|
// Register Name
|
||||||
Name name = new Name(this.repository, this.registerNameTransactionData);
|
Name name = new Name(this.repository, this.registerNameTransactionData);
|
||||||
name.register();
|
name.register();
|
||||||
|
|
||||||
|
if( this.repository.getBlockRepository().getBlockchainHeight() > BlockChain.getInstance().getMultipleNamesPerAccountHeight()) {
|
||||||
|
|
||||||
|
Account account = new Account(this.repository, this.getCreator().getAddress());
|
||||||
|
|
||||||
|
// if there is no primary name established, then the new registered name is the primary name
|
||||||
|
if (account.getPrimaryName().isEmpty()) {
|
||||||
|
account.setPrimaryName(this.registerNameTransactionData.getName());
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
|
@ -45,6 +45,12 @@ public class SellNameTransaction extends Transaction {
|
|||||||
public ValidationResult isValid() throws DataException {
|
public ValidationResult isValid() throws DataException {
|
||||||
String name = this.sellNameTransactionData.getName();
|
String name = this.sellNameTransactionData.getName();
|
||||||
|
|
||||||
|
// if the account has more than one name, then they cannot sell their primary name
|
||||||
|
if( this.repository.getNameRepository().getNamesByOwner(this.getOwner().getAddress()).size() > 1 &&
|
||||||
|
this.getOwner().getPrimaryName().get().equals(name) ) {
|
||||||
|
return ValidationResult.NOT_SUPPORTED;
|
||||||
|
}
|
||||||
|
|
||||||
// Check name size bounds
|
// Check name size bounds
|
||||||
int nameLength = Utf8.encodedLength(name);
|
int nameLength = Utf8.encodedLength(name);
|
||||||
if (nameLength < 1 || nameLength > Name.MAX_NAME_SIZE)
|
if (nameLength < 1 || nameLength > Name.MAX_NAME_SIZE)
|
||||||
|
@ -3,6 +3,7 @@ package org.qortal.transaction;
|
|||||||
import com.google.common.base.Utf8;
|
import com.google.common.base.Utf8;
|
||||||
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.controller.repository.NamesDatabaseIntegrityCheck;
|
import org.qortal.controller.repository.NamesDatabaseIntegrityCheck;
|
||||||
import org.qortal.crypto.Crypto;
|
import org.qortal.crypto.Crypto;
|
||||||
import org.qortal.data.naming.NameData;
|
import org.qortal.data.naming.NameData;
|
||||||
@ -49,6 +50,12 @@ public class UpdateNameTransaction extends Transaction {
|
|||||||
public ValidationResult isValid() throws DataException {
|
public ValidationResult isValid() throws DataException {
|
||||||
String name = this.updateNameTransactionData.getName();
|
String name = this.updateNameTransactionData.getName();
|
||||||
|
|
||||||
|
// if the account has more than one name, then they cannot update their primary name
|
||||||
|
if( this.repository.getNameRepository().getNamesByOwner(this.getOwner().getAddress()).size() > 1 &&
|
||||||
|
this.getOwner().getPrimaryName().get().equals(name) ) {
|
||||||
|
return ValidationResult.NOT_SUPPORTED;
|
||||||
|
}
|
||||||
|
|
||||||
// Check name size bounds
|
// Check name size bounds
|
||||||
int nameLength = Utf8.encodedLength(name);
|
int nameLength = Utf8.encodedLength(name);
|
||||||
if (nameLength < Name.MIN_NAME_SIZE || nameLength > Name.MAX_NAME_SIZE)
|
if (nameLength < Name.MIN_NAME_SIZE || nameLength > Name.MAX_NAME_SIZE)
|
||||||
@ -152,6 +159,16 @@ public class UpdateNameTransaction extends Transaction {
|
|||||||
|
|
||||||
// Save this transaction, now with updated "name reference" to previous transaction that changed name
|
// Save this transaction, now with updated "name reference" to previous transaction that changed name
|
||||||
this.repository.getTransactionRepository().save(this.updateNameTransactionData);
|
this.repository.getTransactionRepository().save(this.updateNameTransactionData);
|
||||||
|
|
||||||
|
if( this.repository.getBlockRepository().getBlockchainHeight() > BlockChain.getInstance().getMultipleNamesPerAccountHeight()) {
|
||||||
|
|
||||||
|
Account account = new Account(this.repository, this.getCreator().getAddress());
|
||||||
|
|
||||||
|
// if updating the primary name, then set primary name to new name
|
||||||
|
if( account.getPrimaryName().isEmpty() || account.getPrimaryName().get().equals(this.updateNameTransactionData.getName())) {
|
||||||
|
account.setPrimaryName(this.updateNameTransactionData.getNewName());
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
@ -167,6 +184,16 @@ public class UpdateNameTransaction extends Transaction {
|
|||||||
|
|
||||||
// Save this transaction, with previous "name reference"
|
// Save this transaction, with previous "name reference"
|
||||||
this.repository.getTransactionRepository().save(this.updateNameTransactionData);
|
this.repository.getTransactionRepository().save(this.updateNameTransactionData);
|
||||||
|
|
||||||
|
if( this.repository.getBlockRepository().getBlockchainHeight() > BlockChain.getInstance().getMultipleNamesPerAccountHeight()) {
|
||||||
|
|
||||||
|
Account account = new Account(this.repository, this.getCreator().getAddress());
|
||||||
|
|
||||||
|
// if the primary name is the new updated name, then it needs to be set back to the previous name
|
||||||
|
if (account.getPrimaryName().isPresent() && account.getPrimaryName().get().equals(this.updateNameTransactionData.getNewName())) {
|
||||||
|
account.setPrimaryName(this.updateNameTransactionData.getName());
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
@ -53,7 +53,7 @@ public class ArbitraryIndexUtils {
|
|||||||
try {
|
try {
|
||||||
fillCache(IndexCache.getInstance());
|
fillCache(IndexCache.getInstance());
|
||||||
} catch (IOException | DataException e) {
|
} catch (IOException | DataException e) {
|
||||||
LOGGER.error(e.getMessage(), e);
|
LOGGER.warn(e.getMessage());
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
@ -111,6 +111,8 @@ public class ArbitraryIndexUtils {
|
|||||||
|
|
||||||
indexDetails.add( new ArbitraryDataIndexDetail(indexResource.name, rank, indices.get(rank - 1), indexResource.identifier ));
|
indexDetails.add( new ArbitraryDataIndexDetail(indexResource.name, rank, indices.get(rank - 1), indexResource.identifier ));
|
||||||
}
|
}
|
||||||
|
} catch (MissingDataException e) {
|
||||||
|
LOGGER.warn( e.getMessage() );
|
||||||
} catch (InvalidFormatException e) {
|
} catch (InvalidFormatException e) {
|
||||||
LOGGER.debug("invalid format, skipping: " + indexResource);
|
LOGGER.debug("invalid format, skipping: " + indexResource);
|
||||||
} catch (UnrecognizedPropertyException e) {
|
} catch (UnrecognizedPropertyException e) {
|
||||||
@ -131,16 +133,12 @@ public class ArbitraryIndexUtils {
|
|||||||
)
|
)
|
||||||
);
|
);
|
||||||
|
|
||||||
LOGGER.info("processed indices by term: count = " + indicesByTerm.size());
|
|
||||||
|
|
||||||
// lock, clear old, load new
|
// lock, clear old, load new
|
||||||
synchronized( IndexCache.getInstance().getIndicesByTerm() ) {
|
synchronized( IndexCache.getInstance().getIndicesByTerm() ) {
|
||||||
IndexCache.getInstance().getIndicesByTerm().clear();
|
IndexCache.getInstance().getIndicesByTerm().clear();
|
||||||
IndexCache.getInstance().getIndicesByTerm().putAll(indicesByTerm);
|
IndexCache.getInstance().getIndicesByTerm().putAll(indicesByTerm);
|
||||||
}
|
}
|
||||||
|
|
||||||
LOGGER.info("loaded indices by term");
|
|
||||||
|
|
||||||
LOGGER.debug("processing indices by issuer ...");
|
LOGGER.debug("processing indices by issuer ...");
|
||||||
Map<String, List<ArbitraryDataIndexDetail>> indicesByIssuer
|
Map<String, List<ArbitraryDataIndexDetail>> indicesByIssuer
|
||||||
= indexDetails.stream().collect(
|
= indexDetails.stream().collect(
|
||||||
@ -154,15 +152,11 @@ public class ArbitraryIndexUtils {
|
|||||||
)
|
)
|
||||||
);
|
);
|
||||||
|
|
||||||
LOGGER.info("processed indices by issuer: count = " + indicesByIssuer.size());
|
|
||||||
|
|
||||||
// lock, clear old, load new
|
// lock, clear old, load new
|
||||||
synchronized( IndexCache.getInstance().getIndicesByIssuer() ) {
|
synchronized( IndexCache.getInstance().getIndicesByIssuer() ) {
|
||||||
IndexCache.getInstance().getIndicesByIssuer().clear();
|
IndexCache.getInstance().getIndicesByIssuer().clear();
|
||||||
IndexCache.getInstance().getIndicesByIssuer().putAll(indicesByIssuer);
|
IndexCache.getInstance().getIndicesByIssuer().putAll(indicesByIssuer);
|
||||||
}
|
}
|
||||||
|
|
||||||
LOGGER.info("loaded indices by issuer");
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -199,7 +193,7 @@ public class ArbitraryIndexUtils {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
public static String getJson(String name, String identifier) throws IOException {
|
public static String getJson(String name, String identifier) throws IOException, MissingDataException {
|
||||||
|
|
||||||
try {
|
try {
|
||||||
ArbitraryDataReader arbitraryDataReader
|
ArbitraryDataReader arbitraryDataReader
|
||||||
@ -217,11 +211,10 @@ public class ArbitraryIndexUtils {
|
|||||||
} catch (MissingDataException e) {
|
} catch (MissingDataException e) {
|
||||||
if (attempts > maxAttempts) {
|
if (attempts > maxAttempts) {
|
||||||
// Give up after 5 attempts
|
// Give up after 5 attempts
|
||||||
throw new IOException("Data unavailable. Please try again later.");
|
throw e;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
Thread.sleep(3000L);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
java.nio.file.Path outputPath = arbitraryDataReader.getFilePath();
|
java.nio.file.Path outputPath = arbitraryDataReader.getFilePath();
|
||||||
|
@ -48,6 +48,24 @@ public class ArbitraryTransactionUtils {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public static List<ArbitraryTransactionData> fetchTransactionDataList(final Repository repository, final List<byte[]> signature) {
|
||||||
|
try {
|
||||||
|
List<TransactionData> transactions = repository.getTransactionRepository().fromSignatures(signature);
|
||||||
|
|
||||||
|
List<ArbitraryTransactionData> list
|
||||||
|
= transactions.stream()
|
||||||
|
.filter( transaction -> transaction instanceof ArbitraryTransactionData )
|
||||||
|
.map( transactionData -> (ArbitraryTransactionData) transactionData)
|
||||||
|
.collect(Collectors.toList());
|
||||||
|
|
||||||
|
return list;
|
||||||
|
|
||||||
|
} catch (DataException e) {
|
||||||
|
LOGGER.error("Repository issue when fetching arbitrary transaction data", e);
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
public static ArbitraryTransactionData fetchLatestPut(Repository repository, ArbitraryTransactionData arbitraryTransactionData) {
|
public static ArbitraryTransactionData fetchLatestPut(Repository repository, ArbitraryTransactionData arbitraryTransactionData) {
|
||||||
if (arbitraryTransactionData == null) {
|
if (arbitraryTransactionData == null) {
|
||||||
return null;
|
return null;
|
||||||
|
@ -6,6 +6,7 @@ import org.qortal.settings.Settings;
|
|||||||
|
|
||||||
import java.io.File;
|
import java.io.File;
|
||||||
import java.io.IOException;
|
import java.io.IOException;
|
||||||
|
import java.io.InputStream;
|
||||||
import java.io.RandomAccessFile;
|
import java.io.RandomAccessFile;
|
||||||
import java.nio.charset.StandardCharsets;
|
import java.nio.charset.StandardCharsets;
|
||||||
import java.nio.file.*;
|
import java.nio.file.*;
|
||||||
@ -232,32 +233,38 @@ public class FilesystemUtils {
|
|||||||
}
|
}
|
||||||
|
|
||||||
public static byte[] getSingleFileContents(Path path, Integer maxLength) throws IOException {
|
public static byte[] getSingleFileContents(Path path, Integer maxLength) throws IOException {
|
||||||
byte[] data = null;
|
Path filePath = null;
|
||||||
// TODO: limit the file size that can be loaded into memory
|
|
||||||
|
|
||||||
// If the path is a file, read the contents directly
|
if (Files.isRegularFile(path)) {
|
||||||
if (path.toFile().isFile()) {
|
filePath = path;
|
||||||
int fileSize = (int)path.toFile().length();
|
} else if (Files.isDirectory(path)) {
|
||||||
maxLength = maxLength != null ? Math.min(maxLength, fileSize) : fileSize;
|
|
||||||
data = FilesystemUtils.readFromFile(path.toString(), 0, maxLength);
|
|
||||||
}
|
|
||||||
|
|
||||||
// Or if it's a directory, only load file contents if there is a single file inside it
|
|
||||||
else if (path.toFile().isDirectory()) {
|
|
||||||
String[] files = ArrayUtils.removeElement(path.toFile().list(), ".qortal");
|
String[] files = ArrayUtils.removeElement(path.toFile().list(), ".qortal");
|
||||||
if (files.length == 1) {
|
if (files.length == 1) {
|
||||||
Path filePath = Paths.get(path.toString(), files[0]);
|
filePath = path.resolve(files[0]);
|
||||||
if (filePath.toFile().isFile()) {
|
|
||||||
int fileSize = (int)filePath.toFile().length();
|
|
||||||
maxLength = maxLength != null ? Math.min(maxLength, fileSize) : fileSize;
|
|
||||||
data = FilesystemUtils.readFromFile(filePath.toString(), 0, maxLength);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
return data;
|
if (filePath == null || !Files.exists(filePath)) {
|
||||||
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
long fileSize = Files.size(filePath);
|
||||||
|
int length = (maxLength != null) ? Math.min(maxLength, (int) Math.min(fileSize, Integer.MAX_VALUE)) : (int) Math.min(fileSize, Integer.MAX_VALUE);
|
||||||
|
|
||||||
|
try (InputStream in = Files.newInputStream(filePath)) {
|
||||||
|
byte[] buffer = new byte[length];
|
||||||
|
int bytesRead = in.read(buffer);
|
||||||
|
if (bytesRead < length) {
|
||||||
|
// Resize buffer to actual read size
|
||||||
|
byte[] trimmed = new byte[bytesRead];
|
||||||
|
System.arraycopy(buffer, 0, trimmed, 0, bytesRead);
|
||||||
|
return trimmed;
|
||||||
|
}
|
||||||
|
return buffer;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* isSingleFileResource
|
* isSingleFileResource
|
||||||
* Returns true if the path points to a file, or a
|
* Returns true if the path points to a file, or a
|
||||||
|
187
src/main/java/org/qortal/utils/ForeignFeesMessageUtils.java
Normal file
187
src/main/java/org/qortal/utils/ForeignFeesMessageUtils.java
Normal file
@ -0,0 +1,187 @@
|
|||||||
|
package org.qortal.utils;
|
||||||
|
|
||||||
|
import com.google.common.primitives.Ints;
|
||||||
|
import com.google.common.primitives.Longs;
|
||||||
|
import org.apache.logging.log4j.LogManager;
|
||||||
|
import org.apache.logging.log4j.Logger;
|
||||||
|
import org.qortal.data.crosschain.ForeignFeeDecodedData;
|
||||||
|
import org.qortal.transform.Transformer;
|
||||||
|
|
||||||
|
import java.io.ByteArrayOutputStream;
|
||||||
|
import java.io.IOException;
|
||||||
|
import java.nio.ByteBuffer;
|
||||||
|
import java.util.ArrayList;
|
||||||
|
import java.util.List;
|
||||||
|
|
||||||
|
import static org.qortal.transform.Transformer.ADDRESS_LENGTH;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Class ForeignFeesMessageUtils
|
||||||
|
*/
|
||||||
|
public class ForeignFeesMessageUtils {
|
||||||
|
|
||||||
|
private static final Logger LOGGER = LogManager.getLogger(ForeignFeesMessageUtils.class);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* From Data To Send Bytes
|
||||||
|
*
|
||||||
|
* Convert foreign fee data into bytes for send messages.
|
||||||
|
*
|
||||||
|
* @param foreignFees the data
|
||||||
|
*
|
||||||
|
* @return the bytes
|
||||||
|
*/
|
||||||
|
public static byte[] fromDataToSendBytes(List<ForeignFeeDecodedData> foreignFees) {
|
||||||
|
|
||||||
|
return fromDataToBytes(foreignFees, true);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* From Data To Bytes
|
||||||
|
*
|
||||||
|
* @param foreignFees
|
||||||
|
* @param includeSignature
|
||||||
|
* @return
|
||||||
|
*/
|
||||||
|
private static byte[] fromDataToBytes(List<ForeignFeeDecodedData> foreignFees, boolean includeSignature) {
|
||||||
|
try {
|
||||||
|
if (foreignFees.isEmpty()) {
|
||||||
|
return new byte[0];
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
// allocate size for each data item for timestamp, AT address, fee and signature
|
||||||
|
int byteSize
|
||||||
|
= foreignFees.size()
|
||||||
|
*
|
||||||
|
(Transformer.TIMESTAMP_LENGTH + Transformer.ADDRESS_LENGTH + Transformer.INT_LENGTH + Transformer.SIGNATURE_LENGTH);
|
||||||
|
|
||||||
|
if( includeSignature ) byteSize += foreignFees.size() * Transformer.SIGNATURE_LENGTH;
|
||||||
|
|
||||||
|
ByteArrayOutputStream bytes = new ByteArrayOutputStream(byteSize);
|
||||||
|
|
||||||
|
// for each foreign fee data item, convert to bytes and fill the array
|
||||||
|
for( ForeignFeeDecodedData feeData : foreignFees) {
|
||||||
|
bytes.write(Longs.toByteArray(feeData.getTimestamp()));
|
||||||
|
bytes.write(Base58.decode(feeData.getAtAddress()));
|
||||||
|
bytes.write(Ints.toByteArray(feeData.getFee()));
|
||||||
|
if( includeSignature ) bytes.write(feeData.getData());
|
||||||
|
}
|
||||||
|
return bytes.toByteArray();
|
||||||
|
}
|
||||||
|
} catch (Exception e) {
|
||||||
|
LOGGER.warn(e.getMessage());
|
||||||
|
|
||||||
|
return new byte[0];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* From Send Bytes to Data
|
||||||
|
*
|
||||||
|
* @param bytes the bytes to convert to data
|
||||||
|
*
|
||||||
|
* @return the data
|
||||||
|
*/
|
||||||
|
public static List<ForeignFeeDecodedData> fromSendBytesToData(ByteBuffer bytes) {
|
||||||
|
|
||||||
|
return fromBytesToData(bytes, true);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* From Bytes To Data
|
||||||
|
*
|
||||||
|
* @param bytes the bytes
|
||||||
|
* @param includeSignature true if the bytes include signatures
|
||||||
|
*
|
||||||
|
* @return the foreign fee data with signatures (data member)
|
||||||
|
*/
|
||||||
|
private static List<ForeignFeeDecodedData> fromBytesToData(ByteBuffer bytes, boolean includeSignature) {
|
||||||
|
if( !bytes.hasRemaining() ) return new ArrayList<>(0);
|
||||||
|
|
||||||
|
List<ForeignFeeDecodedData> foreignFees = new ArrayList<>();
|
||||||
|
|
||||||
|
try {
|
||||||
|
while (bytes.hasRemaining()) {
|
||||||
|
// read in the timestamp as a long
|
||||||
|
long timestamp = bytes.getLong();
|
||||||
|
|
||||||
|
// read in the address as a byte array with a predetermined length
|
||||||
|
byte[] atAddressBytes = new byte[ADDRESS_LENGTH];
|
||||||
|
bytes.get(atAddressBytes);
|
||||||
|
String atAddress = Base58.encode(atAddressBytes);
|
||||||
|
|
||||||
|
// rwad in the fee as an integer
|
||||||
|
int fee = bytes.getInt();
|
||||||
|
|
||||||
|
byte[] signature;
|
||||||
|
|
||||||
|
if( includeSignature ) {
|
||||||
|
signature = new byte[Transformer.SIGNATURE_LENGTH];
|
||||||
|
bytes.get(signature);
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
signature = null;
|
||||||
|
}
|
||||||
|
|
||||||
|
foreignFees.add(new ForeignFeeDecodedData(timestamp, signature, atAddress, fee));
|
||||||
|
|
||||||
|
}
|
||||||
|
}
|
||||||
|
// if there are any exception, log the error as a warning and clear the list before returning it
|
||||||
|
catch (Exception e) {
|
||||||
|
LOGGER.warn(e.getMessage());
|
||||||
|
foreignFees.clear();
|
||||||
|
}
|
||||||
|
|
||||||
|
return foreignFees;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* From Data To Get Bytes
|
||||||
|
*
|
||||||
|
* Convert foreign fees data objects into get foreign fees messages. Get messages do not include signatures.
|
||||||
|
*
|
||||||
|
* @param foreignFees the foreign fees objects
|
||||||
|
*
|
||||||
|
* @return the messages
|
||||||
|
*/
|
||||||
|
public static byte[] fromDataToGetBytes(List<ForeignFeeDecodedData> foreignFees) {
|
||||||
|
return fromDataToBytes(foreignFees, false);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* From Get Bytes to Data
|
||||||
|
*
|
||||||
|
* Convert bytes from get foreign fees messages to foreign fees objects. Get messages do not include signatures.
|
||||||
|
*
|
||||||
|
* @param bytes the bytes to convert
|
||||||
|
*
|
||||||
|
* @return the foreign fees data objects
|
||||||
|
*/
|
||||||
|
public static List<ForeignFeeDecodedData> fromGetBytesToData(ByteBuffer bytes) {
|
||||||
|
return fromBytesToData(bytes, false);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Build Foreign Fees Data Message
|
||||||
|
*
|
||||||
|
* Build the unsigned message for the foreign fees data objects.
|
||||||
|
*
|
||||||
|
* @param timestamp the timestamp in milliseconds
|
||||||
|
* @param atAddress the AT address
|
||||||
|
* @param fee the fee
|
||||||
|
* @return
|
||||||
|
* @throws IOException
|
||||||
|
*/
|
||||||
|
public static byte[] buildForeignFeesDataMessage(Long timestamp, String atAddress, int fee) throws IOException {
|
||||||
|
int byteSize = Transformer.TIMESTAMP_LENGTH + Transformer.ADDRESS_LENGTH + Transformer.INT_LENGTH;
|
||||||
|
|
||||||
|
ByteArrayOutputStream bytes = new ByteArrayOutputStream(byteSize);
|
||||||
|
|
||||||
|
bytes.write(Longs.toByteArray(timestamp));
|
||||||
|
bytes.write(Base58.decode(atAddress));
|
||||||
|
bytes.write(Ints.toByteArray(fee));
|
||||||
|
|
||||||
|
return bytes.toByteArray();
|
||||||
|
}
|
||||||
|
}
|
@ -27,6 +27,8 @@
|
|||||||
|
|
||||||
package org.qortal.utils;
|
package org.qortal.utils;
|
||||||
|
|
||||||
|
import java.io.BufferedOutputStream;
|
||||||
|
|
||||||
import org.qortal.controller.Controller;
|
import org.qortal.controller.Controller;
|
||||||
|
|
||||||
import java.io.File;
|
import java.io.File;
|
||||||
@ -44,12 +46,16 @@ public class ZipUtils {
|
|||||||
File sourceFile = new File(sourcePath);
|
File sourceFile = new File(sourcePath);
|
||||||
boolean isSingleFile = Paths.get(sourcePath).toFile().isFile();
|
boolean isSingleFile = Paths.get(sourcePath).toFile().isFile();
|
||||||
FileOutputStream fileOutputStream = new FileOutputStream(destFilePath);
|
FileOutputStream fileOutputStream = new FileOutputStream(destFilePath);
|
||||||
|
|
||||||
|
// 🔧 Use best speed compression level
|
||||||
ZipOutputStream zipOutputStream = new ZipOutputStream(fileOutputStream);
|
ZipOutputStream zipOutputStream = new ZipOutputStream(fileOutputStream);
|
||||||
ZipUtils.zip(sourceFile, enclosingFolderName, zipOutputStream, isSingleFile);
|
ZipUtils.zip(sourceFile, enclosingFolderName, zipOutputStream, isSingleFile);
|
||||||
|
|
||||||
zipOutputStream.close();
|
zipOutputStream.close();
|
||||||
fileOutputStream.close();
|
fileOutputStream.close();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
public static void zip(final File fileToZip, final String enclosingFolderName, final ZipOutputStream zipOut, boolean isSingleFile) throws IOException, InterruptedException {
|
public static void zip(final File fileToZip, final String enclosingFolderName, final ZipOutputStream zipOut, boolean isSingleFile) throws IOException, InterruptedException {
|
||||||
if (Controller.isStopping()) {
|
if (Controller.isStopping()) {
|
||||||
throw new InterruptedException("Controller is stopping");
|
throw new InterruptedException("Controller is stopping");
|
||||||
@ -82,7 +88,7 @@ public class ZipUtils {
|
|||||||
final FileInputStream fis = new FileInputStream(fileToZip);
|
final FileInputStream fis = new FileInputStream(fileToZip);
|
||||||
final ZipEntry zipEntry = new ZipEntry(enclosingFolderName);
|
final ZipEntry zipEntry = new ZipEntry(enclosingFolderName);
|
||||||
zipOut.putNextEntry(zipEntry);
|
zipOut.putNextEntry(zipEntry);
|
||||||
final byte[] bytes = new byte[1024];
|
final byte[] bytes = new byte[65536];
|
||||||
int length;
|
int length;
|
||||||
while ((length = fis.read(bytes)) >= 0) {
|
while ((length = fis.read(bytes)) >= 0) {
|
||||||
zipOut.write(bytes, 0, length);
|
zipOut.write(bytes, 0, length);
|
||||||
@ -92,8 +98,8 @@ public class ZipUtils {
|
|||||||
|
|
||||||
public static void unzip(String sourcePath, String destPath) throws IOException {
|
public static void unzip(String sourcePath, String destPath) throws IOException {
|
||||||
final File destDir = new File(destPath);
|
final File destDir = new File(destPath);
|
||||||
final byte[] buffer = new byte[1024];
|
final byte[] buffer = new byte[65536];
|
||||||
final ZipInputStream zis = new ZipInputStream(new FileInputStream(sourcePath));
|
try (ZipInputStream zis = new ZipInputStream(new FileInputStream(sourcePath))) {
|
||||||
ZipEntry zipEntry = zis.getNextEntry();
|
ZipEntry zipEntry = zis.getNextEntry();
|
||||||
while (zipEntry != null) {
|
while (zipEntry != null) {
|
||||||
final File newFile = ZipUtils.newFile(destDir, zipEntry);
|
final File newFile = ZipUtils.newFile(destDir, zipEntry);
|
||||||
@ -107,18 +113,19 @@ public class ZipUtils {
|
|||||||
throw new IOException("Failed to create directory " + parent);
|
throw new IOException("Failed to create directory " + parent);
|
||||||
}
|
}
|
||||||
|
|
||||||
final FileOutputStream fos = new FileOutputStream(newFile);
|
try (BufferedOutputStream bos = new BufferedOutputStream(new FileOutputStream(newFile), buffer.length)) {
|
||||||
int len;
|
int len;
|
||||||
while ((len = zis.read(buffer)) > 0) {
|
while ((len = zis.read(buffer)) > 0) {
|
||||||
fos.write(buffer, 0, len);
|
bos.write(buffer, 0, len);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
fos.close();
|
|
||||||
}
|
}
|
||||||
zipEntry = zis.getNextEntry();
|
zipEntry = zis.getNextEntry();
|
||||||
}
|
}
|
||||||
zis.closeEntry();
|
zis.closeEntry();
|
||||||
zis.close();
|
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* See: https://snyk.io/research/zip-slip-vulnerability
|
* See: https://snyk.io/research/zip-slip-vulnerability
|
||||||
|
@ -119,7 +119,9 @@
|
|||||||
"adminsReplaceFoundersHeight": 2012800,
|
"adminsReplaceFoundersHeight": 2012800,
|
||||||
"nullGroupMembershipHeight": 2012800,
|
"nullGroupMembershipHeight": 2012800,
|
||||||
"ignoreLevelForRewardShareHeight": 2012800,
|
"ignoreLevelForRewardShareHeight": 2012800,
|
||||||
"adminQueryFixHeight": 2012800
|
"adminQueryFixHeight": 2012800,
|
||||||
|
"multipleNamesPerAccountHeight": 9999999,
|
||||||
|
"mintedBlocksAdjustmentRemovalHeight": 9999999
|
||||||
},
|
},
|
||||||
"checkpoints": [
|
"checkpoints": [
|
||||||
{ "height": 1136300, "signature": "3BbwawEF2uN8Ni5ofpJXkukoU8ctAPxYoFB7whq9pKfBnjfZcpfEJT4R95NvBDoTP8WDyWvsUvbfHbcr9qSZuYpSKZjUQTvdFf6eqznHGEwhZApWfvXu6zjGCxYCp65F4jsVYYJjkzbjmkCg5WAwN5voudngA23kMK6PpTNygapCzXt" }
|
{ "height": 1136300, "signature": "3BbwawEF2uN8Ni5ofpJXkukoU8ctAPxYoFB7whq9pKfBnjfZcpfEJT4R95NvBDoTP8WDyWvsUvbfHbcr9qSZuYpSKZjUQTvdFf6eqznHGEwhZApWfvXu6zjGCxYCp65F4jsVYYJjkzbjmkCg5WAwN5voudngA23kMK6PpTNygapCzXt" }
|
||||||
|
@ -45,6 +45,7 @@ function parseUrl(url) {
|
|||||||
|
|
||||||
// Remove theme, identifier, and time queries if they exist
|
// Remove theme, identifier, and time queries if they exist
|
||||||
parsedUrl.searchParams.delete("theme");
|
parsedUrl.searchParams.delete("theme");
|
||||||
|
parsedUrl.searchParams.delete("lang");
|
||||||
parsedUrl.searchParams.delete("identifier");
|
parsedUrl.searchParams.delete("identifier");
|
||||||
parsedUrl.searchParams.delete("time");
|
parsedUrl.searchParams.delete("time");
|
||||||
parsedUrl.searchParams.delete("isManualNavigation");
|
parsedUrl.searchParams.delete("isManualNavigation");
|
||||||
@ -213,8 +214,11 @@ function buildResourceUrl(service, name, identifier, path, isLink) {
|
|||||||
if (path != null) url = url.concat((path.startsWith("/") ? "" : "/") + path);
|
if (path != null) url = url.concat((path.startsWith("/") ? "" : "/") + path);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (isLink) url = url.concat((url.includes("?") ? "" : "?") + "&theme=" + _qdnTheme);
|
if (isLink) {
|
||||||
|
const hasQuery = url.includes("?");
|
||||||
|
const queryPrefix = hasQuery ? "&" : "?";
|
||||||
|
url += queryPrefix + "theme=" + _qdnTheme + "&lang=" + _qdnLang;
|
||||||
|
}
|
||||||
return url;
|
return url;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -98,6 +98,45 @@ public class ArbitraryServiceTests extends Common {
|
|||||||
assertEquals(ValidationResult.MISSING_INDEX_FILE, service.validate(path));
|
assertEquals(ValidationResult.MISSING_INDEX_FILE, service.validate(path));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void testValidateWebsiteWithIndexFile() throws IOException {
|
||||||
|
// Generate some random data
|
||||||
|
byte[] data = new byte[1024];
|
||||||
|
new Random().nextBytes(data);
|
||||||
|
|
||||||
|
// Write the data to several files in a temp path
|
||||||
|
Path path = Files.createTempDirectory("testValidateWebsiteWithoutIndexFile");
|
||||||
|
path.toFile().deleteOnExit();
|
||||||
|
Files.write(Paths.get(path.toString(), "index.html"), data, StandardOpenOption.CREATE);
|
||||||
|
Files.write(Paths.get(path.toString(), "data1.html"), data, StandardOpenOption.CREATE);
|
||||||
|
Files.write(Paths.get(path.toString(), "data2"), data, StandardOpenOption.CREATE);
|
||||||
|
Files.write(Paths.get(path.toString(), "data3"), data, StandardOpenOption.CREATE);
|
||||||
|
|
||||||
|
Service service = Service.WEBSITE;
|
||||||
|
assertTrue(service.isValidationRequired());
|
||||||
|
|
||||||
|
// There is no index file in the root
|
||||||
|
assertEquals(ValidationResult.OK, service.validate(path));
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void testValidateWebsiteWithIndexFileOnly() throws IOException {
|
||||||
|
// Generate some random data
|
||||||
|
byte[] data = new byte[1024];
|
||||||
|
new Random().nextBytes(data);
|
||||||
|
|
||||||
|
// Write the data to several files in a temp path
|
||||||
|
Path path = Files.createTempDirectory("testValidateWebsiteWithoutIndexFile");
|
||||||
|
path.toFile().deleteOnExit();
|
||||||
|
Files.write(Paths.get(path.toString(), "index.html"), data, StandardOpenOption.CREATE);
|
||||||
|
|
||||||
|
Service service = Service.WEBSITE;
|
||||||
|
assertTrue(service.isValidationRequired());
|
||||||
|
|
||||||
|
// There is no index file in the root
|
||||||
|
assertEquals(ValidationResult.OK, service.validate(path));
|
||||||
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
public void testValidateWebsiteWithoutIndexFileInRoot() throws IOException {
|
public void testValidateWebsiteWithoutIndexFileInRoot() throws IOException {
|
||||||
// Generate some random data
|
// Generate some random data
|
||||||
|
@ -1,6 +1,7 @@
|
|||||||
package org.qortal.test.group;
|
package org.qortal.test.group;
|
||||||
|
|
||||||
import org.junit.After;
|
import org.junit.After;
|
||||||
|
import org.junit.Assert;
|
||||||
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;
|
||||||
@ -369,6 +370,105 @@ public class DevGroupAdminTests extends Common {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void testOrphanSecondInviteApproval() 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 chloeInvite1 = createGroupInviteForGroupApproval(repository, bob, DEV_GROUP_ID, chloe.getAddress(), 3600);
|
||||||
|
Transaction.ApprovalStatus chloeInvite1Status = signForGroupApproval(repository, chloeInvite1, List.of(bob));
|
||||||
|
|
||||||
|
// assert invite 1 pending
|
||||||
|
assertEquals(Transaction.ApprovalStatus.PENDING, chloeInvite1Status);
|
||||||
|
|
||||||
|
// bob invites chloe again, bob signs which is 33% approval while 40% is needed
|
||||||
|
// since chloe is not a member yet, this invite is valie
|
||||||
|
TransactionData chloeInvite2 = createGroupInviteForGroupApproval(repository, bob, DEV_GROUP_ID, chloe.getAddress(), 3600);
|
||||||
|
Transaction.ApprovalStatus chloeInvite2Status = signForGroupApproval(repository, chloeInvite2, List.of(bob));
|
||||||
|
|
||||||
|
// assert invite 2 is pending
|
||||||
|
assertEquals(Transaction.ApprovalStatus.PENDING, chloeInvite2Status);
|
||||||
|
|
||||||
|
// alice signs which is 66% approval while 40% is needed
|
||||||
|
chloeInvite1Status = signForGroupApproval(repository, chloeInvite1, List.of(alice));
|
||||||
|
|
||||||
|
// assert invite 1 approval
|
||||||
|
assertEquals(Transaction.ApprovalStatus.APPROVED, chloeInvite1Status);
|
||||||
|
|
||||||
|
// chloe joins
|
||||||
|
joinGroup(repository, chloe, DEV_GROUP_ID);
|
||||||
|
|
||||||
|
// assert chloe is in the group
|
||||||
|
assertTrue(isMember(repository, bob.getAddress(), DEV_GROUP_ID));
|
||||||
|
|
||||||
|
// alice signs invite 2 which is 66% approval while 40% is needed
|
||||||
|
chloeInvite2Status = signForGroupApproval(repository, chloeInvite2, List.of(alice));
|
||||||
|
|
||||||
|
// assert invite 2 approval
|
||||||
|
assertEquals(Transaction.ApprovalStatus.APPROVED, chloeInvite2Status);
|
||||||
|
|
||||||
|
boolean exceptionThrown = false;
|
||||||
|
|
||||||
|
try {
|
||||||
|
// confront the bug by orphaning the block of the second approval, approval after the join
|
||||||
|
// prior to the fix, this would raise an exception
|
||||||
|
BlockUtils.orphanLastBlock(repository);
|
||||||
|
} catch (DataException e) {
|
||||||
|
exceptionThrown = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
Assert.assertFalse(exceptionThrown);
|
||||||
|
|
||||||
|
// assert chloe is still a member
|
||||||
|
assertTrue(isMember(repository, bob.getAddress(), DEV_GROUP_ID));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
public void testNullOwnershipMembership() throws DataException{
|
public void testNullOwnershipMembership() throws DataException{
|
||||||
try (final Repository repository = RepositoryManager.getRepository()) {
|
try (final Repository repository = RepositoryManager.getRepository()) {
|
||||||
|
Some files were not shown because too many files have changed in this diff Show More
Loading…
x
Reference in New Issue
Block a user