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:
catbref 2018-07-05 16:24:05 +01:00
parent 104be89b4e
commit b401adcc55
10 changed files with 669 additions and 72 deletions

View File

@ -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

View 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;
}
}

View File

@ -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();

View File

@ -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
// Orphan trades that occurred as a result of this order
for (TradeData tradeData : getTrades()) {
Trade trade = new Trade(this.repository, tradeData);
trade.orphan();
}
// 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 CancelOrderTransactions so that an Order can no longer trade
// 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);
}
}

View File

@ -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);
}
}

View File

@ -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;
}

View File

@ -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);
}
}
}

View File

@ -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, "

View File

@ -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

View File

@ -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()) {