diff --git a/src/main/java/org/qora/api/resource/GroupsResource.java b/src/main/java/org/qora/api/resource/GroupsResource.java index 5532091a..1146fb3f 100644 --- a/src/main/java/org/qora/api/resource/GroupsResource.java +++ b/src/main/java/org/qora/api/resource/GroupsResource.java @@ -30,9 +30,11 @@ import org.qora.crypto.Crypto; import org.qora.data.group.GroupAdminData; import org.qora.data.group.GroupData; import org.qora.data.group.GroupMemberData; +import org.qora.data.transaction.AddGroupAdminTransactionData; import org.qora.data.transaction.CreateGroupTransactionData; import org.qora.data.transaction.JoinGroupTransactionData; import org.qora.data.transaction.LeaveGroupTransactionData; +import org.qora.data.transaction.RemoveGroupAdminTransactionData; import org.qora.data.transaction.UpdateGroupTransactionData; import org.qora.repository.DataException; import org.qora.repository.Repository; @@ -40,9 +42,11 @@ import org.qora.repository.RepositoryManager; import org.qora.transaction.Transaction; import org.qora.transaction.Transaction.ValidationResult; import org.qora.transform.TransformationException; +import org.qora.transform.transaction.AddGroupAdminTransactionTransformer; import org.qora.transform.transaction.CreateGroupTransactionTransformer; import org.qora.transform.transaction.JoinGroupTransactionTransformer; import org.qora.transform.transaction.LeaveGroupTransactionTransformer; +import org.qora.transform.transaction.RemoveGroupAdminTransactionTransformer; import org.qora.transform.transaction.UpdateGroupTransactionTransformer; import org.qora.utils.Base58; @@ -246,6 +250,92 @@ public class GroupsResource { } } + @POST + @Path("/addadmin") + @Operation( + summary = "Build raw, unsigned, ADD_GROUP_ADMIN transaction", + requestBody = @RequestBody( + required = true, + content = @Content( + mediaType = MediaType.APPLICATION_JSON, + schema = @Schema( + implementation = AddGroupAdminTransactionData.class + ) + ) + ), + responses = { + @ApiResponse( + description = "raw, unsigned, ADD_GROUP_ADMIN 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 addGroupAdmin(AddGroupAdminTransactionData 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 = AddGroupAdminTransactionTransformer.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); + } + } + + @POST + @Path("/removeadmin") + @Operation( + summary = "Build raw, unsigned, REMOVE_GROUP_ADMIN transaction", + requestBody = @RequestBody( + required = true, + content = @Content( + mediaType = MediaType.APPLICATION_JSON, + schema = @Schema( + implementation = RemoveGroupAdminTransactionData.class + ) + ) + ), + responses = { + @ApiResponse( + description = "raw, unsigned, REMOVE_GROUP_ADMIN 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 removeGroupAdmin(RemoveGroupAdminTransactionData 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 = RemoveGroupAdminTransactionTransformer.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); + } + } + @POST @Path("/join") @Operation( diff --git a/src/main/java/org/qora/data/transaction/AddGroupAdminTransactionData.java b/src/main/java/org/qora/data/transaction/AddGroupAdminTransactionData.java new file mode 100644 index 00000000..12a1f916 --- /dev/null +++ b/src/main/java/org/qora/data/transaction/AddGroupAdminTransactionData.java @@ -0,0 +1,63 @@ +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 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 AddGroupAdminTransactionData extends TransactionData { + + // Properties + @Schema(description = "group owner's public key", example = "2tiMr5LTpaWCgbRvkPK8TFd7k63DyHJMMFFsz9uBf1ZP") + private byte[] ownerPublicKey; + @Schema(description = "group name", example = "my-group") + private String groupName; + @Schema(description = "member to promote to admin", example = "QixPbJUwsaHsVEofJdozU9zgVqkK6aYhrK") + private String member; + + // Constructors + + // For JAX-RS + protected AddGroupAdminTransactionData() { + super(TransactionType.ADD_GROUP_ADMIN); + } + + public void afterUnmarshal(Unmarshaller u, Object parent) { + this.creatorPublicKey = this.ownerPublicKey; + } + + public AddGroupAdminTransactionData(byte[] ownerPublicKey, String groupName, String member, BigDecimal fee, long timestamp, byte[] reference, byte[] signature) { + super(TransactionType.ADD_GROUP_ADMIN, fee, ownerPublicKey, timestamp, reference, signature); + + this.ownerPublicKey = ownerPublicKey; + this.groupName = groupName; + this.member = member; + } + + public AddGroupAdminTransactionData(byte[] ownerPublicKey, String groupName, String member, BigDecimal fee, long timestamp, byte[] reference) { + this(ownerPublicKey, groupName, member, fee, timestamp, reference, null); + } + + // Getters / setters + + public byte[] getOwnerPublicKey() { + return this.ownerPublicKey; + } + + public String getGroupName() { + return this.groupName; + } + + public String getMember() { + return this.member; + } + +} diff --git a/src/main/java/org/qora/data/transaction/RemoveGroupAdminTransactionData.java b/src/main/java/org/qora/data/transaction/RemoveGroupAdminTransactionData.java new file mode 100644 index 00000000..79e84ef2 --- /dev/null +++ b/src/main/java/org/qora/data/transaction/RemoveGroupAdminTransactionData.java @@ -0,0 +1,85 @@ +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 RemoveGroupAdminTransactionData extends TransactionData { + + // Properties + @Schema(description = "group owner's public key", example = "2tiMr5LTpaWCgbRvkPK8TFd7k63DyHJMMFFsz9uBf1ZP") + private byte[] ownerPublicKey; + @Schema(description = "group name", example = "my-group") + private String groupName; + @Schema(description = "admin to demote", example = "QixPbJUwsaHsVEofJdozU9zgVqkK6aYhrK") + private String admin; + // For internal use when orphaning + @XmlTransient + @Schema(hidden = true) + private byte[] groupReference; + + // Constructors + + // For JAX-RS + protected RemoveGroupAdminTransactionData() { + super(TransactionType.REMOVE_GROUP_ADMIN); + } + + public void afterUnmarshal(Unmarshaller u, Object parent) { + this.creatorPublicKey = this.ownerPublicKey; + } + + public RemoveGroupAdminTransactionData(byte[] ownerPublicKey, String groupName, String admin, byte[] groupReference, BigDecimal fee, long timestamp, byte[] reference, byte[] signature) { + super(TransactionType.REMOVE_GROUP_ADMIN, fee, ownerPublicKey, timestamp, reference, signature); + + this.ownerPublicKey = ownerPublicKey; + this.groupName = groupName; + this.admin = admin; + this.groupReference = groupReference; + } + + public RemoveGroupAdminTransactionData(byte[] ownerPublicKey, String groupName, String admin, byte[] groupReference, BigDecimal fee, long timestamp, byte[] reference) { + this(ownerPublicKey, groupName, admin, groupReference, fee, timestamp, reference, null); + } + + public RemoveGroupAdminTransactionData(byte[] ownerPublicKey, String groupName, String admin, BigDecimal fee, long timestamp, byte[] reference, byte[] signature) { + this(ownerPublicKey, groupName, admin, null, fee, timestamp, reference, signature); + } + + public RemoveGroupAdminTransactionData(byte[] ownerPublicKey, String groupName, String admin, BigDecimal fee, long timestamp, byte[] reference) { + this(ownerPublicKey, groupName, admin, null, fee, timestamp, reference, null); + } + + // Getters / setters + + public byte[] getOwnerPublicKey() { + return this.ownerPublicKey; + } + + public String getGroupName() { + return this.groupName; + } + + public String getAdmin() { + 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/transaction/TransactionData.java b/src/main/java/org/qora/data/transaction/TransactionData.java index 186fc5c2..a860a7a8 100644 --- a/src/main/java/org/qora/data/transaction/TransactionData.java +++ b/src/main/java/org/qora/data/transaction/TransactionData.java @@ -34,6 +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, + AddGroupAdminTransactionData.class, RemoveGroupAdminTransactionData.class, JoinGroupTransactionData.class, LeaveGroupTransactionData.class }) //All properties to be converted to JSON via JAX-RS diff --git a/src/main/java/org/qora/group/Group.java b/src/main/java/org/qora/group/Group.java index 64fb44ab..f578fb05 100644 --- a/src/main/java/org/qora/group/Group.java +++ b/src/main/java/org/qora/group/Group.java @@ -7,9 +7,11 @@ import org.qora.account.PublicKeyAccount; import org.qora.data.group.GroupAdminData; import org.qora.data.group.GroupData; import org.qora.data.group.GroupMemberData; +import org.qora.data.transaction.AddGroupAdminTransactionData; import org.qora.data.transaction.CreateGroupTransactionData; import org.qora.data.transaction.JoinGroupTransactionData; import org.qora.data.transaction.LeaveGroupTransactionData; +import org.qora.data.transaction.RemoveGroupAdminTransactionData; import org.qora.data.transaction.TransactionData; import org.qora.data.transaction.UpdateGroupTransactionData; import org.qora.repository.DataException; @@ -165,6 +167,38 @@ public class Group { } } + public void promoteToAdmin(AddGroupAdminTransactionData addGroupAdminTransactionData) throws DataException { + GroupAdminData groupAdminData = new GroupAdminData(addGroupAdminTransactionData.getGroupName(), addGroupAdminTransactionData.getMember(), addGroupAdminTransactionData.getSignature()); + this.repository.getGroupRepository().save(groupAdminData); + } + + public void unpromoteToAdmin(AddGroupAdminTransactionData addGroupAdminTransactionData) throws DataException { + this.repository.getGroupRepository().deleteAdmin(addGroupAdminTransactionData.getGroupName(), addGroupAdminTransactionData.getMember()); + } + + public void demoteFromAdmin(RemoveGroupAdminTransactionData removeGroupAdminTransactionData) throws DataException { + GroupRepository groupRepository = this.repository.getGroupRepository(); + String groupName = removeGroupAdminTransactionData.getGroupName(); + String admin = removeGroupAdminTransactionData.getAdmin(); + + // Save admin's promotion transaction reference for orphaning purposes + GroupAdminData groupAdminData = groupRepository.getAdmin(groupName, admin); + removeGroupAdminTransactionData.setGroupReference(groupAdminData.getGroupReference()); + + // Demote + groupRepository.deleteAdmin(groupName, admin); + } + + public void undemoteFromAdmin(RemoveGroupAdminTransactionData removeGroupAdminTransactionData) throws DataException { + GroupRepository groupRepository = this.repository.getGroupRepository(); + String groupName = removeGroupAdminTransactionData.getGroupName(); + String admin = removeGroupAdminTransactionData.getAdmin(); + + // Rebuild admin entry using stored promotion transaction reference + GroupAdminData groupAdminData = new GroupAdminData(groupName, admin, removeGroupAdminTransactionData.getGroupReference()); + groupRepository.save(groupAdminData); + } + public void join(JoinGroupTransactionData joinGroupTransactionData) throws DataException { Account joiner = new PublicKeyAccount(this.repository, joinGroupTransactionData.getJoinerPublicKey()); diff --git a/src/main/java/org/qora/repository/hsqldb/HSQLDBDatabaseUpdates.java b/src/main/java/org/qora/repository/hsqldb/HSQLDBDatabaseUpdates.java index d9b530bd..82970ea1 100644 --- a/src/main/java/org/qora/repository/hsqldb/HSQLDBDatabaseUpdates.java +++ b/src/main/java/org/qora/repository/hsqldb/HSQLDBDatabaseUpdates.java @@ -452,6 +452,12 @@ public class HSQLDBDatabaseUpdates { + "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)"); + // Account group add/remove admin transactions + stmt.execute("CREATE TABLE AddGroupAdminTransactions (signature Signature, owner QoraPublicKey NOT NULL, group_name GroupName NOT NULL, address QoraAddress NOT NULL, " + + "PRIMARY KEY (signature), FOREIGN KEY (signature) REFERENCES Transactions (signature) ON DELETE CASCADE)"); + stmt.execute("CREATE TABLE RemoveGroupAdminTransactions (signature Signature, owner QoraPublicKey NOT NULL, group_name GroupName NOT NULL, admin QoraAddress NOT NULL, " + + "group_reference Signature, PRIMARY KEY (signature), FOREIGN KEY (signature) REFERENCES Transactions (signature) ON DELETE CASCADE)"); + // 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)"); diff --git a/src/main/java/org/qora/repository/hsqldb/transaction/HSQLDBAddGroupAdminTransactionRepository.java b/src/main/java/org/qora/repository/hsqldb/transaction/HSQLDBAddGroupAdminTransactionRepository.java new file mode 100644 index 00000000..3eb76790 --- /dev/null +++ b/src/main/java/org/qora/repository/hsqldb/transaction/HSQLDBAddGroupAdminTransactionRepository.java @@ -0,0 +1,49 @@ +package org.qora.repository.hsqldb.transaction; + +import java.math.BigDecimal; +import java.sql.ResultSet; +import java.sql.SQLException; + +import org.qora.data.transaction.AddGroupAdminTransactionData; +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 HSQLDBAddGroupAdminTransactionRepository extends HSQLDBTransactionRepository { + + public HSQLDBAddGroupAdminTransactionRepository(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, address FROM AddGroupAdminTransactions WHERE signature = ?", signature)) { + if (resultSet == null) + return null; + + String groupName = resultSet.getString(1); + String member = resultSet.getString(2); + + return new AddGroupAdminTransactionData(creatorPublicKey, groupName, member, fee, timestamp, reference, signature); + } catch (SQLException e) { + throw new DataException("Unable to fetch add group admin transaction from repository", e); + } + } + + @Override + public void save(TransactionData transactionData) throws DataException { + AddGroupAdminTransactionData addGroupAdminTransactionData = (AddGroupAdminTransactionData) transactionData; + + HSQLDBSaver saveHelper = new HSQLDBSaver("AddGroupAdminTransactions"); + + saveHelper.bind("signature", addGroupAdminTransactionData.getSignature()).bind("owner", addGroupAdminTransactionData.getOwnerPublicKey()) + .bind("group_name", addGroupAdminTransactionData.getGroupName()).bind("address", addGroupAdminTransactionData.getMember()); + + try { + saveHelper.execute(this.repository); + } catch (SQLException e) { + throw new DataException("Unable to save add group admin transaction into repository", e); + } + } + +} diff --git a/src/main/java/org/qora/repository/hsqldb/transaction/HSQLDBRemoveGroupAdminTransactionRepository.java b/src/main/java/org/qora/repository/hsqldb/transaction/HSQLDBRemoveGroupAdminTransactionRepository.java new file mode 100644 index 00000000..45304d92 --- /dev/null +++ b/src/main/java/org/qora/repository/hsqldb/transaction/HSQLDBRemoveGroupAdminTransactionRepository.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.RemoveGroupAdminTransactionData; +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 HSQLDBRemoveGroupAdminTransactionRepository extends HSQLDBTransactionRepository { + + public HSQLDBRemoveGroupAdminTransactionRepository(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, admin, group_reference FROM RemoveGroupAdminTransactions WHERE signature = ?", signature)) { + if (resultSet == null) + return null; + + String groupName = resultSet.getString(1); + String admin = resultSet.getString(2); + byte[] groupReference = resultSet.getBytes(3); + + return new RemoveGroupAdminTransactionData(creatorPublicKey, groupName, admin, groupReference, fee, timestamp, reference, signature); + } catch (SQLException e) { + throw new DataException("Unable to fetch remove group admin transaction from repository", e); + } + } + + @Override + public void save(TransactionData transactionData) throws DataException { + RemoveGroupAdminTransactionData removeGroupAdminTransactionData = (RemoveGroupAdminTransactionData) transactionData; + + HSQLDBSaver saveHelper = new HSQLDBSaver("RemoveGroupAdminTransactions"); + + saveHelper.bind("signature", removeGroupAdminTransactionData.getSignature()).bind("owner", removeGroupAdminTransactionData.getOwnerPublicKey()) + .bind("group_name", removeGroupAdminTransactionData.getGroupName()).bind("admin", removeGroupAdminTransactionData.getAdmin()) + .bind("group_reference", removeGroupAdminTransactionData.getGroupReference()); + + try { + saveHelper.execute(this.repository); + } catch (SQLException e) { + throw new DataException("Unable to save remove group admin 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 19ca85d0..aa9dc7c9 100644 --- a/src/main/java/org/qora/repository/hsqldb/transaction/HSQLDBTransactionRepository.java +++ b/src/main/java/org/qora/repository/hsqldb/transaction/HSQLDBTransactionRepository.java @@ -43,6 +43,8 @@ public class HSQLDBTransactionRepository implements TransactionRepository { private HSQLDBATTransactionRepository atTransactionRepository; private HSQLDBCreateGroupTransactionRepository createGroupTransactionRepository; private HSQLDBUpdateGroupTransactionRepository updateGroupTransactionRepository; + private HSQLDBAddGroupAdminTransactionRepository addGroupAdminTransactionRepository; + private HSQLDBRemoveGroupAdminTransactionRepository removeGroupAdminTransactionRepository; private HSQLDBJoinGroupTransactionRepository joinGroupTransactionRepository; private HSQLDBLeaveGroupTransactionRepository leaveGroupTransactionRepository; @@ -68,6 +70,8 @@ public class HSQLDBTransactionRepository implements TransactionRepository { this.atTransactionRepository = new HSQLDBATTransactionRepository(repository); this.createGroupTransactionRepository = new HSQLDBCreateGroupTransactionRepository(repository); this.updateGroupTransactionRepository = new HSQLDBUpdateGroupTransactionRepository(repository); + this.addGroupAdminTransactionRepository = new HSQLDBAddGroupAdminTransactionRepository(repository); + this.removeGroupAdminTransactionRepository = new HSQLDBRemoveGroupAdminTransactionRepository(repository); this.joinGroupTransactionRepository = new HSQLDBJoinGroupTransactionRepository(repository); this.leaveGroupTransactionRepository = new HSQLDBLeaveGroupTransactionRepository(repository); } @@ -202,6 +206,12 @@ public class HSQLDBTransactionRepository implements TransactionRepository { case UPDATE_GROUP: return this.updateGroupTransactionRepository.fromBase(signature, reference, creatorPublicKey, timestamp, fee); + case ADD_GROUP_ADMIN: + return this.addGroupAdminTransactionRepository.fromBase(signature, reference, creatorPublicKey, timestamp, fee); + + case REMOVE_GROUP_ADMIN: + return this.removeGroupAdminTransactionRepository.fromBase(signature, reference, creatorPublicKey, timestamp, fee); + case JOIN_GROUP: return this.joinGroupTransactionRepository.fromBase(signature, reference, creatorPublicKey, timestamp, fee); @@ -536,6 +546,14 @@ public class HSQLDBTransactionRepository implements TransactionRepository { this.updateGroupTransactionRepository.save(transactionData); break; + case ADD_GROUP_ADMIN: + this.addGroupAdminTransactionRepository.save(transactionData); + break; + + case REMOVE_GROUP_ADMIN: + this.removeGroupAdminTransactionRepository.save(transactionData); + break; + case JOIN_GROUP: this.joinGroupTransactionRepository.save(transactionData); break; diff --git a/src/main/java/org/qora/transaction/AddGroupAdminTransaction.java b/src/main/java/org/qora/transaction/AddGroupAdminTransaction.java new file mode 100644 index 00000000..60ef8179 --- /dev/null +++ b/src/main/java/org/qora/transaction/AddGroupAdminTransaction.java @@ -0,0 +1,157 @@ +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.AddGroupAdminTransactionData; +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 AddGroupAdminTransaction extends Transaction { + + // Properties + private AddGroupAdminTransactionData addGroupAdminTransactionData; + + // Constructors + + public AddGroupAdminTransaction(Repository repository, TransactionData transactionData) { + super(repository, transactionData); + + this.addGroupAdminTransactionData = (AddGroupAdminTransactionData) 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.getOwner().getAddress())) + return true; + + if (address.equals(this.getMember().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.getOwner().getAddress())) + amount = amount.subtract(this.transactionData.getFee()); + + return amount; + } + + // Navigation + + public Account getOwner() throws DataException { + return new PublicKeyAccount(this.repository, this.addGroupAdminTransactionData.getOwnerPublicKey()); + } + + public Account getMember() throws DataException { + return new Account(this.repository, this.addGroupAdminTransactionData.getMember()); + } + + // Processing + + @Override + public ValidationResult isValid() throws DataException { + // Check group name size bounds + int groupNameLength = Utf8.encodedLength(addGroupAdminTransactionData.getGroupName()); + if (groupNameLength < 1 || groupNameLength > Group.MAX_NAME_SIZE) + return ValidationResult.INVALID_NAME_LENGTH; + + // Check group name is lowercase + if (!addGroupAdminTransactionData.getGroupName().equals(addGroupAdminTransactionData.getGroupName().toLowerCase())) + return ValidationResult.NAME_NOT_LOWER_CASE; + + GroupData groupData = this.repository.getGroupRepository().fromGroupName(addGroupAdminTransactionData.getGroupName()); + + // Check group exists + if (groupData == null) + return ValidationResult.GROUP_DOES_NOT_EXIST; + + Account owner = getOwner(); + + // Check transaction's public key matches group's current owner + if (!owner.getAddress().equals(groupData.getOwner())) + return ValidationResult.INVALID_GROUP_OWNER; + + // Check address is a member + if (!this.repository.getGroupRepository().memberExists(addGroupAdminTransactionData.getGroupName(), owner.getAddress())) + return ValidationResult.NOT_GROUP_MEMBER; + + Account member = getMember(); + + // Check member is not already an admin + if (this.repository.getGroupRepository().adminExists(addGroupAdminTransactionData.getGroupName(), member.getAddress())) + return ValidationResult.ALREADY_GROUP_ADMIN; + + // Check fee is positive + if (addGroupAdminTransactionData.getFee().compareTo(BigDecimal.ZERO) <= 0) + return ValidationResult.NEGATIVE_FEE; + + if (!Arrays.equals(owner.getLastReference(), addGroupAdminTransactionData.getReference())) + return ValidationResult.INVALID_REFERENCE; + + // Check creator has enough funds + if (owner.getConfirmedBalance(Asset.QORA).compareTo(addGroupAdminTransactionData.getFee()) < 0) + return ValidationResult.NO_BALANCE; + + return ValidationResult.OK; + } + + @Override + public void process() throws DataException { + // Update Group adminship + Group group = new Group(this.repository, addGroupAdminTransactionData.getGroupName()); + group.promoteToAdmin(addGroupAdminTransactionData); + + // Save this transaction + this.repository.getTransactionRepository().save(addGroupAdminTransactionData); + + // Update owner's balance + Account owner = getOwner(); + owner.setConfirmedBalance(Asset.QORA, owner.getConfirmedBalance(Asset.QORA).subtract(addGroupAdminTransactionData.getFee())); + + // Update owner's reference + owner.setLastReference(addGroupAdminTransactionData.getSignature()); + } + + @Override + public void orphan() throws DataException { + // Revert group adminship + Group group = new Group(this.repository, addGroupAdminTransactionData.getGroupName()); + group.unpromoteToAdmin(addGroupAdminTransactionData); + + // Delete this transaction itself + this.repository.getTransactionRepository().delete(addGroupAdminTransactionData); + + // Update owner's balance + Account owner = getOwner(); + owner.setConfirmedBalance(Asset.QORA, owner.getConfirmedBalance(Asset.QORA).add(addGroupAdminTransactionData.getFee())); + + // Update owner's reference + owner.setLastReference(addGroupAdminTransactionData.getReference()); + } + +} \ No newline at end of file diff --git a/src/main/java/org/qora/transaction/RemoveGroupAdminTransaction.java b/src/main/java/org/qora/transaction/RemoveGroupAdminTransaction.java new file mode 100644 index 00000000..5ec6da22 --- /dev/null +++ b/src/main/java/org/qora/transaction/RemoveGroupAdminTransaction.java @@ -0,0 +1,153 @@ +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.RemoveGroupAdminTransactionData; +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 RemoveGroupAdminTransaction extends Transaction { + + // Properties + private RemoveGroupAdminTransactionData removeGroupAdminTransactionData; + + // Constructors + + public RemoveGroupAdminTransaction(Repository repository, TransactionData transactionData) { + super(repository, transactionData); + + this.removeGroupAdminTransactionData = (RemoveGroupAdminTransactionData) 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.getOwner().getAddress())) + return true; + + if (address.equals(this.getAdmin().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.getOwner().getAddress())) + amount = amount.subtract(this.transactionData.getFee()); + + return amount; + } + + // Navigation + + public Account getOwner() throws DataException { + return new PublicKeyAccount(this.repository, this.removeGroupAdminTransactionData.getOwnerPublicKey()); + } + + public Account getAdmin() throws DataException { + return new Account(this.repository, this.removeGroupAdminTransactionData.getAdmin()); + } + + // Processing + + @Override + public ValidationResult isValid() throws DataException { + // Check group name size bounds + int groupNameLength = Utf8.encodedLength(removeGroupAdminTransactionData.getGroupName()); + if (groupNameLength < 1 || groupNameLength > Group.MAX_NAME_SIZE) + return ValidationResult.INVALID_NAME_LENGTH; + + // Check group name is lowercase + if (!removeGroupAdminTransactionData.getGroupName().equals(removeGroupAdminTransactionData.getGroupName().toLowerCase())) + return ValidationResult.NAME_NOT_LOWER_CASE; + + GroupData groupData = this.repository.getGroupRepository().fromGroupName(removeGroupAdminTransactionData.getGroupName()); + + // Check group exists + if (groupData == null) + return ValidationResult.GROUP_DOES_NOT_EXIST; + + Account owner = getOwner(); + + // Check transaction's public key matches group's current owner + if (!owner.getAddress().equals(groupData.getOwner())) + return ValidationResult.INVALID_GROUP_OWNER; + + Account admin = getAdmin(); + + // Check member is an admin + if (!this.repository.getGroupRepository().adminExists(removeGroupAdminTransactionData.getGroupName(), admin.getAddress())) + return ValidationResult.NOT_GROUP_ADMIN; + + // Check fee is positive + if (removeGroupAdminTransactionData.getFee().compareTo(BigDecimal.ZERO) <= 0) + return ValidationResult.NEGATIVE_FEE; + + if (!Arrays.equals(owner.getLastReference(), removeGroupAdminTransactionData.getReference())) + return ValidationResult.INVALID_REFERENCE; + + // Check creator has enough funds + if (owner.getConfirmedBalance(Asset.QORA).compareTo(removeGroupAdminTransactionData.getFee()) < 0) + return ValidationResult.NO_BALANCE; + + return ValidationResult.OK; + } + + @Override + public void process() throws DataException { + // Update Group adminship + Group group = new Group(this.repository, removeGroupAdminTransactionData.getGroupName()); + group.demoteFromAdmin(removeGroupAdminTransactionData); + + // Save this transaction + this.repository.getTransactionRepository().save(removeGroupAdminTransactionData); + + // Update owner's balance + Account owner = getOwner(); + owner.setConfirmedBalance(Asset.QORA, owner.getConfirmedBalance(Asset.QORA).subtract(removeGroupAdminTransactionData.getFee())); + + // Update owner's reference + owner.setLastReference(removeGroupAdminTransactionData.getSignature()); + } + + @Override + public void orphan() throws DataException { + // Revert group adminship + Group group = new Group(this.repository, removeGroupAdminTransactionData.getGroupName()); + group.undemoteFromAdmin(removeGroupAdminTransactionData); + + // Delete this transaction itself + this.repository.getTransactionRepository().delete(removeGroupAdminTransactionData); + + // Update owner's balance + Account owner = getOwner(); + owner.setConfirmedBalance(Asset.QORA, owner.getConfirmedBalance(Asset.QORA).add(removeGroupAdminTransactionData.getFee())); + + // Update owner's reference + owner.setLastReference(removeGroupAdminTransactionData.getReference()); + } + +} \ No newline at end of file diff --git a/src/main/java/org/qora/transaction/Transaction.java b/src/main/java/org/qora/transaction/Transaction.java index f7a8e80f..f39cbc7d 100644 --- a/src/main/java/org/qora/transaction/Transaction.java +++ b/src/main/java/org/qora/transaction/Transaction.java @@ -131,6 +131,8 @@ public abstract class Transaction { ALREADY_GROUP_MEMBER(51), GROUP_OWNER_CANNOT_LEAVE(52), NOT_GROUP_MEMBER(53), + ALREADY_GROUP_ADMIN(54), + NOT_GROUP_ADMIN(55), NOT_YET_RELEASED(1000); public final int value; @@ -236,6 +238,12 @@ public abstract class Transaction { case UPDATE_GROUP: return new UpdateGroupTransaction(repository, transactionData); + case ADD_GROUP_ADMIN: + return new AddGroupAdminTransaction(repository, transactionData); + + case REMOVE_GROUP_ADMIN: + return new RemoveGroupAdminTransaction(repository, transactionData); + case JOIN_GROUP: return new JoinGroupTransaction(repository, transactionData); diff --git a/src/main/java/org/qora/transform/transaction/AddGroupAdminTransactionTransformer.java b/src/main/java/org/qora/transform/transaction/AddGroupAdminTransactionTransformer.java new file mode 100644 index 00000000..ecf11357 --- /dev/null +++ b/src/main/java/org/qora/transform/transaction/AddGroupAdminTransactionTransformer.java @@ -0,0 +1,104 @@ +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.AddGroupAdminTransactionData; +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 AddGroupAdminTransactionTransformer extends TransactionTransformer { + + // Property lengths + private static final int OWNER_LENGTH = PUBLIC_KEY_LENGTH; + private static final int NAME_SIZE_LENGTH = INT_LENGTH; + private static final int MEMBER_LENGTH = ADDRESS_LENGTH; + + private static final int TYPELESS_DATALESS_LENGTH = BASE_TYPELESS_LENGTH + OWNER_LENGTH + NAME_SIZE_LENGTH + MEMBER_LENGTH; + + static TransactionData fromByteBuffer(ByteBuffer byteBuffer) throws TransformationException { + long timestamp = byteBuffer.getLong(); + + byte[] reference = new byte[REFERENCE_LENGTH]; + byteBuffer.get(reference); + + byte[] ownerPublicKey = Serialization.deserializePublicKey(byteBuffer); + + String groupName = Serialization.deserializeSizedString(byteBuffer, Group.MAX_NAME_SIZE); + + String member = Serialization.deserializeAddress(byteBuffer); + + BigDecimal fee = Serialization.deserializeBigDecimal(byteBuffer); + + byte[] signature = new byte[SIGNATURE_LENGTH]; + byteBuffer.get(signature); + + return new AddGroupAdminTransactionData(ownerPublicKey, groupName, member, fee, timestamp, reference, signature); + } + + public static int getDataLength(TransactionData transactionData) throws TransformationException { + AddGroupAdminTransactionData addGroupAdminTransactionData = (AddGroupAdminTransactionData) transactionData; + + int dataLength = TYPE_LENGTH + TYPELESS_DATALESS_LENGTH + Utf8.encodedLength(addGroupAdminTransactionData.getGroupName()); + + return dataLength; + } + + public static byte[] toBytes(TransactionData transactionData) throws TransformationException { + try { + AddGroupAdminTransactionData addGroupAdminTransactionData = (AddGroupAdminTransactionData) transactionData; + + ByteArrayOutputStream bytes = new ByteArrayOutputStream(); + + bytes.write(Ints.toByteArray(addGroupAdminTransactionData.getType().value)); + bytes.write(Longs.toByteArray(addGroupAdminTransactionData.getTimestamp())); + bytes.write(addGroupAdminTransactionData.getReference()); + + bytes.write(addGroupAdminTransactionData.getCreatorPublicKey()); + Serialization.serializeSizedString(bytes, addGroupAdminTransactionData.getGroupName()); + Serialization.serializeAddress(bytes, addGroupAdminTransactionData.getMember()); + + Serialization.serializeBigDecimal(bytes, addGroupAdminTransactionData.getFee()); + + if (addGroupAdminTransactionData.getSignature() != null) + bytes.write(addGroupAdminTransactionData.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 { + AddGroupAdminTransactionData addGroupAdminTransactionData = (AddGroupAdminTransactionData) transactionData; + + byte[] ownerPublicKey = addGroupAdminTransactionData.getOwnerPublicKey(); + + json.put("owner", PublicKeyAccount.getAddress(ownerPublicKey)); + json.put("ownerPublicKey", HashCode.fromBytes(ownerPublicKey).toString()); + + json.put("groupName", addGroupAdminTransactionData.getGroupName()); + json.put("member", addGroupAdminTransactionData.getMember()); + } catch (ClassCastException e) { + throw new TransformationException(e); + } + + return json; + } + +} diff --git a/src/main/java/org/qora/transform/transaction/RemoveGroupAdminTransactionTransformer.java b/src/main/java/org/qora/transform/transaction/RemoveGroupAdminTransactionTransformer.java new file mode 100644 index 00000000..f9d8376f --- /dev/null +++ b/src/main/java/org/qora/transform/transaction/RemoveGroupAdminTransactionTransformer.java @@ -0,0 +1,104 @@ +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.RemoveGroupAdminTransactionData; +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 RemoveGroupAdminTransactionTransformer extends TransactionTransformer { + + // Property lengths + private static final int OWNER_LENGTH = PUBLIC_KEY_LENGTH; + private static final int NAME_SIZE_LENGTH = INT_LENGTH; + private static final int MEMBER_LENGTH = ADDRESS_LENGTH; + + private static final int TYPELESS_DATALESS_LENGTH = BASE_TYPELESS_LENGTH + OWNER_LENGTH + NAME_SIZE_LENGTH + MEMBER_LENGTH; + + static TransactionData fromByteBuffer(ByteBuffer byteBuffer) throws TransformationException { + long timestamp = byteBuffer.getLong(); + + byte[] reference = new byte[REFERENCE_LENGTH]; + byteBuffer.get(reference); + + byte[] ownerPublicKey = Serialization.deserializePublicKey(byteBuffer); + + String groupName = Serialization.deserializeSizedString(byteBuffer, Group.MAX_NAME_SIZE); + + String admin = Serialization.deserializeAddress(byteBuffer); + + BigDecimal fee = Serialization.deserializeBigDecimal(byteBuffer); + + byte[] signature = new byte[SIGNATURE_LENGTH]; + byteBuffer.get(signature); + + return new RemoveGroupAdminTransactionData(ownerPublicKey, groupName, admin, fee, timestamp, reference, signature); + } + + public static int getDataLength(TransactionData transactionData) throws TransformationException { + RemoveGroupAdminTransactionData removeGroupAdminTransactionData = (RemoveGroupAdminTransactionData) transactionData; + + int dataLength = TYPE_LENGTH + TYPELESS_DATALESS_LENGTH + Utf8.encodedLength(removeGroupAdminTransactionData.getGroupName()); + + return dataLength; + } + + public static byte[] toBytes(TransactionData transactionData) throws TransformationException { + try { + RemoveGroupAdminTransactionData removeGroupAdminTransactionData = (RemoveGroupAdminTransactionData) transactionData; + + ByteArrayOutputStream bytes = new ByteArrayOutputStream(); + + bytes.write(Ints.toByteArray(removeGroupAdminTransactionData.getType().value)); + bytes.write(Longs.toByteArray(removeGroupAdminTransactionData.getTimestamp())); + bytes.write(removeGroupAdminTransactionData.getReference()); + + bytes.write(removeGroupAdminTransactionData.getCreatorPublicKey()); + Serialization.serializeSizedString(bytes, removeGroupAdminTransactionData.getGroupName()); + Serialization.serializeAddress(bytes, removeGroupAdminTransactionData.getAdmin()); + + Serialization.serializeBigDecimal(bytes, removeGroupAdminTransactionData.getFee()); + + if (removeGroupAdminTransactionData.getSignature() != null) + bytes.write(removeGroupAdminTransactionData.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 { + RemoveGroupAdminTransactionData removeGroupAdminTransactionData = (RemoveGroupAdminTransactionData) transactionData; + + byte[] ownerPublicKey = removeGroupAdminTransactionData.getOwnerPublicKey(); + + json.put("owner", PublicKeyAccount.getAddress(ownerPublicKey)); + json.put("ownerPublicKey", HashCode.fromBytes(ownerPublicKey).toString()); + + json.put("groupName", removeGroupAdminTransactionData.getGroupName()); + json.put("admin", removeGroupAdminTransactionData.getAdmin()); + } 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 221ffe92..9b1d5666 100644 --- a/src/main/java/org/qora/transform/transaction/TransactionTransformer.java +++ b/src/main/java/org/qora/transform/transaction/TransactionTransformer.java @@ -100,6 +100,12 @@ public class TransactionTransformer extends Transformer { case UPDATE_GROUP: return UpdateGroupTransactionTransformer.fromByteBuffer(byteBuffer); + case ADD_GROUP_ADMIN: + return AddGroupAdminTransactionTransformer.fromByteBuffer(byteBuffer); + + case REMOVE_GROUP_ADMIN: + return RemoveGroupAdminTransactionTransformer.fromByteBuffer(byteBuffer); + case JOIN_GROUP: return JoinGroupTransactionTransformer.fromByteBuffer(byteBuffer); @@ -173,6 +179,12 @@ public class TransactionTransformer extends Transformer { case UPDATE_GROUP: return UpdateGroupTransactionTransformer.getDataLength(transactionData); + case ADD_GROUP_ADMIN: + return AddGroupAdminTransactionTransformer.getDataLength(transactionData); + + case REMOVE_GROUP_ADMIN: + return RemoveGroupAdminTransactionTransformer.getDataLength(transactionData); + case JOIN_GROUP: return JoinGroupTransactionTransformer.getDataLength(transactionData); @@ -243,6 +255,12 @@ public class TransactionTransformer extends Transformer { case UPDATE_GROUP: return UpdateGroupTransactionTransformer.toBytes(transactionData); + case ADD_GROUP_ADMIN: + return AddGroupAdminTransactionTransformer.toBytes(transactionData); + + case REMOVE_GROUP_ADMIN: + return RemoveGroupAdminTransactionTransformer.toBytes(transactionData); + case JOIN_GROUP: return JoinGroupTransactionTransformer.toBytes(transactionData); @@ -322,6 +340,12 @@ public class TransactionTransformer extends Transformer { case UPDATE_GROUP: return UpdateGroupTransactionTransformer.toBytesForSigningImpl(transactionData); + case ADD_GROUP_ADMIN: + return AddGroupAdminTransactionTransformer.toBytesForSigningImpl(transactionData); + + case REMOVE_GROUP_ADMIN: + return RemoveGroupAdminTransactionTransformer.toBytesForSigningImpl(transactionData); + case JOIN_GROUP: return JoinGroupTransactionTransformer.toBytesForSigningImpl(transactionData); @@ -413,6 +437,12 @@ public class TransactionTransformer extends Transformer { case UPDATE_GROUP: return UpdateGroupTransactionTransformer.toJSON(transactionData); + case ADD_GROUP_ADMIN: + return AddGroupAdminTransactionTransformer.toJSON(transactionData); + + case REMOVE_GROUP_ADMIN: + return RemoveGroupAdminTransactionTransformer.toJSON(transactionData); + case JOIN_GROUP: return JoinGroupTransactionTransformer.toJSON(transactionData);