mirror of
https://github.com/Qortal/qortal.git
synced 2025-07-22 20:26:50 +00:00
Interim commit on new asset trading schema
Better order matching, especially in situations where inexact fractional representations (e.g. 1/12) or rounding issues might occur. Also better matching with indivisible assets. Essentially change ordering from have-amount & price to have-amount and want-return, leaving unit price to be calculated internally to a finer degree (in some cases to 48 decimal points). Corresponding unit tests to cover both legacy and new scenarios. Support for tests to switch between blockchain configs. "New" pricing schema is its own 'feature trigger' independent from general qorav2 switch. Safety checks added during trading process. HSQLDB schema changes (will probably need careful conflict resolution on merge). Still to do: API changes etc.
This commit is contained in:
@@ -20,9 +20,9 @@ public class AggregatedOrder {
|
||||
this.orderData = orderData;
|
||||
}
|
||||
|
||||
@XmlElement(name = "price")
|
||||
public BigDecimal getPrice() {
|
||||
return this.orderData.getPrice();
|
||||
@XmlElement(name = "unitPrice")
|
||||
public BigDecimal getUnitPrice() {
|
||||
return this.orderData.getUnitPrice();
|
||||
}
|
||||
|
||||
@XmlElement(name = "unfulfilled")
|
||||
|
@@ -20,7 +20,7 @@ public class Asset {
|
||||
public static final int MAX_DESCRIPTION_SIZE = 4000;
|
||||
public static final int MAX_DATA_SIZE = 400000;
|
||||
|
||||
public static final long MAX_DIVISIBLE_QUANTITY = 10_000_000_000L;
|
||||
public static final long MAX_DIVISIBLE_QUANTITY = 10_000_000_000L; // but also to 8 decimal places
|
||||
public static final long MAX_INDIVISIBLE_QUANTITY = 1_000_000_000_000_000_000L;
|
||||
|
||||
// Properties
|
||||
|
@@ -6,6 +6,7 @@ import java.math.RoundingMode;
|
||||
import java.util.Arrays;
|
||||
import java.util.List;
|
||||
|
||||
import org.apache.logging.log4j.Level;
|
||||
import org.apache.logging.log4j.LogManager;
|
||||
import org.apache.logging.log4j.Logger;
|
||||
import org.qora.account.Account;
|
||||
@@ -22,6 +23,11 @@ import com.google.common.hash.HashCode;
|
||||
|
||||
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
|
||||
@@ -59,33 +65,41 @@ public class Order {
|
||||
return Order.isFulfilled(this.orderData);
|
||||
}
|
||||
|
||||
public BigDecimal calculateAmountGranularity(AssetData haveAssetData, AssetData wantAssetData, OrderData theirOrderData) {
|
||||
// 100 million to scale BigDecimal.setScale(8) fractional amounts into integers, essentially 1e8
|
||||
BigInteger multiplier = BigInteger.valueOf(100_000_000L);
|
||||
/**
|
||||
* Returns want-asset granularity/unit-size given price.
|
||||
* <p>
|
||||
* @param theirPrice
|
||||
* @return unit price of want asset
|
||||
*/
|
||||
public static BigDecimal calculateAmountGranularity(AssetData haveAssetData, AssetData wantAssetData, BigDecimal theirPrice) {
|
||||
// Multiplier to scale BigDecimal.setScale(8) fractional amounts into integers, essentially 1e8
|
||||
BigInteger multiplier = BigInteger.valueOf(1_0000_0000L);
|
||||
|
||||
// Calculate the minimum increment at which I can buy using greatest-common-divisor
|
||||
BigInteger haveAmount = BigInteger.ONE.multiply(multiplier);
|
||||
BigInteger priceAmount = theirOrderData.getPrice().multiply(new BigDecimal(multiplier)).toBigInteger();
|
||||
BigInteger gcd = haveAmount.gcd(priceAmount);
|
||||
BigInteger haveAmount = multiplier; // 1 unit (* multiplier)
|
||||
//BigInteger wantAmount = BigDecimal.valueOf(100_000_000L).setScale(Asset.BD_SCALE).divide(theirOrderData.getUnitPrice(), RoundingMode.DOWN).toBigInteger();
|
||||
BigInteger wantAmount = theirPrice.movePointRight(8).toBigInteger();
|
||||
|
||||
BigInteger gcd = haveAmount.gcd(wantAmount);
|
||||
haveAmount = haveAmount.divide(gcd);
|
||||
priceAmount = priceAmount.divide(gcd);
|
||||
wantAmount = wantAmount.divide(gcd);
|
||||
|
||||
// Calculate GCD in combination with divisibility
|
||||
if (wantAssetData.getIsDivisible())
|
||||
haveAmount = haveAmount.multiply(multiplier);
|
||||
|
||||
if (haveAssetData.getIsDivisible())
|
||||
priceAmount = priceAmount.multiply(multiplier);
|
||||
wantAmount = wantAmount.multiply(multiplier);
|
||||
|
||||
gcd = haveAmount.gcd(priceAmount);
|
||||
gcd = haveAmount.gcd(wantAmount);
|
||||
|
||||
// Calculate the increment at which we have to buy
|
||||
BigDecimal increment = new BigDecimal(haveAmount.divide(gcd));
|
||||
// Calculate the granularity at which we have to buy
|
||||
BigDecimal granularity = new BigDecimal(haveAmount.divide(gcd));
|
||||
if (wantAssetData.getIsDivisible())
|
||||
increment = increment.divide(new BigDecimal(multiplier));
|
||||
granularity = granularity.movePointLeft(8);
|
||||
|
||||
// Return
|
||||
return increment;
|
||||
return granularity;
|
||||
}
|
||||
|
||||
// Navigation
|
||||
@@ -96,6 +110,32 @@ public class Order {
|
||||
|
||||
// Processing
|
||||
|
||||
private void logOrder(String orderPrefix, boolean isMatchingNotInitial, OrderData orderData) throws DataException {
|
||||
// Avoid calculations if possible
|
||||
if (LOGGER.getLevel().isMoreSpecificThan(Level.DEBUG))
|
||||
return;
|
||||
|
||||
final boolean isNewPricing = orderData.getTimestamp() >= BlockChain.getInstance().getNewAssetPricingTimestamp();
|
||||
final String weThey = isMatchingNotInitial ? "They" : "We";
|
||||
|
||||
AssetData haveAssetData = this.repository.getAssetRepository().fromAssetId(orderData.getHaveAssetId());
|
||||
AssetData wantAssetData = this.repository.getAssetRepository().fromAssetId(orderData.getWantAssetId());
|
||||
|
||||
LOGGER.debug(String.format("%s %s", orderPrefix, HashCode.fromBytes(orderData.getOrderId()).toString()));
|
||||
|
||||
LOGGER.trace(String.format("%s have: %s %s", weThey, orderData.getAmount().stripTrailingZeros().toPlainString(), haveAssetData.getName()));
|
||||
|
||||
if (isNewPricing) {
|
||||
LOGGER.trace(String.format("%s want: %s %s (@ %s %s each)", weThey,
|
||||
orderData.getWantAmount().stripTrailingZeros().toPlainString(), wantAssetData.getName(),
|
||||
orderData.getUnitPrice().toPlainString(), haveAssetData.getName()));
|
||||
} else {
|
||||
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()));
|
||||
}
|
||||
}
|
||||
|
||||
public void process() throws DataException {
|
||||
AssetRepository assetRepository = this.repository.getAssetRepository();
|
||||
|
||||
@@ -111,64 +151,57 @@ public class Order {
|
||||
// Save this order into repository so it's available for matching, possibly by itself
|
||||
this.repository.getAssetRepository().save(this.orderData);
|
||||
|
||||
boolean isOurOrderV2 = this.orderData.getTimestamp() >= BlockChain.getInstance().getQoraV2Timestamp();
|
||||
/*
|
||||
* 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
|
||||
*/
|
||||
|
||||
// Attempt to match orders
|
||||
LOGGER.debug("Processing our order " + HashCode.fromBytes(this.orderData.getOrderId()).toString());
|
||||
LOGGER.trace("We have: " + this.orderData.getAmount().toPlainString() + " " + haveAssetData.getName());
|
||||
|
||||
if (isOurOrderV2)
|
||||
LOGGER.trace("We want " + this.orderData.getPrice().toPlainString() + " " + wantAssetData.getName());
|
||||
else
|
||||
LOGGER.trace("We want " + this.orderData.getPrice().toPlainString() + " " + wantAssetData.getName() + " per " + haveAssetData.getName());
|
||||
/*
|
||||
* 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.
|
||||
// Returned orders are sorted with lowest "price" first.
|
||||
List<OrderData> orders = assetRepository.getOpenOrders(wantAssetId, haveAssetId);
|
||||
LOGGER.trace("Open orders fetched from repository: " + orders.size());
|
||||
|
||||
/*
|
||||
* Our order example (V1):
|
||||
*
|
||||
* haveAssetId=[GOLD], amount=10,000, wantAssetId=0 (QORA), price=0.002
|
||||
*
|
||||
* This translates to "we have 10,000 GOLD and want to buy QORA at a price of 0.002 QORA per GOLD"
|
||||
*
|
||||
* So if our order matched, we'd end up with 10,000 * 0.002 = 20 QORA, essentially costing 1/0.002 = 500 GOLD each.
|
||||
*
|
||||
* So 500 GOLD [each] is our "buyingPrice".
|
||||
*
|
||||
* Our order example (V2):
|
||||
*
|
||||
* haveAssetId=[GOLD], amount=10,000, wantAssetId=0 (QORA), price=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 "buyingPrice".
|
||||
*/
|
||||
BigDecimal ourAmount = this.orderData.getAmount();
|
||||
BigDecimal ourPrice;
|
||||
if (isOurOrderV2)
|
||||
ourPrice = ourAmount.divide(this.orderData.getPrice(), RoundingMode.DOWN);
|
||||
else
|
||||
ourPrice = this.orderData.getPrice();
|
||||
if (orders.isEmpty())
|
||||
return;
|
||||
|
||||
// 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()));
|
||||
|
||||
for (OrderData theirOrderData : orders) {
|
||||
LOGGER.trace("Considering order " + HashCode.fromBytes(theirOrderData.getOrderId()).toString());
|
||||
// Note swapped use of have/want asset data as this is from 'their' perspective.
|
||||
LOGGER.trace("They have: " + theirOrderData.getAmount().toPlainString() + " " + wantAssetData.getName());
|
||||
|
||||
boolean isTheirOrderV2 = theirOrderData.getTimestamp() >= BlockChain.getInstance().getQoraV2Timestamp();
|
||||
|
||||
if (isTheirOrderV2)
|
||||
LOGGER.trace("They want " + theirOrderData.getPrice().toPlainString() + " " + haveAssetData.getName());
|
||||
else
|
||||
LOGGER.trace("They want " + theirOrderData.getPrice().toPlainString() + " " + haveAssetData.getName() + " per " + wantAssetData.getName());
|
||||
logOrder("Considering order", true, theirOrderData);
|
||||
|
||||
/*
|
||||
* Potential matching order example (V1):
|
||||
* Potential matching order example ("old"):
|
||||
*
|
||||
* haveAssetId=0 (QORA), amount=40, wantAssetId=[GOLD], price=486
|
||||
*
|
||||
@@ -176,9 +209,11 @@ public class Order {
|
||||
*
|
||||
* So if their order matched, they'd end up with 40 * 486 = 19,440 GOLD, essentially costing 1/486 = 0.00205761 QORA each.
|
||||
*
|
||||
* So 0.00205761 QORA [each] is their "buyingPrice".
|
||||
*
|
||||
* Potential matching order example (V2):
|
||||
* So 0.00205761 QORA [each] is their unit price and maximum amount is 19,440 GOLD.
|
||||
*/
|
||||
|
||||
/*
|
||||
* Potential matching order example ("new"):
|
||||
*
|
||||
* haveAssetId=0 (QORA), amount=40, wantAssetId=[GOLD], price=19,440
|
||||
*
|
||||
@@ -186,69 +221,124 @@ public class Order {
|
||||
*
|
||||
* 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 "buyingPrice".
|
||||
* So 0.00205761 QORA [each] is their unit price and maximum amount is 19,440 GOLD.
|
||||
*/
|
||||
|
||||
// Round down otherwise their buyingPrice would be better than advertised and cause issues
|
||||
BigDecimal theirBuyingPrice;
|
||||
boolean isTheirOrderNewAssetPricing = theirOrderData.getTimestamp() >= BlockChain.getInstance().getNewAssetPricingTimestamp();
|
||||
|
||||
if (isTheirOrderV2)
|
||||
theirBuyingPrice = theirOrderData.getAmount().divide(theirOrderData.getPrice(), RoundingMode.DOWN);
|
||||
else
|
||||
theirBuyingPrice = BigDecimal.ONE.setScale(8).divide(theirOrderData.getPrice(), RoundingMode.DOWN);
|
||||
LOGGER.trace("theirBuyingPrice: " + theirBuyingPrice.toPlainString() + " " + wantAssetData.getName() + " per " + haveAssetData.getName());
|
||||
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 their buyingPrice is less than what we're willing to pay then we're done as prices only get worse as we iterate through list of orders
|
||||
if (theirBuyingPrice.compareTo(ourPrice) < 0)
|
||||
// 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)
|
||||
break;
|
||||
|
||||
// Calculate how many want-asset we could buy at their price
|
||||
BigDecimal ourAmountLeft = this.getAmountLeft().multiply(theirBuyingPrice).setScale(8, RoundingMode.DOWN);
|
||||
LOGGER.trace("ourAmountLeft (max we could buy at their price): " + ourAmountLeft.toPlainString() + " " + wantAssetData.getName());
|
||||
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());
|
||||
|
||||
// How many want-asset is remaining available in this order
|
||||
BigDecimal theirAmountLeft = Order.getAmountLeft(theirOrderData);
|
||||
LOGGER.trace("theirAmountLeft (max amount remaining in order): " + theirAmountLeft.toPlainString() + " " + wantAssetData.getName());
|
||||
if (isTheirOrderNewAssetPricing) {
|
||||
ourMaxWantAmount = ourMaxWantAmount.max(this.getAmountLeft().divide(theirOrderData.getUnitPrice(), RoundingMode.DOWN).setScale(8, RoundingMode.DOWN));
|
||||
LOGGER.trace("ourMaxWantAmount (max we could buy at their price) using inverted calculation: " + ourMaxWantAmount.stripTrailingZeros().toPlainString() + " " + wantAssetData.getName());
|
||||
}
|
||||
|
||||
// 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());
|
||||
|
||||
// So matchable want-asset amount is the minimum of above two values
|
||||
BigDecimal matchedAmount = ourAmountLeft.min(theirAmountLeft);
|
||||
LOGGER.trace("matchedAmount: " + matchedAmount.toPlainString() + " " + wantAssetData.getName());
|
||||
BigDecimal matchedWantAmount = ourMaxWantAmount.min(theirWantAmountLeft);
|
||||
LOGGER.trace("matchedWantAmount: " + matchedWantAmount.stripTrailingZeros().toPlainString() + " " + wantAssetData.getName());
|
||||
|
||||
// If we can't buy anything then try another order
|
||||
if (matchedAmount.compareTo(BigDecimal.ZERO) <= 0)
|
||||
if (matchedWantAmount.compareTo(BigDecimal.ZERO) <= 0)
|
||||
continue;
|
||||
|
||||
// Calculate amount granularity based on both assets' divisibility
|
||||
BigDecimal increment = this.calculateAmountGranularity(haveAssetData, wantAssetData, theirOrderData);
|
||||
LOGGER.trace("increment (want-asset amount granularity): " + increment.toPlainString() + " " + wantAssetData.getName());
|
||||
matchedAmount = matchedAmount.subtract(matchedAmount.remainder(increment));
|
||||
LOGGER.trace("matchedAmount adjusted for granularity: " + matchedAmount.toPlainString() + " " + wantAssetData.getName());
|
||||
// We can skip granularity if theirWantAmountLeft is an [integer] multiple of matchedWantAmount as that obviously fits
|
||||
if (!isTheirOrderNewAssetPricing || theirWantAmountLeft.remainder(matchedWantAmount).compareTo(BigDecimal.ZERO) > 0) {
|
||||
// Not an integer multiple so do granularity check
|
||||
|
||||
// Calculate amount granularity based on both assets' divisibility
|
||||
BigDecimal wantGranularity = calculateAmountGranularity(haveAssetData, wantAssetData, theirOrderData.getUnitPrice());
|
||||
LOGGER.trace("wantGranularity (want-asset amount granularity): " + wantGranularity.stripTrailingZeros().toPlainString() + " " + wantAssetData.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());
|
||||
}
|
||||
|
||||
// If we can't buy anything then try another order
|
||||
if (matchedAmount.compareTo(BigDecimal.ZERO) <= 0)
|
||||
if (matchedWantAmount.compareTo(BigDecimal.ZERO) <= 0)
|
||||
continue;
|
||||
|
||||
// Safety checks
|
||||
if (matchedWantAmount.compareTo(Order.getAmountLeft(theirOrderData)) > 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());
|
||||
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 tradePrice;
|
||||
if (isTheirOrderV2)
|
||||
tradePrice = matchedAmount.divide(theirBuyingPrice).setScale(8); // XXX is this right?
|
||||
else
|
||||
tradePrice = matchedAmount.multiply(theirOrderData.getPrice()).setScale(8);
|
||||
LOGGER.trace("tradePrice ('want' trade agreed): " + tradePrice.toPlainString() + " " + haveAssetData.getName());
|
||||
BigDecimal haveAmountTraded;
|
||||
|
||||
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());
|
||||
LOGGER.error(message);
|
||||
throw new DataException(message);
|
||||
}
|
||||
|
||||
// Construct trade
|
||||
TradeData tradeData = new TradeData(this.orderData.getOrderId(), theirOrderData.getOrderId(), matchedAmount, tradePrice,
|
||||
TradeData tradeData = new TradeData(this.orderData.getOrderId(), theirOrderData.getOrderId(), matchedWantAmount, haveAmountTraded,
|
||||
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(tradePrice));
|
||||
LOGGER.trace("Updated our order's fulfilled amount to: " + this.orderData.getFulfilled().toPlainString() + " " + haveAssetData.getName());
|
||||
LOGGER.trace("Our order's amount remaining: " + this.getAmountLeft().toPlainString() + " " + haveAssetData.getName());
|
||||
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());
|
||||
|
||||
// Continue on to process other open orders if we still have amount left to match
|
||||
if (this.getAmountLeft().compareTo(BigDecimal.ZERO) <= 0)
|
||||
|
@@ -74,7 +74,8 @@ public class BlockChain {
|
||||
votingTimestamp,
|
||||
arbitraryTimestamp,
|
||||
powfixTimestamp,
|
||||
v2Timestamp;
|
||||
v2Timestamp,
|
||||
newAssetPricingTimestamp;
|
||||
}
|
||||
|
||||
/** Map of which blockchain features are enabled when (height/timestamp) */
|
||||
@@ -251,6 +252,10 @@ public class BlockChain {
|
||||
return featureTriggers.get("v2Timestamp");
|
||||
}
|
||||
|
||||
public long getNewAssetPricingTimestamp() {
|
||||
return featureTriggers.get("newAssetPricingTimestamp");
|
||||
}
|
||||
|
||||
/** Validate blockchain config read from JSON */
|
||||
private void validateConfig() {
|
||||
if (this.genesisInfo == null) {
|
||||
|
@@ -24,8 +24,11 @@ public class OrderData implements Comparable<OrderData> {
|
||||
@Schema(description = "amount of \"have\" asset to trade")
|
||||
private BigDecimal amount;
|
||||
|
||||
@Schema(description = "amount of \"want\" asset to receive")
|
||||
private BigDecimal wantAmount;
|
||||
|
||||
@Schema(description = "amount of \"want\" asset to receive per unit of \"have\" asset traded")
|
||||
private BigDecimal price;
|
||||
private BigDecimal unitPrice;
|
||||
|
||||
@Schema(description = "how much \"have\" asset has traded")
|
||||
private BigDecimal fulfilled;
|
||||
@@ -44,22 +47,24 @@ public class OrderData implements Comparable<OrderData> {
|
||||
protected OrderData() {
|
||||
}
|
||||
|
||||
public OrderData(byte[] orderId, byte[] creatorPublicKey, long haveAssetId, long wantAssetId, BigDecimal amount, BigDecimal fulfilled, BigDecimal price,
|
||||
long timestamp, boolean isClosed, boolean isFulfilled) {
|
||||
public OrderData(byte[] orderId, byte[] creatorPublicKey, long haveAssetId, long wantAssetId, BigDecimal amount, BigDecimal fulfilled, BigDecimal wantAmount,
|
||||
BigDecimal unitPrice, 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.price = price;
|
||||
this.wantAmount = wantAmount;
|
||||
this.unitPrice = unitPrice;
|
||||
this.timestamp = timestamp;
|
||||
this.isClosed = isClosed;
|
||||
this.isFulfilled = isFulfilled;
|
||||
}
|
||||
|
||||
public OrderData(byte[] orderId, byte[] creatorPublicKey, long haveAssetId, long wantAssetId, BigDecimal amount, BigDecimal price, long timestamp) {
|
||||
this(orderId, creatorPublicKey, haveAssetId, wantAssetId, amount, BigDecimal.ZERO.setScale(8), price, timestamp, false, false);
|
||||
/** 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);
|
||||
}
|
||||
|
||||
// Getters/setters
|
||||
@@ -92,8 +97,12 @@ public class OrderData implements Comparable<OrderData> {
|
||||
this.fulfilled = fulfilled;
|
||||
}
|
||||
|
||||
public BigDecimal getPrice() {
|
||||
return this.price;
|
||||
public BigDecimal getWantAmount() {
|
||||
return this.wantAmount;
|
||||
}
|
||||
|
||||
public BigDecimal getUnitPrice() {
|
||||
return this.unitPrice;
|
||||
}
|
||||
|
||||
public long getTimestamp() {
|
||||
@@ -119,7 +128,7 @@ public class OrderData implements Comparable<OrderData> {
|
||||
@Override
|
||||
public int compareTo(OrderData orderData) {
|
||||
// Compare using prices
|
||||
return this.price.compareTo(orderData.getPrice());
|
||||
return this.unitPrice.compareTo(orderData.getUnitPrice());
|
||||
}
|
||||
|
||||
}
|
||||
|
@@ -39,7 +39,7 @@ public interface AssetRepository {
|
||||
|
||||
public List<OrderData> getOpenOrders(long haveAssetId, long wantAssetId, Integer limit, Integer offset, Boolean reverse) throws DataException;
|
||||
|
||||
// Internal, non-API use
|
||||
/** Returns open orders, ordered by ascending unit price (i.e. best price first), for use by order matching logic. */
|
||||
public default List<OrderData> getOpenOrders(long haveAssetId, long wantAssetId) throws DataException {
|
||||
return getOpenOrders(haveAssetId, wantAssetId, null, null, null);
|
||||
}
|
||||
|
@@ -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, price, ordered, is_closed, is_fulfilled FROM AssetOrders WHERE asset_order_id = ?",
|
||||
"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 = ?",
|
||||
orderId)) {
|
||||
if (resultSet == null)
|
||||
return null;
|
||||
@@ -202,13 +202,13 @@ public class HSQLDBAssetRepository implements AssetRepository {
|
||||
long wantAssetId = resultSet.getLong(3);
|
||||
BigDecimal amount = resultSet.getBigDecimal(4);
|
||||
BigDecimal fulfilled = resultSet.getBigDecimal(5);
|
||||
BigDecimal price = resultSet.getBigDecimal(6);
|
||||
long timestamp = resultSet.getTimestamp(7, Calendar.getInstance(HSQLDBRepository.UTC)).getTime();
|
||||
boolean isClosed = resultSet.getBoolean(8);
|
||||
boolean isFulfilled = resultSet.getBoolean(9);
|
||||
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);
|
||||
|
||||
return new OrderData(orderId, creatorPublicKey, haveAssetId, wantAssetId, amount, fulfilled, price,
|
||||
timestamp, isClosed, isFulfilled);
|
||||
return new OrderData(orderId, creatorPublicKey, haveAssetId, wantAssetId, amount, fulfilled, wantAmount, unitPrice, timestamp, isClosed, isFulfilled);
|
||||
} catch (SQLException e) {
|
||||
throw new DataException("Unable to fetch asset order from repository", e);
|
||||
}
|
||||
@@ -217,8 +217,8 @@ public class HSQLDBAssetRepository implements AssetRepository {
|
||||
@Override
|
||||
public List<OrderData> getOpenOrders(long haveAssetId, long wantAssetId, Integer limit, Integer offset,
|
||||
Boolean reverse) throws DataException {
|
||||
String sql = "SELECT creator, asset_order_id, amount, fulfilled, price, ordered FROM AssetOrders "
|
||||
+ "WHERE have_asset_id = ? AND want_asset_id = ? AND is_closed = FALSE AND is_fulfilled = FALSE ORDER BY price";
|
||||
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";
|
||||
if (reverse != null && reverse)
|
||||
sql += " DESC";
|
||||
sql += ", ordered";
|
||||
@@ -237,13 +237,14 @@ public class HSQLDBAssetRepository implements AssetRepository {
|
||||
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();
|
||||
BigDecimal wantAmount = resultSet.getBigDecimal(5);
|
||||
BigDecimal unitPrice = resultSet.getBigDecimal(6);
|
||||
long timestamp = resultSet.getTimestamp(7, 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);
|
||||
wantAmount, unitPrice, timestamp, isClosed, isFulfilled);
|
||||
orders.add(order);
|
||||
} while (resultSet.next());
|
||||
|
||||
@@ -256,8 +257,8 @@ public class HSQLDBAssetRepository implements AssetRepository {
|
||||
@Override
|
||||
public List<OrderData> getAggregatedOpenOrders(long haveAssetId, long wantAssetId, Integer limit, Integer offset,
|
||||
Boolean reverse) throws DataException {
|
||||
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";
|
||||
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";
|
||||
if (reverse != null && reverse)
|
||||
sql += " DESC";
|
||||
sql += HSQLDBRepository.limitOffsetSql(limit, offset);
|
||||
@@ -269,12 +270,12 @@ public class HSQLDBAssetRepository implements AssetRepository {
|
||||
return orders;
|
||||
|
||||
do {
|
||||
BigDecimal price = resultSet.getBigDecimal(1);
|
||||
BigDecimal unitPrice = 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,
|
||||
price, timestamp, false, false);
|
||||
BigDecimal.ZERO, unitPrice, timestamp, false, false);
|
||||
orders.add(order);
|
||||
} while (resultSet.next());
|
||||
|
||||
@@ -287,7 +288,8 @@ public class HSQLDBAssetRepository implements AssetRepository {
|
||||
@Override
|
||||
public List<OrderData> getAccountsOrders(byte[] publicKey, Boolean optIsClosed, Boolean optIsFulfilled,
|
||||
Integer limit, Integer offset, Boolean reverse) throws DataException {
|
||||
String sql = "SELECT asset_order_id, have_asset_id, want_asset_id, amount, fulfilled, price, ordered, is_closed, is_fulfilled FROM AssetOrders WHERE creator = ?";
|
||||
String sql = "SELECT asset_order_id, have_asset_id, want_asset_id, amount, fulfilled, want_amount, unit_price, ordered, is_closed, is_fulfilled "
|
||||
+ "FROM AssetOrders WHERE creator = ?";
|
||||
if (optIsClosed != null)
|
||||
sql += " AND is_closed = " + (optIsClosed ? "TRUE" : "FALSE");
|
||||
if (optIsFulfilled != null)
|
||||
@@ -309,13 +311,14 @@ public class HSQLDBAssetRepository implements AssetRepository {
|
||||
long wantAssetId = resultSet.getLong(3);
|
||||
BigDecimal amount = resultSet.getBigDecimal(4);
|
||||
BigDecimal fulfilled = resultSet.getBigDecimal(5);
|
||||
BigDecimal price = resultSet.getBigDecimal(6);
|
||||
long timestamp = resultSet.getTimestamp(7, Calendar.getInstance(HSQLDBRepository.UTC)).getTime();
|
||||
boolean isClosed = resultSet.getBoolean(8);
|
||||
boolean isFulfilled = resultSet.getBoolean(9);
|
||||
BigDecimal wantAmount = resultSet.getBigDecimal(6);
|
||||
BigDecimal unitPrice = resultSet.getBigDecimal(7);
|
||||
long timestamp = resultSet.getTimestamp(8, Calendar.getInstance(HSQLDBRepository.UTC)).getTime();
|
||||
boolean isClosed = resultSet.getBoolean(9);
|
||||
boolean isFulfilled = resultSet.getBoolean(10);
|
||||
|
||||
OrderData order = new OrderData(orderId, publicKey, haveAssetId, wantAssetId, amount, fulfilled, price,
|
||||
timestamp, isClosed, isFulfilled);
|
||||
OrderData order = new OrderData(orderId, publicKey, haveAssetId, wantAssetId, amount, fulfilled, wantAmount,
|
||||
unitPrice, timestamp, isClosed, isFulfilled);
|
||||
orders.add(order);
|
||||
} while (resultSet.next());
|
||||
|
||||
@@ -328,7 +331,8 @@ public class HSQLDBAssetRepository implements AssetRepository {
|
||||
@Override
|
||||
public List<OrderData> getAccountsOrders(byte[] publicKey, long haveAssetId, long wantAssetId, Boolean optIsClosed,
|
||||
Boolean optIsFulfilled, Integer limit, Integer offset, Boolean reverse) throws DataException {
|
||||
String sql = "SELECT asset_order_id, amount, fulfilled, price, ordered, is_closed, is_fulfilled FROM AssetOrders WHERE creator = ? AND have_asset_id = ? AND want_asset_id = ?";
|
||||
String sql = "SELECT asset_order_id, amount, fulfilled, want_amount, unit_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");
|
||||
if (optIsFulfilled != null)
|
||||
@@ -348,13 +352,14 @@ public class HSQLDBAssetRepository implements AssetRepository {
|
||||
byte[] orderId = resultSet.getBytes(1);
|
||||
BigDecimal amount = resultSet.getBigDecimal(2);
|
||||
BigDecimal fulfilled = resultSet.getBigDecimal(3);
|
||||
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);
|
||||
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);
|
||||
|
||||
OrderData order = new OrderData(orderId, publicKey, haveAssetId, wantAssetId, amount, fulfilled, price,
|
||||
timestamp, isClosed, isFulfilled);
|
||||
OrderData order = new OrderData(orderId, publicKey, haveAssetId, wantAssetId, amount, fulfilled, wantAmount,
|
||||
unitPrice, timestamp, isClosed, isFulfilled);
|
||||
orders.add(order);
|
||||
} while (resultSet.next());
|
||||
|
||||
@@ -371,7 +376,8 @@ 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("price", orderData.getPrice()).bind("ordered", new Timestamp(orderData.getTimestamp()))
|
||||
.bind("want_amount", orderData.getWantAmount()).bind("unit_price", orderData.getUnitPrice())
|
||||
.bind("ordered", new Timestamp(orderData.getTimestamp()))
|
||||
.bind("is_closed", orderData.getIsClosed()).bind("is_fulfilled", orderData.getIsFulfilled());
|
||||
|
||||
try {
|
||||
@@ -395,8 +401,9 @@ public class HSQLDBAssetRepository implements AssetRepository {
|
||||
@Override
|
||||
public List<TradeData> getTrades(long haveAssetId, long wantAssetId, Integer limit, Integer offset, Boolean reverse)
|
||||
throws DataException {
|
||||
String sql = "SELECT initiating_order_id, target_order_id, AssetTrades.target_amount, AssetTrades.initiator_amount, traded FROM AssetOrders JOIN AssetTrades ON initiating_order_id = asset_order_id "
|
||||
+ "WHERE have_asset_id = ? AND want_asset_id = ? ORDER BY traded";
|
||||
String sql = "SELECT initiating_order_id, target_order_id, AssetTrades.target_amount, AssetTrades.initiator_amount, traded "
|
||||
+ "FROM AssetOrders JOIN AssetTrades ON initiating_order_id = asset_order_id "
|
||||
+ "WHERE have_asset_id = ? AND want_asset_id = ? ORDER BY traded";
|
||||
if (reverse != null && reverse)
|
||||
sql += " DESC";
|
||||
sql += HSQLDBRepository.limitOffsetSql(limit, offset);
|
||||
@@ -497,7 +504,8 @@ public class HSQLDBAssetRepository implements AssetRepository {
|
||||
@Override
|
||||
public List<TradeData> getOrdersTrades(byte[] orderId, Integer limit, Integer offset, Boolean reverse)
|
||||
throws DataException {
|
||||
String sql = "SELECT initiating_order_id, target_order_id, target_amount, initiator_amount, traded FROM AssetTrades WHERE initiating_order_id = ? OR target_order_id = ? ORDER BY traded";
|
||||
String sql = "SELECT initiating_order_id, target_order_id, target_amount, initiator_amount, traded "
|
||||
+ "FROM AssetTrades WHERE initiating_order_id = ? OR target_order_id = ? ORDER BY traded";
|
||||
if (reverse != null && reverse)
|
||||
sql += " DESC";
|
||||
sql += HSQLDBRepository.limitOffsetSql(limit, offset);
|
||||
|
@@ -630,6 +630,31 @@ public class HSQLDBDatabaseUpdates {
|
||||
stmt.execute("ALTER TABLE UpdateAssetTransactions ALTER COLUMN new_data AssetDataLob");
|
||||
break;
|
||||
|
||||
case 41:
|
||||
// New asset pricing
|
||||
/*
|
||||
* We store "unit price" for asset orders but need enough precision to accurately
|
||||
* represent fractional values without loss.
|
||||
* Asset quantities can be up to either 1_000_000_000_000_000_000 (19 digits) if indivisible,
|
||||
* or 10_000_000_000.00000000 (11+8 = 19 digits) if divisible.
|
||||
* Two 19-digit numbers need 38 integer and 38 fractional to cover extremes of unit price.
|
||||
* However, we use another 10 more fractional digits to avoid rounding issues.
|
||||
* 38 integer + 48 fractional gives 86, so: DECIMAL (86, 48)
|
||||
*/
|
||||
// Rename price to unit_price to preserve indexes
|
||||
stmt.execute("ALTER TABLE AssetOrders ALTER COLUMN price RENAME TO unit_price");
|
||||
// Adjust precision
|
||||
stmt.execute("ALTER TABLE AssetOrders ALTER COLUMN unit_price DECIMAL(76,48)");
|
||||
// Add want-amount column
|
||||
stmt.execute("ALTER TABLE AssetOrders ADD want_amount QoraAmount BEFORE unit_price");
|
||||
// Calculate want-amount values
|
||||
stmt.execute("UPDATE AssetOrders set want_amount = amount * unit_price");
|
||||
// want-amounts all set, so disallow NULL
|
||||
stmt.execute("ALTER TABLE AssetOrders ALTER COLUMN want_amount SET NOT NULL");
|
||||
// Convert old "price" into buying unit price
|
||||
stmt.execute("UPDATE AssetOrders set unit_price = 1 / unit_price");
|
||||
break;
|
||||
|
||||
default:
|
||||
// nothing to do
|
||||
return false;
|
||||
|
@@ -93,7 +93,6 @@ public class Settings {
|
||||
|
||||
// Tell unmarshaller that there's no JSON root element in the JSON input
|
||||
unmarshaller.setProperty(UnmarshallerProperties.JSON_INCLUDE_ROOT, false);
|
||||
|
||||
} catch (JAXBException e) {
|
||||
LOGGER.error("Unable to process settings file", e);
|
||||
throw new RuntimeException("Unable to process settings file", e);
|
||||
|
@@ -1,6 +1,7 @@
|
||||
package org.qora.transaction;
|
||||
|
||||
import java.math.BigDecimal;
|
||||
import java.math.RoundingMode;
|
||||
import java.util.ArrayList;
|
||||
import java.util.Arrays;
|
||||
import java.util.List;
|
||||
@@ -130,12 +131,12 @@ public class CreateAssetOrderTransaction extends Transaction {
|
||||
return ValidationResult.INVALID_AMOUNT;
|
||||
|
||||
// Check total return from fulfilled order would be integer if "want" asset is not divisible
|
||||
if (createOrderTransactionData.getTimestamp() >= BlockChain.getInstance().getQoraV2Timestamp()) {
|
||||
// v2
|
||||
if (createOrderTransactionData.getTimestamp() >= BlockChain.getInstance().getNewAssetPricingTimestamp()) {
|
||||
// "new" asset pricing
|
||||
if (!wantAssetData.getIsDivisible() && createOrderTransactionData.getPrice().stripTrailingZeros().scale() > 0)
|
||||
return ValidationResult.INVALID_RETURN;
|
||||
} else {
|
||||
// v1
|
||||
// "old" asset pricing
|
||||
if (!wantAssetData.getIsDivisible()
|
||||
&& createOrderTransactionData.getAmount().multiply(createOrderTransactionData.getPrice()).stripTrailingZeros().scale() > 0)
|
||||
return ValidationResult.INVALID_RETURN;
|
||||
@@ -160,9 +161,22 @@ 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.getPrice();
|
||||
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.getPrice());
|
||||
unitPrice = createOrderTransactionData.getPrice();
|
||||
}
|
||||
|
||||
// Process the order itself
|
||||
OrderData orderData = new OrderData(orderId, createOrderTransactionData.getCreatorPublicKey(), createOrderTransactionData.getHaveAssetId(),
|
||||
createOrderTransactionData.getWantAssetId(), createOrderTransactionData.getAmount(), createOrderTransactionData.getPrice(),
|
||||
createOrderTransactionData.getWantAssetId(), createOrderTransactionData.getAmount(), wantAmount, unitPrice,
|
||||
createOrderTransactionData.getTimestamp());
|
||||
|
||||
new Order(this.repository, orderData).process();
|
||||
|
@@ -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 asset wanted per offered asset", TransformationType.ASSET_QUANTITY);
|
||||
layout.add("amount of wanted asset", TransformationType.ASSET_QUANTITY);
|
||||
layout.add("fee", TransformationType.AMOUNT);
|
||||
layout.add("signature", TransformationType.SIGNATURE);
|
||||
}
|
||||
@@ -58,6 +58,7 @@ 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);
|
||||
@@ -86,6 +87,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.getPrice(), AMOUNT_LENGTH);
|
||||
|
||||
Serialization.serializeBigDecimal(bytes, createOrderTransactionData.getFee());
|
||||
|
Reference in New Issue
Block a user