From 02a620d57bebbbc4dcd83d4eb342d9c37830f7f1 Mon Sep 17 00:00:00 2001 From: catbref Date: Fri, 11 Jan 2019 10:42:42 +0000 Subject: [PATCH] Join/Leave account groups + API calls --- .../qora/api/model/GroupWithMemberInfo.java | 10 +- .../org/qora/api/resource/GroupsResource.java | 53 ++++++- .../org/qora/data/group/GroupAdminData.java | 16 +- .../java/org/qora/data/group/GroupData.java | 4 + .../org/qora/data/group/GroupMemberData.java | 16 +- .../transaction/JoinGroupTransactionData.java | 2 +- .../LeaveGroupTransactionData.java | 89 +++++++++++ .../data/transaction/TransactionData.java | 2 +- src/main/java/org/qora/group/Group.java | 109 +++++++++++-- .../org/qora/repository/GroupRepository.java | 12 +- .../hsqldb/HSQLDBDatabaseUpdates.java | 8 +- .../hsqldb/HSQLDBGroupRepository.java | 79 ++++++++-- ...HSQLDBLeaveGroupTransactionRepository.java | 52 +++++++ .../HSQLDBTransactionRepository.java | 9 ++ .../transaction/CreateGroupTransaction.java | 2 +- .../transaction/LeaveGroupTransaction.java | 143 ++++++++++++++++++ .../org/qora/transaction/Transaction.java | 5 + .../LeaveGroupTransactionTransformer.java | 99 ++++++++++++ .../transaction/TransactionTransformer.java | 15 ++ 19 files changed, 676 insertions(+), 49 deletions(-) create mode 100644 src/main/java/org/qora/data/transaction/LeaveGroupTransactionData.java create mode 100644 src/main/java/org/qora/repository/hsqldb/transaction/HSQLDBLeaveGroupTransactionRepository.java create mode 100644 src/main/java/org/qora/transaction/LeaveGroupTransaction.java create mode 100644 src/main/java/org/qora/transform/transaction/LeaveGroupTransactionTransformer.java diff --git a/src/main/java/org/qora/api/model/GroupWithMemberInfo.java b/src/main/java/org/qora/api/model/GroupWithMemberInfo.java index 4846a0da..85c669d4 100644 --- a/src/main/java/org/qora/api/model/GroupWithMemberInfo.java +++ b/src/main/java/org/qora/api/model/GroupWithMemberInfo.java @@ -6,7 +6,6 @@ import javax.xml.bind.annotation.XmlAccessType; import javax.xml.bind.annotation.XmlAccessorType; import javax.xml.bind.annotation.XmlElement; -import org.qora.data.group.GroupAdminData; import org.qora.data.group.GroupData; import org.qora.data.group.GroupMemberData; @@ -23,16 +22,19 @@ public class GroupWithMemberInfo { Integer memberCount; - public List groupAdmins; + @XmlElement(name = "admins") + public List groupAdminAddresses; + + @XmlElement(name = "members") public List groupMembers; // For JAX-RS protected GroupWithMemberInfo() { } - public GroupWithMemberInfo(GroupData groupData, List groupAdmins, List groupMembers, Integer memberCount) { + public GroupWithMemberInfo(GroupData groupData, List groupAdminAddresses, List groupMembers, Integer memberCount) { this.groupData = groupData; - this.groupAdmins = groupAdmins; + this.groupAdminAddresses = groupAdminAddresses; this.groupMembers = groupMembers; this.memberCount = memberCount; } diff --git a/src/main/java/org/qora/api/resource/GroupsResource.java b/src/main/java/org/qora/api/resource/GroupsResource.java index 7eaf175c..5532091a 100644 --- a/src/main/java/org/qora/api/resource/GroupsResource.java +++ b/src/main/java/org/qora/api/resource/GroupsResource.java @@ -32,6 +32,7 @@ import org.qora.data.group.GroupData; import org.qora.data.group.GroupMemberData; import org.qora.data.transaction.CreateGroupTransactionData; import org.qora.data.transaction.JoinGroupTransactionData; +import org.qora.data.transaction.LeaveGroupTransactionData; import org.qora.data.transaction.UpdateGroupTransactionData; import org.qora.repository.DataException; import org.qora.repository.Repository; @@ -41,6 +42,7 @@ import org.qora.transaction.Transaction.ValidationResult; import org.qora.transform.TransformationException; import org.qora.transform.transaction.CreateGroupTransactionTransformer; import org.qora.transform.transaction.JoinGroupTransactionTransformer; +import org.qora.transform.transaction.LeaveGroupTransactionTransformer; import org.qora.transform.transaction.UpdateGroupTransactionTransformer; import org.qora.utils.Base58; @@ -137,7 +139,7 @@ public class GroupsResource { groupMembers = repository.getGroupRepository().getAllGroupMembers(groupData.getGroupName()); // Strip groupName from member info - groupMembers = groupMembers.stream().map(groupMemberData -> new GroupMemberData(null, groupMemberData.getMember(), groupMemberData.getJoined())).collect(Collectors.toList()); + groupMembers = groupMembers.stream().map(groupMemberData -> new GroupMemberData(null, groupMemberData.getMember(), groupMemberData.getJoined(), null)).collect(Collectors.toList()); memberCount = groupMembers.size(); } else { @@ -148,10 +150,10 @@ public class GroupsResource { // Always include admins List groupAdmins = repository.getGroupRepository().getAllGroupAdmins(groupData.getGroupName()); - // Strip groupName from admin info - groupAdmins = groupAdmins.stream().map(groupAdminData -> new GroupAdminData(null, groupAdminData.getAdmin())).collect(Collectors.toList()); + // We only need admin addresses + List groupAdminAddresses = groupAdmins.stream().map(groupAdminData -> groupAdminData.getAdmin()).collect(Collectors.toList()); - return new GroupWithMemberInfo(groupData, groupAdmins, groupMembers, memberCount); + return new GroupWithMemberInfo(groupData, groupAdminAddresses, groupMembers, memberCount); } catch (DataException e) { throw ApiExceptionFactory.INSTANCE.createException(request, ApiError.REPOSITORY_ISSUE, e); } @@ -287,4 +289,47 @@ public class GroupsResource { } } + @POST + @Path("/leave") + @Operation( + summary = "Build raw, unsigned, LEAVE_GROUP transaction", + requestBody = @RequestBody( + required = true, + content = @Content( + mediaType = MediaType.APPLICATION_JSON, + schema = @Schema( + implementation = LeaveGroupTransactionData.class + ) + ) + ), + responses = { + @ApiResponse( + description = "raw, unsigned, LEAVE_GROUP transaction encoded in Base58", + content = @Content( + mediaType = MediaType.TEXT_PLAIN, + schema = @Schema( + type = "string" + ) + ) + ) + } + ) + @ApiErrors({ApiError.TRANSACTION_INVALID, ApiError.TRANSFORMATION_ERROR, ApiError.REPOSITORY_ISSUE}) + public String leaveGroup(LeaveGroupTransactionData transactionData) { + try (final Repository repository = RepositoryManager.getRepository()) { + Transaction transaction = Transaction.fromData(repository, transactionData); + + ValidationResult result = transaction.isValidUnconfirmed(); + if (result != ValidationResult.OK) + throw TransactionsResource.createTransactionInvalidException(request, result); + + byte[] bytes = LeaveGroupTransactionTransformer.toBytes(transactionData); + return Base58.encode(bytes); + } catch (TransformationException e) { + throw ApiExceptionFactory.INSTANCE.createException(request, ApiError.TRANSFORMATION_ERROR, e); + } catch (DataException e) { + throw ApiExceptionFactory.INSTANCE.createException(request, ApiError.REPOSITORY_ISSUE, e); + } + } + } \ No newline at end of file diff --git a/src/main/java/org/qora/data/group/GroupAdminData.java b/src/main/java/org/qora/data/group/GroupAdminData.java index 2e513350..e97347a6 100644 --- a/src/main/java/org/qora/data/group/GroupAdminData.java +++ b/src/main/java/org/qora/data/group/GroupAdminData.java @@ -2,6 +2,7 @@ package org.qora.data.group; import javax.xml.bind.annotation.XmlAccessType; import javax.xml.bind.annotation.XmlAccessorType; +import javax.xml.bind.annotation.XmlTransient; // All properties to be converted to JSON via JAX-RS @XmlAccessorType(XmlAccessType.FIELD) @@ -10,6 +11,10 @@ public class GroupAdminData { // Properties private String groupName; private String admin; + /** Reference to transaction that triggered adminship */ + // No need to ever expose this via API + @XmlTransient + private byte[] groupReference; // Constructors @@ -17,9 +22,10 @@ public class GroupAdminData { protected GroupAdminData() { } - public GroupAdminData(String groupName, String admin) { + public GroupAdminData(String groupName, String admin, byte[] groupReference) { this.groupName = groupName; this.admin = admin; + this.groupReference = groupReference; } // Getters / setters @@ -32,4 +38,12 @@ public class GroupAdminData { return this.admin; } + public byte[] getGroupReference() { + return this.groupReference; + } + + public void setGroupReference(byte[] groupReference) { + this.groupReference = groupReference; + } + } diff --git a/src/main/java/org/qora/data/group/GroupData.java b/src/main/java/org/qora/data/group/GroupData.java index 58b532f9..7b777182 100644 --- a/src/main/java/org/qora/data/group/GroupData.java +++ b/src/main/java/org/qora/data/group/GroupData.java @@ -2,6 +2,7 @@ package org.qora.data.group; import javax.xml.bind.annotation.XmlAccessType; import javax.xml.bind.annotation.XmlAccessorType; +import javax.xml.bind.annotation.XmlTransient; // All properties to be converted to JSON via JAX-RS @XmlAccessorType(XmlAccessType.FIELD) @@ -14,6 +15,9 @@ public class GroupData { private long created; private Long updated; private boolean isOpen; + /** Reference to transaction that created group */ + // No need to ever expose this via API + @XmlTransient private byte[] reference; // Constructors diff --git a/src/main/java/org/qora/data/group/GroupMemberData.java b/src/main/java/org/qora/data/group/GroupMemberData.java index 1e38c4f1..f81adac2 100644 --- a/src/main/java/org/qora/data/group/GroupMemberData.java +++ b/src/main/java/org/qora/data/group/GroupMemberData.java @@ -2,6 +2,7 @@ package org.qora.data.group; import javax.xml.bind.annotation.XmlAccessType; import javax.xml.bind.annotation.XmlAccessorType; +import javax.xml.bind.annotation.XmlTransient; // All properties to be converted to JSON via JAX-RS @XmlAccessorType(XmlAccessType.FIELD) @@ -11,6 +12,10 @@ public class GroupMemberData { private String groupName; private String member; private long joined; + /** Reference to transaction that triggered membership */ + // No need to ever expose this via API + @XmlTransient + private byte[] groupReference; // Constructors @@ -18,10 +23,11 @@ public class GroupMemberData { protected GroupMemberData() { } - public GroupMemberData(String groupName, String member, long joined) { + public GroupMemberData(String groupName, String member, long joined, byte[] groupReference) { this.groupName = groupName; this.member = member; this.joined = joined; + this.groupReference = groupReference; } // Getters / setters @@ -38,4 +44,12 @@ public class GroupMemberData { return this.joined; } + public byte[] getGroupReference() { + return this.groupReference; + } + + public void setGroupReference(byte[] groupReference) { + this.groupReference = groupReference; + } + } diff --git a/src/main/java/org/qora/data/transaction/JoinGroupTransactionData.java b/src/main/java/org/qora/data/transaction/JoinGroupTransactionData.java index cf6e27bd..111701ac 100644 --- a/src/main/java/org/qora/data/transaction/JoinGroupTransactionData.java +++ b/src/main/java/org/qora/data/transaction/JoinGroupTransactionData.java @@ -18,7 +18,7 @@ public class JoinGroupTransactionData extends TransactionData { // Properties @Schema(description = "joiner's public key", example = "2tiMr5LTpaWCgbRvkPK8TFd7k63DyHJMMFFsz9uBf1ZP") private byte[] joinerPublicKey; - @Schema(description = "which group to update", example = "my-group") + @Schema(description = "which group to join", example = "my-group") private String groupName; // Constructors diff --git a/src/main/java/org/qora/data/transaction/LeaveGroupTransactionData.java b/src/main/java/org/qora/data/transaction/LeaveGroupTransactionData.java new file mode 100644 index 00000000..9570a6ce --- /dev/null +++ b/src/main/java/org/qora/data/transaction/LeaveGroupTransactionData.java @@ -0,0 +1,89 @@ +package org.qora.data.transaction; + +import java.math.BigDecimal; + +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.qora.transaction.Transaction.TransactionType; + +import io.swagger.v3.oas.annotations.media.Schema; + +// All properties to be converted to JSON via JAX-RS +@XmlAccessorType(XmlAccessType.FIELD) +@Schema(allOf = { TransactionData.class }) +public class LeaveGroupTransactionData extends TransactionData { + + // Properties + @Schema(description = "leaver's public key", example = "2tiMr5LTpaWCgbRvkPK8TFd7k63DyHJMMFFsz9uBf1ZP") + private byte[] leaverPublicKey; + @Schema(description = "which group to leave", example = "my-group") + private String groupName; + // No need to ever expose this via API + @XmlTransient + private byte[] memberReference; + // No need to ever expose this via API + @XmlTransient + private byte[] adminReference; + + // Constructors + + // For JAX-RS + protected LeaveGroupTransactionData() { + super(TransactionType.LEAVE_GROUP); + } + + public void afterUnmarshal(Unmarshaller u, Object parent) { + this.creatorPublicKey = this.leaverPublicKey; + } + + public LeaveGroupTransactionData(byte[] leaverPublicKey, String groupName, byte[] memberReference, byte[] adminReference, BigDecimal fee, long timestamp, byte[] reference, byte[] signature) { + super(TransactionType.LEAVE_GROUP, fee, leaverPublicKey, timestamp, reference, signature); + + this.leaverPublicKey = leaverPublicKey; + this.groupName = groupName; + this.memberReference = memberReference; + this.adminReference = adminReference; + } + + public LeaveGroupTransactionData(byte[] leaverPublicKey, String groupName, BigDecimal fee, long timestamp, byte[] reference, byte[] signature) { + this(leaverPublicKey, groupName, null, null, fee, timestamp, reference, signature); + } + + public LeaveGroupTransactionData(byte[] leaverPublicKey, String groupName, byte[] memberReference, byte[] adminReference, BigDecimal fee, long timestamp, byte[] reference) { + this(leaverPublicKey, groupName, memberReference, adminReference, fee, timestamp, reference, null); + } + + public LeaveGroupTransactionData(byte[] leaverPublicKey, String groupName, BigDecimal fee, long timestamp, byte[] reference) { + this(leaverPublicKey, groupName, null, null, fee, timestamp, reference, null); + } + + // Getters / setters + + public byte[] getLeaverPublicKey() { + return this.leaverPublicKey; + } + + public String getGroupName() { + return this.groupName; + } + + public byte[] getMemberReference() { + return this.memberReference; + } + + public void setMemberReference(byte[] memberReference) { + this.memberReference = memberReference; + } + + public byte[] getAdminReference() { + return this.adminReference; + } + + public void setAdminReference(byte[] adminReference) { + this.adminReference = adminReference; + } + +} diff --git a/src/main/java/org/qora/data/transaction/TransactionData.java b/src/main/java/org/qora/data/transaction/TransactionData.java index 49164a27..186fc5c2 100644 --- a/src/main/java/org/qora/data/transaction/TransactionData.java +++ b/src/main/java/org/qora/data/transaction/TransactionData.java @@ -34,7 +34,7 @@ import io.swagger.v3.oas.annotations.media.Schema.AccessMode; CreateOrderTransactionData.class, CancelOrderTransactionData.class, MultiPaymentTransactionData.class, DeployATTransactionData.class, MessageTransactionData.class, ATTransactionData.class, CreateGroupTransactionData.class, UpdateGroupTransactionData.class, - JoinGroupTransactionData.class + JoinGroupTransactionData.class, LeaveGroupTransactionData.class }) //All properties to be converted to JSON via JAX-RS @XmlAccessorType(XmlAccessType.FIELD) diff --git a/src/main/java/org/qora/group/Group.java b/src/main/java/org/qora/group/Group.java index 14b575ac..64fb44ab 100644 --- a/src/main/java/org/qora/group/Group.java +++ b/src/main/java/org/qora/group/Group.java @@ -1,5 +1,7 @@ package org.qora.group; +import java.util.Arrays; + import org.qora.account.Account; import org.qora.account.PublicKeyAccount; import org.qora.data.group.GroupAdminData; @@ -7,9 +9,11 @@ import org.qora.data.group.GroupData; import org.qora.data.group.GroupMemberData; import org.qora.data.transaction.CreateGroupTransactionData; import org.qora.data.transaction.JoinGroupTransactionData; +import org.qora.data.transaction.LeaveGroupTransactionData; import org.qora.data.transaction.TransactionData; import org.qora.data.transaction.UpdateGroupTransactionData; import org.qora.repository.DataException; +import org.qora.repository.GroupRepository; import org.qora.repository.Repository; public class Group { @@ -51,14 +55,14 @@ public class Group { // Processing - public void create() throws DataException { + public void create(CreateGroupTransactionData createGroupTransactionData) throws DataException { this.repository.getGroupRepository().save(this.groupData); // Add owner as admin too - this.repository.getGroupRepository().save(new GroupAdminData(this.groupData.getGroupName(), this.groupData.getOwner())); + this.repository.getGroupRepository().save(new GroupAdminData(this.groupData.getGroupName(), this.groupData.getOwner(), createGroupTransactionData.getSignature())); // Add owner as member too - this.repository.getGroupRepository().save(new GroupMemberData(this.groupData.getGroupName(), this.groupData.getOwner(), this.groupData.getCreated())); + this.repository.getGroupRepository().save(new GroupMemberData(this.groupData.getGroupName(), this.groupData.getOwner(), this.groupData.getCreated(), createGroupTransactionData.getSignature())); } public void uncreate() throws DataException { @@ -71,8 +75,6 @@ public class Group { if (previousTransactionData == null) throw new DataException("Unable to revert group transaction as referenced transaction not found in repository"); - // XXX needs code to reinstate owner as admin and member - switch (previousTransactionData.getType()) { case CREATE_GROUP: CreateGroupTransactionData previousCreateGroupTransactionData = (CreateGroupTransactionData) previousTransactionData; @@ -93,9 +95,14 @@ public class Group { default: throw new IllegalStateException("Unable to revert group transaction due to unsupported referenced transaction"); } + + // Previous owner will still be admin and member at this point } public void update(UpdateGroupTransactionData updateGroupTransactionData) throws DataException { + GroupRepository groupRepository = this.repository.getGroupRepository(); + String groupName = updateGroupTransactionData.getGroupName(); + // Update reference in transaction data updateGroupTransactionData.setGroupReference(this.groupData.getReference()); @@ -109,15 +116,29 @@ public class Group { this.groupData.setUpdated(updateGroupTransactionData.getTimestamp()); // Save updated group data - this.repository.getGroupRepository().save(this.groupData); + groupRepository.save(this.groupData); - // XXX new owner should be an admin if not already - // XXX new owner should be a member if not already + String newOwner = updateGroupTransactionData.getNewOwner(); - // XXX what happens to previous owner? retained as admin? + // New owner should be a member if not already + if (!groupRepository.memberExists(groupName, newOwner)) { + GroupMemberData groupMemberData = new GroupMemberData(groupName, newOwner, updateGroupTransactionData.getTimestamp(), updateGroupTransactionData.getSignature()); + groupRepository.save(groupMemberData); + } + + // New owner should be an admin if not already + if (!groupRepository.adminExists(groupName, newOwner)) { + GroupAdminData groupAdminData = new GroupAdminData(groupName, newOwner, updateGroupTransactionData.getSignature()); + groupRepository.save(groupAdminData); + } + + // Previous owner retained as admin and member } public void revert(UpdateGroupTransactionData updateGroupTransactionData) throws DataException { + GroupRepository groupRepository = this.repository.getGroupRepository(); + String groupName = updateGroupTransactionData.getGroupName(); + // Previous group reference is taken from this transaction's cached copy this.groupData.setReference(updateGroupTransactionData.getGroupReference()); @@ -125,21 +146,83 @@ public class Group { this.revert(); // Save reverted group data - this.repository.getGroupRepository().save(this.groupData); + groupRepository.save(this.groupData); + + // If ownership changed we need to do more work. Note groupData's owner is reverted at this point. + String newOwner = updateGroupTransactionData.getNewOwner(); + if (!this.groupData.getOwner().equals(newOwner)) { + // If this update caused [what was] new owner to become admin, then revoke that now. + // (It's possible they were an admin prior to being given ownership so we need to retain that). + GroupAdminData groupAdminData = groupRepository.getAdmin(groupName, newOwner); + if (Arrays.equals(groupAdminData.getGroupReference(), updateGroupTransactionData.getSignature())) + groupRepository.deleteAdmin(groupName, newOwner); + + // If this update caused [what was] new owner to become member, then revoke that now. + // (It's possible they were a member prior to being given ownership so we need to retain that). + GroupMemberData groupMemberData = groupRepository.getMember(groupName, newOwner); + if (Arrays.equals(groupMemberData.getGroupReference(), updateGroupTransactionData.getSignature())) + groupRepository.deleteMember(groupName, newOwner); + } } public void join(JoinGroupTransactionData joinGroupTransactionData) throws DataException { Account joiner = new PublicKeyAccount(this.repository, joinGroupTransactionData.getJoinerPublicKey()); - GroupMemberData groupMemberData = new GroupMemberData(joinGroupTransactionData.getGroupName(), joiner.getAddress(), joinGroupTransactionData.getTimestamp()); + GroupMemberData groupMemberData = new GroupMemberData(joinGroupTransactionData.getGroupName(), joiner.getAddress(), joinGroupTransactionData.getTimestamp(), joinGroupTransactionData.getSignature()); this.repository.getGroupRepository().save(groupMemberData); } public void unjoin(JoinGroupTransactionData joinGroupTransactionData) throws DataException { Account joiner = new PublicKeyAccount(this.repository, joinGroupTransactionData.getJoinerPublicKey()); - GroupMemberData groupMemberData = new GroupMemberData(joinGroupTransactionData.getGroupName(), joiner.getAddress(), joinGroupTransactionData.getTimestamp()); - this.repository.getGroupRepository().delete(groupMemberData); + this.repository.getGroupRepository().deleteMember(joinGroupTransactionData.getGroupName(), joiner.getAddress()); + } + + public void leave(LeaveGroupTransactionData leaveGroupTransactionData) throws DataException { + GroupRepository groupRepository = this.repository.getGroupRepository(); + String groupName = leaveGroupTransactionData.getGroupName(); + Account leaver = new PublicKeyAccount(this.repository, leaveGroupTransactionData.getLeaverPublicKey()); + + // Potentially record reference to transaction that restores previous admin state. + // Owners can't leave as that would leave group ownerless and in unrecoverable state. + + // Owners are also admins, so skip if owner + if (!leaver.getAddress().equals(this.groupData.getOwner())) { + // Fetch admin data for leaver + GroupAdminData groupAdminData = groupRepository.getAdmin(groupName, leaver.getAddress()); + + if (groupAdminData != null) { + // Leaver is admin - use promotion transaction reference as restore-state reference + leaveGroupTransactionData.setAdminReference(groupAdminData.getGroupReference()); + + // Remove as admin + groupRepository.deleteAdmin(groupName, leaver.getAddress()); + } + } + + // Save membership transaction reference + GroupMemberData groupMemberData = groupRepository.getMember(groupName, leaver.getAddress()); + leaveGroupTransactionData.setMemberReference(groupMemberData.getGroupReference()); + + // Remove as member + groupRepository.deleteMember(leaveGroupTransactionData.getGroupName(), leaver.getAddress()); + } + + public void unleave(LeaveGroupTransactionData leaveGroupTransactionData) throws DataException { + GroupRepository groupRepository = this.repository.getGroupRepository(); + String groupName = leaveGroupTransactionData.getGroupName(); + Account leaver = new PublicKeyAccount(this.repository, leaveGroupTransactionData.getLeaverPublicKey()); + + // Rejoin as member + TransactionData membershipTransactionData = this.repository.getTransactionRepository().fromSignature(leaveGroupTransactionData.getMemberReference()); + groupRepository.save(new GroupMemberData(groupName, leaver.getAddress(), membershipTransactionData.getTimestamp(), membershipTransactionData.getSignature())); + + // Put back any admin state based on referenced group-related transaction + byte[] adminTransactionSignature = leaveGroupTransactionData.getAdminReference(); + if (adminTransactionSignature != null) { + GroupAdminData groupAdminData = new GroupAdminData(leaveGroupTransactionData.getGroupName(), leaver.getAddress(), adminTransactionSignature); + groupRepository.save(groupAdminData); + } } } diff --git a/src/main/java/org/qora/repository/GroupRepository.java b/src/main/java/org/qora/repository/GroupRepository.java index 2be3ea2d..15efe819 100644 --- a/src/main/java/org/qora/repository/GroupRepository.java +++ b/src/main/java/org/qora/repository/GroupRepository.java @@ -24,15 +24,21 @@ public interface GroupRepository { // Group Admins + public GroupAdminData getAdmin(String groupName, String address) throws DataException; + + public boolean adminExists(String groupName, String address) throws DataException; + public List getAllGroupAdmins(String groupName) throws DataException; public void save(GroupAdminData groupAdminData) throws DataException; - public void delete(GroupAdminData groupAdminData) throws DataException; + public void deleteAdmin(String groupName, String address) throws DataException; // Group Members - public boolean memberExists(String groupName, String member) throws DataException; + public GroupMemberData getMember(String groupName, String address) throws DataException; + + public boolean memberExists(String groupName, String address) throws DataException; public List getAllGroupMembers(String groupName) throws DataException; @@ -41,6 +47,6 @@ public interface GroupRepository { public void save(GroupMemberData groupMemberData) throws DataException; - public void delete(GroupMemberData groupMemberData) throws DataException; + public void deleteMember(String groupName, String address) throws DataException; } \ No newline at end of file diff --git a/src/main/java/org/qora/repository/hsqldb/HSQLDBDatabaseUpdates.java b/src/main/java/org/qora/repository/hsqldb/HSQLDBDatabaseUpdates.java index 4b66790a..d9b530bd 100644 --- a/src/main/java/org/qora/repository/hsqldb/HSQLDBDatabaseUpdates.java +++ b/src/main/java/org/qora/repository/hsqldb/HSQLDBDatabaseUpdates.java @@ -416,12 +416,13 @@ public class HSQLDBDatabaseUpdates { stmt.execute("CREATE INDEX AccountGroupOwnerIndex on AccountGroups (owner)"); // Admins - stmt.execute("CREATE TABLE AccountGroupAdmins (group_name GroupName, admin QoraAddress, PRIMARY KEY (group_name, admin))"); + stmt.execute("CREATE TABLE AccountGroupAdmins (group_name GroupName, admin QoraAddress, group_reference Signature NOT NULL, PRIMARY KEY (group_name, admin))"); // For finding groups that address administrates stmt.execute("CREATE INDEX AccountGroupAdminIndex on AccountGroupAdmins (admin)"); // Members - stmt.execute("CREATE TABLE AccountGroupMembers (group_name GroupName, address QoraAddress, joined TIMESTAMP WITH TIME ZONE NOT NULL, PRIMARY KEY (group_name, address))"); + stmt.execute("CREATE TABLE AccountGroupMembers (group_name GroupName, address QoraAddress, joined TIMESTAMP WITH TIME ZONE NOT NULL, group_reference Signature NOT NULL, " + + "PRIMARY KEY (group_name, address))"); // For finding groups that address is member stmt.execute("CREATE INDEX AccountGroupMemberIndex on AccountGroupMembers (address)"); @@ -450,13 +451,12 @@ public class HSQLDBDatabaseUpdates { stmt.execute("CREATE TABLE UpdateGroupTransactions (signature Signature, owner QoraPublicKey NOT NULL, group_name GroupName NOT NULL, " + "new_owner QoraAddress NOT NULL, new_description GenericDescription NOT NULL, new_is_open BOOLEAN NOT NULL, group_reference Signature, " + "PRIMARY KEY (signature), FOREIGN KEY (signature) REFERENCES Transactions (signature) ON DELETE CASCADE)"); - break; - case 32: // Account group join/leave transactions stmt.execute("CREATE TABLE JoinGroupTransactions (signature Signature, joiner QoraPublicKey NOT NULL, group_name GroupName NOT NULL, " + "PRIMARY KEY (signature), FOREIGN KEY (signature) REFERENCES Transactions (signature) ON DELETE CASCADE)"); stmt.execute("CREATE TABLE LeaveGroupTransactions (signature Signature, leaver QoraPublicKey NOT NULL, group_name GroupName NOT NULL, " + + "member_reference Signature, admin_reference Signature, " + "PRIMARY KEY (signature), FOREIGN KEY (signature) REFERENCES Transactions (signature) ON DELETE CASCADE)"); break; diff --git a/src/main/java/org/qora/repository/hsqldb/HSQLDBGroupRepository.java b/src/main/java/org/qora/repository/hsqldb/HSQLDBGroupRepository.java index 4325484c..1683fb39 100644 --- a/src/main/java/org/qora/repository/hsqldb/HSQLDBGroupRepository.java +++ b/src/main/java/org/qora/repository/hsqldb/HSQLDBGroupRepository.java @@ -125,9 +125,9 @@ public class HSQLDBGroupRepository implements GroupRepository { Long updated = groupData.getUpdated(); Timestamp updatedTimestamp = updated == null ? null : new Timestamp(updated); - saveHelper.bind("owner", groupData.getOwner()).bind("group_name", groupData.getGroupName()) - .bind("description", groupData.getDescription()).bind("created", new Timestamp(groupData.getCreated())).bind("updated", updatedTimestamp) - .bind("reference", groupData.getReference()).bind("is_open", groupData.getIsOpen()); + saveHelper.bind("owner", groupData.getOwner()).bind("group_name", groupData.getGroupName()).bind("description", groupData.getDescription()) + .bind("created", new Timestamp(groupData.getCreated())).bind("updated", updatedTimestamp).bind("reference", groupData.getReference()) + .bind("is_open", groupData.getIsOpen()); try { saveHelper.execute(this.repository); @@ -156,18 +156,43 @@ public class HSQLDBGroupRepository implements GroupRepository { // Group Admins + @Override + public GroupAdminData getAdmin(String groupName, String address) throws DataException { + try (ResultSet resultSet = this.repository.checkedExecute("SELECT admin, group_reference FROM AccountGroupAdmins WHERE group_name = ?", groupName)) { + if (resultSet == null) + return null; + + String admin = resultSet.getString(1); + byte[] groupReference = resultSet.getBytes(2); + + return new GroupAdminData(groupName, admin, groupReference); + } catch (SQLException e) { + throw new DataException("Unable to fetch group admin from repository", e); + } + } + + @Override + public boolean adminExists(String groupName, String address) throws DataException { + try { + return this.repository.exists("AccountGroupAdmins", "group_name = ? AND admin = ?", groupName, address); + } catch (SQLException e) { + throw new DataException("Unable to check for group admin in repository", e); + } + } + @Override public List getAllGroupAdmins(String groupName) throws DataException { List admins = new ArrayList<>(); - try (ResultSet resultSet = this.repository.checkedExecute("SELECT admin FROM AccountGroupAdmins WHERE group_name = ?", groupName)) { + try (ResultSet resultSet = this.repository.checkedExecute("SELECT admin, group_reference FROM AccountGroupAdmins WHERE group_name = ?", groupName)) { if (resultSet == null) return admins; do { String admin = resultSet.getString(1); + byte[] groupReference = resultSet.getBytes(2); - admins.add(new GroupAdminData(groupName, admin)); + admins.add(new GroupAdminData(groupName, admin, groupReference)); } while (resultSet.next()); return admins; @@ -180,7 +205,8 @@ public class HSQLDBGroupRepository implements GroupRepository { public void save(GroupAdminData groupAdminData) throws DataException { HSQLDBSaver saveHelper = new HSQLDBSaver("AccountGroupAdmins"); - saveHelper.bind("group_name", groupAdminData.getGroupName()).bind("admin", groupAdminData.getAdmin()); + saveHelper.bind("group_name", groupAdminData.getGroupName()).bind("admin", groupAdminData.getAdmin()).bind("group_reference", + groupAdminData.getGroupReference()); try { saveHelper.execute(this.repository); @@ -190,9 +216,9 @@ public class HSQLDBGroupRepository implements GroupRepository { } @Override - public void delete(GroupAdminData groupAdminData) throws DataException { + public void deleteAdmin(String groupName, String address) throws DataException { try { - this.repository.delete("AccountGroupAdmins", "group_name = ? AND admin = ?", groupAdminData.getGroupName(), groupAdminData.getAdmin()); + this.repository.delete("AccountGroupAdmins", "group_name = ? AND admin = ?", groupName, address); } catch (SQLException e) { throw new DataException("Unable to delete group admin info from repository", e); } @@ -201,9 +227,26 @@ public class HSQLDBGroupRepository implements GroupRepository { // Group Members @Override - public boolean memberExists(String groupName, String member) throws DataException { + public GroupMemberData getMember(String groupName, String address) throws DataException { + try (ResultSet resultSet = this.repository.checkedExecute("SELECT address, joined, group_reference FROM AccountGroupMembers WHERE group_name = ?", + groupName)) { + if (resultSet == null) + return null; + + String member = resultSet.getString(1); + long joined = resultSet.getTimestamp(2, Calendar.getInstance(HSQLDBRepository.UTC)).getTime(); + byte[] groupReference = resultSet.getBytes(3); + + return new GroupMemberData(groupName, member, joined, groupReference); + } catch (SQLException e) { + throw new DataException("Unable to fetch group members from repository", e); + } + } + + @Override + public boolean memberExists(String groupName, String address) throws DataException { try { - return this.repository.exists("AccountGroupMembers", "group_name = ? AND address = ?", groupName, member); + return this.repository.exists("AccountGroupMembers", "group_name = ? AND address = ?", groupName, address); } catch (SQLException e) { throw new DataException("Unable to check for group member in repository", e); } @@ -213,15 +256,17 @@ public class HSQLDBGroupRepository implements GroupRepository { public List getAllGroupMembers(String groupName) throws DataException { List members = new ArrayList<>(); - try (ResultSet resultSet = this.repository.checkedExecute("SELECT address, joined FROM AccountGroupMembers WHERE group_name = ?", groupName)) { + try (ResultSet resultSet = this.repository.checkedExecute("SELECT address, joined, group_reference FROM AccountGroupMembers WHERE group_name = ?", + groupName)) { if (resultSet == null) return members; do { String member = resultSet.getString(1); long joined = resultSet.getTimestamp(2, Calendar.getInstance(HSQLDBRepository.UTC)).getTime(); + byte[] groupReference = resultSet.getBytes(3); - members.add(new GroupMemberData(groupName, member, joined)); + members.add(new GroupMemberData(groupName, member, joined, groupReference)); } while (resultSet.next()); return members; @@ -232,7 +277,8 @@ public class HSQLDBGroupRepository implements GroupRepository { @Override public Integer countGroupMembers(String groupName) throws DataException { - try (ResultSet resultSet = this.repository.checkedExecute("SELECT group_name, COUNT(*) FROM AccountGroupMembers WHERE group_name = ? GROUP BY group_name", groupName)) { + try (ResultSet resultSet = this.repository + .checkedExecute("SELECT group_name, COUNT(*) FROM AccountGroupMembers WHERE group_name = ? GROUP BY group_name", groupName)) { if (resultSet == null) return null; @@ -246,7 +292,8 @@ public class HSQLDBGroupRepository implements GroupRepository { public void save(GroupMemberData groupMemberData) throws DataException { HSQLDBSaver saveHelper = new HSQLDBSaver("AccountGroupMembers"); - saveHelper.bind("group_name", groupMemberData.getGroupName()).bind("address", groupMemberData.getMember()).bind("joined", new Timestamp(groupMemberData.getJoined())); + saveHelper.bind("group_name", groupMemberData.getGroupName()).bind("address", groupMemberData.getMember()) + .bind("joined", new Timestamp(groupMemberData.getJoined())).bind("group_reference", groupMemberData.getGroupReference()); try { saveHelper.execute(this.repository); @@ -256,9 +303,9 @@ public class HSQLDBGroupRepository implements GroupRepository { } @Override - public void delete(GroupMemberData groupMemberData) throws DataException { + public void deleteMember(String groupName, String address) throws DataException { try { - this.repository.delete("AccountGroupMembers", "group_name = ? AND address = ?", groupMemberData.getGroupName(), groupMemberData.getMember()); + this.repository.delete("AccountGroupMembers", "group_name = ? AND address = ?", groupName, address); } catch (SQLException e) { throw new DataException("Unable to delete group member info from repository", e); } diff --git a/src/main/java/org/qora/repository/hsqldb/transaction/HSQLDBLeaveGroupTransactionRepository.java b/src/main/java/org/qora/repository/hsqldb/transaction/HSQLDBLeaveGroupTransactionRepository.java new file mode 100644 index 00000000..7bf66f08 --- /dev/null +++ b/src/main/java/org/qora/repository/hsqldb/transaction/HSQLDBLeaveGroupTransactionRepository.java @@ -0,0 +1,52 @@ +package org.qora.repository.hsqldb.transaction; + +import java.math.BigDecimal; +import java.sql.ResultSet; +import java.sql.SQLException; + +import org.qora.data.transaction.LeaveGroupTransactionData; +import org.qora.data.transaction.TransactionData; +import org.qora.repository.DataException; +import org.qora.repository.hsqldb.HSQLDBRepository; +import org.qora.repository.hsqldb.HSQLDBSaver; + +public class HSQLDBLeaveGroupTransactionRepository extends HSQLDBTransactionRepository { + + public HSQLDBLeaveGroupTransactionRepository(HSQLDBRepository repository) { + this.repository = repository; + } + + TransactionData fromBase(byte[] signature, byte[] reference, byte[] creatorPublicKey, long timestamp, BigDecimal fee) throws DataException { + try (ResultSet resultSet = this.repository + .checkedExecute("SELECT group_name, member_reference, admin_reference FROM LeaveGroupTransactions WHERE signature = ?", signature)) { + if (resultSet == null) + return null; + + String groupName = resultSet.getString(1); + byte[] memberReference = resultSet.getBytes(2); + byte[] adminReference = resultSet.getBytes(3); + + return new LeaveGroupTransactionData(creatorPublicKey, groupName, memberReference, adminReference, fee, timestamp, reference, signature); + } catch (SQLException e) { + throw new DataException("Unable to fetch leave group transaction from repository", e); + } + } + + @Override + public void save(TransactionData transactionData) throws DataException { + LeaveGroupTransactionData leaveGroupTransactionData = (LeaveGroupTransactionData) transactionData; + + HSQLDBSaver saveHelper = new HSQLDBSaver("LeaveGroupTransactions"); + + saveHelper.bind("signature", leaveGroupTransactionData.getSignature()).bind("leaver", leaveGroupTransactionData.getLeaverPublicKey()) + .bind("group_name", leaveGroupTransactionData.getGroupName()).bind("member_reference", leaveGroupTransactionData.getMemberReference()) + .bind("admin_reference", leaveGroupTransactionData.getAdminReference()); + + try { + saveHelper.execute(this.repository); + } catch (SQLException e) { + throw new DataException("Unable to save leave group transaction into repository", e); + } + } + +} diff --git a/src/main/java/org/qora/repository/hsqldb/transaction/HSQLDBTransactionRepository.java b/src/main/java/org/qora/repository/hsqldb/transaction/HSQLDBTransactionRepository.java index 6f666a5c..19ca85d0 100644 --- a/src/main/java/org/qora/repository/hsqldb/transaction/HSQLDBTransactionRepository.java +++ b/src/main/java/org/qora/repository/hsqldb/transaction/HSQLDBTransactionRepository.java @@ -44,6 +44,7 @@ public class HSQLDBTransactionRepository implements TransactionRepository { private HSQLDBCreateGroupTransactionRepository createGroupTransactionRepository; private HSQLDBUpdateGroupTransactionRepository updateGroupTransactionRepository; private HSQLDBJoinGroupTransactionRepository joinGroupTransactionRepository; + private HSQLDBLeaveGroupTransactionRepository leaveGroupTransactionRepository; public HSQLDBTransactionRepository(HSQLDBRepository repository) { this.repository = repository; @@ -68,6 +69,7 @@ public class HSQLDBTransactionRepository implements TransactionRepository { this.createGroupTransactionRepository = new HSQLDBCreateGroupTransactionRepository(repository); this.updateGroupTransactionRepository = new HSQLDBUpdateGroupTransactionRepository(repository); this.joinGroupTransactionRepository = new HSQLDBJoinGroupTransactionRepository(repository); + this.leaveGroupTransactionRepository = new HSQLDBLeaveGroupTransactionRepository(repository); } protected HSQLDBTransactionRepository() { @@ -203,6 +205,9 @@ public class HSQLDBTransactionRepository implements TransactionRepository { case JOIN_GROUP: return this.joinGroupTransactionRepository.fromBase(signature, reference, creatorPublicKey, timestamp, fee); + case LEAVE_GROUP: + return this.leaveGroupTransactionRepository.fromBase(signature, reference, creatorPublicKey, timestamp, fee); + default: throw new DataException("Unsupported transaction type [" + type.name() + "] during fetch from HSQLDB repository"); } @@ -535,6 +540,10 @@ public class HSQLDBTransactionRepository implements TransactionRepository { this.joinGroupTransactionRepository.save(transactionData); break; + case LEAVE_GROUP: + this.leaveGroupTransactionRepository.save(transactionData); + break; + default: throw new DataException("Unsupported transaction type [" + transactionData.getType().name() + "] during save into HSQLDB repository"); } diff --git a/src/main/java/org/qora/transaction/CreateGroupTransaction.java b/src/main/java/org/qora/transaction/CreateGroupTransaction.java index e7745b5c..d2e1aaf5 100644 --- a/src/main/java/org/qora/transaction/CreateGroupTransaction.java +++ b/src/main/java/org/qora/transaction/CreateGroupTransaction.java @@ -113,7 +113,7 @@ public class CreateGroupTransaction extends Transaction { public void process() throws DataException { // Create Group Group group = new Group(this.repository, createGroupTransactionData); - group.create(); + group.create(createGroupTransactionData); // Save this transaction this.repository.getTransactionRepository().save(createGroupTransactionData); diff --git a/src/main/java/org/qora/transaction/LeaveGroupTransaction.java b/src/main/java/org/qora/transaction/LeaveGroupTransaction.java new file mode 100644 index 00000000..f8c90d0c --- /dev/null +++ b/src/main/java/org/qora/transaction/LeaveGroupTransaction.java @@ -0,0 +1,143 @@ +package org.qora.transaction; + +import java.math.BigDecimal; +import java.util.Arrays; +import java.util.Collections; +import java.util.List; + +import org.qora.account.Account; +import org.qora.account.PublicKeyAccount; +import org.qora.asset.Asset; +import org.qora.data.transaction.LeaveGroupTransactionData; +import org.qora.data.group.GroupData; +import org.qora.data.transaction.TransactionData; +import org.qora.group.Group; +import org.qora.repository.DataException; +import org.qora.repository.Repository; + +import com.google.common.base.Utf8; + +public class LeaveGroupTransaction extends Transaction { + + // Properties + private LeaveGroupTransactionData leaveGroupTransactionData; + + // Constructors + + public LeaveGroupTransaction(Repository repository, TransactionData transactionData) { + super(repository, transactionData); + + this.leaveGroupTransactionData = (LeaveGroupTransactionData) this.transactionData; + } + + // More information + + @Override + public List getRecipientAccounts() throws DataException { + return Collections.emptyList(); + } + + @Override + public boolean isInvolved(Account account) throws DataException { + String address = account.getAddress(); + + if (address.equals(this.getLeaver().getAddress())) + return true; + + return false; + } + + @Override + public BigDecimal getAmount(Account account) throws DataException { + String address = account.getAddress(); + BigDecimal amount = BigDecimal.ZERO.setScale(8); + + if (address.equals(this.getLeaver().getAddress())) + amount = amount.subtract(this.transactionData.getFee()); + + return amount; + } + + // Navigation + + public Account getLeaver() throws DataException { + return new PublicKeyAccount(this.repository, this.leaveGroupTransactionData.getLeaverPublicKey()); + } + + // Processing + + @Override + public ValidationResult isValid() throws DataException { + // Check group name size bounds + int groupNameLength = Utf8.encodedLength(leaveGroupTransactionData.getGroupName()); + if (groupNameLength < 1 || groupNameLength > Group.MAX_NAME_SIZE) + return ValidationResult.INVALID_NAME_LENGTH; + + // Check group name is lowercase + if (!leaveGroupTransactionData.getGroupName().equals(leaveGroupTransactionData.getGroupName().toLowerCase())) + return ValidationResult.NAME_NOT_LOWER_CASE; + + GroupData groupData = this.repository.getGroupRepository().fromGroupName(leaveGroupTransactionData.getGroupName()); + + // Check group exists + if (groupData == null) + return ValidationResult.GROUP_DOES_NOT_EXIST; + + Account leaver = getLeaver(); + + // Can't leave if group owner + if (leaver.getAddress().equals(groupData.getOwner())) + return ValidationResult.GROUP_OWNER_CANNOT_LEAVE; + + if (!this.repository.getGroupRepository().memberExists(leaveGroupTransactionData.getGroupName(), leaver.getAddress())) + return ValidationResult.NOT_GROUP_MEMBER; + + // Check fee is positive + if (leaveGroupTransactionData.getFee().compareTo(BigDecimal.ZERO) <= 0) + return ValidationResult.NEGATIVE_FEE; + + if (!Arrays.equals(leaver.getLastReference(), leaveGroupTransactionData.getReference())) + return ValidationResult.INVALID_REFERENCE; + + // Check creator has enough funds + if (leaver.getConfirmedBalance(Asset.QORA).compareTo(leaveGroupTransactionData.getFee()) < 0) + return ValidationResult.NO_BALANCE; + + return ValidationResult.OK; + } + + @Override + public void process() throws DataException { + // Update Group Membership + Group group = new Group(this.repository, leaveGroupTransactionData.getGroupName()); + group.leave(leaveGroupTransactionData); + + // Save this transaction with updated member/admin references to transactions that can help restore state + this.repository.getTransactionRepository().save(leaveGroupTransactionData); + + // Update leaver's balance + Account leaver = getLeaver(); + leaver.setConfirmedBalance(Asset.QORA, leaver.getConfirmedBalance(Asset.QORA).subtract(leaveGroupTransactionData.getFee())); + + // Update leaver's reference + leaver.setLastReference(leaveGroupTransactionData.getSignature()); + } + + @Override + public void orphan() throws DataException { + // Revert group membership + Group group = new Group(this.repository, leaveGroupTransactionData.getGroupName()); + group.unleave(leaveGroupTransactionData); + + // Delete this transaction itself + this.repository.getTransactionRepository().delete(leaveGroupTransactionData); + + // Update leaver's balance + Account leaver = getLeaver(); + leaver.setConfirmedBalance(Asset.QORA, leaver.getConfirmedBalance(Asset.QORA).add(leaveGroupTransactionData.getFee())); + + // Update leaver's reference + leaver.setLastReference(leaveGroupTransactionData.getReference()); + } + +} diff --git a/src/main/java/org/qora/transaction/Transaction.java b/src/main/java/org/qora/transaction/Transaction.java index 0705a203..f7a8e80f 100644 --- a/src/main/java/org/qora/transaction/Transaction.java +++ b/src/main/java/org/qora/transaction/Transaction.java @@ -129,6 +129,8 @@ public abstract class Transaction { GROUP_DOES_NOT_EXIST(49), INVALID_GROUP_OWNER(50), ALREADY_GROUP_MEMBER(51), + GROUP_OWNER_CANNOT_LEAVE(52), + NOT_GROUP_MEMBER(53), NOT_YET_RELEASED(1000); public final int value; @@ -237,6 +239,9 @@ public abstract class Transaction { case JOIN_GROUP: return new JoinGroupTransaction(repository, transactionData); + case LEAVE_GROUP: + return new LeaveGroupTransaction(repository, transactionData); + default: throw new IllegalStateException("Unsupported transaction type [" + transactionData.getType().value + "] during fetch from repository"); } diff --git a/src/main/java/org/qora/transform/transaction/LeaveGroupTransactionTransformer.java b/src/main/java/org/qora/transform/transaction/LeaveGroupTransactionTransformer.java new file mode 100644 index 00000000..a02e7e4d --- /dev/null +++ b/src/main/java/org/qora/transform/transaction/LeaveGroupTransactionTransformer.java @@ -0,0 +1,99 @@ +package org.qora.transform.transaction; + +import java.io.ByteArrayOutputStream; +import java.io.IOException; +import java.math.BigDecimal; +import java.nio.ByteBuffer; + +import org.json.simple.JSONObject; +import org.qora.account.PublicKeyAccount; +import org.qora.data.transaction.LeaveGroupTransactionData; +import org.qora.data.transaction.TransactionData; +import org.qora.group.Group; +import org.qora.transform.TransformationException; +import org.qora.utils.Serialization; + +import com.google.common.base.Utf8; +import com.google.common.hash.HashCode; +import com.google.common.primitives.Ints; +import com.google.common.primitives.Longs; + +public class LeaveGroupTransactionTransformer extends TransactionTransformer { + + // Property lengths + private static final int JOINER_LENGTH = PUBLIC_KEY_LENGTH; + private static final int NAME_SIZE_LENGTH = INT_LENGTH; + + private static final int TYPELESS_DATALESS_LENGTH = BASE_TYPELESS_LENGTH + JOINER_LENGTH + NAME_SIZE_LENGTH; + + static TransactionData fromByteBuffer(ByteBuffer byteBuffer) throws TransformationException { + long timestamp = byteBuffer.getLong(); + + byte[] reference = new byte[REFERENCE_LENGTH]; + byteBuffer.get(reference); + + byte[] leaverPublicKey = Serialization.deserializePublicKey(byteBuffer); + + String groupName = Serialization.deserializeSizedString(byteBuffer, Group.MAX_NAME_SIZE); + + BigDecimal fee = Serialization.deserializeBigDecimal(byteBuffer); + + byte[] signature = new byte[SIGNATURE_LENGTH]; + byteBuffer.get(signature); + + return new LeaveGroupTransactionData(leaverPublicKey, groupName, fee, timestamp, reference, signature); + } + + public static int getDataLength(TransactionData transactionData) throws TransformationException { + LeaveGroupTransactionData leaveGroupTransactionData = (LeaveGroupTransactionData) transactionData; + + int dataLength = TYPE_LENGTH + TYPELESS_DATALESS_LENGTH + Utf8.encodedLength(leaveGroupTransactionData.getGroupName()); + + return dataLength; + } + + public static byte[] toBytes(TransactionData transactionData) throws TransformationException { + try { + LeaveGroupTransactionData leaveGroupTransactionData = (LeaveGroupTransactionData) transactionData; + + ByteArrayOutputStream bytes = new ByteArrayOutputStream(); + + bytes.write(Ints.toByteArray(leaveGroupTransactionData.getType().value)); + bytes.write(Longs.toByteArray(leaveGroupTransactionData.getTimestamp())); + bytes.write(leaveGroupTransactionData.getReference()); + + bytes.write(leaveGroupTransactionData.getCreatorPublicKey()); + Serialization.serializeSizedString(bytes, leaveGroupTransactionData.getGroupName()); + + Serialization.serializeBigDecimal(bytes, leaveGroupTransactionData.getFee()); + + if (leaveGroupTransactionData.getSignature() != null) + bytes.write(leaveGroupTransactionData.getSignature()); + + return bytes.toByteArray(); + } catch (IOException | ClassCastException e) { + throw new TransformationException(e); + } + } + + @SuppressWarnings("unchecked") + public static JSONObject toJSON(TransactionData transactionData) throws TransformationException { + JSONObject json = TransactionTransformer.getBaseJSON(transactionData); + + try { + LeaveGroupTransactionData leaveGroupTransactionData = (LeaveGroupTransactionData) transactionData; + + byte[] leaverPublicKey = leaveGroupTransactionData.getLeaverPublicKey(); + + json.put("leaver", PublicKeyAccount.getAddress(leaverPublicKey)); + json.put("leaverPublicKey", HashCode.fromBytes(leaverPublicKey).toString()); + + json.put("groupName", leaveGroupTransactionData.getGroupName()); + } catch (ClassCastException e) { + throw new TransformationException(e); + } + + return json; + } + +} diff --git a/src/main/java/org/qora/transform/transaction/TransactionTransformer.java b/src/main/java/org/qora/transform/transaction/TransactionTransformer.java index 8ffda93b..221ffe92 100644 --- a/src/main/java/org/qora/transform/transaction/TransactionTransformer.java +++ b/src/main/java/org/qora/transform/transaction/TransactionTransformer.java @@ -103,6 +103,9 @@ public class TransactionTransformer extends Transformer { case JOIN_GROUP: return JoinGroupTransactionTransformer.fromByteBuffer(byteBuffer); + case LEAVE_GROUP: + return LeaveGroupTransactionTransformer.fromByteBuffer(byteBuffer); + default: throw new TransformationException("Unsupported transaction type [" + type.value + "] during conversion from bytes"); } @@ -173,6 +176,9 @@ public class TransactionTransformer extends Transformer { case JOIN_GROUP: return JoinGroupTransactionTransformer.getDataLength(transactionData); + case LEAVE_GROUP: + return LeaveGroupTransactionTransformer.getDataLength(transactionData); + default: throw new TransformationException("Unsupported transaction type [" + transactionData.getType().value + "] when requesting byte length"); } @@ -240,6 +246,9 @@ public class TransactionTransformer extends Transformer { case JOIN_GROUP: return JoinGroupTransactionTransformer.toBytes(transactionData); + case LEAVE_GROUP: + return LeaveGroupTransactionTransformer.toBytes(transactionData); + default: throw new TransformationException("Unsupported transaction type [" + transactionData.getType().value + "] during conversion to bytes"); } @@ -316,6 +325,9 @@ public class TransactionTransformer extends Transformer { case JOIN_GROUP: return JoinGroupTransactionTransformer.toBytesForSigningImpl(transactionData); + case LEAVE_GROUP: + return LeaveGroupTransactionTransformer.toBytesForSigningImpl(transactionData); + default: throw new TransformationException( "Unsupported transaction type [" + transactionData.getType().value + "] during conversion to bytes for signing"); @@ -404,6 +416,9 @@ public class TransactionTransformer extends Transformer { case JOIN_GROUP: return JoinGroupTransactionTransformer.toJSON(transactionData); + case LEAVE_GROUP: + return LeaveGroupTransactionTransformer.toJSON(transactionData); + default: throw new TransformationException("Unsupported transaction type [" + transactionData.getType().value + "] during conversion to JSON"); }