diff --git a/src/main/java/org/qortal/data/naming/NameData.java b/src/main/java/org/qortal/data/naming/NameData.java index 16e490a2d..36285c568 100644 --- a/src/main/java/org/qortal/data/naming/NameData.java +++ b/src/main/java/org/qortal/data/naming/NameData.java @@ -30,6 +30,10 @@ public class NameData { @XmlJavaTypeAdapter(value = org.qortal.api.AmountTypeAdapter.class) private Long salePrice; + private boolean isPrivateSale; + + private String saleRecipient; // Not always present + // For internal use - no need to expose this via API @XmlTransient @Schema(hidden = true) @@ -49,6 +53,7 @@ protected NameData() { // Typically used when fetching from repository public NameData(String name, String reducedName, String owner, String data, long registered, Long updated, boolean isForSale, Long salePrice, + boolean isPrivateSale, String saleRecipient, byte[] reference, int creationGroupId) { this.name = name; this.reducedName = reducedName; @@ -59,12 +64,14 @@ public NameData(String name, String reducedName, String owner, String data, long this.reference = reference; this.isForSale = isForSale; this.salePrice = salePrice; + this.isPrivateSale = isPrivateSale; + this.saleRecipient = saleRecipient; this.creationGroupId = creationGroupId; } // Typically used when registering a new name public NameData(String name, String reducedName, String owner, String data, long registered, byte[] reference, int creationGroupId) { - this(name, reducedName, owner, data, registered, null, false, null, reference, creationGroupId); + this(name, reducedName, owner, data, registered, null, false, null, false, null, reference, creationGroupId); } // Getters / setters @@ -129,6 +136,22 @@ public void setSalePrice(Long salePrice) { this.salePrice = salePrice; } + public boolean getIsPrivateSale() { + return this.isPrivateSale; + } + + public void setIsPrivateSale(boolean isPrivateSale) { + this.isPrivateSale = isPrivateSale; + } + + public String getSaleRecipient() { + return this.saleRecipient; + } + + public void setSaleRecipient(String saleRecipient) { + this.saleRecipient = saleRecipient; + } + public byte[] getReference() { return this.reference; } diff --git a/src/main/java/org/qortal/data/transaction/BuyNameTransactionData.java b/src/main/java/org/qortal/data/transaction/BuyNameTransactionData.java index fffdc293b..4d0719f04 100644 --- a/src/main/java/org/qortal/data/transaction/BuyNameTransactionData.java +++ b/src/main/java/org/qortal/data/transaction/BuyNameTransactionData.java @@ -32,6 +32,9 @@ public class BuyNameTransactionData extends TransactionData { // For internal use when orphaning @XmlTransient @Schema(hidden = true) + private boolean isPrivateSale; + @XmlTransient + @Schema(hidden = true) private byte[] nameReference; // Constructors @@ -47,19 +50,23 @@ public void afterUnmarshal(Unmarshaller u, Object parent) { /** From repository */ public BuyNameTransactionData(BaseTransactionData baseTransactionData, - String name, long amount, String seller, byte[] nameReference) { + String name, long amount, String seller, boolean isPrivateSale, byte[] nameReference) { super(TransactionType.BUY_NAME, baseTransactionData); this.buyerPublicKey = baseTransactionData.creatorPublicKey; this.name = name; this.amount = amount; this.seller = seller; + this.isPrivateSale = isPrivateSale; this.nameReference = nameReference; } /** From network/API */ + public BuyNameTransactionData(BaseTransactionData baseTransactionData, String name, long amount, String seller, boolean isPrivateSale) { + this(baseTransactionData, name, amount, seller, isPrivateSale, null); + } public BuyNameTransactionData(BaseTransactionData baseTransactionData, String name, long amount, String seller) { - this(baseTransactionData, name, amount, seller, null); + this(baseTransactionData, name, amount, seller, false, null); } // Getters / setters @@ -80,6 +87,14 @@ public String getSeller() { return this.seller; } + public boolean getIsPrivateSale() { + return this.isPrivateSale; + } + + public void setIsPrivateSale(boolean isPrivateSale) { + this.isPrivateSale = isPrivateSale; + } + public byte[] getNameReference() { return this.nameReference; } diff --git a/src/main/java/org/qortal/data/transaction/CancelSellNameTransactionData.java b/src/main/java/org/qortal/data/transaction/CancelSellNameTransactionData.java index 602db975e..92b450cce 100644 --- a/src/main/java/org/qortal/data/transaction/CancelSellNameTransactionData.java +++ b/src/main/java/org/qortal/data/transaction/CancelSellNameTransactionData.java @@ -23,6 +23,12 @@ public class CancelSellNameTransactionData extends TransactionData { @XmlTransient @Schema(hidden = true) private Long salePrice; + @XmlTransient + @Schema(hidden = true) + private boolean isPrivateSale; + @XmlTransient + @Schema(hidden = true) + private String saleRecipient; // Constructors @@ -35,17 +41,19 @@ public void afterUnmarshal(Unmarshaller u, Object parent) { this.creatorPublicKey = this.ownerPublicKey; } - public CancelSellNameTransactionData(BaseTransactionData baseTransactionData, String name, Long salePrice) { + public CancelSellNameTransactionData(BaseTransactionData baseTransactionData, String name, Long salePrice, boolean isPrivateSale, String saleRecipient) { super(TransactionType.CANCEL_SELL_NAME, baseTransactionData); this.ownerPublicKey = baseTransactionData.creatorPublicKey; this.name = name; this.salePrice = salePrice; + this.isPrivateSale = isPrivateSale; + this.saleRecipient = saleRecipient; } /** From network/API */ public CancelSellNameTransactionData(BaseTransactionData baseTransactionData, String name) { - this(baseTransactionData, name, null); + this(baseTransactionData, name, null, false, null); } // Getters / setters @@ -66,4 +74,20 @@ public void setSalePrice(Long salePrice) { this.salePrice = salePrice; } + public boolean getIsPrivateSale() { + return this.isPrivateSale; + } + + public void setIsPrivateSale(boolean isPrivateSale) { + this.isPrivateSale = isPrivateSale; + } + + public String getSaleRecipient() { + return this.saleRecipient; + } + + public void setSaleRecipient(String saleRecipient) { + this.saleRecipient = saleRecipient; + } + } diff --git a/src/main/java/org/qortal/data/transaction/SellNameTransactionData.java b/src/main/java/org/qortal/data/transaction/SellNameTransactionData.java index d85014281..aa5c3983a 100644 --- a/src/main/java/org/qortal/data/transaction/SellNameTransactionData.java +++ b/src/main/java/org/qortal/data/transaction/SellNameTransactionData.java @@ -25,6 +25,12 @@ public class SellNameTransactionData extends TransactionData { @XmlJavaTypeAdapter(value = org.qortal.api.AmountTypeAdapter.class) private long amount; + @Schema(description = "if sale is for a specific buyer", example = "true") + private boolean isPrivateSale; + + @Schema(description = "intended buyer's address", example = "QgV4s3xnzLhVBEJxcYui4u4q11yhUHsd9v") + private String saleRecipient; + // Constructors // For JAXB @@ -42,6 +48,18 @@ public SellNameTransactionData(BaseTransactionData baseTransactionData, String n this.ownerPublicKey = baseTransactionData.creatorPublicKey; this.name = name; this.amount = amount; + this.isPrivateSale = false; + this.saleRecipient = null; + } + + public SellNameTransactionData(BaseTransactionData baseTransactionData, String name, long amount, boolean isPrivateSale, String saleRecipient) { + super(TransactionType.SELL_NAME, baseTransactionData); + + this.ownerPublicKey = baseTransactionData.creatorPublicKey; + this.name = name; + this.amount = amount; + this.isPrivateSale = isPrivateSale; + this.saleRecipient = saleRecipient; } // Getters / setters @@ -58,4 +76,12 @@ public long getAmount() { return this.amount; } + public boolean getIsPrivateSale() { + return this.isPrivateSale; + } + + public String getSaleRecipient() { + return this.isPrivateSale ? this.saleRecipient : null; + } + } diff --git a/src/main/java/org/qortal/naming/Name.java b/src/main/java/org/qortal/naming/Name.java index 704200dcc..d8118d629 100644 --- a/src/main/java/org/qortal/naming/Name.java +++ b/src/main/java/org/qortal/naming/Name.java @@ -162,6 +162,9 @@ public void sell(SellNameTransactionData sellNameTransactionData) throws DataExc // Mark as for-sale and set price this.nameData.setIsForSale(true); this.nameData.setSalePrice(sellNameTransactionData.getAmount()); + this.nameData.setIsPrivateSale(sellNameTransactionData.getIsPrivateSale()); + if (sellNameTransactionData.getIsPrivateSale()) + this.nameData.setSaleRecipient(sellNameTransactionData.getSaleRecipient()); // Save sale info into repository this.repository.getNameRepository().save(this.nameData); @@ -171,6 +174,8 @@ public void unsell(SellNameTransactionData sellNameTransactionData) throws DataE // Mark not for-sale and unset price this.nameData.setIsForSale(false); this.nameData.setSalePrice(null); + this.nameData.setIsPrivateSale(false); + this.nameData.setSaleRecipient(null); // Save no-sale info into repository this.repository.getNameRepository().save(this.nameData); @@ -183,6 +188,8 @@ public void cancelSell(CancelSellNameTransactionData cancelSellNameTransactionDa // Mark not for-sale this.nameData.setIsForSale(false); this.nameData.setSalePrice(null); + this.nameData.setIsPrivateSale(false); + this.nameData.setSaleRecipient(null); // Save sale info into repository this.repository.getNameRepository().save(this.nameData); @@ -192,6 +199,9 @@ public void uncancelSell(CancelSellNameTransactionData cancelSellNameTransaction // Mark as for-sale using existing price this.nameData.setIsForSale(true); this.nameData.setSalePrice(cancelSellNameTransactionData.getSalePrice()); + this.nameData.setIsPrivateSale(cancelSellNameTransactionData.getIsPrivateSale()); + if (cancelSellNameTransactionData.getIsPrivateSale()) + this.nameData.setSaleRecipient(cancelSellNameTransactionData.getSaleRecipient()); // Save no-sale info into repository this.repository.getNameRepository().save(this.nameData); @@ -234,6 +244,9 @@ public void unbuy(BuyNameTransactionData buyNameTransactionData) throws DataExce // Mark as for-sale using existing price this.nameData.setIsForSale(true); this.nameData.setSalePrice(buyNameTransactionData.getAmount()); + this.nameData.setIsPrivateSale(buyNameTransactionData.getIsPrivateSale()); + if (buyNameTransactionData.getIsPrivateSale()) + this.nameData.setSaleRecipient(Crypto.toAddress(buyNameTransactionData.getCreatorPublicKey())); // Previous name-changing reference is taken from this transaction's cached copy this.nameData.setReference(buyNameTransactionData.getNameReference()); diff --git a/src/main/java/org/qortal/network/message/NamesMessage.java b/src/main/java/org/qortal/network/message/NamesMessage.java index 942818cc0..4ebb92b83 100644 --- a/src/main/java/org/qortal/network/message/NamesMessage.java +++ b/src/main/java/org/qortal/network/message/NamesMessage.java @@ -57,6 +57,13 @@ public NamesMessage(List nameDataList) { bytes.write(Longs.toByteArray(nameData.getSalePrice())); } + int isPrivateSale = nameData.getIsPrivateSale() ? 1 : 0; + bytes.write(Ints.toByteArray(isPrivateSale)); + + if (nameData.getIsPrivateSale()) { + Serialization.serializeAddress(bytes, nameData.getSaleRecipient()); + } + bytes.write(nameData.getReference()); bytes.write(Ints.toByteArray(nameData.getCreationGroupId())); @@ -112,13 +119,20 @@ public static Message fromByteBuffer(int id, ByteBuffer bytes) throws MessageExc salePrice = bytes.getLong(); } + boolean isPrivateSale = (bytes.getInt() == 1); + + String saleRecipient = null; + if (isPrivateSale) { + saleRecipient = Serialization.deserializeAddress(bytes); + } + byte[] reference = new byte[SIGNATURE_LENGTH]; bytes.get(reference); int creationGroupId = bytes.getInt(); NameData nameData = new NameData(name, reducedName, owner, data, registered, updated, - isForSale, salePrice, reference, creationGroupId); + isForSale, salePrice, isPrivateSale, saleRecipient, reference, creationGroupId); nameDataList.add(nameData); } diff --git a/src/main/java/org/qortal/repository/hsqldb/HSQLDBDatabaseUpdates.java b/src/main/java/org/qortal/repository/hsqldb/HSQLDBDatabaseUpdates.java index 54af22e98..02f99cf50 100644 --- a/src/main/java/org/qortal/repository/hsqldb/HSQLDBDatabaseUpdates.java +++ b/src/main/java/org/qortal/repository/hsqldb/HSQLDBDatabaseUpdates.java @@ -1052,6 +1052,20 @@ private static boolean databaseUpdating(Connection connection, boolean wasPristi stmt.execute("UPDATE Accounts SET blocks_minted_penalty = -5000000 WHERE blocks_minted_penalty < 0"); break; + case 50: + // For private name sales + stmt.execute("ALTER TABLE Names ADD is_private_sale BOOLEAN NOT NULL DEFAULT FALSE"); + stmt.execute("ALTER TABLE Names ADD sale_recipient QortalAddress"); + + stmt.execute("ALTER TABLE SellNameTransactions ADD is_private_sale BOOLEAN NOT NULL DEFAULT FALSE"); + stmt.execute("ALTER TABLE SellNameTransactions ADD sale_recipient QortalAddress"); + + stmt.execute("ALTER TABLE CancelSellNameTransactions ADD is_private_sale BOOLEAN NOT NULL DEFAULT FALSE"); + stmt.execute("ALTER TABLE CancelSellNameTransactions ADD sale_recipient QortalAddress"); + + stmt.execute("ALTER TABLE BuyNameTransactions ADD is_private_sale BOOLEAN NOT NULL DEFAULT FALSE"); + break; + default: // nothing to do return false; diff --git a/src/main/java/org/qortal/repository/hsqldb/HSQLDBNameRepository.java b/src/main/java/org/qortal/repository/hsqldb/HSQLDBNameRepository.java index 06e416633..fb3669d97 100644 --- a/src/main/java/org/qortal/repository/hsqldb/HSQLDBNameRepository.java +++ b/src/main/java/org/qortal/repository/hsqldb/HSQLDBNameRepository.java @@ -20,7 +20,7 @@ public HSQLDBNameRepository(HSQLDBRepository repository) { @Override public NameData fromName(String name) throws DataException { String sql = "SELECT reduced_name, owner, data, registered_when, updated_when, " - + "is_for_sale, sale_price, reference, creation_group_id FROM Names WHERE name = ?"; + + "is_for_sale, sale_price, is_private_sale, sale_recipient, reference, creation_group_id FROM Names WHERE name = ?"; try (ResultSet resultSet = this.repository.checkedExecute(sql, name)) { if (resultSet == null) @@ -42,10 +42,16 @@ public NameData fromName(String name) throws DataException { if (salePrice == 0 && resultSet.wasNull()) salePrice = null; - byte[] reference = resultSet.getBytes(8); - int creationGroupId = resultSet.getInt(9); + boolean isPrivateSale = resultSet.getBoolean(8); - return new NameData(name, reducedName, owner, data, registered, updated, isForSale, salePrice, reference, creationGroupId); + String saleRecipient = resultSet.getString(9); + if (!isPrivateSale) + saleRecipient = null; + + byte[] reference = resultSet.getBytes(10); + int creationGroupId = resultSet.getInt(11); + + return new NameData(name, reducedName, owner, data, registered, updated, isForSale, salePrice, isPrivateSale, saleRecipient, reference, creationGroupId); } catch (SQLException e) { throw new DataException("Unable to fetch name info from repository", e); } @@ -63,7 +69,7 @@ public boolean nameExists(String name) throws DataException { @Override public NameData fromReducedName(String reducedName) throws DataException { String sql = "SELECT name, owner, data, registered_when, updated_when, " - + "is_for_sale, sale_price, reference, creation_group_id FROM Names WHERE reduced_name = ?"; + + "is_for_sale, sale_price, is_private_sale, sale_recipient, reference, creation_group_id FROM Names WHERE reduced_name = ?"; try (ResultSet resultSet = this.repository.checkedExecute(sql, reducedName)) { if (resultSet == null) @@ -85,10 +91,16 @@ public NameData fromReducedName(String reducedName) throws DataException { if (salePrice == 0 && resultSet.wasNull()) salePrice = null; - byte[] reference = resultSet.getBytes(8); - int creationGroupId = resultSet.getInt(9); + boolean isPrivateSale = resultSet.getBoolean(8); + + String saleRecipient = resultSet.getString(9); + if (!isPrivateSale) + saleRecipient = null; - return new NameData(name, reducedName, owner, data, registered, updated, isForSale, salePrice, reference, creationGroupId); + byte[] reference = resultSet.getBytes(10); + int creationGroupId = resultSet.getInt(11); + + return new NameData(name, reducedName, owner, data, registered, updated, isForSale, salePrice, isPrivateSale, saleRecipient, reference, creationGroupId); } catch (SQLException e) { throw new DataException("Unable to fetch name info from repository", e); } @@ -108,7 +120,7 @@ public List searchNames(String query, boolean prefixOnly, Integer limi List bindParams = new ArrayList<>(); sql.append("SELECT name, reduced_name, owner, data, registered_when, updated_when, " - + "is_for_sale, sale_price, reference, creation_group_id FROM Names " + + "is_for_sale, sale_price, is_private_sale, sale_recipient, reference, creation_group_id FROM Names " + "WHERE LCASE(name) LIKE ? ORDER BY name"); // Search anywhere in the name, unless "prefixOnly" has been requested @@ -145,10 +157,16 @@ public List searchNames(String query, boolean prefixOnly, Integer limi if (salePrice == 0 && resultSet.wasNull()) salePrice = null; - byte[] reference = resultSet.getBytes(9); - int creationGroupId = resultSet.getInt(10); + boolean isPrivateSale = resultSet.getBoolean(9); + + String saleRecipient = resultSet.getString(10); + if (!isPrivateSale) + saleRecipient = null; + + byte[] reference = resultSet.getBytes(11); + int creationGroupId = resultSet.getInt(12); - names.add(new NameData(name, reducedName, owner, data, registered, updated, isForSale, salePrice, reference, creationGroupId)); + names.add(new NameData(name, reducedName, owner, data, registered, updated, isForSale, salePrice, isPrivateSale, saleRecipient, reference, creationGroupId)); } while (resultSet.next()); return names; @@ -163,7 +181,7 @@ public List getAllNames(Long after, Integer limit, Integer offset, Boo List bindParams = new ArrayList<>(); sql.append("SELECT name, reduced_name, owner, data, registered_when, updated_when, " - + "is_for_sale, sale_price, reference, creation_group_id FROM Names"); + + "is_for_sale, sale_price, is_private_sale, sale_recipient, reference, creation_group_id FROM Names"); if (after != null) { sql.append(" WHERE registered_when > ? OR updated_when > ?"); @@ -202,10 +220,16 @@ public List getAllNames(Long after, Integer limit, Integer offset, Boo if (salePrice == 0 && resultSet.wasNull()) salePrice = null; - byte[] reference = resultSet.getBytes(9); - int creationGroupId = resultSet.getInt(10); + boolean isPrivateSale = resultSet.getBoolean(9); - names.add(new NameData(name, reducedName, owner, data, registered, updated, isForSale, salePrice, reference, creationGroupId)); + String saleRecipient = resultSet.getString(10); + if (!isPrivateSale) + saleRecipient = null; + + byte[] reference = resultSet.getBytes(11); + int creationGroupId = resultSet.getInt(12); + + names.add(new NameData(name, reducedName, owner, data, registered, updated, isForSale, salePrice, isPrivateSale, saleRecipient, reference, creationGroupId)); } while (resultSet.next()); return names; @@ -219,7 +243,7 @@ public List getNamesForSale(Integer limit, Integer offset, Boolean rev StringBuilder sql = new StringBuilder(512); sql.append("SELECT name, reduced_name, owner, data, registered_when, updated_when, " - + "sale_price, reference, creation_group_id FROM Names WHERE is_for_sale = TRUE ORDER BY name"); + + "sale_price, is_private_sale, sale_recipient, reference, creation_group_id FROM Names WHERE is_for_sale = TRUE ORDER BY name"); if (reverse != null && reverse) sql.append(" DESC"); @@ -250,10 +274,16 @@ public List getNamesForSale(Integer limit, Integer offset, Boolean rev if (salePrice == 0 && resultSet.wasNull()) salePrice = null; - byte[] reference = resultSet.getBytes(8); - int creationGroupId = resultSet.getInt(9); + boolean isPrivateSale = resultSet.getBoolean(8); + + String saleRecipient = resultSet.getString(9); + if (!isPrivateSale) + saleRecipient = null; - names.add(new NameData(name, reducedName, owner, data, registered, updated, isForSale, salePrice, reference, creationGroupId)); + byte[] reference = resultSet.getBytes(10); + int creationGroupId = resultSet.getInt(11); + + names.add(new NameData(name, reducedName, owner, data, registered, updated, isForSale, salePrice, isPrivateSale, saleRecipient, reference, creationGroupId)); } while (resultSet.next()); return names; @@ -267,7 +297,7 @@ public List getNamesByOwner(String owner, Integer limit, Integer offse StringBuilder sql = new StringBuilder(512); 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, is_private_sale, sale_recipient, reference, creation_group_id FROM Names WHERE owner = ? ORDER BY name"); if (reverse != null && reverse) sql.append(" DESC"); @@ -297,10 +327,16 @@ public List getNamesByOwner(String owner, Integer limit, Integer offse if (salePrice == 0 && resultSet.wasNull()) salePrice = null; - byte[] reference = resultSet.getBytes(8); - int creationGroupId = resultSet.getInt(9); + boolean isPrivateSale = resultSet.getBoolean(8); + + String saleRecipient = resultSet.getString(9); + if (!isPrivateSale) + saleRecipient = null; + + byte[] reference = resultSet.getBytes(10); + int creationGroupId = resultSet.getInt(11); - names.add(new NameData(name, reducedName, owner, data, registered, updated, isForSale, salePrice, reference, creationGroupId)); + names.add(new NameData(name, reducedName, owner, data, registered, updated, isForSale, salePrice, isPrivateSale, saleRecipient, reference, creationGroupId)); } while (resultSet.next()); return names; @@ -341,6 +377,7 @@ public void save(NameData nameData) throws DataException { .bind("owner", nameData.getOwner()).bind("data", nameData.getData()) .bind("registered_when", nameData.getRegistered()).bind("updated_when", nameData.getUpdated()) .bind("is_for_sale", nameData.isForSale()).bind("sale_price", nameData.getSalePrice()) + .bind("is_private_sale", nameData.getIsPrivateSale()).bind("sale_recipient", nameData.getSaleRecipient()) .bind("reference", nameData.getReference()).bind("creation_group_id", nameData.getCreationGroupId()); try { diff --git a/src/main/java/org/qortal/repository/hsqldb/transaction/HSQLDBBuyNameTransactionRepository.java b/src/main/java/org/qortal/repository/hsqldb/transaction/HSQLDBBuyNameTransactionRepository.java index 724d93dd0..08cb5d37a 100644 --- a/src/main/java/org/qortal/repository/hsqldb/transaction/HSQLDBBuyNameTransactionRepository.java +++ b/src/main/java/org/qortal/repository/hsqldb/transaction/HSQLDBBuyNameTransactionRepository.java @@ -17,7 +17,7 @@ public HSQLDBBuyNameTransactionRepository(HSQLDBRepository repository) { } TransactionData fromBase(BaseTransactionData baseTransactionData) throws DataException { - String sql = "SELECT name, amount, seller, name_reference FROM BuyNameTransactions WHERE signature = ?"; + String sql = "SELECT name, amount, seller, is_private_sale, name_reference FROM BuyNameTransactions WHERE signature = ?"; try (ResultSet resultSet = this.repository.checkedExecute(sql, baseTransactionData.getSignature())) { if (resultSet == null) @@ -26,9 +26,10 @@ TransactionData fromBase(BaseTransactionData baseTransactionData) throws DataExc String name = resultSet.getString(1); long amount = resultSet.getLong(2); String seller = resultSet.getString(3); - byte[] nameReference = resultSet.getBytes(4); + boolean isPrivateSale = resultSet.getBoolean(4); + byte[] nameReference = resultSet.getBytes(5); - return new BuyNameTransactionData(baseTransactionData, name, amount, seller, nameReference); + return new BuyNameTransactionData(baseTransactionData, name, amount, seller, isPrivateSale, nameReference); } catch (SQLException e) { throw new DataException("Unable to fetch buy name transaction from repository", e); } @@ -42,7 +43,7 @@ public void save(TransactionData transactionData) throws DataException { saveHelper.bind("signature", buyNameTransactionData.getSignature()).bind("buyer", buyNameTransactionData.getBuyerPublicKey()) .bind("name", buyNameTransactionData.getName()).bind("amount", buyNameTransactionData.getAmount()) - .bind("seller", buyNameTransactionData.getSeller()).bind("name_reference", buyNameTransactionData.getNameReference()); + .bind("seller", buyNameTransactionData.getSeller()).bind("is_private_sale", buyNameTransactionData.getIsPrivateSale()).bind("name_reference", buyNameTransactionData.getNameReference()); try { saveHelper.execute(this.repository); 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 925b18cc3..f351e6c5c 100644 --- a/src/main/java/org/qortal/repository/hsqldb/transaction/HSQLDBCancelSellNameTransactionRepository.java +++ b/src/main/java/org/qortal/repository/hsqldb/transaction/HSQLDBCancelSellNameTransactionRepository.java @@ -17,7 +17,7 @@ public HSQLDBCancelSellNameTransactionRepository(HSQLDBRepository repository) { } TransactionData fromBase(BaseTransactionData baseTransactionData) throws DataException { - String sql = "SELECT name, sale_price FROM CancelSellNameTransactions WHERE signature = ?"; + String sql = "SELECT name, sale_price, is_private_sale, sale_recipient FROM CancelSellNameTransactions WHERE signature = ?"; try (ResultSet resultSet = this.repository.checkedExecute(sql, baseTransactionData.getSignature())) { if (resultSet == null) @@ -25,8 +25,12 @@ TransactionData fromBase(BaseTransactionData baseTransactionData) throws DataExc String name = resultSet.getString(1); Long salePrice = resultSet.getLong(2); + boolean isPrivateSale = resultSet.getBoolean(3); + String saleRecipient = resultSet.getString(4); + if (!isPrivateSale) + saleRecipient = null; - return new CancelSellNameTransactionData(baseTransactionData, name, salePrice); + return new CancelSellNameTransactionData(baseTransactionData, name, salePrice, isPrivateSale, saleRecipient); } catch (SQLException e) { throw new DataException("Unable to fetch cancel sell name transaction from repository", e); } @@ -39,7 +43,7 @@ public void save(TransactionData transactionData) throws DataException { HSQLDBSaver saveHelper = new HSQLDBSaver("CancelSellNameTransactions"); saveHelper.bind("signature", cancelSellNameTransactionData.getSignature()).bind("owner", cancelSellNameTransactionData.getOwnerPublicKey()).bind("name", - cancelSellNameTransactionData.getName()).bind("sale_price", cancelSellNameTransactionData.getSalePrice()); + cancelSellNameTransactionData.getName()).bind("sale_price", cancelSellNameTransactionData.getSalePrice()).bind("is_private_sale", cancelSellNameTransactionData.getIsPrivateSale()).bind("sale_recipient", cancelSellNameTransactionData.getSaleRecipient()); try { saveHelper.execute(this.repository); diff --git a/src/main/java/org/qortal/repository/hsqldb/transaction/HSQLDBSellNameTransactionRepository.java b/src/main/java/org/qortal/repository/hsqldb/transaction/HSQLDBSellNameTransactionRepository.java index ae4a07dd6..305249e3d 100644 --- a/src/main/java/org/qortal/repository/hsqldb/transaction/HSQLDBSellNameTransactionRepository.java +++ b/src/main/java/org/qortal/repository/hsqldb/transaction/HSQLDBSellNameTransactionRepository.java @@ -17,7 +17,7 @@ public HSQLDBSellNameTransactionRepository(HSQLDBRepository repository) { } TransactionData fromBase(BaseTransactionData baseTransactionData) throws DataException { - String sql = "SELECT name, amount FROM SellNameTransactions WHERE signature = ?"; + String sql = "SELECT name, amount, is_private_sale, sale_recipient FROM SellNameTransactions WHERE signature = ?"; try (ResultSet resultSet = this.repository.checkedExecute(sql, baseTransactionData.getSignature())) { if (resultSet == null) @@ -25,8 +25,12 @@ TransactionData fromBase(BaseTransactionData baseTransactionData) throws DataExc String name = resultSet.getString(1); long amount = resultSet.getLong(2); + boolean isPrivateSale = resultSet.getBoolean(3); + String saleRecipient = resultSet.getString(4); + if (!isPrivateSale) + saleRecipient = null; - return new SellNameTransactionData(baseTransactionData, name, amount); + return new SellNameTransactionData(baseTransactionData, name, amount, isPrivateSale, saleRecipient); } catch (SQLException e) { throw new DataException("Unable to fetch sell name transaction from repository", e); } @@ -39,7 +43,8 @@ public void save(TransactionData transactionData) throws DataException { HSQLDBSaver saveHelper = new HSQLDBSaver("SellNameTransactions"); saveHelper.bind("signature", sellNameTransactionData.getSignature()).bind("owner", sellNameTransactionData.getOwnerPublicKey()) - .bind("name", sellNameTransactionData.getName()).bind("amount", sellNameTransactionData.getAmount()); + .bind("name", sellNameTransactionData.getName()).bind("amount", sellNameTransactionData.getAmount()) + .bind("is_private_sale", sellNameTransactionData.getIsPrivateSale()).bind("sale_recipient", sellNameTransactionData.getSaleRecipient()); try { saveHelper.execute(this.repository); diff --git a/src/main/java/org/qortal/transaction/BuyNameTransaction.java b/src/main/java/org/qortal/transaction/BuyNameTransaction.java index 72c15f69e..53973b639 100644 --- a/src/main/java/org/qortal/transaction/BuyNameTransaction.java +++ b/src/main/java/org/qortal/transaction/BuyNameTransaction.java @@ -91,6 +91,23 @@ public ValidationResult isValid() throws DataException { if (this.buyNameTransactionData.getAmount() != nameData.getSalePrice()) return ValidationResult.INVALID_AMOUNT; + // Check for private sale + if (nameData.getIsPrivateSale()) { + + // Check buyer address matches expected recipient + if (!buyer.getAddress().equals(nameData.getSaleRecipient())) + return ValidationResult.INVALID_NAME_OWNER; + + // Check buyer transaction is set to private + if (!this.buyNameTransactionData.getIsPrivateSale()) + return ValidationResult.INVALID_RETURN; + } else { + + // Check buyer transaction matches if not private + if (this.buyNameTransactionData.getIsPrivateSale()) + return ValidationResult.INVALID_RETURN; + } + // Check buyer has enough funds if (buyer.getConfirmedBalance(Asset.QORT) < this.buyNameTransactionData.getFee()) return ValidationResult.NO_BALANCE; diff --git a/src/main/java/org/qortal/transaction/SellNameTransaction.java b/src/main/java/org/qortal/transaction/SellNameTransaction.java index b2fd1484e..79b4f0579 100644 --- a/src/main/java/org/qortal/transaction/SellNameTransaction.java +++ b/src/main/java/org/qortal/transaction/SellNameTransaction.java @@ -4,6 +4,7 @@ import org.qortal.account.Account; import org.qortal.asset.Asset; import org.qortal.controller.repository.NamesDatabaseIntegrityCheck; +import org.qortal.crypto.Crypto; import org.qortal.data.naming.NameData; import org.qortal.data.transaction.SellNameTransactionData; import org.qortal.data.transaction.TransactionData; @@ -74,9 +75,28 @@ public ValidationResult isValid() throws DataException { if (!owner.getAddress().equals(nameData.getOwner())) return ValidationResult.INVALID_NAME_OWNER; - // Check amount is positive - if (this.sellNameTransactionData.getAmount() <= 0) - return ValidationResult.NEGATIVE_AMOUNT; + // Check for private sale + if (this.sellNameTransactionData.getIsPrivateSale()) { + + // Check recipient address is valid + if (!Crypto.isValidAddress(this.sellNameTransactionData.getSaleRecipient())) + return ValidationResult.INVALID_ADDRESS; + + // Check recipient is not already name owner + if (owner.getAddress().equals(this.sellNameTransactionData.getSaleRecipient())) + return ValidationResult.BUYER_ALREADY_OWNER; + + // Check amount is not negative (allow 0 for private sales) + if (this.sellNameTransactionData.getAmount() < 0) + return ValidationResult.NEGATIVE_AMOUNT; + + // ...for public sales + } else { + + // Check amount is positive (don't allow 0 for public sales) + if (this.sellNameTransactionData.getAmount() <= 0) + return ValidationResult.NEGATIVE_AMOUNT; + } // Check amount within bounds if (this.sellNameTransactionData.getAmount() >= MAX_AMOUNT) diff --git a/src/main/java/org/qortal/transform/transaction/BuyNameTransactionTransformer.java b/src/main/java/org/qortal/transform/transaction/BuyNameTransactionTransformer.java index 8dff8a256..8de7c538b 100644 --- a/src/main/java/org/qortal/transform/transaction/BuyNameTransactionTransformer.java +++ b/src/main/java/org/qortal/transform/transaction/BuyNameTransactionTransformer.java @@ -20,7 +20,7 @@ public class BuyNameTransactionTransformer extends TransactionTransformer { private static final int NAME_SIZE_LENGTH = INT_LENGTH; private static final int SELLER_LENGTH = ADDRESS_LENGTH; - private static final int EXTRAS_LENGTH = NAME_SIZE_LENGTH + AMOUNT_LENGTH + SELLER_LENGTH; + private static final int EXTRAS_LENGTH = NAME_SIZE_LENGTH + AMOUNT_LENGTH + SELLER_LENGTH + BOOLEAN_LENGTH; protected static final TransactionLayout layout; @@ -35,6 +35,7 @@ public class BuyNameTransactionTransformer extends TransactionTransformer { layout.add("name", TransformationType.STRING); layout.add("buy price", TransformationType.AMOUNT); layout.add("seller", TransformationType.ADDRESS); + layout.add("is private sale", TransformationType.BOOLEAN); layout.add("fee", TransformationType.AMOUNT); layout.add("signature", TransformationType.SIGNATURE); } @@ -55,6 +56,8 @@ public static TransactionData fromByteBuffer(ByteBuffer byteBuffer) throws Trans String seller = Serialization.deserializeAddress(byteBuffer); + boolean isPrivateSale = byteBuffer.get() != 0; + long fee = byteBuffer.getLong(); byte[] signature = new byte[SIGNATURE_LENGTH]; @@ -62,7 +65,7 @@ public static TransactionData fromByteBuffer(ByteBuffer byteBuffer) throws Trans BaseTransactionData baseTransactionData = new BaseTransactionData(timestamp, txGroupId, reference, buyerPublicKey, fee, signature); - return new BuyNameTransactionData(baseTransactionData, name, amount, seller); + return new BuyNameTransactionData(baseTransactionData, name, amount, seller, isPrivateSale); } public static int getDataLength(TransactionData transactionData) throws TransformationException { @@ -85,6 +88,8 @@ public static byte[] toBytes(TransactionData transactionData) throws Transformat Serialization.serializeAddress(bytes, buyNameTransactionData.getSeller()); + bytes.write((byte) (buyNameTransactionData.getIsPrivateSale() ? 1 : 0)); + bytes.write(Longs.toByteArray(buyNameTransactionData.getFee())); if (buyNameTransactionData.getSignature() != null) diff --git a/src/main/java/org/qortal/transform/transaction/SellNameTransactionTransformer.java b/src/main/java/org/qortal/transform/transaction/SellNameTransactionTransformer.java index b0ba3f3ef..c86bfd6d6 100644 --- a/src/main/java/org/qortal/transform/transaction/SellNameTransactionTransformer.java +++ b/src/main/java/org/qortal/transform/transaction/SellNameTransactionTransformer.java @@ -19,7 +19,7 @@ public class SellNameTransactionTransformer extends TransactionTransformer { // Property lengths private static final int NAME_SIZE_LENGTH = INT_LENGTH; - private static final int EXTRAS_LENGTH = NAME_SIZE_LENGTH + AMOUNT_LENGTH; + private static final int EXTRAS_LENGTH = NAME_SIZE_LENGTH + AMOUNT_LENGTH + BOOLEAN_LENGTH; protected static final TransactionLayout layout; @@ -33,6 +33,8 @@ public class SellNameTransactionTransformer extends TransactionTransformer { layout.add("name length", TransformationType.INT); layout.add("name", TransformationType.STRING); layout.add("sale price", TransformationType.AMOUNT); + layout.add("is private sale", TransformationType.BOOLEAN); + layout.add("sale recipient", TransformationType.ADDRESS); layout.add("fee", TransformationType.AMOUNT); layout.add("signature", TransformationType.SIGNATURE); } @@ -51,6 +53,12 @@ public static TransactionData fromByteBuffer(ByteBuffer byteBuffer) throws Trans long amount = byteBuffer.getLong(); + boolean isPrivateSale = byteBuffer.get() != 0; + + String saleRecipient = null; + if (isPrivateSale) + saleRecipient = Serialization.deserializeAddress(byteBuffer); + long fee = byteBuffer.getLong(); byte[] signature = new byte[SIGNATURE_LENGTH]; @@ -58,12 +66,15 @@ public static TransactionData fromByteBuffer(ByteBuffer byteBuffer) throws Trans BaseTransactionData baseTransactionData = new BaseTransactionData(timestamp, txGroupId, reference, ownerPublicKey, fee, signature); - return new SellNameTransactionData(baseTransactionData, name, amount); + return new SellNameTransactionData(baseTransactionData, name, amount, isPrivateSale, saleRecipient); } public static int getDataLength(TransactionData transactionData) throws TransformationException { SellNameTransactionData sellNameTransactionData = (SellNameTransactionData) transactionData; + if (sellNameTransactionData.getIsPrivateSale()) + return getBaseLength(transactionData) + EXTRAS_LENGTH + ADDRESS_LENGTH + Utf8.encodedLength(sellNameTransactionData.getName()); + return getBaseLength(transactionData) + EXTRAS_LENGTH + Utf8.encodedLength(sellNameTransactionData.getName()); } @@ -79,6 +90,11 @@ public static byte[] toBytes(TransactionData transactionData) throws Transformat bytes.write(Longs.toByteArray(sellNameTransactionData.getAmount())); + bytes.write((byte) (sellNameTransactionData.getIsPrivateSale() ? 1 : 0)); + + if (sellNameTransactionData.getIsPrivateSale()) + Serialization.serializeAddress(bytes, sellNameTransactionData.getSaleRecipient()); + bytes.write(Longs.toByteArray(sellNameTransactionData.getFee())); if (sellNameTransactionData.getSignature() != null)