diff --git a/src/main/java/org/qora/api/model/AggregatedOrder.java b/src/main/java/org/qora/api/model/AggregatedOrder.java
index 3a56acfd..29865361 100644
--- a/src/main/java/org/qora/api/model/AggregatedOrder.java
+++ b/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")
diff --git a/src/main/java/org/qora/asset/Order.java b/src/main/java/org/qora/asset/Order.java
index 44f4c31c..10947dcf 100644
--- a/src/main/java/org/qora/asset/Order.java
+++ b/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,128 +77,200 @@ 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.
*
- * @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.
+ *
+ * @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.
+ *
+ * e.g. "QORA/GOLD"
+ */
+ public String getPricePair() throws DataException {
+ if (cachedPricePair == null)
+ calcPricePair();
+
+ return cachedPricePair;
+ }
+
+ /** Calculate price pair. (e.g. QORA/GOLD)
+ *
+ * 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.
+ *
+ * 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 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.
+ *
+ * For 'old' pricing, this is the have-asset.
+ * 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.
+ *
+ * For 'old' pricing, this is the want-asset.
+ * 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";
- AssetData haveAssetData = this.repository.getAssetRepository().fromAssetId(orderData.getHaveAssetId());
- AssetData wantAssetData = this.repository.getAssetRepository().fromAssetId(orderData.getWantAssetId());
+ // NOTE: the following values are specific to passed orderData, not the same as class instance values!
- LOGGER.debug(String.format("%s %s", orderPrefix, HashCode.fromBytes(orderData.getOrderId()).toString()));
+ final boolean isOrderNewAssetPricing = orderData.getTimestamp() >= BlockChain.getInstance().getNewAssetPricingTimestamp();
- LOGGER.trace(String.format("%s have: %s %s", weThey, orderData.getAmount().stripTrailingZeros().toPlainString(), haveAssetData.getName()));
+ final long haveAssetId = orderData.getHaveAssetId();
+ final long wantAssetId = orderData.getWantAssetId();
- 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()));
+ final AssetData haveAssetData = this.repository.getAssetRepository().fromAssetId(haveAssetId);
+ final AssetData wantAssetData = this.repository.getAssetRepository().fromAssetId(wantAssetId);
+
+ final long amountAssetId = (isOurOrderNewPricing && wantAssetId > haveAssetId) ? wantAssetId : haveAssetId;
+ final long returnAssetId = (isOurOrderNewPricing && haveAssetId < wantAssetId) ? haveAssetId : wantAssetId;
+
+ final AssetData amountAssetData = this.repository.getAssetRepository().fromAssetId(amountAssetId);
+ final AssetData returnAssetData = this.repository.getAssetRepository().fromAssetId(returnAssetId);
+
+ 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();
- // Subtract asset from creator
+ /** 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 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);
- /*
- * Our order example ("old"):
- *
- * haveAssetId=[GOLD], amount=10,000, wantAssetId=[QORA], price=0.002
- *
- * This translates to "we have 10,000 GOLD and want to buy QORA at a price of 0.002 QORA per GOLD"
- *
- * So if our order matched, we'd end up with 10,000 * 0.002 = 20 QORA, essentially costing 1/0.002 = 500 GOLD each.
- *
- * So 500 GOLD [each] is our (selling) unit price and want-amount is 20 QORA.
- *
- * 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
- */
+ logOrder("Processing our order", true, this.orderData);
- /*
- * Our order example ("new"):
- *
- * haveAssetId=[GOLD], amount=10,000, wantAssetId=0 (QORA), want-amount=20
- *
- * This translates to "we have 10,000 GOLD and want to buy 20 QORA"
- *
- * So if our order matched, we'd end up with 20 QORA, essentially costing 10,000 / 20 = 500 GOLD each.
- *
- * So 500 GOLD [each] is our (selling) unit price and want-amount is 20 QORA.
- *
- * Another example:
- * haveAssetId=[QORA], amount=24, wantAssetId=[GOLD], want-amount=2
- * unit price: 12.00000000 GOLD, want-amount: 2.00000000 GOLD
- */
- logOrder("Processing our order", false, this.orderData);
-
- // Fetch corresponding open orders that might potentially match, hence reversed want/have assetId args.
+ // Fetch corresponding open orders that might potentially match, hence reversed want/have assetIDs.
// Returned orders are sorted with lowest "price" first.
- List orders = assetRepository.getOpenOrders(wantAssetId, haveAssetId);
+ List orders = assetRepository.getOpenOrdersForTrading(wantAssetId, haveAssetId, isOurOrderNewPricing ? this.orderData.getPrice() : null);
LOGGER.trace("Open orders fetched from repository: " + orders.size());
if (orders.isEmpty())
@@ -195,146 +278,145 @@ public class Order {
// Attempt to match orders
- BigDecimal ourUnitPrice = this.orderData.getUnitPrice();
- LOGGER.trace(String.format("Our minimum price: %s %s per %s", ourUnitPrice.toPlainString(), wantAssetData.getName(), haveAssetData.getName()));
+ /*
+ * Potential matching order example ("old"):
+ *
+ * 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.
+ *
+ * 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"
+ *
+ * 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.
+ *
+ * Using their price, we end up with 40 * 486.00074844 = 19440.02993760 QORA. They end up with 40 GOLD.
+ *
+ * If their order had 19,440 QORA left, only 19,440 * 0.00205761 = 39.99993840 GOLD would be traded.
+ */
+
+ /*
+ * Potential matching order example ("new"):
+ *
+ * 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.
+ *
+ * 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"
+ *
+ * Their price is better than our requested 486 QORA/GOLD so this order matches.
+ *
+ * Using their price, we end up with 40 * 486.00074844 = 19440.02993760 QORA. They end up with 40 GOLD.
+ *
+ * If their order only had 36 GOLD left, only 36 * 486.00074844 = 17496.02694384 QORA would be traded.
+ */
+
+ BigDecimal ourPrice = this.orderData.getPrice();
for (OrderData theirOrderData : orders) {
- logOrder("Considering order", true, theirOrderData);
+ logOrder("Considering order", false, 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.
- */
+ // Not used:
+ // boolean isTheirOrderNewAssetPricing = theirOrderData.getTimestamp() >= BlockChain.getInstance().getNewAssetPricingTimestamp();
- /*
- * 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.
- */
+ // Determine their order price
+ BigDecimal theirPrice;
- 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());
+ // 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());
- 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());
- }
-
- // 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;
+ // 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());
- if (isTheirOrderNewAssetPricing) {
- BigDecimal theirTruncatedPrice = theirBuyingPrice.setScale(Order.BD_PRICE_SCALE, RoundingMode.DOWN);
- BigDecimal ourTruncatedPrice = ourUnitPrice.setScale(Order.BD_PRICE_SCALE, RoundingMode.DOWN);
-
- // 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
diff --git a/src/main/java/org/qora/asset/Trade.java b/src/main/java/org/qora/asset/Trade.java
index 444a8755..812f3fc7 100644
--- a/src/main/java/org/qora/asset/Trade.java
+++ b/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());
diff --git a/src/main/java/org/qora/data/asset/OrderData.java b/src/main/java/org/qora/data/asset/OrderData.java
index c12903d4..84189f2e 100644
--- a/src/main/java/org/qora/data/asset/OrderData.java
+++ b/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 {
@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 {
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 {
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 {
@Override
public int compareTo(OrderData orderData) {
// Compare using prices
- return this.unitPrice.compareTo(orderData.getUnitPrice());
+ return this.price.compareTo(orderData.getPrice());
}
}
diff --git a/src/main/java/org/qora/data/transaction/CreateAssetOrderTransactionData.java b/src/main/java/org/qora/data/transaction/CreateAssetOrderTransactionData.java
index 208b1489..346c95f9 100644
--- a/src/main/java/org/qora/data/transaction/CreateAssetOrderTransactionData.java
+++ b/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
diff --git a/src/main/java/org/qora/repository/AssetRepository.java b/src/main/java/org/qora/repository/AssetRepository.java
index b97b03fa..37bcac38 100644
--- a/src/main/java/org/qora/repository/AssetRepository.java
+++ b/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 getOpenOrdersForTrading(long haveAssetId, long wantAssetId, BigDecimal minimumPrice) throws DataException;
+
public List getAggregatedOpenOrders(long haveAssetId, long wantAssetId, Integer limit, Integer offset, Boolean reverse) throws DataException;
public List getAccountsOrders(byte[] publicKey, Boolean optIsClosed, Boolean optIsFulfilled, Integer limit, Integer offset, Boolean reverse)
diff --git a/src/main/java/org/qora/repository/hsqldb/HSQLDBAssetRepository.java b/src/main/java/org/qora/repository/hsqldb/HSQLDBAssetRepository.java
index 6d6a8174..bd23232e 100644
--- a/src/main/java/org/qora/repository/hsqldb/HSQLDBAssetRepository.java
+++ b/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 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 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 orders = new ArrayList();
+
+ 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 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 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);
+ 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, 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());
@@ -331,7 +370,7 @@ public class HSQLDBAssetRepository implements AssetRepository {
@Override
public List getAccountsOrders(byte[] publicKey, long haveAssetId, long wantAssetId, Boolean optIsClosed,
Boolean optIsFulfilled, Integer limit, Integer offset, Boolean reverse) throws DataException {
- 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 {
diff --git a/src/main/java/org/qora/repository/hsqldb/HSQLDBDatabaseUpdates.java b/src/main/java/org/qora/repository/hsqldb/HSQLDBDatabaseUpdates.java
index 80ad244a..316fb3af 100644
--- a/src/main/java/org/qora/repository/hsqldb/HSQLDBDatabaseUpdates.java
+++ b/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;
diff --git a/src/main/java/org/qora/repository/hsqldb/transaction/HSQLDBCreateAssetOrderTransactionRepository.java b/src/main/java/org/qora/repository/hsqldb/transaction/HSQLDBCreateAssetOrderTransactionRepository.java
index 517691a9..1dc663a8 100644
--- a/src/main/java/org/qora/repository/hsqldb/transaction/HSQLDBCreateAssetOrderTransactionRepository.java
+++ b/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);
diff --git a/src/main/java/org/qora/transaction/CreateAssetOrderTransaction.java b/src/main/java/org/qora/transaction/CreateAssetOrderTransaction.java
index fac31c54..47d878b4 100644
--- a/src/main/java/org/qora/transaction/CreateAssetOrderTransaction.java
+++ b/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();
diff --git a/src/main/java/org/qora/transform/transaction/CreateAssetOrderTransactionTransformer.java b/src/main/java/org/qora/transform/transaction/CreateAssetOrderTransactionTransformer.java
index def4303e..5949dab0 100644
--- a/src/main/java/org/qora/transform/transaction/CreateAssetOrderTransactionTransformer.java
+++ b/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());
diff --git a/src/test/java/org/qora/test/TransactionTests.java b/src/test/java/org/qora/test/TransactionTests.java
index efe6b47c..e738cdce 100644
--- a/src/test/java/org/qora/test/TransactionTests.java
+++ b/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);
diff --git a/src/test/java/org/qora/test/assets/GranularityTests.java b/src/test/java/org/qora/test/assets/GranularityTests.java
new file mode 100644
index 00000000..d8d75d8e
--- /dev/null
+++ b/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);
+ }
+
+}
diff --git a/src/test/java/org/qora/test/assets/MixedPricingTests.java b/src/test/java/org/qora/test/assets/MixedPricingTests.java
new file mode 100644
index 00000000..4a9d709b
--- /dev/null
+++ b/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.
+ *
+ * In this test, the order created under 'old' pricing scheme has
+ * "amount" in have-asset?
+ */
+
+}
diff --git a/src/test/java/org/qora/test/assets/NewTradingTests.java b/src/test/java/org/qora/test/assets/NewTradingTests.java
new file mode 100644
index 00000000..1be2d56d
--- /dev/null
+++ b/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.
+ *
+ * New pricing scheme allows two attempts are calculating matched amount
+ * to reduce partial-match issues caused by rounding and recurring fractional digits:
+ *
+ *
+ * - amount * round_down(1 / unit price)
+ * - round_down(amount / unit price)
+ *
+ * Alice's price is 12 QORA per ATNL so the ATNL per QORA unit price is 0.08333333...
+ * Bob wants to spend 24 QORA so:
+ *
+ *
+ * - 24 QORA * (1 / 0.0833333...) = 1.99999999 ATNL
+ * - 24 QORA / 0.08333333.... = 2 ATNL
+ *
+ * The second result is obviously more intuitive as is critical where assets are not divisible,
+ * like ATNL in this test case.
+ *
+ * @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).
+ *
+ * Alice is selling twice as much as Bob wants,
+ * but at the same [calculated] unit price,
+ * so Bob's order should fully match.
+ *
+ * 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).
+ *
+ * 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.
+ *
+ * For example, sell 2 TEST for 24 OTHER so
+ * unit price is 2 / 24 or 0.08333333(recurring) TEST/OTHER.
+ *
+ * But although price is rounded down to 0.08333333,
+ * the price is the same for both sides.
+ *
+ * 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> 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());
+ }
+ }
+
+}
\ No newline at end of file
diff --git a/src/test/java/org/qora/test/assets/OldTradingTests.java b/src/test/java/org/qora/test/assets/OldTradingTests.java
new file mode 100644
index 00000000..a2449d60
--- /dev/null
+++ b/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.
+ *
+ * We use orders similar to some found in legacy qora1 blockchain
+ * to test for expected results with indivisible assets.
+ *
+ * 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> 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.
+ *
+ * For example, sell 2 TEST for 24 QORA so
+ * unit price is 2 / 24 or 0.08333333.
+ *
+ * This inexactness causes the match amount to be
+ * only 1.99999992 instead of the expected 2.00000000.
+ *
+ * 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);
+ }
+
+}
\ No newline at end of file
diff --git a/src/test/java/org/qora/test/assets/TradingTests.java b/src/test/java/org/qora/test/assets/TradingTests.java
deleted file mode 100644
index 7662ecfa..00000000
--- a/src/test/java/org/qora/test/assets/TradingTests.java
+++ /dev/null
@@ -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.
- *
- * 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.
- *
- * 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.
- *
- * New pricing scheme allows two attempts are calculating matched amount
- * to reduce partial-match issues caused by rounding and recurring fractional digits:
- *
- *
- * - amount * round_down(1 / unit price)
- * - round_down(amount / unit price)
- *
- * Alice's price is 12 QORA per ATNL so the ATNL per QORA unit price is 0.08333333...
- * Bob wants to spend 24 QORA so:
- *
- *
- * - 24 QORA * (1 / 0.0833333...) = 1.99999999 ATNL
- * - 24 QORA / 0.08333333.... = 2 ATNL
- *
- * The second result is obviously more intuitive as is critical where assets are not divisible,
- * like ATNL in this test case.
- *
- * @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).
- *
- * Alice is selling twice as much as Bob wants,
- * but at the same [calculated] unit price,
- * so Bob's order should fully match.
- *
- * 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.
- *
- * We use orders similar to some found in legacy qora1 blockchain
- * to test for expected results with indivisible assets.
- *
- * 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> 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.
- *
- * 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.
- *
- * For example, sell 2 TEST for 24 QORA so
- * unit price is 2 / 24 or 0.08333333.
- *
- * This inexactness causes the match amount to be
- * only 1.99999992 instead of the expected 2.00000000.
- *
- * 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> 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> 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);
- }
-
-}
\ No newline at end of file
diff --git a/src/test/java/org/qora/test/common/AccountUtils.java b/src/test/java/org/qora/test/common/AccountUtils.java
index 63e6012a..3e2d3989 100644
--- a/src/test/java/org/qora/test/common/AccountUtils.java
+++ b/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);
+ }
+
}
diff --git a/src/test/java/org/qora/test/common/AssetUtils.java b/src/test/java/org/qora/test/common/AssetUtils.java
index 9cca4eb1..543f0f20 100644
--- a/src/test/java/org/qora/test/common/AssetUtils.java
+++ b/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> 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());
+ }
+ }
+
}
diff --git a/src/test/java/org/qora/test/common/Common.java b/src/test/java/org/qora/test/common/Common.java
index 578e4ddf..a19684c1 100644
--- a/src/test/java/org/qora/test/common/Common.java
+++ b/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 initialAssets;
private static List initialGroups;
private static List 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 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) {
diff --git a/src/test/java/org/qora/test/common/ObjectUtils.java b/src/test/java/org/qora/test/common/ObjectUtils.java
new file mode 100644
index 00000000..4acd035c
--- /dev/null
+++ b/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);
+ }
+ }
+
+}
diff --git a/src/test/resources/log4j2-test.properties b/src/test/resources/log4j2-test.properties
index a74d53ed..5c8723e1 100644
--- a/src/test/resources/log4j2-test.properties
+++ b/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