diff --git a/src/main/java/org/qora/api/resource/ApiDefinition.java b/src/main/java/org/qora/api/resource/ApiDefinition.java index 1a6fd4b4..6ff1153f 100644 --- a/src/main/java/org/qora/api/resource/ApiDefinition.java +++ b/src/main/java/org/qora/api/resource/ApiDefinition.java @@ -13,6 +13,7 @@ import io.swagger.v3.oas.annotations.tags.Tag; @Tag(name = "Admin"), @Tag(name = "Assets"), @Tag(name = "Blocks"), + @Tag(name = "Groups"), @Tag(name = "Names"), @Tag(name = "Payments"), @Tag(name = "Transactions"), diff --git a/src/main/java/org/qora/api/resource/GroupsResource.java b/src/main/java/org/qora/api/resource/GroupsResource.java new file mode 100644 index 00000000..bdaa3319 --- /dev/null +++ b/src/main/java/org/qora/api/resource/GroupsResource.java @@ -0,0 +1,216 @@ +package org.qora.api.resource; + +import io.swagger.v3.oas.annotations.Operation; +import io.swagger.v3.oas.annotations.Parameter; +import io.swagger.v3.oas.annotations.media.ArraySchema; +import io.swagger.v3.oas.annotations.media.Content; +import io.swagger.v3.oas.annotations.media.Schema; +import io.swagger.v3.oas.annotations.parameters.RequestBody; +import io.swagger.v3.oas.annotations.responses.ApiResponse; +import io.swagger.v3.oas.annotations.tags.Tag; + +import java.util.List; + +import javax.servlet.http.HttpServletRequest; +import javax.ws.rs.GET; +import javax.ws.rs.POST; +import javax.ws.rs.Path; +import javax.ws.rs.PathParam; +import javax.ws.rs.Produces; +import javax.ws.rs.QueryParam; +import javax.ws.rs.core.Context; +import javax.ws.rs.core.MediaType; + +import org.qora.api.ApiError; +import org.qora.api.ApiErrors; +import org.qora.api.ApiExceptionFactory; +import org.qora.crypto.Crypto; +import org.qora.data.group.GroupData; +import org.qora.data.transaction.CreateGroupTransactionData; +import org.qora.data.transaction.UpdateGroupTransactionData; +import org.qora.repository.DataException; +import org.qora.repository.Repository; +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.CreateGroupTransactionTransformer; +import org.qora.transform.transaction.UpdateGroupTransactionTransformer; +import org.qora.utils.Base58; + +@Path("/groups") +@Produces({MediaType.APPLICATION_JSON, MediaType.TEXT_PLAIN}) +@Tag(name = "Groups") +public class GroupsResource { + + @Context + HttpServletRequest request; + + @GET + @Operation( + summary = "List all groups", + responses = { + @ApiResponse( + description = "group info", + content = @Content( + mediaType = MediaType.APPLICATION_JSON, + array = @ArraySchema(schema = @Schema(implementation = GroupData.class)) + ) + ) + } + ) + @ApiErrors({ApiError.REPOSITORY_ISSUE}) + public List getAllGroups(@Parameter(ref = "limit") @QueryParam("limit") int limit, @Parameter(ref = "offset") @QueryParam("offset") int offset) { + try (final Repository repository = RepositoryManager.getRepository()) { + List groups = repository.getGroupRepository().getAllGroups(); + + // Pagination would take effect here (or as part of the repository access) + int fromIndex = Integer.min(offset, groups.size()); + int toIndex = limit == 0 ? groups.size() : Integer.min(fromIndex + limit, groups.size()); + groups = groups.subList(fromIndex, toIndex); + + return groups; + } catch (DataException e) { + throw ApiExceptionFactory.INSTANCE.createException(request, ApiError.REPOSITORY_ISSUE, e); + } + } + + @GET + @Path("/address/{address}") + @Operation( + summary = "List all groups owned by address", + responses = { + @ApiResponse( + description = "group info", + content = @Content( + mediaType = MediaType.APPLICATION_JSON, + array = @ArraySchema(schema = @Schema(implementation = GroupData.class)) + ) + ) + } + ) + @ApiErrors({ApiError.INVALID_ADDRESS, ApiError.REPOSITORY_ISSUE}) + public List getGroupsByAddress(@PathParam("address") String address) { + if (!Crypto.isValidAddress(address)) + throw ApiExceptionFactory.INSTANCE.createException(request, ApiError.INVALID_ADDRESS); + + try (final Repository repository = RepositoryManager.getRepository()) { + List groups = repository.getGroupRepository().getGroupsByOwner(address); + + return groups; + } catch (DataException e) { + throw ApiExceptionFactory.INSTANCE.createException(request, ApiError.REPOSITORY_ISSUE, e); + } + } + + @GET + @Path("/{groupname}") + @Operation( + summary = "Info on group", + responses = { + @ApiResponse( + description = "group info", + content = @Content( + mediaType = MediaType.APPLICATION_JSON, + schema = @Schema(implementation = GroupData.class) + ) + ) + } + ) + @ApiErrors({ApiError.REPOSITORY_ISSUE}) + public GroupData getGroup(@PathParam("groupname") String groupName) { + try (final Repository repository = RepositoryManager.getRepository()) { + return repository.getGroupRepository().fromGroupName(groupName); + } catch (DataException e) { + throw ApiExceptionFactory.INSTANCE.createException(request, ApiError.REPOSITORY_ISSUE, e); + } + } + + + @POST + @Path("/create") + @Operation( + summary = "Build raw, unsigned, CREATE_GROUP transaction", + requestBody = @RequestBody( + required = true, + content = @Content( + mediaType = MediaType.APPLICATION_JSON, + schema = @Schema( + implementation = CreateGroupTransactionData.class + ) + ) + ), + responses = { + @ApiResponse( + description = "raw, unsigned, CREATE_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 createGroup(CreateGroupTransactionData 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 = CreateGroupTransactionTransformer.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("/update") + @Operation( + summary = "Build raw, unsigned, UPDATE_GROUP transaction", + requestBody = @RequestBody( + required = true, + content = @Content( + mediaType = MediaType.APPLICATION_JSON, + schema = @Schema( + implementation = UpdateGroupTransactionData.class + ) + ) + ), + responses = { + @ApiResponse( + description = "raw, unsigned, UPDATE_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 updateGroup(UpdateGroupTransactionData 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 = UpdateGroupTransactionTransformer.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/GroupData.java b/src/main/java/org/qora/data/group/GroupData.java new file mode 100644 index 00000000..58b532f9 --- /dev/null +++ b/src/main/java/org/qora/data/group/GroupData.java @@ -0,0 +1,89 @@ +package org.qora.data.group; + +import javax.xml.bind.annotation.XmlAccessType; +import javax.xml.bind.annotation.XmlAccessorType; + +// All properties to be converted to JSON via JAX-RS +@XmlAccessorType(XmlAccessType.FIELD) +public class GroupData { + + // Properties + private String owner; + private String groupName; + private String description; + private long created; + private Long updated; + private boolean isOpen; + private byte[] reference; + + // Constructors + + // necessary for JAX-RS serialization + protected GroupData() { + } + + public GroupData(String owner, String name, String description, long created, Long updated, boolean isOpen, byte[] reference) { + this.owner = owner; + this.groupName = name; + this.description = description; + this.created = created; + this.updated = updated; + this.isOpen = isOpen; + this.reference = reference; + } + + public GroupData(String owner, String name, String description, long created, boolean isOpen, byte[] reference) { + this(owner, name, description, created, null, isOpen, reference); + } + + // Getters / setters + + public String getOwner() { + return this.owner; + } + + public void setOwner(String owner) { + this.owner = owner; + } + + public String getGroupName() { + return this.groupName; + } + + public String getDescription() { + return this.description; + } + + public void setDescription(String description) { + this.description = description; + } + + public long getCreated() { + return this.created; + } + + public Long getUpdated() { + return this.updated; + } + + public void setUpdated(Long updated) { + this.updated = updated; + } + + public byte[] getReference() { + return this.reference; + } + + public void setReference(byte[] reference) { + this.reference = reference; + } + + public boolean getIsOpen() { + return this.isOpen; + } + + public void setIsOpen(boolean isOpen) { + this.isOpen = isOpen; + } + +} diff --git a/src/main/java/org/qora/data/transaction/CreateGroupTransactionData.java b/src/main/java/org/qora/data/transaction/CreateGroupTransactionData.java new file mode 100644 index 00000000..12bad802 --- /dev/null +++ b/src/main/java/org/qora/data/transaction/CreateGroupTransactionData.java @@ -0,0 +1,80 @@ +package org.qora.data.transaction; + +import java.math.BigDecimal; + +import javax.xml.bind.annotation.XmlAccessType; +import javax.xml.bind.annotation.XmlAccessorType; +import javax.xml.bind.annotation.XmlElement; + +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 CreateGroupTransactionData extends TransactionData { + + @Schema(description = "group owner's address", example = "QgV4s3xnzLhVBEJxcYui4u4q11yhUHsd9v") + private String owner; + @Schema(description = "group name", example = "miner-group") + private String groupName; + @Schema(description = "short description of group", example = "this group is for block miners") + private String description; + @Schema(description = "whether anyone can join group (open) or group is invite-only (closed)", example = "true") + private boolean isOpen; + + // Constructors + + // For JAX-RS + protected CreateGroupTransactionData() { + super(TransactionType.CREATE_GROUP); + } + + public CreateGroupTransactionData(byte[] creatorPublicKey, String owner, String groupName, String description, boolean isOpen, BigDecimal fee, long timestamp, byte[] reference, + byte[] signature) { + super(TransactionType.CREATE_GROUP, fee, creatorPublicKey, timestamp, reference, signature); + + this.creatorPublicKey = creatorPublicKey; + this.owner = owner; + this.groupName = groupName; + this.description = description; + this.isOpen = isOpen; + } + + public CreateGroupTransactionData(byte[] creatorPublicKey, String owner, String groupName, String description, boolean isOpen, BigDecimal fee, long timestamp, byte[] reference) { + this(creatorPublicKey, owner, groupName, description, isOpen, fee, timestamp, reference, null); + } + + // Getters / setters + + public String getOwner() { + return this.owner; + } + + public String getGroupName() { + return this.groupName; + } + + public String getDescription() { + return this.description; + } + + public boolean getIsOpen() { + return this.isOpen; + } + + // Re-expose creatorPublicKey for this transaction type for JAXB + @XmlElement(name = "creatorPublicKey") + @Schema(name = "creatorPublicKey", description = "group creator's public key", example = "2tiMr5LTpaWCgbRvkPK8TFd7k63DyHJMMFFsz9uBf1ZP") + public byte[] getGroupCreatorPublicKey() { + return this.creatorPublicKey; + } + + @XmlElement(name = "creatorPublicKey") + @Schema(name = "creatorPublicKey", description = "group creator's public key", example = "2tiMr5LTpaWCgbRvkPK8TFd7k63DyHJMMFFsz9uBf1ZP") + public void setGroupCreatorPublicKey(byte[] creatorPublicKey) { + this.creatorPublicKey = creatorPublicKey; + } + +} diff --git a/src/main/java/org/qora/data/transaction/TransactionData.java b/src/main/java/org/qora/data/transaction/TransactionData.java index 98bc9029..7e9051f9 100644 --- a/src/main/java/org/qora/data/transaction/TransactionData.java +++ b/src/main/java/org/qora/data/transaction/TransactionData.java @@ -27,10 +27,13 @@ import io.swagger.v3.oas.annotations.media.Schema.AccessMode; */ @XmlClassExtractor(TransactionClassExtractor.class) -@XmlSeeAlso({ArbitraryTransactionData.class, ATTransactionData.class, BuyNameTransactionData.class, CancelOrderTransactionData.class, CancelSellNameTransactionData.class, - CreateOrderTransactionData.class, CreatePollTransactionData.class, DeployATTransactionData.class, GenesisTransactionData.class, IssueAssetTransactionData.class, - MessageTransactionData.class, MultiPaymentTransactionData.class, PaymentTransactionData.class, RegisterNameTransactionData.class, SellNameTransactionData.class, - TransferAssetTransactionData.class, UpdateNameTransactionData.class, VoteOnPollTransactionData.class}) +@XmlSeeAlso({GenesisTransactionData.class, PaymentTransactionData.class, RegisterNameTransactionData.class, UpdateNameTransactionData.class, + SellNameTransactionData.class, CancelSellNameTransactionData.class, BuyNameTransactionData.class, + CreatePollTransactionData.class, VoteOnPollTransactionData.class, ArbitraryTransactionData.class, + IssueAssetTransactionData.class, TransferAssetTransactionData.class, + CreateOrderTransactionData.class, CancelOrderTransactionData.class, + MultiPaymentTransactionData.class, DeployATTransactionData.class, MessageTransactionData.class, ATTransactionData.class, + CreateGroupTransactionData.class, UpdateGroupTransactionData.class}) //All properties to be converted to JSON via JAX-RS @XmlAccessorType(XmlAccessType.FIELD) public abstract class TransactionData { diff --git a/src/main/java/org/qora/data/transaction/UpdateGroupTransactionData.java b/src/main/java/org/qora/data/transaction/UpdateGroupTransactionData.java new file mode 100644 index 00000000..19d66d22 --- /dev/null +++ b/src/main/java/org/qora/data/transaction/UpdateGroupTransactionData.java @@ -0,0 +1,98 @@ +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 UpdateGroupTransactionData extends TransactionData { + + // Properties + @Schema(description = "owner's public key", example = "2tiMr5LTpaWCgbRvkPK8TFd7k63DyHJMMFFsz9uBf1ZP") + private byte[] ownerPublicKey; + @Schema(description = "new owner's address", example = "QgV4s3xnzLhVBEJxcYui4u4q11yhUHsd9v") + private String newOwner; + @Schema(description = "which group to update", example = "my-group") + private String groupName; + @Schema(description = "replacement group description", example = "my group for accounts I like") + private String newDescription; + @Schema(description = "new group join policy", example = "true") + private boolean newIsOpen; + // For internal use when orphaning + @XmlTransient + @Schema(hidden = true) + private byte[] groupReference; + + // Constructors + + // For JAX-RS + protected UpdateGroupTransactionData() { + super(TransactionType.UPDATE_GROUP); + } + + public void afterUnmarshal(Unmarshaller u, Object parent) { + this.creatorPublicKey = this.ownerPublicKey; + } + + public UpdateGroupTransactionData(byte[] ownerPublicKey, String groupName, String newOwner, String newDescription, boolean newIsOpen, byte[] groupReference, BigDecimal fee, long timestamp, + byte[] reference, byte[] signature) { + super(TransactionType.UPDATE_GROUP, fee, ownerPublicKey, timestamp, reference, signature); + + this.ownerPublicKey = ownerPublicKey; + this.newOwner = newOwner; + this.groupName = groupName; + this.newDescription = newDescription; + this.newIsOpen = newIsOpen; + this.groupReference = groupReference; + } + + public UpdateGroupTransactionData(byte[] ownerPublicKey, String groupName, String newOwner, String newDescription, boolean newIsOpen, BigDecimal fee, long timestamp, byte[] reference, + byte[] signature) { + this(ownerPublicKey, groupName, newOwner, newDescription, newIsOpen, null, fee, timestamp, reference, signature); + } + + public UpdateGroupTransactionData(byte[] ownerPublicKey, String groupName, String newOwner, String newDescription, boolean newIsOpen, byte[] groupReference, BigDecimal fee, long timestamp, + byte[] reference) { + this(ownerPublicKey, groupName, newOwner, newDescription, newIsOpen, groupReference, fee, timestamp, reference, null); + } + + // Getters / setters + + public byte[] getOwnerPublicKey() { + return this.ownerPublicKey; + } + + public String getNewOwner() { + return this.newOwner; + } + + public String getGroupName() { + return this.groupName; + } + + public String getNewDescription() { + return this.newDescription; + } + + public boolean getNewIsOpen() { + return this.newIsOpen; + } + + public byte[] getGroupReference() { + return this.groupReference; + } + + public void setGroupReference(byte[] groupReference) { + this.groupReference = groupReference; + } + +} diff --git a/src/main/java/org/qora/group/Group.java b/src/main/java/org/qora/group/Group.java new file mode 100644 index 00000000..39b4c9e8 --- /dev/null +++ b/src/main/java/org/qora/group/Group.java @@ -0,0 +1,112 @@ +package org.qora.group; + +import org.qora.data.group.GroupData; +import org.qora.data.transaction.CreateGroupTransactionData; +import org.qora.data.transaction.TransactionData; +import org.qora.data.transaction.UpdateGroupTransactionData; +import org.qora.repository.DataException; +import org.qora.repository.Repository; + +public class Group { + + // Properties + private Repository repository; + private GroupData groupData; + + // Useful constants + public static final int MAX_NAME_SIZE = 400; + public static final int MAX_DESCRIPTION_SIZE = 4000; + + // Constructors + + /** + * Construct Group business object using info from create group transaction. + * + * @param repository + * @param createGroupTransactionData + */ + public Group(Repository repository, CreateGroupTransactionData createGroupTransactionData) { + this.repository = repository; + this.groupData = new GroupData(createGroupTransactionData.getOwner(), + createGroupTransactionData.getGroupName(), createGroupTransactionData.getDescription(), createGroupTransactionData.getTimestamp(), + createGroupTransactionData.getIsOpen(), createGroupTransactionData.getSignature()); + } + + /** + * Construct Group business object using existing group in repository. + * + * @param repository + * @param groupName + * @throws DataException + */ + public Group(Repository repository, String groupName) throws DataException { + this.repository = repository; + this.groupData = this.repository.getGroupRepository().fromGroupName(groupName); + } + + // Processing + + public void create() throws DataException { + this.repository.getGroupRepository().save(this.groupData); + } + + public void uncreate() throws DataException { + this.repository.getGroupRepository().delete(this.groupData.getGroupName()); + } + + private void revert() throws DataException { + TransactionData previousTransactionData = this.repository.getTransactionRepository().fromSignature(this.groupData.getReference()); + if (previousTransactionData == null) + throw new DataException("Unable to revert group transaction as referenced transaction not found in repository"); + + switch (previousTransactionData.getType()) { + case CREATE_GROUP: + CreateGroupTransactionData previousCreateGroupTransactionData = (CreateGroupTransactionData) previousTransactionData; + this.groupData.setOwner(previousCreateGroupTransactionData.getOwner()); + this.groupData.setDescription(previousCreateGroupTransactionData.getDescription()); + this.groupData.setIsOpen(previousCreateGroupTransactionData.getIsOpen()); + this.groupData.setUpdated(null); + break; + + case UPDATE_GROUP: + UpdateGroupTransactionData previousUpdateGroupTransactionData = (UpdateGroupTransactionData) previousTransactionData; + this.groupData.setOwner(previousUpdateGroupTransactionData.getNewOwner()); + this.groupData.setDescription(previousUpdateGroupTransactionData.getNewDescription()); + this.groupData.setIsOpen(previousUpdateGroupTransactionData.getNewIsOpen()); + this.groupData.setUpdated(previousUpdateGroupTransactionData.getTimestamp()); + break; + + default: + throw new IllegalStateException("Unable to revert group transaction due to unsupported referenced transaction"); + } + } + + public void update(UpdateGroupTransactionData updateGroupTransactionData) throws DataException { + // Update reference in transaction data + updateGroupTransactionData.setGroupReference(this.groupData.getReference()); + + // New group reference is this transaction's signature + this.groupData.setReference(updateGroupTransactionData.getSignature()); + + // Update Group's owner and description + this.groupData.setOwner(updateGroupTransactionData.getNewOwner()); + this.groupData.setDescription(updateGroupTransactionData.getNewDescription()); + this.groupData.setIsOpen(updateGroupTransactionData.getNewIsOpen()); + this.groupData.setUpdated(updateGroupTransactionData.getTimestamp()); + + // Save updated group data + this.repository.getGroupRepository().save(this.groupData); + } + + public void revert(UpdateGroupTransactionData updateGroupTransactionData) throws DataException { + // Previous group reference is taken from this transaction's cached copy + this.groupData.setReference(updateGroupTransactionData.getGroupReference()); + + // Previous Group's owner and/or description taken from referenced transaction + this.revert(); + + // Save reverted group data + this.repository.getGroupRepository().save(this.groupData); + } + +} diff --git a/src/main/java/org/qora/orphan.java b/src/main/java/org/qora/orphan.java index 8b801850..1b19c148 100644 --- a/src/main/java/org/qora/orphan.java +++ b/src/main/java/org/qora/orphan.java @@ -1,4 +1,7 @@ package org.qora; +import java.security.Security; + +import org.bouncycastle.jce.provider.BouncyCastleProvider; import org.qora.block.Block; import org.qora.block.BlockChain; import org.qora.controller.Controller; @@ -8,6 +11,7 @@ import org.qora.repository.Repository; import org.qora.repository.RepositoryFactory; import org.qora.repository.RepositoryManager; import org.qora.repository.hsqldb.HSQLDBRepositoryFactory; +import org.qora.settings.Settings; public class orphan { @@ -19,6 +23,11 @@ public class orphan { int targetHeight = Integer.parseInt(args[0]); + Security.insertProviderAt(new BouncyCastleProvider(), 0); + + // Load/check settings, which potentially sets up blockchain config, etc. + Settings.getInstance(); + try { RepositoryFactory repositoryFactory = new HSQLDBRepositoryFactory(Controller.connectionUrl); RepositoryManager.setRepositoryFactory(repositoryFactory); diff --git a/src/main/java/org/qora/repository/GroupRepository.java b/src/main/java/org/qora/repository/GroupRepository.java new file mode 100644 index 00000000..57d5a34f --- /dev/null +++ b/src/main/java/org/qora/repository/GroupRepository.java @@ -0,0 +1,21 @@ +package org.qora.repository; + +import java.util.List; + +import org.qora.data.group.GroupData; + +public interface GroupRepository { + + public GroupData fromGroupName(String groupName) throws DataException; + + public boolean groupExists(String groupName) throws DataException; + + public List getAllGroups() throws DataException; + + public List getGroupsByOwner(String address) throws DataException; + + public void save(GroupData groupData) throws DataException; + + public void delete(String groupName) throws DataException; + +} diff --git a/src/main/java/org/qora/repository/Repository.java b/src/main/java/org/qora/repository/Repository.java index c5407d7b..5b87717c 100644 --- a/src/main/java/org/qora/repository/Repository.java +++ b/src/main/java/org/qora/repository/Repository.java @@ -10,6 +10,8 @@ public interface Repository extends AutoCloseable { public BlockRepository getBlockRepository(); + public GroupRepository getGroupRepository(); + public NameRepository getNameRepository(); public TransactionRepository getTransactionRepository(); diff --git a/src/main/java/org/qora/repository/hsqldb/HSQLDBDatabaseUpdates.java b/src/main/java/org/qora/repository/hsqldb/HSQLDBDatabaseUpdates.java index b9dd6401..474c79d9 100644 --- a/src/main/java/org/qora/repository/hsqldb/HSQLDBDatabaseUpdates.java +++ b/src/main/java/org/qora/repository/hsqldb/HSQLDBDatabaseUpdates.java @@ -73,6 +73,7 @@ public class HSQLDBDatabaseUpdates { switch (databaseVersion) { case 0: // create from new + // FYI: "UCC" in HSQLDB means "upper-case comparison", i.e. case-insensitive stmt.execute("SET DATABASE SQL NAMES TRUE"); // SQL keywords cannot be used as DB object names, e.g. table names stmt.execute("SET DATABASE SQL SYNTAX MYS TRUE"); // Required for our use of INSERT ... ON DUPLICATE KEY UPDATE ... syntax stmt.execute("SET DATABASE SQL RESTRICT EXEC TRUE"); // No multiple-statement execute() or DDL/DML executeQuery() @@ -89,6 +90,7 @@ public class HSQLDBDatabaseUpdates { stmt.execute("CREATE TYPE QoraAddress AS VARCHAR(36)"); stmt.execute("CREATE TYPE QoraPublicKey AS VARBINARY(32)"); stmt.execute("CREATE TYPE QoraAmount AS DECIMAL(27, 8)"); + stmt.execute("CREATE TYPE GenericDescription AS VARCHAR(4000)"); stmt.execute("CREATE TYPE RegisteredName AS VARCHAR(400) COLLATE SQL_TEXT_NO_PAD"); stmt.execute("CREATE TYPE NameData AS VARCHAR(4000)"); stmt.execute("CREATE TYPE PollName AS VARCHAR(400) COLLATE SQL_TEXT_NO_PAD"); @@ -104,6 +106,7 @@ public class HSQLDBDatabaseUpdates { stmt.execute("CREATE TYPE ATState AS BLOB(1M)"); // 16bit * 8 + 16bit * 4 + 16bit * 4 stmt.execute("CREATE TYPE ATStateHash as VARBINARY(32)"); stmt.execute("CREATE TYPE ATMessage AS VARBINARY(256)"); + stmt.execute("CREATE TYPE GroupName AS VARCHAR(400) COLLATE SQL_TEXT_UCC_NO_PAD"); break; case 1: @@ -210,7 +213,7 @@ public class HSQLDBDatabaseUpdates { case 10: // Create Poll Transactions stmt.execute("CREATE TABLE CreatePollTransactions (signature Signature, creator QoraPublicKey NOT NULL, owner QoraAddress NOT NULL, " - + "poll_name PollName NOT NULL, description VARCHAR(4000) NOT NULL, " + + "poll_name PollName NOT NULL, description GenericDescription NOT NULL, " + "PRIMARY KEY (signature), FOREIGN KEY (signature) REFERENCES Transactions (signature) ON DELETE CASCADE)"); // Poll options. NB: option is implicitly NON NULL and UNIQUE due to being part of compound primary key stmt.execute("CREATE TABLE CreatePollTransactionOptions (signature Signature, option_index TINYINT NOT NULL, option_name PollOption, " @@ -245,7 +248,7 @@ public class HSQLDBDatabaseUpdates { // Issue Asset Transactions stmt.execute( "CREATE TABLE IssueAssetTransactions (signature Signature, issuer QoraPublicKey NOT NULL, owner QoraAddress NOT NULL, asset_name AssetName NOT NULL, " - + "description VARCHAR(4000) NOT NULL, quantity BIGINT NOT NULL, is_divisible BOOLEAN NOT NULL, asset_id AssetID, " + + "description GenericDescription NOT NULL, quantity BIGINT NOT NULL, is_divisible BOOLEAN NOT NULL, asset_id AssetID, " + "PRIMARY KEY (signature), FOREIGN KEY (signature) REFERENCES Transactions (signature) ON DELETE CASCADE)"); // For the future: maybe convert quantity from BIGINT to QoraAmount, regardless of divisibility break; @@ -298,7 +301,7 @@ public class HSQLDBDatabaseUpdates { case 21: // Assets (including QORA coin itself) stmt.execute("CREATE TABLE Assets (asset_id AssetID, owner QoraAddress NOT NULL, " - + "asset_name AssetName NOT NULL, description VARCHAR(4000) NOT NULL, " + + "asset_name AssetName NOT NULL, description GenericDescription NOT NULL, " + "quantity BIGINT NOT NULL, is_divisible BOOLEAN NOT NULL, reference Signature NOT NULL, PRIMARY KEY (asset_id))"); // We need a corresponding trigger to make sure new asset_id values are assigned sequentially stmt.execute( @@ -342,7 +345,7 @@ public class HSQLDBDatabaseUpdates { case 25: // Polls/Voting stmt.execute( - "CREATE TABLE Polls (poll_name PollName, description VARCHAR(4000) NOT NULL, creator QoraPublicKey NOT NULL, owner QoraAddress NOT NULL, " + "CREATE TABLE Polls (poll_name PollName, description GenericDescription NOT NULL, creator QoraPublicKey NOT NULL, owner QoraAddress NOT NULL, " + "published TIMESTAMP WITH TIME ZONE NOT NULL, " + "PRIMARY KEY (poll_name))"); // Various options available on a poll stmt.execute("CREATE TABLE PollOptions (poll_name PollName, option_index TINYINT NOT NULL, option_name PollOption, " @@ -390,7 +393,7 @@ public class HSQLDBDatabaseUpdates { break; case 28: - // XXX TEMP fix until database rebuild + // XXX TEMP fixes to registered names - remove before database rebuild! // Allow name_reference to be NULL while transaction is unconfirmed stmt.execute("ALTER TABLE UpdateNameTransactions ALTER COLUMN name_reference SET NULL"); stmt.execute("ALTER TABLE BuyNameTransactions ALTER COLUMN name_reference SET NULL"); @@ -398,6 +401,55 @@ public class HSQLDBDatabaseUpdates { stmt.execute("ALTER TABLE Names DROP COLUMN registrant"); break; + case 29: + // XXX TEMP bridging statements for AccountGroups - remove before database rebuild! + stmt.execute("CREATE TYPE GenericDescription AS VARCHAR(4000)"); + stmt.execute("CREATE TYPE GroupName AS VARCHAR(400) COLLATE SQL_TEXT_UCC_NO_PAD"); + break; + + case 30: + // Account groups + stmt.execute("CREATE TABLE AccountGroups (group_name GroupName, owner QoraAddress NOT NULL, description GenericDescription NOT NULL, " + + "created TIMESTAMP WITH TIME ZONE NOT NULL, updated TIMESTAMP WITH TIME ZONE, is_open BOOLEAN NOT NULL, " + + "reference Signature, PRIMARY KEY (group_name))"); + // For finding groups by owner + stmt.execute("CREATE INDEX AccountGroupOwnerIndex on AccountGroups (owner)"); + // Admins + stmt.execute("CREATE TABLE AccountGroupAdmins (group_name GroupName, admin QoraAddress, 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))"); + // For finding groups that address is member + stmt.execute("CREATE INDEX AccountGroupMemberIndex on AccountGroupMembers (address)"); + + // Invites + // PRIMARY KEY (invitee + group + inviter) because most queries will be "what have I been invited to?" from UI + stmt.execute("CREATE TABLE AccountGroupInvites (group_name GroupName, invitee QoraAddress, inviter QoraAddress, " + + "expiry TIMESTAMP WITH TIME ZONE NOT NULL, PRIMARY KEY (invitee, group_name, inviter))"); + // For finding invites sent by inviter + stmt.execute("CREATE INDEX AccountGroupSentInviteIndex on AccountGroupInvites (inviter)"); + // For finding invites by group + stmt.execute("CREATE INDEX AccountGroupInviteIndex on AccountGroupInvites (group_name)"); + + // Bans + // NULL expiry means does not expire! + stmt.execute("CREATE TABLE AccountGroupBans (group_name GroupName, offender QoraAddress, admin QoraAddress NOT NULL, banned TIMESTAMP WITH TIME ZONE NOT NULL, " + + "reason GenericDescription NOT NULL, expiry TIMESTAMP WITH TIME ZONE, PRIMARY KEY (group_name, offender))"); + // For expiry maintenance + stmt.execute("CREATE INDEX AccountGroupBanExpiryIndex on AccountGroupBans (expiry)"); + break; + + case 31: + // Account group transactions + stmt.execute("CREATE TABLE CreateGroupTransactions (signature Signature, creator QoraPublicKey NOT NULL, group_name GroupName NOT NULL, " + + "owner QoraAddress NOT NULL, description GenericDescription NOT NULL, is_open BOOLEAN NOT NULL, " + + "PRIMARY KEY (signature), FOREIGN KEY (signature) REFERENCES Transactions (signature) ON DELETE CASCADE)"); + 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; + default: // nothing to do return false; diff --git a/src/main/java/org/qora/repository/hsqldb/HSQLDBGroupRepository.java b/src/main/java/org/qora/repository/hsqldb/HSQLDBGroupRepository.java new file mode 100644 index 00000000..ff498e9a --- /dev/null +++ b/src/main/java/org/qora/repository/hsqldb/HSQLDBGroupRepository.java @@ -0,0 +1,144 @@ +package org.qora.repository.hsqldb; + +import java.sql.ResultSet; +import java.sql.SQLException; +import java.sql.Timestamp; +import java.util.ArrayList; +import java.util.Calendar; +import java.util.List; + +import org.qora.data.group.GroupData; +import org.qora.repository.DataException; +import org.qora.repository.GroupRepository; + +public class HSQLDBGroupRepository implements GroupRepository { + + protected HSQLDBRepository repository; + + public HSQLDBGroupRepository(HSQLDBRepository repository) { + this.repository = repository; + } + + @Override + public GroupData fromGroupName(String groupName) throws DataException { + try (ResultSet resultSet = this.repository + .checkedExecute("SELECT owner, description, created, updated, reference, is_open FROM AccountGroups WHERE group_name = ?", groupName)) { + if (resultSet == null) + return null; + + String owner = resultSet.getString(1); + String description = resultSet.getString(2); + long created = resultSet.getTimestamp(3, Calendar.getInstance(HSQLDBRepository.UTC)).getTime(); + + // Special handling for possibly-NULL "updated" column + Timestamp updatedTimestamp = resultSet.getTimestamp(4, Calendar.getInstance(HSQLDBRepository.UTC)); + Long updated = resultSet.wasNull() ? null : updatedTimestamp.getTime(); + + byte[] reference = resultSet.getBytes(5); + boolean isOpen = resultSet.getBoolean(6); + + return new GroupData(owner, groupName, description, created, updated, isOpen, reference); + } catch (SQLException e) { + throw new DataException("Unable to fetch group info from repository", e); + } + } + + @Override + public boolean groupExists(String groupName) throws DataException { + try { + return this.repository.exists("AccountGroups", "group_name = ?", groupName); + } catch (SQLException e) { + throw new DataException("Unable to check for group in repository", e); + } + } + + @Override + public List getAllGroups() throws DataException { + List groups = new ArrayList<>(); + + try (ResultSet resultSet = this.repository + .checkedExecute("SELECT group_name, description, owner, created, updated, reference, is_open FROM AccountGroups")) { + if (resultSet == null) + return groups; + + do { + String groupName = resultSet.getString(1); + String description = resultSet.getString(2); + String owner = resultSet.getString(3); + long created = resultSet.getTimestamp(4, Calendar.getInstance(HSQLDBRepository.UTC)).getTime(); + + // Special handling for possibly-NULL "updated" column + Timestamp updatedTimestamp = resultSet.getTimestamp(5, Calendar.getInstance(HSQLDBRepository.UTC)); + Long updated = resultSet.wasNull() ? null : updatedTimestamp.getTime(); + + byte[] reference = resultSet.getBytes(6); + boolean isOpen = resultSet.getBoolean(7); + + groups.add(new GroupData(owner, groupName, description, created, updated, isOpen, reference)); + } while (resultSet.next()); + + return groups; + } catch (SQLException e) { + throw new DataException("Unable to fetch groups from repository", e); + } + } + + @Override + public List getGroupsByOwner(String owner) throws DataException { + List groups = new ArrayList<>(); + + try (ResultSet resultSet = this.repository + .checkedExecute("SELECT group_name, description, created, updated, reference, is_open FROM AccountGroups WHERE owner = ?", owner)) { + if (resultSet == null) + return groups; + + do { + String groupName = resultSet.getString(1); + String description = resultSet.getString(2); + long created = resultSet.getTimestamp(3, Calendar.getInstance(HSQLDBRepository.UTC)).getTime(); + + // Special handling for possibly-NULL "updated" column + Timestamp updatedTimestamp = resultSet.getTimestamp(4, Calendar.getInstance(HSQLDBRepository.UTC)); + Long updated = updatedTimestamp == null ? null : updatedTimestamp.getTime(); + + byte[] reference = resultSet.getBytes(5); + boolean isOpen = resultSet.getBoolean(6); + + groups.add(new GroupData(owner, groupName, description, created, updated, isOpen, reference)); + } while (resultSet.next()); + + return groups; + } catch (SQLException e) { + throw new DataException("Unable to fetch account's groups from repository", e); + } + } + + @Override + public void save(GroupData groupData) throws DataException { + HSQLDBSaver saveHelper = new HSQLDBSaver("AccountGroups"); + + // Special handling for "updated" timestamp + 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()); + + try { + saveHelper.execute(this.repository); + } catch (SQLException e) { + throw new DataException("Unable to save group info into repository", e); + } + } + + @Override + public void delete(String groupName) throws DataException { + try { + this.repository.delete("AccountGroups", "group_name = ?", groupName); + } catch (SQLException e) { + throw new DataException("Unable to delete group info from repository", e); + } + } + +} diff --git a/src/main/java/org/qora/repository/hsqldb/HSQLDBRepository.java b/src/main/java/org/qora/repository/hsqldb/HSQLDBRepository.java index 90d4f3f9..94082f44 100644 --- a/src/main/java/org/qora/repository/hsqldb/HSQLDBRepository.java +++ b/src/main/java/org/qora/repository/hsqldb/HSQLDBRepository.java @@ -14,6 +14,7 @@ import org.qora.repository.ATRepository; import org.qora.repository.AccountRepository; import org.qora.repository.AssetRepository; import org.qora.repository.BlockRepository; +import org.qora.repository.GroupRepository; import org.qora.repository.DataException; import org.qora.repository.NameRepository; import org.qora.repository.Repository; @@ -57,6 +58,11 @@ public class HSQLDBRepository implements Repository { return new HSQLDBBlockRepository(this); } + @Override + public GroupRepository getGroupRepository() { + return new HSQLDBGroupRepository(this); + } + @Override public NameRepository getNameRepository() { return new HSQLDBNameRepository(this); diff --git a/src/main/java/org/qora/repository/hsqldb/transaction/HSQLDBCreateGroupTransactionRepository.java b/src/main/java/org/qora/repository/hsqldb/transaction/HSQLDBCreateGroupTransactionRepository.java new file mode 100644 index 00000000..d51cd9d5 --- /dev/null +++ b/src/main/java/org/qora/repository/hsqldb/transaction/HSQLDBCreateGroupTransactionRepository.java @@ -0,0 +1,53 @@ +package org.qora.repository.hsqldb.transaction; + +import java.math.BigDecimal; +import java.sql.ResultSet; +import java.sql.SQLException; + +import org.qora.data.transaction.CreateGroupTransactionData; +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 HSQLDBCreateGroupTransactionRepository extends HSQLDBTransactionRepository { + + public HSQLDBCreateGroupTransactionRepository(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 owner, group_name, description, is_open FROM CreateGroupTransactions WHERE signature = ?", + signature)) { + if (resultSet == null) + return null; + + String owner = resultSet.getString(1); + String groupName = resultSet.getString(2); + String description = resultSet.getString(3); + boolean isOpen = resultSet.getBoolean(4); + + return new CreateGroupTransactionData(creatorPublicKey, owner, groupName, description, isOpen, fee, timestamp, reference, signature); + } catch (SQLException e) { + throw new DataException("Unable to fetch create group transaction from repository", e); + } + } + + @Override + public void save(TransactionData transactionData) throws DataException { + CreateGroupTransactionData createGroupTransactionData = (CreateGroupTransactionData) transactionData; + + HSQLDBSaver saveHelper = new HSQLDBSaver("CreateGroupTransactions"); + + saveHelper.bind("signature", createGroupTransactionData.getSignature()).bind("creator", createGroupTransactionData.getCreatorPublicKey()) + .bind("owner", createGroupTransactionData.getOwner()).bind("group_name", createGroupTransactionData.getGroupName()) + .bind("description", createGroupTransactionData.getDescription()).bind("is_open", createGroupTransactionData.getIsOpen()); + + try { + saveHelper.execute(this.repository); + } catch (SQLException e) { + throw new DataException("Unable to save create 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 565be0b3..68d3e0cd 100644 --- a/src/main/java/org/qora/repository/hsqldb/transaction/HSQLDBTransactionRepository.java +++ b/src/main/java/org/qora/repository/hsqldb/transaction/HSQLDBTransactionRepository.java @@ -41,6 +41,8 @@ public class HSQLDBTransactionRepository implements TransactionRepository { private HSQLDBDeployATTransactionRepository deployATTransactionRepository; private HSQLDBMessageTransactionRepository messageTransactionRepository; private HSQLDBATTransactionRepository atTransactionRepository; + private HSQLDBCreateGroupTransactionRepository createGroupTransactionRepository; + private HSQLDBUpdateGroupTransactionRepository updateGroupTransactionRepository; public HSQLDBTransactionRepository(HSQLDBRepository repository) { this.repository = repository; @@ -62,6 +64,8 @@ public class HSQLDBTransactionRepository implements TransactionRepository { this.deployATTransactionRepository = new HSQLDBDeployATTransactionRepository(repository); this.messageTransactionRepository = new HSQLDBMessageTransactionRepository(repository); this.atTransactionRepository = new HSQLDBATTransactionRepository(repository); + this.createGroupTransactionRepository = new HSQLDBCreateGroupTransactionRepository(repository); + this.updateGroupTransactionRepository = new HSQLDBUpdateGroupTransactionRepository(repository); } protected HSQLDBTransactionRepository() { @@ -188,6 +192,12 @@ public class HSQLDBTransactionRepository implements TransactionRepository { case AT: return this.atTransactionRepository.fromBase(signature, reference, creatorPublicKey, timestamp, fee); + case CREATE_GROUP: + return this.createGroupTransactionRepository.fromBase(signature, reference, creatorPublicKey, timestamp, fee); + + case UPDATE_GROUP: + return this.updateGroupTransactionRepository.fromBase(signature, reference, creatorPublicKey, timestamp, fee); + default: throw new DataException("Unsupported transaction type [" + type.name() + "] during fetch from HSQLDB repository"); } @@ -508,6 +518,14 @@ public class HSQLDBTransactionRepository implements TransactionRepository { this.atTransactionRepository.save(transactionData); break; + case CREATE_GROUP: + this.createGroupTransactionRepository.save(transactionData); + break; + + case UPDATE_GROUP: + this.updateGroupTransactionRepository.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/repository/hsqldb/transaction/HSQLDBUpdateGroupTransactionRepository.java b/src/main/java/org/qora/repository/hsqldb/transaction/HSQLDBUpdateGroupTransactionRepository.java new file mode 100644 index 00000000..58c28728 --- /dev/null +++ b/src/main/java/org/qora/repository/hsqldb/transaction/HSQLDBUpdateGroupTransactionRepository.java @@ -0,0 +1,55 @@ +package org.qora.repository.hsqldb.transaction; + +import java.math.BigDecimal; +import java.sql.ResultSet; +import java.sql.SQLException; + +import org.qora.data.transaction.UpdateGroupTransactionData; +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 HSQLDBUpdateGroupTransactionRepository extends HSQLDBTransactionRepository { + + public HSQLDBUpdateGroupTransactionRepository(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, new_owner, new_description, new_is_open, group_reference FROM UpdateGroupTransactions WHERE signature = ?", + signature)) { + if (resultSet == null) + return null; + + String groupName = resultSet.getString(1); + String newOwner = resultSet.getString(2); + String newDescription = resultSet.getString(3); + boolean newIsOpen = resultSet.getBoolean(4); + byte[] groupReference = resultSet.getBytes(5); + + return new UpdateGroupTransactionData(creatorPublicKey, groupName, newOwner, newDescription, newIsOpen, groupReference, fee, timestamp, reference, signature); + } catch (SQLException e) { + throw new DataException("Unable to fetch update group transaction from repository", e); + } + } + + @Override + public void save(TransactionData transactionData) throws DataException { + UpdateGroupTransactionData updateGroupTransactionData = (UpdateGroupTransactionData) transactionData; + + HSQLDBSaver saveHelper = new HSQLDBSaver("UpdateGroupTransactions"); + + saveHelper.bind("signature", updateGroupTransactionData.getSignature()).bind("owner", updateGroupTransactionData.getOwnerPublicKey()) + .bind("group_name", updateGroupTransactionData.getGroupName()).bind("new_owner", updateGroupTransactionData.getNewOwner()) + .bind("new_description", updateGroupTransactionData.getNewDescription()).bind("new_is_open", updateGroupTransactionData.getNewIsOpen()) + .bind("group_reference", updateGroupTransactionData.getGroupReference()); + + try { + saveHelper.execute(this.repository); + } catch (SQLException e) { + throw new DataException("Unable to save update group transaction into repository", e); + } + } + +} diff --git a/src/main/java/org/qora/transaction/CreateGroupTransaction.java b/src/main/java/org/qora/transaction/CreateGroupTransaction.java new file mode 100644 index 00000000..e7745b5c --- /dev/null +++ b/src/main/java/org/qora/transaction/CreateGroupTransaction.java @@ -0,0 +1,146 @@ +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.asset.Asset; +import org.qora.crypto.Crypto; +import org.qora.data.transaction.CreateGroupTransactionData; +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 CreateGroupTransaction extends Transaction { + + // Properties + private CreateGroupTransactionData createGroupTransactionData; + + // Constructors + + public CreateGroupTransaction(Repository repository, TransactionData transactionData) { + super(repository, transactionData); + + this.createGroupTransactionData = (CreateGroupTransactionData) this.transactionData; + } + + // More information + + @Override + public List getRecipientAccounts() throws DataException { + return Collections.singletonList(getOwner()); + } + + @Override + public boolean isInvolved(Account account) throws DataException { + String address = account.getAddress(); + + if (address.equals(this.getCreator().getAddress())) + return true; + + if (address.equals(this.getOwner().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.getCreator().getAddress())) + amount = amount.subtract(this.transactionData.getFee()); + + return amount; + } + + // Navigation + + public Account getOwner() throws DataException { + return new Account(this.repository, this.createGroupTransactionData.getOwner()); + } + + // Processing + + @Override + public ValidationResult isValid() throws DataException { + // Check owner address is valid + if (!Crypto.isValidAddress(createGroupTransactionData.getOwner())) + return ValidationResult.INVALID_ADDRESS; + + // Check group name size bounds + int groupNameLength = Utf8.encodedLength(createGroupTransactionData.getGroupName()); + if (groupNameLength < 1 || groupNameLength > Group.MAX_NAME_SIZE) + return ValidationResult.INVALID_NAME_LENGTH; + + // Check description size bounds + int descriptionLength = Utf8.encodedLength(createGroupTransactionData.getDescription()); + if (descriptionLength < 1 || descriptionLength > Group.MAX_DESCRIPTION_SIZE) + return ValidationResult.INVALID_DESCRIPTION_LENGTH; + + // Check group name is lowercase + if (!createGroupTransactionData.getGroupName().equals(createGroupTransactionData.getGroupName().toLowerCase())) + return ValidationResult.NAME_NOT_LOWER_CASE; + + // Check the group name isn't already taken + if (this.repository.getGroupRepository().groupExists(createGroupTransactionData.getGroupName())) + return ValidationResult.GROUP_ALREADY_EXISTS; + + // Check fee is positive + if (createGroupTransactionData.getFee().compareTo(BigDecimal.ZERO) <= 0) + return ValidationResult.NEGATIVE_FEE; + + // Check reference is correct + Account creator = getCreator(); + + if (!Arrays.equals(creator.getLastReference(), createGroupTransactionData.getReference())) + return ValidationResult.INVALID_REFERENCE; + + // Check creator has enough funds + if (creator.getConfirmedBalance(Asset.QORA).compareTo(createGroupTransactionData.getFee()) < 0) + return ValidationResult.NO_BALANCE; + + return ValidationResult.OK; + } + + @Override + public void process() throws DataException { + // Create Group + Group group = new Group(this.repository, createGroupTransactionData); + group.create(); + + // Save this transaction + this.repository.getTransactionRepository().save(createGroupTransactionData); + + // Update creator's balance + Account creator = getCreator(); + creator.setConfirmedBalance(Asset.QORA, creator.getConfirmedBalance(Asset.QORA).subtract(createGroupTransactionData.getFee())); + + // Update creator's reference + creator.setLastReference(createGroupTransactionData.getSignature()); + } + + @Override + public void orphan() throws DataException { + // Uncreate group + Group group = new Group(this.repository, createGroupTransactionData.getGroupName()); + group.uncreate(); + + // Delete this transaction itself + this.repository.getTransactionRepository().delete(createGroupTransactionData); + + // Update creator's balance + Account creator = getCreator(); + creator.setConfirmedBalance(Asset.QORA, creator.getConfirmedBalance(Asset.QORA).add(createGroupTransactionData.getFee())); + + // Update creator's reference + creator.setLastReference(createGroupTransactionData.getReference()); + } + +} diff --git a/src/main/java/org/qora/transaction/Transaction.java b/src/main/java/org/qora/transaction/Transaction.java index 6a0bb2ba..caa459ce 100644 --- a/src/main/java/org/qora/transaction/Transaction.java +++ b/src/main/java/org/qora/transaction/Transaction.java @@ -52,7 +52,18 @@ public abstract class Transaction { DELEGATION(18), SUPERNODE(19), AIRDROP(20), - AT(21); + AT(21), + CREATE_GROUP(22), + UPDATE_GROUP(23), + ADD_GROUP_ADMIN(24), + REMOVE_GROUP_ADMIN(25), + GROUP_BAN(26), + GROUP_UNBAN(27), + GROUP_KICK(28), + GROUP_INVITE(29), + CANCEL_GROUP_INVITE(30), + JOIN_GROUP(31), + LEAVE_GROUP(32); public final int value; @@ -114,6 +125,9 @@ public abstract class Transaction { TIMESTAMP_TOO_OLD(45), TIMESTAMP_TOO_NEW(46), TOO_MANY_UNCONFIRMED(47), + GROUP_ALREADY_EXISTS(48), + GROUP_DOES_NOT_EXIST(49), + INVALID_GROUP_OWNER(50), NOT_YET_RELEASED(1000); public final int value; @@ -213,6 +227,12 @@ public abstract class Transaction { case AT: return new ATTransaction(repository, transactionData); + case CREATE_GROUP: + return new CreateGroupTransaction(repository, transactionData); + + case UPDATE_GROUP: + return new UpdateGroupTransaction(repository, transactionData); + default: throw new IllegalStateException("Unsupported transaction type [" + transactionData.getType().value + "] during fetch from repository"); } diff --git a/src/main/java/org/qora/transaction/UpdateGroupTransaction.java b/src/main/java/org/qora/transaction/UpdateGroupTransaction.java new file mode 100644 index 00000000..94e1031b --- /dev/null +++ b/src/main/java/org/qora/transaction/UpdateGroupTransaction.java @@ -0,0 +1,159 @@ +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.crypto.Crypto; +import org.qora.data.transaction.UpdateGroupTransactionData; +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 UpdateGroupTransaction extends Transaction { + + // Properties + private UpdateGroupTransactionData updateGroupTransactionData; + + // Constructors + + public UpdateGroupTransaction(Repository repository, TransactionData transactionData) { + super(repository, transactionData); + + this.updateGroupTransactionData = (UpdateGroupTransactionData) this.transactionData; + } + + // More information + + @Override + public List getRecipientAccounts() throws DataException { + return Collections.singletonList(getNewOwner()); + } + + @Override + public boolean isInvolved(Account account) throws DataException { + String address = account.getAddress(); + + if (address.equals(this.getOwner().getAddress())) + return true; + + if (address.equals(this.getNewOwner().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.updateGroupTransactionData.getOwnerPublicKey()); + } + + public Account getNewOwner() throws DataException { + return new Account(this.repository, this.updateGroupTransactionData.getNewOwner()); + } + + // Processing + + @Override + public ValidationResult isValid() throws DataException { + // Check new owner address is valid + if (!Crypto.isValidAddress(updateGroupTransactionData.getNewOwner())) + return ValidationResult.INVALID_ADDRESS; + + // Check group name size bounds + int groupNameLength = Utf8.encodedLength(updateGroupTransactionData.getGroupName()); + if (groupNameLength < 1 || groupNameLength > Group.MAX_NAME_SIZE) + return ValidationResult.INVALID_NAME_LENGTH; + + // Check new description size bounds + int newDescriptionLength = Utf8.encodedLength(updateGroupTransactionData.getNewDescription()); + if (newDescriptionLength < 1 || newDescriptionLength > Group.MAX_DESCRIPTION_SIZE) + return ValidationResult.INVALID_DESCRIPTION_LENGTH; + + // Check group name is lowercase + if (!updateGroupTransactionData.getGroupName().equals(updateGroupTransactionData.getGroupName().toLowerCase())) + return ValidationResult.NAME_NOT_LOWER_CASE; + + GroupData groupData = this.repository.getGroupRepository().fromGroupName(updateGroupTransactionData.getGroupName()); + + // Check group exists + if (groupData == null) + return ValidationResult.GROUP_DOES_NOT_EXIST; + + // Check transaction's public key matches group's current owner + Account owner = getOwner(); + if (!owner.getAddress().equals(groupData.getOwner())) + return ValidationResult.INVALID_GROUP_OWNER; + + // Check fee is positive + if (updateGroupTransactionData.getFee().compareTo(BigDecimal.ZERO) <= 0) + return ValidationResult.NEGATIVE_FEE; + + // Check reference is correct + Account creator = getCreator(); + + if (!Arrays.equals(creator.getLastReference(), updateGroupTransactionData.getReference())) + return ValidationResult.INVALID_REFERENCE; + + // Check creator has enough funds + if (creator.getConfirmedBalance(Asset.QORA).compareTo(updateGroupTransactionData.getFee()) < 0) + return ValidationResult.NO_BALANCE; + + return ValidationResult.OK; + } + + @Override + public void process() throws DataException { + // Update Group + Group group = new Group(this.repository, updateGroupTransactionData.getGroupName()); + group.update(updateGroupTransactionData); + + // Save this transaction, now with updated "group reference" to previous transaction that updated group + this.repository.getTransactionRepository().save(updateGroupTransactionData); + + // Update owner's balance + Account owner = getOwner(); + owner.setConfirmedBalance(Asset.QORA, owner.getConfirmedBalance(Asset.QORA).subtract(updateGroupTransactionData.getFee())); + + // Update owner's reference + owner.setLastReference(updateGroupTransactionData.getSignature()); + } + + @Override + public void orphan() throws DataException { + // Revert name + Group group = new Group(this.repository, updateGroupTransactionData.getGroupName()); + group.revert(updateGroupTransactionData); + + // Delete this transaction itself + this.repository.getTransactionRepository().delete(updateGroupTransactionData); + + // Update owner's balance + Account owner = getOwner(); + owner.setConfirmedBalance(Asset.QORA, owner.getConfirmedBalance(Asset.QORA).add(updateGroupTransactionData.getFee())); + + // Update owner's reference + owner.setLastReference(updateGroupTransactionData.getReference()); + } + +} diff --git a/src/main/java/org/qora/transform/transaction/CreateGroupTransactionTransformer.java b/src/main/java/org/qora/transform/transaction/CreateGroupTransactionTransformer.java new file mode 100644 index 00000000..0f449a13 --- /dev/null +++ b/src/main/java/org/qora/transform/transaction/CreateGroupTransactionTransformer.java @@ -0,0 +1,116 @@ +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.CreateGroupTransactionData; +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 CreateGroupTransactionTransformer extends TransactionTransformer { + + // Property lengths + private static final int CREATOR_LENGTH = PUBLIC_KEY_LENGTH; + private static final int OWNER_LENGTH = ADDRESS_LENGTH; + private static final int NAME_SIZE_LENGTH = INT_LENGTH; + private static final int DESCRIPTION_SIZE_LENGTH = INT_LENGTH; + private static final int IS_OPEN_LENGTH = BOOLEAN_LENGTH; + + private static final int TYPELESS_DATALESS_LENGTH = BASE_TYPELESS_LENGTH + CREATOR_LENGTH + OWNER_LENGTH + NAME_SIZE_LENGTH + DESCRIPTION_SIZE_LENGTH + IS_OPEN_LENGTH; + + static TransactionData fromByteBuffer(ByteBuffer byteBuffer) throws TransformationException { + long timestamp = byteBuffer.getLong(); + + byte[] reference = new byte[REFERENCE_LENGTH]; + byteBuffer.get(reference); + + byte[] creatorPublicKey = Serialization.deserializePublicKey(byteBuffer); + + String owner = Serialization.deserializeAddress(byteBuffer); + + String groupName = Serialization.deserializeSizedString(byteBuffer, Group.MAX_NAME_SIZE); + + String description = Serialization.deserializeSizedString(byteBuffer, Group.MAX_DESCRIPTION_SIZE); + + boolean isOpen = byteBuffer.get() != 0; + + BigDecimal fee = Serialization.deserializeBigDecimal(byteBuffer); + + byte[] signature = new byte[SIGNATURE_LENGTH]; + byteBuffer.get(signature); + + return new CreateGroupTransactionData(creatorPublicKey, owner, groupName, description, isOpen, fee, timestamp, reference, signature); + } + + public static int getDataLength(TransactionData transactionData) throws TransformationException { + CreateGroupTransactionData createGroupTransactionData = (CreateGroupTransactionData) transactionData; + + int dataLength = TYPE_LENGTH + TYPELESS_DATALESS_LENGTH + Utf8.encodedLength(createGroupTransactionData.getGroupName()) + + Utf8.encodedLength(createGroupTransactionData.getDescription()); + + return dataLength; + } + + public static byte[] toBytes(TransactionData transactionData) throws TransformationException { + try { + CreateGroupTransactionData createGroupTransactionData = (CreateGroupTransactionData) transactionData; + + ByteArrayOutputStream bytes = new ByteArrayOutputStream(); + + bytes.write(Ints.toByteArray(createGroupTransactionData.getType().value)); + bytes.write(Longs.toByteArray(createGroupTransactionData.getTimestamp())); + bytes.write(createGroupTransactionData.getReference()); + + bytes.write(createGroupTransactionData.getCreatorPublicKey()); + Serialization.serializeAddress(bytes, createGroupTransactionData.getOwner()); + Serialization.serializeSizedString(bytes, createGroupTransactionData.getGroupName()); + Serialization.serializeSizedString(bytes, createGroupTransactionData.getDescription()); + + bytes.write((byte) (createGroupTransactionData.getIsOpen() ? 1 : 0)); + + Serialization.serializeBigDecimal(bytes, createGroupTransactionData.getFee()); + + if (createGroupTransactionData.getSignature() != null) + bytes.write(createGroupTransactionData.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 { + CreateGroupTransactionData createGroupTransactionData = (CreateGroupTransactionData) transactionData; + + byte[] creatorPublicKey = createGroupTransactionData.getCreatorPublicKey(); + + json.put("creator", PublicKeyAccount.getAddress(creatorPublicKey)); + json.put("creatorPublicKey", HashCode.fromBytes(creatorPublicKey).toString()); + + json.put("owner", createGroupTransactionData.getOwner()); + json.put("groupName", createGroupTransactionData.getGroupName()); + json.put("description", createGroupTransactionData.getDescription()); + json.put("isOpen", createGroupTransactionData.getIsOpen()); + } 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 ee244d34..1c2c3c29 100644 --- a/src/main/java/org/qora/transform/transaction/TransactionTransformer.java +++ b/src/main/java/org/qora/transform/transaction/TransactionTransformer.java @@ -94,6 +94,12 @@ public class TransactionTransformer extends Transformer { case DEPLOY_AT: return DeployATTransactionTransformer.fromByteBuffer(byteBuffer); + case CREATE_GROUP: + return CreateGroupTransactionTransformer.fromByteBuffer(byteBuffer); + + case UPDATE_GROUP: + return UpdateGroupTransactionTransformer.fromByteBuffer(byteBuffer); + default: throw new TransformationException("Unsupported transaction type [" + type.value + "] during conversion from bytes"); } @@ -155,6 +161,12 @@ public class TransactionTransformer extends Transformer { case DEPLOY_AT: return DeployATTransactionTransformer.getDataLength(transactionData); + case CREATE_GROUP: + return CreateGroupTransactionTransformer.getDataLength(transactionData); + + case UPDATE_GROUP: + return UpdateGroupTransactionTransformer.getDataLength(transactionData); + default: throw new TransformationException("Unsupported transaction type [" + transactionData.getType().value + "] when requesting byte length"); } @@ -213,6 +225,12 @@ public class TransactionTransformer extends Transformer { case DEPLOY_AT: return DeployATTransactionTransformer.toBytes(transactionData); + case CREATE_GROUP: + return CreateGroupTransactionTransformer.toBytes(transactionData); + + case UPDATE_GROUP: + return UpdateGroupTransactionTransformer.toBytes(transactionData); + default: throw new TransformationException("Unsupported transaction type [" + transactionData.getType().value + "] during conversion to bytes"); } @@ -280,6 +298,12 @@ public class TransactionTransformer extends Transformer { case DEPLOY_AT: return DeployATTransactionTransformer.toBytesForSigningImpl(transactionData); + case CREATE_GROUP: + return CreateGroupTransactionTransformer.toBytesForSigningImpl(transactionData); + + case UPDATE_GROUP: + return UpdateGroupTransactionTransformer.toBytesForSigningImpl(transactionData); + default: throw new TransformationException( "Unsupported transaction type [" + transactionData.getType().value + "] during conversion to bytes for signing"); @@ -359,6 +383,12 @@ public class TransactionTransformer extends Transformer { case DEPLOY_AT: return DeployATTransactionTransformer.toJSON(transactionData); + case CREATE_GROUP: + return CreateGroupTransactionTransformer.toJSON(transactionData); + + case UPDATE_GROUP: + return UpdateGroupTransactionTransformer.toJSON(transactionData); + default: throw new TransformationException("Unsupported transaction type [" + transactionData.getType().value + "] during conversion to JSON"); } diff --git a/src/main/java/org/qora/transform/transaction/UpdateGroupTransactionTransformer.java b/src/main/java/org/qora/transform/transaction/UpdateGroupTransactionTransformer.java new file mode 100644 index 00000000..9d741594 --- /dev/null +++ b/src/main/java/org/qora/transform/transaction/UpdateGroupTransactionTransformer.java @@ -0,0 +1,117 @@ +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.UpdateGroupTransactionData; +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 UpdateGroupTransactionTransformer extends TransactionTransformer { + + // Property lengths + private static final int OWNER_LENGTH = PUBLIC_KEY_LENGTH; + private static final int NEW_OWNER_LENGTH = ADDRESS_LENGTH; + private static final int NAME_SIZE_LENGTH = INT_LENGTH; + private static final int NEW_DESCRIPTION_SIZE_LENGTH = INT_LENGTH; + private static final int NEW_IS_OPEN_LENGTH = BOOLEAN_LENGTH; + + private static final int TYPELESS_DATALESS_LENGTH = BASE_TYPELESS_LENGTH + OWNER_LENGTH + NEW_OWNER_LENGTH + NAME_SIZE_LENGTH + NEW_DESCRIPTION_SIZE_LENGTH + NEW_IS_OPEN_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 newOwner = Serialization.deserializeAddress(byteBuffer); + + String newDescription = Serialization.deserializeSizedString(byteBuffer, Group.MAX_DESCRIPTION_SIZE); + + boolean newIsOpen = byteBuffer.get() != 0; + + BigDecimal fee = Serialization.deserializeBigDecimal(byteBuffer); + + byte[] signature = new byte[SIGNATURE_LENGTH]; + byteBuffer.get(signature); + + return new UpdateGroupTransactionData(ownerPublicKey, groupName, newOwner, newDescription, newIsOpen, fee, timestamp, reference, signature); + } + + public static int getDataLength(TransactionData transactionData) throws TransformationException { + UpdateGroupTransactionData updateGroupTransactionData = (UpdateGroupTransactionData) transactionData; + + int dataLength = TYPE_LENGTH + TYPELESS_DATALESS_LENGTH + Utf8.encodedLength(updateGroupTransactionData.getGroupName()) + + Utf8.encodedLength(updateGroupTransactionData.getNewDescription()); + + return dataLength; + } + + public static byte[] toBytes(TransactionData transactionData) throws TransformationException { + try { + UpdateGroupTransactionData updateGroupTransactionData = (UpdateGroupTransactionData) transactionData; + + ByteArrayOutputStream bytes = new ByteArrayOutputStream(); + + bytes.write(Ints.toByteArray(updateGroupTransactionData.getType().value)); + bytes.write(Longs.toByteArray(updateGroupTransactionData.getTimestamp())); + bytes.write(updateGroupTransactionData.getReference()); + + bytes.write(updateGroupTransactionData.getCreatorPublicKey()); + Serialization.serializeSizedString(bytes, updateGroupTransactionData.getGroupName()); + + Serialization.serializeAddress(bytes, updateGroupTransactionData.getNewOwner()); + Serialization.serializeSizedString(bytes, updateGroupTransactionData.getNewDescription()); + + bytes.write((byte) (updateGroupTransactionData.getNewIsOpen() ? 1 : 0)); + + Serialization.serializeBigDecimal(bytes, updateGroupTransactionData.getFee()); + + if (updateGroupTransactionData.getSignature() != null) + bytes.write(updateGroupTransactionData.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 { + UpdateGroupTransactionData updateGroupTransactionData = (UpdateGroupTransactionData) transactionData; + + byte[] ownerPublicKey = updateGroupTransactionData.getOwnerPublicKey(); + + json.put("owner", PublicKeyAccount.getAddress(ownerPublicKey)); + json.put("ownerPublicKey", HashCode.fromBytes(ownerPublicKey).toString()); + + json.put("groupName", updateGroupTransactionData.getGroupName()); + json.put("newOwner", updateGroupTransactionData.getNewOwner()); + json.put("newDescription", updateGroupTransactionData.getNewDescription()); + json.put("newIsOpen", updateGroupTransactionData.getNewIsOpen()); + } catch (ClassCastException e) { + throw new TransformationException(e); + } + + return json; + } + +}