forked from Qortal/qortal
Merge pull request #7 from catbref/assets
Merge "assets" branch into master
This commit is contained in:
commit
d9be12e62f
@ -130,6 +130,13 @@ public class Account {
|
||||
}
|
||||
|
||||
public void setConfirmedBalance(long assetId, BigDecimal balance) throws DataException {
|
||||
// Safety feature!
|
||||
if (balance.compareTo(BigDecimal.ZERO) < 0) {
|
||||
String message = String.format("Refusing to set negative balance %s [assetId %d] for %s", balance.toPlainString(), assetId, this.address);
|
||||
LOGGER.error(message);
|
||||
throw new DataException(message);
|
||||
}
|
||||
|
||||
// Can't have a balance without an account - make sure it exists!
|
||||
this.repository.getAccountRepository().ensureAccount(this.buildAccountData());
|
||||
|
||||
|
@ -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")
|
||||
|
@ -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);
|
||||
}
|
||||
|
@ -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,10 +6,12 @@ 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;
|
||||
import org.qora.account.PublicKeyAccount;
|
||||
import org.qora.block.BlockChain;
|
||||
import org.qora.data.asset.AssetData;
|
||||
import org.qora.data.asset.OrderData;
|
||||
import org.qora.data.asset.TradeData;
|
||||
@ -21,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
|
||||
@ -58,33 +65,49 @@ 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, 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 = BigInteger.ONE.multiply(multiplier);
|
||||
BigInteger priceAmount = theirOrderData.getPrice().multiply(new BigDecimal(multiplier)).toBigInteger();
|
||||
BigInteger gcd = haveAmount.gcd(priceAmount);
|
||||
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);
|
||||
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
|
||||
@ -95,6 +118,25 @@ 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 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()));
|
||||
|
||||
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();
|
||||
|
||||
@ -110,37 +152,57 @@ public class Order {
|
||||
// Save this order into repository so it's available for matching, possibly by itself
|
||||
this.repository.getAssetRepository().save(this.orderData);
|
||||
|
||||
// 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());
|
||||
LOGGER.trace("We want " + this.orderData.getPrice().toPlainString() + " " + wantAssetData.getName() + " per " + haveAssetData.getName());
|
||||
/*
|
||||
* 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
|
||||
*/
|
||||
|
||||
/*
|
||||
* 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:
|
||||
*
|
||||
* haveAssetId=[GOLD], amount=10,000, wantAssetId=0 (QORA), price=0.002
|
||||
*
|
||||
* This translates to "we have 10,000 GOLD and want to buy QORA at a price of 0.002 QORA per GOLD"
|
||||
*
|
||||
* So if our order matched, we'd end up with 10,000 * 0.002 = 20 QORA, essentially costing 1/0.002 = 500 GOLD each.
|
||||
*
|
||||
* So 500 GOLD [each] is our "buyingPrice".
|
||||
*/
|
||||
BigDecimal ourPrice = this.orderData.getPrice();
|
||||
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());
|
||||
LOGGER.trace("They want " + theirOrderData.getPrice().toPlainString() + " " + haveAssetData.getName() + " per " + wantAssetData.getName());
|
||||
logOrder("Considering order", true, theirOrderData);
|
||||
|
||||
/*
|
||||
* Potential matching order example:
|
||||
* Potential matching order example ("old"):
|
||||
*
|
||||
* haveAssetId=0 (QORA), amount=40, wantAssetId=[GOLD], price=486
|
||||
*
|
||||
@ -148,58 +210,131 @@ 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".
|
||||
* 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 = BigDecimal.ONE.setScale(8).divide(theirOrderData.getPrice(), RoundingMode.DOWN);
|
||||
LOGGER.trace("theirBuyingPrice: " + theirBuyingPrice.toPlainString() + " " + wantAssetData.getName() + " per " + haveAssetData.getName());
|
||||
/*
|
||||
* 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.
|
||||
*/
|
||||
|
||||
// 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)
|
||||
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 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());
|
||||
// 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());
|
||||
BigDecimal ourMaxWantAmount = this.getAmountLeft().multiply(theirBuyingPrice).setScale(8, RoundingMode.DOWN);
|
||||
LOGGER.trace("ourMaxWantAmount (max we could buy at their price): " + ourMaxWantAmount.stripTrailingZeros().toPlainString() + " " + wantAssetData.getName());
|
||||
|
||||
if (isTheirOrderNewAssetPricing) {
|
||||
ourMaxWantAmount = ourMaxWantAmount.max(this.getAmountLeft().divide(theirOrderData.getUnitPrice(), RoundingMode.DOWN).setScale(8, RoundingMode.DOWN));
|
||||
LOGGER.trace("ourMaxWantAmount (max we could buy at their price) using inverted calculation: " + ourMaxWantAmount.stripTrailingZeros().toPlainString() + " " + wantAssetData.getName());
|
||||
}
|
||||
|
||||
// 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());
|
||||
// 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());
|
||||
|
||||
// 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 = 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) {
|
||||
|
@ -310,6 +310,7 @@ public class GenesisBlock extends Block {
|
||||
}
|
||||
|
||||
transaction.process();
|
||||
creator.setLastReference(transactionData.getSignature());
|
||||
}
|
||||
} catch (TransformationException e) {
|
||||
throw new RuntimeException("Can't process genesis block transaction", e);
|
||||
|
@ -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,8 +25,12 @@ public class OrderData implements Comparable<OrderData> {
|
||||
@Schema(description = "amount of \"have\" asset to trade")
|
||||
private BigDecimal amount;
|
||||
|
||||
@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")
|
||||
private BigDecimal price;
|
||||
private BigDecimal unitPrice;
|
||||
|
||||
@Schema(description = "how much \"have\" asset has traded")
|
||||
private BigDecimal fulfilled;
|
||||
@ -44,22 +49,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 +99,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 +130,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());
|
||||
}
|
||||
|
||||
}
|
||||
|
@ -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 per unit of \"have\" asset traded")
|
||||
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
|
||||
|
@ -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");
|
||||
// Rename corresponding column in CreateAssetOrderTransactions
|
||||
stmt.execute("ALTER TABLE CreateAssetOrderTransactions ALTER COLUMN price RENAME TO want_amount");
|
||||
break;
|
||||
|
||||
default:
|
||||
// nothing to do
|
||||
return false;
|
||||
|
@ -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);
|
||||
|
@ -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;
|
||||
@ -82,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
|
||||
@ -130,9 +131,16 @@ 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 (!wantAssetData.getIsDivisible()
|
||||
&& createOrderTransactionData.getAmount().multiply(createOrderTransactionData.getPrice()).stripTrailingZeros().scale() > 0)
|
||||
return ValidationResult.INVALID_RETURN;
|
||||
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;
|
||||
}
|
||||
|
||||
return ValidationResult.OK;
|
||||
}
|
||||
@ -153,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.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(), 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,7 +87,8 @@ public class CreateAssetOrderTransactionTransformer extends TransactionTransform
|
||||
|
||||
Serialization.serializeBigDecimal(bytes, createOrderTransactionData.getAmount(), AMOUNT_LENGTH);
|
||||
|
||||
Serialization.serializeBigDecimal(bytes, createOrderTransactionData.getPrice(), AMOUNT_LENGTH);
|
||||
// Under "new" asset pricing, this is actually the want-amount
|
||||
Serialization.serializeBigDecimal(bytes, createOrderTransactionData.getWantAmount(), AMOUNT_LENGTH);
|
||||
|
||||
Serialization.serializeBigDecimal(bytes, createOrderTransactionData.getFee());
|
||||
|
||||
@ -126,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());
|
||||
|
||||
|
@ -10,6 +10,7 @@ import org.qora.group.Group;
|
||||
import org.qora.repository.DataException;
|
||||
import org.qora.repository.Repository;
|
||||
import org.qora.repository.RepositoryManager;
|
||||
import org.qora.test.common.Common;
|
||||
import org.qora.transaction.DeployAtTransaction;
|
||||
import org.qora.transform.TransformationException;
|
||||
import org.qora.utils.Base58;
|
||||
|
@ -11,6 +11,7 @@ import org.qora.data.transaction.TransactionData;
|
||||
import org.qora.repository.DataException;
|
||||
import org.qora.repository.Repository;
|
||||
import org.qora.repository.RepositoryManager;
|
||||
import org.qora.test.common.Common;
|
||||
import org.qora.transaction.Transaction;
|
||||
import org.qora.transform.TransformationException;
|
||||
import org.qora.transform.block.BlockTransformer;
|
||||
|
@ -3,6 +3,7 @@ package org.qora.test;
|
||||
import org.junit.Test;
|
||||
import org.qora.block.BlockChain;
|
||||
import org.qora.repository.DataException;
|
||||
import org.qora.test.common.Common;
|
||||
|
||||
public class BlockchainTests extends Common {
|
||||
|
||||
|
@ -1,54 +0,0 @@
|
||||
package org.qora.test;
|
||||
|
||||
import static org.junit.Assert.assertEquals;
|
||||
|
||||
import java.security.Security;
|
||||
|
||||
import org.bitcoinj.core.Base58;
|
||||
import org.bouncycastle.jce.provider.BouncyCastleProvider;
|
||||
import org.bouncycastle.jsse.provider.BouncyCastleJsseProvider;
|
||||
import org.junit.AfterClass;
|
||||
import org.junit.BeforeClass;
|
||||
import org.qora.repository.DataException;
|
||||
import org.qora.repository.Repository;
|
||||
import org.qora.repository.RepositoryFactory;
|
||||
import org.qora.repository.RepositoryManager;
|
||||
import org.qora.repository.hsqldb.HSQLDBRepositoryFactory;
|
||||
import org.qora.settings.Settings;
|
||||
|
||||
public class Common {
|
||||
|
||||
public static final String testConnectionUrl = "jdbc:hsqldb:mem:testdb";
|
||||
public static final String testSettingsFilename = "test-settings.json";
|
||||
|
||||
public static final byte[] v2testPrivateKey = Base58.decode("A9MNsATgQgruBUjxy2rjWY36Yf19uRioKZbiLFT2P7c6");
|
||||
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.
|
||||
Settings.fileInstance(testSettingsFilename);
|
||||
}
|
||||
|
||||
@BeforeClass
|
||||
public static void setRepository() throws DataException {
|
||||
RepositoryFactory repositoryFactory = new HSQLDBRepositoryFactory(testConnectionUrl);
|
||||
RepositoryManager.setRepositoryFactory(repositoryFactory);
|
||||
}
|
||||
|
||||
@AfterClass
|
||||
public static void closeRepository() throws DataException {
|
||||
RepositoryManager.closeRepositoryFactory();
|
||||
}
|
||||
|
||||
public static void assetEmptyBlockchain(Repository repository) throws DataException {
|
||||
assertEquals("Blockchain should be empty for this test", 0, repository.getBlockRepository().getBlockchainHeight());
|
||||
}
|
||||
|
||||
}
|
@ -3,6 +3,7 @@ package org.qora.test;
|
||||
import org.junit.Test;
|
||||
import org.qora.block.BlockChain;
|
||||
import org.qora.crypto.Crypto;
|
||||
import org.qora.test.common.Common;
|
||||
|
||||
import static org.junit.Assert.*;
|
||||
|
||||
|
@ -10,6 +10,7 @@ import org.qora.data.transaction.TransactionData;
|
||||
import org.qora.repository.DataException;
|
||||
import org.qora.repository.Repository;
|
||||
import org.qora.repository.RepositoryManager;
|
||||
import org.qora.test.common.Common;
|
||||
import org.qora.transaction.Transaction;
|
||||
|
||||
import static org.junit.Assert.*;
|
||||
|
@ -12,6 +12,7 @@ import org.qora.group.Group.ApprovalThreshold;
|
||||
import org.qora.repository.DataException;
|
||||
import org.qora.repository.Repository;
|
||||
import org.qora.repository.RepositoryManager;
|
||||
import org.qora.test.common.Common;
|
||||
import org.qora.transaction.CreateGroupTransaction;
|
||||
import org.qora.transaction.PaymentTransaction;
|
||||
import org.qora.transaction.Transaction;
|
||||
|
@ -8,6 +8,7 @@ import org.qora.repository.DataException;
|
||||
import org.qora.repository.Repository;
|
||||
import org.qora.repository.RepositoryManager;
|
||||
import org.qora.repository.TransactionRepository;
|
||||
import org.qora.test.common.Common;
|
||||
import org.qora.transaction.Transaction.TransactionType;
|
||||
import org.qora.utils.Base58;
|
||||
|
||||
|
@ -7,6 +7,7 @@ import org.qora.repository.DataException;
|
||||
import org.qora.repository.Repository;
|
||||
import org.qora.repository.RepositoryManager;
|
||||
import org.qora.repository.TransactionRepository;
|
||||
import org.qora.test.common.Common;
|
||||
import org.qora.transaction.Transaction.TransactionType;
|
||||
import org.qora.utils.Base58;
|
||||
|
||||
|
@ -4,6 +4,7 @@ import org.junit.Test;
|
||||
import org.qora.repository.DataException;
|
||||
import org.qora.repository.Repository;
|
||||
import org.qora.repository.RepositoryManager;
|
||||
import org.qora.test.common.Common;
|
||||
|
||||
import static org.junit.Assert.*;
|
||||
|
||||
|
@ -10,6 +10,7 @@ import org.qora.group.Group;
|
||||
import org.qora.repository.DataException;
|
||||
import org.qora.repository.Repository;
|
||||
import org.qora.repository.RepositoryManager;
|
||||
import org.qora.test.common.Common;
|
||||
import org.qora.utils.Base58;
|
||||
|
||||
public class SaveTests extends Common {
|
||||
|
@ -9,6 +9,7 @@ import org.qora.data.transaction.TransactionData;
|
||||
import org.qora.repository.DataException;
|
||||
import org.qora.repository.Repository;
|
||||
import org.qora.repository.RepositoryManager;
|
||||
import org.qora.test.common.Common;
|
||||
import org.qora.transaction.GenesisTransaction;
|
||||
import org.qora.transaction.Transaction;
|
||||
import org.qora.transaction.Transaction.TransactionType;
|
||||
|
@ -8,6 +8,7 @@ import org.qora.data.block.BlockData;
|
||||
import org.qora.repository.DataException;
|
||||
import org.qora.repository.Repository;
|
||||
import org.qora.repository.RepositoryManager;
|
||||
import org.qora.test.common.Common;
|
||||
import org.qora.utils.Base58;
|
||||
import org.qora.utils.NTP;
|
||||
|
||||
|
@ -39,6 +39,7 @@ import org.qora.repository.AssetRepository;
|
||||
import org.qora.repository.DataException;
|
||||
import org.qora.repository.Repository;
|
||||
import org.qora.repository.RepositoryManager;
|
||||
import org.qora.test.common.Common;
|
||||
import org.qora.transaction.BuyNameTransaction;
|
||||
import org.qora.transaction.CancelAssetOrderTransaction;
|
||||
import org.qora.transaction.CancelSellNameTransaction;
|
||||
@ -989,7 +990,7 @@ public class TransactionTests extends Common {
|
||||
TradeData tradeData = trades.get(0);
|
||||
|
||||
// Check trade has correct values
|
||||
BigDecimal expectedAmount = amount.divide(originalOrderData.getPrice()).setScale(8);
|
||||
BigDecimal expectedAmount = amount.divide(originalOrderData.getUnitPrice()).setScale(8);
|
||||
BigDecimal actualAmount = tradeData.getTargetAmount();
|
||||
assertTrue(expectedAmount.compareTo(actualAmount) == 0);
|
||||
|
||||
|
441
src/test/java/org/qora/test/assets/TradingTests.java
Normal file
441
src/test/java/org/qora/test/assets/TradingTests.java
Normal file
@ -0,0 +1,441 @@
|
||||
package org.qora.test.assets;
|
||||
|
||||
import org.junit.After;
|
||||
import org.junit.Before;
|
||||
import org.junit.Test;
|
||||
import org.qora.asset.Asset;
|
||||
import org.qora.asset.Order;
|
||||
import org.qora.block.BlockChain;
|
||||
import org.qora.data.asset.AssetData;
|
||||
import org.qora.data.asset.OrderData;
|
||||
import org.qora.repository.DataException;
|
||||
import org.qora.repository.Repository;
|
||||
import org.qora.repository.RepositoryManager;
|
||||
import org.qora.test.common.AccountUtils;
|
||||
import org.qora.test.common.AssetUtils;
|
||||
import org.qora.test.common.Common;
|
||||
|
||||
import java.math.BigDecimal;
|
||||
import java.util.Map;
|
||||
|
||||
public class TradingTests extends Common {
|
||||
|
||||
@Before
|
||||
public void beforeTest() throws DataException {
|
||||
Common.useDefaultSettings();
|
||||
}
|
||||
|
||||
@After
|
||||
public void afterTest() throws DataException {
|
||||
Common.orphanCheck();
|
||||
}
|
||||
|
||||
/**
|
||||
* Check granularity adjustment values.
|
||||
* <p>
|
||||
* If trading at a price of 12 eggs for 1 coin
|
||||
* then trades can only happen at multiples of
|
||||
* 0.000000001 or 0.00000012 depending on direction.
|
||||
*/
|
||||
@Test
|
||||
public void testDivisibleGranularities() {
|
||||
testGranularity(true, true, "12", "1", "0.00000012");
|
||||
testGranularity(true, true, "1", "12", "0.00000001");
|
||||
}
|
||||
|
||||
/**
|
||||
* Check granularity adjustment values.
|
||||
* <p>
|
||||
* If trading at a price of 123 riches per 50301 rags,
|
||||
* then the GCD(123, 50301) is 3 and so trades can only
|
||||
* happen at multiples of (50301/3) = 16767 rags or
|
||||
* (123/3) = 41 riches.
|
||||
*/
|
||||
@Test
|
||||
public void testIndivisibleGranularities() {
|
||||
testGranularity(false, false, "50301", "123", "16767");
|
||||
testGranularity(false, false, "123", "50301", "41");
|
||||
}
|
||||
|
||||
private void testGranularity(boolean isOurHaveDivisible, boolean isOurWantDivisible, String theirHaveAmount, String theirWantAmount, String expectedGranularity) {
|
||||
final long newPricingTimestamp = BlockChain.getInstance().getNewAssetPricingTimestamp() + 1;
|
||||
|
||||
final AssetData ourHaveAssetData = new AssetData(null, null, null, 0, isOurHaveDivisible, null, 0, null);
|
||||
final AssetData ourWantAssetData = new AssetData(null, null, null, 0, isOurWantDivisible, null, 0, null);
|
||||
|
||||
OrderData theirOrderData = new OrderData(null, null, 0, 0, new BigDecimal(theirHaveAmount), new BigDecimal(theirWantAmount), null, newPricingTimestamp);
|
||||
|
||||
BigDecimal granularity = Order.calculateAmountGranularity(ourHaveAssetData, ourWantAssetData, theirOrderData);
|
||||
assertEqualBigDecimals("Granularity incorrect", new BigDecimal(expectedGranularity), granularity);
|
||||
}
|
||||
|
||||
/**
|
||||
* Check matching of indivisible amounts.
|
||||
* <p>
|
||||
* New pricing scheme allows two attempts are calculating matched amount
|
||||
* to reduce partial-match issues caused by rounding and recurring fractional digits:
|
||||
* <p>
|
||||
* <ol>
|
||||
* <li> amount * round_down(1 / unit price) </li>
|
||||
* <li> round_down(amount / unit price) </li>
|
||||
* </ol>
|
||||
* Alice's price is 12 QORA per ATNL so the ATNL per QORA unit price is 0.08333333...<br>
|
||||
* Bob wants to spend 24 QORA so:
|
||||
* <p>
|
||||
* <ol>
|
||||
* <li> 24 QORA * (1 / 0.0833333...) = 1.99999999 ATNL </li>
|
||||
* <li> 24 QORA / 0.08333333.... = 2 ATNL </li>
|
||||
* </ol>
|
||||
* The second result is obviously more intuitive as is critical where assets are not divisible,
|
||||
* like ATNL in this test case.
|
||||
* <p>
|
||||
* @see TradingTests#testOldNonExactFraction
|
||||
* @see TradingTests#testNonExactFraction
|
||||
* @throws DataException
|
||||
*/
|
||||
@Test
|
||||
public void testMixedDivisibility() throws DataException {
|
||||
// Issue indivisible asset
|
||||
long atnlAssetId;
|
||||
try (Repository repository = RepositoryManager.getRepository()) {
|
||||
// Issue indivisible asset
|
||||
atnlAssetId = AssetUtils.issueAsset(repository, "alice", "ATNL", 100000000L, false);
|
||||
}
|
||||
|
||||
final BigDecimal atnlAmount = BigDecimal.valueOf(2L).setScale(8);
|
||||
final BigDecimal qoraAmount = BigDecimal.valueOf(24L).setScale(8);
|
||||
|
||||
genericTradeTest(atnlAssetId, Asset.QORA, atnlAmount, qoraAmount, qoraAmount, atnlAmount, atnlAmount, qoraAmount);
|
||||
}
|
||||
|
||||
/**
|
||||
* Check matching of indivisible amounts (new pricing).
|
||||
* <p>
|
||||
* Alice is selling twice as much as Bob wants,
|
||||
* but at the same [calculated] unit price,
|
||||
* so Bob's order should fully match.
|
||||
* <p>
|
||||
* However, in legacy/"old" mode, the granularity checks
|
||||
* would prevent this trade.
|
||||
*/
|
||||
@Test
|
||||
public void testIndivisible() throws DataException {
|
||||
// Issue some indivisible assets
|
||||
long ragsAssetId;
|
||||
long richesAssetId;
|
||||
try (Repository repository = RepositoryManager.getRepository()) {
|
||||
// Issue indivisble asset
|
||||
ragsAssetId = AssetUtils.issueAsset(repository, "alice", "rags", 12345678L, false);
|
||||
|
||||
// Issue another indivisble asset
|
||||
richesAssetId = AssetUtils.issueAsset(repository, "bob", "riches", 87654321L, false);
|
||||
}
|
||||
|
||||
final BigDecimal ragsAmount = BigDecimal.valueOf(50301L).setScale(8);
|
||||
final BigDecimal richesAmount = BigDecimal.valueOf(123L).setScale(8);
|
||||
|
||||
final BigDecimal two = BigDecimal.valueOf(2L);
|
||||
|
||||
genericTradeTest(ragsAssetId, richesAssetId, ragsAmount.multiply(two), richesAmount.multiply(two), richesAmount, ragsAmount, ragsAmount, richesAmount);
|
||||
}
|
||||
|
||||
/**
|
||||
* Check matching of indivisible amounts.
|
||||
* <p>
|
||||
* We use orders similar to some found in legacy qora1 blockchain
|
||||
* to test for expected results with indivisible assets.
|
||||
* <p>
|
||||
* In addition, although the 3rd "further" order would match up to 999 RUB.iPLZ,
|
||||
* granularity at that price reduces matched amount to 493 RUB.iPLZ.
|
||||
*/
|
||||
@Test
|
||||
public void testOldIndivisible() throws DataException {
|
||||
Common.useSettings("test-settings-old-asset.json");
|
||||
|
||||
// Issue some indivisible assets
|
||||
long asset112Id;
|
||||
long asset113Id;
|
||||
try (Repository repository = RepositoryManager.getRepository()) {
|
||||
// Issue indivisble asset
|
||||
asset112Id = AssetUtils.issueAsset(repository, "alice", "RUB.iPLZ", 999999999999L, false);
|
||||
|
||||
// Issue another indivisble asset
|
||||
asset113Id = AssetUtils.issueAsset(repository, "bob", "RU.GZP.V123", 10000L, false);
|
||||
}
|
||||
|
||||
// Transfer some assets so orders can be created
|
||||
try (Repository repository = RepositoryManager.getRepository()) {
|
||||
AssetUtils.transferAsset(repository, "alice", "bob", asset112Id, BigDecimal.valueOf(5000L).setScale(8));
|
||||
AssetUtils.transferAsset(repository, "bob", "alice", asset113Id, BigDecimal.valueOf(5000L).setScale(8));
|
||||
}
|
||||
|
||||
final BigDecimal asset113Amount = new BigDecimal("1000").setScale(8);
|
||||
final BigDecimal asset112Price = new BigDecimal("1.00000000").setScale(8);
|
||||
|
||||
final BigDecimal asset112Amount = new BigDecimal("2000").setScale(8);
|
||||
final BigDecimal asset113Price = new BigDecimal("0.98600000").setScale(8);
|
||||
|
||||
final BigDecimal asset112Matched = new BigDecimal("1000").setScale(8);
|
||||
final BigDecimal asset113Matched = new BigDecimal("1000").setScale(8);
|
||||
|
||||
genericTradeTest(asset113Id, asset112Id, asset113Amount, asset112Price, asset112Amount, asset113Price, asset113Matched, asset112Matched);
|
||||
|
||||
// Further trade
|
||||
final BigDecimal asset113Amount2 = new BigDecimal("986").setScale(8);
|
||||
final BigDecimal asset112Price2 = new BigDecimal("1.00000000").setScale(8);
|
||||
|
||||
final BigDecimal asset112Matched2 = new BigDecimal("500").setScale(8);
|
||||
final BigDecimal asset113Matched2 = new BigDecimal("493").setScale(8);
|
||||
|
||||
try (Repository repository = RepositoryManager.getRepository()) {
|
||||
Map<String, Map<Long, BigDecimal>> initialBalances = AccountUtils.getBalances(repository, asset112Id, asset113Id);
|
||||
|
||||
// Create further order
|
||||
byte[] furtherOrderId = AssetUtils.createOrder(repository, "alice", asset113Id, asset112Id, asset113Amount2, asset112Price2);
|
||||
|
||||
// Check balances to check expected outcome
|
||||
BigDecimal expectedBalance;
|
||||
|
||||
// Alice asset 113
|
||||
expectedBalance = initialBalances.get("alice").get(asset113Id).subtract(asset113Amount2);
|
||||
assertBalance(repository, "alice", asset113Id, expectedBalance);
|
||||
|
||||
// Alice asset 112
|
||||
expectedBalance = initialBalances.get("alice").get(asset112Id).add(asset112Matched2);
|
||||
assertBalance(repository, "alice", asset112Id, expectedBalance);
|
||||
|
||||
BigDecimal expectedFulfilled = asset113Matched2;
|
||||
BigDecimal actualFulfilled = repository.getAssetRepository().fromOrderId(furtherOrderId).getFulfilled();
|
||||
assertEqualBigDecimals("Order fulfilled incorrect", expectedFulfilled, actualFulfilled);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Check full matching of orders with prices that
|
||||
* can't be represented in floating binary.
|
||||
* <p>
|
||||
* For example, sell 1 GOLD for 12 QORA so
|
||||
* price is 1/12 or 0.08333333..., which could
|
||||
* lead to rounding issues or inexact match amounts,
|
||||
* but we counter this using the technique described in
|
||||
* {@link #testMixedDivisibility()}
|
||||
*/
|
||||
@Test
|
||||
public void testNonExactFraction() throws DataException {
|
||||
final BigDecimal otherAmount = BigDecimal.valueOf(24L).setScale(8);
|
||||
final BigDecimal qoraAmount = BigDecimal.valueOf(2L).setScale(8);
|
||||
|
||||
genericTradeTest(AssetUtils.testAssetId, Asset.QORA, otherAmount, qoraAmount, qoraAmount, otherAmount, otherAmount, qoraAmount);
|
||||
}
|
||||
|
||||
/**
|
||||
* Check legacy partial matching of orders with prices that
|
||||
* can't be represented in floating binary.
|
||||
* <p>
|
||||
* For example, sell 2 TEST for 24 QORA so
|
||||
* unit price is 2 / 24 or 0.08333333.
|
||||
* <p>
|
||||
* This inexactness causes the match amount to be
|
||||
* only 1.99999992 instead of the expected 2.00000000.
|
||||
* <p>
|
||||
* However this behaviour is "grandfathered" in legacy/"old"
|
||||
* mode so we need to test.
|
||||
*/
|
||||
@Test
|
||||
public void testOldNonExactFraction() throws DataException {
|
||||
Common.useSettings("test-settings-old-asset.json");
|
||||
|
||||
final BigDecimal initialAmount = new BigDecimal("24.00000000").setScale(8);
|
||||
final BigDecimal initialPrice = new BigDecimal("0.08333333").setScale(8);
|
||||
|
||||
final BigDecimal matchedAmount = new BigDecimal("2.00000000").setScale(8);
|
||||
final BigDecimal matchedPrice = new BigDecimal("12.00000000").setScale(8);
|
||||
|
||||
// Due to rounding these are the expected traded amounts.
|
||||
final BigDecimal tradedQoraAmount = new BigDecimal("24.00000000").setScale(8);
|
||||
final BigDecimal tradedOtherAmount = new BigDecimal("1.99999992").setScale(8);
|
||||
|
||||
genericTradeTest(AssetUtils.testAssetId, Asset.QORA, initialAmount, initialPrice, matchedAmount, matchedPrice, tradedQoraAmount, tradedOtherAmount);
|
||||
}
|
||||
|
||||
/**
|
||||
* Check that better prices are used in preference when matching orders.
|
||||
*/
|
||||
@Test
|
||||
public void testPriceImprovement() throws DataException {
|
||||
final BigDecimal qoraAmount = BigDecimal.valueOf(24L).setScale(8);
|
||||
final BigDecimal betterQoraAmount = BigDecimal.valueOf(25L).setScale(8);
|
||||
final BigDecimal bestQoraAmount = BigDecimal.valueOf(31L).setScale(8);
|
||||
|
||||
final BigDecimal otherAmount = BigDecimal.valueOf(2L).setScale(8);
|
||||
|
||||
try (Repository repository = RepositoryManager.getRepository()) {
|
||||
Map<String, Map<Long, BigDecimal>> initialBalances = AccountUtils.getBalances(repository, Asset.QORA, AssetUtils.testAssetId);
|
||||
|
||||
// Create best initial order
|
||||
AssetUtils.createOrder(repository, "bob", Asset.QORA, AssetUtils.testAssetId, qoraAmount, otherAmount);
|
||||
|
||||
// Create initial order better than first
|
||||
AssetUtils.createOrder(repository, "chloe", Asset.QORA, AssetUtils.testAssetId, bestQoraAmount, otherAmount);
|
||||
|
||||
// Create initial order
|
||||
AssetUtils.createOrder(repository, "dilbert", Asset.QORA, AssetUtils.testAssetId, betterQoraAmount, otherAmount);
|
||||
|
||||
// Create matching order
|
||||
AssetUtils.createOrder(repository, "alice", AssetUtils.testAssetId, Asset.QORA, otherAmount, qoraAmount);
|
||||
|
||||
// Check balances to check expected outcome
|
||||
BigDecimal expectedBalance;
|
||||
|
||||
// We're expecting Alice's order to match with Chloe's order (as Bob's and Dilberts's orders have worse prices)
|
||||
|
||||
// Alice Qora
|
||||
expectedBalance = initialBalances.get("alice").get(Asset.QORA).add(bestQoraAmount);
|
||||
assertBalance(repository, "alice", Asset.QORA, expectedBalance);
|
||||
|
||||
// Alice test asset
|
||||
expectedBalance = initialBalances.get("alice").get(AssetUtils.testAssetId).subtract(otherAmount);
|
||||
assertBalance(repository, "alice", AssetUtils.testAssetId, expectedBalance);
|
||||
|
||||
// Bob Qora
|
||||
expectedBalance = initialBalances.get("bob").get(Asset.QORA).subtract(qoraAmount);
|
||||
assertBalance(repository, "bob", Asset.QORA, expectedBalance);
|
||||
|
||||
// Bob test asset
|
||||
expectedBalance = initialBalances.get("bob").get(AssetUtils.testAssetId);
|
||||
assertBalance(repository, "bob", AssetUtils.testAssetId, expectedBalance);
|
||||
|
||||
// Chloe Qora
|
||||
expectedBalance = initialBalances.get("chloe").get(Asset.QORA).subtract(bestQoraAmount);
|
||||
assertBalance(repository, "chloe", Asset.QORA, expectedBalance);
|
||||
|
||||
// Chloe test asset
|
||||
expectedBalance = initialBalances.get("chloe").get(AssetUtils.testAssetId).add(otherAmount);
|
||||
assertBalance(repository, "chloe", AssetUtils.testAssetId, expectedBalance);
|
||||
|
||||
// Dilbert Qora
|
||||
expectedBalance = initialBalances.get("dilbert").get(Asset.QORA).subtract(betterQoraAmount);
|
||||
assertBalance(repository, "dilbert", Asset.QORA, expectedBalance);
|
||||
|
||||
// Dilbert test asset
|
||||
expectedBalance = initialBalances.get("dilbert").get(AssetUtils.testAssetId);
|
||||
assertBalance(repository, "dilbert", AssetUtils.testAssetId, expectedBalance);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Check legacy qora1 blockchain matching behaviour.
|
||||
*/
|
||||
@Test
|
||||
public void testQora1Compat() throws DataException {
|
||||
// Asset 61 [ATFunding] was issued by QYsLsfwMRBPnunmuWmFkM4hvGsfooY8ssU with 250,000,000 quantity and was divisible.
|
||||
|
||||
// Initial order 2jMinWSBjxaLnQvhcEoWGs2JSdX7qbwxMTZenQXXhjGYDHCJDL6EjXPz5VXYuUfZM5LvRNNbcaeBbM6Xhb4tN53g
|
||||
// Creator was QZyuTa3ygjThaPRhrCp1BW4R5Sed6uAGN8 at 2014-10-23 11:14:42.525000+0:00
|
||||
// Have: 150000 [ATFunding], Price: 1.7000000 QORA
|
||||
|
||||
// Matching order 3Ufqi52nDL3Gi7KqVXpgebVN5FmLrdq2XyUJ11BwSV4byxQ2z96Q5CQeawGyanhpXS4XkYAaJTrNxsDDDxyxwbMN
|
||||
// Creator was QMRoD3RS5vJ4DVNBhBgGtQG4KT3PhkNALH at 2015-03-27 12:24:02.945000+0:00
|
||||
// Have: 2 QORA, Price: 0.58 [ATFunding]
|
||||
|
||||
// Trade: 1.17647050 [ATFunding] for 1.99999985 QORA
|
||||
|
||||
// Load/check settings, which potentially sets up blockchain config, etc.
|
||||
Common.useSettings("test-settings-old-asset.json");
|
||||
|
||||
// Transfer some test asset to bob
|
||||
try (Repository repository = RepositoryManager.getRepository()) {
|
||||
AssetUtils.transferAsset(repository, "alice", "bob", AssetUtils.testAssetId, BigDecimal.valueOf(200000L).setScale(8));
|
||||
}
|
||||
|
||||
final BigDecimal initialAmount = new BigDecimal("150000").setScale(8);
|
||||
final BigDecimal initialPrice = new BigDecimal("1.70000000").setScale(8);
|
||||
|
||||
final BigDecimal matchingAmount = new BigDecimal("2.00000000").setScale(8);
|
||||
final BigDecimal matchingPrice = new BigDecimal("0.58000000").setScale(8);
|
||||
|
||||
final BigDecimal tradedOtherAmount = new BigDecimal("1.17647050").setScale(8);
|
||||
final BigDecimal tradedQoraAmount = new BigDecimal("1.99999985").setScale(8);
|
||||
|
||||
genericTradeTest(AssetUtils.testAssetId, Asset.QORA, initialAmount, initialPrice, matchingAmount, matchingPrice, tradedOtherAmount, tradedQoraAmount);
|
||||
}
|
||||
|
||||
/**
|
||||
* Check legacy qora1 blockchain matching behaviour.
|
||||
*/
|
||||
@Test
|
||||
public void testQora1Compat2() throws DataException {
|
||||
// Asset 95 [Bitcoin] was issued by QiGx93L9rNHSNWCY1bJnQTPwB3nhxYTCUj with 21000000 quantity and was divisible.
|
||||
// Asset 96 [BitBTC] was issued by QiGx93L9rNHSNWCY1bJnQTPwB3nhxYTCUj with 21000000 quantity and was divisible.
|
||||
|
||||
// Initial order 3jinKPHEak9xrjeYtCaE1PawwRZeRkhYA6q4A7sqej7f3jio8WwXwXpfLWVZkPQ3h6cVdwPhcDFNgbbrBXcipHee
|
||||
// Creator was QiGx93L9rNHSNWCY1bJnQTPwB3nhxYTCUj at 2015-06-10 20:31:44.840000+0:00
|
||||
// Have: 1000000 [BitBTC], Price: 0.90000000 [Bitcoin]
|
||||
|
||||
// Matching order Jw1UfgspZ344waF8qLhGJanJXVa32FBoVvMW5ByFkyHvZEumF4fPqbaGMa76ba1imC4WX5t3Roa7r23Ys6rhKAA
|
||||
// Creator was QiGx93L9rNHSNWCY1bJnQTPwB3nhxYTCUj at 2015-06-14 17:49:41.410000+0:00
|
||||
// Have: 73251 [Bitcoin], Price: 1.01 [BitBTC]
|
||||
|
||||
// Trade: 81389.99991860 [BitBTC] for 73250.99992674 [Bitcoin]
|
||||
|
||||
// Load/check settings, which potentially sets up blockchain config, etc.
|
||||
Common.useSettings("test-settings-old-asset.json");
|
||||
|
||||
// Transfer some test asset to bob
|
||||
try (Repository repository = RepositoryManager.getRepository()) {
|
||||
AssetUtils.transferAsset(repository, "alice", "bob", AssetUtils.testAssetId, BigDecimal.valueOf(200000L).setScale(8));
|
||||
}
|
||||
|
||||
final BigDecimal initialAmount = new BigDecimal("1000000").setScale(8);
|
||||
final BigDecimal initialPrice = new BigDecimal("0.90000000").setScale(8);
|
||||
|
||||
final BigDecimal matchingAmount = new BigDecimal("73251").setScale(8);
|
||||
final BigDecimal matchingPrice = new BigDecimal("1.01000000").setScale(8);
|
||||
|
||||
final BigDecimal tradedHaveAmount = new BigDecimal("81389.99991860").setScale(8);
|
||||
final BigDecimal tradedWantAmount = new BigDecimal("73250.99992674").setScale(8);
|
||||
|
||||
genericTradeTest(Asset.QORA, AssetUtils.testAssetId, initialAmount, initialPrice, matchingAmount, matchingPrice, tradedHaveAmount, tradedWantAmount);
|
||||
}
|
||||
|
||||
private void genericTradeTest(long haveAssetId, long wantAssetId,
|
||||
BigDecimal initialAmount, BigDecimal initialPrice,
|
||||
BigDecimal matchingAmount, BigDecimal matchingPrice,
|
||||
BigDecimal tradedHaveAmount, BigDecimal tradedWantAmount) throws DataException {
|
||||
try (Repository repository = RepositoryManager.getRepository()) {
|
||||
Map<String, Map<Long, BigDecimal>> initialBalances = AccountUtils.getBalances(repository, haveAssetId, wantAssetId);
|
||||
|
||||
// Create initial order
|
||||
AssetUtils.createOrder(repository, "alice", haveAssetId, wantAssetId, initialAmount, initialPrice);
|
||||
|
||||
// Create matching order
|
||||
AssetUtils.createOrder(repository, "bob", wantAssetId, haveAssetId, matchingAmount, matchingPrice);
|
||||
|
||||
// Check balances to check expected outcome
|
||||
BigDecimal expectedBalance;
|
||||
|
||||
// Alice have asset
|
||||
expectedBalance = initialBalances.get("alice").get(haveAssetId).subtract(initialAmount);
|
||||
assertBalance(repository, "alice", haveAssetId, expectedBalance);
|
||||
|
||||
// Alice want asset
|
||||
expectedBalance = initialBalances.get("alice").get(wantAssetId).add(tradedWantAmount);
|
||||
assertBalance(repository, "alice", wantAssetId, expectedBalance);
|
||||
|
||||
// Bob want asset
|
||||
expectedBalance = initialBalances.get("bob").get(wantAssetId).subtract(matchingAmount);
|
||||
assertBalance(repository, "bob", wantAssetId, expectedBalance);
|
||||
|
||||
// Bob have asset
|
||||
expectedBalance = initialBalances.get("bob").get(haveAssetId).add(tradedHaveAmount);
|
||||
assertBalance(repository, "bob", haveAssetId, expectedBalance);
|
||||
}
|
||||
}
|
||||
|
||||
private static void assertBalance(Repository repository, String accountName, long assetId, BigDecimal expectedBalance) throws DataException {
|
||||
BigDecimal actualBalance = Common.getTestAccount(repository, accountName).getConfirmedBalance(assetId);
|
||||
|
||||
assertEqualBigDecimals(String.format("Test account '%s' asset %d balance incorrect", accountName, assetId), expectedBalance, actualBalance);
|
||||
}
|
||||
|
||||
}
|
32
src/test/java/org/qora/test/common/AccountUtils.java
Normal file
32
src/test/java/org/qora/test/common/AccountUtils.java
Normal file
@ -0,0 +1,32 @@
|
||||
package org.qora.test.common;
|
||||
|
||||
import java.math.BigDecimal;
|
||||
import java.util.HashMap;
|
||||
import java.util.Map;
|
||||
|
||||
import org.qora.repository.DataException;
|
||||
import org.qora.repository.Repository;
|
||||
|
||||
public class AccountUtils {
|
||||
|
||||
public static Map<String, Map<Long, BigDecimal>> getBalances(Repository repository, long... assetIds) throws DataException {
|
||||
Map<String, Map<Long, BigDecimal>> balances = new HashMap<>();
|
||||
|
||||
for (TestAccount account : Common.getTestAccounts(repository))
|
||||
for (Long assetId : assetIds) {
|
||||
BigDecimal balance = account.getConfirmedBalance(assetId);
|
||||
|
||||
balances.compute(account.accountName, (key, value) -> {
|
||||
if (value == null)
|
||||
value = new HashMap<Long, BigDecimal>();
|
||||
|
||||
value.put(assetId, balance);
|
||||
|
||||
return value;
|
||||
});
|
||||
}
|
||||
|
||||
return balances;
|
||||
}
|
||||
|
||||
}
|
59
src/test/java/org/qora/test/common/AssetUtils.java
Normal file
59
src/test/java/org/qora/test/common/AssetUtils.java
Normal file
@ -0,0 +1,59 @@
|
||||
package org.qora.test.common;
|
||||
|
||||
import java.math.BigDecimal;
|
||||
|
||||
import org.qora.account.PrivateKeyAccount;
|
||||
import org.qora.data.transaction.CreateAssetOrderTransactionData;
|
||||
import org.qora.data.transaction.IssueAssetTransactionData;
|
||||
import org.qora.data.transaction.TransactionData;
|
||||
import org.qora.data.transaction.TransferAssetTransactionData;
|
||||
import org.qora.group.Group;
|
||||
import org.qora.repository.DataException;
|
||||
import org.qora.repository.Repository;
|
||||
|
||||
public class AssetUtils {
|
||||
|
||||
public static final int txGroupId = Group.NO_GROUP;
|
||||
public static final BigDecimal fee = BigDecimal.ONE.setScale(8);
|
||||
public static final long testAssetId = 1L;
|
||||
|
||||
public static long issueAsset(Repository repository, String issuerAccountName, String assetName, long quantity, boolean isDivisible) throws DataException {
|
||||
PrivateKeyAccount account = Common.getTestAccount(repository, issuerAccountName);
|
||||
|
||||
byte[] reference = account.getLastReference();
|
||||
long timestamp = repository.getTransactionRepository().fromSignature(reference).getTimestamp() + 1000;
|
||||
|
||||
TransactionData transactionData = new IssueAssetTransactionData(timestamp, AssetUtils.txGroupId, reference, account.getPublicKey(), account.getAddress(), assetName, "desc", quantity, isDivisible, "{}", AssetUtils.fee);
|
||||
|
||||
TransactionUtils.signAndForge(repository, transactionData, account);
|
||||
|
||||
return repository.getAssetRepository().fromAssetName(assetName).getAssetId();
|
||||
}
|
||||
|
||||
public static void transferAsset(Repository repository, String fromAccountName, String toAccountName, long assetId, BigDecimal amount) throws DataException {
|
||||
PrivateKeyAccount fromAccount = Common.getTestAccount(repository, fromAccountName);
|
||||
PrivateKeyAccount toAccount = Common.getTestAccount(repository, toAccountName);
|
||||
|
||||
byte[] reference = fromAccount.getLastReference();
|
||||
long timestamp = repository.getTransactionRepository().fromSignature(reference).getTimestamp() + 1000;
|
||||
|
||||
TransactionData transactionData = new TransferAssetTransactionData(timestamp, AssetUtils.txGroupId, reference, fromAccount.getPublicKey(), toAccount.getAddress(), amount, assetId, AssetUtils.fee);
|
||||
|
||||
TransactionUtils.signAndForge(repository, transactionData, fromAccount);
|
||||
}
|
||||
|
||||
public static byte[] createOrder(Repository repository, String accountName, long haveAssetId, long wantAssetId, BigDecimal amount, BigDecimal wantAmount) 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);
|
||||
|
||||
TransactionUtils.signAndForge(repository, transactionData, account);
|
||||
|
||||
return repository.getAssetRepository().getAccountsOrders(account.getPublicKey(), null, null, null, null, true).get(0).getOrderId();
|
||||
}
|
||||
|
||||
}
|
169
src/test/java/org/qora/test/common/Common.java
Normal file
169
src/test/java/org/qora/test/common/Common.java
Normal file
@ -0,0 +1,169 @@
|
||||
package org.qora.test.common;
|
||||
|
||||
import static org.junit.Assert.*;
|
||||
|
||||
import java.math.BigDecimal;
|
||||
import java.net.URL;
|
||||
import java.security.Security;
|
||||
import java.util.Collections;
|
||||
import java.util.HashMap;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.function.Function;
|
||||
import java.util.function.Predicate;
|
||||
import java.util.stream.Collectors;
|
||||
|
||||
import org.bitcoinj.core.Base58;
|
||||
import org.bouncycastle.jce.provider.BouncyCastleProvider;
|
||||
import org.bouncycastle.jsse.provider.BouncyCastleJsseProvider;
|
||||
import org.junit.AfterClass;
|
||||
import org.junit.BeforeClass;
|
||||
import org.qora.block.Block;
|
||||
import org.qora.block.BlockChain;
|
||||
import org.qora.data.account.AccountBalanceData;
|
||||
import org.qora.data.asset.AssetData;
|
||||
import org.qora.data.block.BlockData;
|
||||
import org.qora.data.group.GroupData;
|
||||
import org.qora.repository.AccountRepository.BalanceOrdering;
|
||||
import org.qora.repository.DataException;
|
||||
import org.qora.repository.Repository;
|
||||
import org.qora.repository.RepositoryFactory;
|
||||
import org.qora.repository.RepositoryManager;
|
||||
import org.qora.repository.hsqldb.HSQLDBRepositoryFactory;
|
||||
import org.qora.settings.Settings;
|
||||
|
||||
public class Common {
|
||||
|
||||
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";
|
||||
|
||||
private static List<AssetData> initialAssets;
|
||||
private static List<GroupData> initialGroups;
|
||||
private static List<AccountBalanceData> initialBalances;
|
||||
|
||||
// TODO: converts users of these constants to TestAccount schema
|
||||
public static final byte[] v2testPrivateKey = Base58.decode("A9MNsATgQgruBUjxy2rjWY36Yf19uRioKZbiLFT2P7c6");
|
||||
public static final byte[] v2testPublicKey = Base58.decode("2tiMr5LTpaWCgbRvkPK8TFd7k63DyHJMMFFsz9uBf1ZP");
|
||||
public static final String v2testAddress = "QgV4s3xnzLhVBEJxcYui4u4q11yhUHsd9v";
|
||||
|
||||
static {
|
||||
// This must go before any calls to LogManager/Logger
|
||||
System.setProperty("java.util.logging.manager", "org.apache.logging.log4j.jul.LogManager");
|
||||
|
||||
Security.insertProviderAt(new BouncyCastleProvider(), 0);
|
||||
Security.insertProviderAt(new BouncyCastleJsseProvider(), 1);
|
||||
|
||||
// Load/check settings, which potentially sets up blockchain config, etc.
|
||||
URL testSettingsUrl = Common.class.getClassLoader().getResource(testSettingsFilename);
|
||||
assertNotNull("Test settings JSON file not found", testSettingsUrl);
|
||||
Settings.fileInstance(testSettingsUrl.getPath());
|
||||
}
|
||||
|
||||
private static Map<String, TestAccount> testAccountsByName = new HashMap<>();
|
||||
static {
|
||||
testAccountsByName.put("alice", new TestAccount(null, "alice", "A9MNsATgQgruBUjxy2rjWY36Yf19uRioKZbiLFT2P7c6"));
|
||||
testAccountsByName.put("bob", new TestAccount(null, "bob", "AdTd9SUEYSdTW8mgK3Gu72K97bCHGdUwi2VvLNjUohot"));
|
||||
testAccountsByName.put("chloe", new TestAccount(null, "chloe", "HqVngdE1AmEyDpfwTZqUdFHB13o4bCmpoTNAKEqki66K"));
|
||||
testAccountsByName.put("dilbert", new TestAccount(null, "dilbert", "Gakhh6Ln4vtBFM88nE9JmDaLBDtUBg51aVFpWfSkyVw5"));
|
||||
}
|
||||
|
||||
public static TestAccount getTestAccount(Repository repository, String name) {
|
||||
return new TestAccount(repository, name, testAccountsByName.get(name).getSeed());
|
||||
}
|
||||
|
||||
public static List<TestAccount> getTestAccounts(Repository repository) {
|
||||
return testAccountsByName.values().stream().map(account -> new TestAccount(repository, account.accountName, account.getSeed())).collect(Collectors.toList());
|
||||
}
|
||||
|
||||
public static void useSettings(String settingsFilename) throws DataException {
|
||||
closeRepository();
|
||||
|
||||
// Load/check settings, which potentially sets up blockchain config, etc.
|
||||
URL testSettingsUrl = Common.class.getClassLoader().getResource(settingsFilename);
|
||||
assertNotNull("Test settings JSON file not found", testSettingsUrl);
|
||||
Settings.fileInstance(testSettingsUrl.getPath());
|
||||
|
||||
setRepository();
|
||||
|
||||
resetBlockchain();
|
||||
}
|
||||
|
||||
public static void useDefaultSettings() throws DataException {
|
||||
useSettings(testSettingsFilename);
|
||||
}
|
||||
|
||||
public static void resetBlockchain() throws DataException {
|
||||
BlockChain.validate();
|
||||
|
||||
try (final Repository repository = RepositoryManager.getRepository()) {
|
||||
// Build snapshot of initial state in case we want to compare with post-test orphaning
|
||||
initialAssets = repository.getAssetRepository().getAllAssets();
|
||||
initialGroups = repository.getGroupRepository().getAllGroups();
|
||||
initialBalances = repository.getAccountRepository().getAssetBalances(Collections.emptyList(), Collections.emptyList(), BalanceOrdering.ASSET_ACCOUNT, null, null, null);
|
||||
|
||||
// Check that each test account can fetch their last reference
|
||||
for (TestAccount testAccount : getTestAccounts(repository))
|
||||
assertNotNull(String.format("Test account '%s' should have existing transaction", testAccount.accountName), testAccount.getLastReference());
|
||||
}
|
||||
}
|
||||
|
||||
/** Orphan back to genesis block and compare initial snapshot. */
|
||||
public static void orphanCheck() throws DataException {
|
||||
try (final Repository repository = RepositoryManager.getRepository()) {
|
||||
// Orphan back to genesis block
|
||||
while (repository.getBlockRepository().getBlockchainHeight() > 1) {
|
||||
BlockData blockData = repository.getBlockRepository().getLastBlock();
|
||||
Block block = new Block(repository, blockData);
|
||||
block.orphan();
|
||||
repository.saveChanges();
|
||||
}
|
||||
|
||||
List<AssetData> remainingAssets = repository.getAssetRepository().getAllAssets();
|
||||
checkOrphanedLists("asset", initialAssets, remainingAssets, AssetData::getAssetId);
|
||||
|
||||
List<GroupData> remainingGroups = repository.getGroupRepository().getAllGroups();
|
||||
checkOrphanedLists("group", initialGroups, remainingGroups, GroupData::getGroupId);
|
||||
|
||||
List<AccountBalanceData> remainingBalances = repository.getAccountRepository().getAssetBalances(Collections.emptyList(), Collections.emptyList(), BalanceOrdering.ASSET_ACCOUNT, null, null, null);
|
||||
checkOrphanedLists("account balance", initialBalances, remainingBalances, entry -> entry.getAssetName() + "-" + entry.getAddress());
|
||||
}
|
||||
}
|
||||
|
||||
private static <T> void checkOrphanedLists(String typeName, List<T> initial, List<T> remaining, Function<T, ? extends Object> keyExtractor) {
|
||||
Predicate<T> isInitial = entry -> initial.stream().anyMatch(initialEntry -> keyExtractor.apply(initialEntry).equals(keyExtractor.apply(entry)));
|
||||
Predicate<T> isRemaining = entry -> remaining.stream().anyMatch(remainingEntry -> keyExtractor.apply(remainingEntry).equals(keyExtractor.apply(entry)));
|
||||
|
||||
// Check all initial entries remain
|
||||
for (T initialEntry : initial)
|
||||
assertTrue(String.format("Genesis %s %s missing", typeName, keyExtractor.apply(initialEntry)), isRemaining.test(initialEntry));
|
||||
|
||||
// Remove initial entries from remaining to see there are any leftover
|
||||
remaining.removeIf(isInitial);
|
||||
|
||||
assertTrue(String.format("Non-genesis %s remains", typeName), remaining.isEmpty());
|
||||
}
|
||||
|
||||
@BeforeClass
|
||||
public static void setRepository() throws DataException {
|
||||
RepositoryFactory repositoryFactory = new HSQLDBRepositoryFactory(testConnectionUrl);
|
||||
RepositoryManager.setRepositoryFactory(repositoryFactory);
|
||||
}
|
||||
|
||||
@AfterClass
|
||||
public static void closeRepository() throws DataException {
|
||||
RepositoryManager.closeRepositoryFactory();
|
||||
}
|
||||
|
||||
public static void assertEmptyBlockchain(Repository repository) throws DataException {
|
||||
assertEquals("Blockchain should be empty for this test", 0, repository.getBlockRepository().getBlockchainHeight());
|
||||
}
|
||||
|
||||
public static void assertEqualBigDecimals(String message, BigDecimal expected, BigDecimal actual) {
|
||||
assertTrue(String.format("%s: expected %s, actual %s", message, expected.toPlainString(), actual.toPlainString()),
|
||||
actual.compareTo(expected) == 0);
|
||||
}
|
||||
|
||||
}
|
19
src/test/java/org/qora/test/common/TestAccount.java
Normal file
19
src/test/java/org/qora/test/common/TestAccount.java
Normal file
@ -0,0 +1,19 @@
|
||||
package org.qora.test.common;
|
||||
|
||||
import org.qora.account.PrivateKeyAccount;
|
||||
import org.qora.repository.Repository;
|
||||
import org.qora.utils.Base58;
|
||||
|
||||
public class TestAccount extends PrivateKeyAccount {
|
||||
public final String accountName;
|
||||
|
||||
public TestAccount(Repository repository, String accountName, byte[] privateKey) {
|
||||
super(repository, privateKey);
|
||||
|
||||
this.accountName = accountName;
|
||||
}
|
||||
|
||||
public TestAccount(Repository repository, String accountName, String privateKey) {
|
||||
this(repository, accountName, Base58.decode(privateKey));
|
||||
}
|
||||
}
|
34
src/test/java/org/qora/test/common/TransactionUtils.java
Normal file
34
src/test/java/org/qora/test/common/TransactionUtils.java
Normal file
@ -0,0 +1,34 @@
|
||||
package org.qora.test.common;
|
||||
|
||||
import static org.junit.Assert.assertEquals;
|
||||
import static org.junit.Assert.assertTrue;
|
||||
|
||||
import org.qora.account.PrivateKeyAccount;
|
||||
import org.qora.block.BlockGenerator;
|
||||
import org.qora.data.transaction.TransactionData;
|
||||
import org.qora.repository.DataException;
|
||||
import org.qora.repository.Repository;
|
||||
import org.qora.transaction.Transaction;
|
||||
import org.qora.transaction.Transaction.ValidationResult;
|
||||
|
||||
public class TransactionUtils {
|
||||
|
||||
public static void signAndForge(Repository repository, TransactionData transactionData, PrivateKeyAccount signingAccount) throws DataException {
|
||||
Transaction transaction = Transaction.fromData(repository, transactionData);
|
||||
transaction.sign(signingAccount);
|
||||
|
||||
// Add to unconfirmed
|
||||
assertTrue("Transaction's signature should be valid", transaction.isSignatureValid());
|
||||
|
||||
ValidationResult result = transaction.isValidUnconfirmed();
|
||||
assertEquals("Transaction invalid", ValidationResult.OK, result);
|
||||
|
||||
repository.getTransactionRepository().save(transactionData);
|
||||
repository.getTransactionRepository().unconfirmTransaction(transactionData);
|
||||
repository.saveChanges();
|
||||
|
||||
// Generate block
|
||||
BlockGenerator.generateTestingBlock(repository, signingAccount);
|
||||
}
|
||||
|
||||
}
|
92
src/test/resources/log4j2-test.properties
Normal file
92
src/test/resources/log4j2-test.properties
Normal file
@ -0,0 +1,92 @@
|
||||
rootLogger.level = info
|
||||
# On Windows, this might be rewritten as:
|
||||
# property.filename = ${sys:user.home}\\AppData\\Roaming\\Qora\\log.txt
|
||||
property.filename = log.txt
|
||||
|
||||
rootLogger.appenderRef.console.ref = stdout
|
||||
rootLogger.appenderRef.rolling.ref = FILE
|
||||
|
||||
# Override HSQLDB logging level to "warn" as too much is logged at "info"
|
||||
logger.hsqldb.name = hsqldb.db
|
||||
logger.hsqldb.level = warn
|
||||
|
||||
# Support optional, per-session HSQLDB debugging
|
||||
logger.hsqldbDebug.name = org.qora.repository.hsqldb.HSQLDBRepository
|
||||
logger.hsqldbDebug.level = debug
|
||||
|
||||
# Suppress extraneous Jersey warning
|
||||
logger.jerseyInject.name = org.glassfish.jersey.internal.inject.Providers
|
||||
logger.jerseyInject.level = error
|
||||
|
||||
# Suppress extraneous Jetty entries
|
||||
# 2019-02-14 11:46:27 INFO ContextHandler:851 - Started o.e.j.s.ServletContextHandler@6949e948{/,null,AVAILABLE}
|
||||
# 2019-02-14 11:46:27 INFO AbstractConnector:289 - Started ServerConnector@50ad322b{HTTP/1.1,[http/1.1]}{0.0.0.0:9085}
|
||||
# 2019-02-14 11:46:27 INFO Server:374 - jetty-9.4.11.v20180605; built: 2018-06-05T18:24:03.829Z; git: d5fc0523cfa96bfebfbda19606cad384d772f04c; jvm 1.8.0_181-b13
|
||||
# 2019-02-14 11:46:27 INFO Server:411 - Started @2539ms
|
||||
logger.oejsSCH.name = org.eclipse.jetty
|
||||
logger.oejsSCH.level = warn
|
||||
|
||||
# Suppress extraneous slf4j entries
|
||||
# 2019-02-14 11:46:27 INFO log:193 - Logging initialized @1636ms to org.eclipse.jetty.util.log.Slf4jLog
|
||||
logger.slf4j.name = org.slf4j
|
||||
logger.slf4j.level = warn
|
||||
|
||||
# Suppress extraneous Reflections entry
|
||||
# 2019-02-27 10:45:25 WARN Reflections:179 - given scan urls are empty. set urls in the configuration
|
||||
logger.reflections.name = org.reflections.Reflections
|
||||
logger.reflections.level = error
|
||||
|
||||
# Debugging transactions
|
||||
logger.transactions.name = org.qora.transaction
|
||||
logger.transactions.level = debug
|
||||
|
||||
# Debugging transformers
|
||||
logger.transformers.name = org.qora.transform.transaction
|
||||
logger.transformers.level = debug
|
||||
|
||||
# Debugging transaction searches
|
||||
logger.txSearch.name = org.qora.repository.hsqldb.transaction.HSQLDBTransactionRepository
|
||||
logger.txSearch.level = trace
|
||||
|
||||
# Debug block generator
|
||||
logger.blockgen.name = org.qora.block.BlockGenerator
|
||||
logger.blockgen.level = trace
|
||||
|
||||
# Debug synchronization
|
||||
logger.sync.name = org.qora.controller.Synchronizer
|
||||
logger.sync.level = trace
|
||||
|
||||
# Debug networking
|
||||
logger.network.name = org.qora.network.Network
|
||||
logger.network.level = trace
|
||||
logger.peer.name = org.qora.network.Peer
|
||||
logger.peer.level = trace
|
||||
logger.controller.name = org.qora.controller.Controller
|
||||
logger.controller.level = trace
|
||||
|
||||
# Debug defaultGroupId
|
||||
logger.defgrp.name = org.qora.account.Account
|
||||
logger.defgrp.level = trace
|
||||
|
||||
# Debug asset trades
|
||||
logger.assettrades.name = org.qora.asset.Order
|
||||
logger.assettrades.level = trace
|
||||
|
||||
appender.console.type = Console
|
||||
appender.console.name = stdout
|
||||
appender.console.layout.type = PatternLayout
|
||||
appender.console.layout.pattern = %d{yyyy-MM-dd HH:mm:ss} %-5p %c{1}:%L - %m%n
|
||||
appender.console.filter.threshold.type = ThresholdFilter
|
||||
appender.console.filter.threshold.level = error
|
||||
|
||||
appender.rolling.type = RollingFile
|
||||
appender.rolling.name = FILE
|
||||
appender.rolling.layout.type = PatternLayout
|
||||
appender.rolling.layout.pattern = %d{yyyy-MM-dd HH:mm:ss} %-5p %c{1}:%L - %m%n
|
||||
appender.rolling.filePattern = ${filename}.%i
|
||||
appender.rolling.policy.type = SizeBasedTriggeringPolicy
|
||||
appender.rolling.policy.size = 4MB
|
||||
# Set the immediate flush to true (default)
|
||||
# appender.rolling.immediateFlush = true
|
||||
# Set the append to true (default), should not overwrite
|
||||
# appender.rolling.append=true
|
34
src/test/resources/test-chain-old-asset.json
Normal file
34
src/test/resources/test-chain-old-asset.json
Normal file
@ -0,0 +1,34 @@
|
||||
{
|
||||
"isTestNet": true,
|
||||
"maxBalance": "10000000000",
|
||||
"blockDifficultyInterval": 10,
|
||||
"minBlockTime": 30,
|
||||
"maxBlockTime": 60,
|
||||
"blockTimestampMargin": 500,
|
||||
"maxBytesPerUnitFee": 1024,
|
||||
"unitFee": "0.1",
|
||||
"requireGroupForApproval": false,
|
||||
"genesisInfo": {
|
||||
"version": 4,
|
||||
"timestamp": 0,
|
||||
"generatingBalance": "10000000",
|
||||
"transactions": [
|
||||
{ "type": "ISSUE_ASSET", "owner": "QcFmNxSArv5tWEzCtTKb2Lqc5QkKuQ7RNs", "assetName": "QORA", "description": "QORA native coin", "data": "", "quantity": 10000000000, "isDivisible": true, "fee": 0, "reference": "3Verk6ZKBJc3WTTVfxFC9icSjKdM8b92eeJEpJP8qNizG4ZszNFq8wdDYdSjJXq2iogDFR1njyhsBdVpbvDfjzU7" },
|
||||
{ "type": "GENESIS", "recipient": "QgV4s3xnzLhVBEJxcYui4u4q11yhUHsd9v", "amount": "9876543210.12345678", "fee": 0 },
|
||||
{ "type": "GENESIS", "recipient": "QixPbJUwsaHsVEofJdozU9zgVqkK6aYhrK", "amount": "1000000", "fee": 0 },
|
||||
{ "type": "GENESIS", "recipient": "QaUpHNhT3Ygx6avRiKobuLdusppR5biXjL", "amount": "1000000", "fee": 0 },
|
||||
{ "type": "GENESIS", "recipient": "Qci5m9k4rcwe4ruKrZZQKka4FzUUMut3er", "amount": "1000000", "fee": 0 },
|
||||
{ "type": "ISSUE_ASSET", "owner": "QgV4s3xnzLhVBEJxcYui4u4q11yhUHsd9v", "assetName": "test", "description": "test asset", "data": "", "quantity": 1000000, "isDivisible": true, "fee": 0 }
|
||||
]
|
||||
},
|
||||
"featureTriggers": {
|
||||
"messageHeight": 0,
|
||||
"atHeight": 0,
|
||||
"assetsTimestamp": 0,
|
||||
"votingTimestamp": 0,
|
||||
"arbitraryTimestamp": 0,
|
||||
"powfixTimestamp": 0,
|
||||
"v2Timestamp": 0,
|
||||
"newAssetPricingTimestamp": 1600000000000
|
||||
}
|
||||
}
|
@ -7,16 +7,19 @@
|
||||
"blockTimestampMargin": 500,
|
||||
"maxBytesPerUnitFee": 1024,
|
||||
"unitFee": "0.1",
|
||||
"requireGroupForApproval": true,
|
||||
"defaultGroupId": 2,
|
||||
"requireGroupForApproval": false,
|
||||
"genesisInfo": {
|
||||
"version": 4,
|
||||
"timestamp": 0,
|
||||
"generatingBalance": "10000000",
|
||||
"transactions": [
|
||||
{ "type": "ISSUE_ASSET", "owner": "QcFmNxSArv5tWEzCtTKb2Lqc5QkKuQ7RNs", "assetName": "QORA", "description": "QORA native coin", "quantity": 10000000000, "isDivisible": true, "fee": 0, "reference": "3Verk6ZKBJc3WTTVfxFC9icSjKdM8b92eeJEpJP8qNizG4ZszNFq8wdDYdSjJXq2iogDFR1njyhsBdVpbvDfjzU7" },
|
||||
{ "type": "ISSUE_ASSET", "owner": "QcFmNxSArv5tWEzCtTKb2Lqc5QkKuQ7RNs", "assetName": "QORA", "description": "QORA native coin", "data": "", "quantity": 10000000000, "isDivisible": true, "fee": 0, "reference": "3Verk6ZKBJc3WTTVfxFC9icSjKdM8b92eeJEpJP8qNizG4ZszNFq8wdDYdSjJXq2iogDFR1njyhsBdVpbvDfjzU7" },
|
||||
{ "type": "GENESIS", "recipient": "QgV4s3xnzLhVBEJxcYui4u4q11yhUHsd9v", "amount": "9876543210.12345678", "fee": 0 },
|
||||
{ "type": "CREATE_GROUP", "creatorPublicKey": "2tiMr5LTpaWCgbRvkPK8TFd7k63DyHJMMFFsz9uBf1ZP", "owner": "QgV4s3xnzLhVBEJxcYui4u4q11yhUHsd9v", "groupName": "dev-group", "description": "developer group", "isOpen": false, "approvalThreshold": "PCT100", "minimumBlockDelay": 0, "maximumBlockDelay": 1440 }
|
||||
{ "type": "GENESIS", "recipient": "QixPbJUwsaHsVEofJdozU9zgVqkK6aYhrK", "amount": "1000000", "fee": 0 },
|
||||
{ "type": "GENESIS", "recipient": "QaUpHNhT3Ygx6avRiKobuLdusppR5biXjL", "amount": "1000000", "fee": 0 },
|
||||
{ "type": "GENESIS", "recipient": "Qci5m9k4rcwe4ruKrZZQKka4FzUUMut3er", "amount": "1000000", "fee": 0 },
|
||||
{ "type": "CREATE_GROUP", "creatorPublicKey": "2tiMr5LTpaWCgbRvkPK8TFd7k63DyHJMMFFsz9uBf1ZP", "owner": "QgV4s3xnzLhVBEJxcYui4u4q11yhUHsd9v", "groupName": "dev-group", "description": "developer group", "isOpen": false, "approvalThreshold": "PCT100", "minimumBlockDelay": 0, "maximumBlockDelay": 1440 },
|
||||
{ "type": "ISSUE_ASSET", "owner": "QgV4s3xnzLhVBEJxcYui4u4q11yhUHsd9v", "assetName": "test", "description": "test asset", "data": "", "quantity": 1000000, "isDivisible": true, "fee": 0 }
|
||||
]
|
||||
},
|
||||
"featureTriggers": {
|
||||
@ -26,6 +29,7 @@
|
||||
"votingTimestamp": 0,
|
||||
"arbitraryTimestamp": 0,
|
||||
"powfixTimestamp": 0,
|
||||
"v2Timestamp": 0
|
||||
"v2Timestamp": 0,
|
||||
"newAssetPricingTimestamp": 0
|
||||
}
|
||||
}
|
6
src/test/resources/test-settings-old-asset.json
Normal file
6
src/test/resources/test-settings-old-asset.json
Normal file
@ -0,0 +1,6 @@
|
||||
{
|
||||
"restrictedApi": false,
|
||||
"blockchainConfig": "src/test/resources/test-chain-old-asset.json",
|
||||
"wipeUnconfirmedOnStart": false,
|
||||
"minPeers": 0
|
||||
}
|
@ -1,6 +1,6 @@
|
||||
{
|
||||
"restrictedApi": false,
|
||||
"blockchainConfig": "src/test/resources/test-v2qorachain.json",
|
||||
"blockchainConfig": "src/test/resources/test-chain-v2.json",
|
||||
"wipeUnconfirmedOnStart": false,
|
||||
"minPeers": 0
|
||||
}
|
Loading…
Reference in New Issue
Block a user