forked from Qortal/qortal
Added asset order matching support.
Added isFulfilled property to asset orders so completed orders can be filtered out. Fixed migrate app by adding dummy name_reference values to UpdateNameTransactions and BuyNameTransactions INSERTS. Fixed migrate app to use "poll_name" instead of "poll" for column name. Ditto "option_name" instead of "option". Fixed some other incorrect column names in HSQLDBAssetRepository. More unit tests but probably need yet more to cover complicated asset order matching with various divisibility settings. Maybe fuzzing would help here somehow?
This commit is contained in:
parent
104be89b4e
commit
b401adcc55
@ -13,9 +13,10 @@ public class OrderData implements Comparable<OrderData> {
|
||||
private BigDecimal price;
|
||||
private long timestamp;
|
||||
private boolean isClosed;
|
||||
private boolean isFulfilled;
|
||||
|
||||
public OrderData(byte[] orderId, byte[] creatorPublicKey, long haveAssetId, long wantAssetId, BigDecimal amount, BigDecimal fulfilled, BigDecimal price,
|
||||
long timestamp, boolean isClosed) {
|
||||
long timestamp, boolean isClosed, boolean isFulfilled) {
|
||||
this.orderId = orderId;
|
||||
this.creatorPublicKey = creatorPublicKey;
|
||||
this.haveAssetId = haveAssetId;
|
||||
@ -25,10 +26,11 @@ public class OrderData implements Comparable<OrderData> {
|
||||
this.price = price;
|
||||
this.timestamp = timestamp;
|
||||
this.isClosed = isClosed;
|
||||
this.isFulfilled = isFulfilled;
|
||||
}
|
||||
|
||||
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, false);
|
||||
this(orderId, creatorPublicKey, haveAssetId, wantAssetId, amount, BigDecimal.ZERO.setScale(8), price, timestamp, false, false);
|
||||
}
|
||||
|
||||
public byte[] getOrderId() {
|
||||
@ -75,6 +77,14 @@ public class OrderData implements Comparable<OrderData> {
|
||||
this.isClosed = isClosed;
|
||||
}
|
||||
|
||||
public boolean getIsFulfilled() {
|
||||
return this.isFulfilled;
|
||||
}
|
||||
|
||||
public void setIsFulfilled(boolean isFulfilled) {
|
||||
this.isFulfilled = isFulfilled;
|
||||
}
|
||||
|
||||
@Override
|
||||
public int compareTo(OrderData orderData) {
|
||||
// Compare using prices
|
||||
|
46
src/data/assets/TradeData.java
Normal file
46
src/data/assets/TradeData.java
Normal file
@ -0,0 +1,46 @@
|
||||
package data.assets;
|
||||
|
||||
import java.math.BigDecimal;
|
||||
|
||||
public class TradeData {
|
||||
|
||||
// Properties
|
||||
private byte[] initiator;
|
||||
private byte[] target;
|
||||
private BigDecimal amount;
|
||||
private BigDecimal price;
|
||||
private long timestamp;
|
||||
|
||||
// Constructors
|
||||
|
||||
public TradeData(byte[] initiator, byte[] target, BigDecimal amount, BigDecimal price, long timestamp) {
|
||||
this.initiator = initiator;
|
||||
this.target = target;
|
||||
this.amount = amount;
|
||||
this.price = price;
|
||||
this.timestamp = timestamp;
|
||||
}
|
||||
|
||||
// Getters/setters
|
||||
|
||||
public byte[] getInitiator() {
|
||||
return this.initiator;
|
||||
}
|
||||
|
||||
public byte[] getTarget() {
|
||||
return this.target;
|
||||
}
|
||||
|
||||
public BigDecimal getAmount() {
|
||||
return this.amount;
|
||||
}
|
||||
|
||||
public BigDecimal getPrice() {
|
||||
return this.price;
|
||||
}
|
||||
|
||||
public long getTimestamp() {
|
||||
return this.timestamp;
|
||||
}
|
||||
|
||||
}
|
@ -133,19 +133,19 @@ public class migrate {
|
||||
PreparedStatement registerNamePStmt = c
|
||||
.prepareStatement("INSERT INTO RegisterNameTransactions " + formatWithPlaceholders("signature", "registrant", "name", "owner", "data"));
|
||||
PreparedStatement updateNamePStmt = c
|
||||
.prepareStatement("INSERT INTO UpdateNameTransactions " + formatWithPlaceholders("signature", "owner", "name", "new_owner", "new_data"));
|
||||
.prepareStatement("INSERT INTO UpdateNameTransactions " + formatWithPlaceholders("signature", "owner", "name", "new_owner", "new_data", "name_reference"));
|
||||
PreparedStatement sellNamePStmt = c
|
||||
.prepareStatement("INSERT INTO SellNameTransactions " + formatWithPlaceholders("signature", "owner", "name", "amount"));
|
||||
PreparedStatement cancelSellNamePStmt = c
|
||||
.prepareStatement("INSERT INTO CancelSellNameTransactions " + formatWithPlaceholders("signature", "owner", "name"));
|
||||
PreparedStatement buyNamePStmt = c
|
||||
.prepareStatement("INSERT INTO BuyNameTransactions " + formatWithPlaceholders("signature", "buyer", "name", "seller", "amount"));
|
||||
.prepareStatement("INSERT INTO BuyNameTransactions " + formatWithPlaceholders("signature", "buyer", "name", "seller", "amount", "name_reference"));
|
||||
PreparedStatement createPollPStmt = c
|
||||
.prepareStatement("INSERT INTO CreatePollTransactions " + formatWithPlaceholders("signature", "creator", "owner", "poll", "description"));
|
||||
.prepareStatement("INSERT INTO CreatePollTransactions " + formatWithPlaceholders("signature", "creator", "owner", "poll_name", "description"));
|
||||
PreparedStatement createPollOptionPStmt = c
|
||||
.prepareStatement("INSERT INTO CreatePollTransactionOptions " + formatWithPlaceholders("signature", "option"));
|
||||
.prepareStatement("INSERT INTO CreatePollTransactionOptions " + formatWithPlaceholders("signature", "option_name"));
|
||||
PreparedStatement voteOnPollPStmt = c
|
||||
.prepareStatement("INSERT INTO VoteOnPollTransactions " + formatWithPlaceholders("signature", "voter", "poll", "option_index"));
|
||||
.prepareStatement("INSERT INTO VoteOnPollTransactions " + formatWithPlaceholders("signature", "voter", "poll_name", "option_index"));
|
||||
PreparedStatement arbitraryPStmt = c
|
||||
.prepareStatement("INSERT INTO ArbitraryTransactions " + formatWithPlaceholders("signature", "creator", "service", "data_hash"));
|
||||
PreparedStatement issueAssetPStmt = c.prepareStatement("INSERT INTO IssueAssetTransactions "
|
||||
@ -396,6 +396,7 @@ public class migrate {
|
||||
updateNamePStmt.setString(3, (String) transaction.get("name"));
|
||||
updateNamePStmt.setString(4, (String) transaction.get("newOwner"));
|
||||
updateNamePStmt.setString(5, (String) transaction.get("newValue"));
|
||||
updateNamePStmt.setBytes(6, txSignature); // dummy value for name_reference
|
||||
|
||||
updateNamePStmt.execute();
|
||||
updateNamePStmt.clearParameters();
|
||||
@ -426,6 +427,7 @@ public class migrate {
|
||||
buyNamePStmt.setString(3, (String) transaction.get("name"));
|
||||
buyNamePStmt.setString(4, (String) transaction.get("seller"));
|
||||
buyNamePStmt.setBigDecimal(5, BigDecimal.valueOf(Double.valueOf((String) transaction.get("amount")).doubleValue()));
|
||||
buyNamePStmt.setBytes(6, txSignature); // dummy value for name_reference
|
||||
|
||||
buyNamePStmt.execute();
|
||||
buyNamePStmt.clearParameters();
|
||||
|
@ -1,9 +1,16 @@
|
||||
package qora.assets;
|
||||
|
||||
import java.math.BigDecimal;
|
||||
import java.math.BigInteger;
|
||||
import java.math.RoundingMode;
|
||||
import java.util.List;
|
||||
|
||||
import data.assets.AssetData;
|
||||
import data.assets.OrderData;
|
||||
import data.assets.TradeData;
|
||||
import qora.account.Account;
|
||||
import qora.account.PublicKeyAccount;
|
||||
import repository.AssetRepository;
|
||||
import repository.DataException;
|
||||
import repository.Repository;
|
||||
|
||||
@ -28,44 +35,168 @@ public class Order {
|
||||
|
||||
// More information
|
||||
|
||||
public static BigDecimal getAmountLeft(OrderData orderData) {
|
||||
return orderData.getAmount().subtract(orderData.getFulfilled());
|
||||
}
|
||||
|
||||
public BigDecimal getAmountLeft() {
|
||||
return this.orderData.getAmount().subtract(this.orderData.getFulfilled());
|
||||
return Order.getAmountLeft(this.orderData);
|
||||
}
|
||||
|
||||
public static boolean isFulfilled(OrderData orderData) {
|
||||
return orderData.getFulfilled().compareTo(orderData.getAmount()) == 0;
|
||||
}
|
||||
|
||||
public boolean isFulfilled() {
|
||||
return this.orderData.getFulfilled().compareTo(this.orderData.getAmount()) == 0;
|
||||
return Order.isFulfilled(this.orderData);
|
||||
}
|
||||
|
||||
// TODO
|
||||
// public List<Trade> getInitiatedTrades() {}
|
||||
public BigDecimal calculateAmountGranularity(AssetData haveAssetData, AssetData wantAssetData, OrderData theirOrderData) {
|
||||
// 100 million to scale BigDecimal.setScale(8) fractional amounts into integers, essentially 1e8
|
||||
BigInteger multiplier = BigInteger.valueOf(100_000_000L);
|
||||
|
||||
// TODO
|
||||
// public boolean isConfirmed() {}
|
||||
// Calculate the minimum increment at which I can buy using greatest-common-divisor
|
||||
BigInteger haveAmount = BigInteger.ONE.multiply(multiplier);
|
||||
BigInteger priceAmount = theirOrderData.getPrice().multiply(new BigDecimal(multiplier)).toBigInteger();
|
||||
BigInteger gcd = haveAmount.gcd(priceAmount);
|
||||
haveAmount = haveAmount.divide(gcd);
|
||||
priceAmount = priceAmount.divide(gcd);
|
||||
|
||||
// Calculate GCD in combination with divisibility
|
||||
if (wantAssetData.getIsDivisible())
|
||||
haveAmount = haveAmount.multiply(multiplier);
|
||||
|
||||
if (haveAssetData.getIsDivisible())
|
||||
priceAmount = priceAmount.multiply(multiplier);
|
||||
|
||||
gcd = haveAmount.gcd(priceAmount);
|
||||
|
||||
// Calculate the increment at which we have to buy
|
||||
BigDecimal increment = new BigDecimal(haveAmount.divide(gcd));
|
||||
if (wantAssetData.getIsDivisible())
|
||||
increment = increment.divide(new BigDecimal(multiplier));
|
||||
|
||||
// Return
|
||||
return increment;
|
||||
}
|
||||
|
||||
// Navigation
|
||||
|
||||
// XXX is this getInitiatedTrades() above?
|
||||
public List<Trade> getTrades() {
|
||||
// TODO
|
||||
|
||||
return null;
|
||||
public List<TradeData> getTrades() throws DataException {
|
||||
return this.repository.getAssetRepository().getOrdersTrades(this.orderData.getOrderId());
|
||||
}
|
||||
|
||||
// Processing
|
||||
|
||||
public void process() throws DataException {
|
||||
AssetRepository assetRepository = this.repository.getAssetRepository();
|
||||
|
||||
long haveAssetId = this.orderData.getHaveAssetId();
|
||||
AssetData haveAssetData = assetRepository.fromAssetId(haveAssetId);
|
||||
long wantAssetId = this.orderData.getWantAssetId();
|
||||
AssetData wantAssetData = assetRepository.fromAssetId(wantAssetId);
|
||||
|
||||
// Subtract asset from creator
|
||||
Account creator = new PublicKeyAccount(this.repository, this.orderData.getCreatorPublicKey());
|
||||
creator.setConfirmedBalance(haveAssetId, creator.getConfirmedBalance(haveAssetId).subtract(this.orderData.getAmount()));
|
||||
|
||||
// Save this order into repository so it's available for matching, possibly by itself
|
||||
this.repository.getAssetRepository().save(this.orderData);
|
||||
|
||||
// TODO
|
||||
// Attempt to match orders
|
||||
|
||||
// Fetch corresponding open orders that might potentially match, hence reversed want/have assetId args.
|
||||
// Returned orders are sorted with lowest "price" first.
|
||||
List<OrderData> orders = assetRepository.getOpenOrders(wantAssetId, haveAssetId);
|
||||
|
||||
/*
|
||||
* Our order example:
|
||||
*
|
||||
* haveAssetId=[GOLD], amount=10,000, wantAssetId=0 (QORA), price=0.002
|
||||
*
|
||||
* This translates to "we have 10,000 GOLD and want to buy QORA at a price of 0.002 QORA per GOLD"
|
||||
*
|
||||
* So if our order matched, we'd end up with 10,000 * 0.002 = 20 QORA, essentially costing 1/0.002 = 500 GOLD each.
|
||||
*
|
||||
* So 500 GOLD [each] is our "buyingPrice".
|
||||
*/
|
||||
BigDecimal ourPrice = this.orderData.getPrice();
|
||||
|
||||
for (OrderData theirOrderData : orders) {
|
||||
/*
|
||||
* Potential matching order example:
|
||||
*
|
||||
* haveAssetId=0 (QORA), amount=40, wantAssetId=[GOLD], price=486
|
||||
*
|
||||
* This translates to "we have 40 QORA and want to buy GOLD at a price of 486 GOLD per QORA"
|
||||
*
|
||||
* So if their order matched, they'd end up with 40 * 486 = 19,440 GOLD, essentially costing 1/486 = 0.00205761 QORA each.
|
||||
*
|
||||
* So 0.00205761 QORA [each] is their "buyingPrice".
|
||||
*/
|
||||
|
||||
// Round down otherwise their buyingPrice would be better than advertised and cause issues
|
||||
BigDecimal theirBuyingPrice = BigDecimal.ONE.setScale(8).divide(theirOrderData.getPrice(), RoundingMode.DOWN);
|
||||
|
||||
// If their buyingPrice is less than what we're willing to pay then we're done as prices only get worse as we iterate through list of orders
|
||||
if (theirBuyingPrice.compareTo(ourPrice) < 0)
|
||||
break;
|
||||
|
||||
// Calculate how many want-asset we could buy at their price
|
||||
BigDecimal ourAmountLeft = this.getAmountLeft().multiply(theirBuyingPrice).setScale(8, RoundingMode.DOWN);
|
||||
// How many want-asset is left available in this order
|
||||
BigDecimal theirAmountLeft = Order.getAmountLeft(theirOrderData);
|
||||
// So matchable want-asset amount is the minimum of above two values
|
||||
BigDecimal matchedAmount = ourAmountLeft.min(theirAmountLeft);
|
||||
|
||||
// If we can't buy anything then we're done
|
||||
if (matchedAmount.compareTo(BigDecimal.ZERO) <= 0)
|
||||
break;
|
||||
|
||||
// Calculate amount granularity based on both assets' divisibility
|
||||
BigDecimal increment = this.calculateAmountGranularity(haveAssetData, wantAssetData, theirOrderData);
|
||||
matchedAmount = matchedAmount.subtract(matchedAmount.remainder(increment));
|
||||
|
||||
// If we can't buy anything then we're done
|
||||
if (matchedAmount.compareTo(BigDecimal.ZERO) <= 0)
|
||||
break;
|
||||
|
||||
// Trade can go ahead!
|
||||
|
||||
// Calculate the total cost to us based on their price
|
||||
BigDecimal tradePrice = matchedAmount.multiply(theirOrderData.getPrice()).setScale(8);
|
||||
|
||||
// Construct trade
|
||||
TradeData tradeData = new TradeData(this.orderData.getOrderId(), theirOrderData.getOrderId(), matchedAmount, tradePrice,
|
||||
this.orderData.getTimestamp());
|
||||
// Process trade, updating corresponding orders in repository
|
||||
Trade trade = new Trade(this.repository, tradeData);
|
||||
trade.process();
|
||||
|
||||
// Update our order in terms of fulfilment, etc. but do not save into repository as that's handled by Trade above
|
||||
this.orderData.setFulfilled(this.orderData.getFulfilled().add(matchedAmount));
|
||||
|
||||
// Continue on to process other open orders in case we still have amount left to match
|
||||
}
|
||||
}
|
||||
|
||||
public void orphan() throws DataException {
|
||||
// TODO
|
||||
|
||||
this.repository.getAssetRepository().delete(this.orderData.getOrderId());
|
||||
// Orphan trades that occurred as a result of this order
|
||||
for (TradeData tradeData : getTrades()) {
|
||||
Trade trade = new Trade(this.repository, tradeData);
|
||||
trade.orphan();
|
||||
}
|
||||
|
||||
// This is CancelOrderTransactions so that an Order can no longer trade
|
||||
// Delete this order from repository
|
||||
this.repository.getAssetRepository().delete(this.orderData.getOrderId());
|
||||
|
||||
// Return asset to creator
|
||||
long haveAssetId = this.orderData.getHaveAssetId();
|
||||
Account creator = new PublicKeyAccount(this.repository, this.orderData.getCreatorPublicKey());
|
||||
creator.setConfirmedBalance(haveAssetId, creator.getConfirmedBalance(haveAssetId).add(this.orderData.getAmount()));
|
||||
}
|
||||
|
||||
// This is called by CancelOrderTransaction so that an Order can no longer trade
|
||||
public void cancel() throws DataException {
|
||||
this.orderData.setIsClosed(true);
|
||||
this.repository.getAssetRepository().save(this.orderData);
|
||||
@ -73,7 +204,8 @@ public class Order {
|
||||
|
||||
// Opposite of cancel() above for use during orphaning
|
||||
public void reopen() throws DataException {
|
||||
// TODO
|
||||
this.orderData.setIsClosed(false);
|
||||
this.repository.getAssetRepository().save(this.orderData);
|
||||
}
|
||||
|
||||
}
|
||||
|
@ -1,47 +1,80 @@
|
||||
package qora.assets;
|
||||
|
||||
import java.math.BigDecimal;
|
||||
import java.math.BigInteger;
|
||||
import data.assets.OrderData;
|
||||
import data.assets.TradeData;
|
||||
import qora.account.Account;
|
||||
import qora.account.PublicKeyAccount;
|
||||
import repository.AssetRepository;
|
||||
import repository.DataException;
|
||||
import repository.Repository;
|
||||
|
||||
public class Trade {
|
||||
|
||||
// Properties
|
||||
private BigInteger initiator;
|
||||
private BigInteger target;
|
||||
private BigDecimal amount;
|
||||
private BigDecimal price;
|
||||
private long timestamp;
|
||||
private Repository repository;
|
||||
private TradeData tradeData;
|
||||
|
||||
// Constructors
|
||||
|
||||
public Trade(BigInteger initiator, BigInteger target, BigDecimal amount, BigDecimal price, long timestamp) {
|
||||
this.initiator = initiator;
|
||||
this.target = target;
|
||||
this.amount = amount;
|
||||
this.price = price;
|
||||
this.timestamp = timestamp;
|
||||
public Trade(Repository repository, TradeData tradeData) {
|
||||
this.repository = repository;
|
||||
this.tradeData = tradeData;
|
||||
}
|
||||
|
||||
// Getters/setters
|
||||
// Processing
|
||||
|
||||
public BigInteger getInitiator() {
|
||||
return this.initiator;
|
||||
public void process() throws DataException {
|
||||
AssetRepository assetRepository = this.repository.getAssetRepository();
|
||||
|
||||
// Save trade into repository
|
||||
assetRepository.save(tradeData);
|
||||
|
||||
// Update corresponding Orders on both sides of trade
|
||||
OrderData initiatingOrder = assetRepository.fromOrderId(this.tradeData.getInitiator());
|
||||
initiatingOrder.setFulfilled(initiatingOrder.getFulfilled().add(tradeData.getPrice()));
|
||||
initiatingOrder.setIsFulfilled(Order.isFulfilled(initiatingOrder));
|
||||
assetRepository.save(initiatingOrder);
|
||||
|
||||
OrderData targetOrder = assetRepository.fromOrderId(this.tradeData.getTarget());
|
||||
targetOrder.setFulfilled(targetOrder.getFulfilled().add(tradeData.getAmount()));
|
||||
targetOrder.setIsFulfilled(Order.isFulfilled(targetOrder));
|
||||
assetRepository.save(targetOrder);
|
||||
|
||||
// Actually transfer asset balances
|
||||
Account initiatingCreator = new PublicKeyAccount(this.repository, initiatingOrder.getCreatorPublicKey());
|
||||
initiatingCreator.setConfirmedBalance(initiatingOrder.getWantAssetId(),
|
||||
initiatingCreator.getConfirmedBalance(initiatingOrder.getWantAssetId()).add(tradeData.getAmount()));
|
||||
|
||||
Account targetCreator = new PublicKeyAccount(this.repository, targetOrder.getCreatorPublicKey());
|
||||
targetCreator.setConfirmedBalance(targetOrder.getWantAssetId(),
|
||||
targetCreator.getConfirmedBalance(targetOrder.getWantAssetId()).add(tradeData.getPrice()));
|
||||
}
|
||||
|
||||
public BigInteger getTarget() {
|
||||
return this.target;
|
||||
}
|
||||
public void orphan() throws DataException {
|
||||
AssetRepository assetRepository = this.repository.getAssetRepository();
|
||||
|
||||
public BigDecimal getAmount() {
|
||||
return this.amount;
|
||||
}
|
||||
// Revert corresponding Orders on both sides of trade
|
||||
OrderData initiatingOrder = assetRepository.fromOrderId(this.tradeData.getInitiator());
|
||||
initiatingOrder.setFulfilled(initiatingOrder.getFulfilled().subtract(tradeData.getPrice()));
|
||||
initiatingOrder.setIsFulfilled(Order.isFulfilled(initiatingOrder));
|
||||
assetRepository.save(initiatingOrder);
|
||||
|
||||
public BigDecimal getPrice() {
|
||||
return this.price;
|
||||
}
|
||||
OrderData targetOrder = assetRepository.fromOrderId(this.tradeData.getTarget());
|
||||
targetOrder.setFulfilled(targetOrder.getFulfilled().subtract(tradeData.getAmount()));
|
||||
targetOrder.setIsFulfilled(Order.isFulfilled(targetOrder));
|
||||
assetRepository.save(targetOrder);
|
||||
|
||||
public long getTimestamp() {
|
||||
return this.timestamp;
|
||||
// Reverse asset transfers
|
||||
Account initiatingCreator = new PublicKeyAccount(this.repository, initiatingOrder.getCreatorPublicKey());
|
||||
initiatingCreator.setConfirmedBalance(initiatingOrder.getWantAssetId(),
|
||||
initiatingCreator.getConfirmedBalance(initiatingOrder.getWantAssetId()).subtract(tradeData.getAmount()));
|
||||
|
||||
Account targetCreator = new PublicKeyAccount(this.repository, targetOrder.getCreatorPublicKey());
|
||||
targetCreator.setConfirmedBalance(targetOrder.getWantAssetId(),
|
||||
targetCreator.getConfirmedBalance(targetOrder.getWantAssetId()).subtract(tradeData.getPrice()));
|
||||
|
||||
// Remove trade from repository
|
||||
assetRepository.delete(tradeData);
|
||||
}
|
||||
|
||||
}
|
||||
|
@ -1,7 +1,10 @@
|
||||
package repository;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
import data.assets.AssetData;
|
||||
import data.assets.OrderData;
|
||||
import data.assets.TradeData;
|
||||
|
||||
public interface AssetRepository {
|
||||
|
||||
@ -23,8 +26,18 @@ public interface AssetRepository {
|
||||
|
||||
public OrderData fromOrderId(byte[] orderId) throws DataException;
|
||||
|
||||
public List<OrderData> getOpenOrders(long haveAssetId, long wantAssetId) throws DataException;
|
||||
|
||||
public void save(OrderData orderData) throws DataException;
|
||||
|
||||
public void delete(byte[] orderId) throws DataException;
|
||||
|
||||
// Trades
|
||||
|
||||
public List<TradeData> getOrdersTrades(byte[] orderId) throws DataException;
|
||||
|
||||
public void save(TradeData tradeData) throws DataException;
|
||||
|
||||
public void delete(TradeData tradeData) throws DataException;
|
||||
|
||||
}
|
||||
|
@ -3,9 +3,13 @@ package repository.hsqldb;
|
||||
import java.math.BigDecimal;
|
||||
import java.sql.ResultSet;
|
||||
import java.sql.SQLException;
|
||||
import java.sql.Timestamp;
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
|
||||
import data.assets.AssetData;
|
||||
import data.assets.OrderData;
|
||||
import data.assets.TradeData;
|
||||
import repository.AssetRepository;
|
||||
import repository.DataException;
|
||||
|
||||
@ -77,6 +81,7 @@ 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())
|
||||
.bind("reference", assetData.getReference());
|
||||
@ -104,7 +109,7 @@ public class HSQLDBAssetRepository implements AssetRepository {
|
||||
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, is_closed FROM AssetOrders WHERE asset_order_id = ?",
|
||||
"SELECT creator, have_asset_id, want_asset_id, amount, fulfilled, price, ordered, is_closed, is_fulfilled FROM AssetOrders WHERE asset_order_id = ?",
|
||||
orderId);
|
||||
if (resultSet == null)
|
||||
return null;
|
||||
@ -117,18 +122,53 @@ public class HSQLDBAssetRepository implements AssetRepository {
|
||||
BigDecimal price = resultSet.getBigDecimal(6);
|
||||
long timestamp = resultSet.getTimestamp(7).getTime();
|
||||
boolean isClosed = resultSet.getBoolean(8);
|
||||
boolean isFulfilled = resultSet.getBoolean(9);
|
||||
|
||||
return new OrderData(orderId, creatorPublicKey, haveAssetId, wantAssetId, amount, fulfilled, price, timestamp, isClosed);
|
||||
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);
|
||||
}
|
||||
}
|
||||
|
||||
public List<OrderData> getOpenOrders(long haveAssetId, long wantAssetId) throws DataException {
|
||||
List<OrderData> orders = new ArrayList<OrderData>();
|
||||
|
||||
try {
|
||||
ResultSet resultSet = this.repository.checkedExecute(
|
||||
"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 ASC",
|
||||
haveAssetId, wantAssetId);
|
||||
if (resultSet == null)
|
||||
return orders;
|
||||
|
||||
do {
|
||||
byte[] creatorPublicKey = resultSet.getBytes(1);
|
||||
byte[] orderId = resultSet.getBytes(2);
|
||||
BigDecimal amount = resultSet.getBigDecimal(3);
|
||||
BigDecimal fulfilled = resultSet.getBigDecimal(4);
|
||||
BigDecimal price = resultSet.getBigDecimal(5);
|
||||
long timestamp = resultSet.getTimestamp(6).getTime();
|
||||
boolean isClosed = false;
|
||||
boolean isFulfilled = false;
|
||||
|
||||
OrderData order = new OrderData(orderId, creatorPublicKey, haveAssetId, wantAssetId, amount, fulfilled, price, timestamp, isClosed,
|
||||
isFulfilled);
|
||||
orders.add(order);
|
||||
} while (resultSet.next());
|
||||
|
||||
return orders;
|
||||
} catch (SQLException e) {
|
||||
throw new DataException("Unable to fetch asset orders from repository", e);
|
||||
}
|
||||
}
|
||||
|
||||
public void save(OrderData orderData) throws DataException {
|
||||
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("isClosed", orderData.getIsClosed());
|
||||
.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 {
|
||||
saveHelper.execute(this.repository);
|
||||
@ -139,10 +179,59 @@ public class HSQLDBAssetRepository implements AssetRepository {
|
||||
|
||||
public void delete(byte[] orderId) throws DataException {
|
||||
try {
|
||||
this.repository.delete("AssetOrders", "orderId = ?", orderId);
|
||||
this.repository.delete("AssetOrders", "asset_order_id = ?", orderId);
|
||||
} catch (SQLException e) {
|
||||
throw new DataException("Unable to delete asset order from repository", e);
|
||||
}
|
||||
}
|
||||
|
||||
// Trades
|
||||
|
||||
public List<TradeData> getOrdersTrades(byte[] initiatingOrderId) throws DataException {
|
||||
List<TradeData> trades = new ArrayList<TradeData>();
|
||||
|
||||
try {
|
||||
ResultSet resultSet = this.repository.checkedExecute("SELECT target_order_id, amount, price, traded FROM AssetTrades WHERE initiating_order_id = ?",
|
||||
initiatingOrderId);
|
||||
if (resultSet == null)
|
||||
return trades;
|
||||
|
||||
do {
|
||||
byte[] targetOrderId = resultSet.getBytes(1);
|
||||
BigDecimal amount = resultSet.getBigDecimal(2);
|
||||
BigDecimal price = resultSet.getBigDecimal(3);
|
||||
long timestamp = resultSet.getTimestamp(4).getTime();
|
||||
|
||||
TradeData trade = new TradeData(initiatingOrderId, targetOrderId, amount, price, timestamp);
|
||||
trades.add(trade);
|
||||
} while (resultSet.next());
|
||||
|
||||
return trades;
|
||||
} catch (SQLException e) {
|
||||
throw new DataException("Unable to fetch asset order's trades from repository", e);
|
||||
}
|
||||
}
|
||||
|
||||
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()));
|
||||
|
||||
try {
|
||||
saveHelper.execute(this.repository);
|
||||
} catch (SQLException e) {
|
||||
throw new DataException("Unable to save asset trade into repository", e);
|
||||
}
|
||||
}
|
||||
|
||||
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());
|
||||
} catch (SQLException e) {
|
||||
throw new DataException("Unable to delete asset trade from repository", e);
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
@ -300,16 +300,24 @@ public class HSQLDBDatabaseUpdates {
|
||||
// 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, is_closed BOOLEAN NOT NULL, "
|
||||
+ "PRIMARY KEY (asset_order_id))");
|
||||
// For quick matching of orders. is_closed included so inactive orders can be filtered out.
|
||||
stmt.execute("CREATE INDEX AssetOrderHaveIndex on AssetOrders (have_asset_id, is_closed)");
|
||||
stmt.execute("CREATE INDEX AssetOrderWantIndex on AssetOrders (want_asset_id, is_closed)");
|
||||
+ "amount QoraAmount NOT NULL, fulfilled QoraAmount NOT NULL, price QoraAmount NOT NULL, "
|
||||
+ "ordered TIMESTAMP NOT NULL, is_closed BOOLEAN NOT NULL, is_fulfilled BOOLEAN NOT NULL, " + "PRIMARY KEY (asset_order_id))");
|
||||
// For quick matching of orders. is_closed are is_fulfilled included so inactive orders can be filtered out.
|
||||
stmt.execute("CREATE INDEX AssetOrderMatchingIndex on AssetOrders (have_asset_id, want_asset_id, is_closed, is_fulfilled)");
|
||||
// For when a user wants to look up their current/historic orders. is_closed included so user can filter by active/inactive orders.
|
||||
stmt.execute("CREATE INDEX AssetOrderCreatorIndex on AssetOrders (creator, is_closed)");
|
||||
break;
|
||||
|
||||
case 24:
|
||||
// Asset Trades
|
||||
stmt.execute("CREATE TABLE AssetTrades (initiating_order_id AssetOrderId NOT NULL, target_order_id AssetOrderId NOT NULL, "
|
||||
+ "amount QoraAmount NOT NULL, price QoraAmount NOT NULL, traded TIMESTAMP NOT NULL)");
|
||||
// For looking up historic trades based on orders
|
||||
stmt.execute("CREATE INDEX AssetTradeBuyOrderIndex on AssetTrades (initiating_order_id, traded)");
|
||||
stmt.execute("CREATE INDEX AssetTradeSellOrderIndex on AssetTrades (target_order_id, traded)");
|
||||
break;
|
||||
|
||||
case 25:
|
||||
// Polls/Voting
|
||||
stmt.execute(
|
||||
"CREATE TABLE Polls (poll_name PollName, description VARCHAR(4000) NOT NULL, creator QoraPublicKey NOT NULL, owner QoraAddress NOT NULL, "
|
||||
@ -324,7 +332,7 @@ public class HSQLDBDatabaseUpdates {
|
||||
stmt.execute("CREATE INDEX PollOwnerIndex on Polls (owner)");
|
||||
break;
|
||||
|
||||
case 25:
|
||||
case 26:
|
||||
// Registered Names
|
||||
stmt.execute(
|
||||
"CREATE TABLE Names (name RegisteredName, data VARCHAR(4000) NOT NULL, registrant QoraPublicKey NOT NULL, owner QoraAddress NOT NULL, "
|
||||
|
@ -18,10 +18,14 @@ import data.PaymentData;
|
||||
import data.account.AccountBalanceData;
|
||||
import data.account.AccountData;
|
||||
import data.assets.AssetData;
|
||||
import data.assets.OrderData;
|
||||
import data.assets.TradeData;
|
||||
import data.block.BlockData;
|
||||
import data.naming.NameData;
|
||||
import data.transaction.BuyNameTransactionData;
|
||||
import data.transaction.CancelOrderTransactionData;
|
||||
import data.transaction.CancelSellNameTransactionData;
|
||||
import data.transaction.CreateOrderTransactionData;
|
||||
import data.transaction.CreatePollTransactionData;
|
||||
import data.transaction.IssueAssetTransactionData;
|
||||
import data.transaction.MessageTransactionData;
|
||||
@ -42,7 +46,9 @@ import qora.assets.Asset;
|
||||
import qora.block.Block;
|
||||
import qora.block.BlockChain;
|
||||
import qora.transaction.BuyNameTransaction;
|
||||
import qora.transaction.CancelOrderTransaction;
|
||||
import qora.transaction.CancelSellNameTransaction;
|
||||
import qora.transaction.CreateOrderTransaction;
|
||||
import qora.transaction.CreatePollTransaction;
|
||||
import qora.transaction.IssueAssetTransaction;
|
||||
import qora.transaction.MessageTransaction;
|
||||
@ -73,8 +79,9 @@ public class TransactionTests {
|
||||
private static final byte[] senderSeed = HashCode.fromString("0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef").asBytes();
|
||||
private static final byte[] recipientSeed = HashCode.fromString("fedcba9876543210fedcba9876543210fedcba9876543210fedcba9876543210").asBytes();
|
||||
|
||||
private static final BigDecimal initialGeneratorBalance = BigDecimal.valueOf(1_000_000_000L);
|
||||
private static final BigDecimal initialSenderBalance = BigDecimal.valueOf(1_000_000L);
|
||||
private static final BigDecimal initialGeneratorBalance = BigDecimal.valueOf(1_000_000_000L).setScale(8);
|
||||
private static final BigDecimal initialSenderBalance = BigDecimal.valueOf(1_000_000L).setScale(8);
|
||||
private static final BigDecimal genericPaymentAmount = BigDecimal.valueOf(1_000L).setScale(8);
|
||||
|
||||
private Repository repository;
|
||||
private AccountRepository accountRepository;
|
||||
@ -135,7 +142,7 @@ public class TransactionTests {
|
||||
|
||||
private Transaction createPayment(PrivateKeyAccount sender, String recipient) throws DataException {
|
||||
// Make a new payment transaction
|
||||
BigDecimal amount = BigDecimal.valueOf(1_000L);
|
||||
BigDecimal amount = genericPaymentAmount;
|
||||
BigDecimal fee = BigDecimal.ONE;
|
||||
long timestamp = parentBlockData.getTimestamp() + 1_000;
|
||||
PaymentTransactionData paymentTransactionData = new PaymentTransactionData(sender.getPublicKey(), recipient, amount, fee, timestamp, reference);
|
||||
@ -163,7 +170,7 @@ public class TransactionTests {
|
||||
assertTrue(paymentTransaction.isSignatureValid());
|
||||
assertEquals(ValidationResult.OK, paymentTransaction.isValid());
|
||||
|
||||
// Forge new block with payment transaction
|
||||
// Forge new block with transaction
|
||||
Block block = new Block(repository, parentBlockData, generator, null, null);
|
||||
block.addTransaction(paymentTransactionData);
|
||||
block.sign();
|
||||
@ -591,7 +598,7 @@ public class TransactionTests {
|
||||
lastBlock.orphan();
|
||||
repository.saveChanges();
|
||||
|
||||
// Recheck poll's votes
|
||||
// Re-check poll's votes
|
||||
votes = repository.getVotingRepository().getVotes(pollName);
|
||||
assertNotNull(votes);
|
||||
|
||||
@ -621,7 +628,7 @@ public class TransactionTests {
|
||||
assertTrue(issueAssetTransaction.isSignatureValid());
|
||||
assertEquals(ValidationResult.OK, issueAssetTransaction.isValid());
|
||||
|
||||
// Forge new block with payment transaction
|
||||
// Forge new block with transaction
|
||||
Block block = new Block(repository, parentBlockData, generator, null, null);
|
||||
block.addTransaction(issueAssetTransactionData);
|
||||
block.sign();
|
||||
@ -711,7 +718,7 @@ public class TransactionTests {
|
||||
assertTrue(transferAssetTransaction.isSignatureValid());
|
||||
assertEquals(ValidationResult.OK, transferAssetTransaction.isValid());
|
||||
|
||||
// Forge new block with payment transaction
|
||||
// Forge new block with transaction
|
||||
Block block = new Block(repository, parentBlockData, generator, null, null);
|
||||
block.addTransaction(transferAssetTransactionData);
|
||||
block.sign();
|
||||
@ -775,12 +782,267 @@ public class TransactionTests {
|
||||
|
||||
@Test
|
||||
public void testCreateAssetOrderTransaction() throws DataException {
|
||||
// TODO
|
||||
// Issue asset using another test
|
||||
testIssueAssetTransaction();
|
||||
|
||||
// Asset info
|
||||
String assetName = "test asset";
|
||||
AssetRepository assetRepo = this.repository.getAssetRepository();
|
||||
AssetData originalAssetData = assetRepo.fromAssetName(assetName);
|
||||
long assetId = originalAssetData.getAssetId();
|
||||
|
||||
// Buyer
|
||||
PrivateKeyAccount buyer = new PrivateKeyAccount(repository, recipientSeed);
|
||||
|
||||
// Send buyer some funds so they have a reference
|
||||
Transaction somePaymentTransaction = createPayment(sender, buyer.getAddress());
|
||||
byte[] buyersReference = somePaymentTransaction.getTransactionData().getSignature();
|
||||
|
||||
// Forge new block with transaction
|
||||
Block block = new Block(repository, parentBlockData, generator, null, null);
|
||||
block.addTransaction(somePaymentTransaction.getTransactionData());
|
||||
block.sign();
|
||||
|
||||
block.process();
|
||||
repository.saveChanges();
|
||||
parentBlockData = block.getBlockData();
|
||||
|
||||
// Order: buyer has 10 QORA and wants to buy "test asset" at a price of 50 "test asset" per QORA.
|
||||
long haveAssetId = Asset.QORA;
|
||||
BigDecimal amount = BigDecimal.valueOf(10).setScale(8);
|
||||
long wantAssetId = assetId;
|
||||
BigDecimal price = BigDecimal.valueOf(50).setScale(8);
|
||||
BigDecimal fee = BigDecimal.ONE;
|
||||
long timestamp = parentBlockData.getTimestamp() + 1_000;
|
||||
|
||||
CreateOrderTransactionData createOrderTransactionData = new CreateOrderTransactionData(buyer.getPublicKey(), haveAssetId, wantAssetId, amount, price,
|
||||
fee, timestamp, buyersReference);
|
||||
Transaction createOrderTransaction = new CreateOrderTransaction(this.repository, createOrderTransactionData);
|
||||
createOrderTransaction.calcSignature(buyer);
|
||||
assertTrue(createOrderTransaction.isSignatureValid());
|
||||
assertEquals(ValidationResult.OK, createOrderTransaction.isValid());
|
||||
|
||||
// Forge new block with transaction
|
||||
block = new Block(repository, parentBlockData, generator, null, null);
|
||||
block.addTransaction(createOrderTransactionData);
|
||||
block.sign();
|
||||
|
||||
assertTrue("Block signatures invalid", block.isSignatureValid());
|
||||
assertEquals("Block is invalid", Block.ValidationResult.OK, block.isValid());
|
||||
|
||||
block.process();
|
||||
repository.saveChanges();
|
||||
|
||||
// Check order was created
|
||||
byte[] orderId = createOrderTransactionData.getSignature();
|
||||
OrderData orderData = assetRepo.fromOrderId(orderId);
|
||||
assertNotNull(orderData);
|
||||
|
||||
// Check buyer's balance reduced
|
||||
BigDecimal expectedBalance = genericPaymentAmount.subtract(amount).subtract(fee);
|
||||
BigDecimal actualBalance = buyer.getConfirmedBalance(haveAssetId);
|
||||
assertTrue(expectedBalance.compareTo(actualBalance) == 0);
|
||||
|
||||
// Orphan transaction
|
||||
block.orphan();
|
||||
repository.saveChanges();
|
||||
|
||||
// Check order no longer exists
|
||||
orderData = assetRepo.fromOrderId(orderId);
|
||||
assertNull(orderData);
|
||||
|
||||
// Check buyer's balance restored
|
||||
expectedBalance = genericPaymentAmount;
|
||||
actualBalance = buyer.getConfirmedBalance(haveAssetId);
|
||||
assertTrue(expectedBalance.compareTo(actualBalance) == 0);
|
||||
|
||||
// Re-process to allow use by other tests
|
||||
block.process();
|
||||
repository.saveChanges();
|
||||
|
||||
// Update variables for use by other tests
|
||||
reference = sender.getLastReference();
|
||||
parentBlockData = block.getBlockData();
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testCancelAssetOrderTransaction() throws DataException {
|
||||
// TODO
|
||||
// Issue asset and create order using another test
|
||||
testCreateAssetOrderTransaction();
|
||||
|
||||
// Asset info
|
||||
String assetName = "test asset";
|
||||
AssetRepository assetRepo = this.repository.getAssetRepository();
|
||||
AssetData originalAssetData = assetRepo.fromAssetName(assetName);
|
||||
long assetId = originalAssetData.getAssetId();
|
||||
|
||||
// Buyer
|
||||
PrivateKeyAccount buyer = new PrivateKeyAccount(repository, recipientSeed);
|
||||
|
||||
// Fetch orders
|
||||
long haveAssetId = Asset.QORA;
|
||||
long wantAssetId = assetId;
|
||||
List<OrderData> orders = assetRepo.getOpenOrders(haveAssetId, wantAssetId);
|
||||
|
||||
assertNotNull(orders);
|
||||
assertEquals(1, orders.size());
|
||||
|
||||
OrderData originalOrderData = orders.get(0);
|
||||
assertNotNull(originalOrderData);
|
||||
assertFalse(originalOrderData.getIsClosed());
|
||||
|
||||
// Create cancel order transaction
|
||||
byte[] orderId = originalOrderData.getOrderId();
|
||||
BigDecimal fee = BigDecimal.ONE;
|
||||
long timestamp = parentBlockData.getTimestamp() + 1_000;
|
||||
byte[] buyersReference = buyer.getLastReference();
|
||||
CancelOrderTransactionData cancelOrderTransactionData = new CancelOrderTransactionData(buyer.getPublicKey(), orderId, fee, timestamp, buyersReference);
|
||||
|
||||
Transaction cancelOrderTransaction = new CancelOrderTransaction(this.repository, cancelOrderTransactionData);
|
||||
cancelOrderTransaction.calcSignature(buyer);
|
||||
assertTrue(cancelOrderTransaction.isSignatureValid());
|
||||
assertEquals(ValidationResult.OK, cancelOrderTransaction.isValid());
|
||||
|
||||
// Forge new block with transaction
|
||||
Block block = new Block(repository, parentBlockData, generator, null, null);
|
||||
block.addTransaction(cancelOrderTransactionData);
|
||||
block.sign();
|
||||
|
||||
assertTrue("Block signatures invalid", block.isSignatureValid());
|
||||
assertEquals("Block is invalid", Block.ValidationResult.OK, block.isValid());
|
||||
|
||||
block.process();
|
||||
repository.saveChanges();
|
||||
|
||||
// Check order is marked as cancelled
|
||||
OrderData cancelledOrderData = assetRepo.fromOrderId(orderId);
|
||||
assertNotNull(cancelledOrderData);
|
||||
assertTrue(cancelledOrderData.getIsClosed());
|
||||
|
||||
// Orphan
|
||||
block.orphan();
|
||||
repository.saveChanges();
|
||||
|
||||
// Check order is no longer marked as cancelled
|
||||
OrderData uncancelledOrderData = assetRepo.fromOrderId(orderId);
|
||||
assertNotNull(uncancelledOrderData);
|
||||
assertFalse(uncancelledOrderData.getIsClosed());
|
||||
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testMatchingCreateAssetOrderTransaction() throws DataException {
|
||||
// Issue asset and create order using another test
|
||||
testCreateAssetOrderTransaction();
|
||||
|
||||
// Asset info
|
||||
String assetName = "test asset";
|
||||
AssetRepository assetRepo = this.repository.getAssetRepository();
|
||||
AssetData originalAssetData = assetRepo.fromAssetName(assetName);
|
||||
long assetId = originalAssetData.getAssetId();
|
||||
|
||||
// Buyer
|
||||
PrivateKeyAccount buyer = new PrivateKeyAccount(repository, recipientSeed);
|
||||
|
||||
// Fetch orders
|
||||
long originalHaveAssetId = Asset.QORA;
|
||||
long originalWantAssetId = assetId;
|
||||
List<OrderData> orders = assetRepo.getOpenOrders(originalHaveAssetId, originalWantAssetId);
|
||||
|
||||
assertNotNull(orders);
|
||||
assertEquals(1, orders.size());
|
||||
|
||||
OrderData originalOrderData = orders.get(0);
|
||||
assertNotNull(originalOrderData);
|
||||
assertFalse(originalOrderData.getIsClosed());
|
||||
|
||||
// Original asset owner (sender) will sell asset to "buyer"
|
||||
|
||||
// Order: seller has 40 "test asset" and wants to buy QORA at a price of 1/60 QORA per "test asset".
|
||||
// This order should be a partial match for original order, and at a better price than asked
|
||||
long haveAssetId = Asset.QORA;
|
||||
BigDecimal amount = BigDecimal.valueOf(40).setScale(8);
|
||||
long wantAssetId = assetId;
|
||||
BigDecimal price = BigDecimal.ONE.setScale(8).divide(BigDecimal.valueOf(60).setScale(8));
|
||||
BigDecimal fee = BigDecimal.ONE;
|
||||
long timestamp = parentBlockData.getTimestamp() + 1_000;
|
||||
|
||||
CreateOrderTransactionData createOrderTransactionData = new CreateOrderTransactionData(sender.getPublicKey(), haveAssetId, wantAssetId, amount, price,
|
||||
fee, timestamp, reference);
|
||||
Transaction createOrderTransaction = new CreateOrderTransaction(this.repository, createOrderTransactionData);
|
||||
createOrderTransaction.calcSignature(sender);
|
||||
assertTrue(createOrderTransaction.isSignatureValid());
|
||||
assertEquals(ValidationResult.OK, createOrderTransaction.isValid());
|
||||
|
||||
// Forge new block with transaction
|
||||
Block block = new Block(repository, parentBlockData, generator, null, null);
|
||||
block.addTransaction(createOrderTransactionData);
|
||||
block.sign();
|
||||
|
||||
assertTrue("Block signatures invalid", block.isSignatureValid());
|
||||
assertEquals("Block is invalid", Block.ValidationResult.OK, block.isValid());
|
||||
|
||||
block.process();
|
||||
repository.saveChanges();
|
||||
|
||||
// Check order was created
|
||||
byte[] orderId = createOrderTransactionData.getSignature();
|
||||
OrderData orderData = assetRepo.fromOrderId(orderId);
|
||||
assertNotNull(orderData);
|
||||
assertFalse(orderData.getIsFulfilled());
|
||||
|
||||
// Check order has trades
|
||||
List<TradeData> trades = assetRepo.getOrdersTrades(orderId);
|
||||
assertNotNull(trades);
|
||||
assertEquals(1, trades.size());
|
||||
TradeData tradeData = trades.get(0);
|
||||
|
||||
// Check trade has correct values
|
||||
BigDecimal expectedAmount = amount.multiply(price);
|
||||
BigDecimal actualAmount = tradeData.getAmount();
|
||||
assertTrue(expectedAmount.compareTo(actualAmount) == 0);
|
||||
|
||||
BigDecimal expectedPrice = originalOrderData.getPrice().multiply(amount);
|
||||
BigDecimal actualPrice = tradeData.getPrice();
|
||||
assertTrue(expectedPrice.compareTo(actualPrice) == 0);
|
||||
|
||||
// Check seller's "test asset" balance
|
||||
BigDecimal expectedBalance = BigDecimal.valueOf(1_000_000L).setScale(8).subtract(amount);
|
||||
BigDecimal actualBalance = sender.getConfirmedBalance(haveAssetId);
|
||||
assertTrue(expectedBalance.compareTo(actualBalance) == 0);
|
||||
|
||||
// Check buyer's "test asset" balance
|
||||
expectedBalance = amount;
|
||||
actualBalance = buyer.getConfirmedBalance(haveAssetId);
|
||||
assertTrue(expectedBalance.compareTo(actualBalance) == 0);
|
||||
|
||||
// Check seller's QORA balance
|
||||
expectedBalance = initialSenderBalance.subtract(BigDecimal.ONE).subtract(BigDecimal.ONE);
|
||||
actualBalance = sender.getConfirmedBalance(wantAssetId);
|
||||
assertTrue(expectedBalance.compareTo(actualBalance) == 0);
|
||||
|
||||
// Orphan transaction
|
||||
block.orphan();
|
||||
repository.saveChanges();
|
||||
|
||||
// Check order no longer exists
|
||||
orderData = assetRepo.fromOrderId(orderId);
|
||||
assertNull(orderData);
|
||||
|
||||
// Check trades no longer exist
|
||||
trades = assetRepo.getOrdersTrades(orderId);
|
||||
assertNotNull(trades);
|
||||
assertEquals(0, trades.size());
|
||||
|
||||
// Check seller's "test asset" balance restored
|
||||
expectedBalance = BigDecimal.valueOf(1_000_000L).setScale(8);
|
||||
actualBalance = sender.getConfirmedBalance(haveAssetId);
|
||||
assertTrue(expectedBalance.compareTo(actualBalance) == 0);
|
||||
|
||||
// Check buyer's "test asset" balance restored
|
||||
expectedBalance = BigDecimal.ZERO.setScale(8);
|
||||
actualBalance = buyer.getConfirmedBalance(haveAssetId);
|
||||
assertTrue(expectedBalance.compareTo(actualBalance) == 0);
|
||||
}
|
||||
|
||||
@Test
|
||||
|
@ -16,11 +16,11 @@ import com.google.common.primitives.Bytes;
|
||||
import com.google.common.primitives.Ints;
|
||||
import com.google.common.primitives.Longs;
|
||||
|
||||
import data.assets.TradeData;
|
||||
import data.block.BlockData;
|
||||
import data.transaction.TransactionData;
|
||||
import qora.account.PublicKeyAccount;
|
||||
import qora.assets.Order;
|
||||
import qora.assets.Trade;
|
||||
import qora.block.Block;
|
||||
import qora.transaction.CreateOrderTransaction;
|
||||
import qora.transaction.Transaction;
|
||||
@ -224,6 +224,8 @@ public class BlockTransformer extends Transformer {
|
||||
|
||||
// Add transaction info
|
||||
JSONArray transactionsJson = new JSONArray();
|
||||
|
||||
// XXX this should be moved out to API as it requires repository access
|
||||
boolean tradesHappened = false;
|
||||
|
||||
try {
|
||||
@ -234,10 +236,10 @@ public class BlockTransformer extends Transformer {
|
||||
if (transaction.getTransactionData().getType() == Transaction.TransactionType.CREATE_ASSET_ORDER) {
|
||||
CreateOrderTransaction orderTransaction = (CreateOrderTransaction) transaction;
|
||||
Order order = orderTransaction.getOrder();
|
||||
List<Trade> trades = order.getTrades();
|
||||
List<TradeData> trades = order.getTrades();
|
||||
|
||||
// Filter out trades with timestamps that don't match order transaction's timestamp
|
||||
trades.removeIf((Trade trade) -> trade.getTimestamp() != order.getOrderData().getTimestamp());
|
||||
// Filter out trades with initiatingOrderId that doesn't match this order
|
||||
trades.removeIf((TradeData tradeData) -> !Arrays.equals(tradeData.getInitiator(), order.getOrderData().getOrderId()));
|
||||
|
||||
// Any trades left?
|
||||
if (!trades.isEmpty()) {
|
||||
|
Loading…
Reference in New Issue
Block a user