From d53777f4615d1b098fde47f47d55bbd2520f4a13 Mon Sep 17 00:00:00 2001 From: catbref Date: Wed, 13 Mar 2019 14:06:52 +0000 Subject: [PATCH] Added "data" field to assets, added UPDATE_ASSET tx + fixes --- .../org/qora/api/resource/AssetsResource.java | 50 ++++++ src/main/java/org/qora/asset/Asset.java | 70 +++++++- src/main/java/org/qora/asset/Trade.java | 16 +- .../java/org/qora/block/GenesisBlock.java | 3 +- .../java/org/qora/data/asset/AssetData.java | 34 +++- .../org/qora/data/asset/RecentTradeData.java | 16 +- .../java/org/qora/data/asset/TradeData.java | 18 +- .../IssueAssetTransactionData.java | 17 +- .../data/transaction/TransactionData.java | 3 +- .../UpdateAssetTransactionData.java | 92 ++++++++++ .../hsqldb/HSQLDBAssetRepository.java | 151 ++++++++++------ .../hsqldb/HSQLDBDatabaseUpdates.java | 20 +++ ...HSQLDBIssueAssetTransactionRepository.java | 10 +- ...SQLDBUpdateAssetTransactionRepository.java | 61 +++++++ .../transaction/IssueAssetTransaction.java | 23 ++- .../org/qora/transaction/Transaction.java | 4 +- .../transaction/UpdateAssetTransaction.java | 164 ++++++++++++++++++ .../IssueAssetTransactionTransformer.java | 51 ++++-- .../transaction/TransactionTransformer.java | 4 +- .../UpdateAssetTransactionTransformer.java | 115 ++++++++++++ .../java/org/qora/test/TransactionTests.java | 7 +- 21 files changed, 805 insertions(+), 124 deletions(-) create mode 100644 src/main/java/org/qora/data/transaction/UpdateAssetTransactionData.java create mode 100644 src/main/java/org/qora/repository/hsqldb/transaction/HSQLDBUpdateAssetTransactionRepository.java create mode 100644 src/main/java/org/qora/transaction/UpdateAssetTransaction.java create mode 100644 src/main/java/org/qora/transform/transaction/UpdateAssetTransactionTransformer.java diff --git a/src/main/java/org/qora/api/resource/AssetsResource.java b/src/main/java/org/qora/api/resource/AssetsResource.java index a6128d8e..20a78f2d 100644 --- a/src/main/java/org/qora/api/resource/AssetsResource.java +++ b/src/main/java/org/qora/api/resource/AssetsResource.java @@ -43,6 +43,7 @@ import org.qora.data.transaction.CancelAssetOrderTransactionData; import org.qora.data.transaction.CreateAssetOrderTransactionData; 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.Repository; 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.CreateAssetOrderTransactionTransformer; import org.qora.transform.transaction.IssueAssetTransactionTransformer; +import org.qora.transform.transaction.UpdateAssetTransactionTransformer; import org.qora.utils.Base58; @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); + } + } + } diff --git a/src/main/java/org/qora/asset/Asset.java b/src/main/java/org/qora/asset/Asset.java index c754e5fc..436d3532 100644 --- a/src/main/java/org/qora/asset/Asset.java +++ b/src/main/java/org/qora/asset/Asset.java @@ -2,6 +2,8 @@ package org.qora.asset; import org.qora.data.asset.AssetData; 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.Repository; @@ -12,6 +14,15 @@ public class Asset { */ 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 private Repository repository; private AssetData assetData; @@ -25,9 +36,12 @@ public class Asset { public Asset(Repository repository, IssueAssetTransactionData issueAssetTransactionData) { 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(), - issueAssetTransactionData.getDescription(), issueAssetTransactionData.getQuantity(), issueAssetTransactionData.getIsDivisible(), - issueAssetTransactionData.getReference()); + issueAssetTransactionData.getDescription(), issueAssetTransactionData.getQuantity(), + issueAssetTransactionData.getIsDivisible(), issueAssetTransactionData.getData(), + issueAssetTransactionData.getTxGroupId(), issueAssetTransactionData.getSignature()); } public Asset(Repository repository, long assetId) throws DataException { @@ -51,4 +65,56 @@ public class Asset { 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); + } + } diff --git a/src/main/java/org/qora/asset/Trade.java b/src/main/java/org/qora/asset/Trade.java index 1fe54873..444a8755 100644 --- a/src/main/java/org/qora/asset/Trade.java +++ b/src/main/java/org/qora/asset/Trade.java @@ -31,14 +31,14 @@ public class Trade { // Update corresponding Orders on both sides of trade 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)); // Set isClosed to true if isFulfilled now true initiatingOrder.setIsClosed(initiatingOrder.getIsFulfilled()); assetRepository.save(initiatingOrder); 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)); // Set isClosed to true if isFulfilled now true targetOrder.setIsClosed(targetOrder.getIsFulfilled()); @@ -47,11 +47,11 @@ public class Trade { // Actually transfer asset balances Account initiatingCreator = new PublicKeyAccount(this.repository, initiatingOrder.getCreatorPublicKey()); 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()); targetCreator.setConfirmedBalance(targetOrder.getWantAssetId(), - targetCreator.getConfirmedBalance(targetOrder.getWantAssetId()).add(tradeData.getPrice())); + targetCreator.getConfirmedBalance(targetOrder.getWantAssetId()).add(tradeData.getInitiatorAmount())); } public void orphan() throws DataException { @@ -59,14 +59,14 @@ public class Trade { // Revert corresponding Orders on both sides of trade 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)); // Set isClosed to false if isFulfilled now false initiatingOrder.setIsClosed(initiatingOrder.getIsFulfilled()); assetRepository.save(initiatingOrder); 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)); // Set isClosed to false if isFulfilled now false targetOrder.setIsClosed(targetOrder.getIsFulfilled()); @@ -75,11 +75,11 @@ public class Trade { // Reverse asset transfers Account initiatingCreator = new PublicKeyAccount(this.repository, initiatingOrder.getCreatorPublicKey()); 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()); targetCreator.setConfirmedBalance(targetOrder.getWantAssetId(), - targetCreator.getConfirmedBalance(targetOrder.getWantAssetId()).subtract(tradeData.getPrice())); + targetCreator.getConfirmedBalance(targetOrder.getWantAssetId()).subtract(tradeData.getInitiatorAmount())); // Remove trade from repository assetRepository.delete(tradeData); diff --git a/src/main/java/org/qora/block/GenesisBlock.java b/src/main/java/org/qora/block/GenesisBlock.java index 7b633298..81b45eaa 100644 --- a/src/main/java/org/qora/block/GenesisBlock.java +++ b/src/main/java/org/qora/block/GenesisBlock.java @@ -22,6 +22,7 @@ import org.qora.data.asset.AssetData; import org.qora.data.block.BlockData; import org.qora.data.transaction.IssueAssetTransactionData; import org.qora.data.transaction.TransactionData; +import org.qora.group.Group; import org.qora.repository.DataException; import org.qora.repository.Repository; import org.qora.transaction.Transaction; @@ -117,7 +118,7 @@ public class GenesisBlock extends Block { IssueAssetTransactionData issueAssetTransactionData = (IssueAssetTransactionData) transactionData; 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()); } diff --git a/src/main/java/org/qora/data/asset/AssetData.java b/src/main/java/org/qora/data/asset/AssetData.java index d0652d21..341d8243 100644 --- a/src/main/java/org/qora/data/asset/AssetData.java +++ b/src/main/java/org/qora/data/asset/AssetData.java @@ -17,6 +17,8 @@ public class AssetData { private String description; private long quantity; private boolean isDivisible; + private String data; + private int creationGroupId; // No need to expose this via API @XmlTransient @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. - 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.owner = owner; this.name = name; this.description = description; this.quantity = quantity; this.isDivisible = isDivisible; + this.data = data; + this.creationGroupId = creationGroupId; this.reference = reference; } // New asset with unassigned assetId - public AssetData(String owner, String name, String description, long quantity, boolean isDivisible, byte[] reference) { - this(null, owner, name, description, quantity, isDivisible, 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, data, creationGroupId, reference); } // Getters/Setters @@ -58,6 +62,10 @@ public class AssetData { return this.owner; } + public void setOwner(String owner) { + this.owner = owner; + } + public String getName() { return this.name; } @@ -66,6 +74,10 @@ public class AssetData { return this.description; } + public void setDescription(String description) { + this.description = description; + } + public long getQuantity() { return this.quantity; } @@ -74,8 +86,24 @@ public class AssetData { 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() { return this.reference; } + public void setReference(byte[] reference) { + this.reference = reference; + } + } diff --git a/src/main/java/org/qora/data/asset/RecentTradeData.java b/src/main/java/org/qora/data/asset/RecentTradeData.java index e1295c61..dbc49ce5 100644 --- a/src/main/java/org/qora/data/asset/RecentTradeData.java +++ b/src/main/java/org/qora/data/asset/RecentTradeData.java @@ -16,9 +16,9 @@ public class RecentTradeData { private long otherAssetId; - private BigDecimal amount; + private BigDecimal otherAmount; - private BigDecimal price; + private BigDecimal amount; @Schema( description = "when trade happened" @@ -31,11 +31,11 @@ public class 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.otherAssetId = otherAssetId; + this.otherAmount = otherAmount; this.amount = amount; - this.price = price; this.timestamp = timestamp; } @@ -49,12 +49,12 @@ public class RecentTradeData { return this.otherAssetId; } - public BigDecimal getAmount() { - return this.amount; + public BigDecimal getOtherAmount() { + return this.otherAmount; } - public BigDecimal getPrice() { - return this.price; + public BigDecimal getAmount() { + return this.amount; } public long getTimestamp() { diff --git a/src/main/java/org/qora/data/asset/TradeData.java b/src/main/java/org/qora/data/asset/TradeData.java index e4b6e88a..e4824690 100644 --- a/src/main/java/org/qora/data/asset/TradeData.java +++ b/src/main/java/org/qora/data/asset/TradeData.java @@ -23,11 +23,11 @@ public class TradeData { @Schema(name = "targetAmount", description = "amount traded from target order") @XmlElement(name = "targetAmount") - private BigDecimal amount; + private BigDecimal targetAmount; @Schema(name = "initiatorAmount", description = "amount traded from initiating order") @XmlElement(name = "initiatorAmount") - private BigDecimal price; + private BigDecimal initiatorAmount; @Schema(description = "when trade happened") private long timestamp; @@ -38,11 +38,11 @@ public class 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.target = target; - this.amount = amount; - this.price = price; + this.targetAmount = targetAmount; + this.initiatorAmount = initiatorAmount; this.timestamp = timestamp; } @@ -56,12 +56,12 @@ public class TradeData { return this.target; } - public BigDecimal getAmount() { - return this.amount; + public BigDecimal getTargetAmount() { + return this.targetAmount; } - public BigDecimal getPrice() { - return this.price; + public BigDecimal getInitiatorAmount() { + return this.initiatorAmount; } public long getTimestamp() { diff --git a/src/main/java/org/qora/data/transaction/IssueAssetTransactionData.java b/src/main/java/org/qora/data/transaction/IssueAssetTransactionData.java index 975bbc9e..ce05690a 100644 --- a/src/main/java/org/qora/data/transaction/IssueAssetTransactionData.java +++ b/src/main/java/org/qora/data/transaction/IssueAssetTransactionData.java @@ -37,6 +37,8 @@ public class IssueAssetTransactionData extends TransactionData { private long quantity; @Schema(description = "whether asset quantities can be fractional", example = "true") 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 @@ -58,7 +60,7 @@ public class IssueAssetTransactionData extends TransactionData { } 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); this.assetId = assetId; @@ -68,16 +70,17 @@ public class IssueAssetTransactionData extends TransactionData { this.description = description; this.quantity = quantity; this.isDivisible = isDivisible; + this.data = data; } public IssueAssetTransactionData(long timestamp, int txGroupId, byte[] reference, byte[] issuerPublicKey, String owner, String assetName, - String description, long quantity, boolean isDivisible, BigDecimal fee, byte[] signature) { - this(timestamp, txGroupId, reference, issuerPublicKey, null, owner, assetName, description, quantity, isDivisible, fee, signature); + String description, long quantity, boolean isDivisible, String data, BigDecimal fee, byte[] 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, - String description, long quantity, boolean isDivisible, BigDecimal fee) { - this(timestamp, txGroupId, reference, issuerPublicKey, null, owner, assetName, description, quantity, isDivisible, fee, null); + String description, long quantity, boolean isDivisible, String data, BigDecimal fee) { + this(timestamp, txGroupId, reference, issuerPublicKey, null, owner, assetName, description, quantity, isDivisible, data, fee, null); } // Getters/Setters @@ -118,4 +121,8 @@ public class IssueAssetTransactionData extends TransactionData { return this.isDivisible; } + public String getData() { + return this.data; + } + } diff --git a/src/main/java/org/qora/data/transaction/TransactionData.java b/src/main/java/org/qora/data/transaction/TransactionData.java index fdcd50e8..2c102440 100644 --- a/src/main/java/org/qora/data/transaction/TransactionData.java +++ b/src/main/java/org/qora/data/transaction/TransactionData.java @@ -36,7 +36,8 @@ import io.swagger.v3.oas.annotations.media.Schema.AccessMode; GroupBanTransactionData.class, CancelGroupBanTransactionData.class, GroupKickTransactionData.class, GroupInviteTransactionData.class, JoinGroupTransactionData.class, LeaveGroupTransactionData.class, - GroupApprovalTransactionData.class, SetGroupTransactionData.class + GroupApprovalTransactionData.class, SetGroupTransactionData.class, + UpdateAssetTransactionData.class }) //All properties to be converted to JSON via JAXB @XmlAccessorType(XmlAccessType.FIELD) diff --git a/src/main/java/org/qora/data/transaction/UpdateAssetTransactionData.java b/src/main/java/org/qora/data/transaction/UpdateAssetTransactionData.java new file mode 100644 index 00000000..3d839a86 --- /dev/null +++ b/src/main/java/org/qora/data/transaction/UpdateAssetTransactionData.java @@ -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; + } + +} diff --git a/src/main/java/org/qora/repository/hsqldb/HSQLDBAssetRepository.java b/src/main/java/org/qora/repository/hsqldb/HSQLDBAssetRepository.java index aced3d0a..1736be81 100644 --- a/src/main/java/org/qora/repository/hsqldb/HSQLDBAssetRepository.java +++ b/src/main/java/org/qora/repository/hsqldb/HSQLDBAssetRepository.java @@ -28,8 +28,9 @@ public class HSQLDBAssetRepository implements AssetRepository { @Override public AssetData fromAssetId(long assetId) throws DataException { - try (ResultSet resultSet = this.repository - .checkedExecute("SELECT owner, asset_name, description, quantity, is_divisible, reference FROM Assets WHERE asset_id = ?", assetId)) { + try (ResultSet resultSet = this.repository.checkedExecute( + "SELECT owner, asset_name, description, quantity, is_divisible, data, creation_group_id, reference FROM Assets WHERE asset_id = ?", + assetId)) { if (resultSet == null) return null; @@ -38,9 +39,12 @@ public class HSQLDBAssetRepository implements AssetRepository { String description = resultSet.getString(3); long quantity = resultSet.getLong(4); 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) { throw new DataException("Unable to fetch asset from repository", e); } @@ -48,8 +52,9 @@ public class HSQLDBAssetRepository implements AssetRepository { @Override public AssetData fromAssetName(String assetName) throws DataException { - try (ResultSet resultSet = this.repository - .checkedExecute("SELECT owner, asset_id, description, quantity, is_divisible, reference FROM Assets WHERE asset_name = ?", assetName)) { + try (ResultSet resultSet = this.repository.checkedExecute( + "SELECT owner, asset_id, description, quantity, is_divisible, data, creation_group_id, reference FROM Assets WHERE asset_name = ?", + assetName)) { if (resultSet == null) return null; @@ -58,9 +63,12 @@ public class HSQLDBAssetRepository implements AssetRepository { String description = resultSet.getString(3); long quantity = resultSet.getLong(4); 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) { throw new DataException("Unable to fetch asset from repository", e); } @@ -86,7 +94,7 @@ public class HSQLDBAssetRepository implements AssetRepository { @Override public List 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) sql += " DESC"; sql += HSQLDBRepository.limitOffsetSql(limit, offset); @@ -98,15 +106,18 @@ public class HSQLDBAssetRepository implements AssetRepository { return assets; do { - String owner = resultSet.getString(1); - long assetId = resultSet.getLong(2); - String description = resultSet.getString(3); - long quantity = resultSet.getLong(4); - boolean isDivisible = resultSet.getBoolean(5); - byte[] reference = resultSet.getBytes(6); - String assetName = resultSet.getString(7); + long assetId = resultSet.getLong(1); + String owner = resultSet.getString(2); + String assetName = resultSet.getString(3); + String description = resultSet.getString(4); + long quantity = resultSet.getLong(5); + boolean isDivisible = resultSet.getBoolean(6); + 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()); return assets; @@ -119,8 +130,10 @@ public class HSQLDBAssetRepository implements AssetRepository { public void save(AssetData assetData) throws DataException { HSQLDBSaver saveHelper = new HSQLDBSaver("Assets"); - saveHelper.bind("asset_id", assetData.getAssetId()).bind("owner", assetData.getOwner()).bind("asset_name", assetData.getName()) - .bind("description", assetData.getDescription()).bind("quantity", assetData.getQuantity()).bind("is_divisible", assetData.getIsDivisible()) + saveHelper.bind("asset_id", assetData.getAssetId()).bind("owner", assetData.getOwner()) + .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()); try { @@ -128,7 +141,8 @@ public class HSQLDBAssetRepository implements AssetRepository { if (assetData.getAssetId() == null) { // 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) 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 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) { throw new DataException("Unable to fetch asset order from repository", e); } } @Override - public List getOpenOrders(long haveAssetId, long wantAssetId, Integer limit, Integer offset, Boolean reverse) throws DataException { + public List 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 " + "WHERE have_asset_id = ? AND want_asset_id = ? AND is_closed = FALSE AND is_fulfilled = FALSE ORDER BY price"; if (reverse != null && reverse) @@ -202,8 +218,8 @@ public class HSQLDBAssetRepository implements AssetRepository { boolean isClosed = false; boolean isFulfilled = false; - OrderData order = new OrderData(orderId, creatorPublicKey, haveAssetId, wantAssetId, amount, fulfilled, price, timestamp, isClosed, - isFulfilled); + OrderData order = new OrderData(orderId, creatorPublicKey, haveAssetId, wantAssetId, amount, fulfilled, + price, timestamp, isClosed, isFulfilled); orders.add(order); } while (resultSet.next()); @@ -214,7 +230,8 @@ public class HSQLDBAssetRepository implements AssetRepository { } @Override - public List getAggregatedOpenOrders(long haveAssetId, long wantAssetId, Integer limit, Integer offset, Boolean reverse) throws DataException { + public List getAggregatedOpenOrders(long haveAssetId, long wantAssetId, Integer limit, Integer offset, + Boolean reverse) throws DataException { 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"; if (reverse != null && reverse) @@ -232,7 +249,8 @@ public class HSQLDBAssetRepository implements AssetRepository { BigDecimal totalUnfulfilled = resultSet.getBigDecimal(2); 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); } while (resultSet.next()); @@ -243,8 +261,8 @@ public class HSQLDBAssetRepository implements AssetRepository { } @Override - public List getAccountsOrders(byte[] publicKey, Boolean optIsClosed, Boolean optIsFulfilled, Integer limit, Integer offset, Boolean reverse) - throws DataException { + public List getAccountsOrders(byte[] publicKey, Boolean optIsClosed, Boolean optIsFulfilled, + 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 = ?"; if (optIsClosed != null) sql += " AND is_closed = " + (optIsClosed ? "TRUE" : "FALSE"); @@ -272,7 +290,8 @@ public class HSQLDBAssetRepository implements AssetRepository { boolean isClosed = resultSet.getBoolean(8); 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); } while (resultSet.next()); @@ -283,8 +302,8 @@ public class HSQLDBAssetRepository implements AssetRepository { } @Override - public List getAccountsOrders(byte[] publicKey, long haveAssetId, long wantAssetId, Boolean optIsClosed, Boolean optIsFulfilled, Integer limit, - Integer offset, Boolean reverse) throws DataException { + public List getAccountsOrders(byte[] publicKey, long haveAssetId, long wantAssetId, Boolean optIsClosed, + 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 = ?"; if (optIsClosed != null) sql += " AND is_closed = " + (optIsClosed ? "TRUE" : "FALSE"); @@ -310,7 +329,8 @@ public class HSQLDBAssetRepository implements AssetRepository { boolean isClosed = resultSet.getBoolean(6); 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); } while (resultSet.next()); @@ -325,8 +345,9 @@ public class HSQLDBAssetRepository implements AssetRepository { HSQLDBSaver saveHelper = new HSQLDBSaver("AssetOrders"); 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("fulfilled", orderData.getFulfilled()).bind("price", orderData.getPrice()).bind("ordered", new Timestamp(orderData.getTimestamp())) + .bind("have_asset_id", orderData.getHaveAssetId()).bind("want_asset_id", orderData.getWantAssetId()) + .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()); try { @@ -348,8 +369,9 @@ public class HSQLDBAssetRepository implements AssetRepository { // Trades @Override - public List getTrades(long haveAssetId, long wantAssetId, Integer limit, Integer offset, Boolean reverse) throws DataException { - 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 " + public List getTrades(long haveAssetId, long wantAssetId, Integer limit, Integer offset, Boolean reverse) + 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"; if (reverse != null && reverse) sql += " DESC"; @@ -364,11 +386,12 @@ public class HSQLDBAssetRepository implements AssetRepository { do { byte[] initiatingOrderId = resultSet.getBytes(1); byte[] targetOrderId = resultSet.getBytes(2); - BigDecimal amount = resultSet.getBigDecimal(3); - BigDecimal price = resultSet.getBigDecimal(4); + BigDecimal targetAmount = resultSet.getBigDecimal(3); + BigDecimal initiatorAmount = resultSet.getBigDecimal(4); 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); } while (resultSet.next()); @@ -379,19 +402,25 @@ public class HSQLDBAssetRepository implements AssetRepository { } @Override - public List getRecentTrades(List assetIds, List otherAssetIds, Integer limit, Integer offset, Boolean reverse) throws DataException { + public List getRecentTrades(List assetIds, List otherAssetIds, Integer limit, + Integer offset, Boolean reverse) throws DataException { // 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 if (!assetIds.isEmpty()) // 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()) { tradedAssetsSubquery += assetIds.isEmpty() ? " WHERE " : " AND "; // 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"; @@ -403,8 +432,9 @@ public class HSQLDBAssetRepository implements AssetRepository { + "ORDER BY traded DESC LIMIT 2"; // Put it all together - String sql = "SELECT have_asset_id, want_asset_id, RecentTrades.amount, RecentTrades.price, RecentTrades.traded " + "FROM (" + tradedAssetsSubquery - + ") AS TradedAssets " + ", LATERAL (" + recentTradesSubquery + ") AS RecentTrades (amount, price, traded) " + "ORDER BY have_asset_id"; + String sql = "SELECT have_asset_id, want_asset_id, RecentTrades.target_amount, RecentTrades.initiator_amount, RecentTrades.traded " + + "FROM (" + tradedAssetsSubquery + ") AS TradedAssets " + ", LATERAL (" + recentTradesSubquery + + ") AS RecentTrades (target_amount, initiator_amount, traded) " + "ORDER BY have_asset_id"; if (reverse != null && reverse) sql += " DESC"; @@ -425,11 +455,12 @@ public class HSQLDBAssetRepository implements AssetRepository { do { long haveAssetId = resultSet.getLong(1); long wantAssetId = resultSet.getLong(2); - BigDecimal amount = resultSet.getBigDecimal(3); - BigDecimal price = resultSet.getBigDecimal(4); + BigDecimal otherAmount = resultSet.getBigDecimal(3); + BigDecimal amount = resultSet.getBigDecimal(4); 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); } while (resultSet.next()); @@ -440,8 +471,9 @@ public class HSQLDBAssetRepository implements AssetRepository { } @Override - public List getOrdersTrades(byte[] orderId, Integer limit, Integer offset, Boolean reverse) throws DataException { - 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"; + public List getOrdersTrades(byte[] orderId, Integer limit, Integer offset, Boolean reverse) + 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) sql += " DESC"; sql += HSQLDBRepository.limitOffsetSql(limit, offset); @@ -455,11 +487,12 @@ public class HSQLDBAssetRepository implements AssetRepository { do { byte[] initiatingOrderId = resultSet.getBytes(1); byte[] targetOrderId = resultSet.getBytes(2); - BigDecimal amount = resultSet.getBigDecimal(3); - BigDecimal price = resultSet.getBigDecimal(4); + BigDecimal targetAmount = resultSet.getBigDecimal(3); + BigDecimal initiatorAmount = resultSet.getBigDecimal(4); 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); } while (resultSet.next()); @@ -473,8 +506,10 @@ public class HSQLDBAssetRepository implements AssetRepository { public void save(TradeData tradeData) throws DataException { HSQLDBSaver saveHelper = new HSQLDBSaver("AssetTrades"); - saveHelper.bind("initiating_order_id", tradeData.getInitiator()).bind("target_order_id", tradeData.getTarget()).bind("amount", tradeData.getAmount()) - .bind("price", tradeData.getPrice()).bind("traded", new Timestamp(tradeData.getTimestamp())); + saveHelper.bind("initiating_order_id", tradeData.getInitiator()).bind("target_order_id", tradeData.getTarget()) + .bind("target_amount", tradeData.getTargetAmount()) + .bind("initiator_amount", tradeData.getInitiatorAmount()) + .bind("traded", new Timestamp(tradeData.getTimestamp())); try { saveHelper.execute(this.repository); @@ -486,8 +521,10 @@ public class HSQLDBAssetRepository implements AssetRepository { @Override public void delete(TradeData tradeData) throws DataException { try { - this.repository.delete("AssetTrades", "initiating_order_id = ? AND target_order_id = ? AND amount = ? AND price = ?", tradeData.getInitiator(), - tradeData.getTarget(), tradeData.getAmount(), tradeData.getPrice()); + this.repository.delete("AssetTrades", + "initiating_order_id = ? AND target_order_id = ? AND target_amount = ? AND initiator_amount = ?", + tradeData.getInitiator(), tradeData.getTarget(), tradeData.getTargetAmount(), + tradeData.getInitiatorAmount()); } catch (SQLException e) { throw new DataException("Unable to delete asset trade from repository", e); } diff --git a/src/main/java/org/qora/repository/hsqldb/HSQLDBDatabaseUpdates.java b/src/main/java/org/qora/repository/hsqldb/HSQLDBDatabaseUpdates.java index 83c342fe..eb79f296 100644 --- a/src/main/java/org/qora/repository/hsqldb/HSQLDBDatabaseUpdates.java +++ b/src/main/java/org/qora/repository/hsqldb/HSQLDBDatabaseUpdates.java @@ -592,6 +592,26 @@ public class HSQLDBDatabaseUpdates { stmt.execute("UPDATE AssetOrders SET is_closed = TRUE WHERE is_fulfilled = TRUE"); 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: // nothing to do return false; diff --git a/src/main/java/org/qora/repository/hsqldb/transaction/HSQLDBIssueAssetTransactionRepository.java b/src/main/java/org/qora/repository/hsqldb/transaction/HSQLDBIssueAssetTransactionRepository.java index cd5ffb8d..5a1365cd 100644 --- a/src/main/java/org/qora/repository/hsqldb/transaction/HSQLDBIssueAssetTransactionRepository.java +++ b/src/main/java/org/qora/repository/hsqldb/transaction/HSQLDBIssueAssetTransactionRepository.java @@ -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 { 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) return null; @@ -27,14 +27,15 @@ public class HSQLDBIssueAssetTransactionRepository extends HSQLDBTransactionRepo String description = resultSet.getString(3); long quantity = resultSet.getLong(4); boolean isDivisible = resultSet.getBoolean(5); + String data = resultSet.getString(6); // Special null-checking for asset ID - Long assetId = resultSet.getLong(6); + Long assetId = resultSet.getLong(7); if (resultSet.wasNull()) assetId = null; return new IssueAssetTransactionData(timestamp, txGroupId, reference, creatorPublicKey, assetId, owner, assetName, description, quantity, isDivisible, - fee, signature); + data, fee, signature); } catch (SQLException 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()) .bind("owner", issueAssetTransactionData.getOwner()).bind("asset_name", issueAssetTransactionData.getAssetName()) .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 { saveHelper.execute(this.repository); diff --git a/src/main/java/org/qora/repository/hsqldb/transaction/HSQLDBUpdateAssetTransactionRepository.java b/src/main/java/org/qora/repository/hsqldb/transaction/HSQLDBUpdateAssetTransactionRepository.java new file mode 100644 index 00000000..5e272a7e --- /dev/null +++ b/src/main/java/org/qora/repository/hsqldb/transaction/HSQLDBUpdateAssetTransactionRepository.java @@ -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); + } + } + +} diff --git a/src/main/java/org/qora/transaction/IssueAssetTransaction.java b/src/main/java/org/qora/transaction/IssueAssetTransaction.java index 25870a1f..345b189b 100644 --- a/src/main/java/org/qora/transaction/IssueAssetTransaction.java +++ b/src/main/java/org/qora/transaction/IssueAssetTransaction.java @@ -22,10 +22,6 @@ public class IssueAssetTransaction extends Transaction { // Properties private IssueAssetTransactionData issueAssetTransactionData; - // Other useful constants - public static final int MAX_NAME_SIZE = 400; - public static final int MAX_DESCRIPTION_SIZE = 4000; - // Constructors public IssueAssetTransaction(Repository repository, TransactionData transactionData) { @@ -86,22 +82,35 @@ public class IssueAssetTransaction extends Transaction { if (this.issueAssetTransactionData.getTimestamp() < BlockChain.getInstance().getAssetsReleaseTimestamp()) 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 if (!Crypto.isValidAddress(issueAssetTransactionData.getOwner())) return ValidationResult.INVALID_ADDRESS; // Check name size bounds 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; // Check description size bounds 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; // 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) return ValidationResult.INVALID_QUANTITY; diff --git a/src/main/java/org/qora/transaction/Transaction.java b/src/main/java/org/qora/transaction/Transaction.java index 622a6950..8910ccb4 100644 --- a/src/main/java/org/qora/transaction/Transaction.java +++ b/src/main/java/org/qora/transaction/Transaction.java @@ -71,7 +71,8 @@ public abstract class Transaction { JOIN_GROUP(31, false), LEAVE_GROUP(32, false), GROUP_APPROVAL(33, false), - SET_GROUP(34, false); + SET_GROUP(34, false), + UPDATE_ASSET(35, true); public final int value; public final boolean needsApproval; @@ -187,6 +188,7 @@ public abstract class Transaction { INVALID_TX_GROUP_ID(67), TX_GROUP_ID_MISMATCH(68), MULTIPLE_NAMES_FORBIDDEN(69), + INVALID_ASSET_OWNER(70), NOT_YET_RELEASED(1000); public final int value; diff --git a/src/main/java/org/qora/transaction/UpdateAssetTransaction.java b/src/main/java/org/qora/transaction/UpdateAssetTransaction.java new file mode 100644 index 00000000..59974cc1 --- /dev/null +++ b/src/main/java/org/qora/transaction/UpdateAssetTransaction.java @@ -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 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()); + } + +} diff --git a/src/main/java/org/qora/transform/transaction/IssueAssetTransactionTransformer.java b/src/main/java/org/qora/transform/transaction/IssueAssetTransactionTransformer.java index 49644147..6c5b0d86 100644 --- a/src/main/java/org/qora/transform/transaction/IssueAssetTransactionTransformer.java +++ b/src/main/java/org/qora/transform/transaction/IssueAssetTransactionTransformer.java @@ -6,10 +6,10 @@ import java.math.BigDecimal; import java.nio.ByteBuffer; import java.util.Arrays; +import org.qora.asset.Asset; import org.qora.block.BlockChain; import org.qora.data.transaction.IssueAssetTransactionData; import org.qora.data.transaction.TransactionData; -import org.qora.transaction.IssueAssetTransaction; import org.qora.transaction.Transaction.TransactionType; import org.qora.transform.TransformationException; 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 IS_DIVISIBLE_LENGTH = BOOLEAN_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; @@ -45,6 +47,8 @@ public class IssueAssetTransactionTransformer extends TransactionTransformer { layout.add("asset description", TransformationType.STRING); layout.add("asset quantity", TransformationType.LONG); 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("signature", TransformationType.SIGNATURE); } @@ -63,16 +67,22 @@ public class IssueAssetTransactionTransformer extends TransactionTransformer { 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(); 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]; - // 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()) byteBuffer.get(assetReference); @@ -81,17 +91,23 @@ public class IssueAssetTransactionTransformer extends TransactionTransformer { byte[] signature = new byte[SIGNATURE_LENGTH]; byteBuffer.get(signature); - return new IssueAssetTransactionData(timestamp, txGroupId, reference, issuerPublicKey, owner, assetName, description, quantity, isDivisible, fee, - signature); + return new IssueAssetTransactionData(timestamp, txGroupId, reference, issuerPublicKey, owner, assetName, + description, quantity, isDivisible, data, fee, signature); } public static int getDataLength(TransactionData transactionData) throws TransformationException { 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()); - // 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()) dataLength += ASSET_REFERENCE_LENGTH; @@ -115,7 +131,13 @@ public class IssueAssetTransactionTransformer extends TransactionTransformer { bytes.write(Longs.toByteArray(issueAssetTransactionData.getQuantity())); 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()) { byte[] assetReference = issueAssetTransactionData.getSignature(); 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 * @return byte[] @@ -151,7 +174,11 @@ public class IssueAssetTransactionTransformer extends TransactionTransformer { // Special v1 version // 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; Arrays.fill(bytes, start, end, (byte) 0); diff --git a/src/main/java/org/qora/transform/transaction/TransactionTransformer.java b/src/main/java/org/qora/transform/transaction/TransactionTransformer.java index d79d1a67..d8147a4e 100644 --- a/src/main/java/org/qora/transform/transaction/TransactionTransformer.java +++ b/src/main/java/org/qora/transform/transaction/TransactionTransformer.java @@ -208,11 +208,9 @@ public abstract class TransactionTransformer extends Transformer { try { return (TransactionData) method.invoke(null, byteBuffer); - } catch (BufferUnderflowException e) { - throw new TransformationException("Byte data too short for transaction type [" + type.value + "]"); } catch (InvocationTargetException e) { 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) throw (TransformationException) e.getCause(); diff --git a/src/main/java/org/qora/transform/transaction/UpdateAssetTransactionTransformer.java b/src/main/java/org/qora/transform/transaction/UpdateAssetTransactionTransformer.java new file mode 100644 index 00000000..e7e61dc9 --- /dev/null +++ b/src/main/java/org/qora/transform/transaction/UpdateAssetTransactionTransformer.java @@ -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); + } + } + +} diff --git a/src/test/java/org/qora/test/TransactionTests.java b/src/test/java/org/qora/test/TransactionTests.java index 54a1c7dd..495e295b 100644 --- a/src/test/java/org/qora/test/TransactionTests.java +++ b/src/test/java/org/qora/test/TransactionTests.java @@ -605,9 +605,10 @@ public class TransactionTests extends Common { boolean isDivisible = true; BigDecimal fee = BigDecimal.ONE; long timestamp = parentBlockData.getTimestamp() + 1_000; + String data = (timestamp >= BlockChain.getInstance().getQoraV2Timestamp()) ? "{}" : null; 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); issueAssetTransaction.sign(sender); @@ -989,11 +990,11 @@ public class TransactionTests extends Common { // Check trade has correct values BigDecimal expectedAmount = amount.divide(originalOrderData.getPrice()).setScale(8); - BigDecimal actualAmount = tradeData.getAmount(); + BigDecimal actualAmount = tradeData.getTargetAmount(); assertTrue(expectedAmount.compareTo(actualAmount) == 0); BigDecimal expectedPrice = amount; - BigDecimal actualPrice = tradeData.getPrice(); + BigDecimal actualPrice = tradeData.getInitiatorAmount(); assertTrue(expectedPrice.compareTo(actualPrice) == 0); // Check seller's "test asset" balance