Browse Source

New asset pricing scheme (take 2)

Orders are back to having "amount" and "price".
(No more "unitPrice" or "wantAmount").

Order "amount" is expressed in terms of asset with highest
assetID.
"price" is expressed in (lowest-assetID)/(highest-assetID).

Given an order with two assets, e.g. QORA (0) and GOLD (31),
"amount" is in GOLD (31), "price" is in QORA/GOLD (0/31).

Order's "fulfilled" is in the same asset as "amount".

Yet more tests and debugging.

For simplicity's sake, the change to HSQLDB repository is
assumed to take place when 'new' pricing switch also
occurs.

Don't forget to change "newAssetPricingTimestamp" in
blockchain config JSON file.
pull/67/head
catbref 6 years ago
parent
commit
a5e963911d
  1. 6
      src/main/java/org/qora/api/model/AggregatedOrder.java
  2. 429
      src/main/java/org/qora/asset/Order.java
  3. 25
      src/main/java/org/qora/asset/Trade.java
  4. 33
      src/main/java/org/qora/data/asset/OrderData.java
  5. 19
      src/main/java/org/qora/data/transaction/CreateAssetOrderTransactionData.java
  6. 3
      src/main/java/org/qora/repository/AssetRepository.java
  7. 109
      src/main/java/org/qora/repository/hsqldb/HSQLDBAssetRepository.java
  8. 30
      src/main/java/org/qora/repository/hsqldb/HSQLDBDatabaseUpdates.java
  9. 8
      src/main/java/org/qora/repository/hsqldb/transaction/HSQLDBCreateAssetOrderTransactionRepository.java
  10. 90
      src/main/java/org/qora/transaction/CreateAssetOrderTransaction.java
  11. 8
      src/main/java/org/qora/transform/transaction/CreateAssetOrderTransactionTransformer.java
  12. 2
      src/test/java/org/qora/test/TransactionTests.java
  13. 85
      src/test/java/org/qora/test/assets/GranularityTests.java
  14. 27
      src/test/java/org/qora/test/assets/MixedPricingTests.java
  15. 349
      src/test/java/org/qora/test/assets/NewTradingTests.java
  16. 215
      src/test/java/org/qora/test/assets/OldTradingTests.java
  17. 441
      src/test/java/org/qora/test/assets/TradingTests.java
  18. 6
      src/test/java/org/qora/test/common/AccountUtils.java
  19. 65
      src/test/java/org/qora/test/common/AssetUtils.java
  20. 35
      src/test/java/org/qora/test/common/Common.java
  21. 21
      src/test/java/org/qora/test/common/ObjectUtils.java
  22. 4
      src/test/resources/log4j2-test.properties

6
src/main/java/org/qora/api/model/AggregatedOrder.java

@ -20,9 +20,9 @@ public class AggregatedOrder {
this.orderData = orderData;
}
@XmlElement(name = "unitPrice")
public BigDecimal getUnitPrice() {
return this.orderData.getUnitPrice();
@XmlElement(name = "price")
public BigDecimal getPrice() {
return this.orderData.getPrice();
}
@XmlElement(name = "unfulfilled")

429
src/main/java/org/qora/asset/Order.java

@ -18,27 +18,38 @@ import org.qora.data.asset.TradeData;
import org.qora.repository.AssetRepository;
import org.qora.repository.DataException;
import org.qora.repository.Repository;
import com.google.common.hash.HashCode;
import org.qora.utils.Base58;
public class Order {
/** BigDecimal scale for representing unit price in asset orders. */
public static final int BD_PRICE_SCALE = 38;
/** BigDecimal scale for representing unit price in asset orders in storage context. */
public static final int BD_PRICE_STORAGE_SCALE = BD_PRICE_SCALE + 10;
private static final Logger LOGGER = LogManager.getLogger(Order.class);
// Properties
private Repository repository;
private OrderData orderData;
// Used quite a bit
private final boolean isOurOrderNewPricing;
private final long haveAssetId;
private final long wantAssetId;
/** Cache of price-pair units e.g. QORA/GOLD, but use getPricePair() instead! */
private String cachedPricePair;
/** Cache of have-asset data - but use getHaveAsset() instead! */
AssetData cachedHaveAssetData;
/** Cache of want-asset data - but use getWantAsset() instead! */
AssetData cachedWantAssetData;
// Constructors
public Order(Repository repository, OrderData orderData) {
this.repository = repository;
this.orderData = orderData;
this.isOurOrderNewPricing = this.orderData.getTimestamp() >= BlockChain.getInstance().getNewAssetPricingTimestamp();
this.haveAssetId = this.orderData.getHaveAssetId();
this.wantAssetId = this.orderData.getWantAssetId();
}
// Getters/Setters
@ -66,275 +77,346 @@ public class Order {
}
/**
* Returns want-asset granularity/unit-size given price.
* Returns granularity/batch-size of matched-amount, given price, so that return-amount is valid size.
* <p>
* @param theirPrice
* @return unit price of want asset
* If matched-amount of matched-asset is traded when two orders match,
* then the corresponding return-amount of the other (return) asset needs to be either
* an integer, if return-asset is indivisible,
* or to the nearest 0.00000001 if return-asset is divisible.
* <p>
* @return granularity of matched-amount
*/
public static BigDecimal calculateAmountGranularity(AssetData haveAssetData, AssetData wantAssetData, OrderData theirOrderData) {
public static BigDecimal calculateAmountGranularity(boolean isAmountAssetDivisible, boolean isReturnAssetDivisible, BigDecimal price) {
// Multiplier to scale BigDecimal fractional amounts into integer domain
BigInteger multiplier = BigInteger.valueOf(1_0000_0000L);
// Calculate the minimum increment at which I can buy using greatest-common-divisor
BigInteger haveAmount;
BigInteger wantAmount;
if (theirOrderData.getTimestamp() >= BlockChain.getInstance().getNewAssetPricingTimestamp()) {
// "new" pricing scheme
haveAmount = theirOrderData.getAmount().movePointRight(8).toBigInteger();
wantAmount = theirOrderData.getWantAmount().movePointRight(8).toBigInteger();
} else {
// legacy "old" behaviour
haveAmount = multiplier; // 1 unit (* multiplier)
wantAmount = theirOrderData.getUnitPrice().movePointRight(8).toBigInteger();
}
// Calculate the minimum increment for matched-amount using greatest-common-divisor
BigInteger returnAmount = multiplier; // 1 unit (* multiplier)
BigInteger matchedAmount = price.movePointRight(8).toBigInteger();
BigInteger gcd = haveAmount.gcd(wantAmount);
haveAmount = haveAmount.divide(gcd);
wantAmount = wantAmount.divide(gcd);
BigInteger gcd = returnAmount.gcd(matchedAmount);
returnAmount = returnAmount.divide(gcd);
matchedAmount = matchedAmount.divide(gcd);
// Calculate GCD in combination with divisibility
if (wantAssetData.getIsDivisible())
haveAmount = haveAmount.multiply(multiplier);
if (isAmountAssetDivisible)
returnAmount = returnAmount.multiply(multiplier);
if (haveAssetData.getIsDivisible())
wantAmount = wantAmount.multiply(multiplier);
if (isReturnAssetDivisible)
matchedAmount = matchedAmount.multiply(multiplier);
gcd = haveAmount.gcd(wantAmount);
gcd = returnAmount.gcd(matchedAmount);
// Calculate the granularity at which we have to buy
BigDecimal granularity = new BigDecimal(haveAmount.divide(gcd));
if (wantAssetData.getIsDivisible())
BigDecimal granularity = new BigDecimal(returnAmount.divide(gcd));
if (isAmountAssetDivisible)
granularity = granularity.movePointLeft(8);
// Return
return granularity;
}
/**
* Returns price-pair in string form.
* <p>
* e.g. <tt>"QORA/GOLD"</tt>
*/
public String getPricePair() throws DataException {
if (cachedPricePair == null)
calcPricePair();
return cachedPricePair;
}
/** Calculate price pair. (e.g. QORA/GOLD)
* <p>
* Under 'new' pricing scheme, lowest-assetID asset is first,
* so if QORA has assetID 0 and GOLD has assetID 10, then
* the pricing pair is QORA/GOLD.
* <p>
* This means the "amount" fields are expressed in terms
* of the higher-assetID asset. (e.g. GOLD)
*/
private void calcPricePair() throws DataException {
AssetData haveAssetData = getHaveAsset();
AssetData wantAssetData = getWantAsset();
if (isOurOrderNewPricing && haveAssetId > wantAssetId)
cachedPricePair = wantAssetData.getName() + "/" + haveAssetData.getName();
else
cachedPricePair = haveAssetData.getName() + "/" + wantAssetData.getName();
}
private BigDecimal calcHaveAssetCommittment() {
BigDecimal committedCost = this.orderData.getAmount();
// If 'new' pricing and "amount" is in want-asset then we need to convert
if (isOurOrderNewPricing && haveAssetId < wantAssetId)
committedCost = committedCost.multiply(this.orderData.getPrice()).setScale(8, RoundingMode.HALF_UP);
return committedCost;
}
// Navigation
public List<TradeData> getTrades() throws DataException {
return this.repository.getAssetRepository().getOrdersTrades(this.orderData.getOrderId());
}
public AssetData getHaveAsset() throws DataException {
if (cachedHaveAssetData == null)
cachedHaveAssetData = this.repository.getAssetRepository().fromAssetId(haveAssetId);
return cachedHaveAssetData;
}
public AssetData getWantAsset() throws DataException {
if (cachedWantAssetData == null)
cachedWantAssetData = this.repository.getAssetRepository().fromAssetId(wantAssetId);
return cachedWantAssetData;
}
/**
* Returns AssetData for asset in effect for "amount" field.
* <p>
* For 'old' pricing, this is the have-asset.<br>
* For 'new' pricing, this is the asset with highest assetID.
*/
public AssetData getAmountAsset() throws DataException {
if (isOurOrderNewPricing && wantAssetId > haveAssetId)
return getWantAsset();
else
return getHaveAsset();
}
/**
* Returns AssetData for other (return) asset traded.
* <p>
* For 'old' pricing, this is the want-asset.<br>
* For 'new' pricing, this is the asset with lowest assetID.
*/
public AssetData getReturnAsset() throws DataException {
if (isOurOrderNewPricing && haveAssetId < wantAssetId)
return getHaveAsset();
else
return getWantAsset();
}
// Processing
private void logOrder(String orderPrefix, boolean isMatchingNotInitial, OrderData orderData) throws DataException {
private void logOrder(String orderPrefix, boolean isOurOrder, OrderData orderData) throws DataException {
// Avoid calculations if possible
if (LOGGER.getLevel().isMoreSpecificThan(Level.DEBUG))
return;
final String weThey = isMatchingNotInitial ? "They" : "We";
final String weThey = isOurOrder ? "We" : "They";
final String ourTheir = isOurOrder ? "Our" : "Their";
// NOTE: the following values are specific to passed orderData, not the same as class instance values!
final boolean isOrderNewAssetPricing = orderData.getTimestamp() >= BlockChain.getInstance().getNewAssetPricingTimestamp();
final long haveAssetId = orderData.getHaveAssetId();
final long wantAssetId = orderData.getWantAssetId();
AssetData haveAssetData = this.repository.getAssetRepository().fromAssetId(orderData.getHaveAssetId());
AssetData wantAssetData = this.repository.getAssetRepository().fromAssetId(orderData.getWantAssetId());
final AssetData haveAssetData = this.repository.getAssetRepository().fromAssetId(haveAssetId);
final AssetData wantAssetData = this.repository.getAssetRepository().fromAssetId(wantAssetId);
LOGGER.debug(String.format("%s %s", orderPrefix, HashCode.fromBytes(orderData.getOrderId()).toString()));
final long amountAssetId = (isOurOrderNewPricing && wantAssetId > haveAssetId) ? wantAssetId : haveAssetId;
final long returnAssetId = (isOurOrderNewPricing && haveAssetId < wantAssetId) ? haveAssetId : wantAssetId;
LOGGER.trace(String.format("%s have: %s %s", weThey, orderData.getAmount().stripTrailingZeros().toPlainString(), haveAssetData.getName()));
final AssetData amountAssetData = this.repository.getAssetRepository().fromAssetId(amountAssetId);
final AssetData returnAssetData = this.repository.getAssetRepository().fromAssetId(returnAssetId);
LOGGER.trace(String.format("%s want at least %s %s per %s (minimum %s %s total)", weThey,
orderData.getUnitPrice().toPlainString(), wantAssetData.getName(), haveAssetData.getName(),
orderData.getWantAmount().stripTrailingZeros().toPlainString(), wantAssetData.getName()));
LOGGER.debug(String.format("%s %s", orderPrefix, Base58.encode(orderData.getOrderId())));
LOGGER.trace(String.format("%s have %s, want %s. '%s' pricing scheme.", weThey, haveAssetData.getName(), wantAssetData.getName(), isOrderNewAssetPricing ? "new" : "old"));
LOGGER.trace(String.format("%s amount: %s (ordered) - %s (fulfilled) = %s %s left", ourTheir,
orderData.getAmount().stripTrailingZeros().toPlainString(),
orderData.getFulfilled().stripTrailingZeros().toPlainString(),
Order.getAmountLeft(orderData).stripTrailingZeros().toPlainString(),
amountAssetData.getName()));
BigDecimal maxReturnAmount = Order.getAmountLeft(orderData).multiply(orderData.getPrice()).setScale(8, RoundingMode.HALF_UP);
LOGGER.trace(String.format("%s price: %s %s (%s %s tradable)", ourTheir,
orderData.getPrice().toPlainString(), getPricePair(),
maxReturnAmount.stripTrailingZeros().toPlainString(), returnAssetData.getName()));
}
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);
AssetData haveAssetData = getHaveAsset();
AssetData wantAssetData = getWantAsset();
/** The asset while working out amount that matches. */
AssetData matchingAssetData = isOurOrderNewPricing ? getAmountAsset() : wantAssetData;
/** The return asset traded if trade completes. */
AssetData returnAssetData = isOurOrderNewPricing ? getReturnAsset() : haveAssetData;
// Subtract asset from creator
// Subtract have-asset from creator
Account creator = new PublicKeyAccount(this.repository, this.orderData.getCreatorPublicKey());
creator.setConfirmedBalance(haveAssetId, creator.getConfirmedBalance(haveAssetId).subtract(this.orderData.getAmount()));
creator.setConfirmedBalance(haveAssetId, creator.getConfirmedBalance(haveAssetId).subtract(this.calcHaveAssetCommittment()));
// Save this order into repository so it's available for matching, possibly by itself
this.repository.getAssetRepository().save(this.orderData);
logOrder("Processing our order", true, this.orderData);
// Fetch corresponding open orders that might potentially match, hence reversed want/have assetIDs.
// Returned orders are sorted with lowest "price" first.
List<OrderData> orders = assetRepository.getOpenOrdersForTrading(wantAssetId, haveAssetId, isOurOrderNewPricing ? this.orderData.getPrice() : null);
LOGGER.trace("Open orders fetched from repository: " + orders.size());
if (orders.isEmpty())
return;
// Attempt to match orders
/*
* Our order example ("old"):
* Potential matching order example ("old"):
*
* haveAssetId=[GOLD], amount=10,000, wantAssetId=[QORA], price=0.002
* Our order:
* haveAssetId=[GOLD], wantAssetId=0 (QORA), amount=40 (GOLD), price=486 (QORA/GOLD)
* This translates to "we have 40 GOLD and want QORA at a price of 486 QORA per GOLD"
* If our order matched, we'd end up with 40 * 486 = 19,440 QORA.
*
* This translates to "we have 10,000 GOLD and want to buy QORA at a price of 0.002 QORA per GOLD"
* Their order:
* haveAssetId=0 (QORA), wantAssetId=[GOLD], amount=20,000 (QORA), price=0.00205761 (GOLD/QORA)
* This translates to "they have 20,000 QORA and want GOLD at a price of 0.00205761 GOLD per QORA"
*
* So if our order matched, we'd end up with 10,000 * 0.002 = 20 QORA, essentially costing 1/0.002 = 500 GOLD each.
* Their price, converted into 'our' units of QORA/GOLD, is: 1 / 0.00205761 = 486.00074844 QORA/GOLD.
* This is better than our requested 486 QORA/GOLD so this order matches.
*
* So 500 GOLD [each] is our (selling) unit price and want-amount is 20 QORA.
* Using their price, we end up with 40 * 486.00074844 = 19440.02993760 QORA. They end up with 40 GOLD.
*
* Another example (showing representation error and hence move to "new" pricing):
* haveAssetId=[QORA], amount=24, wantAssetId=[GOLD], price=0.08333333
* unit price: 12.00000048 GOLD, want-amount: 1.9999992 GOLD
* If their order had 19,440 QORA left, only 19,440 * 0.00205761 = 39.99993840 GOLD would be traded.
*/
/*
* Our order example ("new"):
* Potential matching order example ("new"):
*
* haveAssetId=[GOLD], amount=10,000, wantAssetId=0 (QORA), want-amount=20
* Our order:
* haveAssetId=[GOLD], wantAssetId=0 (QORA), amount=40 (GOLD), price=486 (QORA/GOLD)
* This translates to "we have 40 GOLD and want QORA at a price of 486 QORA per GOLD"
* If our order matched, we'd end up with 19,440 QORA at a cost of 19,440 / 486 = 40 GOLD.
*
* This translates to "we have 10,000 GOLD and want to buy 20 QORA"
* Their order:
* haveAssetId=0 (QORA), wantAssetId=[GOLD], amount=40 (GOLD), price=486.00074844 (QORA/GOLD)
* This translates to "they have QORA and want GOLD at a price of 486.00074844 QORA per GOLD"
*
* So if our order matched, we'd end up with 20 QORA, essentially costing 10,000 / 20 = 500 GOLD each.
* Their price is better than our requested 486 QORA/GOLD so this order matches.
*
* So 500 GOLD [each] is our (selling) unit price and want-amount is 20 QORA.
* Using their price, we end up with 40 * 486.00074844 = 19440.02993760 QORA. They end up with 40 GOLD.
*
* Another example:
* haveAssetId=[QORA], amount=24, wantAssetId=[GOLD], want-amount=2
* unit price: 12.00000000 GOLD, want-amount: 2.00000000 GOLD
* If their order only had 36 GOLD left, only 36 * 486.00074844 = 17496.02694384 QORA would be traded.
*/
logOrder("Processing our order", false, this.orderData);
// 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);
LOGGER.trace("Open orders fetched from repository: " + orders.size());
BigDecimal ourPrice = this.orderData.getPrice();
if (orders.isEmpty())
return;
for (OrderData theirOrderData : orders) {
logOrder("Considering order", false, theirOrderData);
// Attempt to match orders
// Not used:
// boolean isTheirOrderNewAssetPricing = theirOrderData.getTimestamp() >= BlockChain.getInstance().getNewAssetPricingTimestamp();
BigDecimal ourUnitPrice = this.orderData.getUnitPrice();
LOGGER.trace(String.format("Our minimum price: %s %s per %s", ourUnitPrice.toPlainString(), wantAssetData.getName(), haveAssetData.getName()));
// Determine their order price
BigDecimal theirPrice;
for (OrderData theirOrderData : orders) {
logOrder("Considering order", true, theirOrderData);
/*
* Potential matching order example ("old"):
*
* 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 unit price and maximum amount is 19,440 GOLD.
*/
/*
* Potential matching order example ("new"):
*
* haveAssetId=0 (QORA), amount=40, wantAssetId=[GOLD], price=19,440
*
* This translates to "we have 40 QORA and want to buy 19,440 GOLD"
*
* So if their order matched, they'd end up with 19,440 GOLD, essentially costing 40 / 19,440 = 0.00205761 QORA each.
*
* So 0.00205761 QORA [each] is their unit price and maximum amount is 19,440 GOLD.
*/
boolean isTheirOrderNewAssetPricing = theirOrderData.getTimestamp() >= BlockChain.getInstance().getNewAssetPricingTimestamp();
BigDecimal theirBuyingPrice = BigDecimal.ONE.setScale(isTheirOrderNewAssetPricing ? Order.BD_PRICE_STORAGE_SCALE : 8).divide(theirOrderData.getUnitPrice(), RoundingMode.DOWN);
LOGGER.trace(String.format("Their price: %s %s per %s", theirBuyingPrice.toPlainString(), wantAssetData.getName(), haveAssetData.getName()));
if (isOurOrderNewPricing) {
// Pricing units are the same way round for both orders, so no conversion needed.
// Orders under 'old' pricing have been converted during repository update.
theirPrice = theirOrderData.getPrice();
LOGGER.trace(String.format("Their price: %s %s", theirPrice.toPlainString(), getPricePair()));
} else {
// If our order is 'old' pricing then all other existing orders must be 'old' pricing too
// Their order pricing will be inverted, so convert
theirPrice = BigDecimal.ONE.setScale(8).divide(theirOrderData.getPrice(), RoundingMode.DOWN);
LOGGER.trace(String.format("Their price: %s %s per %s", theirPrice.toPlainString(), wantAssetData.getName(), haveAssetData.getName()));
}
// If their buyingPrice is less than what we're willing to accept then we're done as prices only get worse as we iterate through list of orders
if (theirBuyingPrice.compareTo(ourUnitPrice) < 0)
if (theirPrice.compareTo(ourPrice) < 0)
break;
// Calculate how many want-asset we could buy at their price
BigDecimal ourMaxWantAmount = this.getAmountLeft().multiply(theirBuyingPrice).setScale(8, RoundingMode.DOWN);
LOGGER.trace("ourMaxWantAmount (max we could buy at their price): " + ourMaxWantAmount.stripTrailingZeros().toPlainString() + " " + wantAssetData.getName());
if (isTheirOrderNewAssetPricing) {
ourMaxWantAmount = ourMaxWantAmount.max(this.getAmountLeft().divide(theirOrderData.getUnitPrice(), RoundingMode.DOWN).setScale(8, RoundingMode.DOWN));
LOGGER.trace("ourMaxWantAmount (max we could buy at their price) using inverted calculation: " + ourMaxWantAmount.stripTrailingZeros().toPlainString() + " " + wantAssetData.getName());
}
// Calculate how much we could buy at their price.
BigDecimal ourMaxAmount;
if (isOurOrderNewPricing)
// In 'new' pricing scheme, "amount" is expressed in terms of asset with highest assetID
ourMaxAmount = this.getAmountLeft();
else
// In 'old' pricing scheme, "amount" is expressed in terms of our want-asset.
ourMaxAmount = this.getAmountLeft().multiply(theirPrice).setScale(8, RoundingMode.DOWN);
LOGGER.trace("ourMaxAmount (max we could trade at their price): " + ourMaxAmount.stripTrailingZeros().toPlainString() + " " + matchingAssetData.getName());
// How many want-asset is remaining available in their order. (have-asset amount from their perspective).
BigDecimal theirWantAmountLeft = Order.getAmountLeft(theirOrderData);
LOGGER.trace("theirWantAmountLeft (max amount remaining in their order): " + theirWantAmountLeft.stripTrailingZeros().toPlainString() + " " + wantAssetData.getName());
// How much is remaining available in their order.
BigDecimal theirAmountLeft = Order.getAmountLeft(theirOrderData);
LOGGER.trace("theirAmountLeft (max amount remaining in their order): " + theirAmountLeft.stripTrailingZeros().toPlainString() + " " + matchingAssetData.getName());
// So matchable want-asset amount is the minimum of above two values
BigDecimal matchedWantAmount = ourMaxWantAmount.min(theirWantAmountLeft);
LOGGER.trace("matchedWantAmount: " + matchedWantAmount.stripTrailingZeros().toPlainString() + " " + wantAssetData.getName());
BigDecimal matchedAmount = ourMaxAmount.min(theirAmountLeft);
LOGGER.trace("matchedAmount: " + matchedAmount.stripTrailingZeros().toPlainString() + " " + matchingAssetData.getName());
// If we can't buy anything then try another order
if (matchedWantAmount.compareTo(BigDecimal.ZERO) <= 0)
if (matchedAmount.compareTo(BigDecimal.ZERO) <= 0)
continue;
// Calculate want-amount granularity, based on price and both assets' divisibility, so that have-amount traded is a valid amount (integer or to 8 d.p.)
BigDecimal wantGranularity = calculateAmountGranularity(haveAssetData, wantAssetData, theirOrderData);
LOGGER.trace("wantGranularity (want-asset amount granularity): " + wantGranularity.stripTrailingZeros().toPlainString() + " " + wantAssetData.getName());
// Calculate amount granularity, based on price and both assets' divisibility, so that return-amount traded is a valid value (integer or to 8 d.p.)
BigDecimal granularity = calculateAmountGranularity(matchingAssetData.getIsDivisible(), returnAssetData.getIsDivisible(), theirOrderData.getPrice());
LOGGER.trace("granularity (amount granularity): " + granularity.stripTrailingZeros().toPlainString() + " " + matchingAssetData.getName());
// Reduce matched amount (if need be) to fit granularity
matchedWantAmount = matchedWantAmount.subtract(matchedWantAmount.remainder(wantGranularity));
LOGGER.trace("matchedWantAmount adjusted for granularity: " + matchedWantAmount.stripTrailingZeros().toPlainString() + " " + wantAssetData.getName());
matchedAmount = matchedAmount.subtract(matchedAmount.remainder(granularity));
LOGGER.trace("matchedAmount adjusted for granularity: " + matchedAmount.stripTrailingZeros().toPlainString() + " " + matchingAssetData.getName());
// If we can't buy anything then try another order
if (matchedWantAmount.compareTo(BigDecimal.ZERO) <= 0)
if (matchedAmount.compareTo(BigDecimal.ZERO) <= 0)
continue;
// Safety checks
if (matchedWantAmount.compareTo(Order.getAmountLeft(theirOrderData)) > 0) {
// Safety check
if (!matchingAssetData.getIsDivisible() && matchedAmount.stripTrailingZeros().scale() > 0) {
Account participant = new PublicKeyAccount(this.repository, theirOrderData.getCreatorPublicKey());
String message = String.format("Refusing to trade more %s then requested %s [assetId %d] for %s",
matchedWantAmount.toPlainString(), Order.getAmountLeft(theirOrderData).toPlainString(), wantAssetId, participant.getAddress());
LOGGER.error(message);
throw new DataException(message);
}
if (!wantAssetData.getIsDivisible() && matchedWantAmount.stripTrailingZeros().scale() > 0) {
Account participant = new PublicKeyAccount(this.repository, theirOrderData.getCreatorPublicKey());
String message = String.format("Refusing to trade fractional %s [indivisible assetId %d] for %s",
matchedWantAmount.toPlainString(), wantAssetId, participant.getAddress());
String message = String.format("Refusing to trade fractional %s [indivisible assetID %d] for %s",
matchedAmount.toPlainString(), matchingAssetData.getAssetId(), participant.getAddress());
LOGGER.error(message);
throw new DataException(message);
}
// Trade can go ahead!
// Calculate the total cost to us, in have-asset, based on their price
BigDecimal haveAmountTraded;
if (isTheirOrderNewAssetPricing) {
BigDecimal theirTruncatedPrice = theirBuyingPrice.setScale(Order.BD_PRICE_SCALE, RoundingMode.DOWN);
BigDecimal ourTruncatedPrice = ourUnitPrice.setScale(Order.BD_PRICE_SCALE, RoundingMode.DOWN);
// Calculate the total cost to us, in return-asset, based on their price
BigDecimal returnAmountTraded = matchedAmount.multiply(theirOrderData.getPrice()).setScale(8, RoundingMode.DOWN);
LOGGER.trace("returnAmountTraded: " + returnAmountTraded.stripTrailingZeros().toPlainString() + " " + returnAssetData.getName());
// Safety check
if (theirTruncatedPrice.compareTo(ourTruncatedPrice) < 0) {
String message = String.format("Refusing to trade at worse price %s than our minimum of %s",
theirTruncatedPrice.toPlainString(), ourTruncatedPrice.toPlainString(), creator.getAddress());
LOGGER.error(message);
throw new DataException(message);
}
haveAmountTraded = matchedWantAmount.divide(theirTruncatedPrice, RoundingMode.DOWN).setScale(8, RoundingMode.DOWN);
} else {
haveAmountTraded = matchedWantAmount.multiply(theirOrderData.getUnitPrice()).setScale(8, RoundingMode.DOWN);
}
LOGGER.trace("haveAmountTraded: " + haveAmountTraded.stripTrailingZeros().toPlainString() + " " + haveAssetData.getName());
// Safety checks
if (haveAmountTraded.compareTo(this.getAmountLeft()) > 0) {
String message = String.format("Refusing to trade more %s then requested %s [assetId %d] for %s",
haveAmountTraded.toPlainString(), this.getAmountLeft().toPlainString(), haveAssetId, creator.getAddress());
LOGGER.error(message);
throw new DataException(message);
}
if (!haveAssetData.getIsDivisible() && haveAmountTraded.stripTrailingZeros().scale() > 0) {
String message = String.format("Refusing to trade fractional %s [indivisible assetId %d] for %s",
haveAmountTraded.toPlainString(), haveAssetId, creator.getAddress());
// Safety check
if (!returnAssetData.getIsDivisible() && returnAmountTraded.stripTrailingZeros().scale() > 0) {
String message = String.format("Refusing to trade fractional %s [indivisible assetID %d] for %s",
returnAmountTraded.toPlainString(), returnAssetData.getAssetId(), creator.getAddress());
LOGGER.error(message);
throw new DataException(message);
}
// Construct trade
TradeData tradeData = new TradeData(this.orderData.getOrderId(), theirOrderData.getOrderId(), matchedWantAmount, haveAmountTraded,
BigDecimal tradedWantAmount = (isOurOrderNewPricing && wantAssetId < haveAssetId) ? returnAmountTraded : matchedAmount;
BigDecimal tradedHaveAmount = (isOurOrderNewPricing && haveAssetId > wantAssetId) ? matchedAmount : returnAmountTraded;
TradeData tradeData = new TradeData(this.orderData.getOrderId(), theirOrderData.getOrderId(), tradedWantAmount, tradedHaveAmount,
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(haveAmountTraded));
LOGGER.trace("Updated our order's fulfilled amount to: " + this.orderData.getFulfilled().stripTrailingZeros().toPlainString() + " " + haveAssetData.getName());
LOGGER.trace("Our order's amount remaining: " + this.getAmountLeft().stripTrailingZeros().toPlainString() + " " + haveAssetData.getName());
BigDecimal amountFulfilled = isOurOrderNewPricing ? matchedAmount : returnAmountTraded;
this.orderData.setFulfilled(this.orderData.getFulfilled().add(amountFulfilled));
LOGGER.trace("Updated our order's fulfilled amount to: " + this.orderData.getFulfilled().stripTrailingZeros().toPlainString() + " " + matchingAssetData.getName());
LOGGER.trace("Our order's amount remaining: " + this.getAmountLeft().stripTrailingZeros().toPlainString() + " " + matchingAssetData.getName());
// Continue on to process other open orders if we still have amount left to match
if (this.getAmountLeft().compareTo(BigDecimal.ZERO) <= 0)
@ -355,8 +437,9 @@ public class Order {
// 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()));
creator.setConfirmedBalance(haveAssetId, creator.getConfirmedBalance(haveAssetId).add(this.calcHaveAssetCommittment()));
}
// This is called by CancelOrderTransaction so that an Order can no longer trade

25
src/main/java/org/qora/asset/Trade.java

@ -1,7 +1,10 @@
package org.qora.asset;
import java.math.BigDecimal;
import org.qora.account.Account;
import org.qora.account.PublicKeyAccount;
import org.qora.block.BlockChain;
import org.qora.data.asset.OrderData;
import org.qora.data.asset.TradeData;
import org.qora.repository.AssetRepository;
@ -31,14 +34,19 @@ public class Trade {
// Update corresponding Orders on both sides of trade
OrderData initiatingOrder = assetRepository.fromOrderId(this.tradeData.getInitiator());
initiatingOrder.setFulfilled(initiatingOrder.getFulfilled().add(tradeData.getInitiatorAmount()));
OrderData targetOrder = assetRepository.fromOrderId(this.tradeData.getTarget());
// Under 'new' pricing scheme, "amount" and "fulfilled" are the same asset for both orders
boolean isNewPricing = initiatingOrder.getTimestamp() > BlockChain.getInstance().getNewAssetPricingTimestamp();
BigDecimal newPricingAmount = (initiatingOrder.getHaveAssetId() < initiatingOrder.getWantAssetId()) ? this.tradeData.getTargetAmount() : this.tradeData.getInitiatorAmount();
initiatingOrder.setFulfilled(initiatingOrder.getFulfilled().add(isNewPricing ? newPricingAmount : tradeData.getInitiatorAmount()));
initiatingOrder.setIsFulfilled(Order.isFulfilled(initiatingOrder));
// Set isClosed to true if isFulfilled now true
initiatingOrder.setIsClosed(initiatingOrder.getIsFulfilled());
assetRepository.save(initiatingOrder);
OrderData targetOrder = assetRepository.fromOrderId(this.tradeData.getTarget());
targetOrder.setFulfilled(targetOrder.getFulfilled().add(tradeData.getTargetAmount()));
targetOrder.setFulfilled(targetOrder.getFulfilled().add(isNewPricing ? newPricingAmount : tradeData.getTargetAmount()));
targetOrder.setIsFulfilled(Order.isFulfilled(targetOrder));
// Set isClosed to true if isFulfilled now true
targetOrder.setIsClosed(targetOrder.getIsFulfilled());
@ -59,14 +67,19 @@ public class Trade {
// Revert corresponding Orders on both sides of trade
OrderData initiatingOrder = assetRepository.fromOrderId(this.tradeData.getInitiator());
initiatingOrder.setFulfilled(initiatingOrder.getFulfilled().subtract(tradeData.getInitiatorAmount()));
OrderData targetOrder = assetRepository.fromOrderId(this.tradeData.getTarget());
// Under 'new' pricing scheme, "amount" and "fulfilled" are the same asset for both orders
boolean isNewPricing = initiatingOrder.getTimestamp() > BlockChain.getInstance().getNewAssetPricingTimestamp();
BigDecimal newPricingAmount = (initiatingOrder.getHaveAssetId() < initiatingOrder.getWantAssetId()) ? this.tradeData.getTargetAmount() : this.tradeData.getInitiatorAmount();
initiatingOrder.setFulfilled(initiatingOrder.getFulfilled().subtract(isNewPricing ? newPricingAmount : tradeData.getInitiatorAmount()));
initiatingOrder.setIsFulfilled(Order.isFulfilled(initiatingOrder));
// Set isClosed to false if isFulfilled now false
initiatingOrder.setIsClosed(initiatingOrder.getIsFulfilled());
assetRepository.save(initiatingOrder);
OrderData targetOrder = assetRepository.fromOrderId(this.tradeData.getTarget());
targetOrder.setFulfilled(targetOrder.getFulfilled().subtract(tradeData.getTargetAmount()));
targetOrder.setFulfilled(targetOrder.getFulfilled().subtract(isNewPricing ? newPricingAmount : tradeData.getTargetAmount()));
targetOrder.setIsFulfilled(Order.isFulfilled(targetOrder));
// Set isClosed to false if isFulfilled now false
targetOrder.setIsClosed(targetOrder.getIsFulfilled());

33
src/main/java/org/qora/data/asset/OrderData.java

@ -4,7 +4,6 @@ import java.math.BigDecimal;
import javax.xml.bind.annotation.XmlAccessType;
import javax.xml.bind.annotation.XmlAccessorType;
import javax.xml.bind.annotation.XmlElement;
import io.swagger.v3.oas.annotations.media.Schema;
@ -22,17 +21,13 @@ public class OrderData implements Comparable<OrderData> {
@Schema(description = "asset wanted to receive by order creator")
private long wantAssetId;
@Schema(description = "amount of \"have\" asset to trade")
@Schema(description = "amount of highest-assetID asset to trade")
private BigDecimal amount;
@Schema(name = "return", description = "amount of \"want\" asset to receive")
@XmlElement(name = "return")
private BigDecimal wantAmount;
@Schema(description = "price in lowest-assetID asset / highest-assetID asset")
private BigDecimal price;
@Schema(description = "amount of \"want\" asset to receive per unit of \"have\" asset traded")
private BigDecimal unitPrice;
@Schema(description = "how much \"have\" asset has traded")
@Schema(description = "how much of \"amount\" has traded")
private BigDecimal fulfilled;
private long timestamp;
@ -49,24 +44,22 @@ public class OrderData implements Comparable<OrderData> {
protected OrderData() {
}
public OrderData(byte[] orderId, byte[] creatorPublicKey, long haveAssetId, long wantAssetId, BigDecimal amount, BigDecimal fulfilled, BigDecimal wantAmount,
BigDecimal unitPrice, long timestamp, boolean isClosed, boolean isFulfilled) {
public OrderData(byte[] orderId, byte[] creatorPublicKey, long haveAssetId, long wantAssetId, BigDecimal amount, BigDecimal fulfilled, BigDecimal price, long timestamp, boolean isClosed, boolean isFulfilled) {
this.orderId = orderId;
this.creatorPublicKey = creatorPublicKey;
this.haveAssetId = haveAssetId;
this.wantAssetId = wantAssetId;
this.amount = amount;
this.fulfilled = fulfilled;
this.wantAmount = wantAmount;
this.unitPrice = unitPrice;
this.price = price;
this.timestamp = timestamp;
this.isClosed = isClosed;
this.isFulfilled = isFulfilled;
}
/** Constructs OrderData using typical deserialized network data */
public OrderData(byte[] orderId, byte[] creatorPublicKey, long haveAssetId, long wantAssetId, BigDecimal amount, BigDecimal wantAmount, BigDecimal unitPrice, long timestamp) {
this(orderId, creatorPublicKey, haveAssetId, wantAssetId, amount, BigDecimal.ZERO.setScale(8), wantAmount, unitPrice, timestamp, false, false);
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, false);
}
// Getters/setters
@ -99,12 +92,8 @@ public class OrderData implements Comparable<OrderData> {
this.fulfilled = fulfilled;
}
public BigDecimal getWantAmount() {
return this.wantAmount;
}
public BigDecimal getUnitPrice() {
return this.unitPrice;
public BigDecimal getPrice() {
return this.price;
}
public long getTimestamp() {
@ -130,7 +119,7 @@ public class OrderData implements Comparable<OrderData> {
@Override
public int compareTo(OrderData orderData) {
// Compare using prices
return this.unitPrice.compareTo(orderData.getUnitPrice());
return this.price.compareTo(orderData.getPrice());
}
}

19
src/main/java/org/qora/data/transaction/CreateAssetOrderTransactionData.java

@ -20,11 +20,10 @@ public class CreateAssetOrderTransactionData extends TransactionData {
private long haveAssetId;
@Schema(description = "ID of asset wanted to receive by order creator", example = "0")
private long wantAssetId;
@Schema(description = "amount of \"have\" asset to trade")
@Schema(description = "amount of highest-assetID asset to trade")
private BigDecimal amount;
@Schema(name = "return", description = "amount of \"want\" asset to receive")
@XmlElement(name = "return")
private BigDecimal wantAmount;
@Schema(description = "price in lowest-assetID asset / highest-assetID asset")
private BigDecimal price;
// Constructors
@ -34,18 +33,18 @@ public class CreateAssetOrderTransactionData extends TransactionData {
}
public CreateAssetOrderTransactionData(long timestamp, int txGroupId, byte[] reference, byte[] creatorPublicKey, long haveAssetId, long wantAssetId,
BigDecimal amount, BigDecimal wantAmount, BigDecimal fee, byte[] signature) {
BigDecimal amount, BigDecimal price, BigDecimal fee, byte[] signature) {
super(TransactionType.CREATE_ASSET_ORDER, timestamp, txGroupId, reference, creatorPublicKey, fee, signature);
this.haveAssetId = haveAssetId;
this.wantAssetId = wantAssetId;
this.amount = amount;
this.wantAmount = wantAmount;
this.price = price;
}
public CreateAssetOrderTransactionData(long timestamp, int txGroupId, byte[] reference, byte[] creatorPublicKey, long haveAssetId, long wantAssetId,
BigDecimal amount, BigDecimal wantAmount, BigDecimal fee) {
this(timestamp, txGroupId, reference, creatorPublicKey, haveAssetId, wantAssetId, amount, wantAmount, fee, null);
BigDecimal amount, BigDecimal price, BigDecimal fee) {
this(timestamp, txGroupId, reference, creatorPublicKey, haveAssetId, wantAssetId, amount, price, fee, null);
}
// Getters/Setters
@ -62,8 +61,8 @@ public class CreateAssetOrderTransactionData extends TransactionData {
return this.amount;
}
public BigDecimal getWantAmount() {
return this.wantAmount;
public BigDecimal getPrice() {
return this.price;
}
// Re-expose creatorPublicKey for this transaction type for JAXB

3
src/main/java/org/qora/repository/AssetRepository.java

@ -1,5 +1,6 @@
package org.qora.repository;
import java.math.BigDecimal;
import java.util.List;
import org.qora.data.asset.AssetData;
@ -44,6 +45,8 @@ public interface AssetRepository {
return getOpenOrders(haveAssetId, wantAssetId, null, null, null);
}
public List<OrderData> getOpenOrdersForTrading(long haveAssetId, long wantAssetId, BigDecimal minimumPrice) throws DataException;
public List<OrderData> getAggregatedOpenOrders(long haveAssetId, long wantAssetId, Integer limit, Integer offset, Boolean reverse) throws DataException;
public List<OrderData> getAccountsOrders(byte[] publicKey, Boolean optIsClosed, Boolean optIsFulfilled, Integer limit, Integer offset, Boolean reverse)

109
src/main/java/org/qora/repository/hsqldb/HSQLDBAssetRepository.java

@ -192,7 +192,7 @@ public class HSQLDBAssetRepository implements AssetRepository {
@Override
public OrderData fromOrderId(byte[] orderId) throws DataException {
try (ResultSet resultSet = this.repository.checkedExecute(
"SELECT creator, have_asset_id, want_asset_id, amount, fulfilled, want_amount, unit_price, ordered, is_closed, is_fulfilled 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;
@ -202,13 +202,12 @@ public class HSQLDBAssetRepository implements AssetRepository {
long wantAssetId = resultSet.getLong(3);
BigDecimal amount = resultSet.getBigDecimal(4);
BigDecimal fulfilled = resultSet.getBigDecimal(5);
BigDecimal wantAmount = resultSet.getBigDecimal(6);
BigDecimal unitPrice = resultSet.getBigDecimal(7);
long timestamp = resultSet.getTimestamp(8, Calendar.getInstance(HSQLDBRepository.UTC)).getTime();
boolean isClosed = resultSet.getBoolean(9);
boolean isFulfilled = resultSet.getBoolean(10);
BigDecimal price = resultSet.getBigDecimal(6);
long timestamp = resultSet.getTimestamp(7, Calendar.getInstance(HSQLDBRepository.UTC)).getTime();
boolean isClosed = resultSet.getBoolean(8);
boolean isFulfilled = resultSet.getBoolean(9);
return new OrderData(orderId, creatorPublicKey, haveAssetId, wantAssetId, amount, fulfilled, wantAmount, unitPrice, timestamp, isClosed, isFulfilled);
return new OrderData(orderId, creatorPublicKey, haveAssetId, wantAssetId, amount, fulfilled, price, timestamp, isClosed, isFulfilled);
} catch (SQLException e) {
throw new DataException("Unable to fetch asset order from repository", e);
}
@ -217,8 +216,8 @@ public class HSQLDBAssetRepository implements AssetRepository {
@Override
public List<OrderData> getOpenOrders(long haveAssetId, long wantAssetId, Integer limit, Integer offset,
Boolean reverse) throws DataException {
String sql = "SELECT creator, asset_order_id, amount, fulfilled, want_amount, unit_price, ordered FROM AssetOrders "
+ "WHERE have_asset_id = ? AND want_asset_id = ? AND is_closed = FALSE AND is_fulfilled = FALSE ORDER BY unit_price";
String sql = "SELECT creator, asset_order_id, amount, fulfilled, price, ordered FROM AssetOrders "
+ "WHERE have_asset_id = ? AND want_asset_id = ? AND is_closed = FALSE AND is_fulfilled = FALSE ORDER BY price";
if (reverse != null && reverse)
sql += " DESC";
sql += ", ordered";
@ -237,14 +236,13 @@ public class HSQLDBAssetRepository implements AssetRepository {
byte[] orderId = resultSet.getBytes(2);
BigDecimal amount = resultSet.getBigDecimal(3);
BigDecimal fulfilled = resultSet.getBigDecimal(4);
BigDecimal wantAmount = resultSet.getBigDecimal(5);
BigDecimal unitPrice = resultSet.getBigDecimal(6);
long timestamp = resultSet.getTimestamp(7, Calendar.getInstance(HSQLDBRepository.UTC)).getTime();
BigDecimal price = resultSet.getBigDecimal(5);
long timestamp = resultSet.getTimestamp(6, Calendar.getInstance(HSQLDBRepository.UTC)).getTime();
boolean isClosed = false;
boolean isFulfilled = false;
OrderData order = new OrderData(orderId, creatorPublicKey, haveAssetId, wantAssetId, amount, fulfilled,
wantAmount, unitPrice, timestamp, isClosed, isFulfilled);
price, timestamp, isClosed, isFulfilled);
orders.add(order);
} while (resultSet.next());
@ -254,11 +252,53 @@ public class HSQLDBAssetRepository implements AssetRepository {
}
}
@Override
public List<OrderData> getOpenOrdersForTrading(long haveAssetId, long wantAssetId, BigDecimal minimumPrice) throws DataException {
Object[] bindParams;
String sql = "SELECT creator, asset_order_id, amount, fulfilled, price, ordered FROM AssetOrders "
+ "WHERE have_asset_id = ? AND want_asset_id = ? AND NOT is_closed AND NOT is_fulfilled ";
if (minimumPrice != null) {
sql += "AND price >= ? ";
bindParams = new Object[] {haveAssetId, wantAssetId, minimumPrice};
} else {
bindParams = new Object[] {haveAssetId, wantAssetId};
}
sql += "ORDER BY price DESC, ordered";
List<OrderData> orders = new ArrayList<OrderData>();
try (ResultSet resultSet = this.repository.checkedExecute(sql, bindParams)) {
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, Calendar.getInstance(HSQLDBRepository.UTC)).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 open asset orders for trading from repository", e);
}
}
@Override
public List<OrderData> getAggregatedOpenOrders(long haveAssetId, long wantAssetId, Integer limit, Integer offset,
Boolean reverse) throws DataException {
String sql = "SELECT unit_price, SUM(amount - fulfilled), MAX(ordered) FROM AssetOrders "
+ "WHERE have_asset_id = ? AND want_asset_id = ? AND is_closed = FALSE AND is_fulfilled = FALSE GROUP BY unit_price ORDER BY unit_price";
String sql = "SELECT price, SUM(amount - fulfilled), MAX(ordered) FROM AssetOrders "
+ "WHERE have_asset_id = ? AND want_asset_id = ? AND is_closed = FALSE AND is_fulfilled = FALSE GROUP BY price ORDER BY price";
if (reverse != null && reverse)
sql += " DESC";
sql += HSQLDBRepository.limitOffsetSql(limit, offset);
@ -270,12 +310,12 @@ public class HSQLDBAssetRepository implements AssetRepository {
return orders;
do {
BigDecimal unitPrice = resultSet.getBigDecimal(1);
BigDecimal price = resultSet.getBigDecimal(1);
BigDecimal totalUnfulfilled = resultSet.getBigDecimal(2);
long timestamp = resultSet.getTimestamp(3).getTime();
OrderData order = new OrderData(null, null, haveAssetId, wantAssetId, totalUnfulfilled, BigDecimal.ZERO,
BigDecimal.ZERO, unitPrice, timestamp, false, false);
price, timestamp, false, false);
orders.add(order);
} while (resultSet.next());
@ -288,7 +328,7 @@ public class HSQLDBAssetRepository implements AssetRepository {
@Override
public List<OrderData> getAccountsOrders(byte[] publicKey, Boolean optIsClosed, Boolean optIsFulfilled,
Integer limit, Integer offset, Boolean reverse) throws DataException {
String sql = "SELECT asset_order_id, have_asset_id, want_asset_id, amount, fulfilled, want_amount, unit_price, ordered, is_closed, is_fulfilled "
String sql = "SELECT asset_order_id, have_asset_id, want_asset_id, amount, fulfilled, price, ordered, is_closed, is_fulfilled "
+ "FROM AssetOrders WHERE creator = ?";
if (optIsClosed != null)
sql += " AND is_closed = " + (optIsClosed ? "TRUE" : "FALSE");
@ -311,14 +351,13 @@ public class HSQLDBAssetRepository implements AssetRepository {
long wantAssetId = resultSet.getLong(3);
BigDecimal amount = resultSet.getBigDecimal(4);
BigDecimal fulfilled = resultSet.getBigDecimal(5);
BigDecimal wantAmount = resultSet.getBigDecimal(6);
BigDecimal unitPrice = resultSet.getBigDecimal(7);
long timestamp = resultSet.getTimestamp(8, Calendar.getInstance(HSQLDBRepository.UTC)).getTime();
boolean isClosed = resultSet.getBoolean(9);
boolean isFulfilled = resultSet.getBoolean(10);
OrderData order = new OrderData(orderId, publicKey, haveAssetId, wantAssetId, amount, fulfilled, wantAmount,
unitPrice, timestamp, isClosed, isFulfilled);
BigDecimal price = resultSet.getBigDecimal(6);
long timestamp = resultSet.getTimestamp(7, Calendar.getInstance(HSQLDBRepository.UTC)).getTime();
boolean isClosed = resultSet.getBoolean(8);
boolean isFulfilled = resultSet.getBoolean(9);
OrderData order = new OrderData(orderId, publicKey, haveAssetId, wantAssetId, amount, fulfilled,
price, timestamp, isClosed, isFulfilled);
orders.add(order);
} while (resultSet.next());
@ -331,7 +370,7 @@ public class HSQLDBAssetRepository implements AssetRepository {
@Override
public List<OrderData> getAccountsOrders(byte[] publicKey, long haveAssetId, long wantAssetId, Boolean optIsClosed,
Boolean optIsFulfilled, Integer limit, Integer offset, Boolean reverse) throws DataException {
String sql = "SELECT asset_order_id, amount, fulfilled, want_amount, unit_price, ordered, is_closed, is_fulfilled "
String sql = "SELECT asset_order_id, amount, fulfilled, price, ordered, is_closed, is_fulfilled "
+ "FROM AssetOrders WHERE creator = ? AND have_asset_id = ? AND want_asset_id = ?";
if (optIsClosed != null)
sql += " AND is_closed = " + (optIsClosed ? "TRUE" : "FALSE");
@ -352,14 +391,13 @@ public class HSQLDBAssetRepository implements AssetRepository {
byte[] orderId = resultSet.getBytes(1);
BigDecimal amount = resultSet.getBigDecimal(2);
BigDecimal fulfilled = resultSet.getBigDecimal(3);
BigDecimal wantAmount = resultSet.getBigDecimal(4);
BigDecimal unitPrice = resultSet.getBigDecimal(5);
long timestamp = resultSet.getTimestamp(6, Calendar.getInstance(HSQLDBRepository.UTC)).getTime();
boolean isClosed = resultSet.getBoolean(7);
boolean isFulfilled = resultSet.getBoolean(8);
BigDecimal price = resultSet.getBigDecimal(4);
long timestamp = resultSet.getTimestamp(5, Calendar.getInstance(HSQLDBRepository.UTC)).getTime();
boolean isClosed = resultSet.getBoolean(6);
boolean isFulfilled = resultSet.getBoolean(7);
OrderData order = new OrderData(orderId, publicKey, haveAssetId, wantAssetId, amount, fulfilled, wantAmount,
unitPrice, timestamp, isClosed, isFulfilled);
OrderData order = new OrderData(orderId, publicKey, haveAssetId, wantAssetId, amount, fulfilled,
price, timestamp, isClosed, isFulfilled);
orders.add(order);
} while (resultSet.next());
@ -376,8 +414,7 @@ public class HSQLDBAssetRepository implements AssetRepository {
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("want_amount", orderData.getWantAmount()).bind("unit_price", orderData.getUnitPrice())
.bind("ordered", new Timestamp(orderData.getTimestamp()))
.bind("price", orderData.getPrice()).bind("ordered", new Timestamp(orderData.getTimestamp()))
.bind("is_closed", orderData.getIsClosed()).bind("is_fulfilled", orderData.getIsFulfilled());
try {

30
src/main/java/org/qora/repository/hsqldb/HSQLDBDatabaseUpdates.java

@ -7,6 +7,7 @@ import java.sql.Statement;
import org.apache.logging.log4j.LogManager;
import org.apache.logging.log4j.Logger;
import org.qora.block.BlockChain;
public class HSQLDBDatabaseUpdates {
@ -655,6 +656,35 @@ public class HSQLDBDatabaseUpdates {
stmt.execute("ALTER TABLE CreateAssetOrderTransactions ALTER COLUMN price RENAME TO want_amount");
break;
case 42:
// New asset pricing #2
/*
* Use "price" (discard want-amount) but enforce pricing units in one direction
* to avoid all the reciprocal and round issues.
*/
stmt.execute("ALTER TABLE CreateAssetOrderTransactions ALTER COLUMN want_amount RENAME TO price");
stmt.execute("ALTER TABLE AssetOrders DROP COLUMN want_amount");
stmt.execute("ALTER TABLE AssetOrders ALTER COLUMN unit_price RENAME TO price");
stmt.execute("ALTER TABLE AssetOrders ALTER COLUMN price QoraAmount");
/*
* Normalize any 'old' orders to 'new' pricing.
* We must do this so that requesting open orders can be sorted by price.
*/
// Make sure new asset pricing timestamp (used below) is UTC
stmt.execute("SET TIME ZONE INTERVAL '0:00' HOUR TO MINUTE");
// Normalize amount/fulfilled to asset with highest assetID, BEFORE price correction
stmt.execute("UPDATE AssetOrders SET amount = amount * price, fulfilled = fulfilled * price "
+ "WHERE ordered < timestamp(" + BlockChain.getInstance().getNewAssetPricingTimestamp() + ") "
+ "AND have_asset_id < want_asset_id");
// Normalize price into lowest-assetID/highest-assetID price-pair, e.g. QORA/asset100
// Note: HSQLDB uses BigDecimal's dividend.divide(divisor, RoundingMode.DOWN) too
stmt.execute("UPDATE AssetOrders SET price = CAST(1 AS QoraAmount) / price "
+ "WHERE ordered < timestamp(" + BlockChain.getInstance().getNewAssetPricingTimestamp() + ") "
+ "AND have_asset_id < want_asset_id");
// Revert time zone change above
stmt.execute("SET TIME ZONE LOCAL");
break;
default:
// nothing to do
return false;

8
src/main/java/org/qora/repository/hsqldb/transaction/HSQLDBCreateAssetOrderTransactionRepository.java

@ -18,16 +18,16 @@ public class HSQLDBCreateAssetOrderTransactionRepository extends HSQLDBTransacti
TransactionData fromBase(long timestamp, int txGroupId, byte[] reference, byte[] creatorPublicKey, BigDecimal fee, byte[] signature) throws DataException {
try (ResultSet resultSet = this.repository
.checkedExecute("SELECT have_asset_id, amount, want_asset_id, want_amount FROM CreateAssetOrderTransactions WHERE signature = ?", signature)) {
.checkedExecute("SELECT have_asset_id, amount, want_asset_id, price FROM CreateAssetOrderTransactions WHERE signature = ?", signature)) {
if (resultSet == null)
return null;
long haveAssetId = resultSet.getLong(1);
BigDecimal amount = resultSet.getBigDecimal(2);
long wantAssetId = resultSet.getLong(3);
BigDecimal wantAmount = resultSet.getBigDecimal(4);
BigDecimal price = resultSet.getBigDecimal(4);
return new CreateAssetOrderTransactionData(timestamp, txGroupId, reference, creatorPublicKey, haveAssetId, wantAssetId, amount, wantAmount, fee, signature);
return new CreateAssetOrderTransactionData(timestamp, txGroupId, reference, creatorPublicKey, haveAssetId, wantAssetId, amount, price, fee, signature);
} catch (SQLException e) {
throw new DataException("Unable to fetch create order transaction from repository", e);
}
@ -41,7 +41,7 @@ public class HSQLDBCreateAssetOrderTransactionRepository extends HSQLDBTransacti
saveHelper.bind("signature", createOrderTransactionData.getSignature()).bind("creator", createOrderTransactionData.getCreatorPublicKey())
.bind("have_asset_id", createOrderTransactionData.getHaveAssetId()).bind("amount", createOrderTransactionData.getAmount())
.bind("want_asset_id", createOrderTransactionData.getWantAssetId()).bind("want_amount", createOrderTransactionData.getWantAmount());
.bind("want_asset_id", createOrderTransactionData.getWantAssetId()).bind("price", createOrderTransactionData.getPrice());
try {
saveHelper.execute(this.repository);

90
src/main/java/org/qora/transaction/CreateAssetOrderTransaction.java

@ -1,7 +1,6 @@
package org.qora.transaction;
import java.math.BigDecimal;
import java.math.RoundingMode;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.List;
@ -83,7 +82,7 @@ public class CreateAssetOrderTransaction extends Transaction {
return ValidationResult.NEGATIVE_AMOUNT;
// Check price is positive
if (createOrderTransactionData.getWantAmount().compareTo(BigDecimal.ZERO) <= 0)
if (createOrderTransactionData.getPrice().compareTo(BigDecimal.ZERO) <= 0)
return ValidationResult.NEGATIVE_PRICE;
// Check fee is positive
@ -104,19 +103,63 @@ public class CreateAssetOrderTransaction extends Transaction {
Account creator = getCreator();
// Check reference is correct
if (!Arrays.equals(creator.getLastReference(), createOrderTransactionData.getReference()))
return ValidationResult.INVALID_REFERENCE;
boolean isNewPricing = createOrderTransactionData.getTimestamp() >= BlockChain.getInstance().getNewAssetPricingTimestamp();
BigDecimal committedCost;
BigDecimal maxOtherAmount;
if (isNewPricing) {
/*
* This is different under "new" pricing scheme as "amount" might be either have-asset or want-asset,
* whichever has the highest assetID.
*
* e.g. with assetID 11 "GOLD":
* haveAssetId: 0 (QORA), wantAssetId: 11 (GOLD), amount: 123 (GOLD), price: 400 (QORA/GOLD)
* stake 49200 QORA, return 123 GOLD
*
* haveAssetId: 11 (GOLD), wantAssetId: 0 (QORA), amount: 123 (GOLD), price: 400 (QORA/GOLD)
* stake 123 GOLD, return 49200 QORA
*/
boolean isAmountWantAsset = haveAssetId < wantAssetId;
if (isAmountWantAsset) {
// have/commit 49200 QORA, want/return 123 GOLD
committedCost = createOrderTransactionData.getAmount().multiply(createOrderTransactionData.getPrice());
maxOtherAmount = createOrderTransactionData.getAmount();
} else {
// have/commit 123 GOLD, want/return 49200 QORA
committedCost = createOrderTransactionData.getAmount();
maxOtherAmount = createOrderTransactionData.getAmount().multiply(createOrderTransactionData.getPrice());
}
} else {
/*
* Under "old" pricing scheme, "amount" is always have-asset and price is always want-per-have.
*
* e.g. with assetID 11 "GOLD":
* haveAssetId: 0 (QORA), wantAssetId: 11 (GOLD), amount: 49200 (QORA), price: 0.00250000 (GOLD/QORA)
* haveAssetId: 11 (GOLD), wantAssetId: 0 (QORA), amount: 123 (GOLD), price: 400 (QORA/GOLD)
*/
committedCost = createOrderTransactionData.getAmount();
maxOtherAmount = createOrderTransactionData.getAmount().multiply(createOrderTransactionData.getPrice());
}
// Check amount is integer if amount's asset is not divisible
if (!haveAssetData.getIsDivisible() && committedCost.stripTrailingZeros().scale() > 0)
return ValidationResult.INVALID_AMOUNT;
// Check total return from fulfilled order would be integer if return's asset is not divisible
if (!wantAssetData.getIsDivisible() && maxOtherAmount.stripTrailingZeros().scale() > 0)
return ValidationResult.INVALID_RETURN;
// 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())) < 0)
if (creator.getConfirmedBalance(Asset.QORA).compareTo(committedCost.add(createOrderTransactionData.getFee())) < 0)
return ValidationResult.NO_BALANCE;
} else {
// Check creator has enough funds for amount in whatever asset
if (creator.getConfirmedBalance(haveAssetId).compareTo(createOrderTransactionData.getAmount()) < 0)
if (creator.getConfirmedBalance(haveAssetId).compareTo(committedCost) < 0)
return ValidationResult.NO_BALANCE;
// Check creator has enough funds for fee in QORA
@ -126,21 +169,9 @@ public class CreateAssetOrderTransaction extends Transaction {
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 (createOrderTransactionData.getTimestamp() >= BlockChain.getInstance().getNewAssetPricingTimestamp()) {
// "new" asset pricing
if (!wantAssetData.getIsDivisible() && createOrderTransactionData.getWantAmount().stripTrailingZeros().scale() > 0)
return ValidationResult.INVALID_RETURN;
} else {
// "old" asset pricing
if (!wantAssetData.getIsDivisible()
&& createOrderTransactionData.getAmount().multiply(createOrderTransactionData.getWantAmount()).stripTrailingZeros().scale() > 0)
return ValidationResult.INVALID_RETURN;
}
// Check reference is correct
if (!Arrays.equals(creator.getLastReference(), createOrderTransactionData.getReference()))
return ValidationResult.INVALID_REFERENCE;
return ValidationResult.OK;
}
@ -161,22 +192,9 @@ public class CreateAssetOrderTransaction extends Transaction {
// Order Id is transaction's signature
byte[] orderId = createOrderTransactionData.getSignature();
BigDecimal wantAmount;
BigDecimal unitPrice;
if (createOrderTransactionData.getTimestamp() >= BlockChain.getInstance().getNewAssetPricingTimestamp()) {
// "new" asset pricing: want-amount provided, unit price to be calculated
wantAmount = createOrderTransactionData.getWantAmount();
unitPrice = wantAmount.setScale(Order.BD_PRICE_STORAGE_SCALE).divide(createOrderTransactionData.getAmount().setScale(Order.BD_PRICE_STORAGE_SCALE), RoundingMode.DOWN);
} else {
// "old" asset pricing: selling unit price provided, want-amount to be calculated
wantAmount = createOrderTransactionData.getAmount().multiply(createOrderTransactionData.getWantAmount());
unitPrice = createOrderTransactionData.getWantAmount(); // getWantAmount() was getPrice() in the "old" pricing scheme
}
// Process the order itself
OrderData orderData = new OrderData(orderId, createOrderTransactionData.getCreatorPublicKey(), createOrderTransactionData.getHaveAssetId(),
createOrderTransactionData.getWantAssetId(), createOrderTransactionData.getAmount(), wantAmount, unitPrice,
createOrderTransactionData.getWantAssetId(), createOrderTransactionData.getAmount(), createOrderTransactionData.getPrice(),
createOrderTransactionData.getTimestamp());
new Order(this.repository, orderData).process();

8
src/main/java/org/qora/transform/transaction/CreateAssetOrderTransactionTransformer.java

@ -35,7 +35,7 @@ public class CreateAssetOrderTransactionTransformer extends TransactionTransform
layout.add("ID of asset of offer", TransformationType.LONG);
layout.add("ID of asset wanted", TransformationType.LONG);
layout.add("amount of asset on offer", TransformationType.ASSET_QUANTITY);
layout.add("amount of wanted asset", TransformationType.ASSET_QUANTITY);
layout.add("trade price in (lowest-assetID asset)/(highest-assetID asset)", TransformationType.ASSET_QUANTITY);
layout.add("fee", TransformationType.AMOUNT);
layout.add("signature", TransformationType.SIGNATURE);
}
@ -58,7 +58,6 @@ public class CreateAssetOrderTransactionTransformer extends TransactionTransform
BigDecimal amount = Serialization.deserializeBigDecimal(byteBuffer, AMOUNT_LENGTH);
// Under "new" asset pricing, this is actually the want-amount
BigDecimal price = Serialization.deserializeBigDecimal(byteBuffer, AMOUNT_LENGTH);
BigDecimal fee = Serialization.deserializeBigDecimal(byteBuffer);
@ -87,8 +86,7 @@ public class CreateAssetOrderTransactionTransformer extends TransactionTransform
Serialization.serializeBigDecimal(bytes, createOrderTransactionData.getAmount(), AMOUNT_LENGTH);
// Under "new" asset pricing, this is actually the want-amount
Serialization.serializeBigDecimal(bytes, createOrderTransactionData.getWantAmount(), AMOUNT_LENGTH);
Serialization.serializeBigDecimal(bytes, createOrderTransactionData.getPrice(), AMOUNT_LENGTH);
Serialization.serializeBigDecimal(bytes, createOrderTransactionData.getFee());
@ -128,7 +126,7 @@ public class CreateAssetOrderTransactionTransformer extends TransactionTransform
Serialization.serializeBigDecimal(bytes, createOrderTransactionData.getAmount(), AMOUNT_LENGTH);
// This is the crucial difference
Serialization.serializeBigDecimal(bytes, createOrderTransactionData.getWantAmount(), FEE_LENGTH);
Serialization.serializeBigDecimal(bytes, createOrderTransactionData.getPrice(), FEE_LENGTH);
Serialization.serializeBigDecimal(bytes, createOrderTransactionData.getFee());

2
src/test/java/org/qora/test/TransactionTests.java

@ -990,7 +990,7 @@ public class TransactionTests extends Common {
TradeData tradeData = trades.get(0);
// Check trade has correct values
BigDecimal expectedAmount = amount.divide(originalOrderData.getUnitPrice()).setScale(8);
BigDecimal expectedAmount = amount.divide(originalOrderData.getPrice()).setScale(8);
BigDecimal actualAmount = tradeData.getTargetAmount();
assertTrue(expectedAmount.compareTo(actualAmount) == 0);

85
src/test/java/org/qora/test/assets/GranularityTests.java

@ -0,0 +1,85 @@
package org.qora.test.assets;
import java.math.BigDecimal;
import java.math.RoundingMode;
import org.junit.After;
import org.junit.Before;
import org.junit.Test;
import org.qora.asset.Order;
import org.qora.repository.DataException;
import org.qora.test.common.Common;
public class GranularityTests extends Common {
@Before
public void beforeTest() throws DataException {
Common.useDefaultSettings();
}
@After
public void afterTest() throws DataException {
Common.orphanCheck();
}
/**
* Check granularity adjustment values.
*/
@Test
public void testGranularities() {
// Price 1/12 is rounded down to 0.08333333.
// To keep [divisible] amount * 0.08333333 to nearest 0.00000001 then amounts need to be multiples of 1.00000000.
testGranularity(true, true, "1", "12", "1");
// Any amount * 12 will be valid for divisible asset so granularity is 0.00000001
testGranularity(true, true, "12", "1", "0.00000001");
// Price 1/10 is 0.10000000.
// To keep amount * 0.1 to nearest 1 then amounts need to be multiples of 10.
testGranularity(false, false, "1", "10", "10");
// Price is 50307/123 which is 409
// Any [indivisible] amount * 409 will be valid for divisible asset to granularity is 1
testGranularity(false, false, "50307", "123", "1");
// Price 1/800 is 0.00125000
// Amounts are indivisible so must be integer.
// Return-amounts are divisible and can be fractional.
// So even though amount needs to be multiples of 1.00000000,
// return-amount will always end up being valid.
// Thus at price 0.00125000 we expect granularity to be 1
testGranularity(false, true, "1", "800", "1");
// Price 1/800 is 0.00125000
// Amounts are divisible so can be fractional.
// Return-amounts are indivisible so must be integer.
// So even though amount can be multiples of 0.00000001,
// return-amount needs to be multiples of 1.00000000
// Thus at price 0.00125000 we expect granularity to be 800
testGranularity(true, false, "1", "800", "800");
// Price 800
// Amounts are indivisible so must be integer.
// Return-amounts are divisible so can be fractional.
// So even though amount needs to be multiples of 1.00000000,
// return-amount will always end up being valid.
// Thus at price 800 we expect granularity to be 1
testGranularity(false, true, "800", "1", "1");
// Price 800
// Amounts are divisible so can be fractional.
// Return-amounts are indivisible so must be integer.
// So even though amount can be multiples of 0.00000001,
// return-amount needs to be multiples of 1.00000000
// Thus at price 800 we expect granularity to be 0.00125000
testGranularity(true, false, "800", "1", "0.00125000");
}
private void testGranularity(boolean isAmountAssetDivisible, boolean isReturnAssetDivisible, String dividend, String divisor, String expectedGranularity) {
final BigDecimal price = new BigDecimal(dividend).setScale(8).divide(new BigDecimal(divisor).setScale(8), RoundingMode.DOWN);
BigDecimal granularity = Order.calculateAmountGranularity(isAmountAssetDivisible, isReturnAssetDivisible, price);
assertEqualBigDecimals("Granularity incorrect", new BigDecimal(expectedGranularity), granularity);
}
}

27
src/test/java/org/qora/test/assets/MixedPricingTests.java

@ -0,0 +1,27 @@
package org.qora.test.assets;
import org.junit.After;
import org.junit.Before;
import org.qora.repository.DataException;
import org.qora.test.common.Common;
public class MixedPricingTests extends Common{
@Before
public void beforeTest() throws DataException {
Common.useSettings("test-settings-old-asset.json");
}
@After
public void afterTest() throws DataException {
Common.orphanCheck();
}
/**
* Check order matching between 'old' pricing order and 'new' pricing order.
* <p>
* In this test, the order created under 'old' pricing scheme has
* "amount" in have-asset?
*/
}

349
src/test/java/org/qora/test/assets/NewTradingTests.java

@ -0,0 +1,349 @@
package org.qora.test.assets;
import org.junit.After;
import org.junit.Before;
import org.junit.Test;
import org.qora.asset.Asset;
import org.qora.data.asset.OrderData;
import org.qora.repository.DataException;
import org.qora.repository.Repository;
import org.qora.repository.RepositoryManager;
import org.qora.test.common.AccountUtils;
import org.qora.test.common.AssetUtils;
import org.qora.test.common.Common;
import java.math.BigDecimal;
import java.math.RoundingMode;
import java.util.Map;
public class NewTradingTests extends Common {
@Before
public void beforeTest() throws DataException {
Common.useDefaultSettings();
}
@After
public void afterTest() throws DataException {
Common.orphanCheck();
}
@Test
public void testSimple() throws DataException {
final BigDecimal testAmount = BigDecimal.valueOf(24L).setScale(8);
final BigDecimal price = BigDecimal.valueOf(2L).setScale(8);
final BigDecimal qoraAmount = BigDecimal.valueOf(48L).setScale(8);
// amounts are in test-asset
// prices are in qora/test
final BigDecimal aliceAmount = testAmount;
final BigDecimal alicePrice = price;
final BigDecimal bobAmount = testAmount;
final BigDecimal bobPrice = price;
final BigDecimal aliceCommitment = testAmount;
final BigDecimal bobCommitment = qoraAmount;
final BigDecimal aliceReturn = qoraAmount;
final BigDecimal bobReturn = testAmount;
// alice (target) order: have 'testAmount' test, want qora @ 'price' qora/test (commits testAmount test)
// bob (initiating) order: have qora, want 'testAmount' test @ 'price' qora/test (commits testAmount*price = qoraAmount)
// Alice should be -testAmount, +qoraAmount
// Bob should be -qoraAmount, +testAmount
AssetUtils.genericTradeTest(AssetUtils.testAssetId, Asset.QORA, aliceAmount, alicePrice, bobAmount, bobPrice, aliceCommitment, bobCommitment, aliceReturn, bobReturn);
}
/**
* Check matching of indivisible amounts.
* <p>
* New pricing scheme allows two attempts are calculating matched amount
* to reduce partial-match issues caused by rounding and recurring fractional digits:
* <p>
* <ol>
* <li> amount * round_down(1 / unit price) </li>
* <li> round_down(amount / unit price) </li>
* </ol>
* Alice's price is 12 QORA per ATNL so the ATNL per QORA unit price is 0.08333333...<br>
* Bob wants to spend 24 QORA so:
* <p>
* <ol>
* <li> 24 QORA * (1 / 0.0833333...) = 1.99999999 ATNL </li>
* <li> 24 QORA / 0.08333333.... = 2 ATNL </li>
* </ol>
* The second result is obviously more intuitive as is critical where assets are not divisible,
* like ATNL in this test case.
* <p>
* @see NewTradingTests#testOldNonExactFraction
* @see NewTradingTests#testNonExactFraction
* @throws DataException
*/
@Test
public void testMixedDivisibility() throws DataException {
// Issue indivisible asset
long atnlAssetId;
try (Repository repository = RepositoryManager.getRepository()) {
// Issue indivisible asset
atnlAssetId = AssetUtils.issueAsset(repository, "alice", "ATNL", 100000000L, false);
}
final BigDecimal atnlAmount = BigDecimal.valueOf(2L).setScale(8);
final BigDecimal qoraAmount = BigDecimal.valueOf(24L).setScale(8);
final BigDecimal price = qoraAmount.divide(atnlAmount, RoundingMode.DOWN);
// amounts are in ATNL
// prices are in qora/ATNL
final BigDecimal aliceAmount = atnlAmount;
final BigDecimal alicePrice = price;
final BigDecimal bobAmount = atnlAmount;
final BigDecimal bobPrice = price;
final BigDecimal aliceCommitment = atnlAmount;
final BigDecimal bobCommitment = qoraAmount;
final BigDecimal aliceReturn = qoraAmount;
final BigDecimal bobReturn = atnlAmount;
AssetUtils.genericTradeTest(atnlAssetId, Asset.QORA, aliceAmount, alicePrice, bobAmount, bobPrice, aliceCommitment, bobCommitment, aliceReturn, bobReturn);
}
/**
* Check matching of indivisible amounts (new pricing).
* <p>
* Alice is selling twice as much as Bob wants,
* but at the same [calculated] unit price,
* so Bob's order should fully match.
* <p>
* However, in legacy/"old" mode, the granularity checks
* would prevent this trade.
*/
@Test
public void testIndivisible() throws DataException {
// Issue some indivisible assets
long ragsAssetId;
long richesAssetId;
try (Repository repository = RepositoryManager.getRepository()) {
// Issue indivisible asset
ragsAssetId = AssetUtils.issueAsset(repository, "alice", "rags", 1000000L, false);
// Issue another indivisible asset
richesAssetId = AssetUtils.issueAsset(repository, "bob", "riches", 1000000L, false);
}
// "amount" will be in riches, "price" will be in rags/riches
final BigDecimal ragsAmount = BigDecimal.valueOf(50307L).setScale(8);
final BigDecimal richesAmount = BigDecimal.valueOf(123L).setScale(8);
final BigDecimal price = ragsAmount.divide(richesAmount, RoundingMode.DOWN);
final BigDecimal two = BigDecimal.valueOf(2L);
final BigDecimal aliceAmount = richesAmount.multiply(two).setScale(8);
final BigDecimal alicePrice = price;
final BigDecimal aliceCommitment = aliceAmount.multiply(alicePrice).setScale(8); // rags
final BigDecimal bobAmount = richesAmount;
final BigDecimal bobPrice = price;
final BigDecimal bobCommitment = bobAmount; // riches
final BigDecimal aliceReturn = bobAmount; // riches
final BigDecimal bobReturn = bobAmount.multiply(alicePrice).setScale(8); // rags
AssetUtils.genericTradeTest(ragsAssetId, richesAssetId, aliceAmount, alicePrice, bobAmount, bobPrice, aliceCommitment, bobCommitment, aliceReturn, bobReturn);
}
/**
* Check partial matching of indivisible amounts (new pricing).
* <p>
* Assume both "rags" and "riches" assets are indivisible.
*
* Alice places an order:
* Have rags, want riches, amount 3 riches, price 1 rags/riches
*
* Alice has 1 * 3 = 3 rags subtracted from their rags balance.
*
* Bob places an order:
* Have riches, want rags, amount 8 riches, price 0.25 rags/riches
*
* Bob has 8 riches subtracted from their riches balance.
* Bob expects at least 8 * 0.25 = 2 rags if his order fully completes.
*
* Alice is offering more rags for riches than Bob expects.
* So Alice's order is a match for Bob's, and Alice's order price is used.
*
* Bob wants to trade 8 riches, but Alice only wants to trade 3 riches,
* so the matched amount is 3 riches.
*
* Bob gains 3 * 1 = 3 rags and Alice gains 3 riches.
* Alice's order has 0 riches left (fully completed).
*
* Bob's order has 8 - 3 = 5 riches left.
*
* At Bob's order's price of 0.25 rags/riches,
* it would take 1.25 rags to complete the rest of Bob's order.
* But rags are indivisible so this can't happen at that price.
*
* However, someone could buy at a better price, e.g. 0.4 rags/riches,
* trading 2 rags for 5 riches.
*
* Or Bob could cancel the rest of his order and be refunded 5 riches.
*/
@Test
public void testPartialIndivisible() throws DataException {
// Issue some indivisible assets
long ragsAssetId;
long richesAssetId;
try (Repository repository = RepositoryManager.getRepository()) {
// Issue indivisible asset
ragsAssetId = AssetUtils.issueAsset(repository, "alice", "rags", 1000000L, false);
// Issue another indivisible asset
richesAssetId = AssetUtils.issueAsset(repository, "bob", "riches", 1000000L, false);
}
// "amount" will be in riches, "price" will be in rags/riches
final BigDecimal aliceAmount = new BigDecimal("3").setScale(8);
final BigDecimal alicePrice = new BigDecimal("1").setScale(8);
final BigDecimal aliceCommitment = aliceAmount.multiply(alicePrice).setScale(8); // rags
final BigDecimal bobAmount = new BigDecimal("8").setScale(8);
final BigDecimal bobPrice = new BigDecimal("0.25").setScale(8);
final BigDecimal bobCommitment = bobAmount; // riches
final BigDecimal aliceReturn = aliceAmount; // riches
final BigDecimal bobReturn = aliceAmount.multiply(alicePrice).setScale(8);
AssetUtils.genericTradeTest(ragsAssetId, richesAssetId, aliceAmount, alicePrice, bobAmount, bobPrice, aliceCommitment, bobCommitment, aliceReturn, bobReturn);
}
/**
* Check matching of orders with prices that
* would have had reciprocals that can't be represented in floating binary.
* <p>
* For example, sell 2 TEST for 24 OTHER so
* unit price is 2 / 24 or 0.08333333(recurring) TEST/OTHER.
* <p>
* But although price is rounded down to 0.08333333,
* the price is the same for both sides.
* <p>
* Traded amounts are expected to be 24 OTHER
* and 1.99999992 TEST.
*/
@Test
public void testNonExactFraction() throws DataException {
final BigDecimal aliceAmount = new BigDecimal("24.00000000").setScale(8);
final BigDecimal alicePrice = new BigDecimal("0.08333333").setScale(8);
final BigDecimal aliceCommitment = new BigDecimal("1.99999992").setScale(8);
final BigDecimal bobAmount = new BigDecimal("24.00000000").setScale(8);
final BigDecimal bobPrice = new BigDecimal("0.08333333").setScale(8);
final BigDecimal bobCommitment = new BigDecimal("24.00000000").setScale(8);
// Expected traded amounts
final BigDecimal aliceReturn = new BigDecimal("24.00000000").setScale(8); // other
final BigDecimal bobReturn = new BigDecimal("1.99999992").setScale(8); // test
long otherAssetId;
try (Repository repository = RepositoryManager.getRepository()) {
otherAssetId = AssetUtils.issueAsset(repository, "bob", "other", 5000L, true);
}
AssetUtils.genericTradeTest(AssetUtils.testAssetId, otherAssetId, aliceAmount, alicePrice, bobAmount, bobPrice, aliceCommitment, bobCommitment, aliceReturn, bobReturn);
}
/**
* Check that better prices are used in preference when matching orders.
*/
@Test
public void testPriceImprovement() throws DataException {
final BigDecimal initialTestAssetAmount = new BigDecimal("24.00000000").setScale(8);
final BigDecimal basePrice = new BigDecimal("1.00000000").setScale(8);
final BigDecimal betterPrice = new BigDecimal("2.10000000").setScale(8);
final BigDecimal bestPrice = new BigDecimal("2.40000000").setScale(8);
final BigDecimal minimalPrice = new BigDecimal("0.00000001").setScale(8);
final BigDecimal matchingTestAssetAmount = new BigDecimal("12.00000000").setScale(8);
try (Repository repository = RepositoryManager.getRepository()) {
Map<String, Map<Long, BigDecimal>> initialBalances = AccountUtils.getBalances(repository, Asset.QORA, AssetUtils.testAssetId);
// Create 'better' initial order
byte[] bobOrderId = AssetUtils.createOrder(repository, "bob", Asset.QORA, AssetUtils.testAssetId, initialTestAssetAmount, betterPrice);
// Create 'best' initial - surrounded by other orders so price improvement code should re-order results
byte[] chloeOrderId = AssetUtils.createOrder(repository, "chloe", Asset.QORA, AssetUtils.testAssetId, initialTestAssetAmount, bestPrice);
// Create 'base' initial order
byte[] dilbertOrderId = AssetUtils.createOrder(repository, "dilbert", Asset.QORA, AssetUtils.testAssetId, initialTestAssetAmount, basePrice);
// Create matching order
byte[] aliceOrderId = AssetUtils.createOrder(repository, "alice", AssetUtils.testAssetId, Asset.QORA, matchingTestAssetAmount, minimalPrice);
// Check balances to check expected outcome
BigDecimal expectedBalance;
// We're expecting Alice's order to match with Chloe's order (as Bob's and Dilberts's orders have worse prices)
BigDecimal matchedQoraAmount = matchingTestAssetAmount.multiply(bestPrice).setScale(8, RoundingMode.DOWN);
BigDecimal tradedTestAssetAmount = matchingTestAssetAmount;
// Alice Qora
expectedBalance = initialBalances.get("alice").get(Asset.QORA).add(matchedQoraAmount);
AccountUtils.assertBalance(repository, "alice", Asset.QORA, expectedBalance);
// Alice test asset
expectedBalance = initialBalances.get("alice").get(AssetUtils.testAssetId).subtract(matchingTestAssetAmount);
AccountUtils.assertBalance(repository, "alice", AssetUtils.testAssetId, expectedBalance);
// Bob Qora
expectedBalance = initialBalances.get("bob").get(Asset.QORA).subtract(initialTestAssetAmount.multiply(betterPrice).setScale(8, RoundingMode.DOWN));
AccountUtils.assertBalance(repository, "bob", Asset.QORA, expectedBalance);
// Bob test asset
expectedBalance = initialBalances.get("bob").get(AssetUtils.testAssetId);
AccountUtils.assertBalance(repository, "bob", AssetUtils.testAssetId, expectedBalance);
// Chloe Qora
expectedBalance = initialBalances.get("chloe").get(Asset.QORA).subtract(initialTestAssetAmount.multiply(bestPrice).setScale(8, RoundingMode.DOWN));
AccountUtils.assertBalance(repository, "chloe", Asset.QORA, expectedBalance);
// Chloe test asset
expectedBalance = initialBalances.get("chloe").get(AssetUtils.testAssetId).add(tradedTestAssetAmount);
AccountUtils.assertBalance(repository, "chloe", AssetUtils.testAssetId, expectedBalance);
// Dilbert Qora
expectedBalance = initialBalances.get("dilbert").get(Asset.QORA).subtract(initialTestAssetAmount.multiply(basePrice).setScale(8, RoundingMode.DOWN));
AccountUtils.assertBalance(repository, "dilbert", Asset.QORA, expectedBalance);
// Dilbert test asset
expectedBalance = initialBalances.get("dilbert").get(AssetUtils.testAssetId);
AccountUtils.assertBalance(repository, "dilbert", AssetUtils.testAssetId, expectedBalance);
// Check orders
OrderData aliceOrderData = repository.getAssetRepository().fromOrderId(aliceOrderId);
OrderData bobOrderData = repository.getAssetRepository().fromOrderId(bobOrderId);
OrderData chloeOrderData = repository.getAssetRepository().fromOrderId(chloeOrderId);
OrderData dilbertOrderData = repository.getAssetRepository().fromOrderId(dilbertOrderId);
// Alice's fulfilled
Common.assertEqualBigDecimals("Alice's order's fulfilled amount incorrect", tradedTestAssetAmount, aliceOrderData.getFulfilled());
// Bob's fulfilled should be zero
Common.assertEqualBigDecimals("Bob's order should be totally unfulfilled", BigDecimal.ZERO, bobOrderData.getFulfilled());
// Chloe's fulfilled
Common.assertEqualBigDecimals("Chloe's order's fulfilled amount incorrect", tradedTestAssetAmount, chloeOrderData.getFulfilled());
// Dilbert's fulfilled should be zero
Common.assertEqualBigDecimals("Dilbert's order should be totally unfulfilled", BigDecimal.ZERO, dilbertOrderData.getFulfilled());
}
}
}

215
src/test/java/org/qora/test/assets/OldTradingTests.java

@ -0,0 +1,215 @@
package org.qora.test.assets;
import org.junit.After;
import org.junit.Before;
import org.junit.Test;
import org.qora.asset.Asset;
import org.qora.repository.DataException;
import org.qora.repository.Repository;
import org.qora.repository.RepositoryManager;
import org.qora.test.common.AccountUtils;
import org.qora.test.common.AssetUtils;
import org.qora.test.common.Common;
import java.math.BigDecimal;
import java.util.Map;
public class OldTradingTests extends Common {
@Before
public void beforeTest() throws DataException {
Common.useSettings("test-settings-old-asset.json");
}
@After
public void afterTest() throws DataException {
Common.orphanCheck();
}
/**
* Check matching of indivisible amounts.
* <p>
* We use orders similar to some found in legacy qora1 blockchain
* to test for expected results with indivisible assets.
* <p>
* In addition, although the 3rd "further" order would match up to 999 RUB.iPLZ,
* granularity at that price reduces matched amount to 493 RUB.iPLZ.
*/
@Test
public void testOldIndivisible() throws DataException {
Common.useSettings("test-settings-old-asset.json");
// Issue some indivisible assets
long asset112Id;
long asset113Id;
try (Repository repository = RepositoryManager.getRepository()) {
// Issue indivisible asset
asset112Id = AssetUtils.issueAsset(repository, "alice", "RUB.iPLZ", 999999999999L, false);
// Issue another indivisible asset
asset113Id = AssetUtils.issueAsset(repository, "bob", "RU.GZP.V123", 10000L, false);
}
// Transfer some assets so orders can be created
try (Repository repository = RepositoryManager.getRepository()) {
AssetUtils.transferAsset(repository, "alice", "bob", asset112Id, BigDecimal.valueOf(5000L).setScale(8));
AssetUtils.transferAsset(repository, "bob", "alice", asset113Id, BigDecimal.valueOf(5000L).setScale(8));
}
final BigDecimal asset113Amount = new BigDecimal("1000").setScale(8);
final BigDecimal asset112Price = new BigDecimal("1.00000000").setScale(8);
final BigDecimal asset112Amount = new BigDecimal("2000").setScale(8);
final BigDecimal asset113Price = new BigDecimal("0.98600000").setScale(8);
final BigDecimal asset112Matched = new BigDecimal("1000").setScale(8);
final BigDecimal asset113Matched = new BigDecimal("1000").setScale(8);
AssetUtils.genericTradeTest(asset113Id, asset112Id, asset113Amount, asset112Price, asset112Amount, asset113Price, asset113Amount, asset112Amount, asset112Matched, asset113Matched);
// Further trade
final BigDecimal asset113Amount2 = new BigDecimal("986").setScale(8);
final BigDecimal asset112Price2 = new BigDecimal("1.00000000").setScale(8);
final BigDecimal asset112Matched2 = new BigDecimal("500").setScale(8);
final BigDecimal asset113Matched2 = new BigDecimal("493").setScale(8);
try (Repository repository = RepositoryManager.getRepository()) {
Map<String, Map<Long, BigDecimal>> initialBalances = AccountUtils.getBalances(repository, asset112Id, asset113Id);
// Create further order
byte[] furtherOrderId = AssetUtils.createOrder(repository, "alice", asset113Id, asset112Id, asset113Amount2, asset112Price2);
// Check balances to check expected outcome
BigDecimal expectedBalance;
// Alice asset 113
expectedBalance = initialBalances.get("alice").get(asset113Id).subtract(asset113Amount2);
AccountUtils.assertBalance(repository, "alice", asset113Id, expectedBalance);
// Alice asset 112
expectedBalance = initialBalances.get("alice").get(asset112Id).add(asset112Matched2);
AccountUtils.assertBalance(repository, "alice", asset112Id, expectedBalance);
BigDecimal expectedFulfilled = asset113Matched2;
BigDecimal actualFulfilled = repository.getAssetRepository().fromOrderId(furtherOrderId).getFulfilled();
assertEqualBigDecimals("Order fulfilled incorrect", expectedFulfilled, actualFulfilled);
}
}
/**
* Check legacy partial matching of orders with prices that
* can't be represented in floating binary.
* <p>
* For example, sell 2 TEST for 24 QORA so
* unit price is 2 / 24 or 0.08333333.
* <p>
* This inexactness causes the match amount to be
* only 1.99999992 instead of the expected 2.00000000.
* <p>
* However this behaviour is "grandfathered" in legacy/"old"
* mode so we need to test.
*/
@Test
public void testOldNonExactFraction() throws DataException {
Common.useSettings("test-settings-old-asset.json");
final BigDecimal aliceAmount = new BigDecimal("24.00000000").setScale(8);
final BigDecimal alicePrice = new BigDecimal("0.08333333").setScale(8);
final BigDecimal bobAmount = new BigDecimal("2.00000000").setScale(8);
final BigDecimal bobPrice = new BigDecimal("12.00000000").setScale(8);
final BigDecimal aliceCommitment = aliceAmount;
final BigDecimal bobCommitment = bobAmount;
// Due to rounding these are the expected traded amounts.
final BigDecimal aliceReturn = new BigDecimal("1.99999992").setScale(8);
final BigDecimal bobReturn = new BigDecimal("24.00000000").setScale(8);
AssetUtils.genericTradeTest(AssetUtils.testAssetId, Asset.QORA, aliceAmount, alicePrice, bobAmount, bobPrice, aliceCommitment, bobCommitment, aliceReturn, bobReturn);
}
/**
* Check legacy qora1 blockchain matching behaviour.
*/
@Test
public void testQora1Compat() throws DataException {
// Asset 61 [ATFunding] was issued by QYsLsfwMRBPnunmuWmFkM4hvGsfooY8ssU with 250,000,000 quantity and was divisible.
// Target order 2jMinWSBjxaLnQvhcEoWGs2JSdX7qbwxMTZenQXXhjGYDHCJDL6EjXPz5VXYuUfZM5LvRNNbcaeBbM6Xhb4tN53g
// Creator was QZyuTa3ygjThaPRhrCp1BW4R5Sed6uAGN8 at 2014-10-23 11:14:42.525000+0:00
// Have: 150000 [ATFunding], Price: 1.7000000 QORA
// Initiating order 3Ufqi52nDL3Gi7KqVXpgebVN5FmLrdq2XyUJ11BwSV4byxQ2z96Q5CQeawGyanhpXS4XkYAaJTrNxsDDDxyxwbMN
// Creator was QMRoD3RS5vJ4DVNBhBgGtQG4KT3PhkNALH at 2015-03-27 12:24:02.945000+0:00
// Have: 2 QORA, Price: 0.58 [ATFunding]
// Trade: 1.17647050 [ATFunding] for 1.99999985 QORA
// Load/check settings, which potentially sets up blockchain config, etc.
Common.useSettings("test-settings-old-asset.json");
// Transfer some test asset to bob
try (Repository repository = RepositoryManager.getRepository()) {
AssetUtils.transferAsset(repository, "alice", "bob", AssetUtils.testAssetId, BigDecimal.valueOf(200000L).setScale(8));
}
final BigDecimal aliceAmount = new BigDecimal("150000").setScale(8);
final BigDecimal alicePrice = new BigDecimal("1.70000000").setScale(8);
final BigDecimal bobAmount = new BigDecimal("2.00000000").setScale(8);
final BigDecimal bobPrice = new BigDecimal("0.58000000").setScale(8);
final BigDecimal aliceCommitment = aliceAmount;
final BigDecimal bobCommitment = bobAmount;
final BigDecimal aliceReturn = new BigDecimal("1.99999985").setScale(8);
final BigDecimal bobReturn = new BigDecimal("1.17647050").setScale(8);
AssetUtils.genericTradeTest(AssetUtils.testAssetId, Asset.QORA, aliceAmount, alicePrice, bobAmount, bobPrice, aliceCommitment, bobCommitment, aliceReturn, bobReturn);
}
/**
* Check legacy qora1 blockchain matching behaviour.
*/
@Test
public void testQora1Compat2() throws DataException {
// Asset 95 [Bitcoin] was issued by QiGx93L9rNHSNWCY1bJnQTPwB3nhxYTCUj with 21000000 quantity and was divisible.
// Asset 96 [BitBTC] was issued by QiGx93L9rNHSNWCY1bJnQTPwB3nhxYTCUj with 21000000 quantity and was divisible.
// Target order 3jinKPHEak9xrjeYtCaE1PawwRZeRkhYA6q4A7sqej7f3jio8WwXwXpfLWVZkPQ3h6cVdwPhcDFNgbbrBXcipHee
// Creator was QiGx93L9rNHSNWCY1bJnQTPwB3nhxYTCUj at 2015-06-10 20:31:44.840000+0:00
// Have: 1000000 [BitBTC], Price: 0.90000000 [Bitcoin]
// Initiating order Jw1UfgspZ344waF8qLhGJanJXVa32FBoVvMW5ByFkyHvZEumF4fPqbaGMa76ba1imC4WX5t3Roa7r23Ys6rhKAA
// Creator was QiGx93L9rNHSNWCY1bJnQTPwB3nhxYTCUj at 2015-06-14 17:49:41.410000+0:00
// Have: 73251 [Bitcoin], Price: 1.01 [BitBTC]
// Trade: 81389.99991860 [BitBTC] for 73250.99992674 [Bitcoin]
// Load/check settings, which potentially sets up blockchain config, etc.
Common.useSettings("test-settings-old-asset.json");
// Transfer some test asset to bob
try (Repository repository = RepositoryManager.getRepository()) {
AssetUtils.transferAsset(repository, "alice", "bob", AssetUtils.testAssetId, BigDecimal.valueOf(200000L).setScale(8));
}
final BigDecimal aliceAmount = new BigDecimal("1000000").setScale(8);
final BigDecimal alicePrice = new BigDecimal("0.90000000").setScale(8);
final BigDecimal bobAmount = new BigDecimal("73251").setScale(8);
final BigDecimal bobPrice = new BigDecimal("1.01000000").setScale(8);
final BigDecimal aliceCommitment = aliceAmount;
final BigDecimal bobCommitment = bobAmount;
final BigDecimal aliceReturn = new BigDecimal("73250.99992674").setScale(8);
final BigDecimal bobReturn = new BigDecimal("81389.99991860").setScale(8);
AssetUtils.genericTradeTest(Asset.QORA, AssetUtils.testAssetId, aliceAmount, alicePrice, bobAmount, bobPrice, aliceCommitment, bobCommitment, aliceReturn, bobReturn);
}
}

441
src/test/java/org/qora/test/assets/TradingTests.java

@ -1,441 +0,0 @@
package org.qora.test.assets;
import org.junit.After;
import org.junit.Before;
import org.junit.Test;
import org.qora.asset.Asset;
import org.qora.asset.Order;
import org.qora.block.BlockChain;
import org.qora.data.asset.AssetData;
import org.qora.data.asset.OrderData;
import org.qora.repository.DataException;
import org.qora.repository.Repository;
import org.qora.repository.RepositoryManager;
import org.qora.test.common.AccountUtils;
import org.qora.test.common.AssetUtils;
import org.qora.test.common.Common;
import java.math.BigDecimal;
import java.util.Map;
public class TradingTests extends Common {
@Before
public void beforeTest() throws DataException {
Common.useDefaultSettings();
}
@After
public void afterTest() throws DataException {
Common.orphanCheck();
}
/**
* Check granularity adjustment values.
* <p>
* If trading at a price of 12 eggs for 1 coin
* then trades can only happen at multiples of
* 0.000000001 or 0.00000012 depending on direction.
*/
@Test
public void testDivisibleGranularities() {
testGranularity(true, true, "12", "1", "0.00000012");
testGranularity(true, true, "1", "12", "0.00000001");
}
/**
* Check granularity adjustment values.
* <p>
* If trading at a price of 123 riches per 50301 rags,
* then the GCD(123, 50301) is 3 and so trades can only
* happen at multiples of (50301/3) = 16767 rags or
* (123/3) = 41 riches.
*/
@Test
public void testIndivisibleGranularities() {
testGranularity(false, false, "50301", "123", "16767");
testGranularity(false, false, "123", "50301", "41");
}
private void testGranularity(boolean isOurHaveDivisible, boolean isOurWantDivisible, String theirHaveAmount, String theirWantAmount, String expectedGranularity) {
final long newPricingTimestamp = BlockChain.getInstance().getNewAssetPricingTimestamp() + 1;
final AssetData ourHaveAssetData = new AssetData(null, null, null, 0, isOurHaveDivisible, null, 0, null);
final AssetData ourWantAssetData = new AssetData(null, null, null, 0, isOurWantDivisible, null, 0, null);
OrderData theirOrderData = new OrderData(null, null, 0, 0, new BigDecimal(theirHaveAmount), new BigDecimal(theirWantAmount), null, newPricingTimestamp);
BigDecimal granularity = Order.calculateAmountGranularity(ourHaveAssetData, ourWantAssetData, theirOrderData);
assertEqualBigDecimals("Granularity incorrect", new BigDecimal(expectedGranularity), granularity);
}
/**
* Check matching of indivisible amounts.
* <p>
* New pricing scheme allows two attempts are calculating matched amount
* to reduce partial-match issues caused by rounding and recurring fractional digits:
* <p>
* <ol>
* <li> amount * round_down(1 / unit price) </li>
* <li> round_down(amount / unit price) </li>
* </ol>
* Alice's price is 12 QORA per ATNL so the ATNL per QORA unit price is 0.08333333...<br>
* Bob wants to spend 24 QORA so:
* <p>
* <ol>
* <li> 24 QORA * (1 / 0.0833333...) = 1.99999999 ATNL </li>
* <li> 24 QORA / 0.08333333.... = 2 ATNL </li>
* </ol>
* The second result is obviously more intuitive as is critical where assets are not divisible,
* like ATNL in this test case.
* <p>
* @see TradingTests#testOldNonExactFraction
* @see TradingTests#testNonExactFraction
* @throws DataException
*/
@Test
public void testMixedDivisibility() throws DataException {
// Issue indivisible asset
long atnlAssetId;
try (Repository repository = RepositoryManager.getRepository()) {
// Issue indivisible asset
atnlAssetId = AssetUtils.issueAsset(repository, "alice", "ATNL", 100000000L, false);
}
final BigDecimal atnlAmount = BigDecimal.valueOf(2L).setScale(8);
final BigDecimal qoraAmount = BigDecimal.valueOf(24L).setScale(8);
genericTradeTest(atnlAssetId, Asset.QORA, atnlAmount, qoraAmount, qoraAmount, atnlAmount, atnlAmount, qoraAmount);
}
/**
* Check matching of indivisible amounts (new pricing).
* <p>
* Alice is selling twice as much as Bob wants,
* but at the same [calculated] unit price,
* so Bob's order should fully match.
* <p>
* However, in legacy/"old" mode, the granularity checks
* would prevent this trade.
*/
@Test
public void testIndivisible() throws DataException {
// Issue some indivisible assets
long ragsAssetId;
long richesAssetId;
try (Repository repository = RepositoryManager.getRepository()) {
// Issue indivisble asset
ragsAssetId = AssetUtils.issueAsset(repository, "alice", "rags", 12345678L, false);
// Issue another indivisble asset
richesAssetId = AssetUtils.issueAsset(repository, "bob", "riches", 87654321L, false);
}
final BigDecimal ragsAmount = BigDecimal.valueOf(50301L).setScale(8);
final BigDecimal richesAmount = BigDecimal.valueOf(123L).setScale(8);
final BigDecimal two = BigDecimal.valueOf(2L);
genericTradeTest(ragsAssetId, richesAssetId, ragsAmount.multiply(two), richesAmount.multiply(two), richesAmount, ragsAmount, ragsAmount, richesAmount);
}
/**
* Check matching of indivisible amounts.
* <p>
* We use orders similar to some found in legacy qora1 blockchain
* to test for expected results with indivisible assets.
* <p>
* In addition, although the 3rd "further" order would match up to 999 RUB.iPLZ,
* granularity at that price reduces matched amount to 493 RUB.iPLZ.
*/
@Test
public void testOldIndivisible() throws DataException {
Common.useSettings("test-settings-old-asset.json");
// Issue some indivisible assets
long asset112Id;
long asset113Id;
try (Repository repository = RepositoryManager.getRepository()) {
// Issue indivisble asset
asset112Id = AssetUtils.issueAsset(repository, "alice", "RUB.iPLZ", 999999999999L, false);
// Issue another indivisble asset
asset113Id = AssetUtils.issueAsset(repository, "bob", "RU.GZP.V123", 10000L, false);
}
// Transfer some assets so orders can be created
try (Repository repository = RepositoryManager.getRepository()) {
AssetUtils.transferAsset(repository, "alice", "bob", asset112Id, BigDecimal.valueOf(5000L).setScale(8));
AssetUtils.transferAsset(repository, "bob", "alice", asset113Id, BigDecimal.valueOf(5000L).setScale(8));
}
final BigDecimal asset113Amount = new BigDecimal("1000").setScale(8);
final BigDecimal asset112Price = new BigDecimal("1.00000000").setScale(8);
final BigDecimal asset112Amount = new BigDecimal("2000").setScale(8);
final BigDecimal asset113Price = new BigDecimal("0.98600000").setScale(8);
final BigDecimal asset112Matched = new BigDecimal("1000").setScale(8);
final BigDecimal asset113Matched = new BigDecimal("1000").setScale(8);
genericTradeTest(asset113Id, asset112Id, asset113Amount, asset112Price, asset112Amount, asset113Price, asset113Matched, asset112Matched);
// Further trade
final BigDecimal asset113Amount2 = new BigDecimal("986").setScale(8);
final BigDecimal asset112Price2 = new BigDecimal("1.00000000").setScale(8);
final BigDecimal asset112Matched2 = new BigDecimal("500").setScale(8);
final BigDecimal asset113Matched2 = new BigDecimal("493").setScale(8);
try (Repository repository = RepositoryManager.getRepository()) {
Map<String, Map<Long, BigDecimal>> initialBalances = AccountUtils.getBalances(repository, asset112Id, asset113Id);
// Create further order
byte[] furtherOrderId = AssetUtils.createOrder(repository, "alice", asset113Id, asset112Id, asset113Amount2, asset112Price2);
// Check balances to check expected outcome
BigDecimal expectedBalance;
// Alice asset 113
expectedBalance = initialBalances.get("alice").get(asset113Id).subtract(asset113Amount2);
assertBalance(repository, "alice", asset113Id, expectedBalance);
// Alice asset 112
expectedBalance = initialBalances.get("alice").get(asset112Id).add(asset112Matched2);
assertBalance(repository, "alice", asset112Id, expectedBalance);
BigDecimal expectedFulfilled = asset113Matched2;
BigDecimal actualFulfilled = repository.getAssetRepository().fromOrderId(furtherOrderId).getFulfilled();
assertEqualBigDecimals("Order fulfilled incorrect", expectedFulfilled, actualFulfilled);
}
}
/**
* Check full matching of orders with prices that
* can't be represented in floating binary.
* <p>
* For example, sell 1 GOLD for 12 QORA so
* price is 1/12 or 0.08333333..., which could
* lead to rounding issues or inexact match amounts,
* but we counter this using the technique described in
* {@link #testMixedDivisibility()}
*/
@Test
public void testNonExactFraction() throws DataException {
final BigDecimal otherAmount = BigDecimal.valueOf(24L).setScale(8);
final BigDecimal qoraAmount = BigDecimal.valueOf(2L).setScale(8);
genericTradeTest(AssetUtils.testAssetId, Asset.QORA, otherAmount, qoraAmount, qoraAmount, otherAmount, otherAmount, qoraAmount);
}
/**
* Check legacy partial matching of orders with prices that
* can't be represented in floating binary.
* <p>
* For example, sell 2 TEST for 24 QORA so
* unit price is 2 / 24 or 0.08333333.
* <p>
* This inexactness causes the match amount to be
* only 1.99999992 instead of the expected 2.00000000.
* <p>
* However this behaviour is "grandfathered" in legacy/"old"
* mode so we need to test.
*/
@Test
public void testOldNonExactFraction() throws DataException {
Common.useSettings("test-settings-old-asset.json");
final BigDecimal initialAmount = new BigDecimal("24.00000000").setScale(8);
final BigDecimal initialPrice = new BigDecimal("0.08333333").setScale(8);
final BigDecimal matchedAmount = new BigDecimal("2.00000000").setScale(8);
final BigDecimal matchedPrice = new BigDecimal("12.00000000").setScale(8);
// Due to rounding these are the expected traded amounts.
final BigDecimal tradedQoraAmount = new BigDecimal("24.00000000").setScale(8);
final BigDecimal tradedOtherAmount = new BigDecimal("1.99999992").setScale(8);
genericTradeTest(AssetUtils.testAssetId, Asset.QORA, initialAmount, initialPrice, matchedAmount, matchedPrice, tradedQoraAmount, tradedOtherAmount);
}
/**
* Check that better prices are used in preference when matching orders.
*/
@Test
public void testPriceImprovement() throws DataException {
final BigDecimal qoraAmount = BigDecimal.valueOf(24L).setScale(8);
final BigDecimal betterQoraAmount = BigDecimal.valueOf(25L).setScale(8);
final BigDecimal bestQoraAmount = BigDecimal.valueOf(31L).setScale(8);
final BigDecimal otherAmount = BigDecimal.valueOf(2L).setScale(8);
try (Repository repository = RepositoryManager.getRepository()) {
Map<String, Map<Long, BigDecimal>> initialBalances = AccountUtils.getBalances(repository, Asset.QORA, AssetUtils.testAssetId);
// Create best initial order
AssetUtils.createOrder(repository, "bob", Asset.QORA, AssetUtils.testAssetId, qoraAmount, otherAmount);
// Create initial order better than first
AssetUtils.createOrder(repository, "chloe", Asset.QORA, AssetUtils.testAssetId, bestQoraAmount, otherAmount);
// Create initial order
AssetUtils.createOrder(repository, "dilbert", Asset.QORA, AssetUtils.testAssetId, betterQoraAmount, otherAmount);
// Create matching order
AssetUtils.createOrder(repository, "alice", AssetUtils.testAssetId, Asset.QORA, otherAmount, qoraAmount);
// Check balances to check expected outcome
BigDecimal expectedBalance;
// We're expecting Alice's order to match with Chloe's order (as Bob's and Dilberts's orders have worse prices)
// Alice Qora
expectedBalance = initialBalances.get("alice").get(Asset.QORA).add(bestQoraAmount);
assertBalance(repository, "alice", Asset.QORA, expectedBalance);
// Alice test asset
expectedBalance = initialBalances.get("alice").get(AssetUtils.testAssetId).subtract(otherAmount);
assertBalance(repository, "alice", AssetUtils.testAssetId, expectedBalance);
// Bob Qora
expectedBalance = initialBalances.get("bob").get(Asset.QORA).subtract(qoraAmount);
assertBalance(repository, "bob", Asset.QORA, expectedBalance);
// Bob test asset
expectedBalance = initialBalances.get("bob").get(AssetUtils.testAssetId);
assertBalance(repository, "bob", AssetUtils.testAssetId, expectedBalance);
// Chloe Qora
expectedBalance = initialBalances.get("chloe").get(Asset.QORA).subtract(bestQoraAmount);
assertBalance(repository, "chloe", Asset.QORA, expectedBalance);
// Chloe test asset
expectedBalance = initialBalances.get("chloe").get(AssetUtils.testAssetId).add(otherAmount);
assertBalance(repository, "chloe", AssetUtils.testAssetId, expectedBalance);
// Dilbert Qora
expectedBalance = initialBalances.get("dilbert").get(Asset.QORA).subtract(betterQoraAmount);
assertBalance(repository, "dilbert", Asset.QORA, expectedBalance);
// Dilbert test asset
expectedBalance = initialBalances.get("dilbert").get(AssetUtils.testAssetId);
assertBalance(repository, "dilbert", AssetUtils.testAssetId, expectedBalance);
}
}
/**
* Check legacy qora1 blockchain matching behaviour.
*/
@Test
public void testQora1Compat() throws DataException {
// Asset 61 [ATFunding] was issued by QYsLsfwMRBPnunmuWmFkM4hvGsfooY8ssU with 250,000,000 quantity and was divisible.
// Initial order 2jMinWSBjxaLnQvhcEoWGs2JSdX7qbwxMTZenQXXhjGYDHCJDL6EjXPz5VXYuUfZM5LvRNNbcaeBbM6Xhb4tN53g
// Creator was QZyuTa3ygjThaPRhrCp1BW4R5Sed6uAGN8 at 2014-10-23 11:14:42.525000+0:00
// Have: 150000 [ATFunding], Price: 1.7000000 QORA
// Matching order 3Ufqi52nDL3Gi7KqVXpgebVN5FmLrdq2XyUJ11BwSV4byxQ2z96Q5CQeawGyanhpXS4XkYAaJTrNxsDDDxyxwbMN
// Creator was QMRoD3RS5vJ4DVNBhBgGtQG4KT3PhkNALH at 2015-03-27 12:24:02.945000+0:00
// Have: 2 QORA, Price: 0.58 [ATFunding]
// Trade: 1.17647050 [ATFunding] for 1.99999985 QORA
// Load/check settings, which potentially sets up blockchain config, etc.
Common.useSettings("test-settings-old-asset.json");
// Transfer some test asset to bob
try (Repository repository = RepositoryManager.getRepository()) {
AssetUtils.transferAsset(repository, "alice", "bob", AssetUtils.testAssetId, BigDecimal.valueOf(200000L).setScale(8));
}
final BigDecimal initialAmount = new BigDecimal("150000").setScale(8);
final BigDecimal initialPrice = new BigDecimal("1.70000000").setScale(8);
final BigDecimal matchingAmount = new BigDecimal("2.00000000").setScale(8);
final BigDecimal matchingPrice = new BigDecimal("0.58000000").setScale(8);
final BigDecimal tradedOtherAmount = new BigDecimal("1.17647050").setScale(8);
final BigDecimal tradedQoraAmount = new BigDecimal("1.99999985").setScale(8);
genericTradeTest(AssetUtils.testAssetId, Asset.QORA, initialAmount, initialPrice, matchingAmount, matchingPrice, tradedOtherAmount, tradedQoraAmount);
}
/**
* Check legacy qora1 blockchain matching behaviour.
*/
@Test
public void testQora1Compat2() throws DataException {
// Asset 95 [Bitcoin] was issued by QiGx93L9rNHSNWCY1bJnQTPwB3nhxYTCUj with 21000000 quantity and was divisible.
// Asset 96 [BitBTC] was issued by QiGx93L9rNHSNWCY1bJnQTPwB3nhxYTCUj with 21000000 quantity and was divisible.
// Initial order 3jinKPHEak9xrjeYtCaE1PawwRZeRkhYA6q4A7sqej7f3jio8WwXwXpfLWVZkPQ3h6cVdwPhcDFNgbbrBXcipHee
// Creator was QiGx93L9rNHSNWCY1bJnQTPwB3nhxYTCUj at 2015-06-10 20:31:44.840000+0:00
// Have: 1000000 [BitBTC], Price: 0.90000000 [Bitcoin]
// Matching order Jw1UfgspZ344waF8qLhGJanJXVa32FBoVvMW5ByFkyHvZEumF4fPqbaGMa76ba1imC4WX5t3Roa7r23Ys6rhKAA
// Creator was QiGx93L9rNHSNWCY1bJnQTPwB3nhxYTCUj at 2015-06-14 17:49:41.410000+0:00
// Have: 73251 [Bitcoin], Price: 1.01 [BitBTC]
// Trade: 81389.99991860 [BitBTC] for 73250.99992674 [Bitcoin]
// Load/check settings, which potentially sets up blockchain config, etc.
Common.useSettings("test-settings-old-asset.json");
// Transfer some test asset to bob
try (Repository repository = RepositoryManager.getRepository()) {
AssetUtils.transferAsset(repository, "alice", "bob", AssetUtils.testAssetId, BigDecimal.valueOf(200000L).setScale(8));
}
final BigDecimal initialAmount = new BigDecimal("1000000").setScale(8);
final BigDecimal initialPrice = new BigDecimal("0.90000000").setScale(8);
final BigDecimal matchingAmount = new BigDecimal("73251").setScale(8);
final BigDecimal matchingPrice = new BigDecimal("1.01000000").setScale(8);
final BigDecimal tradedHaveAmount = new BigDecimal("81389.99991860").setScale(8);
final BigDecimal tradedWantAmount = new BigDecimal("73250.99992674").setScale(8);
genericTradeTest(Asset.QORA, AssetUtils.testAssetId, initialAmount, initialPrice, matchingAmount, matchingPrice, tradedHaveAmount, tradedWantAmount);
}
private void genericTradeTest(long haveAssetId, long wantAssetId,
BigDecimal initialAmount, BigDecimal initialPrice,
BigDecimal matchingAmount, BigDecimal matchingPrice,
BigDecimal tradedHaveAmount, BigDecimal tradedWantAmount) throws DataException {
try (Repository repository = RepositoryManager.getRepository()) {
Map<String, Map<Long, BigDecimal>> initialBalances = AccountUtils.getBalances(repository, haveAssetId, wantAssetId);
// Create initial order
AssetUtils.createOrder(repository, "alice", haveAssetId, wantAssetId, initialAmount, initialPrice);
// Create matching order
AssetUtils.createOrder(repository, "bob", wantAssetId, haveAssetId, matchingAmount, matchingPrice);
// Check balances to check expected outcome
BigDecimal expectedBalance;
// Alice have asset
expectedBalance = initialBalances.get("alice").get(haveAssetId).subtract(initialAmount);
assertBalance(repository, "alice", haveAssetId, expectedBalance);
// Alice want asset
expectedBalance = initialBalances.get("alice").get(wantAssetId).add(tradedWantAmount);
assertBalance(repository, "alice", wantAssetId, expectedBalance);
// Bob want asset
expectedBalance = initialBalances.get("bob").get(wantAssetId).subtract(matchingAmount);
assertBalance(repository, "bob", wantAssetId, expectedBalance);
// Bob have asset
expectedBalance = initialBalances.get("bob").get(haveAssetId).add(tradedHaveAmount);
assertBalance(repository, "bob", haveAssetId, expectedBalance);
}
}
private static void assertBalance(Repository repository, String accountName, long assetId, BigDecimal expectedBalance) throws DataException {
BigDecimal actualBalance = Common.getTestAccount(repository, accountName).getConfirmedBalance(assetId);
assertEqualBigDecimals(String.format("Test account '%s' asset %d balance incorrect", accountName, assetId), expectedBalance, actualBalance);
}
}

6
src/test/java/org/qora/test/common/AccountUtils.java

@ -29,4 +29,10 @@ public class AccountUtils {
return balances;
}
public static void assertBalance(Repository repository, String accountName, long assetId, BigDecimal expectedBalance) throws DataException {
BigDecimal actualBalance = Common.getTestAccount(repository, accountName).getConfirmedBalance(assetId);
Common.assertEqualBigDecimals(String.format("Test account '%s' asset %d balance incorrect", accountName, assetId), expectedBalance, actualBalance);
}
}

65
src/test/java/org/qora/test/common/AssetUtils.java

@ -1,8 +1,13 @@
package org.qora.test.common;
import static org.junit.Assert.assertNotNull;
import java.math.BigDecimal;
import java.util.Map;
import org.qora.account.PrivateKeyAccount;
import org.qora.block.BlockChain;
import org.qora.data.asset.OrderData;
import org.qora.data.transaction.CreateAssetOrderTransactionData;
import org.qora.data.transaction.IssueAssetTransactionData;
import org.qora.data.transaction.TransactionData;
@ -10,6 +15,7 @@ import org.qora.data.transaction.TransferAssetTransactionData;
import org.qora.group.Group;
import org.qora.repository.DataException;
import org.qora.repository.Repository;
import org.qora.repository.RepositoryManager;
public class AssetUtils {
@ -42,18 +48,71 @@ public class AssetUtils {
TransactionUtils.signAndForge(repository, transactionData, fromAccount);
}
public static byte[] createOrder(Repository repository, String accountName, long haveAssetId, long wantAssetId, BigDecimal amount, BigDecimal wantAmount) throws DataException {
public static byte[] createOrder(Repository repository, String accountName, long haveAssetId, long wantAssetId, BigDecimal amount, BigDecimal price) throws DataException {
PrivateKeyAccount account = Common.getTestAccount(repository, accountName);
byte[] reference = account.getLastReference();
long timestamp = repository.getTransactionRepository().fromSignature(reference).getTimestamp() + 1000;
// Note: "price" is not the same in V2 as in V1
TransactionData transactionData = new CreateAssetOrderTransactionData(timestamp, txGroupId, reference, account.getPublicKey(), haveAssetId, wantAssetId, amount, wantAmount, fee);
TransactionData transactionData = new CreateAssetOrderTransactionData(timestamp, txGroupId, reference, account.getPublicKey(), haveAssetId, wantAssetId, amount, price, fee);
TransactionUtils.signAndForge(repository, transactionData, account);
return repository.getAssetRepository().getAccountsOrders(account.getPublicKey(), null, null, null, null, true).get(0).getOrderId();
}
public static void genericTradeTest(long haveAssetId, long wantAssetId,
BigDecimal aliceAmount, BigDecimal alicePrice,
BigDecimal bobAmount, BigDecimal bobPrice,
BigDecimal aliceCommitment, BigDecimal bobCommitment,
BigDecimal aliceReturn, BigDecimal bobReturn) throws DataException {
try (Repository repository = RepositoryManager.getRepository()) {
Map<String, Map<Long, BigDecimal>> initialBalances = AccountUtils.getBalances(repository, haveAssetId, wantAssetId);
// Create target order
byte[] targetOrderId = createOrder(repository, "alice", haveAssetId, wantAssetId, aliceAmount, alicePrice);
// Create initiating order
byte[] initiatingOrderId = createOrder(repository, "bob", wantAssetId, haveAssetId, bobAmount, bobPrice);
// Check balances to check expected outcome
BigDecimal expectedBalance;
// Alice have asset
expectedBalance = initialBalances.get("alice").get(haveAssetId).subtract(aliceCommitment);
AccountUtils.assertBalance(repository, "alice", haveAssetId, expectedBalance);
// Alice want asset
expectedBalance = initialBalances.get("alice").get(wantAssetId).add(aliceReturn);
AccountUtils.assertBalance(repository, "alice", wantAssetId, expectedBalance);
// Bob want asset
expectedBalance = initialBalances.get("bob").get(wantAssetId).subtract(bobCommitment);
AccountUtils.assertBalance(repository, "bob", wantAssetId, expectedBalance);
// Bob have asset
expectedBalance = initialBalances.get("bob").get(haveAssetId).add(bobReturn);
AccountUtils.assertBalance(repository, "bob", haveAssetId, expectedBalance);
// Check orders
BigDecimal expectedFulfilled;
// Check matching order
OrderData targetOrderData = repository.getAssetRepository().fromOrderId(targetOrderId);
OrderData initiatingOrderData = repository.getAssetRepository().fromOrderId(initiatingOrderId);
boolean isNewPricing = initiatingOrderData.getTimestamp() > BlockChain.getInstance().getNewAssetPricingTimestamp();
BigDecimal newPricingAmount = (initiatingOrderData.getHaveAssetId() < initiatingOrderData.getWantAssetId()) ? bobReturn : aliceReturn;
assertNotNull("matching order missing", initiatingOrderData);
expectedFulfilled = isNewPricing ? newPricingAmount : aliceReturn;
Common.assertEqualBigDecimals(String.format("Bob's order \"fulfilled\" incorrect"), expectedFulfilled, initiatingOrderData.getFulfilled());
// Check initial order
assertNotNull("initial order missing", targetOrderData);
expectedFulfilled = isNewPricing ? newPricingAmount : bobReturn;
Common.assertEqualBigDecimals(String.format("Alice's order \"fulfilled\" incorrect"), expectedFulfilled, targetOrderData.getFulfilled());
}
}
}

35
src/test/java/org/qora/test/common/Common.java

@ -13,6 +13,8 @@ import java.util.function.Function;
import java.util.function.Predicate;
import java.util.stream.Collectors;
import org.apache.logging.log4j.LogManager;
import org.apache.logging.log4j.Logger;
import org.bitcoinj.core.Base58;
import org.bouncycastle.jce.provider.BouncyCastleProvider;
import org.bouncycastle.jsse.provider.BouncyCastleJsseProvider;
@ -34,12 +36,29 @@ import org.qora.settings.Settings;
public class Common {
static {
// This must go before any calls to LogManager/Logger
System.setProperty("java.util.logging.manager", "org.apache.logging.log4j.jul.LogManager");
Security.insertProviderAt(new BouncyCastleProvider(), 0);
Security.insertProviderAt(new BouncyCastleJsseProvider(), 1);
}
private static final Logger LOGGER = LogManager.getLogger(Common.class);
public static final String testConnectionUrl = "jdbc:hsqldb:mem:testdb";
// For debugging, use this instead to write DB to disk for examination:
// public static final String testConnectionUrl = "jdbc:hsqldb:file:testdb/blockchain;create=true";
public static final String testSettingsFilename = "test-settings-v2.json";
static {
// Load/check settings, which potentially sets up blockchain config, etc.
URL testSettingsUrl = Common.class.getClassLoader().getResource(testSettingsFilename);
assertNotNull("Test settings JSON file not found", testSettingsUrl);
Settings.fileInstance(testSettingsUrl.getPath());
}
private static List<AssetData> initialAssets;
private static List<GroupData> initialGroups;
private static List<AccountBalanceData> initialBalances;
@ -49,19 +68,6 @@ public class Common {
public static final byte[] v2testPublicKey = Base58.decode("2tiMr5LTpaWCgbRvkPK8TFd7k63DyHJMMFFsz9uBf1ZP");
public static final String v2testAddress = "QgV4s3xnzLhVBEJxcYui4u4q11yhUHsd9v";
static {
// This must go before any calls to LogManager/Logger
System.setProperty("java.util.logging.manager", "org.apache.logging.log4j.jul.LogManager");
Security.insertProviderAt(new BouncyCastleProvider(), 0);
Security.insertProviderAt(new BouncyCastleJsseProvider(), 1);
// Load/check settings, which potentially sets up blockchain config, etc.
URL testSettingsUrl = Common.class.getClassLoader().getResource(testSettingsFilename);
assertNotNull("Test settings JSON file not found", testSettingsUrl);
Settings.fileInstance(testSettingsUrl.getPath());
}
private static Map<String, TestAccount> testAccountsByName = new HashMap<>();
static {
testAccountsByName.put("alice", new TestAccount(null, "alice", "A9MNsATgQgruBUjxy2rjWY36Yf19uRioKZbiLFT2P7c6"));
@ -82,6 +88,7 @@ public class Common {
closeRepository();
// Load/check settings, which potentially sets up blockchain config, etc.
LOGGER.debug(String.format("Using setting file: %s", settingsFilename));
URL testSettingsUrl = Common.class.getClassLoader().getResource(settingsFilename);
assertNotNull("Test settings JSON file not found", testSettingsUrl);
Settings.fileInstance(testSettingsUrl.getPath());
@ -112,6 +119,8 @@ public class Common {
/** Orphan back to genesis block and compare initial snapshot. */
public static void orphanCheck() throws DataException {
LOGGER.debug("Orphaning back to genesis block");
try (final Repository repository = RepositoryManager.getRepository()) {
// Orphan back to genesis block
while (repository.getBlockRepository().getBlockchainHeight() > 1) {

21
src/test/java/org/qora/test/common/ObjectUtils.java

@ -0,0 +1,21 @@
package org.qora.test.common;
import java.lang.reflect.InvocationTargetException;
import java.lang.reflect.Method;
import java.util.Arrays;
public class ObjectUtils {
public static Object callMethod(Object obj, String methodName, Object... args) {
Method[] methods = obj.getClass().getDeclaredMethods();
Method foundMethod = Arrays.stream(methods).filter(method -> method.getName().equals(methodName)).findFirst().get();
try {
return foundMethod.invoke(obj, args);
} catch (IllegalAccessException | IllegalArgumentException | InvocationTargetException e) {
throw new RuntimeException("method call failed", e);
}
}
}

4
src/test/resources/log4j2-test.properties

@ -14,6 +14,10 @@ logger.hsqldb.level = warn
logger.hsqldbDebug.name = org.qora.repository.hsqldb.HSQLDBRepository
logger.hsqldbDebug.level = debug
# Unit test debugging
logger.tests.name = org.qora.test
logger.tests.level = debug
# Suppress extraneous Jersey warning
logger.jerseyInject.name = org.glassfish.jersey.internal.inject.Providers
logger.jerseyInject.level = error

Loading…
Cancel
Save