Added "data" field to assets, added UPDATE_ASSET tx + fixes

This commit is contained in:
catbref 2019-03-13 14:06:52 +00:00
parent 8f72d9d423
commit d53777f461
21 changed files with 805 additions and 124 deletions

View File

@ -43,6 +43,7 @@ import org.qora.data.transaction.CancelAssetOrderTransactionData;
import org.qora.data.transaction.CreateAssetOrderTransactionData; import org.qora.data.transaction.CreateAssetOrderTransactionData;
import org.qora.data.transaction.IssueAssetTransactionData; import org.qora.data.transaction.IssueAssetTransactionData;
import org.qora.data.transaction.TransactionData; import org.qora.data.transaction.TransactionData;
import org.qora.data.transaction.UpdateAssetTransactionData;
import org.qora.repository.DataException; import org.qora.repository.DataException;
import org.qora.repository.Repository; import org.qora.repository.Repository;
import org.qora.repository.RepositoryManager; import org.qora.repository.RepositoryManager;
@ -53,6 +54,7 @@ import org.qora.transform.TransformationException;
import org.qora.transform.transaction.CancelAssetOrderTransactionTransformer; import org.qora.transform.transaction.CancelAssetOrderTransactionTransformer;
import org.qora.transform.transaction.CreateAssetOrderTransactionTransformer; import org.qora.transform.transaction.CreateAssetOrderTransactionTransformer;
import org.qora.transform.transaction.IssueAssetTransactionTransformer; import org.qora.transform.transaction.IssueAssetTransactionTransformer;
import org.qora.transform.transaction.UpdateAssetTransactionTransformer;
import org.qora.utils.Base58; import org.qora.utils.Base58;
@Path("/assets") @Path("/assets")
@ -763,4 +765,52 @@ public class AssetsResource {
} }
} }
@POST
@Path("/update")
@Operation(
summary = "Update asset",
requestBody = @RequestBody(
required = true,
content = @Content(
mediaType = MediaType.APPLICATION_JSON,
schema = @Schema(
implementation = UpdateAssetTransactionData.class
)
)
),
responses = {
@ApiResponse(
description = "raw, unsigned, UPDATE_ASSET transaction encoded in Base58",
content = @Content(
mediaType = MediaType.TEXT_PLAIN,
schema = @Schema(
type = "string"
)
)
)
}
)
@ApiErrors({
ApiError.NON_PRODUCTION, ApiError.TRANSFORMATION_ERROR, ApiError.REPOSITORY_ISSUE, ApiError.TRANSACTION_INVALID
})
public String updateAsset(UpdateAssetTransactionData transactionData) {
if (Settings.getInstance().isApiRestricted())
throw ApiExceptionFactory.INSTANCE.createException(request, ApiError.NON_PRODUCTION);
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 = UpdateAssetTransactionTransformer.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);
}
}
} }

View File

@ -2,6 +2,8 @@ package org.qora.asset;
import org.qora.data.asset.AssetData; import org.qora.data.asset.AssetData;
import org.qora.data.transaction.IssueAssetTransactionData; import org.qora.data.transaction.IssueAssetTransactionData;
import org.qora.data.transaction.TransactionData;
import org.qora.data.transaction.UpdateAssetTransactionData;
import org.qora.repository.DataException; import org.qora.repository.DataException;
import org.qora.repository.Repository; import org.qora.repository.Repository;
@ -12,6 +14,15 @@ public class Asset {
*/ */
public static final long QORA = 0L; public static final long QORA = 0L;
// Other useful constants
public static final int MAX_NAME_SIZE = 400;
public static final int MAX_DESCRIPTION_SIZE = 4000;
public static final int MAX_DATA_SIZE = 4000;
public static final long MAX_DIVISIBLE_QUANTITY = 10_000_000_000L;
public static final long MAX_INDIVISIBLE_QUANTITY = 1_000_000_000_000_000_000L;
// Properties // Properties
private Repository repository; private Repository repository;
private AssetData assetData; private AssetData assetData;
@ -25,9 +36,12 @@ public class Asset {
public Asset(Repository repository, IssueAssetTransactionData issueAssetTransactionData) { public Asset(Repository repository, IssueAssetTransactionData issueAssetTransactionData) {
this.repository = repository; this.repository = repository;
// NOTE: transaction's reference is used to look up newly assigned assetID on creation!
this.assetData = new AssetData(issueAssetTransactionData.getOwner(), issueAssetTransactionData.getAssetName(), this.assetData = new AssetData(issueAssetTransactionData.getOwner(), issueAssetTransactionData.getAssetName(),
issueAssetTransactionData.getDescription(), issueAssetTransactionData.getQuantity(), issueAssetTransactionData.getIsDivisible(), issueAssetTransactionData.getDescription(), issueAssetTransactionData.getQuantity(),
issueAssetTransactionData.getReference()); issueAssetTransactionData.getIsDivisible(), issueAssetTransactionData.getData(),
issueAssetTransactionData.getTxGroupId(), issueAssetTransactionData.getSignature());
} }
public Asset(Repository repository, long assetId) throws DataException { public Asset(Repository repository, long assetId) throws DataException {
@ -51,4 +65,56 @@ public class Asset {
this.repository.getAssetRepository().delete(this.assetData.getAssetId()); this.repository.getAssetRepository().delete(this.assetData.getAssetId());
} }
public void update(UpdateAssetTransactionData updateAssetTransactionData) throws DataException {
// Update reference in transaction data
updateAssetTransactionData.setOrphanReference(this.assetData.getReference());
// New reference is this transaction's signature
this.assetData.setReference(updateAssetTransactionData.getSignature());
// Update asset's owner, description and data
this.assetData.setOwner(updateAssetTransactionData.getNewOwner());
this.assetData.setDescription(updateAssetTransactionData.getNewDescription());
this.assetData.setData(updateAssetTransactionData.getNewData());
// Save updated asset
this.repository.getAssetRepository().save(this.assetData);
}
public void revert(UpdateAssetTransactionData updateAssetTransactionData) throws DataException {
// Previous asset reference is taken from this transaction's cached copy
this.assetData.setReference(updateAssetTransactionData.getOrphanReference());
// Previous owner, description and/or data taken from referenced transaction
TransactionData previousTransactionData = this.repository.getTransactionRepository()
.fromSignature(this.assetData.getReference());
if (previousTransactionData == null)
throw new IllegalStateException("Missing referenced transaction when orphaning UPDATE_ASSET");
switch (previousTransactionData.getType()) {
case ISSUE_ASSET:
IssueAssetTransactionData previousIssueAssetTransactionData = (IssueAssetTransactionData) previousTransactionData;
this.assetData.setOwner(previousIssueAssetTransactionData.getOwner());
this.assetData.setDescription(previousIssueAssetTransactionData.getDescription());
this.assetData.setData(previousIssueAssetTransactionData.getData());
break;
case UPDATE_ASSET:
UpdateAssetTransactionData previousUpdateAssetTransactionData = (UpdateAssetTransactionData) previousTransactionData;
this.assetData.setOwner(previousUpdateAssetTransactionData.getNewOwner());
this.assetData.setDescription(previousUpdateAssetTransactionData.getNewDescription());
this.assetData.setData(previousUpdateAssetTransactionData.getNewData());
break;
default:
throw new IllegalStateException("Invalid referenced transaction when orphaning UPDATE_ASSET");
}
// Save reverted asset
this.repository.getAssetRepository().save(this.assetData);
}
} }

View File

@ -31,14 +31,14 @@ public class Trade {
// Update corresponding Orders on both sides of trade // Update corresponding Orders on both sides of trade
OrderData initiatingOrder = assetRepository.fromOrderId(this.tradeData.getInitiator()); OrderData initiatingOrder = assetRepository.fromOrderId(this.tradeData.getInitiator());
initiatingOrder.setFulfilled(initiatingOrder.getFulfilled().add(tradeData.getPrice())); initiatingOrder.setFulfilled(initiatingOrder.getFulfilled().add(tradeData.getInitiatorAmount()));
initiatingOrder.setIsFulfilled(Order.isFulfilled(initiatingOrder)); initiatingOrder.setIsFulfilled(Order.isFulfilled(initiatingOrder));
// Set isClosed to true if isFulfilled now true // Set isClosed to true if isFulfilled now true
initiatingOrder.setIsClosed(initiatingOrder.getIsFulfilled()); initiatingOrder.setIsClosed(initiatingOrder.getIsFulfilled());
assetRepository.save(initiatingOrder); assetRepository.save(initiatingOrder);
OrderData targetOrder = assetRepository.fromOrderId(this.tradeData.getTarget()); OrderData targetOrder = assetRepository.fromOrderId(this.tradeData.getTarget());
targetOrder.setFulfilled(targetOrder.getFulfilled().add(tradeData.getAmount())); targetOrder.setFulfilled(targetOrder.getFulfilled().add(tradeData.getTargetAmount()));
targetOrder.setIsFulfilled(Order.isFulfilled(targetOrder)); targetOrder.setIsFulfilled(Order.isFulfilled(targetOrder));
// Set isClosed to true if isFulfilled now true // Set isClosed to true if isFulfilled now true
targetOrder.setIsClosed(targetOrder.getIsFulfilled()); targetOrder.setIsClosed(targetOrder.getIsFulfilled());
@ -47,11 +47,11 @@ public class Trade {
// Actually transfer asset balances // Actually transfer asset balances
Account initiatingCreator = new PublicKeyAccount(this.repository, initiatingOrder.getCreatorPublicKey()); Account initiatingCreator = new PublicKeyAccount(this.repository, initiatingOrder.getCreatorPublicKey());
initiatingCreator.setConfirmedBalance(initiatingOrder.getWantAssetId(), initiatingCreator.setConfirmedBalance(initiatingOrder.getWantAssetId(),
initiatingCreator.getConfirmedBalance(initiatingOrder.getWantAssetId()).add(tradeData.getAmount())); initiatingCreator.getConfirmedBalance(initiatingOrder.getWantAssetId()).add(tradeData.getTargetAmount()));
Account targetCreator = new PublicKeyAccount(this.repository, targetOrder.getCreatorPublicKey()); Account targetCreator = new PublicKeyAccount(this.repository, targetOrder.getCreatorPublicKey());
targetCreator.setConfirmedBalance(targetOrder.getWantAssetId(), targetCreator.setConfirmedBalance(targetOrder.getWantAssetId(),
targetCreator.getConfirmedBalance(targetOrder.getWantAssetId()).add(tradeData.getPrice())); targetCreator.getConfirmedBalance(targetOrder.getWantAssetId()).add(tradeData.getInitiatorAmount()));
} }
public void orphan() throws DataException { public void orphan() throws DataException {
@ -59,14 +59,14 @@ public class Trade {
// Revert corresponding Orders on both sides of trade // Revert corresponding Orders on both sides of trade
OrderData initiatingOrder = assetRepository.fromOrderId(this.tradeData.getInitiator()); OrderData initiatingOrder = assetRepository.fromOrderId(this.tradeData.getInitiator());
initiatingOrder.setFulfilled(initiatingOrder.getFulfilled().subtract(tradeData.getPrice())); initiatingOrder.setFulfilled(initiatingOrder.getFulfilled().subtract(tradeData.getInitiatorAmount()));
initiatingOrder.setIsFulfilled(Order.isFulfilled(initiatingOrder)); initiatingOrder.setIsFulfilled(Order.isFulfilled(initiatingOrder));
// Set isClosed to false if isFulfilled now false // Set isClosed to false if isFulfilled now false
initiatingOrder.setIsClosed(initiatingOrder.getIsFulfilled()); initiatingOrder.setIsClosed(initiatingOrder.getIsFulfilled());
assetRepository.save(initiatingOrder); assetRepository.save(initiatingOrder);
OrderData targetOrder = assetRepository.fromOrderId(this.tradeData.getTarget()); OrderData targetOrder = assetRepository.fromOrderId(this.tradeData.getTarget());
targetOrder.setFulfilled(targetOrder.getFulfilled().subtract(tradeData.getAmount())); targetOrder.setFulfilled(targetOrder.getFulfilled().subtract(tradeData.getTargetAmount()));
targetOrder.setIsFulfilled(Order.isFulfilled(targetOrder)); targetOrder.setIsFulfilled(Order.isFulfilled(targetOrder));
// Set isClosed to false if isFulfilled now false // Set isClosed to false if isFulfilled now false
targetOrder.setIsClosed(targetOrder.getIsFulfilled()); targetOrder.setIsClosed(targetOrder.getIsFulfilled());
@ -75,11 +75,11 @@ public class Trade {
// Reverse asset transfers // Reverse asset transfers
Account initiatingCreator = new PublicKeyAccount(this.repository, initiatingOrder.getCreatorPublicKey()); Account initiatingCreator = new PublicKeyAccount(this.repository, initiatingOrder.getCreatorPublicKey());
initiatingCreator.setConfirmedBalance(initiatingOrder.getWantAssetId(), initiatingCreator.setConfirmedBalance(initiatingOrder.getWantAssetId(),
initiatingCreator.getConfirmedBalance(initiatingOrder.getWantAssetId()).subtract(tradeData.getAmount())); initiatingCreator.getConfirmedBalance(initiatingOrder.getWantAssetId()).subtract(tradeData.getTargetAmount()));
Account targetCreator = new PublicKeyAccount(this.repository, targetOrder.getCreatorPublicKey()); Account targetCreator = new PublicKeyAccount(this.repository, targetOrder.getCreatorPublicKey());
targetCreator.setConfirmedBalance(targetOrder.getWantAssetId(), targetCreator.setConfirmedBalance(targetOrder.getWantAssetId(),
targetCreator.getConfirmedBalance(targetOrder.getWantAssetId()).subtract(tradeData.getPrice())); targetCreator.getConfirmedBalance(targetOrder.getWantAssetId()).subtract(tradeData.getInitiatorAmount()));
// Remove trade from repository // Remove trade from repository
assetRepository.delete(tradeData); assetRepository.delete(tradeData);

View File

@ -22,6 +22,7 @@ import org.qora.data.asset.AssetData;
import org.qora.data.block.BlockData; import org.qora.data.block.BlockData;
import org.qora.data.transaction.IssueAssetTransactionData; import org.qora.data.transaction.IssueAssetTransactionData;
import org.qora.data.transaction.TransactionData; import org.qora.data.transaction.TransactionData;
import org.qora.group.Group;
import org.qora.repository.DataException; import org.qora.repository.DataException;
import org.qora.repository.Repository; import org.qora.repository.Repository;
import org.qora.transaction.Transaction; import org.qora.transaction.Transaction;
@ -117,7 +118,7 @@ public class GenesisBlock extends Block {
IssueAssetTransactionData issueAssetTransactionData = (IssueAssetTransactionData) transactionData; IssueAssetTransactionData issueAssetTransactionData = (IssueAssetTransactionData) transactionData;
return new AssetData(issueAssetTransactionData.getOwner(), issueAssetTransactionData.getAssetName(), issueAssetTransactionData.getDescription(), return new AssetData(issueAssetTransactionData.getOwner(), issueAssetTransactionData.getAssetName(), issueAssetTransactionData.getDescription(),
issueAssetTransactionData.getQuantity(), issueAssetTransactionData.getIsDivisible(), issueAssetTransactionData.getReference()); issueAssetTransactionData.getQuantity(), issueAssetTransactionData.getIsDivisible(), null, Group.NO_GROUP, issueAssetTransactionData.getReference());
}).collect(Collectors.toList()); }).collect(Collectors.toList());
} }

View File

@ -17,6 +17,8 @@ public class AssetData {
private String description; private String description;
private long quantity; private long quantity;
private boolean isDivisible; private boolean isDivisible;
private String data;
private int creationGroupId;
// No need to expose this via API // No need to expose this via API
@XmlTransient @XmlTransient
@Schema(hidden = true) @Schema(hidden = true)
@ -29,19 +31,21 @@ public class AssetData {
} }
// NOTE: key is Long, not long, because it can be null if asset ID/key not yet assigned. // NOTE: key is Long, not long, because it can be null if asset ID/key not yet assigned.
public AssetData(Long assetId, String owner, String name, String description, long quantity, boolean isDivisible, byte[] reference) { public AssetData(Long assetId, String owner, String name, String description, long quantity, boolean isDivisible, String data, int creationGroupId, byte[] reference) {
this.assetId = assetId; this.assetId = assetId;
this.owner = owner; this.owner = owner;
this.name = name; this.name = name;
this.description = description; this.description = description;
this.quantity = quantity; this.quantity = quantity;
this.isDivisible = isDivisible; this.isDivisible = isDivisible;
this.data = data;
this.creationGroupId = creationGroupId;
this.reference = reference; this.reference = reference;
} }
// New asset with unassigned assetId // New asset with unassigned assetId
public AssetData(String owner, String name, String description, long quantity, boolean isDivisible, byte[] reference) { public AssetData(String owner, String name, String description, long quantity, boolean isDivisible, String data, int creationGroupId, byte[] reference) {
this(null, owner, name, description, quantity, isDivisible, reference); this(null, owner, name, description, quantity, isDivisible, data, creationGroupId, reference);
} }
// Getters/Setters // Getters/Setters
@ -58,6 +62,10 @@ public class AssetData {
return this.owner; return this.owner;
} }
public void setOwner(String owner) {
this.owner = owner;
}
public String getName() { public String getName() {
return this.name; return this.name;
} }
@ -66,6 +74,10 @@ public class AssetData {
return this.description; return this.description;
} }
public void setDescription(String description) {
this.description = description;
}
public long getQuantity() { public long getQuantity() {
return this.quantity; return this.quantity;
} }
@ -74,8 +86,24 @@ public class AssetData {
return this.isDivisible; return this.isDivisible;
} }
public String getData() {
return this.data;
}
public void setData(String data) {
this.data = data;
}
public int getCreationGroupId() {
return this.creationGroupId;
}
public byte[] getReference() { public byte[] getReference() {
return this.reference; return this.reference;
} }
public void setReference(byte[] reference) {
this.reference = reference;
}
} }

View File

@ -16,9 +16,9 @@ public class RecentTradeData {
private long otherAssetId; private long otherAssetId;
private BigDecimal amount; private BigDecimal otherAmount;
private BigDecimal price; private BigDecimal amount;
@Schema( @Schema(
description = "when trade happened" description = "when trade happened"
@ -31,11 +31,11 @@ public class RecentTradeData {
protected RecentTradeData() { protected RecentTradeData() {
} }
public RecentTradeData(long assetId, long otherAssetId, BigDecimal amount, BigDecimal price, long timestamp) { public RecentTradeData(long assetId, long otherAssetId, BigDecimal otherAmount, BigDecimal amount, long timestamp) {
this.assetId = assetId; this.assetId = assetId;
this.otherAssetId = otherAssetId; this.otherAssetId = otherAssetId;
this.otherAmount = otherAmount;
this.amount = amount; this.amount = amount;
this.price = price;
this.timestamp = timestamp; this.timestamp = timestamp;
} }
@ -49,12 +49,12 @@ public class RecentTradeData {
return this.otherAssetId; return this.otherAssetId;
} }
public BigDecimal getAmount() { public BigDecimal getOtherAmount() {
return this.amount; return this.otherAmount;
} }
public BigDecimal getPrice() { public BigDecimal getAmount() {
return this.price; return this.amount;
} }
public long getTimestamp() { public long getTimestamp() {

View File

@ -23,11 +23,11 @@ public class TradeData {
@Schema(name = "targetAmount", description = "amount traded from target order") @Schema(name = "targetAmount", description = "amount traded from target order")
@XmlElement(name = "targetAmount") @XmlElement(name = "targetAmount")
private BigDecimal amount; private BigDecimal targetAmount;
@Schema(name = "initiatorAmount", description = "amount traded from initiating order") @Schema(name = "initiatorAmount", description = "amount traded from initiating order")
@XmlElement(name = "initiatorAmount") @XmlElement(name = "initiatorAmount")
private BigDecimal price; private BigDecimal initiatorAmount;
@Schema(description = "when trade happened") @Schema(description = "when trade happened")
private long timestamp; private long timestamp;
@ -38,11 +38,11 @@ public class TradeData {
protected TradeData() { protected TradeData() {
} }
public TradeData(byte[] initiator, byte[] target, BigDecimal amount, BigDecimal price, long timestamp) { public TradeData(byte[] initiator, byte[] target, BigDecimal targetAmount, BigDecimal initiatorAmount, long timestamp) {
this.initiator = initiator; this.initiator = initiator;
this.target = target; this.target = target;
this.amount = amount; this.targetAmount = targetAmount;
this.price = price; this.initiatorAmount = initiatorAmount;
this.timestamp = timestamp; this.timestamp = timestamp;
} }
@ -56,12 +56,12 @@ public class TradeData {
return this.target; return this.target;
} }
public BigDecimal getAmount() { public BigDecimal getTargetAmount() {
return this.amount; return this.targetAmount;
} }
public BigDecimal getPrice() { public BigDecimal getInitiatorAmount() {
return this.price; return this.initiatorAmount;
} }
public long getTimestamp() { public long getTimestamp() {

View File

@ -37,6 +37,8 @@ public class IssueAssetTransactionData extends TransactionData {
private long quantity; private long quantity;
@Schema(description = "whether asset quantities can be fractional", example = "true") @Schema(description = "whether asset quantities can be fractional", example = "true")
private boolean isDivisible; private boolean isDivisible;
@Schema(description = "non-human-readable asset-related data, typically JSON", example = "{\"logo\": \"data:image/jpeg;base64,/9j/4AAQSkZJRgA==\"}")
private String data;
// Constructors // Constructors
@ -58,7 +60,7 @@ public class IssueAssetTransactionData extends TransactionData {
} }
public IssueAssetTransactionData(long timestamp, int txGroupId, byte[] reference, byte[] issuerPublicKey, Long assetId, String owner, public IssueAssetTransactionData(long timestamp, int txGroupId, byte[] reference, byte[] issuerPublicKey, Long assetId, String owner,
String assetName, String description, long quantity, boolean isDivisible, BigDecimal fee, byte[] signature) { String assetName, String description, long quantity, boolean isDivisible, String data, BigDecimal fee, byte[] signature) {
super(TransactionType.ISSUE_ASSET, timestamp, txGroupId, reference, issuerPublicKey, fee, signature); super(TransactionType.ISSUE_ASSET, timestamp, txGroupId, reference, issuerPublicKey, fee, signature);
this.assetId = assetId; this.assetId = assetId;
@ -68,16 +70,17 @@ public class IssueAssetTransactionData extends TransactionData {
this.description = description; this.description = description;
this.quantity = quantity; this.quantity = quantity;
this.isDivisible = isDivisible; this.isDivisible = isDivisible;
this.data = data;
} }
public IssueAssetTransactionData(long timestamp, int txGroupId, byte[] reference, byte[] issuerPublicKey, String owner, String assetName, public IssueAssetTransactionData(long timestamp, int txGroupId, byte[] reference, byte[] issuerPublicKey, String owner, String assetName,
String description, long quantity, boolean isDivisible, BigDecimal fee, byte[] signature) { String description, long quantity, boolean isDivisible, String data, BigDecimal fee, byte[] signature) {
this(timestamp, txGroupId, reference, issuerPublicKey, null, owner, assetName, description, quantity, isDivisible, fee, signature); this(timestamp, txGroupId, reference, issuerPublicKey, null, owner, assetName, description, quantity, isDivisible, data, fee, signature);
} }
public IssueAssetTransactionData(long timestamp, int txGroupId, byte[] reference, byte[] issuerPublicKey, String owner, String assetName, public IssueAssetTransactionData(long timestamp, int txGroupId, byte[] reference, byte[] issuerPublicKey, String owner, String assetName,
String description, long quantity, boolean isDivisible, BigDecimal fee) { String description, long quantity, boolean isDivisible, String data, BigDecimal fee) {
this(timestamp, txGroupId, reference, issuerPublicKey, null, owner, assetName, description, quantity, isDivisible, fee, null); this(timestamp, txGroupId, reference, issuerPublicKey, null, owner, assetName, description, quantity, isDivisible, data, fee, null);
} }
// Getters/Setters // Getters/Setters
@ -118,4 +121,8 @@ public class IssueAssetTransactionData extends TransactionData {
return this.isDivisible; return this.isDivisible;
} }
public String getData() {
return this.data;
}
} }

View File

@ -36,7 +36,8 @@ import io.swagger.v3.oas.annotations.media.Schema.AccessMode;
GroupBanTransactionData.class, CancelGroupBanTransactionData.class, GroupBanTransactionData.class, CancelGroupBanTransactionData.class,
GroupKickTransactionData.class, GroupInviteTransactionData.class, GroupKickTransactionData.class, GroupInviteTransactionData.class,
JoinGroupTransactionData.class, LeaveGroupTransactionData.class, JoinGroupTransactionData.class, LeaveGroupTransactionData.class,
GroupApprovalTransactionData.class, SetGroupTransactionData.class GroupApprovalTransactionData.class, SetGroupTransactionData.class,
UpdateAssetTransactionData.class
}) })
//All properties to be converted to JSON via JAXB //All properties to be converted to JSON via JAXB
@XmlAccessorType(XmlAccessType.FIELD) @XmlAccessorType(XmlAccessType.FIELD)

View File

@ -0,0 +1,92 @@
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 JAXB
@XmlAccessorType(XmlAccessType.FIELD)
@Schema(allOf = { TransactionData.class })
public class UpdateAssetTransactionData extends TransactionData {
// Properties
private long assetId;
@Schema(description = "asset owner's public key", example = "2tiMr5LTpaWCgbRvkPK8TFd7k63DyHJMMFFsz9uBf1ZP")
private byte[] ownerPublicKey;
@Schema(description = "asset new owner's address", example = "QgV4s3xnzLhVBEJxcYui4u4q11yhUHsd9v")
private String newOwner;
@Schema(description = "asset new description", example = "Gold asset - 1 unit represents one 1kg of gold")
private String newDescription;
@Schema(description = "new asset-related data, typically JSON", example = "{\"logo\": \"data:image/jpeg;base64,/9j/4AAQSkZJRgA==\"}")
private String newData;
// No need to expose this via API
@XmlTransient
@Schema(hidden = true)
private byte[] orphanReference;
// Constructors
// For JAXB
protected UpdateAssetTransactionData() {
super(TransactionType.UPDATE_ASSET);
}
public void afterUnmarshal(Unmarshaller u, Object parent) {
this.creatorPublicKey = this.ownerPublicKey;
}
public UpdateAssetTransactionData(long timestamp, int txGroupId, byte[] reference, byte[] ownerPublicKey, long assetId, String newOwner,
String newDescription, String newData, BigDecimal fee, byte[] orphanReference, byte[] signature) {
super(TransactionType.UPDATE_ASSET, timestamp, txGroupId, reference, ownerPublicKey, fee, signature);
this.assetId = assetId;
this.ownerPublicKey = ownerPublicKey;
this.newOwner = newOwner;
this.newDescription = newDescription;
this.newData = newData;
this.orphanReference = orphanReference;
}
public UpdateAssetTransactionData(long timestamp, int txGroupId, byte[] reference, byte[] ownerPublicKey, long assetId, String newOwner,
String newDescription, String newData, BigDecimal fee, byte[] orphanReference) {
this(timestamp, txGroupId, reference, ownerPublicKey, assetId, newOwner, newDescription, newData, fee, orphanReference, null);
}
// Getters/Setters
public long getAssetId() {
return this.assetId;
}
public byte[] getOwnerPublicKey() {
return this.ownerPublicKey;
}
public String getNewOwner() {
return this.newOwner;
}
public String getNewDescription() {
return this.newDescription;
}
public String getNewData() {
return this.newData;
}
public byte[] getOrphanReference() {
return this.orphanReference;
}
public void setOrphanReference(byte[] orphanReference) {
this.orphanReference = orphanReference;
}
}

View File

@ -28,8 +28,9 @@ public class HSQLDBAssetRepository implements AssetRepository {
@Override @Override
public AssetData fromAssetId(long assetId) throws DataException { public AssetData fromAssetId(long assetId) throws DataException {
try (ResultSet resultSet = this.repository try (ResultSet resultSet = this.repository.checkedExecute(
.checkedExecute("SELECT owner, asset_name, description, quantity, is_divisible, reference FROM Assets WHERE asset_id = ?", assetId)) { "SELECT owner, asset_name, description, quantity, is_divisible, data, creation_group_id, reference FROM Assets WHERE asset_id = ?",
assetId)) {
if (resultSet == null) if (resultSet == null)
return null; return null;
@ -38,9 +39,12 @@ public class HSQLDBAssetRepository implements AssetRepository {
String description = resultSet.getString(3); String description = resultSet.getString(3);
long quantity = resultSet.getLong(4); long quantity = resultSet.getLong(4);
boolean isDivisible = resultSet.getBoolean(5); boolean isDivisible = resultSet.getBoolean(5);
byte[] reference = resultSet.getBytes(6); String data = resultSet.getString(6);
int creationGroupId = resultSet.getInt(7);
byte[] reference = resultSet.getBytes(8);
return new AssetData(assetId, owner, assetName, description, quantity, isDivisible, reference); return new AssetData(assetId, owner, assetName, description, quantity, isDivisible, data, creationGroupId,
reference);
} catch (SQLException e) { } catch (SQLException e) {
throw new DataException("Unable to fetch asset from repository", e); throw new DataException("Unable to fetch asset from repository", e);
} }
@ -48,8 +52,9 @@ public class HSQLDBAssetRepository implements AssetRepository {
@Override @Override
public AssetData fromAssetName(String assetName) throws DataException { public AssetData fromAssetName(String assetName) throws DataException {
try (ResultSet resultSet = this.repository try (ResultSet resultSet = this.repository.checkedExecute(
.checkedExecute("SELECT owner, asset_id, description, quantity, is_divisible, reference FROM Assets WHERE asset_name = ?", assetName)) { "SELECT owner, asset_id, description, quantity, is_divisible, data, creation_group_id, reference FROM Assets WHERE asset_name = ?",
assetName)) {
if (resultSet == null) if (resultSet == null)
return null; return null;
@ -58,9 +63,12 @@ public class HSQLDBAssetRepository implements AssetRepository {
String description = resultSet.getString(3); String description = resultSet.getString(3);
long quantity = resultSet.getLong(4); long quantity = resultSet.getLong(4);
boolean isDivisible = resultSet.getBoolean(5); boolean isDivisible = resultSet.getBoolean(5);
byte[] reference = resultSet.getBytes(6); String data = resultSet.getString(6);
int creationGroupId = resultSet.getInt(7);
byte[] reference = resultSet.getBytes(8);
return new AssetData(assetId, owner, assetName, description, quantity, isDivisible, reference); return new AssetData(assetId, owner, assetName, description, quantity, isDivisible, data, creationGroupId,
reference);
} catch (SQLException e) { } catch (SQLException e) {
throw new DataException("Unable to fetch asset from repository", e); throw new DataException("Unable to fetch asset from repository", e);
} }
@ -86,7 +94,7 @@ public class HSQLDBAssetRepository implements AssetRepository {
@Override @Override
public List<AssetData> getAllAssets(Integer limit, Integer offset, Boolean reverse) throws DataException { public List<AssetData> getAllAssets(Integer limit, Integer offset, Boolean reverse) throws DataException {
String sql = "SELECT owner, asset_id, description, quantity, is_divisible, reference, asset_name FROM Assets ORDER BY asset_id"; String sql = "SELECT asset_id, owner, asset_name, description, quantity, is_divisible, data, creation_group_id, reference FROM Assets ORDER BY asset_id";
if (reverse != null && reverse) if (reverse != null && reverse)
sql += " DESC"; sql += " DESC";
sql += HSQLDBRepository.limitOffsetSql(limit, offset); sql += HSQLDBRepository.limitOffsetSql(limit, offset);
@ -98,15 +106,18 @@ public class HSQLDBAssetRepository implements AssetRepository {
return assets; return assets;
do { do {
String owner = resultSet.getString(1); long assetId = resultSet.getLong(1);
long assetId = resultSet.getLong(2); String owner = resultSet.getString(2);
String description = resultSet.getString(3); String assetName = resultSet.getString(3);
long quantity = resultSet.getLong(4); String description = resultSet.getString(4);
boolean isDivisible = resultSet.getBoolean(5); long quantity = resultSet.getLong(5);
byte[] reference = resultSet.getBytes(6); boolean isDivisible = resultSet.getBoolean(6);
String assetName = resultSet.getString(7); String data = resultSet.getString(7);
int creationGroupId = resultSet.getInt(8);
byte[] reference = resultSet.getBytes(9);
assets.add(new AssetData(assetId, owner, assetName, description, quantity, isDivisible, reference)); assets.add(new AssetData(assetId, owner, assetName, description, quantity, isDivisible, data,
creationGroupId, reference));
} while (resultSet.next()); } while (resultSet.next());
return assets; return assets;
@ -119,8 +130,10 @@ public class HSQLDBAssetRepository implements AssetRepository {
public void save(AssetData assetData) throws DataException { public void save(AssetData assetData) throws DataException {
HSQLDBSaver saveHelper = new HSQLDBSaver("Assets"); HSQLDBSaver saveHelper = new HSQLDBSaver("Assets");
saveHelper.bind("asset_id", assetData.getAssetId()).bind("owner", assetData.getOwner()).bind("asset_name", assetData.getName()) saveHelper.bind("asset_id", assetData.getAssetId()).bind("owner", assetData.getOwner())
.bind("description", assetData.getDescription()).bind("quantity", assetData.getQuantity()).bind("is_divisible", assetData.getIsDivisible()) .bind("asset_name", assetData.getName()).bind("description", assetData.getDescription())
.bind("quantity", assetData.getQuantity()).bind("is_divisible", assetData.getIsDivisible())
.bind("data", assetData.getData()).bind("creation_group_id", assetData.getCreationGroupId())
.bind("reference", assetData.getReference()); .bind("reference", assetData.getReference());
try { try {
@ -128,7 +141,8 @@ public class HSQLDBAssetRepository implements AssetRepository {
if (assetData.getAssetId() == null) { if (assetData.getAssetId() == null) {
// Fetch new assetId // Fetch new assetId
try (ResultSet resultSet = this.repository.checkedExecute("SELECT asset_id FROM Assets WHERE reference = ?", assetData.getReference())) { try (ResultSet resultSet = this.repository
.checkedExecute("SELECT asset_id FROM Assets WHERE reference = ?", assetData.getReference())) {
if (resultSet == null) if (resultSet == null)
throw new DataException("Unable to fetch new asset ID from repository"); throw new DataException("Unable to fetch new asset ID from repository");
@ -169,14 +183,16 @@ public class HSQLDBAssetRepository implements AssetRepository {
boolean isClosed = resultSet.getBoolean(8); boolean isClosed = resultSet.getBoolean(8);
boolean isFulfilled = resultSet.getBoolean(9); boolean isFulfilled = resultSet.getBoolean(9);
return new OrderData(orderId, creatorPublicKey, haveAssetId, wantAssetId, amount, fulfilled, price, timestamp, isClosed, isFulfilled); return new OrderData(orderId, creatorPublicKey, haveAssetId, wantAssetId, amount, fulfilled, price,
timestamp, isClosed, isFulfilled);
} catch (SQLException e) { } catch (SQLException e) {
throw new DataException("Unable to fetch asset order from repository", e); throw new DataException("Unable to fetch asset order from repository", e);
} }
} }
@Override @Override
public List<OrderData> getOpenOrders(long haveAssetId, long wantAssetId, Integer limit, Integer offset, Boolean reverse) throws DataException { public List<OrderData> getOpenOrders(long haveAssetId, long wantAssetId, Integer limit, Integer offset,
Boolean reverse) throws DataException {
String sql = "SELECT creator, asset_order_id, amount, fulfilled, price, ordered FROM AssetOrders " String sql = "SELECT creator, asset_order_id, amount, fulfilled, price, ordered FROM AssetOrders "
+ "WHERE have_asset_id = ? AND want_asset_id = ? AND is_closed = FALSE AND is_fulfilled = FALSE ORDER BY price"; + "WHERE have_asset_id = ? AND want_asset_id = ? AND is_closed = FALSE AND is_fulfilled = FALSE ORDER BY price";
if (reverse != null && reverse) if (reverse != null && reverse)
@ -202,8 +218,8 @@ public class HSQLDBAssetRepository implements AssetRepository {
boolean isClosed = false; boolean isClosed = false;
boolean isFulfilled = false; boolean isFulfilled = false;
OrderData order = new OrderData(orderId, creatorPublicKey, haveAssetId, wantAssetId, amount, fulfilled, price, timestamp, isClosed, OrderData order = new OrderData(orderId, creatorPublicKey, haveAssetId, wantAssetId, amount, fulfilled,
isFulfilled); price, timestamp, isClosed, isFulfilled);
orders.add(order); orders.add(order);
} while (resultSet.next()); } while (resultSet.next());
@ -214,7 +230,8 @@ public class HSQLDBAssetRepository implements AssetRepository {
} }
@Override @Override
public List<OrderData> getAggregatedOpenOrders(long haveAssetId, long wantAssetId, Integer limit, Integer offset, Boolean reverse) throws DataException { public List<OrderData> getAggregatedOpenOrders(long haveAssetId, long wantAssetId, Integer limit, Integer offset,
Boolean reverse) throws DataException {
String sql = "SELECT price, SUM(amount - fulfilled), MAX(ordered) FROM AssetOrders " String sql = "SELECT price, SUM(amount - fulfilled), MAX(ordered) FROM AssetOrders "
+ "WHERE have_asset_id = ? AND want_asset_id = ? AND is_closed = FALSE AND is_fulfilled = FALSE GROUP BY price ORDER BY price"; + "WHERE have_asset_id = ? AND want_asset_id = ? AND is_closed = FALSE AND is_fulfilled = FALSE GROUP BY price ORDER BY price";
if (reverse != null && reverse) if (reverse != null && reverse)
@ -232,7 +249,8 @@ public class HSQLDBAssetRepository implements AssetRepository {
BigDecimal totalUnfulfilled = resultSet.getBigDecimal(2); BigDecimal totalUnfulfilled = resultSet.getBigDecimal(2);
long timestamp = resultSet.getTimestamp(3).getTime(); long timestamp = resultSet.getTimestamp(3).getTime();
OrderData order = new OrderData(null, null, haveAssetId, wantAssetId, totalUnfulfilled, BigDecimal.ZERO, price, timestamp, false, false); OrderData order = new OrderData(null, null, haveAssetId, wantAssetId, totalUnfulfilled, BigDecimal.ZERO,
price, timestamp, false, false);
orders.add(order); orders.add(order);
} while (resultSet.next()); } while (resultSet.next());
@ -243,8 +261,8 @@ public class HSQLDBAssetRepository implements AssetRepository {
} }
@Override @Override
public List<OrderData> getAccountsOrders(byte[] publicKey, Boolean optIsClosed, Boolean optIsFulfilled, Integer limit, Integer offset, Boolean reverse) public List<OrderData> getAccountsOrders(byte[] publicKey, Boolean optIsClosed, Boolean optIsFulfilled,
throws DataException { Integer limit, Integer offset, Boolean reverse) throws DataException {
String sql = "SELECT asset_order_id, have_asset_id, want_asset_id, amount, fulfilled, price, ordered, is_closed, is_fulfilled FROM AssetOrders WHERE creator = ?"; String sql = "SELECT asset_order_id, have_asset_id, want_asset_id, amount, fulfilled, price, ordered, is_closed, is_fulfilled FROM AssetOrders WHERE creator = ?";
if (optIsClosed != null) if (optIsClosed != null)
sql += " AND is_closed = " + (optIsClosed ? "TRUE" : "FALSE"); sql += " AND is_closed = " + (optIsClosed ? "TRUE" : "FALSE");
@ -272,7 +290,8 @@ public class HSQLDBAssetRepository implements AssetRepository {
boolean isClosed = resultSet.getBoolean(8); boolean isClosed = resultSet.getBoolean(8);
boolean isFulfilled = resultSet.getBoolean(9); boolean isFulfilled = resultSet.getBoolean(9);
OrderData order = new OrderData(orderId, publicKey, haveAssetId, wantAssetId, amount, fulfilled, price, timestamp, isClosed, isFulfilled); OrderData order = new OrderData(orderId, publicKey, haveAssetId, wantAssetId, amount, fulfilled, price,
timestamp, isClosed, isFulfilled);
orders.add(order); orders.add(order);
} while (resultSet.next()); } while (resultSet.next());
@ -283,8 +302,8 @@ public class HSQLDBAssetRepository implements AssetRepository {
} }
@Override @Override
public List<OrderData> getAccountsOrders(byte[] publicKey, long haveAssetId, long wantAssetId, Boolean optIsClosed, Boolean optIsFulfilled, Integer limit, public List<OrderData> getAccountsOrders(byte[] publicKey, long haveAssetId, long wantAssetId, Boolean optIsClosed,
Integer offset, Boolean reverse) throws DataException { Boolean optIsFulfilled, Integer limit, Integer offset, Boolean reverse) throws DataException {
String sql = "SELECT asset_order_id, amount, fulfilled, price, ordered, is_closed, is_fulfilled FROM AssetOrders WHERE creator = ? AND have_asset_id = ? AND want_asset_id = ?"; String sql = "SELECT asset_order_id, amount, fulfilled, price, ordered, is_closed, is_fulfilled FROM AssetOrders WHERE creator = ? AND have_asset_id = ? AND want_asset_id = ?";
if (optIsClosed != null) if (optIsClosed != null)
sql += " AND is_closed = " + (optIsClosed ? "TRUE" : "FALSE"); sql += " AND is_closed = " + (optIsClosed ? "TRUE" : "FALSE");
@ -310,7 +329,8 @@ public class HSQLDBAssetRepository implements AssetRepository {
boolean isClosed = resultSet.getBoolean(6); boolean isClosed = resultSet.getBoolean(6);
boolean isFulfilled = resultSet.getBoolean(7); boolean isFulfilled = resultSet.getBoolean(7);
OrderData order = new OrderData(orderId, publicKey, haveAssetId, wantAssetId, amount, fulfilled, price, timestamp, isClosed, isFulfilled); OrderData order = new OrderData(orderId, publicKey, haveAssetId, wantAssetId, amount, fulfilled, price,
timestamp, isClosed, isFulfilled);
orders.add(order); orders.add(order);
} while (resultSet.next()); } while (resultSet.next());
@ -325,8 +345,9 @@ public class HSQLDBAssetRepository implements AssetRepository {
HSQLDBSaver saveHelper = new HSQLDBSaver("AssetOrders"); HSQLDBSaver saveHelper = new HSQLDBSaver("AssetOrders");
saveHelper.bind("asset_order_id", orderData.getOrderId()).bind("creator", orderData.getCreatorPublicKey()) saveHelper.bind("asset_order_id", orderData.getOrderId()).bind("creator", orderData.getCreatorPublicKey())
.bind("have_asset_id", orderData.getHaveAssetId()).bind("want_asset_id", orderData.getWantAssetId()).bind("amount", orderData.getAmount()) .bind("have_asset_id", orderData.getHaveAssetId()).bind("want_asset_id", orderData.getWantAssetId())
.bind("fulfilled", orderData.getFulfilled()).bind("price", orderData.getPrice()).bind("ordered", new Timestamp(orderData.getTimestamp())) .bind("amount", orderData.getAmount()).bind("fulfilled", orderData.getFulfilled())
.bind("price", orderData.getPrice()).bind("ordered", new Timestamp(orderData.getTimestamp()))
.bind("is_closed", orderData.getIsClosed()).bind("is_fulfilled", orderData.getIsFulfilled()); .bind("is_closed", orderData.getIsClosed()).bind("is_fulfilled", orderData.getIsFulfilled());
try { try {
@ -348,8 +369,9 @@ public class HSQLDBAssetRepository implements AssetRepository {
// Trades // Trades
@Override @Override
public List<TradeData> getTrades(long haveAssetId, long wantAssetId, Integer limit, Integer offset, Boolean reverse) throws DataException { public List<TradeData> getTrades(long haveAssetId, long wantAssetId, Integer limit, Integer offset, Boolean reverse)
String sql = "SELECT initiating_order_id, target_order_id, AssetTrades.amount, AssetTrades.price, traded FROM AssetOrders JOIN AssetTrades ON initiating_order_id = asset_order_id " throws DataException {
String sql = "SELECT initiating_order_id, target_order_id, AssetTrades.target_amount, AssetTrades.initiator_amount, traded FROM AssetOrders JOIN AssetTrades ON initiating_order_id = asset_order_id "
+ "WHERE have_asset_id = ? AND want_asset_id = ? ORDER BY traded"; + "WHERE have_asset_id = ? AND want_asset_id = ? ORDER BY traded";
if (reverse != null && reverse) if (reverse != null && reverse)
sql += " DESC"; sql += " DESC";
@ -364,11 +386,12 @@ public class HSQLDBAssetRepository implements AssetRepository {
do { do {
byte[] initiatingOrderId = resultSet.getBytes(1); byte[] initiatingOrderId = resultSet.getBytes(1);
byte[] targetOrderId = resultSet.getBytes(2); byte[] targetOrderId = resultSet.getBytes(2);
BigDecimal amount = resultSet.getBigDecimal(3); BigDecimal targetAmount = resultSet.getBigDecimal(3);
BigDecimal price = resultSet.getBigDecimal(4); BigDecimal initiatorAmount = resultSet.getBigDecimal(4);
long timestamp = resultSet.getTimestamp(5, Calendar.getInstance(HSQLDBRepository.UTC)).getTime(); long timestamp = resultSet.getTimestamp(5, Calendar.getInstance(HSQLDBRepository.UTC)).getTime();
TradeData trade = new TradeData(initiatingOrderId, targetOrderId, amount, price, timestamp); TradeData trade = new TradeData(initiatingOrderId, targetOrderId, targetAmount, initiatorAmount,
timestamp);
trades.add(trade); trades.add(trade);
} while (resultSet.next()); } while (resultSet.next());
@ -379,19 +402,25 @@ public class HSQLDBAssetRepository implements AssetRepository {
} }
@Override @Override
public List<RecentTradeData> getRecentTrades(List<Long> assetIds, List<Long> otherAssetIds, Integer limit, Integer offset, Boolean reverse) throws DataException { public List<RecentTradeData> getRecentTrades(List<Long> assetIds, List<Long> otherAssetIds, Integer limit,
Integer offset, Boolean reverse) throws DataException {
// Find assetID pairs that have actually been traded // Find assetID pairs that have actually been traded
String tradedAssetsSubquery = "SELECT have_asset_id, want_asset_id " + "FROM AssetTrades JOIN AssetOrders ON asset_order_id = initiating_order_id "; String tradedAssetsSubquery = "SELECT have_asset_id, want_asset_id "
+ "FROM AssetTrades JOIN AssetOrders ON asset_order_id = initiating_order_id ";
// Optionally limit traded assetID pairs // Optionally limit traded assetID pairs
if (!assetIds.isEmpty()) if (!assetIds.isEmpty())
// longs are safe enough to use literally // longs are safe enough to use literally
tradedAssetsSubquery += "WHERE have_asset_id IN (" + String.join(", ", assetIds.stream().map(assetId -> assetId.toString()).collect(Collectors.toList())) + ")"; tradedAssetsSubquery += "WHERE have_asset_id IN (" + String.join(", ",
assetIds.stream().map(assetId -> assetId.toString()).collect(Collectors.toList())) + ")";
if (!otherAssetIds.isEmpty()) { if (!otherAssetIds.isEmpty()) {
tradedAssetsSubquery += assetIds.isEmpty() ? " WHERE " : " AND "; tradedAssetsSubquery += assetIds.isEmpty() ? " WHERE " : " AND ";
// longs are safe enough to use literally // longs are safe enough to use literally
tradedAssetsSubquery += "want_asset_id IN (" + String.join(", ", otherAssetIds.stream().map(assetId -> assetId.toString()).collect(Collectors.toList())) + ")"; tradedAssetsSubquery += "want_asset_id IN ("
+ String.join(", ",
otherAssetIds.stream().map(assetId -> assetId.toString()).collect(Collectors.toList()))
+ ")";
} }
tradedAssetsSubquery += " GROUP BY have_asset_id, want_asset_id"; tradedAssetsSubquery += " GROUP BY have_asset_id, want_asset_id";
@ -403,8 +432,9 @@ public class HSQLDBAssetRepository implements AssetRepository {
+ "ORDER BY traded DESC LIMIT 2"; + "ORDER BY traded DESC LIMIT 2";
// Put it all together // Put it all together
String sql = "SELECT have_asset_id, want_asset_id, RecentTrades.amount, RecentTrades.price, RecentTrades.traded " + "FROM (" + tradedAssetsSubquery String sql = "SELECT have_asset_id, want_asset_id, RecentTrades.target_amount, RecentTrades.initiator_amount, RecentTrades.traded "
+ ") AS TradedAssets " + ", LATERAL (" + recentTradesSubquery + ") AS RecentTrades (amount, price, traded) " + "ORDER BY have_asset_id"; + "FROM (" + tradedAssetsSubquery + ") AS TradedAssets " + ", LATERAL (" + recentTradesSubquery
+ ") AS RecentTrades (target_amount, initiator_amount, traded) " + "ORDER BY have_asset_id";
if (reverse != null && reverse) if (reverse != null && reverse)
sql += " DESC"; sql += " DESC";
@ -425,11 +455,12 @@ public class HSQLDBAssetRepository implements AssetRepository {
do { do {
long haveAssetId = resultSet.getLong(1); long haveAssetId = resultSet.getLong(1);
long wantAssetId = resultSet.getLong(2); long wantAssetId = resultSet.getLong(2);
BigDecimal amount = resultSet.getBigDecimal(3); BigDecimal otherAmount = resultSet.getBigDecimal(3);
BigDecimal price = resultSet.getBigDecimal(4); BigDecimal amount = resultSet.getBigDecimal(4);
long timestamp = resultSet.getTimestamp(5, Calendar.getInstance(HSQLDBRepository.UTC)).getTime(); long timestamp = resultSet.getTimestamp(5, Calendar.getInstance(HSQLDBRepository.UTC)).getTime();
RecentTradeData recentTrade = new RecentTradeData(haveAssetId, wantAssetId, amount, price, timestamp); RecentTradeData recentTrade = new RecentTradeData(haveAssetId, wantAssetId, otherAmount, amount,
timestamp);
recentTrades.add(recentTrade); recentTrades.add(recentTrade);
} while (resultSet.next()); } while (resultSet.next());
@ -440,8 +471,9 @@ public class HSQLDBAssetRepository implements AssetRepository {
} }
@Override @Override
public List<TradeData> getOrdersTrades(byte[] orderId, Integer limit, Integer offset, Boolean reverse) throws DataException { public List<TradeData> getOrdersTrades(byte[] orderId, Integer limit, Integer offset, Boolean reverse)
String sql = "SELECT initiating_order_id, target_order_id, amount, price, traded FROM AssetTrades WHERE initiating_order_id = ? OR target_order_id = ? ORDER BY traded"; throws DataException {
String sql = "SELECT initiating_order_id, target_order_id, target_amount, initiator_amount, traded FROM AssetTrades WHERE initiating_order_id = ? OR target_order_id = ? ORDER BY traded";
if (reverse != null && reverse) if (reverse != null && reverse)
sql += " DESC"; sql += " DESC";
sql += HSQLDBRepository.limitOffsetSql(limit, offset); sql += HSQLDBRepository.limitOffsetSql(limit, offset);
@ -455,11 +487,12 @@ public class HSQLDBAssetRepository implements AssetRepository {
do { do {
byte[] initiatingOrderId = resultSet.getBytes(1); byte[] initiatingOrderId = resultSet.getBytes(1);
byte[] targetOrderId = resultSet.getBytes(2); byte[] targetOrderId = resultSet.getBytes(2);
BigDecimal amount = resultSet.getBigDecimal(3); BigDecimal targetAmount = resultSet.getBigDecimal(3);
BigDecimal price = resultSet.getBigDecimal(4); BigDecimal initiatorAmount = resultSet.getBigDecimal(4);
long timestamp = resultSet.getTimestamp(5, Calendar.getInstance(HSQLDBRepository.UTC)).getTime(); long timestamp = resultSet.getTimestamp(5, Calendar.getInstance(HSQLDBRepository.UTC)).getTime();
TradeData trade = new TradeData(initiatingOrderId, targetOrderId, amount, price, timestamp); TradeData trade = new TradeData(initiatingOrderId, targetOrderId, targetAmount, initiatorAmount,
timestamp);
trades.add(trade); trades.add(trade);
} while (resultSet.next()); } while (resultSet.next());
@ -473,8 +506,10 @@ public class HSQLDBAssetRepository implements AssetRepository {
public void save(TradeData tradeData) throws DataException { public void save(TradeData tradeData) throws DataException {
HSQLDBSaver saveHelper = new HSQLDBSaver("AssetTrades"); HSQLDBSaver saveHelper = new HSQLDBSaver("AssetTrades");
saveHelper.bind("initiating_order_id", tradeData.getInitiator()).bind("target_order_id", tradeData.getTarget()).bind("amount", tradeData.getAmount()) saveHelper.bind("initiating_order_id", tradeData.getInitiator()).bind("target_order_id", tradeData.getTarget())
.bind("price", tradeData.getPrice()).bind("traded", new Timestamp(tradeData.getTimestamp())); .bind("target_amount", tradeData.getTargetAmount())
.bind("initiator_amount", tradeData.getInitiatorAmount())
.bind("traded", new Timestamp(tradeData.getTimestamp()));
try { try {
saveHelper.execute(this.repository); saveHelper.execute(this.repository);
@ -486,8 +521,10 @@ public class HSQLDBAssetRepository implements AssetRepository {
@Override @Override
public void delete(TradeData tradeData) throws DataException { public void delete(TradeData tradeData) throws DataException {
try { try {
this.repository.delete("AssetTrades", "initiating_order_id = ? AND target_order_id = ? AND amount = ? AND price = ?", tradeData.getInitiator(), this.repository.delete("AssetTrades",
tradeData.getTarget(), tradeData.getAmount(), tradeData.getPrice()); "initiating_order_id = ? AND target_order_id = ? AND target_amount = ? AND initiator_amount = ?",
tradeData.getInitiator(), tradeData.getTarget(), tradeData.getTargetAmount(),
tradeData.getInitiatorAmount());
} catch (SQLException e) { } catch (SQLException e) {
throw new DataException("Unable to delete asset trade from repository", e); throw new DataException("Unable to delete asset trade from repository", e);
} }

View File

@ -592,6 +592,26 @@ public class HSQLDBDatabaseUpdates {
stmt.execute("UPDATE AssetOrders SET is_closed = TRUE WHERE is_fulfilled = TRUE"); stmt.execute("UPDATE AssetOrders SET is_closed = TRUE WHERE is_fulfilled = TRUE");
break; break;
case 38:
// Rename asset trade columns for clarity
stmt.execute("ALTER TABLE AssetTrades ALTER COLUMN amount RENAME TO target_amount");
stmt.execute("ALTER TABLE AssetTrades ALTER COLUMN price RENAME TO initiator_amount");
// Add support for asset "data" - typically JSON map like registered name data
stmt.execute("CREATE TYPE AssetData AS VARCHAR(4000)");
stmt.execute("ALTER TABLE Assets ADD data AssetData NOT NULL DEFAULT '' BEFORE reference");
stmt.execute("ALTER TABLE Assets ADD creation_group_id GroupID NOT NULL DEFAULT 0 BEFORE reference");
// Add support for asset "data" to ISSUE_ASSET transaction
stmt.execute("ALTER TABLE IssueAssetTransactions ADD data AssetData NOT NULL DEFAULT '' BEFORE asset_id");
// Add support for UPDATE_ASSET transactions
stmt.execute("CREATE TABLE UpdateAssetTransactions (signature Signature, owner QoraPublicKey NOT NULL, asset_id AssetID NOT NULL, "
+ "new_owner QoraAddress NOT NULL, new_description GenericDescription NOT NULL, new_data AssetData NOT NULL, "
+ "orphan_reference Signature, PRIMARY KEY (signature), FOREIGN KEY (signature) REFERENCES Transactions (signature) ON DELETE CASCADE)");
// Correct Assets.reference to use ISSUE_ASSET transaction's signature instead of reference.
// This is to help UPDATE_ASSET orphaning.
stmt.execute("MERGE INTO Assets USING (SELECT asset_id, signature FROM Assets JOIN Transactions USING (reference) JOIN IssueAssetTransactions USING (signature)) AS Updates "
+ "ON Assets.asset_id = Updates.asset_id WHEN MATCHED THEN UPDATE SET Assets.reference = Updates.signature");
break;
default: default:
// nothing to do // nothing to do
return false; return false;

View File

@ -18,7 +18,7 @@ public class HSQLDBIssueAssetTransactionRepository extends HSQLDBTransactionRepo
TransactionData fromBase(long timestamp, int txGroupId, byte[] reference, byte[] creatorPublicKey, BigDecimal fee, byte[] signature) throws DataException { TransactionData fromBase(long timestamp, int txGroupId, byte[] reference, byte[] creatorPublicKey, BigDecimal fee, byte[] signature) throws DataException {
try (ResultSet resultSet = this.repository.checkedExecute( try (ResultSet resultSet = this.repository.checkedExecute(
"SELECT owner, asset_name, description, quantity, is_divisible, asset_id FROM IssueAssetTransactions WHERE signature = ?", signature)) { "SELECT owner, asset_name, description, quantity, is_divisible, data, asset_id FROM IssueAssetTransactions WHERE signature = ?", signature)) {
if (resultSet == null) if (resultSet == null)
return null; return null;
@ -27,14 +27,15 @@ public class HSQLDBIssueAssetTransactionRepository extends HSQLDBTransactionRepo
String description = resultSet.getString(3); String description = resultSet.getString(3);
long quantity = resultSet.getLong(4); long quantity = resultSet.getLong(4);
boolean isDivisible = resultSet.getBoolean(5); boolean isDivisible = resultSet.getBoolean(5);
String data = resultSet.getString(6);
// Special null-checking for asset ID // Special null-checking for asset ID
Long assetId = resultSet.getLong(6); Long assetId = resultSet.getLong(7);
if (resultSet.wasNull()) if (resultSet.wasNull())
assetId = null; assetId = null;
return new IssueAssetTransactionData(timestamp, txGroupId, reference, creatorPublicKey, assetId, owner, assetName, description, quantity, isDivisible, return new IssueAssetTransactionData(timestamp, txGroupId, reference, creatorPublicKey, assetId, owner, assetName, description, quantity, isDivisible,
fee, signature); data, fee, signature);
} catch (SQLException e) { } catch (SQLException e) {
throw new DataException("Unable to fetch issue asset transaction from repository", e); throw new DataException("Unable to fetch issue asset transaction from repository", e);
} }
@ -49,7 +50,8 @@ public class HSQLDBIssueAssetTransactionRepository extends HSQLDBTransactionRepo
saveHelper.bind("signature", issueAssetTransactionData.getSignature()).bind("issuer", issueAssetTransactionData.getIssuerPublicKey()) saveHelper.bind("signature", issueAssetTransactionData.getSignature()).bind("issuer", issueAssetTransactionData.getIssuerPublicKey())
.bind("owner", issueAssetTransactionData.getOwner()).bind("asset_name", issueAssetTransactionData.getAssetName()) .bind("owner", issueAssetTransactionData.getOwner()).bind("asset_name", issueAssetTransactionData.getAssetName())
.bind("description", issueAssetTransactionData.getDescription()).bind("quantity", issueAssetTransactionData.getQuantity()) .bind("description", issueAssetTransactionData.getDescription()).bind("quantity", issueAssetTransactionData.getQuantity())
.bind("is_divisible", issueAssetTransactionData.getIsDivisible()).bind("asset_id", issueAssetTransactionData.getAssetId()); .bind("is_divisible", issueAssetTransactionData.getIsDivisible())
.bind("data", issueAssetTransactionData.getData()).bind("asset_id", issueAssetTransactionData.getAssetId());
try { try {
saveHelper.execute(this.repository); saveHelper.execute(this.repository);

View File

@ -0,0 +1,61 @@
package org.qora.repository.hsqldb.transaction;
import java.math.BigDecimal;
import java.sql.ResultSet;
import java.sql.SQLException;
import org.qora.data.transaction.TransactionData;
import org.qora.data.transaction.UpdateAssetTransactionData;
import org.qora.repository.DataException;
import org.qora.repository.hsqldb.HSQLDBRepository;
import org.qora.repository.hsqldb.HSQLDBSaver;
public class HSQLDBUpdateAssetTransactionRepository extends HSQLDBTransactionRepository {
public HSQLDBUpdateAssetTransactionRepository(HSQLDBRepository repository) {
this.repository = repository;
}
TransactionData fromBase(long timestamp, int txGroupId, byte[] reference, byte[] creatorPublicKey, BigDecimal fee,
byte[] signature) throws DataException {
try (ResultSet resultSet = this.repository.checkedExecute(
"SELECT asset_id, new_owner, new_description, new_data, orphan_reference FROM UpdateAssetTransactions WHERE signature = ?",
signature)) {
if (resultSet == null)
return null;
long assetId = resultSet.getLong(1);
String newOwner = resultSet.getString(2);
String newDescription = resultSet.getString(3);
String newData = resultSet.getString(4);
byte[] orphanReference = resultSet.getBytes(5);
return new UpdateAssetTransactionData(timestamp, txGroupId, reference, creatorPublicKey, assetId, newOwner,
newDescription, newData, fee, orphanReference, signature);
} catch (SQLException e) {
throw new DataException("Unable to fetch update asset transaction from repository", e);
}
}
@Override
public void save(TransactionData transactionData) throws DataException {
UpdateAssetTransactionData updateAssetTransactionData = (UpdateAssetTransactionData) transactionData;
HSQLDBSaver saveHelper = new HSQLDBSaver("UpdateAssetTransactions");
saveHelper.bind("signature", updateAssetTransactionData.getSignature())
.bind("owner", updateAssetTransactionData.getOwnerPublicKey())
.bind("asset_id", updateAssetTransactionData.getAssetId())
.bind("new_owner", updateAssetTransactionData.getNewOwner())
.bind("new_description", updateAssetTransactionData.getNewDescription())
.bind("new_data", updateAssetTransactionData.getNewData())
.bind("orphan_reference", updateAssetTransactionData.getOrphanReference());
try {
saveHelper.execute(this.repository);
} catch (SQLException e) {
throw new DataException("Unable to save update asset transaction into repository", e);
}
}
}

View File

@ -22,10 +22,6 @@ public class IssueAssetTransaction extends Transaction {
// Properties // Properties
private IssueAssetTransactionData issueAssetTransactionData; private IssueAssetTransactionData issueAssetTransactionData;
// Other useful constants
public static final int MAX_NAME_SIZE = 400;
public static final int MAX_DESCRIPTION_SIZE = 4000;
// Constructors // Constructors
public IssueAssetTransaction(Repository repository, TransactionData transactionData) { public IssueAssetTransaction(Repository repository, TransactionData transactionData) {
@ -86,22 +82,35 @@ public class IssueAssetTransaction extends Transaction {
if (this.issueAssetTransactionData.getTimestamp() < BlockChain.getInstance().getAssetsReleaseTimestamp()) if (this.issueAssetTransactionData.getTimestamp() < BlockChain.getInstance().getAssetsReleaseTimestamp())
return ValidationResult.NOT_YET_RELEASED; return ValidationResult.NOT_YET_RELEASED;
// "data" field is only allowed in v2
String data = this.issueAssetTransactionData.getData();
if (this.issueAssetTransactionData.getTimestamp() >= BlockChain.getInstance().getQoraV2Timestamp()) {
// v2 so check data field properly
int dataLength = Utf8.encodedLength(data);
if (data == null || dataLength < 1 || dataLength > Asset.MAX_DATA_SIZE)
return ValidationResult.INVALID_DATA_LENGTH;
} else {
// pre-v2 so disallow data field
if (data != null)
return ValidationResult.NOT_YET_RELEASED;
}
// Check owner address is valid // Check owner address is valid
if (!Crypto.isValidAddress(issueAssetTransactionData.getOwner())) if (!Crypto.isValidAddress(issueAssetTransactionData.getOwner()))
return ValidationResult.INVALID_ADDRESS; return ValidationResult.INVALID_ADDRESS;
// Check name size bounds // Check name size bounds
int assetNameLength = Utf8.encodedLength(issueAssetTransactionData.getAssetName()); int assetNameLength = Utf8.encodedLength(issueAssetTransactionData.getAssetName());
if (assetNameLength < 1 || assetNameLength > IssueAssetTransaction.MAX_NAME_SIZE) if (assetNameLength < 1 || assetNameLength > Asset.MAX_NAME_SIZE)
return ValidationResult.INVALID_NAME_LENGTH; return ValidationResult.INVALID_NAME_LENGTH;
// Check description size bounds // Check description size bounds
int assetDescriptionlength = Utf8.encodedLength(issueAssetTransactionData.getDescription()); int assetDescriptionlength = Utf8.encodedLength(issueAssetTransactionData.getDescription());
if (assetDescriptionlength < 1 || assetDescriptionlength > IssueAssetTransaction.MAX_DESCRIPTION_SIZE) if (assetDescriptionlength < 1 || assetDescriptionlength > Asset.MAX_DESCRIPTION_SIZE)
return ValidationResult.INVALID_DESCRIPTION_LENGTH; return ValidationResult.INVALID_DESCRIPTION_LENGTH;
// Check quantity - either 10 billion or if that's not enough: a billion billion! // Check quantity - either 10 billion or if that's not enough: a billion billion!
long maxQuantity = issueAssetTransactionData.getIsDivisible() ? 10_000_000_000L : 1_000_000_000_000_000_000L; long maxQuantity = issueAssetTransactionData.getIsDivisible() ? Asset.MAX_DIVISIBLE_QUANTITY : Asset.MAX_INDIVISIBLE_QUANTITY;
if (issueAssetTransactionData.getQuantity() < 1 || issueAssetTransactionData.getQuantity() > maxQuantity) if (issueAssetTransactionData.getQuantity() < 1 || issueAssetTransactionData.getQuantity() > maxQuantity)
return ValidationResult.INVALID_QUANTITY; return ValidationResult.INVALID_QUANTITY;

View File

@ -71,7 +71,8 @@ public abstract class Transaction {
JOIN_GROUP(31, false), JOIN_GROUP(31, false),
LEAVE_GROUP(32, false), LEAVE_GROUP(32, false),
GROUP_APPROVAL(33, false), GROUP_APPROVAL(33, false),
SET_GROUP(34, false); SET_GROUP(34, false),
UPDATE_ASSET(35, true);
public final int value; public final int value;
public final boolean needsApproval; public final boolean needsApproval;
@ -187,6 +188,7 @@ public abstract class Transaction {
INVALID_TX_GROUP_ID(67), INVALID_TX_GROUP_ID(67),
TX_GROUP_ID_MISMATCH(68), TX_GROUP_ID_MISMATCH(68),
MULTIPLE_NAMES_FORBIDDEN(69), MULTIPLE_NAMES_FORBIDDEN(69),
INVALID_ASSET_OWNER(70),
NOT_YET_RELEASED(1000); NOT_YET_RELEASED(1000);
public final int value; public final int value;

View File

@ -0,0 +1,164 @@
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.block.BlockChain;
import org.qora.crypto.Crypto;
import org.qora.data.asset.AssetData;
import org.qora.data.transaction.TransactionData;
import org.qora.data.transaction.UpdateAssetTransactionData;
import org.qora.repository.DataException;
import org.qora.repository.Repository;
import com.google.common.base.Utf8;
public class UpdateAssetTransaction extends Transaction {
// Properties
private UpdateAssetTransactionData updateAssetTransactionData;
// Constructors
public UpdateAssetTransaction(Repository repository, TransactionData transactionData) {
super(repository, transactionData);
this.updateAssetTransactionData = (UpdateAssetTransactionData) 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 PublicKeyAccount getOwner() throws DataException {
return new PublicKeyAccount(this.repository, this.updateAssetTransactionData.getOwnerPublicKey());
}
public Account getNewOwner() throws DataException {
return new Account(this.repository, this.updateAssetTransactionData.getNewOwner());
}
// Processing
@Override
public ValidationResult isValid() throws DataException {
// V2-only transaction
if (this.updateAssetTransactionData.getTimestamp() < BlockChain.getInstance().getQoraV2Timestamp())
return ValidationResult.NOT_YET_RELEASED;
// Check asset actually exists
AssetData assetData = this.repository.getAssetRepository().fromAssetId(updateAssetTransactionData.getAssetId());
if (assetData == null)
return ValidationResult.ASSET_DOES_NOT_EXIST;
// Check transaction's public key matches asset's current owner
PublicKeyAccount currentOwner = getOwner();
if (!assetData.getOwner().equals(currentOwner.getAddress()))
return ValidationResult.INVALID_ASSET_OWNER;
// Check new owner address is valid
if (!Crypto.isValidAddress(updateAssetTransactionData.getNewOwner()))
return ValidationResult.INVALID_ADDRESS;
// Check new description size bounds
int newDescriptionLength = Utf8.encodedLength(updateAssetTransactionData.getNewDescription());
if (newDescriptionLength < 1 || newDescriptionLength > Asset.MAX_DESCRIPTION_SIZE)
return ValidationResult.INVALID_DATA_LENGTH;
// Check new data size bounds
int newDataLength = Utf8.encodedLength(updateAssetTransactionData.getNewData());
if (newDataLength < 1 || newDataLength > Asset.MAX_DATA_SIZE)
return ValidationResult.INVALID_DATA_LENGTH;
// As this transaction type could require approval, check txGroupId
// matches groupID at creation
if (assetData.getCreationGroupId() != updateAssetTransactionData.getTxGroupId())
return ValidationResult.TX_GROUP_ID_MISMATCH;
// Check fee is positive
if (updateAssetTransactionData.getFee().compareTo(BigDecimal.ZERO) <= 0)
return ValidationResult.NEGATIVE_FEE;
// Check reference is correct
if (!Arrays.equals(currentOwner.getLastReference(), updateAssetTransactionData.getReference()))
return ValidationResult.INVALID_REFERENCE;
// Check current owner has enough funds
if (currentOwner.getConfirmedBalance(Asset.QORA).compareTo(updateAssetTransactionData.getFee()) < 0)
return ValidationResult.NO_BALANCE;
return ValidationResult.OK;
}
@Override
public void process() throws DataException {
// Update Asset
Asset asset = new Asset(this.repository, updateAssetTransactionData.getAssetId());
asset.update(updateAssetTransactionData);
// Save this transaction, now with updated "name reference" to previous
// transaction that updated name
this.repository.getTransactionRepository().save(updateAssetTransactionData);
// Update old owner's balance
Account owner = getOwner();
owner.setConfirmedBalance(Asset.QORA,
owner.getConfirmedBalance(Asset.QORA).subtract(updateAssetTransactionData.getFee()));
// Update owner's reference
owner.setLastReference(updateAssetTransactionData.getSignature());
}
@Override
public void orphan() throws DataException {
// Revert asset
Asset asset = new Asset(this.repository, updateAssetTransactionData.getAssetId());
asset.revert(updateAssetTransactionData);
// Delete this transaction itself
this.repository.getTransactionRepository().delete(updateAssetTransactionData);
// Update owner's balance
Account owner = getOwner();
owner.setConfirmedBalance(Asset.QORA,
owner.getConfirmedBalance(Asset.QORA).add(updateAssetTransactionData.getFee()));
// Update owner's reference
owner.setLastReference(updateAssetTransactionData.getReference());
}
}

View File

@ -6,10 +6,10 @@ import java.math.BigDecimal;
import java.nio.ByteBuffer; import java.nio.ByteBuffer;
import java.util.Arrays; import java.util.Arrays;
import org.qora.asset.Asset;
import org.qora.block.BlockChain; import org.qora.block.BlockChain;
import org.qora.data.transaction.IssueAssetTransactionData; import org.qora.data.transaction.IssueAssetTransactionData;
import org.qora.data.transaction.TransactionData; import org.qora.data.transaction.TransactionData;
import org.qora.transaction.IssueAssetTransaction;
import org.qora.transaction.Transaction.TransactionType; import org.qora.transaction.Transaction.TransactionType;
import org.qora.transform.TransformationException; import org.qora.transform.TransformationException;
import org.qora.utils.Serialization; import org.qora.utils.Serialization;
@ -26,8 +26,10 @@ public class IssueAssetTransactionTransformer extends TransactionTransformer {
private static final int QUANTITY_LENGTH = LONG_LENGTH; private static final int QUANTITY_LENGTH = LONG_LENGTH;
private static final int IS_DIVISIBLE_LENGTH = BOOLEAN_LENGTH; private static final int IS_DIVISIBLE_LENGTH = BOOLEAN_LENGTH;
private static final int ASSET_REFERENCE_LENGTH = REFERENCE_LENGTH; private static final int ASSET_REFERENCE_LENGTH = REFERENCE_LENGTH;
private static final int DATA_SIZE_LENGTH = INT_LENGTH;
private static final int EXTRAS_LENGTH = OWNER_LENGTH + NAME_SIZE_LENGTH + DESCRIPTION_SIZE_LENGTH + QUANTITY_LENGTH + IS_DIVISIBLE_LENGTH; private static final int EXTRAS_LENGTH = OWNER_LENGTH + NAME_SIZE_LENGTH + DESCRIPTION_SIZE_LENGTH + QUANTITY_LENGTH
+ IS_DIVISIBLE_LENGTH;
protected static final TransactionLayout layout; protected static final TransactionLayout layout;
@ -45,6 +47,8 @@ public class IssueAssetTransactionTransformer extends TransactionTransformer {
layout.add("asset description", TransformationType.STRING); layout.add("asset description", TransformationType.STRING);
layout.add("asset quantity", TransformationType.LONG); layout.add("asset quantity", TransformationType.LONG);
layout.add("can asset quantities be fractional?", TransformationType.BOOLEAN); layout.add("can asset quantities be fractional?", TransformationType.BOOLEAN);
layout.add("asset data length", TransformationType.INT);
layout.add("asset data", TransformationType.STRING);
layout.add("fee", TransformationType.AMOUNT); layout.add("fee", TransformationType.AMOUNT);
layout.add("signature", TransformationType.SIGNATURE); layout.add("signature", TransformationType.SIGNATURE);
} }
@ -63,16 +67,22 @@ public class IssueAssetTransactionTransformer extends TransactionTransformer {
String owner = Serialization.deserializeAddress(byteBuffer); String owner = Serialization.deserializeAddress(byteBuffer);
String assetName = Serialization.deserializeSizedString(byteBuffer, IssueAssetTransaction.MAX_NAME_SIZE); String assetName = Serialization.deserializeSizedString(byteBuffer, Asset.MAX_NAME_SIZE);
String description = Serialization.deserializeSizedString(byteBuffer, IssueAssetTransaction.MAX_DESCRIPTION_SIZE); String description = Serialization.deserializeSizedString(byteBuffer, Asset.MAX_DESCRIPTION_SIZE);
long quantity = byteBuffer.getLong(); long quantity = byteBuffer.getLong();
boolean isDivisible = byteBuffer.get() != 0; boolean isDivisible = byteBuffer.get() != 0;
// in v2, assets have "data" field
String data = null;
if (timestamp >= BlockChain.getInstance().getQoraV2Timestamp())
data = Serialization.deserializeSizedString(byteBuffer, Asset.MAX_DATA_SIZE);
byte[] assetReference = new byte[ASSET_REFERENCE_LENGTH]; byte[] assetReference = new byte[ASSET_REFERENCE_LENGTH];
// In v1, IssueAssetTransaction uses Asset.parse which also deserializes reference. // In v1, IssueAssetTransaction uses Asset.parse which also deserializes
// reference.
if (timestamp < BlockChain.getInstance().getQoraV2Timestamp()) if (timestamp < BlockChain.getInstance().getQoraV2Timestamp())
byteBuffer.get(assetReference); byteBuffer.get(assetReference);
@ -81,17 +91,23 @@ public class IssueAssetTransactionTransformer extends TransactionTransformer {
byte[] signature = new byte[SIGNATURE_LENGTH]; byte[] signature = new byte[SIGNATURE_LENGTH];
byteBuffer.get(signature); byteBuffer.get(signature);
return new IssueAssetTransactionData(timestamp, txGroupId, reference, issuerPublicKey, owner, assetName, description, quantity, isDivisible, fee, return new IssueAssetTransactionData(timestamp, txGroupId, reference, issuerPublicKey, owner, assetName,
signature); description, quantity, isDivisible, data, fee, signature);
} }
public static int getDataLength(TransactionData transactionData) throws TransformationException { public static int getDataLength(TransactionData transactionData) throws TransformationException {
IssueAssetTransactionData issueAssetTransactionData = (IssueAssetTransactionData) transactionData; IssueAssetTransactionData issueAssetTransactionData = (IssueAssetTransactionData) transactionData;
int dataLength = getBaseLength(transactionData) + EXTRAS_LENGTH + Utf8.encodedLength(issueAssetTransactionData.getAssetName()) int dataLength = getBaseLength(transactionData) + EXTRAS_LENGTH
+ Utf8.encodedLength(issueAssetTransactionData.getAssetName())
+ Utf8.encodedLength(issueAssetTransactionData.getDescription()); + Utf8.encodedLength(issueAssetTransactionData.getDescription());
// In v1, IssueAssetTransaction uses Asset.toBytes which also serializes reference. // In v2, assets have "data" field
if (transactionData.getTimestamp() < BlockChain.getInstance().getQoraV2Timestamp())
dataLength += DATA_SIZE_LENGTH + Utf8.encodedLength(issueAssetTransactionData.getData());
// In v1, IssueAssetTransaction uses Asset.toBytes which also serializes
// reference.
if (transactionData.getTimestamp() < BlockChain.getInstance().getQoraV2Timestamp()) if (transactionData.getTimestamp() < BlockChain.getInstance().getQoraV2Timestamp())
dataLength += ASSET_REFERENCE_LENGTH; dataLength += ASSET_REFERENCE_LENGTH;
@ -115,7 +131,13 @@ public class IssueAssetTransactionTransformer extends TransactionTransformer {
bytes.write(Longs.toByteArray(issueAssetTransactionData.getQuantity())); bytes.write(Longs.toByteArray(issueAssetTransactionData.getQuantity()));
bytes.write((byte) (issueAssetTransactionData.getIsDivisible() ? 1 : 0)); bytes.write((byte) (issueAssetTransactionData.getIsDivisible() ? 1 : 0));
// In v1, IssueAssetTransaction uses Asset.toBytes which also serializes Asset's reference which is the IssueAssetTransaction's signature // In v2, assets have "data"
if (transactionData.getTimestamp() >= BlockChain.getInstance().getQoraV2Timestamp())
Serialization.serializeSizedString(bytes, issueAssetTransactionData.getData());
// In v1, IssueAssetTransaction uses Asset.toBytes which also
// serializes Asset's reference which is the IssueAssetTransaction's
// signature
if (transactionData.getTimestamp() < BlockChain.getInstance().getQoraV2Timestamp()) { if (transactionData.getTimestamp() < BlockChain.getInstance().getQoraV2Timestamp()) {
byte[] assetReference = issueAssetTransactionData.getSignature(); byte[] assetReference = issueAssetTransactionData.getSignature();
if (assetReference != null) if (assetReference != null)
@ -136,7 +158,8 @@ public class IssueAssetTransactionTransformer extends TransactionTransformer {
} }
/** /**
* In Qora v1, the bytes used for verification have asset's reference zeroed so we need to test for v1-ness and adjust the bytes accordingly. * In Qora v1, the bytes used for verification have asset's reference zeroed
* so we need to test for v1-ness and adjust the bytes accordingly.
* *
* @param transactionData * @param transactionData
* @return byte[] * @return byte[]
@ -151,7 +174,11 @@ public class IssueAssetTransactionTransformer extends TransactionTransformer {
// Special v1 version // Special v1 version
// Zero duplicate signature/reference // Zero duplicate signature/reference
int start = bytes.length - ASSET_REFERENCE_LENGTH - FEE_LENGTH; // before asset reference (and fee) int start = bytes.length - ASSET_REFERENCE_LENGTH - FEE_LENGTH; // before
// asset
// reference
// (and
// fee)
int end = start + ASSET_REFERENCE_LENGTH; int end = start + ASSET_REFERENCE_LENGTH;
Arrays.fill(bytes, start, end, (byte) 0); Arrays.fill(bytes, start, end, (byte) 0);

View File

@ -208,11 +208,9 @@ public abstract class TransactionTransformer extends Transformer {
try { try {
return (TransactionData) method.invoke(null, byteBuffer); return (TransactionData) method.invoke(null, byteBuffer);
} catch (BufferUnderflowException e) {
throw new TransformationException("Byte data too short for transaction type [" + type.value + "]");
} catch (InvocationTargetException e) { } catch (InvocationTargetException e) {
if (e.getCause() instanceof BufferUnderflowException) if (e.getCause() instanceof BufferUnderflowException)
throw (BufferUnderflowException) e.getCause(); throw new TransformationException("Byte data too short for transaction type [" + type.value + "]");
if (e.getCause() instanceof TransformationException) if (e.getCause() instanceof TransformationException)
throw (TransformationException) e.getCause(); throw (TransformationException) e.getCause();

View File

@ -0,0 +1,115 @@
package org.qora.transform.transaction;
import java.io.ByteArrayOutputStream;
import java.io.IOException;
import java.math.BigDecimal;
import java.nio.ByteBuffer;
import org.qora.asset.Asset;
import org.qora.block.BlockChain;
import org.qora.data.transaction.UpdateAssetTransactionData;
import org.qora.data.transaction.TransactionData;
import org.qora.transaction.Transaction.TransactionType;
import org.qora.transform.TransformationException;
import org.qora.utils.Serialization;
import com.google.common.base.Utf8;
import com.google.common.primitives.Longs;
public class UpdateAssetTransactionTransformer extends TransactionTransformer {
// Property lengths
private static final int ASSET_ID_LENGTH = LONG_LENGTH;
private static final int NEW_OWNER_LENGTH = ADDRESS_LENGTH;
private static final int NEW_DESCRIPTION_SIZE_LENGTH = INT_LENGTH;
private static final int NEW_DATA_SIZE_LENGTH = INT_LENGTH;
private static final int EXTRAS_LENGTH = ASSET_ID_LENGTH + NEW_OWNER_LENGTH + NEW_DESCRIPTION_SIZE_LENGTH
+ NEW_DATA_SIZE_LENGTH;
protected static final TransactionLayout layout;
static {
layout = new TransactionLayout();
layout.add("txType: " + TransactionType.ISSUE_ASSET.valueString, TransformationType.INT);
layout.add("timestamp", TransformationType.TIMESTAMP);
layout.add("transaction's groupID", TransformationType.INT);
layout.add("reference", TransformationType.SIGNATURE);
layout.add("asset owner's public key", TransformationType.PUBLIC_KEY);
layout.add("asset ID", TransformationType.LONG);
layout.add("asset new owner", TransformationType.ADDRESS);
layout.add("asset new description length", TransformationType.INT);
layout.add("asset new description", TransformationType.STRING);
layout.add("asset new data length", TransformationType.INT);
layout.add("asset new data", TransformationType.STRING);
layout.add("fee", TransformationType.AMOUNT);
layout.add("signature", TransformationType.SIGNATURE);
}
public static TransactionData fromByteBuffer(ByteBuffer byteBuffer) throws TransformationException {
long timestamp = byteBuffer.getLong();
int txGroupId = 0;
if (timestamp >= BlockChain.getInstance().getQoraV2Timestamp())
txGroupId = byteBuffer.getInt();
byte[] reference = new byte[REFERENCE_LENGTH];
byteBuffer.get(reference);
byte[] ownerPublicKey = Serialization.deserializePublicKey(byteBuffer);
long assetId = byteBuffer.getLong();
String newOwner = Serialization.deserializeAddress(byteBuffer);
String newDescription = Serialization.deserializeSizedString(byteBuffer, Asset.MAX_DESCRIPTION_SIZE);
String newData = Serialization.deserializeSizedString(byteBuffer, Asset.MAX_DATA_SIZE);
BigDecimal fee = Serialization.deserializeBigDecimal(byteBuffer);
byte[] signature = new byte[SIGNATURE_LENGTH];
byteBuffer.get(signature);
return new UpdateAssetTransactionData(timestamp, txGroupId, reference, ownerPublicKey, assetId, newOwner,
newDescription, newData, fee, null, signature);
}
public static int getDataLength(TransactionData transactionData) throws TransformationException {
UpdateAssetTransactionData updateAssetTransactionData = (UpdateAssetTransactionData) transactionData;
int dataLength = getBaseLength(transactionData) + EXTRAS_LENGTH
+ Utf8.encodedLength(updateAssetTransactionData.getNewDescription())
+ Utf8.encodedLength(updateAssetTransactionData.getNewData());
return dataLength;
}
public static byte[] toBytes(TransactionData transactionData) throws TransformationException {
try {
UpdateAssetTransactionData updateAssetTransactionData = (UpdateAssetTransactionData) transactionData;
ByteArrayOutputStream bytes = new ByteArrayOutputStream();
transformCommonBytes(transactionData, bytes);
bytes.write(Longs.toByteArray(updateAssetTransactionData.getAssetId()));
Serialization.serializeAddress(bytes, updateAssetTransactionData.getNewOwner());
Serialization.serializeSizedString(bytes, updateAssetTransactionData.getNewDescription());
Serialization.serializeSizedString(bytes, updateAssetTransactionData.getNewData());
Serialization.serializeBigDecimal(bytes, updateAssetTransactionData.getFee());
if (updateAssetTransactionData.getSignature() != null)
bytes.write(updateAssetTransactionData.getSignature());
return bytes.toByteArray();
} catch (IOException | ClassCastException e) {
throw new TransformationException(e);
}
}
}

View File

@ -605,9 +605,10 @@ public class TransactionTests extends Common {
boolean isDivisible = true; boolean isDivisible = true;
BigDecimal fee = BigDecimal.ONE; BigDecimal fee = BigDecimal.ONE;
long timestamp = parentBlockData.getTimestamp() + 1_000; long timestamp = parentBlockData.getTimestamp() + 1_000;
String data = (timestamp >= BlockChain.getInstance().getQoraV2Timestamp()) ? "{}" : null;
IssueAssetTransactionData issueAssetTransactionData = new IssueAssetTransactionData(timestamp, Group.NO_GROUP, reference, sender.getPublicKey(), IssueAssetTransactionData issueAssetTransactionData = new IssueAssetTransactionData(timestamp, Group.NO_GROUP, reference, sender.getPublicKey(),
sender.getAddress(), assetName, description, quantity, isDivisible, fee); sender.getAddress(), assetName, description, quantity, isDivisible, data, fee);
Transaction issueAssetTransaction = new IssueAssetTransaction(repository, issueAssetTransactionData); Transaction issueAssetTransaction = new IssueAssetTransaction(repository, issueAssetTransactionData);
issueAssetTransaction.sign(sender); issueAssetTransaction.sign(sender);
@ -989,11 +990,11 @@ public class TransactionTests extends Common {
// Check trade has correct values // Check trade has correct values
BigDecimal expectedAmount = amount.divide(originalOrderData.getPrice()).setScale(8); BigDecimal expectedAmount = amount.divide(originalOrderData.getPrice()).setScale(8);
BigDecimal actualAmount = tradeData.getAmount(); BigDecimal actualAmount = tradeData.getTargetAmount();
assertTrue(expectedAmount.compareTo(actualAmount) == 0); assertTrue(expectedAmount.compareTo(actualAmount) == 0);
BigDecimal expectedPrice = amount; BigDecimal expectedPrice = amount;
BigDecimal actualPrice = tradeData.getPrice(); BigDecimal actualPrice = tradeData.getInitiatorAmount();
assertTrue(expectedPrice.compareTo(actualPrice) == 0); assertTrue(expectedPrice.compareTo(actualPrice) == 0);
// Check seller's "test asset" balance // Check seller's "test asset" balance