diff --git a/src/data/assets/OrderData.java b/src/data/assets/OrderData.java new file mode 100644 index 00000000..018638b7 --- /dev/null +++ b/src/data/assets/OrderData.java @@ -0,0 +1,74 @@ +package data.assets; + +import java.math.BigDecimal; + +public class OrderData implements Comparable { + + private byte[] orderId; + private byte[] creatorPublicKey; + private long haveAssetId; + private long wantAssetId; + private BigDecimal amount; + private BigDecimal fulfilled; + private BigDecimal price; + private long timestamp; + + public OrderData(byte[] orderId, byte[] creatorPublicKey, long haveAssetId, long wantAssetId, BigDecimal amount, BigDecimal fulfilled, BigDecimal price, + long timestamp) { + this.orderId = orderId; + this.creatorPublicKey = creatorPublicKey; + this.haveAssetId = haveAssetId; + this.wantAssetId = wantAssetId; + this.amount = amount; + this.fulfilled = fulfilled; + this.price = price; + this.timestamp = timestamp; + } + + public OrderData(byte[] orderId, byte[] creatorPublicKey, long haveAssetId, long wantAssetId, BigDecimal amount, BigDecimal price, long timestamp) { + this(orderId, creatorPublicKey, haveAssetId, wantAssetId, amount, BigDecimal.ZERO.setScale(8), price, timestamp); + } + + public byte[] getOrderId() { + return this.orderId; + } + + public byte[] getCreatorPublicKey() { + return this.creatorPublicKey; + } + + public long getHaveAssetId() { + return this.haveAssetId; + } + + public long getWantAssetId() { + return this.wantAssetId; + } + + public BigDecimal getAmount() { + return this.amount; + } + + public BigDecimal getFulfilled() { + return this.fulfilled; + } + + public void setFulfilled(BigDecimal fulfilled) { + this.fulfilled = fulfilled; + } + + public BigDecimal getPrice() { + return this.price; + } + + public long getTimestamp() { + return this.timestamp; + } + + @Override + public int compareTo(OrderData orderData) { + // Compare using prices + return this.price.compareTo(orderData.getPrice()); + } + +} diff --git a/src/data/transaction/TransferAssetTransactionData.java b/src/data/transaction/TransferAssetTransactionData.java new file mode 100644 index 00000000..15b993c7 --- /dev/null +++ b/src/data/transaction/TransferAssetTransactionData.java @@ -0,0 +1,50 @@ +package data.transaction; + +import java.math.BigDecimal; + +import qora.transaction.Transaction.TransactionType; + +public class TransferAssetTransactionData extends TransactionData { + + // Properties + private byte[] senderPublicKey; + private String recipient; + private BigDecimal amount; + private long assetId; + + // Constructors + + public TransferAssetTransactionData(byte[] senderPublicKey, String recipient, BigDecimal amount, long assetId, BigDecimal fee, long timestamp, + byte[] reference, byte[] signature) { + super(TransactionType.TRANSFER_ASSET, fee, senderPublicKey, timestamp, reference, signature); + + this.senderPublicKey = senderPublicKey; + this.recipient = recipient; + this.amount = amount; + this.assetId = assetId; + } + + public TransferAssetTransactionData(byte[] senderPublicKey, String recipient, BigDecimal amount, long assetId, BigDecimal fee, long timestamp, + byte[] reference) { + this(senderPublicKey, recipient, amount, assetId, fee, timestamp, reference, null); + } + + // Getters/setters + + public byte[] getSenderPublicKey() { + return this.senderPublicKey; + } + + public String getRecipient() { + return this.recipient; + } + + public BigDecimal getAmount() { + return this.amount; + } + + public long getAssetId() { + return this.assetId; + } + +} diff --git a/src/qora/assets/Asset.java b/src/qora/assets/Asset.java index aaf25600..e105545a 100644 --- a/src/qora/assets/Asset.java +++ b/src/qora/assets/Asset.java @@ -1,5 +1,8 @@ package qora.assets; +import data.assets.AssetData; +import repository.Repository; + public class Asset { /** @@ -7,4 +10,15 @@ public class Asset { */ public static final long QORA = 0L; + // Properties + private Repository repository; + private AssetData assetData; + + // Constructors + + public Asset(Repository repository, AssetData assetData) { + this.repository = repository; + this.assetData = assetData; + } + } diff --git a/src/qora/assets/Order.java b/src/qora/assets/Order.java index ae0a4f22..c9fb21eb 100644 --- a/src/qora/assets/Order.java +++ b/src/qora/assets/Order.java @@ -1,93 +1,39 @@ package qora.assets; import java.math.BigDecimal; -import java.math.BigInteger; import java.util.List; -import qora.account.Account; -import transform.TransformationException; +import data.assets.OrderData; +import repository.DataException; +import repository.Repository; -public class Order implements Comparable { +public class Order { // Properties - private BigInteger id; - private Account creator; - private long haveAssetId; - private long wantAssetId; - private BigDecimal amount; - private BigDecimal price; - private long timestamp; - - // Other properties - private BigDecimal fulfilled; + private Repository repository; + private OrderData orderData; // Constructors - public Order(BigInteger id, Account creator, long haveAssetId, long wantAssetId, BigDecimal amount, BigDecimal price, long timestamp) { - this.id = id; - this.creator = creator; - this.haveAssetId = haveAssetId; - this.wantAssetId = wantAssetId; - this.amount = amount; - this.price = price; - this.timestamp = timestamp; - - this.fulfilled = BigDecimal.ZERO.setScale(8); + public Order(Repository repository, OrderData orderData) { + this.repository = repository; + this.orderData = orderData; } - public Order(BigInteger id, Account creator, long haveAssetId, long wantAssetId, BigDecimal amount, BigDecimal fulfilled, BigDecimal price, - long timestamp) { - this(id, creator, haveAssetId, wantAssetId, amount, price, timestamp); + // Getters/Setters - this.fulfilled = fulfilled; - } - - // Getters/setters - - public BigInteger getId() { - return this.id; - } - - public Account getCreator() { - return this.creator; - } - - public long getHaveAssetId() { - return this.haveAssetId; - } - - public long getWantAssetId() { - return this.wantAssetId; - } - - public BigDecimal getAmount() { - return this.amount; - } - - public BigDecimal getPrice() { - return this.price; - } - - public long getTimestamp() { - return this.timestamp; - } - - public BigDecimal getFulfilled() { - return this.fulfilled; - } - - public void setFulfilled(BigDecimal fulfilled) { - this.fulfilled = fulfilled; + public OrderData getOrderData() { + return this.orderData; } // More information public BigDecimal getAmountLeft() { - return this.amount.subtract(this.fulfilled); + return this.orderData.getAmount().subtract(this.orderData.getFulfilled()); } public boolean isFulfilled() { - return this.fulfilled.compareTo(this.amount) == 0; + return this.orderData.getFulfilled().compareTo(this.orderData.getAmount()) == 0; } // TODO @@ -96,8 +42,6 @@ public class Order implements Comparable { // TODO // public boolean isConfirmed() {} - // Load/Save/Delete - // Navigation // XXX is this getInitiatedTrades() above? @@ -107,35 +51,14 @@ public class Order implements Comparable { return null; } - // Converters - - public static Order parse(byte[] data) throws TransformationException { - // TODO - return null; - } - - public byte[] toBytes() { - // TODO - - return null; - } - // Processing - // Other - - @Override - public int compareTo(Order order) { - // Compare using prices - return this.price.compareTo(order.getPrice()); + public void process() throws DataException { + // TODO } - public Order copy() { - try { - return parse(this.toBytes()); - } catch (TransformationException e) { - return null; - } + public void orphan() throws DataException { + // TODO } } diff --git a/src/qora/transaction/CreateOrderTransaction.java b/src/qora/transaction/CreateOrderTransaction.java index b8b82eac..96ec2fbd 100644 --- a/src/qora/transaction/CreateOrderTransaction.java +++ b/src/qora/transaction/CreateOrderTransaction.java @@ -1,8 +1,18 @@ package qora.transaction; -import data.transaction.TransactionData; +import java.math.BigDecimal; +import java.util.Arrays; +import data.assets.AssetData; +import data.assets.OrderData; +import data.transaction.CreateOrderTransactionData; +import data.transaction.TransactionData; +import qora.account.Account; +import qora.account.PublicKeyAccount; +import qora.assets.Asset; import qora.assets.Order; +import qora.block.Block; +import repository.AssetRepository; import repository.DataException; import repository.Repository; @@ -27,17 +37,117 @@ public class CreateOrderTransaction extends Transaction { // Processing public ValidationResult isValid() throws DataException { - // TODO + CreateOrderTransactionData createOrderTransactionData = (CreateOrderTransactionData) this.transactionData; + long haveAssetId = createOrderTransactionData.getHaveAssetId(); + long wantAssetId = createOrderTransactionData.getWantAssetId(); + + // Check have/want assets are not the same + if (haveAssetId == wantAssetId) + return ValidationResult.HAVE_EQUALS_WANT; + + // Check amount is positive + if (createOrderTransactionData.getAmount().compareTo(BigDecimal.ZERO) <= 0) + return ValidationResult.NEGATIVE_AMOUNT; + + // Check price is positive + if (createOrderTransactionData.getPrice().compareTo(BigDecimal.ZERO) <= 0) + return ValidationResult.NEGATIVE_PRICE; + + // Check fee is positive + if (createOrderTransactionData.getFee().compareTo(BigDecimal.ZERO) <= 0) + return ValidationResult.NEGATIVE_FEE; + + AssetRepository assetRepository = this.repository.getAssetRepository(); + + // Check "have" asset exists + AssetData haveAssetData = assetRepository.fromAssetId(haveAssetId); + if (haveAssetData == null) + return ValidationResult.ASSET_DOES_NOT_EXIST; + + // Check "want" asset exists + AssetData wantAssetData = assetRepository.fromAssetId(wantAssetId); + if (wantAssetData == null) + return ValidationResult.ASSET_DOES_NOT_EXIST; + + Account creator = new PublicKeyAccount(this.repository, createOrderTransactionData.getCreatorPublicKey()); + + // Check reference is correct + if (!Arrays.equals(creator.getLastReference(), createOrderTransactionData.getReference())) + return ValidationResult.INVALID_REFERENCE; + + // Check order creator has enough asset balance AFTER removing fee, in case asset is QORA + // If asset is QORA then we need to check amount + fee in one go + if (haveAssetId == Asset.QORA) { + // Check creator has enough funds for amount + fee in QORA + if (creator.getConfirmedBalance(Asset.QORA).compareTo(createOrderTransactionData.getAmount().add(createOrderTransactionData.getFee())) == -1) + return ValidationResult.NO_BALANCE; + } else { + // Check creator has enough funds for amount in whatever asset + if (creator.getConfirmedBalance(haveAssetId).compareTo(createOrderTransactionData.getAmount()) == -1) + return ValidationResult.NO_BALANCE; + + // Check creator has enough funds for fee in QORA + // NOTE: in Gen1 pre-POWFIX-RELEASE transactions didn't have this check + if (createOrderTransactionData.getTimestamp() >= Block.POWFIX_RELEASE_TIMESTAMP + && creator.getConfirmedBalance(Asset.QORA).compareTo(createOrderTransactionData.getFee()) == -1) + return ValidationResult.NO_BALANCE; + } + + // Check "have" amount is integer if "have" asset is not divisible + if (!haveAssetData.getIsDivisible() && createOrderTransactionData.getAmount().stripTrailingZeros().scale() > 0) + return ValidationResult.INVALID_AMOUNT; + + // Check total return from fulfilled order would be integer if "want" asset is not divisible + if (!wantAssetData.getIsDivisible() + && createOrderTransactionData.getAmount().multiply(createOrderTransactionData.getPrice()).stripTrailingZeros().scale() > 0) + return ValidationResult.INVALID_RETURN; return ValidationResult.OK; } public void process() throws DataException { - // TODO + CreateOrderTransactionData createOrderTransactionData = (CreateOrderTransactionData) this.transactionData; + Account creator = new PublicKeyAccount(this.repository, createOrderTransactionData.getCreatorPublicKey()); + + // Update creator's balance due to fee + creator.setConfirmedBalance(Asset.QORA, creator.getConfirmedBalance(Asset.QORA).subtract(createOrderTransactionData.getFee())); + + // Update creator's last reference + creator.setLastReference(createOrderTransactionData.getSignature()); + + // Save this transaction itself + this.repository.getTransactionRepository().save(createOrderTransactionData); + + // Order Id is transaction's signature + byte[] orderId = createOrderTransactionData.getSignature(); + + // Process the order itself + OrderData orderData = new OrderData(orderId, createOrderTransactionData.getCreatorPublicKey(), createOrderTransactionData.getHaveAssetId(), + createOrderTransactionData.getWantAssetId(), createOrderTransactionData.getAmount(), createOrderTransactionData.getPrice(), + createOrderTransactionData.getTimestamp()); + + new Order(this.repository, orderData).process(); } public void orphan() throws DataException { - // TODO + CreateOrderTransactionData createOrderTransactionData = (CreateOrderTransactionData) this.transactionData; + Account creator = new PublicKeyAccount(this.repository, createOrderTransactionData.getCreatorPublicKey()); + + // Update creator's balance due to fee + creator.setConfirmedBalance(Asset.QORA, creator.getConfirmedBalance(Asset.QORA).add(createOrderTransactionData.getFee())); + + // Update creator's last reference + creator.setLastReference(createOrderTransactionData.getReference()); + + // Order Id is transaction's signature + byte[] orderId = createOrderTransactionData.getSignature(); + + // Orphan the order itself + OrderData orderData = this.repository.getAssetRepository().fromOrderId(orderId); + new Order(this.repository, orderData).orphan(); + + // Delete this transaction + this.repository.getTransactionRepository().delete(createOrderTransactionData); } } diff --git a/src/qora/transaction/Transaction.java b/src/qora/transaction/Transaction.java index 43610610..67f69c1e 100644 --- a/src/qora/transaction/Transaction.java +++ b/src/qora/transaction/Transaction.java @@ -41,8 +41,8 @@ public abstract class Transaction { // Validation results public enum ValidationResult { - OK(1), INVALID_ADDRESS(2), NEGATIVE_AMOUNT(3), NEGATIVE_FEE(4), NO_BALANCE(5), INVALID_REFERENCE(6), INVALID_NAME_LENGTH(7), INVALID_DESCRIPTION_LENGTH( - 18), INVALID_DATA_LENGTH(27), INVALID_QUANTITY(28), ASSET_DOES_NOT_EXIST(29), ASSET_ALREADY_EXISTS(43), NOT_YET_RELEASED(1000); + OK(1), INVALID_ADDRESS(2), NEGATIVE_AMOUNT(3), NEGATIVE_FEE(4), NO_BALANCE(5), INVALID_REFERENCE(6), INVALID_NAME_LENGTH(7), INVALID_AMOUNT(15), INVALID_DESCRIPTION_LENGTH( + 18), INVALID_DATA_LENGTH(27), INVALID_QUANTITY(28), ASSET_DOES_NOT_EXIST(29), INVALID_RETURN(30), HAVE_EQUALS_WANT(31), NEGATIVE_PRICE(35), ASSET_ALREADY_EXISTS(43), NOT_YET_RELEASED(1000); public final int value; diff --git a/src/qora/transaction/TransferAssetTransaction.java b/src/qora/transaction/TransferAssetTransaction.java new file mode 100644 index 00000000..101c8407 --- /dev/null +++ b/src/qora/transaction/TransferAssetTransaction.java @@ -0,0 +1,142 @@ +package qora.transaction; + +import java.math.BigDecimal; +import java.util.Arrays; + +import data.assets.AssetData; +import data.transaction.TransactionData; +import data.transaction.TransferAssetTransactionData; +import utils.NTP; +import qora.account.Account; +import qora.account.PublicKeyAccount; +import qora.assets.Asset; +import qora.block.Block; +import qora.crypto.Crypto; +import repository.AssetRepository; +import repository.DataException; +import repository.Repository; + +public class TransferAssetTransaction extends Transaction { + + // Constructors + + public TransferAssetTransaction(Repository repository, TransactionData transactionData) { + super(repository, transactionData); + } + + // Processing + + @Override + public ValidationResult isValid() throws DataException { + // Lowest cost checks first + + TransferAssetTransactionData transferAssetTransactionData = (TransferAssetTransactionData) this.transactionData; + + // Are IssueAssetTransactions even allowed at this point? + if (NTP.getTime() < Block.ASSETS_RELEASE_TIMESTAMP) + return ValidationResult.NOT_YET_RELEASED; + + // Check recipient address is valid + if (!Crypto.isValidAddress(transferAssetTransactionData.getRecipient())) + return ValidationResult.INVALID_ADDRESS; + + // Check amount is positive + if (transferAssetTransactionData.getAmount().compareTo(BigDecimal.ZERO) <= 0) + return ValidationResult.NEGATIVE_FEE; + + // Check fee is positive + if (transferAssetTransactionData.getFee().compareTo(BigDecimal.ZERO) <= 0) + return ValidationResult.NEGATIVE_FEE; + + // Check reference is correct + PublicKeyAccount sender = new PublicKeyAccount(this.repository, transferAssetTransactionData.getSenderPublicKey()); + + if (!Arrays.equals(sender.getLastReference(), transferAssetTransactionData.getReference())) + return ValidationResult.INVALID_REFERENCE; + + // Check sender has enough asset balance AFTER removing fee, in case asset is QORA + long assetId = transferAssetTransactionData.getAssetId(); + // If asset is QORA then we need to check amount + fee in one go + if (assetId == Asset.QORA) { + // Check sender has enough funds for amount + fee in QORA + if (sender.getConfirmedBalance(Asset.QORA).compareTo(transferAssetTransactionData.getAmount().add(transferAssetTransactionData.getFee())) == -1) + return ValidationResult.NO_BALANCE; + } else { + // Check sender has enough funds for amount in whatever asset + if (sender.getConfirmedBalance(assetId).compareTo(transferAssetTransactionData.getAmount()) == -1) + return ValidationResult.NO_BALANCE; + + // Check sender has enough funds for fee in QORA + // NOTE: in Gen1 pre-POWFIX-RELEASE transactions didn't have this check + if (transferAssetTransactionData.getTimestamp() >= Block.POWFIX_RELEASE_TIMESTAMP + && sender.getConfirmedBalance(Asset.QORA).compareTo(transferAssetTransactionData.getFee()) == -1) + return ValidationResult.NO_BALANCE; + } + + // Check asset amount is integer if asset is not divisible + AssetRepository assetRepository = this.repository.getAssetRepository(); + AssetData assetData = assetRepository.fromAssetId(assetId); + if (!assetData.getIsDivisible() && transferAssetTransactionData.getAmount().stripTrailingZeros().scale() > 0) + return ValidationResult.INVALID_AMOUNT; + + return ValidationResult.OK; + } + + // PROCESS/ORPHAN + + @Override + public void process() throws DataException { + TransferAssetTransactionData transferAssetTransactionData = (TransferAssetTransactionData) this.transactionData; + long assetId = transferAssetTransactionData.getAssetId(); + + // Save this transaction itself + this.repository.getTransactionRepository().save(this.transactionData); + + // Update sender's balance due to amount + Account sender = new PublicKeyAccount(this.repository, transferAssetTransactionData.getSenderPublicKey()); + sender.setConfirmedBalance(assetId, sender.getConfirmedBalance(assetId).subtract(transferAssetTransactionData.getAmount())); + // Update sender's balance due to fee + sender.setConfirmedBalance(Asset.QORA, sender.getConfirmedBalance(Asset.QORA).subtract(transferAssetTransactionData.getFee())); + + // Update recipient's balance + Account recipient = new Account(this.repository, transferAssetTransactionData.getRecipient()); + recipient.setConfirmedBalance(assetId, recipient.getConfirmedBalance(assetId).add(transferAssetTransactionData.getAmount())); + + // Update sender's reference + sender.setLastReference(transferAssetTransactionData.getSignature()); + + // For QORA amounts only: if recipient has no reference yet, then this is their starting reference + if (assetId == Asset.QORA && recipient.getLastReference() == null) + recipient.setLastReference(transferAssetTransactionData.getSignature()); + } + + @Override + public void orphan() throws DataException { + TransferAssetTransactionData transferAssetTransactionData = (TransferAssetTransactionData) this.transactionData; + long assetId = transferAssetTransactionData.getAssetId(); + + // Delete this transaction itself + this.repository.getTransactionRepository().delete(this.transactionData); + + // Update sender's balance due to amount + Account sender = new PublicKeyAccount(this.repository, transferAssetTransactionData.getSenderPublicKey()); + sender.setConfirmedBalance(assetId, sender.getConfirmedBalance(assetId).add(transferAssetTransactionData.getAmount())); + // Update sender's balance due to fee + sender.setConfirmedBalance(Asset.QORA, sender.getConfirmedBalance(Asset.QORA).add(transferAssetTransactionData.getFee())); + + // Update recipient's balance + Account recipient = new Account(this.repository, transferAssetTransactionData.getRecipient()); + recipient.setConfirmedBalance(assetId, recipient.getConfirmedBalance(assetId).subtract(transferAssetTransactionData.getAmount())); + + // Update sender's reference + sender.setLastReference(transferAssetTransactionData.getReference()); + + /* + * For QORA amounts only: If recipient's last reference is this transaction's signature, then they can't have made any transactions of their own (which + * would have changed their last reference) thus this is their first reference so remove it. + */ + if (assetId == Asset.QORA && Arrays.equals(recipient.getLastReference(), transferAssetTransactionData.getSignature())) + recipient.setLastReference(null); + } + +} diff --git a/src/repository/AssetRepository.java b/src/repository/AssetRepository.java index 7b2ff7e6..94a7a9b7 100644 --- a/src/repository/AssetRepository.java +++ b/src/repository/AssetRepository.java @@ -1,9 +1,12 @@ package repository; import data.assets.AssetData; +import data.assets.OrderData; public interface AssetRepository { + // Assets + public AssetData fromAssetId(long assetId) throws DataException; public boolean assetExists(long assetId) throws DataException; @@ -14,4 +17,8 @@ public interface AssetRepository { public void delete(long assetId) throws DataException; + // Orders + + public OrderData fromOrderId(byte[] orderId) throws DataException; + } diff --git a/src/repository/hsqldb/HSQLDBAssetRepository.java b/src/repository/hsqldb/HSQLDBAssetRepository.java index d2e69296..8212bcec 100644 --- a/src/repository/hsqldb/HSQLDBAssetRepository.java +++ b/src/repository/hsqldb/HSQLDBAssetRepository.java @@ -1,9 +1,11 @@ package repository.hsqldb; +import java.math.BigDecimal; import java.sql.ResultSet; import java.sql.SQLException; import data.assets.AssetData; +import data.assets.OrderData; import repository.AssetRepository; import repository.DataException; @@ -15,6 +17,8 @@ public class HSQLDBAssetRepository implements AssetRepository { this.repository = repository; } + // Assets + public AssetData fromAssetId(long assetId) throws DataException { try { ResultSet resultSet = this.repository @@ -75,4 +79,27 @@ public class HSQLDBAssetRepository implements AssetRepository { } } + // Orders + + public OrderData fromOrderId(byte[] orderId) throws DataException { + try { + ResultSet resultSet = this.repository.checkedExecute( + "SELECT creator, have_asset_id, want_asset_id, amount, fulfilled, price, timestamp FROM AssetOrders WHERE asset_order_id = ?", orderId); + if (resultSet == null) + return null; + + byte[] creatorPublicKey = this.repository.getResultSetBytes(resultSet.getBinaryStream(1)); + long haveAssetId = resultSet.getLong(2); + long wantAssetId = resultSet.getLong(3); + BigDecimal amount = resultSet.getBigDecimal(4); + BigDecimal fulfilled = resultSet.getBigDecimal(5); + BigDecimal price = resultSet.getBigDecimal(6); + long timestamp = resultSet.getTimestamp(7).getTime(); + + return new OrderData(orderId, creatorPublicKey, haveAssetId, wantAssetId, amount, fulfilled, price, timestamp); + } catch (SQLException e) { + throw new DataException("Unable to fetch order from repository", e); + } + } + } diff --git a/src/repository/hsqldb/HSQLDBDatabaseUpdates.java b/src/repository/hsqldb/HSQLDBDatabaseUpdates.java index a25030f2..8ce732eb 100644 --- a/src/repository/hsqldb/HSQLDBDatabaseUpdates.java +++ b/src/repository/hsqldb/HSQLDBDatabaseUpdates.java @@ -90,7 +90,7 @@ public class HSQLDBDatabaseUpdates { stmt.execute("CREATE TYPE DataHash AS VARCHAR(100)"); stmt.execute("CREATE TYPE AssetID AS BIGINT"); stmt.execute("CREATE TYPE AssetName AS VARCHAR(400) COLLATE SQL_TEXT_UCC"); - stmt.execute("CREATE TYPE AssetOrderID AS VARCHAR(100)"); + stmt.execute("CREATE TYPE AssetOrderID AS VARBINARY(64)"); stmt.execute("CREATE TYPE ATName AS VARCHAR(200) COLLATE SQL_TEXT_UCC"); stmt.execute("CREATE TYPE ATType AS VARCHAR(200) COLLATE SQL_TEXT_UCC"); break; @@ -227,7 +227,7 @@ public class HSQLDBDatabaseUpdates { case 15: // Transfer Asset Transactions stmt.execute("CREATE TABLE TransferAssetTransactions (signature Signature, sender QoraPublicKey NOT NULL, recipient QoraAddress NOT NULL, " - + "asset_id AssetID NOT NULL, amount QoraAmount NOT NULL, " + + "asset_id AssetID NOT NULL, amount QoraAmount NOT NULL," + "PRIMARY KEY (signature), FOREIGN KEY (signature) REFERENCES Transactions (signature) ON DELETE CASCADE)"); break; @@ -241,7 +241,7 @@ public class HSQLDBDatabaseUpdates { case 17: // Cancel Asset Order Transactions stmt.execute("CREATE TABLE CancelAssetOrderTransactions (signature Signature, creator QoraPublicKey NOT NULL, " - + "asset_order AssetOrderID NOT NULL, " + + "asset_order_id AssetOrderID NOT NULL, " + "PRIMARY KEY (signature), FOREIGN KEY (signature) REFERENCES Transactions (signature) ON DELETE CASCADE)"); break; @@ -282,6 +282,16 @@ public class HSQLDBDatabaseUpdates { "CREATE TABLE AccountBalances (account QoraAddress, asset_id AssetID, balance QoraAmount NOT NULL, PRIMARY KEY (account, asset_id))"); break; + case 23: + // Asset Orders + stmt.execute( + "CREATE TABLE AssetOrders (asset_order_id AssetOrderID, creator QoraPublicKey NOT NULL, have_asset_id AssetID NOT NULL, want_asset_id AssetID NOT NULL, " + + "amount QoraAmount NOT NULL, fulfilled QoraAmount NOT NULL, price QoraAmount NOT NULL, ordered TIMESTAMP NOT NULL, " + + "PRIMARY KEY (asset_order_id))"); + stmt.execute("CREATE INDEX AssetOrderHaveIndex on AssetOrders (have_asset_id)"); + stmt.execute("CREATE INDEX AssetOrderWantIndex on AssetOrders (want_asset_id)"); + break; + default: // nothing to do return false; diff --git a/src/repository/hsqldb/transaction/HSQLDBCreateOrderTransactionRepository.java b/src/repository/hsqldb/transaction/HSQLDBCreateOrderTransactionRepository.java index 0c31e94b..f30f3b7c 100644 --- a/src/repository/hsqldb/transaction/HSQLDBCreateOrderTransactionRepository.java +++ b/src/repository/hsqldb/transaction/HSQLDBCreateOrderTransactionRepository.java @@ -18,8 +18,8 @@ public class HSQLDBCreateOrderTransactionRepository extends HSQLDBTransactionRep TransactionData fromBase(byte[] signature, byte[] reference, byte[] creatorPublicKey, long timestamp, BigDecimal fee) throws DataException { try { - ResultSet rs = this.repository.checkedExecute("SELECT have_asset_id, amount, want_asset_id, price FROM CreateOrderTransactions WHERE signature = ?", - signature); + ResultSet rs = this.repository.checkedExecute( + "SELECT have_asset_id, amount, want_asset_id, price FROM CreateAssetOrderTransactions WHERE signature = ?", signature); if (rs == null) return null; @@ -41,7 +41,8 @@ public class HSQLDBCreateOrderTransactionRepository extends HSQLDBTransactionRep HSQLDBSaver saveHelper = new HSQLDBSaver("CreateAssetOrderTransactions"); saveHelper.bind("signature", createOrderTransactionData.getSignature()).bind("creator", createOrderTransactionData.getCreatorPublicKey()) - .bind("have_asset_id", createOrderTransactionData.getHaveAssetId()).bind("amount", createOrderTransactionData.getAmount()); + .bind("have_asset_id", createOrderTransactionData.getHaveAssetId()).bind("amount", createOrderTransactionData.getAmount()) + .bind("want_asset_id", createOrderTransactionData.getWantAssetId()).bind("price", createOrderTransactionData.getPrice()); try { saveHelper.execute(this.repository); diff --git a/src/transform/block/BlockTransformer.java b/src/transform/block/BlockTransformer.java index e93f6ea9..4f014a1e 100644 --- a/src/transform/block/BlockTransformer.java +++ b/src/transform/block/BlockTransformer.java @@ -237,7 +237,7 @@ public class BlockTransformer extends Transformer { List trades = order.getTrades(); // Filter out trades with timestamps that don't match order transaction's timestamp - trades.removeIf((Trade trade) -> trade.getTimestamp() != order.getTimestamp()); + trades.removeIf((Trade trade) -> trade.getTimestamp() != order.getOrderData().getTimestamp()); // Any trades left? if (!trades.isEmpty()) { diff --git a/src/transform/transaction/TransferAssetTransactionTransformer.java b/src/transform/transaction/TransferAssetTransactionTransformer.java new file mode 100644 index 00000000..e908ee70 --- /dev/null +++ b/src/transform/transaction/TransferAssetTransactionTransformer.java @@ -0,0 +1,107 @@ +package transform.transaction; + +import java.io.ByteArrayOutputStream; +import java.io.IOException; +import java.math.BigDecimal; +import java.nio.ByteBuffer; + +import org.json.simple.JSONObject; + +import com.google.common.hash.HashCode; +import com.google.common.primitives.Ints; +import com.google.common.primitives.Longs; + +import data.transaction.TransactionData; +import data.transaction.TransferAssetTransactionData; +import qora.account.PublicKeyAccount; +import transform.TransformationException; +import utils.Base58; +import utils.Serialization; + +public class TransferAssetTransactionTransformer extends TransactionTransformer { + + // Property lengths + private static final int SENDER_LENGTH = PUBLIC_KEY_LENGTH; + private static final int RECIPIENT_LENGTH = ADDRESS_LENGTH; + private static final int ASSET_ID_LENGTH = LONG_LENGTH; + private static final int AMOUNT_LENGTH = 12; + + private static final int TYPELESS_LENGTH = BASE_TYPELESS_LENGTH + SENDER_LENGTH + RECIPIENT_LENGTH + ASSET_ID_LENGTH + AMOUNT_LENGTH; + + static TransactionData fromByteBuffer(ByteBuffer byteBuffer) throws TransformationException { + if (byteBuffer.remaining() < TYPELESS_LENGTH) + throw new TransformationException("Byte data too short for TransferAssetTransaction"); + + long timestamp = byteBuffer.getLong(); + + byte[] reference = new byte[REFERENCE_LENGTH]; + byteBuffer.get(reference); + + byte[] senderPublicKey = Serialization.deserializePublicKey(byteBuffer); + String recipient = Serialization.deserializeRecipient(byteBuffer); + long assetId = byteBuffer.getLong(); + BigDecimal amount = Serialization.deserializeBigDecimal(byteBuffer, AMOUNT_LENGTH); + + BigDecimal fee = Serialization.deserializeBigDecimal(byteBuffer); + + byte[] signature = new byte[SIGNATURE_LENGTH]; + byteBuffer.get(signature); + + return new TransferAssetTransactionData(senderPublicKey, recipient, amount, assetId, fee, timestamp, reference, signature); + } + + public static int getDataLength(TransactionData transactionData) throws TransformationException { + return TYPE_LENGTH + TYPELESS_LENGTH; + } + + public static byte[] toBytes(TransactionData transactionData) throws TransformationException { + try { + TransferAssetTransactionData transferAssetTransactionData = (TransferAssetTransactionData) transactionData; + + ByteArrayOutputStream bytes = new ByteArrayOutputStream(); + + bytes.write(Ints.toByteArray(transferAssetTransactionData.getType().value)); + bytes.write(Longs.toByteArray(transferAssetTransactionData.getTimestamp())); + bytes.write(transferAssetTransactionData.getReference()); + + bytes.write(transferAssetTransactionData.getSenderPublicKey()); + bytes.write(Base58.decode(transferAssetTransactionData.getRecipient())); + bytes.write(Longs.toByteArray(transferAssetTransactionData.getAssetId())); + Serialization.serializeBigDecimal(bytes, transferAssetTransactionData.getAmount(), AMOUNT_LENGTH); + + Serialization.serializeBigDecimal(bytes, transferAssetTransactionData.getFee()); + bytes.write(transferAssetTransactionData.getSignature()); + + return bytes.toByteArray(); + } catch (IOException | ClassCastException e) { + throw new TransformationException(e); + } + } + + @SuppressWarnings("unchecked") + public static JSONObject toJSON(TransactionData transactionData) throws TransformationException { + JSONObject json = TransactionTransformer.getBaseJSON(transactionData); + + try { + TransferAssetTransactionData transferAssetTransactionData = (TransferAssetTransactionData) transactionData; + + byte[] senderPublicKey = transferAssetTransactionData.getSenderPublicKey(); + + json.put("sender", PublicKeyAccount.getAddress(senderPublicKey)); + json.put("senderPublicKey", HashCode.fromBytes(senderPublicKey).toString()); + json.put("recipient", transferAssetTransactionData.getRecipient()); + + // For gen1 support: + json.put("asset", transferAssetTransactionData.getAssetId()); + // Gen2 version: + json.put("assetId", transferAssetTransactionData.getAssetId()); + + json.put("amount", transferAssetTransactionData.getAmount().toPlainString()); + } catch (ClassCastException e) { + throw new TransformationException(e); + } + + return json; + } + +}