Browse Source

Account groups: create group + update group + API calls

(Also minor fix for orphan.java).

Note use of afterUnmarshal() in TransactionData-subclass to replace trash code in Transaction-subclass constructor.
See UpdateGroupTransactionData.afterUnmarshal() compared to RegisterNameTransaction constructor.
split-DB
catbref 6 years ago
parent
commit
83abede8ab
  1. 1
      src/main/java/org/qora/api/resource/ApiDefinition.java
  2. 216
      src/main/java/org/qora/api/resource/GroupsResource.java
  3. 89
      src/main/java/org/qora/data/group/GroupData.java
  4. 80
      src/main/java/org/qora/data/transaction/CreateGroupTransactionData.java
  5. 11
      src/main/java/org/qora/data/transaction/TransactionData.java
  6. 98
      src/main/java/org/qora/data/transaction/UpdateGroupTransactionData.java
  7. 112
      src/main/java/org/qora/group/Group.java
  8. 9
      src/main/java/org/qora/orphan.java
  9. 21
      src/main/java/org/qora/repository/GroupRepository.java
  10. 2
      src/main/java/org/qora/repository/Repository.java
  11. 62
      src/main/java/org/qora/repository/hsqldb/HSQLDBDatabaseUpdates.java
  12. 144
      src/main/java/org/qora/repository/hsqldb/HSQLDBGroupRepository.java
  13. 6
      src/main/java/org/qora/repository/hsqldb/HSQLDBRepository.java
  14. 53
      src/main/java/org/qora/repository/hsqldb/transaction/HSQLDBCreateGroupTransactionRepository.java
  15. 18
      src/main/java/org/qora/repository/hsqldb/transaction/HSQLDBTransactionRepository.java
  16. 55
      src/main/java/org/qora/repository/hsqldb/transaction/HSQLDBUpdateGroupTransactionRepository.java
  17. 146
      src/main/java/org/qora/transaction/CreateGroupTransaction.java
  18. 22
      src/main/java/org/qora/transaction/Transaction.java
  19. 159
      src/main/java/org/qora/transaction/UpdateGroupTransaction.java
  20. 116
      src/main/java/org/qora/transform/transaction/CreateGroupTransactionTransformer.java
  21. 30
      src/main/java/org/qora/transform/transaction/TransactionTransformer.java
  22. 117
      src/main/java/org/qora/transform/transaction/UpdateGroupTransactionTransformer.java

1
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"),

216
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<GroupData> getAllGroups(@Parameter(ref = "limit") @QueryParam("limit") int limit, @Parameter(ref = "offset") @QueryParam("offset") int offset) {
try (final Repository repository = RepositoryManager.getRepository()) {
List<GroupData> 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<GroupData> getGroupsByAddress(@PathParam("address") String address) {
if (!Crypto.isValidAddress(address))
throw ApiExceptionFactory.INSTANCE.createException(request, ApiError.INVALID_ADDRESS);
try (final Repository repository = RepositoryManager.getRepository()) {
List<GroupData> 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);
}
}
}

89
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;
}
}

80
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;
}
}

11
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 {

98
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;
}
}

112
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);
}
}

9
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);

21
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<GroupData> getAllGroups() throws DataException;
public List<GroupData> getGroupsByOwner(String address) throws DataException;
public void save(GroupData groupData) throws DataException;
public void delete(String groupName) throws DataException;
}

2
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();

62
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;

144
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<GroupData> getAllGroups() throws DataException {
List<GroupData> 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<GroupData> getGroupsByOwner(String owner) throws DataException {
List<GroupData> 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);
}
}
}

6
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);

53
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);
}
}
}

18
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");
}

55
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);
}
}
}

146
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<Account> 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());
}
}

22
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");
}

159
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<Account> 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());
}
}

116
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;
}
}

30
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");
}

117
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;
}
}
Loading…
Cancel
Save