diff --git a/src/main/java/org/qortal/data/group/GroupData.java b/src/main/java/org/qortal/data/group/GroupData.java index f25f1ce3..c97f5438 100644 --- a/src/main/java/org/qortal/data/group/GroupData.java +++ b/src/main/java/org/qortal/data/group/GroupData.java @@ -35,6 +35,11 @@ public class GroupData { @Schema(hidden = true) private int creationGroupId; + // For internal use + @XmlTransient + @Schema(hidden = true) + private String reducedGroupName; + // We abuse GroupData for API purposes by adding this unrelated field. Not always present. private Boolean isAdmin; @@ -45,10 +50,12 @@ public class GroupData { } /** Constructs new GroupData with nullable groupId and nullable updated [timestamp] */ - public GroupData(Integer groupId, String owner, String name, String description, long created, Long updated, boolean isOpen, ApprovalThreshold approvalThreshold, int minBlockDelay, int maxBlockDelay, byte[] reference, int creationGroupId) { + public GroupData(Integer groupId, String owner, String groupName, String description, long created, Long updated, + boolean isOpen, ApprovalThreshold approvalThreshold, int minBlockDelay, int maxBlockDelay, byte[] reference, + int creationGroupId, String reducedGroupName) { this.groupId = groupId; this.owner = owner; - this.groupName = name; + this.groupName = groupName; this.description = description; this.created = created; this.updated = updated; @@ -58,11 +65,15 @@ public class GroupData { this.minimumBlockDelay = minBlockDelay; this.maximumBlockDelay = maxBlockDelay; this.creationGroupId = creationGroupId; + this.reducedGroupName = reducedGroupName; } /** Constructs new GroupData with unassigned groupId */ - public GroupData(String owner, String name, String description, long created, boolean isOpen, ApprovalThreshold approvalThreshold, int minBlockDelay, int maxBlockDelay, byte[] reference, int creationGroupId) { - this(null, owner, name, description, created, null, isOpen, approvalThreshold, minBlockDelay, maxBlockDelay, reference, creationGroupId); + public GroupData(String owner, String groupName, String description, long created, boolean isOpen, + ApprovalThreshold approvalThreshold, int minBlockDelay, int maxBlockDelay, byte[] reference, + int creationGroupId, String reducedGroupName) { + this(null, owner, groupName, description, created, null, isOpen, approvalThreshold, minBlockDelay, + maxBlockDelay, reference, creationGroupId, reducedGroupName); } // Getters / setters @@ -115,7 +126,7 @@ public class GroupData { this.reference = reference; } - public boolean getIsOpen() { + public boolean isOpen() { return this.isOpen; } @@ -143,6 +154,14 @@ public class GroupData { return this.creationGroupId; } + public String getReducedGroupName() { + return this.reducedGroupName; + } + + public void setReducedGroupName(String reducedGroupName) { + this.reducedGroupName = reducedGroupName; + } + // This is for API call GET /groups/member/{address} public Boolean isAdmin() { diff --git a/src/main/java/org/qortal/data/transaction/CreateGroupTransactionData.java b/src/main/java/org/qortal/data/transaction/CreateGroupTransactionData.java index 0ef6383b..4bcaa497 100644 --- a/src/main/java/org/qortal/data/transaction/CreateGroupTransactionData.java +++ b/src/main/java/org/qortal/data/transaction/CreateGroupTransactionData.java @@ -1,10 +1,14 @@ 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.XmlElement; +import javax.xml.bind.annotation.XmlTransient; import org.eclipse.persistence.oxm.annotations.XmlDiscriminatorValue; +import org.qortal.block.GenesisBlock; +import org.qortal.group.Group; import org.qortal.group.Group.ApprovalThreshold; import org.qortal.transaction.Transaction.TransactionType; @@ -24,40 +28,32 @@ public class CreateGroupTransactionData extends TransactionData { // Properties // groupId can be null but assigned during save() or during load from repository - @Schema( - accessMode = AccessMode.READ_ONLY, - description = "assigned group ID" - ) + @Schema(accessMode = AccessMode.READ_ONLY, description = "assigned group ID") private Integer groupId = null; - @Schema( - description = "group owner's address", - example = "QgV4s3xnzLhVBEJxcYui4u4q11yhUHsd9v" - ) - private String owner; - @Schema( - description = "group name", - example = "miner-group" - ) + + @Schema(description = "group name", example = "miner-group") private String groupName; - @Schema( - description = "short description of group", - example = "this group is for block miners" - ) + + @Schema(description = "short description of group", example = "this group is for block miners") private String description; - @Schema( - description = "whether anyone can join group (open) or group is invite-only (closed)", - example = "true" - ) + + @Schema(description = "whether anyone can join group (open) or group is invite-only (closed)", example = "true") private boolean isOpen; - @Schema( - description = "how many group admins are required to approve group member transactions" - ) + + @Schema(description = "how many group admins are required to approve group member transactions") private ApprovalThreshold approvalThreshold; + @Schema(description = "minimum block delay before approval takes effect") private int minimumBlockDelay; + @Schema(description = "maximum block delay before which transaction approval must be reached") private int maximumBlockDelay; + // For internal use + @XmlTransient + @Schema(hidden = true) + private String reducedGroupName; + // Constructors // For JAXB @@ -65,13 +61,22 @@ public class CreateGroupTransactionData extends TransactionData { super(TransactionType.CREATE_GROUP); } + public void afterUnmarshal(Unmarshaller u, Object parent) { + /* + * If we're being constructed as part of the genesis block info inside blockchain config + * then we need to construct 'reduced' group name. + */ + if (parent instanceof GenesisBlock.GenesisInfo && this.reducedGroupName == null) + this.reducedGroupName = Group.reduceName(this.groupName); + } + /** From repository */ public CreateGroupTransactionData(BaseTransactionData baseTransactionData, - String owner, String groupName, String description, boolean isOpen, - ApprovalThreshold approvalThreshold, int minimumBlockDelay, int maximumBlockDelay, Integer groupId) { + String groupName, String description, boolean isOpen, + ApprovalThreshold approvalThreshold, int minimumBlockDelay, int maximumBlockDelay, + Integer groupId, String reducedGroupName) { super(TransactionType.CREATE_GROUP, baseTransactionData); - this.owner = owner; this.groupName = groupName; this.description = description; this.isOpen = isOpen; @@ -79,21 +84,18 @@ public class CreateGroupTransactionData extends TransactionData { this.minimumBlockDelay = minimumBlockDelay; this.maximumBlockDelay = maximumBlockDelay; this.groupId = groupId; + this.reducedGroupName = reducedGroupName; } /** From network/API */ public CreateGroupTransactionData(BaseTransactionData baseTransactionData, - String owner, String groupName, String description, boolean isOpen, + String groupName, String description, boolean isOpen, ApprovalThreshold approvalThreshold, int minimumBlockDelay, int maximumBlockDelay) { - this(baseTransactionData, owner, groupName, description, isOpen, approvalThreshold, minimumBlockDelay, maximumBlockDelay, null); + this(baseTransactionData, groupName, description, isOpen, approvalThreshold, minimumBlockDelay, maximumBlockDelay, null, null); } // Getters / setters - public String getOwner() { - return this.owner; - } - public String getGroupName() { return this.groupName; } @@ -102,7 +104,7 @@ public class CreateGroupTransactionData extends TransactionData { return this.description; } - public boolean getIsOpen() { + public boolean isOpen() { return this.isOpen; } @@ -126,27 +128,23 @@ public class CreateGroupTransactionData extends TransactionData { this.groupId = groupId; } + public String getReducedGroupName() { + return this.reducedGroupName; + } + + public void setReducedGroupName(String reducedGroupName) { + this.reducedGroupName = reducedGroupName; + } + // Re-expose creatorPublicKey for this transaction type for JAXB - @XmlElement( - name = "creatorPublicKey" - ) - @Schema( - name = "creatorPublicKey", - description = "group creator's public key", - example = "2tiMr5LTpaWCgbRvkPK8TFd7k63DyHJMMFFsz9uBf1ZP" - ) + @XmlElement(name = "creatorPublicKey") + @Schema(name = "creatorPublicKey", description = "group creator's public key", example = "2tiMr5LTpaWCgbRvkPK8TFd7k63DyHJMMFFsz9uBf1ZP") public byte[] getGroupCreatorPublicKey() { return this.creatorPublicKey; } - @XmlElement( - name = "creatorPublicKey" - ) - @Schema( - name = "creatorPublicKey", - description = "group creator's public key", - example = "2tiMr5LTpaWCgbRvkPK8TFd7k63DyHJMMFFsz9uBf1ZP" - ) + @XmlElement(name = "creatorPublicKey") + @Schema(name = "creatorPublicKey", description = "group creator's public key", example = "2tiMr5LTpaWCgbRvkPK8TFd7k63DyHJMMFFsz9uBf1ZP") public void setGroupCreatorPublicKey(byte[] creatorPublicKey) { this.creatorPublicKey = creatorPublicKey; } diff --git a/src/main/java/org/qortal/group/Group.java b/src/main/java/org/qortal/group/Group.java index f00be199..a3cc017c 100644 --- a/src/main/java/org/qortal/group/Group.java +++ b/src/main/java/org/qortal/group/Group.java @@ -8,6 +8,7 @@ import java.util.Map; import org.qortal.account.Account; import org.qortal.account.PublicKeyAccount; +import org.qortal.crypto.Crypto; import org.qortal.data.group.GroupAdminData; import org.qortal.data.group.GroupBanData; import org.qortal.data.group.GroupData; @@ -29,6 +30,7 @@ import org.qortal.data.transaction.UpdateGroupTransactionData; import org.qortal.repository.DataException; import org.qortal.repository.GroupRepository; import org.qortal.repository.Repository; +import org.qortal.utils.Unicode; public class Group { @@ -78,6 +80,7 @@ public class Group { // Useful constants public static final int NO_GROUP = 0; + public static final int MIN_NAME_SIZE = 3; public static final int MAX_NAME_SIZE = 32; public static final int MAX_DESCRIPTION_SIZE = 128; /** Max size of kick/ban reason */ @@ -95,10 +98,14 @@ public class Group { this.repository = repository; this.groupRepository = repository.getGroupRepository(); - this.groupData = new GroupData(createGroupTransactionData.getOwner(), createGroupTransactionData.getGroupName(), - createGroupTransactionData.getDescription(), createGroupTransactionData.getTimestamp(), createGroupTransactionData.getIsOpen(), - createGroupTransactionData.getApprovalThreshold(), createGroupTransactionData.getMinimumBlockDelay(), - createGroupTransactionData.getMaximumBlockDelay(), createGroupTransactionData.getSignature(), createGroupTransactionData.getTxGroupId()); + String owner = Crypto.toAddress(createGroupTransactionData.getCreatorPublicKey()); + + this.groupData = new GroupData(owner, createGroupTransactionData.getGroupName(), + createGroupTransactionData.getDescription(), createGroupTransactionData.getTimestamp(), + createGroupTransactionData.isOpen(), createGroupTransactionData.getApprovalThreshold(), + createGroupTransactionData.getMinimumBlockDelay(), createGroupTransactionData.getMaximumBlockDelay(), + createGroupTransactionData.getSignature(), createGroupTransactionData.getTxGroupId(), + createGroupTransactionData.getReducedGroupName()); } /** @@ -123,6 +130,10 @@ public class Group { // Shortcuts to aid code clarity + public static String reduceName(String name) { + return Unicode.sanitize(name); + } + // Membership private GroupMemberData getMember(String member) throws DataException { @@ -355,16 +366,20 @@ public class Group { throw new DataException("Unable to revert group transaction as referenced transaction not found in repository"); switch (previousTransactionData.getType()) { - case CREATE_GROUP: + case CREATE_GROUP: { CreateGroupTransactionData previousCreateGroupTransactionData = (CreateGroupTransactionData) previousTransactionData; - this.groupData.setOwner(previousCreateGroupTransactionData.getOwner()); + + String owner = Crypto.toAddress(previousCreateGroupTransactionData.getCreatorPublicKey()); + + this.groupData.setOwner(owner); this.groupData.setDescription(previousCreateGroupTransactionData.getDescription()); - this.groupData.setIsOpen(previousCreateGroupTransactionData.getIsOpen()); + this.groupData.setIsOpen(previousCreateGroupTransactionData.isOpen()); this.groupData.setApprovalThreshold(previousCreateGroupTransactionData.getApprovalThreshold()); this.groupData.setUpdated(null); break; + } - case UPDATE_GROUP: + case UPDATE_GROUP: { UpdateGroupTransactionData previousUpdateGroupTransactionData = (UpdateGroupTransactionData) previousTransactionData; this.groupData.setOwner(previousUpdateGroupTransactionData.getNewOwner()); this.groupData.setDescription(previousUpdateGroupTransactionData.getNewDescription()); @@ -372,6 +387,7 @@ public class Group { this.groupData.setApprovalThreshold(previousUpdateGroupTransactionData.getNewApprovalThreshold()); this.groupData.setUpdated(previousUpdateGroupTransactionData.getTimestamp()); break; + } default: throw new IllegalStateException("Unable to revert group transaction due to unsupported referenced transaction"); @@ -722,7 +738,7 @@ public class Group { // If there is no invites and this group is "closed" (i.e. invite-only) then // this is now a pending "join request" - if (groupInviteData == null && !groupData.getIsOpen()) { + if (groupInviteData == null && !groupData.isOpen()) { // Save join request this.addJoinRequest(joiner.getAddress(), joinGroupTransactionData.getSignature()); @@ -761,7 +777,7 @@ public class Group { byte[] inviteReference = joinGroupTransactionData.getInviteReference(); // Was this a join-request only? - if (inviteReference == null && !groupData.getIsOpen()) { + if (inviteReference == null && !groupData.isOpen()) { // Delete join request this.deleteJoinRequest(joiner.getAddress()); } else { diff --git a/src/main/java/org/qortal/repository/GroupRepository.java b/src/main/java/org/qortal/repository/GroupRepository.java index 26074b74..bcee7d25 100644 --- a/src/main/java/org/qortal/repository/GroupRepository.java +++ b/src/main/java/org/qortal/repository/GroupRepository.java @@ -21,6 +21,8 @@ public interface GroupRepository { public boolean groupExists(String groupName) throws DataException; + public boolean reducedGroupNameExists(String reducedGroupName) throws DataException; + public List getAllGroups(Integer limit, Integer offset, Boolean reverse) throws DataException; public default List getAllGroups() throws DataException { diff --git a/src/main/java/org/qortal/repository/hsqldb/HSQLDBDatabaseUpdates.java b/src/main/java/org/qortal/repository/hsqldb/HSQLDBDatabaseUpdates.java index 12874e63..8cfa8f06 100644 --- a/src/main/java/org/qortal/repository/hsqldb/HSQLDBDatabaseUpdates.java +++ b/src/main/java/org/qortal/repository/hsqldb/HSQLDBDatabaseUpdates.java @@ -444,12 +444,15 @@ public class HSQLDBDatabaseUpdates { case 12: // Groups - stmt.execute("CREATE TABLE Groups (group_id GroupID, owner QortalAddress NOT NULL, group_name GroupName, " + stmt.execute("CREATE TABLE Groups (group_id GroupID, owner QortalAddress NOT NULL, group_name GroupName NOT NULL, " + "created_when EpochMillis NOT NULL, updated_when EpochMillis, is_open BOOLEAN NOT NULL, " + "approval_threshold TINYINT NOT NULL, min_block_delay INTEGER NOT NULL, max_block_delay INTEGER NOT NULL, " - + "reference Signature, creation_group_id GroupID, description GenericDescription NOT NULL, PRIMARY KEY (group_id))"); - // For when a user wants to lookup an group by name + + "reference Signature, creation_group_id GroupID, reduced_group_name GroupName NOT NULL, " + + "description GenericDescription NOT NULL, PRIMARY KEY (group_id))"); + // For finding groups by name stmt.execute("CREATE INDEX GroupNameIndex on Groups (group_name)"); + // For finding groups by reduced name + stmt.execute("CREATE INDEX GroupReducedNameIndex on Groups (reduced_group_name)"); // For finding groups by owner stmt.execute("CREATE INDEX GroupOwnerIndex ON Groups (owner)"); @@ -499,7 +502,7 @@ public class HSQLDBDatabaseUpdates { // Group transactions // Create group stmt.execute("CREATE TABLE CreateGroupTransactions (signature Signature, creator QortalPublicKey NOT NULL, group_name GroupName NOT NULL, " - + "owner QortalAddress NOT NULL, is_open BOOLEAN NOT NULL, approval_threshold TINYINT NOT NULL, " + + "is_open BOOLEAN NOT NULL, approval_threshold TINYINT NOT NULL, reduced_group_name GroupName NOT NULL, " + "min_block_delay INTEGER NOT NULL, max_block_delay INTEGER NOT NULL, group_id GroupID, description GenericDescription NOT NULL, " + TRANSACTION_KEYS + ")"); diff --git a/src/main/java/org/qortal/repository/hsqldb/HSQLDBGroupRepository.java b/src/main/java/org/qortal/repository/hsqldb/HSQLDBGroupRepository.java index 460b5641..60984ee0 100644 --- a/src/main/java/org/qortal/repository/hsqldb/HSQLDBGroupRepository.java +++ b/src/main/java/org/qortal/repository/hsqldb/HSQLDBGroupRepository.java @@ -28,7 +28,8 @@ public class HSQLDBGroupRepository implements GroupRepository { @Override public GroupData fromGroupId(int groupId) throws DataException { String sql = "SELECT group_name, owner, description, created_when, updated_when, reference, is_open, " - + "approval_threshold, min_block_delay, max_block_delay, creation_group_id FROM Groups WHERE group_id = ?"; + + "approval_threshold, min_block_delay, max_block_delay, creation_group_id, reduced_group_name " + + "FROM Groups WHERE group_id = ?"; try (ResultSet resultSet = this.repository.checkedExecute(sql, groupId)) { if (resultSet == null) @@ -53,9 +54,10 @@ public class HSQLDBGroupRepository implements GroupRepository { int maxBlockDelay = resultSet.getInt(10); int creationGroupId = resultSet.getInt(11); + String reducedGroupName = resultSet.getString(12); return new GroupData(groupId, owner, groupName, description, created, updated, isOpen, - approvalThreshold, minBlockDelay, maxBlockDelay, reference, creationGroupId); + approvalThreshold, minBlockDelay, maxBlockDelay, reference, creationGroupId, reducedGroupName); } catch (SQLException e) { throw new DataException("Unable to fetch group info from repository", e); } @@ -64,7 +66,8 @@ public class HSQLDBGroupRepository implements GroupRepository { @Override public GroupData fromGroupName(String groupName) throws DataException { String sql = "SELECT group_id, owner, description, created_when, updated_when, reference, is_open, " - + "approval_threshold, min_block_delay, max_block_delay, creation_group_id FROM Groups WHERE group_name = ?"; + + "approval_threshold, min_block_delay, max_block_delay, creation_group_id, reduced_group_name " + + "FROM Groups WHERE group_name = ?"; try (ResultSet resultSet = this.repository.checkedExecute(sql, groupName)) { if (resultSet == null) @@ -89,9 +92,10 @@ public class HSQLDBGroupRepository implements GroupRepository { int maxBlockDelay = resultSet.getInt(10); int creationGroupId = resultSet.getInt(11); + String reducedGroupName = resultSet.getString(12); return new GroupData(groupId, owner, groupName, description, created, updated, isOpen, - approvalThreshold, minBlockDelay, maxBlockDelay, reference, creationGroupId); + approvalThreshold, minBlockDelay, maxBlockDelay, reference, creationGroupId, reducedGroupName); } catch (SQLException e) { throw new DataException("Unable to fetch group info from repository", e); } @@ -115,12 +119,22 @@ public class HSQLDBGroupRepository implements GroupRepository { } } + @Override + public boolean reducedGroupNameExists(String reducedGroupName) throws DataException { + try { + return this.repository.exists("Groups", "reduced_group_name = ?", reducedGroupName); + } catch (SQLException e) { + throw new DataException("Unable to check for reduced group name in repository", e); + } + } + @Override public List getAllGroups(Integer limit, Integer offset, Boolean reverse) throws DataException { StringBuilder sql = new StringBuilder(512); sql.append("SELECT group_id, owner, group_name, description, created_when, updated_when, reference, is_open, " - + "approval_threshold, min_block_delay, max_block_delay, creation_group_id FROM Groups ORDER BY group_name"); + + "approval_threshold, min_block_delay, max_block_delay, creation_group_id, reduced_group_name " + + "FROM Groups ORDER BY group_name"); if (reverse != null && reverse) sql.append(" DESC"); @@ -154,9 +168,10 @@ public class HSQLDBGroupRepository implements GroupRepository { int maxBlockDelay = resultSet.getInt(11); int creationGroupId = resultSet.getInt(12); + String reducedGroupName = resultSet.getString(13); groups.add(new GroupData(groupId, owner, groupName, description, created, updated, isOpen, - approvalThreshold, minBlockDelay, maxBlockDelay, reference, creationGroupId)); + approvalThreshold, minBlockDelay, maxBlockDelay, reference, creationGroupId, reducedGroupName)); } while (resultSet.next()); return groups; @@ -170,7 +185,8 @@ public class HSQLDBGroupRepository implements GroupRepository { StringBuilder sql = new StringBuilder(512); sql.append("SELECT group_id, group_name, description, created_when, updated_when, reference, is_open, " - + "approval_threshold, min_block_delay, max_block_delay, creation_group_id FROM Groups WHERE owner = ? ORDER BY group_name"); + + "approval_threshold, min_block_delay, max_block_delay, creation_group_id, reduced_group_name " + + "FROM Groups WHERE owner = ? ORDER BY group_name"); if (reverse != null && reverse) sql.append(" DESC"); @@ -203,9 +219,10 @@ public class HSQLDBGroupRepository implements GroupRepository { int maxBlockDelay = resultSet.getInt(10); int creationGroupId = resultSet.getInt(11); + String reducedGroupName = resultSet.getString(12); groups.add(new GroupData(groupId, owner, groupName, description, created, updated, isOpen, - approvalThreshold, minBlockDelay, maxBlockDelay, reference, creationGroupId)); + approvalThreshold, minBlockDelay, maxBlockDelay, reference, creationGroupId, reducedGroupName)); } while (resultSet.next()); return groups; @@ -219,7 +236,7 @@ public class HSQLDBGroupRepository implements GroupRepository { StringBuilder sql = new StringBuilder(512); sql.append("SELECT group_id, owner, group_name, description, created_when, updated_when, reference, is_open, " - + "approval_threshold, min_block_delay, max_block_delay, creation_group_id, admin FROM Groups " + + "approval_threshold, min_block_delay, max_block_delay, creation_group_id, reduced_group_name, admin FROM Groups " + "JOIN GroupMembers USING (group_id) " + "LEFT OUTER JOIN GroupAdmins ON GroupAdmins.group_id = GroupMembers.group_id AND GroupAdmins.admin = GroupMembers.address " + "WHERE address = ? ORDER BY group_name"); @@ -256,12 +273,13 @@ public class HSQLDBGroupRepository implements GroupRepository { int maxBlockDelay = resultSet.getInt(11); int creationGroupId = resultSet.getInt(12); + String reducedGroupName = resultSet.getString(13); - resultSet.getString(13); // 'admin' + resultSet.getString(14); // 'admin' boolean isAdmin = !resultSet.wasNull(); GroupData groupData = new GroupData(groupId, owner, groupName, description, created, updated, isOpen, - approvalThreshold, minBlockDelay, maxBlockDelay, reference, creationGroupId); + approvalThreshold, minBlockDelay, maxBlockDelay, reference, creationGroupId, reducedGroupName); groupData.setIsAdmin(isAdmin); @@ -280,9 +298,9 @@ public class HSQLDBGroupRepository implements GroupRepository { saveHelper.bind("group_id", groupData.getGroupId()).bind("owner", groupData.getOwner()).bind("group_name", groupData.getGroupName()) .bind("description", groupData.getDescription()).bind("created_when", groupData.getCreated()).bind("updated_when", groupData.getUpdated()) - .bind("reference", groupData.getReference()).bind("is_open", groupData.getIsOpen()).bind("approval_threshold", groupData.getApprovalThreshold().value) + .bind("reference", groupData.getReference()).bind("is_open", groupData.isOpen()).bind("approval_threshold", groupData.getApprovalThreshold().value) .bind("min_block_delay", groupData.getMinimumBlockDelay()).bind("max_block_delay", groupData.getMaximumBlockDelay()) - .bind("creation_group_id", groupData.getCreationGroupId()); + .bind("creation_group_id", groupData.getCreationGroupId()).bind("reduced_group_name", groupData.getReducedGroupName()); try { saveHelper.execute(this.repository); diff --git a/src/main/java/org/qortal/repository/hsqldb/transaction/HSQLDBCreateGroupTransactionRepository.java b/src/main/java/org/qortal/repository/hsqldb/transaction/HSQLDBCreateGroupTransactionRepository.java index fb20c504..583b9fa6 100644 --- a/src/main/java/org/qortal/repository/hsqldb/transaction/HSQLDBCreateGroupTransactionRepository.java +++ b/src/main/java/org/qortal/repository/hsqldb/transaction/HSQLDBCreateGroupTransactionRepository.java @@ -18,28 +18,30 @@ public class HSQLDBCreateGroupTransactionRepository extends HSQLDBTransactionRep } TransactionData fromBase(BaseTransactionData baseTransactionData) throws DataException { - String sql = "SELECT owner, group_name, description, is_open, approval_threshold, min_block_delay, max_block_delay, group_id FROM CreateGroupTransactions WHERE signature = ?"; + String sql = "SELECT group_name, description, is_open, approval_threshold, min_block_delay, max_block_delay, group_id, reduced_group_name " + + "FROM CreateGroupTransactions WHERE signature = ?"; try (ResultSet resultSet = this.repository.checkedExecute(sql, baseTransactionData.getSignature())) { if (resultSet == null) return null; - String owner = resultSet.getString(1); - String groupName = resultSet.getString(2); - String description = resultSet.getString(3); - boolean isOpen = resultSet.getBoolean(4); + String groupName = resultSet.getString(1); + String description = resultSet.getString(2); + boolean isOpen = resultSet.getBoolean(3); - ApprovalThreshold approvalThreshold = ApprovalThreshold.valueOf(resultSet.getInt(5)); + ApprovalThreshold approvalThreshold = ApprovalThreshold.valueOf(resultSet.getInt(4)); - int minBlockDelay = resultSet.getInt(6); - int maxBlockDelay = resultSet.getInt(7); + int minBlockDelay = resultSet.getInt(5); + int maxBlockDelay = resultSet.getInt(6); - Integer groupId = resultSet.getInt(8); + Integer groupId = resultSet.getInt(7); if (groupId == 0 && resultSet.wasNull()) groupId = null; - return new CreateGroupTransactionData(baseTransactionData, owner, groupName, description, isOpen, approvalThreshold, - minBlockDelay, maxBlockDelay, groupId); + String reducedGroupName = resultSet.getString(8); + + return new CreateGroupTransactionData(baseTransactionData, groupName, description, isOpen, approvalThreshold, + minBlockDelay, maxBlockDelay, groupId, reducedGroupName); } catch (SQLException e) { throw new DataException("Unable to fetch create group transaction from repository", e); } @@ -52,8 +54,8 @@ public class HSQLDBCreateGroupTransactionRepository extends HSQLDBTransactionRep HSQLDBSaver saveHelper = new HSQLDBSaver("CreateGroupTransactions"); saveHelper.bind("signature", createGroupTransactionData.getSignature()).bind("creator", createGroupTransactionData.getCreatorPublicKey()) - .bind("owner", createGroupTransactionData.getOwner()).bind("group_name", createGroupTransactionData.getGroupName()) - .bind("description", createGroupTransactionData.getDescription()).bind("is_open", createGroupTransactionData.getIsOpen()) + .bind("group_name", createGroupTransactionData.getGroupName()).bind("reduced_group_name", createGroupTransactionData.getReducedGroupName()) + .bind("description", createGroupTransactionData.getDescription()).bind("is_open", createGroupTransactionData.isOpen()) .bind("approval_threshold", createGroupTransactionData.getApprovalThreshold().value) .bind("min_block_delay", createGroupTransactionData.getMinimumBlockDelay()) .bind("max_block_delay", createGroupTransactionData.getMaximumBlockDelay()).bind("group_id", createGroupTransactionData.getGroupId()); diff --git a/src/main/java/org/qortal/transaction/CreateGroupTransaction.java b/src/main/java/org/qortal/transaction/CreateGroupTransaction.java index 0240bd90..17a0b052 100644 --- a/src/main/java/org/qortal/transaction/CreateGroupTransaction.java +++ b/src/main/java/org/qortal/transaction/CreateGroupTransaction.java @@ -5,12 +5,12 @@ import java.util.List; import org.qortal.account.Account; import org.qortal.asset.Asset; -import org.qortal.crypto.Crypto; import org.qortal.data.transaction.CreateGroupTransactionData; import org.qortal.data.transaction.TransactionData; import org.qortal.group.Group; import org.qortal.repository.DataException; import org.qortal.repository.Repository; +import org.qortal.utils.Unicode; import com.google.common.base.Utf8; @@ -31,7 +31,7 @@ public class CreateGroupTransaction extends Transaction { @Override public List getRecipientAddresses() throws DataException { - return Collections.singletonList(this.createGroupTransactionData.getOwner()); + return Collections.emptyList(); } // Navigation @@ -40,14 +40,19 @@ public class CreateGroupTransaction extends Transaction { return this.getCreator(); } + private synchronized String getReducedGroupName() { + if (this.createGroupTransactionData.getReducedGroupName() == null) { + String reducedGroupName = Group.reduceName(this.createGroupTransactionData.getGroupName()); + this.createGroupTransactionData.setReducedGroupName(reducedGroupName); + } + + return this.createGroupTransactionData.getReducedGroupName(); + } + // Processing @Override public ValidationResult isValid() throws DataException { - // Check owner address is valid - if (!Crypto.isValidAddress(this.createGroupTransactionData.getOwner())) - return ValidationResult.INVALID_ADDRESS; - // Check approval threshold is valid if (this.createGroupTransactionData.getApprovalThreshold() == null) return ValidationResult.INVALID_GROUP_APPROVAL_THRESHOLD; @@ -62,9 +67,11 @@ public class CreateGroupTransaction extends Transaction { if (this.createGroupTransactionData.getMaximumBlockDelay() < this.createGroupTransactionData.getMinimumBlockDelay()) return ValidationResult.INVALID_GROUP_BLOCK_DELAY; + String groupName = this.createGroupTransactionData.getGroupName(); + // Check group name size bounds - int groupNameLength = Utf8.encodedLength(this.createGroupTransactionData.getGroupName()); - if (groupNameLength < 1 || groupNameLength > Group.MAX_NAME_SIZE) + int groupNameLength = Utf8.encodedLength(groupName); + if (groupNameLength < Group.MIN_NAME_SIZE || groupNameLength > Group.MAX_NAME_SIZE) return ValidationResult.INVALID_NAME_LENGTH; // Check description size bounds @@ -72,8 +79,8 @@ public class CreateGroupTransaction extends Transaction { if (descriptionLength < 1 || descriptionLength > Group.MAX_DESCRIPTION_SIZE) return ValidationResult.INVALID_DESCRIPTION_LENGTH; - // Check group name is lowercase - if (!this.createGroupTransactionData.getGroupName().equals(this.createGroupTransactionData.getGroupName().toLowerCase())) + // Check name is in normalized form (no leading/trailing whitespace, etc.) + if (!groupName.equals(Unicode.normalize(groupName))) return ValidationResult.NAME_NOT_LOWER_CASE; Account creator = getCreator(); @@ -82,13 +89,16 @@ public class CreateGroupTransaction extends Transaction { if (creator.getConfirmedBalance(Asset.QORT) < this.createGroupTransactionData.getFee()) return ValidationResult.NO_BALANCE; + // Fill in missing reduced name. Caller is likely to save this as next step. + getReducedGroupName(); + return ValidationResult.OK; } @Override public ValidationResult isProcessable() throws DataException { // Check the group name isn't already taken - if (this.repository.getGroupRepository().groupExists(this.createGroupTransactionData.getGroupName())) + if (this.repository.getGroupRepository().reducedGroupNameExists(getReducedGroupName())) return ValidationResult.GROUP_ALREADY_EXISTS; return ValidationResult.OK; diff --git a/src/main/java/org/qortal/transform/transaction/CreateGroupTransactionTransformer.java b/src/main/java/org/qortal/transform/transaction/CreateGroupTransactionTransformer.java index 8a3d16f0..29afba9d 100644 --- a/src/main/java/org/qortal/transform/transaction/CreateGroupTransactionTransformer.java +++ b/src/main/java/org/qortal/transform/transaction/CreateGroupTransactionTransformer.java @@ -20,14 +20,13 @@ import com.google.common.primitives.Longs; public class CreateGroupTransactionTransformer extends TransactionTransformer { // Property lengths - private static final int OWNER_LENGTH = ADDRESS_LENGTH; private static final int NAME_SIZE_LENGTH = INT_LENGTH; private static final int DESCRIPTION_SIZE_LENGTH = INT_LENGTH; private static final int IS_OPEN_LENGTH = BOOLEAN_LENGTH; private static final int APPROVAL_THRESHOLD_LENGTH = BYTE_LENGTH; private static final int BLOCK_DELAY_LENGTH = INT_LENGTH; - private static final int EXTRAS_LENGTH = OWNER_LENGTH + NAME_SIZE_LENGTH + DESCRIPTION_SIZE_LENGTH + IS_OPEN_LENGTH + private static final int EXTRAS_LENGTH = NAME_SIZE_LENGTH + DESCRIPTION_SIZE_LENGTH + IS_OPEN_LENGTH + APPROVAL_THRESHOLD_LENGTH + BLOCK_DELAY_LENGTH + BLOCK_DELAY_LENGTH; protected static final TransactionLayout layout; @@ -39,7 +38,6 @@ public class CreateGroupTransactionTransformer extends TransactionTransformer { layout.add("transaction's groupID", TransformationType.INT); layout.add("reference", TransformationType.SIGNATURE); layout.add("group creator's public key", TransformationType.PUBLIC_KEY); - layout.add("group owner's address", TransformationType.ADDRESS); layout.add("group's name length", TransformationType.INT); layout.add("group's name", TransformationType.STRING); layout.add("group's description length", TransformationType.INT); @@ -62,8 +60,6 @@ public class CreateGroupTransactionTransformer extends TransactionTransformer { byte[] creatorPublicKey = Serialization.deserializePublicKey(byteBuffer); - String owner = Serialization.deserializeAddress(byteBuffer); - String groupName = Serialization.deserializeSizedString(byteBuffer, Group.MAX_NAME_SIZE); String description = Serialization.deserializeSizedString(byteBuffer, Group.MAX_DESCRIPTION_SIZE); @@ -83,7 +79,7 @@ public class CreateGroupTransactionTransformer extends TransactionTransformer { BaseTransactionData baseTransactionData = new BaseTransactionData(timestamp, txGroupId, reference, creatorPublicKey, fee, signature); - return new CreateGroupTransactionData(baseTransactionData, owner, groupName, description, isOpen, approvalThreshold, minBlockDelay, maxBlockDelay); + return new CreateGroupTransactionData(baseTransactionData, groupName, description, isOpen, approvalThreshold, minBlockDelay, maxBlockDelay); } public static int getDataLength(TransactionData transactionData) throws TransformationException { @@ -101,13 +97,11 @@ public class CreateGroupTransactionTransformer extends TransactionTransformer { transformCommonBytes(transactionData, bytes); - Serialization.serializeAddress(bytes, createGroupTransactionData.getOwner()); - Serialization.serializeSizedString(bytes, createGroupTransactionData.getGroupName()); Serialization.serializeSizedString(bytes, createGroupTransactionData.getDescription()); - bytes.write((byte) (createGroupTransactionData.getIsOpen() ? 1 : 0)); + bytes.write((byte) (createGroupTransactionData.isOpen() ? 1 : 0)); bytes.write((byte) createGroupTransactionData.getApprovalThreshold().value); diff --git a/src/test/java/org/qortal/test/common/GroupUtils.java b/src/test/java/org/qortal/test/common/GroupUtils.java index a026b605..19d229fd 100644 --- a/src/test/java/org/qortal/test/common/GroupUtils.java +++ b/src/test/java/org/qortal/test/common/GroupUtils.java @@ -27,7 +27,7 @@ public class GroupUtils { String groupDescription = groupName + " (test group)"; BaseTransactionData baseTransactionData = new BaseTransactionData(timestamp, Group.NO_GROUP, reference, account.getPublicKey(), GroupUtils.fee, null); - TransactionData transactionData = new CreateGroupTransactionData(baseTransactionData, account.getAddress(), groupName, groupDescription, isOpen, approvalThreshold, minimumBlockDelay, maximumBlockDelay); + TransactionData transactionData = new CreateGroupTransactionData(baseTransactionData, groupName, groupDescription, isOpen, approvalThreshold, minimumBlockDelay, maximumBlockDelay); TransactionUtils.signAndMint(repository, transactionData, account); diff --git a/src/test/java/org/qortal/test/common/transaction/CreateGroupTestTransaction.java b/src/test/java/org/qortal/test/common/transaction/CreateGroupTestTransaction.java index c81527c7..a68c7aeb 100644 --- a/src/test/java/org/qortal/test/common/transaction/CreateGroupTestTransaction.java +++ b/src/test/java/org/qortal/test/common/transaction/CreateGroupTestTransaction.java @@ -14,7 +14,6 @@ public class CreateGroupTestTransaction extends TestTransaction { public static TransactionData randomTransaction(Repository repository, PrivateKeyAccount account, boolean wantValid) throws DataException { Random random = new Random(); - String owner = account.getAddress(); String groupName = "test group " + random.nextInt(1_000_000); String description = "random test group"; final boolean isOpen = false; @@ -22,7 +21,7 @@ public class CreateGroupTestTransaction extends TestTransaction { final int minimumBlockDelay = 5; final int maximumBlockDelay = 20; - return new CreateGroupTransactionData(generateBase(account), owner, groupName, description, isOpen, approvalThreshold, minimumBlockDelay, maximumBlockDelay); + return new CreateGroupTransactionData(generateBase(account), groupName, description, isOpen, approvalThreshold, minimumBlockDelay, maximumBlockDelay); } } diff --git a/src/test/java/org/qortal/test/group/GroupBlockDelayTests.java b/src/test/java/org/qortal/test/group/GroupBlockDelayTests.java index 1af2857f..b4375fe0 100644 --- a/src/test/java/org/qortal/test/group/GroupBlockDelayTests.java +++ b/src/test/java/org/qortal/test/group/GroupBlockDelayTests.java @@ -60,13 +60,12 @@ public class GroupBlockDelayTests extends Common { } private CreateGroupTransaction buildCreateGroupWithDelays(Repository repository, PrivateKeyAccount account, int minimumBlockDelay, int maximumBlockDelay) throws DataException { - String owner = account.getAddress(); String groupName = "test group"; String description = "random test group"; final boolean isOpen = false; ApprovalThreshold approvalThreshold = ApprovalThreshold.PCT40; - CreateGroupTransactionData transactionData = new CreateGroupTransactionData(TestTransaction.generateBase(account), owner, groupName, description, isOpen, approvalThreshold, minimumBlockDelay, maximumBlockDelay); + CreateGroupTransactionData transactionData = new CreateGroupTransactionData(TestTransaction.generateBase(account), groupName, description, isOpen, approvalThreshold, minimumBlockDelay, maximumBlockDelay); return new CreateGroupTransaction(repository, transactionData); } diff --git a/src/test/java/org/qortal/test/group/MiscTests.java b/src/test/java/org/qortal/test/group/MiscTests.java new file mode 100644 index 00000000..95808d14 --- /dev/null +++ b/src/test/java/org/qortal/test/group/MiscTests.java @@ -0,0 +1,59 @@ +package org.qortal.test.group; + +import static org.junit.Assert.assertTrue; + +import org.junit.After; +import org.junit.Before; +import org.junit.Test; +import org.qortal.account.PrivateKeyAccount; +import org.qortal.data.transaction.CreateGroupTransactionData; +import org.qortal.group.Group.ApprovalThreshold; +import org.qortal.repository.DataException; +import org.qortal.repository.Repository; +import org.qortal.repository.RepositoryManager; +import org.qortal.test.common.Common; +import org.qortal.test.common.TransactionUtils; +import org.qortal.test.common.transaction.TestTransaction; +import org.qortal.transaction.Transaction; +import org.qortal.transaction.Transaction.ValidationResult; + +public class MiscTests extends Common { + + @Before + public void beforeTest() throws DataException { + Common.useDefaultSettings(); + } + + @After + public void afterTest() throws DataException { + Common.orphanCheck(); + } + + @Test + public void testCreateGroupWithExistingName() throws DataException { + try (final Repository repository = RepositoryManager.getRepository()) { + // Register-name + PrivateKeyAccount alice = Common.getTestAccount(repository, "alice"); + String groupName = "test-group"; + String description = "test group"; + + final boolean isOpen = false; + ApprovalThreshold approvalThreshold = ApprovalThreshold.PCT40; + int minimumBlockDelay = 10; + int maximumBlockDelay = 1440; + + CreateGroupTransactionData transactionData = new CreateGroupTransactionData(TestTransaction.generateBase(alice), groupName, description, isOpen, approvalThreshold, minimumBlockDelay, maximumBlockDelay); + TransactionUtils.signAndMint(repository, transactionData, alice); + + // duplicate + String duplicateGroupName = "TEST-gr0up"; + transactionData = new CreateGroupTransactionData(TestTransaction.generateBase(alice), duplicateGroupName, description, isOpen, approvalThreshold, minimumBlockDelay, maximumBlockDelay); + Transaction transaction = Transaction.fromData(repository, transactionData); + transaction.sign(alice); + + ValidationResult result = transaction.importAsUnconfirmed(); + assertTrue("Transaction should be invalid", ValidationResult.OK != result); + } + } + +} diff --git a/src/test/resources/test-chain-v2-qora-holder-extremes.json b/src/test/resources/test-chain-v2-qora-holder-extremes.json index 2bd65c5f..2962f7a7 100644 --- a/src/test/resources/test-chain-v2-qora-holder-extremes.json +++ b/src/test/resources/test-chain-v2-qora-holder-extremes.json @@ -62,7 +62,7 @@ { "type": "GENESIS", "recipient": "QaUpHNhT3Ygx6avRiKobuLdusppR5biXjL", "amount": "637557960.49687541", "assetId": 1 }, { "type": "GENESIS", "recipient": "Qci5m9k4rcwe4ruKrZZQKka4FzUUMut3er", "amount": "0.666", "assetId": 1 }, - { "type": "CREATE_GROUP", "creatorPublicKey": "2tiMr5LTpaWCgbRvkPK8TFd7k63DyHJMMFFsz9uBf1ZP", "owner": "QgV4s3xnzLhVBEJxcYui4u4q11yhUHsd9v", "groupName": "dev-group", "description": "developer group", "isOpen": false, "approvalThreshold": "PCT100", "minimumBlockDelay": 0, "maximumBlockDelay": 1440 }, + { "type": "CREATE_GROUP", "creatorPublicKey": "2tiMr5LTpaWCgbRvkPK8TFd7k63DyHJMMFFsz9uBf1ZP", "groupName": "dev-group", "description": "developer group", "isOpen": false, "approvalThreshold": "PCT100", "minimumBlockDelay": 0, "maximumBlockDelay": 1440 }, { "type": "ISSUE_ASSET", "issuerPublicKey": "2tiMr5LTpaWCgbRvkPK8TFd7k63DyHJMMFFsz9uBf1ZP", "assetName": "TEST", "description": "test asset", "data": "", "quantity": 1000000, "isDivisible": true, "fee": 0 }, { "type": "ISSUE_ASSET", "issuerPublicKey": "C6wuddsBV3HzRrXUtezE7P5MoRXp5m3mEDokRDGZB6ry", "assetName": "OTHER", "description": "other test asset", "data": "", "quantity": 1000000, "isDivisible": true, "fee": 0 }, diff --git a/src/test/resources/test-chain-v2-qora-holder.json b/src/test/resources/test-chain-v2-qora-holder.json index 63186eb9..11ccb0b0 100644 --- a/src/test/resources/test-chain-v2-qora-holder.json +++ b/src/test/resources/test-chain-v2-qora-holder.json @@ -62,7 +62,7 @@ { "type": "GENESIS", "recipient": "QaUpHNhT3Ygx6avRiKobuLdusppR5biXjL", "amount": "10000", "assetId": 1 }, { "type": "GENESIS", "recipient": "Qci5m9k4rcwe4ruKrZZQKka4FzUUMut3er", "amount": "8", "assetId": 1 }, - { "type": "CREATE_GROUP", "creatorPublicKey": "2tiMr5LTpaWCgbRvkPK8TFd7k63DyHJMMFFsz9uBf1ZP", "owner": "QgV4s3xnzLhVBEJxcYui4u4q11yhUHsd9v", "groupName": "dev-group", "description": "developer group", "isOpen": false, "approvalThreshold": "PCT100", "minimumBlockDelay": 0, "maximumBlockDelay": 1440 }, + { "type": "CREATE_GROUP", "creatorPublicKey": "2tiMr5LTpaWCgbRvkPK8TFd7k63DyHJMMFFsz9uBf1ZP", "groupName": "dev-group", "description": "developer group", "isOpen": false, "approvalThreshold": "PCT100", "minimumBlockDelay": 0, "maximumBlockDelay": 1440 }, { "type": "ISSUE_ASSET", "issuerPublicKey": "2tiMr5LTpaWCgbRvkPK8TFd7k63DyHJMMFFsz9uBf1ZP", "assetName": "TEST", "description": "test asset", "data": "", "quantity": 1000000, "isDivisible": true, "fee": 0 }, { "type": "ISSUE_ASSET", "issuerPublicKey": "C6wuddsBV3HzRrXUtezE7P5MoRXp5m3mEDokRDGZB6ry", "assetName": "OTHER", "description": "other test asset", "data": "", "quantity": 1000000, "isDivisible": true, "fee": 0 }, diff --git a/src/test/resources/test-chain-v2.json b/src/test/resources/test-chain-v2.json index 6ab9002b..1939f357 100644 --- a/src/test/resources/test-chain-v2.json +++ b/src/test/resources/test-chain-v2.json @@ -59,7 +59,7 @@ { "type": "GENESIS", "recipient": "QaUpHNhT3Ygx6avRiKobuLdusppR5biXjL", "amount": "1000000" }, { "type": "GENESIS", "recipient": "Qci5m9k4rcwe4ruKrZZQKka4FzUUMut3er", "amount": "1000000" }, - { "type": "CREATE_GROUP", "creatorPublicKey": "2tiMr5LTpaWCgbRvkPK8TFd7k63DyHJMMFFsz9uBf1ZP", "owner": "QgV4s3xnzLhVBEJxcYui4u4q11yhUHsd9v", "groupName": "dev-group", "description": "developer group", "isOpen": false, "approvalThreshold": "PCT100", "minimumBlockDelay": 0, "maximumBlockDelay": 1440 }, + { "type": "CREATE_GROUP", "creatorPublicKey": "2tiMr5LTpaWCgbRvkPK8TFd7k63DyHJMMFFsz9uBf1ZP", "groupName": "dev-group", "description": "developer group", "isOpen": false, "approvalThreshold": "PCT100", "minimumBlockDelay": 0, "maximumBlockDelay": 1440 }, { "type": "ISSUE_ASSET", "issuerPublicKey": "2tiMr5LTpaWCgbRvkPK8TFd7k63DyHJMMFFsz9uBf1ZP", "assetName": "TEST", "description": "test asset", "data": "", "quantity": "1000000", "isDivisible": true, "fee": 0 }, { "type": "ISSUE_ASSET", "issuerPublicKey": "C6wuddsBV3HzRrXUtezE7P5MoRXp5m3mEDokRDGZB6ry", "assetName": "OTHER", "description": "other test asset", "data": "", "quantity": "1000000", "isDivisible": true, "fee": 0 },