forked from Qortal/qortal
Merge branch 'master' into q-apps
This commit is contained in:
commit
ca09dd264f
@ -11,6 +11,7 @@ import io.swagger.v3.oas.annotations.security.SecurityRequirement;
|
||||
import io.swagger.v3.oas.annotations.tags.Tag;
|
||||
|
||||
import java.util.List;
|
||||
import java.util.Objects;
|
||||
import java.util.stream.Collectors;
|
||||
|
||||
import javax.servlet.http.HttpServletRequest;
|
||||
@ -38,9 +39,12 @@ import org.qortal.crypto.Crypto;
|
||||
import org.qortal.data.at.ATData;
|
||||
import org.qortal.data.crosschain.CrossChainTradeData;
|
||||
import org.qortal.data.crosschain.TradeBotData;
|
||||
import org.qortal.data.transaction.MessageTransactionData;
|
||||
import org.qortal.data.transaction.TransactionData;
|
||||
import org.qortal.repository.DataException;
|
||||
import org.qortal.repository.Repository;
|
||||
import org.qortal.repository.RepositoryManager;
|
||||
import org.qortal.transaction.Transaction;
|
||||
import org.qortal.utils.Base58;
|
||||
import org.qortal.utils.NTP;
|
||||
|
||||
@ -223,6 +227,17 @@ public class CrossChainTradeBotResource {
|
||||
if (crossChainTradeData.mode != AcctMode.OFFERING)
|
||||
throw ApiExceptionFactory.INSTANCE.createException(request, ApiError.INVALID_CRITERIA);
|
||||
|
||||
// Check if there is a buy or a cancel request in progress for this trade
|
||||
List<Transaction.TransactionType> txTypes = List.of(Transaction.TransactionType.MESSAGE);
|
||||
List<TransactionData> unconfirmed = repository.getTransactionRepository().getUnconfirmedTransactions(txTypes, null, 0, 0, false);
|
||||
for (TransactionData transactionData : unconfirmed) {
|
||||
MessageTransactionData messageTransactionData = (MessageTransactionData) transactionData;
|
||||
if (Objects.equals(messageTransactionData.getRecipient(), atAddress)) {
|
||||
// There is a pending request for this trade, so block this buy attempt to reduce the risk of refunds
|
||||
throw ApiExceptionFactory.INSTANCE.createCustomException(request, ApiError.INVALID_CRITERIA, "Trade has an existing buy request or is pending cancellation.");
|
||||
}
|
||||
}
|
||||
|
||||
AcctTradeBot.ResponseResult result = TradeBot.getInstance().startResponse(repository, atData, acct, crossChainTradeData,
|
||||
tradeBotRespondRequest.foreignKey, tradeBotRespondRequest.receivingAddress);
|
||||
|
||||
|
@ -60,6 +60,9 @@ public class ArbitraryDataReader {
|
||||
private int layerCount;
|
||||
private byte[] latestSignature;
|
||||
|
||||
// The resource being read
|
||||
ArbitraryDataResource arbitraryDataResource = null;
|
||||
|
||||
public ArbitraryDataReader(String resourceId, ResourceIdType resourceIdType, Service service, String identifier) {
|
||||
// Ensure names are always lowercase
|
||||
if (resourceIdType == ResourceIdType.NAME) {
|
||||
@ -116,6 +119,11 @@ public class ArbitraryDataReader {
|
||||
return new ArbitraryDataBuildQueueItem(this.resourceId, this.resourceIdType, this.service, this.identifier);
|
||||
}
|
||||
|
||||
private ArbitraryDataResource createArbitraryDataResource() {
|
||||
return new ArbitraryDataResource(this.resourceId, this.resourceIdType, this.service, this.identifier);
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* loadAsynchronously
|
||||
*
|
||||
@ -163,6 +171,8 @@ public class ArbitraryDataReader {
|
||||
return;
|
||||
}
|
||||
|
||||
this.arbitraryDataResource = this.createArbitraryDataResource();
|
||||
|
||||
this.preExecute();
|
||||
this.deleteExistingFiles();
|
||||
this.fetch();
|
||||
@ -436,7 +446,7 @@ public class ArbitraryDataReader {
|
||||
byte[] secret = this.secret58 != null ? Base58.decode(this.secret58) : null;
|
||||
if (secret != null && secret.length == Transformer.AES256_LENGTH) {
|
||||
try {
|
||||
LOGGER.info("Decrypting using algorithm {}...", algorithm);
|
||||
LOGGER.debug("Decrypting {} using algorithm {}...", this.arbitraryDataResource, algorithm);
|
||||
Path unencryptedPath = Paths.get(this.workingPath.toString(), "zipped.zip");
|
||||
SecretKey aesKey = new SecretKeySpec(secret, 0, secret.length, "AES");
|
||||
AES.decryptFile(algorithm, aesKey, this.filePath.toString(), unencryptedPath.toString());
|
||||
@ -447,7 +457,7 @@ public class ArbitraryDataReader {
|
||||
|
||||
} catch (NoSuchAlgorithmException | InvalidAlgorithmParameterException | NoSuchPaddingException
|
||||
| BadPaddingException | IllegalBlockSizeException | IOException | InvalidKeyException e) {
|
||||
LOGGER.info(String.format("Exception when decrypting using algorithm %s", algorithm), e);
|
||||
LOGGER.info(String.format("Exception when decrypting %s using algorithm %s", this.arbitraryDataResource, algorithm), e);
|
||||
throw new DataException(String.format("Unable to decrypt file at path %s using algorithm %s: %s", this.filePath, algorithm, e.getMessage()));
|
||||
}
|
||||
} else {
|
||||
|
@ -48,7 +48,6 @@ public class ArbitraryDataStorageManager extends Thread {
|
||||
private List<ArbitraryTransactionData> hostedTransactions;
|
||||
|
||||
private String searchQuery;
|
||||
private List<ArbitraryTransactionData> searchResultsTransactions;
|
||||
|
||||
private static final long DIRECTORY_SIZE_CHECK_INTERVAL = 10 * 60 * 1000L; // 10 minutes
|
||||
|
||||
@ -344,11 +343,6 @@ public class ArbitraryDataStorageManager extends Thread {
|
||||
*/
|
||||
|
||||
public List<ArbitraryTransactionData> searchHostedTransactions(Repository repository, String query, Integer limit, Integer offset) {
|
||||
// Load from results cache if we can (results that exists for the same query), to avoid disk reads
|
||||
if (this.searchResultsTransactions != null && this.searchQuery.equals(query.toLowerCase())) {
|
||||
return ArbitraryTransactionUtils.limitOffsetTransactions(this.searchResultsTransactions, limit, offset);
|
||||
}
|
||||
|
||||
// Using cache if we can, to avoid disk reads
|
||||
if (this.hostedTransactions == null) {
|
||||
this.hostedTransactions = this.loadAllHostedTransactions(repository);
|
||||
@ -376,10 +370,7 @@ public class ArbitraryDataStorageManager extends Thread {
|
||||
// Sort by newest first
|
||||
searchResultsList.sort(Comparator.comparingLong(ArbitraryTransactionData::getTimestamp).reversed());
|
||||
|
||||
// Update cache
|
||||
this.searchResultsTransactions = searchResultsList;
|
||||
|
||||
return ArbitraryTransactionUtils.limitOffsetTransactions(this.searchResultsTransactions, limit, offset);
|
||||
return ArbitraryTransactionUtils.limitOffsetTransactions(searchResultsList, limit, offset);
|
||||
}
|
||||
|
||||
/**
|
||||
|
@ -102,6 +102,21 @@ public class NamesDatabaseIntegrityCheck {
|
||||
}
|
||||
}
|
||||
|
||||
// Process CANCEL_SELL_NAME transactions
|
||||
if (currentTransaction.getType() == TransactionType.CANCEL_SELL_NAME) {
|
||||
CancelSellNameTransactionData cancelSellNameTransactionData = (CancelSellNameTransactionData) currentTransaction;
|
||||
Name nameObj = new Name(repository, cancelSellNameTransactionData.getName());
|
||||
if (nameObj != null && nameObj.getNameData() != null) {
|
||||
nameObj.cancelSell(cancelSellNameTransactionData);
|
||||
modificationCount++;
|
||||
LOGGER.trace("Processed CANCEL_SELL_NAME transaction for name {}", name);
|
||||
}
|
||||
else {
|
||||
// Something went wrong
|
||||
throw new DataException(String.format("Name data not found for name %s", cancelSellNameTransactionData.getName()));
|
||||
}
|
||||
}
|
||||
|
||||
// Process BUY_NAME transactions
|
||||
if (currentTransaction.getType() == TransactionType.BUY_NAME) {
|
||||
BuyNameTransactionData buyNameTransactionData = (BuyNameTransactionData) currentTransaction;
|
||||
@ -128,7 +143,7 @@ public class NamesDatabaseIntegrityCheck {
|
||||
public int rebuildAllNames() {
|
||||
int modificationCount = 0;
|
||||
try (final Repository repository = RepositoryManager.getRepository()) {
|
||||
List<String> names = this.fetchAllNames(repository);
|
||||
List<String> names = this.fetchAllNames(repository); // TODO: de-duplicate, to speed up this process
|
||||
for (String name : names) {
|
||||
modificationCount += this.rebuildName(name, repository);
|
||||
}
|
||||
@ -326,6 +341,10 @@ public class NamesDatabaseIntegrityCheck {
|
||||
TransactionType.BUY_NAME, Arrays.asList("name = ?"), Arrays.asList(name));
|
||||
signatures.addAll(buyNameTransactions);
|
||||
|
||||
List<byte[]> cancelSellNameTransactions = repository.getTransactionRepository().getSignaturesMatchingCustomCriteria(
|
||||
TransactionType.CANCEL_SELL_NAME, Arrays.asList("name = ?"), Arrays.asList(name));
|
||||
signatures.addAll(cancelSellNameTransactions);
|
||||
|
||||
List<TransactionData> transactions = new ArrayList<>();
|
||||
for (byte[] signature : signatures) {
|
||||
TransactionData transactionData = repository.getTransactionRepository().fromSignature(signature);
|
||||
@ -390,6 +409,12 @@ public class NamesDatabaseIntegrityCheck {
|
||||
names.add(sellNameTransactionData.getName());
|
||||
}
|
||||
}
|
||||
if ((transactionData instanceof CancelSellNameTransactionData)) {
|
||||
CancelSellNameTransactionData cancelSellNameTransactionData = (CancelSellNameTransactionData) transactionData;
|
||||
if (!names.contains(cancelSellNameTransactionData.getName())) {
|
||||
names.add(cancelSellNameTransactionData.getName());
|
||||
}
|
||||
}
|
||||
}
|
||||
return names;
|
||||
}
|
||||
|
@ -47,7 +47,8 @@ public class Dogecoin extends Bitcoiny {
|
||||
// Servers chosen on NO BASIS WHATSOEVER from various sources!
|
||||
new Server("electrum1.cipig.net", ConnectionType.SSL, 20060),
|
||||
new Server("electrum2.cipig.net", ConnectionType.SSL, 20060),
|
||||
new Server("electrum3.cipig.net", ConnectionType.SSL, 20060));
|
||||
new Server("electrum3.cipig.net", ConnectionType.SSL, 20060),
|
||||
new Server("161.97.137.235", ConnectionType.SSL, 50002));
|
||||
// TODO: add more mainnet servers. It's too centralized.
|
||||
}
|
||||
|
||||
|
@ -5,6 +5,7 @@ import java.math.BigDecimal;
|
||||
import java.net.InetSocketAddress;
|
||||
import java.net.Socket;
|
||||
import java.net.SocketAddress;
|
||||
import java.text.DecimalFormat;
|
||||
import java.util.*;
|
||||
import java.util.regex.Matcher;
|
||||
import java.util.regex.Pattern;
|
||||
@ -30,7 +31,11 @@ public class ElectrumX extends BitcoinyBlockchainProvider {
|
||||
private static final Logger LOGGER = LogManager.getLogger(ElectrumX.class);
|
||||
private static final Random RANDOM = new Random();
|
||||
|
||||
// See: https://electrumx.readthedocs.io/en/latest/protocol-changes.html
|
||||
private static final double MIN_PROTOCOL_VERSION = 1.2;
|
||||
private static final double MAX_PROTOCOL_VERSION = 2.0; // Higher than current latest, for hopeful future-proofing
|
||||
private static final String CLIENT_NAME = "Qortal";
|
||||
|
||||
private static final int BLOCK_HEADER_LENGTH = 80;
|
||||
|
||||
// "message": "daemon error: DaemonError({'code': -5, 'message': 'No such mempool or blockchain transaction. Use gettransaction for wallet transactions.'})"
|
||||
@ -679,6 +684,9 @@ public class ElectrumX extends BitcoinyBlockchainProvider {
|
||||
this.scanner = new Scanner(this.socket.getInputStream());
|
||||
this.scanner.useDelimiter("\n");
|
||||
|
||||
// All connections need to start with a version negotiation
|
||||
this.connectedRpc("server.version");
|
||||
|
||||
// Check connection is suitable by asking for server features, including genesis block hash
|
||||
JSONObject featuresJson = (JSONObject) this.connectedRpc("server.features");
|
||||
|
||||
@ -725,6 +733,17 @@ public class ElectrumX extends BitcoinyBlockchainProvider {
|
||||
|
||||
JSONArray requestParams = new JSONArray();
|
||||
requestParams.addAll(Arrays.asList(params));
|
||||
|
||||
// server.version needs additional params to negotiate a version
|
||||
if (method.equals("server.version")) {
|
||||
requestParams.add(CLIENT_NAME);
|
||||
List<String> versions = new ArrayList<>();
|
||||
DecimalFormat df = new DecimalFormat("#.#");
|
||||
versions.add(df.format(MIN_PROTOCOL_VERSION));
|
||||
versions.add(df.format(MAX_PROTOCOL_VERSION));
|
||||
requestParams.add(versions);
|
||||
}
|
||||
|
||||
requestJson.put("params", requestParams);
|
||||
|
||||
String request = requestJson.toJSONString() + "\n";
|
||||
|
@ -54,7 +54,8 @@ public class Litecoin extends Bitcoiny {
|
||||
new Server("electrum2.cipig.net", Server.ConnectionType.SSL, 20063),
|
||||
new Server("electrum3.cipig.net", Server.ConnectionType.SSL, 20063),
|
||||
new Server("ltc.litepay.ch", Server.ConnectionType.SSL, 50022),
|
||||
new Server("ltc.rentonrisk.com", Server.ConnectionType.SSL, 50002));
|
||||
new Server("ltc.rentonrisk.com", Server.ConnectionType.SSL, 50002),
|
||||
new Server("62.171.169.176", Server.ConnectionType.SSL, 50002));
|
||||
}
|
||||
|
||||
@Override
|
||||
|
@ -3,6 +3,7 @@ package org.qortal.data.transaction;
|
||||
import javax.xml.bind.Unmarshaller;
|
||||
import javax.xml.bind.annotation.XmlAccessType;
|
||||
import javax.xml.bind.annotation.XmlAccessorType;
|
||||
import javax.xml.bind.annotation.XmlTransient;
|
||||
|
||||
import org.qortal.transaction.Transaction.TransactionType;
|
||||
|
||||
@ -19,6 +20,11 @@ public class CancelSellNameTransactionData extends TransactionData {
|
||||
@Schema(description = "which name to cancel selling", example = "my-name")
|
||||
private String name;
|
||||
|
||||
// For internal use when orphaning
|
||||
@XmlTransient
|
||||
@Schema(hidden = true)
|
||||
private Long salePrice;
|
||||
|
||||
// Constructors
|
||||
|
||||
// For JAXB
|
||||
@ -30,11 +36,17 @@ public class CancelSellNameTransactionData extends TransactionData {
|
||||
this.creatorPublicKey = this.ownerPublicKey;
|
||||
}
|
||||
|
||||
public CancelSellNameTransactionData(BaseTransactionData baseTransactionData, String name) {
|
||||
public CancelSellNameTransactionData(BaseTransactionData baseTransactionData, String name, Long salePrice) {
|
||||
super(TransactionType.CANCEL_SELL_NAME, baseTransactionData);
|
||||
|
||||
this.ownerPublicKey = baseTransactionData.creatorPublicKey;
|
||||
this.name = name;
|
||||
this.salePrice = salePrice;
|
||||
}
|
||||
|
||||
/** From network/API */
|
||||
public CancelSellNameTransactionData(BaseTransactionData baseTransactionData, String name) {
|
||||
this(baseTransactionData, name, null);
|
||||
}
|
||||
|
||||
// Getters / setters
|
||||
@ -47,4 +59,12 @@ public class CancelSellNameTransactionData extends TransactionData {
|
||||
return this.name;
|
||||
}
|
||||
|
||||
public Long getSalePrice() {
|
||||
return this.salePrice;
|
||||
}
|
||||
|
||||
public void setSalePrice(Long salePrice) {
|
||||
this.salePrice = salePrice;
|
||||
}
|
||||
|
||||
}
|
||||
|
@ -180,8 +180,12 @@ public class Name {
|
||||
}
|
||||
|
||||
public void cancelSell(CancelSellNameTransactionData cancelSellNameTransactionData) throws DataException {
|
||||
// Mark not for-sale but leave price in case we want to orphan
|
||||
// Update previous sale price in transaction data
|
||||
cancelSellNameTransactionData.setSalePrice(this.nameData.getSalePrice());
|
||||
|
||||
// Mark not for-sale
|
||||
this.nameData.setIsForSale(false);
|
||||
this.nameData.setSalePrice(null);
|
||||
|
||||
// Save sale info into repository
|
||||
this.repository.getNameRepository().save(this.nameData);
|
||||
@ -190,6 +194,7 @@ public class Name {
|
||||
public void uncancelSell(CancelSellNameTransactionData cancelSellNameTransactionData) throws DataException {
|
||||
// Mark as for-sale using existing price
|
||||
this.nameData.setIsForSale(true);
|
||||
this.nameData.setSalePrice(cancelSellNameTransactionData.getSalePrice());
|
||||
|
||||
// Save no-sale info into repository
|
||||
this.repository.getNameRepository().save(this.nameData);
|
||||
|
@ -339,7 +339,7 @@ public class Network {
|
||||
try {
|
||||
if (!isConnected) {
|
||||
// Add this signature to the list of pending requests for this peer
|
||||
LOGGER.info("Making connection to peer {} to request files for signature {}...", peerAddressString, Base58.encode(signature));
|
||||
LOGGER.debug("Making connection to peer {} to request files for signature {}...", peerAddressString, Base58.encode(signature));
|
||||
Peer peer = new Peer(peerData);
|
||||
peer.setIsDataPeer(true);
|
||||
peer.addPendingSignatureRequest(signature);
|
||||
|
@ -988,6 +988,11 @@ public class HSQLDBDatabaseUpdates {
|
||||
stmt.execute("CREATE INDEX ChatTransactionsChatReferenceIndex ON ChatTransactions (chat_reference)");
|
||||
break;
|
||||
|
||||
case 46:
|
||||
// We need to track the sale price when canceling a name sale, so it can be put back when orphaned
|
||||
stmt.execute("ALTER TABLE CancelSellNameTransactions ADD sale_price QortalAmount");
|
||||
break;
|
||||
|
||||
default:
|
||||
// nothing to do
|
||||
return false;
|
||||
|
@ -28,7 +28,6 @@ public class HSQLDBMessageRepository implements MessageRepository {
|
||||
StringBuilder sql = new StringBuilder(1024);
|
||||
sql.append("SELECT signature from MessageTransactions "
|
||||
+ "JOIN Transactions USING (signature) "
|
||||
+ "JOIN BlockTransactions ON transaction_signature = signature "
|
||||
+ "WHERE ");
|
||||
|
||||
List<String> whereClauses = new ArrayList<>();
|
||||
|
@ -17,15 +17,16 @@ public class HSQLDBCancelSellNameTransactionRepository extends HSQLDBTransaction
|
||||
}
|
||||
|
||||
TransactionData fromBase(BaseTransactionData baseTransactionData) throws DataException {
|
||||
String sql = "SELECT name FROM CancelSellNameTransactions WHERE signature = ?";
|
||||
String sql = "SELECT name, sale_price FROM CancelSellNameTransactions WHERE signature = ?";
|
||||
|
||||
try (ResultSet resultSet = this.repository.checkedExecute(sql, baseTransactionData.getSignature())) {
|
||||
if (resultSet == null)
|
||||
return null;
|
||||
|
||||
String name = resultSet.getString(1);
|
||||
Long salePrice = resultSet.getLong(2);
|
||||
|
||||
return new CancelSellNameTransactionData(baseTransactionData, name);
|
||||
return new CancelSellNameTransactionData(baseTransactionData, name, salePrice);
|
||||
} catch (SQLException e) {
|
||||
throw new DataException("Unable to fetch cancel sell name transaction from repository", e);
|
||||
}
|
||||
@ -38,7 +39,7 @@ public class HSQLDBCancelSellNameTransactionRepository extends HSQLDBTransaction
|
||||
HSQLDBSaver saveHelper = new HSQLDBSaver("CancelSellNameTransactions");
|
||||
|
||||
saveHelper.bind("signature", cancelSellNameTransactionData.getSignature()).bind("owner", cancelSellNameTransactionData.getOwnerPublicKey()).bind("name",
|
||||
cancelSellNameTransactionData.getName());
|
||||
cancelSellNameTransactionData.getName()).bind("sale_price", cancelSellNameTransactionData.getSalePrice());
|
||||
|
||||
try {
|
||||
saveHelper.execute(this.repository);
|
||||
|
@ -165,6 +165,52 @@ public class BuySellTests extends Common {
|
||||
assertEquals("price incorrect", price, nameData.getSalePrice());
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testCancelSellNameAndRelist() throws DataException {
|
||||
// Register-name and sell-name
|
||||
testSellName();
|
||||
|
||||
// Cancel Sell-name
|
||||
CancelSellNameTransactionData transactionData = new CancelSellNameTransactionData(TestTransaction.generateBase(alice), name);
|
||||
TransactionUtils.signAndMint(repository, transactionData, alice);
|
||||
|
||||
NameData nameData;
|
||||
|
||||
// Check name is no longer for sale
|
||||
nameData = repository.getNameRepository().fromName(name);
|
||||
assertFalse(nameData.isForSale());
|
||||
assertNull(nameData.getSalePrice());
|
||||
|
||||
// Re-sell-name
|
||||
Long newPrice = random.nextInt(1000) * Amounts.MULTIPLIER;
|
||||
SellNameTransactionData sellNameTransactionData = new SellNameTransactionData(TestTransaction.generateBase(alice), name, newPrice);
|
||||
TransactionUtils.signAndMint(repository, sellNameTransactionData, alice);
|
||||
|
||||
// Check name is for sale
|
||||
nameData = repository.getNameRepository().fromName(name);
|
||||
assertTrue(nameData.isForSale());
|
||||
assertEquals("price incorrect", newPrice, nameData.getSalePrice());
|
||||
|
||||
// Orphan sell-name
|
||||
BlockUtils.orphanLastBlock(repository);
|
||||
|
||||
// Check name no longer for sale
|
||||
nameData = repository.getNameRepository().fromName(name);
|
||||
assertFalse(nameData.isForSale());
|
||||
assertNull(nameData.getSalePrice());
|
||||
|
||||
// Orphan cancel-sell-name
|
||||
BlockUtils.orphanLastBlock(repository);
|
||||
|
||||
// Check name is for sale (at original price)
|
||||
nameData = repository.getNameRepository().fromName(name);
|
||||
assertTrue(nameData.isForSale());
|
||||
assertEquals("price incorrect", price, nameData.getSalePrice());
|
||||
|
||||
// Orphan sell-name and register-name
|
||||
BlockUtils.orphanBlocks(repository, 2);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testBuyName() throws DataException {
|
||||
// Register-name and sell-name
|
||||
|
Loading…
x
Reference in New Issue
Block a user