mirror of
https://github.com/Qortal/qortal.git
synced 2025-07-22 20:26:50 +00:00
Completing work on new asset trading changes
Changed API call GET /assets to NOT return asset "data" fields as they can be huge. If need be, call GET /assets/info to fetch a specific asset's data field. Improve asset trade amount granularity, especially for indivisible assets, under "new" pricing scheme only. Added corresponding tests for granularity adjustments. Fix/unify asset order logging text under "old" and "new" pricing schemes. Change asset order related API data models so that old "price" is now "unitPrice" and add new "return" as in amount of want-asset to receive if have-asset "amount" was fully matched. (Affects OrderData, CreateAssetOrderTransactionData) Some changes to the HSQLDB tables. Don't forget to add "newAssetPricingTimestamp" to your blockchain config's "featureTriggers" map.
This commit is contained in:
@@ -75,7 +75,7 @@ public class AssetsResource {
|
||||
|
||||
@GET
|
||||
@Operation(
|
||||
summary = "List all known assets",
|
||||
summary = "List all known assets (without data field)",
|
||||
responses = {
|
||||
@ApiResponse(
|
||||
description = "asset info",
|
||||
@@ -100,7 +100,11 @@ public class AssetsResource {
|
||||
ref = "reverse"
|
||||
) @QueryParam("reverse") Boolean reverse) {
|
||||
try (final Repository repository = RepositoryManager.getRepository()) {
|
||||
return repository.getAssetRepository().getAllAssets(limit, offset, reverse);
|
||||
List<AssetData> assets = repository.getAssetRepository().getAllAssets(limit, offset, reverse);
|
||||
|
||||
assets.forEach(asset -> asset.setData(null));
|
||||
|
||||
return assets;
|
||||
} catch (DataException e) {
|
||||
throw ApiExceptionFactory.INSTANCE.createException(request, ApiError.REPOSITORY_ISSUE, e);
|
||||
}
|
||||
|
@@ -71,14 +71,22 @@ public class Order {
|
||||
* @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
|
||||
public static BigDecimal calculateAmountGranularity(AssetData haveAssetData, AssetData wantAssetData, OrderData theirOrderData) {
|
||||
// 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 = 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 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();
|
||||
}
|
||||
|
||||
BigInteger gcd = haveAmount.gcd(wantAmount);
|
||||
haveAmount = haveAmount.divide(gcd);
|
||||
@@ -115,7 +123,6 @@ public class Order {
|
||||
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());
|
||||
@@ -125,15 +132,9 @@ public class Order {
|
||||
|
||||
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()));
|
||||
}
|
||||
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 {
|
||||
@@ -254,18 +255,13 @@ public class Order {
|
||||
if (matchedWantAmount.compareTo(BigDecimal.ZERO) <= 0)
|
||||
continue;
|
||||
|
||||
// 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 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 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());
|
||||
}
|
||||
// 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 (matchedWantAmount.compareTo(BigDecimal.ZERO) <= 0)
|
||||
|
@@ -4,6 +4,7 @@ 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;
|
||||
|
||||
@@ -24,7 +25,8 @@ 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")
|
||||
@Schema(name = "return", description = "amount of \"want\" asset to receive")
|
||||
@XmlElement(name = "return")
|
||||
private BigDecimal wantAmount;
|
||||
|
||||
@Schema(description = "amount of \"want\" asset to receive per unit of \"have\" asset traded")
|
||||
|
@@ -22,8 +22,9 @@ public class CreateAssetOrderTransactionData extends TransactionData {
|
||||
private long wantAssetId;
|
||||
@Schema(description = "amount of \"have\" asset to trade")
|
||||
private BigDecimal amount;
|
||||
@Schema(description = "amount of \"want\" asset to receive")
|
||||
private BigDecimal price;
|
||||
@Schema(name = "return", description = "amount of \"want\" asset to receive")
|
||||
@XmlElement(name = "return")
|
||||
private BigDecimal wantAmount;
|
||||
|
||||
// Constructors
|
||||
|
||||
@@ -33,18 +34,18 @@ public class CreateAssetOrderTransactionData extends TransactionData {
|
||||
}
|
||||
|
||||
public CreateAssetOrderTransactionData(long timestamp, int txGroupId, byte[] reference, byte[] creatorPublicKey, long haveAssetId, long wantAssetId,
|
||||
BigDecimal amount, BigDecimal price, BigDecimal fee, byte[] signature) {
|
||||
BigDecimal amount, BigDecimal wantAmount, 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.price = price;
|
||||
this.wantAmount = wantAmount;
|
||||
}
|
||||
|
||||
public CreateAssetOrderTransactionData(long timestamp, int txGroupId, byte[] reference, byte[] creatorPublicKey, long haveAssetId, long wantAssetId,
|
||||
BigDecimal amount, BigDecimal price, BigDecimal fee) {
|
||||
this(timestamp, txGroupId, reference, creatorPublicKey, haveAssetId, wantAssetId, amount, price, fee, null);
|
||||
BigDecimal amount, BigDecimal wantAmount, BigDecimal fee) {
|
||||
this(timestamp, txGroupId, reference, creatorPublicKey, haveAssetId, wantAssetId, amount, wantAmount, fee, null);
|
||||
}
|
||||
|
||||
// Getters/Setters
|
||||
@@ -61,8 +62,8 @@ public class CreateAssetOrderTransactionData extends TransactionData {
|
||||
return this.amount;
|
||||
}
|
||||
|
||||
public BigDecimal getPrice() {
|
||||
return this.price;
|
||||
public BigDecimal getWantAmount() {
|
||||
return this.wantAmount;
|
||||
}
|
||||
|
||||
// Re-expose creatorPublicKey for this transaction type for JAXB
|
||||
|
@@ -651,8 +651,8 @@ public class HSQLDBDatabaseUpdates {
|
||||
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");
|
||||
// Rename corresponding column in CreateAssetOrderTransactions
|
||||
stmt.execute("ALTER TABLE CreateAssetOrderTransactions ALTER COLUMN price RENAME TO want_amount");
|
||||
break;
|
||||
|
||||
default:
|
||||
|
@@ -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, price FROM CreateAssetOrderTransactions WHERE signature = ?", signature)) {
|
||||
.checkedExecute("SELECT have_asset_id, amount, want_asset_id, want_amount 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 price = resultSet.getBigDecimal(4);
|
||||
BigDecimal wantAmount = resultSet.getBigDecimal(4);
|
||||
|
||||
return new CreateAssetOrderTransactionData(timestamp, txGroupId, reference, creatorPublicKey, haveAssetId, wantAssetId, amount, price, fee, signature);
|
||||
return new CreateAssetOrderTransactionData(timestamp, txGroupId, reference, creatorPublicKey, haveAssetId, wantAssetId, amount, wantAmount, 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("price", createOrderTransactionData.getPrice());
|
||||
.bind("want_asset_id", createOrderTransactionData.getWantAssetId()).bind("want_amount", createOrderTransactionData.getWantAmount());
|
||||
|
||||
try {
|
||||
saveHelper.execute(this.repository);
|
||||
|
@@ -83,7 +83,7 @@ public class CreateAssetOrderTransaction extends Transaction {
|
||||
return ValidationResult.NEGATIVE_AMOUNT;
|
||||
|
||||
// Check price is positive
|
||||
if (createOrderTransactionData.getPrice().compareTo(BigDecimal.ZERO) <= 0)
|
||||
if (createOrderTransactionData.getWantAmount().compareTo(BigDecimal.ZERO) <= 0)
|
||||
return ValidationResult.NEGATIVE_PRICE;
|
||||
|
||||
// Check fee is positive
|
||||
@@ -133,12 +133,12 @@ public class CreateAssetOrderTransaction extends Transaction {
|
||||
// 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.getPrice().stripTrailingZeros().scale() > 0)
|
||||
if (!wantAssetData.getIsDivisible() && createOrderTransactionData.getWantAmount().stripTrailingZeros().scale() > 0)
|
||||
return ValidationResult.INVALID_RETURN;
|
||||
} else {
|
||||
// "old" asset pricing
|
||||
if (!wantAssetData.getIsDivisible()
|
||||
&& createOrderTransactionData.getAmount().multiply(createOrderTransactionData.getPrice()).stripTrailingZeros().scale() > 0)
|
||||
&& createOrderTransactionData.getAmount().multiply(createOrderTransactionData.getWantAmount()).stripTrailingZeros().scale() > 0)
|
||||
return ValidationResult.INVALID_RETURN;
|
||||
}
|
||||
|
||||
@@ -166,12 +166,12 @@ public class CreateAssetOrderTransaction extends Transaction {
|
||||
|
||||
if (createOrderTransactionData.getTimestamp() >= BlockChain.getInstance().getNewAssetPricingTimestamp()) {
|
||||
// "new" asset pricing: want-amount provided, unit price to be calculated
|
||||
wantAmount = createOrderTransactionData.getPrice();
|
||||
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.getPrice());
|
||||
unitPrice = createOrderTransactionData.getPrice();
|
||||
wantAmount = createOrderTransactionData.getAmount().multiply(createOrderTransactionData.getWantAmount());
|
||||
unitPrice = createOrderTransactionData.getWantAmount(); // getWantAmount() was getPrice() in the "old" pricing scheme
|
||||
}
|
||||
|
||||
// Process the order itself
|
||||
|
@@ -88,7 +88,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.getWantAmount(), AMOUNT_LENGTH);
|
||||
|
||||
Serialization.serializeBigDecimal(bytes, createOrderTransactionData.getFee());
|
||||
|
||||
@@ -128,7 +128,7 @@ public class CreateAssetOrderTransactionTransformer extends TransactionTransform
|
||||
Serialization.serializeBigDecimal(bytes, createOrderTransactionData.getAmount(), AMOUNT_LENGTH);
|
||||
|
||||
// This is the crucial difference
|
||||
Serialization.serializeBigDecimal(bytes, createOrderTransactionData.getPrice(), FEE_LENGTH);
|
||||
Serialization.serializeBigDecimal(bytes, createOrderTransactionData.getWantAmount(), FEE_LENGTH);
|
||||
|
||||
Serialization.serializeBigDecimal(bytes, createOrderTransactionData.getFee());
|
||||
|
||||
|
Reference in New Issue
Block a user