forked from Qortal/qortal
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.
This commit is contained in:
parent
74bf930698
commit
83abede8ab
@ -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
Normal file
216
src/main/java/org/qora/api/resource/GroupsResource.java
Normal file
@ -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
Normal file
89
src/main/java/org/qora/data/group/GroupData.java
Normal file
@ -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;
|
||||
}
|
||||
|
||||
}
|
@ -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;
|
||||
}
|
||||
|
||||
}
|
@ -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 {
|
||||
|
@ -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
Normal file
112
src/main/java/org/qora/group/Group.java
Normal file
@ -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);
|
||||
}
|
||||
|
||||
}
|
@ -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
Normal file
21
src/main/java/org/qora/repository/GroupRepository.java
Normal file
@ -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;
|
||||
|
||||
}
|
@ -10,6 +10,8 @@ public interface Repository extends AutoCloseable {
|
||||
|
||||
public BlockRepository getBlockRepository();
|
||||
|
||||
public GroupRepository getGroupRepository();
|
||||
|
||||
public NameRepository getNameRepository();
|
||||
|
||||
public TransactionRepository getTransactionRepository();
|
||||
|
@ -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;
|
||||
|
@ -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);
|
||||
}
|
||||
}
|
||||
|
||||
}
|
@ -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);
|
||||
|
@ -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);
|
||||
}
|
||||
}
|
||||
|
||||
}
|
@ -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");
|
||||
}
|
||||
|
@ -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
Normal file
146
src/main/java/org/qora/transaction/CreateGroupTransaction.java
Normal file
@ -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());
|
||||
}
|
||||
|
||||
}
|
@ -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
Normal file
159
src/main/java/org/qora/transaction/UpdateGroupTransaction.java
Normal file
@ -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());
|
||||
}
|
||||
|
||||
}
|
@ -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;
|
||||
}
|
||||
|
||||
}
|
@ -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");
|
||||
}
|
||||
|
@ -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…
Reference in New Issue
Block a user