From 6b36d94c6f20637155c855060c579f0c7873b8fc Mon Sep 17 00:00:00 2001 From: CalDescent Date: Fri, 27 Jan 2023 12:48:42 +0000 Subject: [PATCH 01/11] Removed searchResultsTransactions cache, to simplify code. The hostedTransactions cache is still in place, which limits disk reads when searching, so this additional cache isn't really needed. --- .../arbitrary/ArbitraryDataStorageManager.java | 11 +---------- 1 file changed, 1 insertion(+), 10 deletions(-) diff --git a/src/main/java/org/qortal/controller/arbitrary/ArbitraryDataStorageManager.java b/src/main/java/org/qortal/controller/arbitrary/ArbitraryDataStorageManager.java index 4568d3fd..ededbfa6 100644 --- a/src/main/java/org/qortal/controller/arbitrary/ArbitraryDataStorageManager.java +++ b/src/main/java/org/qortal/controller/arbitrary/ArbitraryDataStorageManager.java @@ -48,7 +48,6 @@ public class ArbitraryDataStorageManager extends Thread { private List hostedTransactions; private String searchQuery; - private List 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 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); } /** From 8c708558cb4371ca1cff063ba944ab73a1425bbc Mon Sep 17 00:00:00 2001 From: CalDescent Date: Fri, 27 Jan 2023 14:33:34 +0000 Subject: [PATCH 02/11] Implemented ElectrumX version negotiation. Fixes issues with DOGE wallet. --- .../java/org/qortal/crosschain/ElectrumX.java | 19 +++++++++++++++++++ 1 file changed, 19 insertions(+) diff --git a/src/main/java/org/qortal/crosschain/ElectrumX.java b/src/main/java/org/qortal/crosschain/ElectrumX.java index e1eb1963..a331b111 100644 --- a/src/main/java/org/qortal/crosschain/ElectrumX.java +++ b/src/main/java/org/qortal/crosschain/ElectrumX.java @@ -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 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"; From bf06d47842337c62636eabe15dbeb8fbd8b4a933 Mon Sep 17 00:00:00 2001 From: CalDescent Date: Fri, 27 Jan 2023 16:55:43 +0000 Subject: [PATCH 03/11] Create an ArbitraryDataResource object when building. Eventually this could be passed in to the reader instead of the individual components (service, name, identifier, etc) This is now used to improve logging when extracting. --- .../org/qortal/arbitrary/ArbitraryDataReader.java | 12 +++++++++++- 1 file changed, 11 insertions(+), 1 deletion(-) diff --git a/src/main/java/org/qortal/arbitrary/ArbitraryDataReader.java b/src/main/java/org/qortal/arbitrary/ArbitraryDataReader.java index d1a8b4f5..91e9eeb7 100644 --- a/src/main/java/org/qortal/arbitrary/ArbitraryDataReader.java +++ b/src/main/java/org/qortal/arbitrary/ArbitraryDataReader.java @@ -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.info("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()); From 5962ebd08a9e7042a092a9455a66c3708025f0e8 Mon Sep 17 00:00:00 2001 From: CalDescent Date: Fri, 27 Jan 2023 16:56:53 +0000 Subject: [PATCH 04/11] More logging improvements in ArbitraryDataReader.decrypt() --- src/main/java/org/qortal/arbitrary/ArbitraryDataReader.java | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/main/java/org/qortal/arbitrary/ArbitraryDataReader.java b/src/main/java/org/qortal/arbitrary/ArbitraryDataReader.java index 91e9eeb7..78723958 100644 --- a/src/main/java/org/qortal/arbitrary/ArbitraryDataReader.java +++ b/src/main/java/org/qortal/arbitrary/ArbitraryDataReader.java @@ -446,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 {}...", this.arbitraryDataResource, 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()); @@ -457,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 { From 6ad0989ea27f8c60436aada0223549c385cb9100 Mon Sep 17 00:00:00 2001 From: CalDescent Date: Fri, 27 Jan 2023 18:35:44 +0000 Subject: [PATCH 05/11] Reduce log spam --- src/main/java/org/qortal/network/Network.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/main/java/org/qortal/network/Network.java b/src/main/java/org/qortal/network/Network.java index 8aac68f0..f8f73c2a 100644 --- a/src/main/java/org/qortal/network/Network.java +++ b/src/main/java/org/qortal/network/Network.java @@ -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); From ae44065d7e08cf1d6d77ae1ba4caf827b8bc6f29 Mon Sep 17 00:00:00 2001 From: CalDescent Date: Fri, 27 Jan 2023 19:34:23 +0000 Subject: [PATCH 06/11] Fixed issue with CancelSellName transactions. --- .../CancelSellNameTransactionData.java | 22 ++++++++- src/main/java/org/qortal/naming/Name.java | 7 ++- .../hsqldb/HSQLDBDatabaseUpdates.java | 5 ++ ...DBCancelSellNameTransactionRepository.java | 7 +-- .../org/qortal/test/naming/BuySellTests.java | 46 +++++++++++++++++++ 5 files changed, 82 insertions(+), 5 deletions(-) diff --git a/src/main/java/org/qortal/data/transaction/CancelSellNameTransactionData.java b/src/main/java/org/qortal/data/transaction/CancelSellNameTransactionData.java index ff3d0a08..14677daf 100644 --- a/src/main/java/org/qortal/data/transaction/CancelSellNameTransactionData.java +++ b/src/main/java/org/qortal/data/transaction/CancelSellNameTransactionData.java @@ -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; + } + } diff --git a/src/main/java/org/qortal/naming/Name.java b/src/main/java/org/qortal/naming/Name.java index 97fe8bbb..ecf826a5 100644 --- a/src/main/java/org/qortal/naming/Name.java +++ b/src/main/java/org/qortal/naming/Name.java @@ -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); diff --git a/src/main/java/org/qortal/repository/hsqldb/HSQLDBDatabaseUpdates.java b/src/main/java/org/qortal/repository/hsqldb/HSQLDBDatabaseUpdates.java index e72e5fab..aecac034 100644 --- a/src/main/java/org/qortal/repository/hsqldb/HSQLDBDatabaseUpdates.java +++ b/src/main/java/org/qortal/repository/hsqldb/HSQLDBDatabaseUpdates.java @@ -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; diff --git a/src/main/java/org/qortal/repository/hsqldb/transaction/HSQLDBCancelSellNameTransactionRepository.java b/src/main/java/org/qortal/repository/hsqldb/transaction/HSQLDBCancelSellNameTransactionRepository.java index 5f2ea35a..fc8e0bb3 100644 --- a/src/main/java/org/qortal/repository/hsqldb/transaction/HSQLDBCancelSellNameTransactionRepository.java +++ b/src/main/java/org/qortal/repository/hsqldb/transaction/HSQLDBCancelSellNameTransactionRepository.java @@ -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); diff --git a/src/test/java/org/qortal/test/naming/BuySellTests.java b/src/test/java/org/qortal/test/naming/BuySellTests.java index faed3d72..4530820e 100644 --- a/src/test/java/org/qortal/test/naming/BuySellTests.java +++ b/src/test/java/org/qortal/test/naming/BuySellTests.java @@ -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 From 06d8a21714990231d778c09f70454285f859409e Mon Sep 17 00:00:00 2001 From: CalDescent Date: Fri, 27 Jan 2023 19:38:26 +0000 Subject: [PATCH 07/11] Added CANCEL_SELL_NAME equivalents to NamesDatabaseIntegrityCheck.java --- .../NamesDatabaseIntegrityCheck.java | 27 ++++++++++++++++++- 1 file changed, 26 insertions(+), 1 deletion(-) diff --git a/src/main/java/org/qortal/controller/repository/NamesDatabaseIntegrityCheck.java b/src/main/java/org/qortal/controller/repository/NamesDatabaseIntegrityCheck.java index e69d1a35..004fa692 100644 --- a/src/main/java/org/qortal/controller/repository/NamesDatabaseIntegrityCheck.java +++ b/src/main/java/org/qortal/controller/repository/NamesDatabaseIntegrityCheck.java @@ -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 names = this.fetchAllNames(repository); + List 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 cancelSellNameTransactions = repository.getTransactionRepository().getSignaturesMatchingCustomCriteria( + TransactionType.CANCEL_SELL_NAME, Arrays.asList("name = ?"), Arrays.asList(name)); + signatures.addAll(cancelSellNameTransactions); + List 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; } From a24ba40d5c34b81618f0f05151fbdc8ee240ee4f Mon Sep 17 00:00:00 2001 From: CalDescent Date: Sat, 28 Jan 2023 09:54:15 +0000 Subject: [PATCH 08/11] Added additional Dogecoin ElectrumX server. --- src/main/java/org/qortal/crosschain/Dogecoin.java | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/src/main/java/org/qortal/crosschain/Dogecoin.java b/src/main/java/org/qortal/crosschain/Dogecoin.java index d6955e18..9af8d990 100644 --- a/src/main/java/org/qortal/crosschain/Dogecoin.java +++ b/src/main/java/org/qortal/crosschain/Dogecoin.java @@ -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. } From 876658256f735853040e930b057ee1db025d54a2 Mon Sep 17 00:00:00 2001 From: CalDescent Date: Sat, 28 Jan 2023 11:57:15 +0000 Subject: [PATCH 09/11] Prevent a P2SH address being funded for a trade if there is an unconfirmed buy or cancel request in progress for it already. This prevents foreign coins from leaving the local wallet when there is a high probability that the trade will fail, and therefore should reduce the chances of losing transaction fees due to refunds. Whenever this occurs, the UI will show "Trade has an existing buy request or is pending cancellation." after clicking Buy. --- .../api/resource/CrossChainTradeBotResource.java | 15 +++++++++++++++ 1 file changed, 15 insertions(+) diff --git a/src/main/java/org/qortal/api/resource/CrossChainTradeBotResource.java b/src/main/java/org/qortal/api/resource/CrossChainTradeBotResource.java index 3c8bd28f..aefca016 100644 --- a/src/main/java/org/qortal/api/resource/CrossChainTradeBotResource.java +++ b/src/main/java/org/qortal/api/resource/CrossChainTradeBotResource.java @@ -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 txTypes = List.of(Transaction.TransactionType.MESSAGE); + List 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); From e86b9b1caf99a01e4565ba4aa171e471b765b9ca Mon Sep 17 00:00:00 2001 From: CalDescent Date: Sat, 28 Jan 2023 15:34:30 +0000 Subject: [PATCH 10/11] Added additional Litecoin ElectrumX server. --- src/main/java/org/qortal/crosschain/Litecoin.java | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/src/main/java/org/qortal/crosschain/Litecoin.java b/src/main/java/org/qortal/crosschain/Litecoin.java index 3ab30b2b..6fc6ba50 100644 --- a/src/main/java/org/qortal/crosschain/Litecoin.java +++ b/src/main/java/org/qortal/crosschain/Litecoin.java @@ -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 From c5c826453be2ec64f949d76aca128650ceec71d5 Mon Sep 17 00:00:00 2001 From: CalDescent Date: Sat, 28 Jan 2023 15:41:48 +0000 Subject: [PATCH 11/11] Removed unnecessary join when finding MESSAGE transactions, which caused secret to be unavailable when querying pruned blocks. --- .../org/qortal/repository/hsqldb/HSQLDBMessageRepository.java | 1 - 1 file changed, 1 deletion(-) diff --git a/src/main/java/org/qortal/repository/hsqldb/HSQLDBMessageRepository.java b/src/main/java/org/qortal/repository/hsqldb/HSQLDBMessageRepository.java index f00c79fc..f31c5cd8 100644 --- a/src/main/java/org/qortal/repository/hsqldb/HSQLDBMessageRepository.java +++ b/src/main/java/org/qortal/repository/hsqldb/HSQLDBMessageRepository.java @@ -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 whereClauses = new ArrayList<>();