diff --git a/src/main/java/org/qora/api/resource/AssetsResource.java b/src/main/java/org/qora/api/resource/AssetsResource.java index 1bea0c2e..9a0b14e7 100644 --- a/src/main/java/org/qora/api/resource/AssetsResource.java +++ b/src/main/java/org/qora/api/resource/AssetsResource.java @@ -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 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); } diff --git a/src/main/java/org/qora/asset/Order.java b/src/main/java/org/qora/asset/Order.java index 0f169206..44f4c31c 100644 --- a/src/main/java/org/qora/asset/Order.java +++ b/src/main/java/org/qora/asset/Order.java @@ -71,14 +71,22 @@ public class Order { * @param theirPrice * @return unit price of want asset */ - public static BigDecimal calculateAmountGranularity(AssetData haveAssetData, AssetData wantAssetData, BigDecimal theirPrice) { - // Multiplier to scale BigDecimal.setScale(8) fractional amounts into integers, essentially 1e8 + public static BigDecimal calculateAmountGranularity(AssetData haveAssetData, AssetData wantAssetData, OrderData theirOrderData) { + // Multiplier to scale BigDecimal fractional amounts into integer domain BigInteger multiplier = BigInteger.valueOf(1_0000_0000L); // Calculate the minimum increment at which I can buy using greatest-common-divisor - BigInteger haveAmount = multiplier; // 1 unit (* multiplier) - //BigInteger wantAmount = BigDecimal.valueOf(100_000_000L).setScale(Asset.BD_SCALE).divide(theirOrderData.getUnitPrice(), RoundingMode.DOWN).toBigInteger(); - BigInteger wantAmount = theirPrice.movePointRight(8).toBigInteger(); + BigInteger haveAmount; + BigInteger wantAmount; + if (theirOrderData.getTimestamp() >= BlockChain.getInstance().getNewAssetPricingTimestamp()) { + // "new" pricing scheme + haveAmount = theirOrderData.getAmount().movePointRight(8).toBigInteger(); + wantAmount = theirOrderData.getWantAmount().movePointRight(8).toBigInteger(); + } else { + // legacy "old" behaviour + haveAmount = multiplier; // 1 unit (* multiplier) + wantAmount = theirOrderData.getUnitPrice().movePointRight(8).toBigInteger(); + } BigInteger gcd = haveAmount.gcd(wantAmount); haveAmount = haveAmount.divide(gcd); @@ -115,7 +123,6 @@ public class Order { if (LOGGER.getLevel().isMoreSpecificThan(Level.DEBUG)) return; - final boolean isNewPricing = orderData.getTimestamp() >= BlockChain.getInstance().getNewAssetPricingTimestamp(); final String weThey = isMatchingNotInitial ? "They" : "We"; AssetData haveAssetData = this.repository.getAssetRepository().fromAssetId(orderData.getHaveAssetId()); @@ -125,15 +132,9 @@ public class Order { LOGGER.trace(String.format("%s have: %s %s", weThey, orderData.getAmount().stripTrailingZeros().toPlainString(), haveAssetData.getName())); - if (isNewPricing) { - LOGGER.trace(String.format("%s want: %s %s (@ %s %s each)", weThey, - orderData.getWantAmount().stripTrailingZeros().toPlainString(), wantAssetData.getName(), - orderData.getUnitPrice().toPlainString(), haveAssetData.getName())); - } else { - LOGGER.trace(String.format("%s want at least %s %s per %s (minimum %s %s total)", weThey, - orderData.getUnitPrice().toPlainString(), wantAssetData.getName(), haveAssetData.getName(), - orderData.getWantAmount().stripTrailingZeros().toPlainString(), wantAssetData.getName())); - } + LOGGER.trace(String.format("%s want at least %s %s per %s (minimum %s %s total)", weThey, + orderData.getUnitPrice().toPlainString(), wantAssetData.getName(), haveAssetData.getName(), + orderData.getWantAmount().stripTrailingZeros().toPlainString(), wantAssetData.getName())); } public void process() throws DataException { @@ -254,18 +255,13 @@ public class Order { if (matchedWantAmount.compareTo(BigDecimal.ZERO) <= 0) continue; - // We can skip granularity if theirWantAmountLeft is an [integer] multiple of matchedWantAmount as that obviously fits - if (!isTheirOrderNewAssetPricing || theirWantAmountLeft.remainder(matchedWantAmount).compareTo(BigDecimal.ZERO) > 0) { - // Not an integer multiple so do granularity check + // Calculate want-amount granularity, based on price and both assets' divisibility, so that have-amount traded is a valid amount (integer or to 8 d.p.) + BigDecimal wantGranularity = calculateAmountGranularity(haveAssetData, wantAssetData, theirOrderData); + LOGGER.trace("wantGranularity (want-asset amount granularity): " + wantGranularity.stripTrailingZeros().toPlainString() + " " + wantAssetData.getName()); - // Calculate amount granularity based on both assets' divisibility - BigDecimal wantGranularity = calculateAmountGranularity(haveAssetData, wantAssetData, theirOrderData.getUnitPrice()); - LOGGER.trace("wantGranularity (want-asset amount granularity): " + wantGranularity.stripTrailingZeros().toPlainString() + " " + wantAssetData.getName()); - - // Reduce matched amount (if need be) to fit granularity - matchedWantAmount = matchedWantAmount.subtract(matchedWantAmount.remainder(wantGranularity)); - LOGGER.trace("matchedWantAmount adjusted for granularity: " + matchedWantAmount.stripTrailingZeros().toPlainString() + " " + wantAssetData.getName()); - } + // Reduce matched amount (if need be) to fit granularity + matchedWantAmount = matchedWantAmount.subtract(matchedWantAmount.remainder(wantGranularity)); + LOGGER.trace("matchedWantAmount adjusted for granularity: " + matchedWantAmount.stripTrailingZeros().toPlainString() + " " + wantAssetData.getName()); // If we can't buy anything then try another order if (matchedWantAmount.compareTo(BigDecimal.ZERO) <= 0) diff --git a/src/main/java/org/qora/data/asset/OrderData.java b/src/main/java/org/qora/data/asset/OrderData.java index 4794c1f3..c12903d4 100644 --- a/src/main/java/org/qora/data/asset/OrderData.java +++ b/src/main/java/org/qora/data/asset/OrderData.java @@ -4,6 +4,7 @@ import java.math.BigDecimal; import javax.xml.bind.annotation.XmlAccessType; import javax.xml.bind.annotation.XmlAccessorType; +import javax.xml.bind.annotation.XmlElement; import io.swagger.v3.oas.annotations.media.Schema; @@ -24,7 +25,8 @@ public class OrderData implements Comparable { @Schema(description = "amount of \"have\" asset to trade") private BigDecimal amount; - @Schema(description = "amount of \"want\" asset to receive") + @Schema(name = "return", description = "amount of \"want\" asset to receive") + @XmlElement(name = "return") private BigDecimal wantAmount; @Schema(description = "amount of \"want\" asset to receive per unit of \"have\" asset traded") diff --git a/src/main/java/org/qora/data/transaction/CreateAssetOrderTransactionData.java b/src/main/java/org/qora/data/transaction/CreateAssetOrderTransactionData.java index e826c454..208b1489 100644 --- a/src/main/java/org/qora/data/transaction/CreateAssetOrderTransactionData.java +++ b/src/main/java/org/qora/data/transaction/CreateAssetOrderTransactionData.java @@ -22,8 +22,9 @@ public class CreateAssetOrderTransactionData extends TransactionData { private long wantAssetId; @Schema(description = "amount of \"have\" asset to trade") private BigDecimal amount; - @Schema(description = "amount of \"want\" asset to receive") - private BigDecimal price; + @Schema(name = "return", description = "amount of \"want\" asset to receive") + @XmlElement(name = "return") + private BigDecimal wantAmount; // Constructors @@ -33,18 +34,18 @@ public class CreateAssetOrderTransactionData extends TransactionData { } public CreateAssetOrderTransactionData(long timestamp, int txGroupId, byte[] reference, byte[] creatorPublicKey, long haveAssetId, long wantAssetId, - BigDecimal amount, BigDecimal price, BigDecimal fee, byte[] signature) { + BigDecimal amount, BigDecimal wantAmount, BigDecimal fee, byte[] signature) { super(TransactionType.CREATE_ASSET_ORDER, timestamp, txGroupId, reference, creatorPublicKey, fee, signature); this.haveAssetId = haveAssetId; this.wantAssetId = wantAssetId; this.amount = amount; - this.price = price; + this.wantAmount = wantAmount; } public CreateAssetOrderTransactionData(long timestamp, int txGroupId, byte[] reference, byte[] creatorPublicKey, long haveAssetId, long wantAssetId, - BigDecimal amount, BigDecimal price, BigDecimal fee) { - this(timestamp, txGroupId, reference, creatorPublicKey, haveAssetId, wantAssetId, amount, price, fee, null); + BigDecimal amount, BigDecimal wantAmount, BigDecimal fee) { + this(timestamp, txGroupId, reference, creatorPublicKey, haveAssetId, wantAssetId, amount, wantAmount, fee, null); } // Getters/Setters @@ -61,8 +62,8 @@ public class CreateAssetOrderTransactionData extends TransactionData { return this.amount; } - public BigDecimal getPrice() { - return this.price; + public BigDecimal getWantAmount() { + return this.wantAmount; } // Re-expose creatorPublicKey for this transaction type for JAXB diff --git a/src/main/java/org/qora/repository/hsqldb/HSQLDBDatabaseUpdates.java b/src/main/java/org/qora/repository/hsqldb/HSQLDBDatabaseUpdates.java index 75f8cc1f..80ad244a 100644 --- a/src/main/java/org/qora/repository/hsqldb/HSQLDBDatabaseUpdates.java +++ b/src/main/java/org/qora/repository/hsqldb/HSQLDBDatabaseUpdates.java @@ -651,8 +651,8 @@ public class HSQLDBDatabaseUpdates { stmt.execute("UPDATE AssetOrders set want_amount = amount * unit_price"); // want-amounts all set, so disallow NULL stmt.execute("ALTER TABLE AssetOrders ALTER COLUMN want_amount SET NOT NULL"); - // Convert old "price" into buying unit price - stmt.execute("UPDATE AssetOrders set unit_price = 1 / unit_price"); + // Rename corresponding column in CreateAssetOrderTransactions + stmt.execute("ALTER TABLE CreateAssetOrderTransactions ALTER COLUMN price RENAME TO want_amount"); break; default: diff --git a/src/main/java/org/qora/repository/hsqldb/transaction/HSQLDBCreateAssetOrderTransactionRepository.java b/src/main/java/org/qora/repository/hsqldb/transaction/HSQLDBCreateAssetOrderTransactionRepository.java index 1dc663a8..517691a9 100644 --- a/src/main/java/org/qora/repository/hsqldb/transaction/HSQLDBCreateAssetOrderTransactionRepository.java +++ b/src/main/java/org/qora/repository/hsqldb/transaction/HSQLDBCreateAssetOrderTransactionRepository.java @@ -18,16 +18,16 @@ public class HSQLDBCreateAssetOrderTransactionRepository extends HSQLDBTransacti TransactionData fromBase(long timestamp, int txGroupId, byte[] reference, byte[] creatorPublicKey, BigDecimal fee, byte[] signature) throws DataException { try (ResultSet resultSet = this.repository - .checkedExecute("SELECT have_asset_id, amount, want_asset_id, 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); diff --git a/src/main/java/org/qora/transaction/CreateAssetOrderTransaction.java b/src/main/java/org/qora/transaction/CreateAssetOrderTransaction.java index 04d1017b..fac31c54 100644 --- a/src/main/java/org/qora/transaction/CreateAssetOrderTransaction.java +++ b/src/main/java/org/qora/transaction/CreateAssetOrderTransaction.java @@ -83,7 +83,7 @@ public class CreateAssetOrderTransaction extends Transaction { return ValidationResult.NEGATIVE_AMOUNT; // Check price is positive - if (createOrderTransactionData.getPrice().compareTo(BigDecimal.ZERO) <= 0) + if (createOrderTransactionData.getWantAmount().compareTo(BigDecimal.ZERO) <= 0) return ValidationResult.NEGATIVE_PRICE; // Check fee is positive @@ -133,12 +133,12 @@ public class CreateAssetOrderTransaction extends Transaction { // Check total return from fulfilled order would be integer if "want" asset is not divisible if (createOrderTransactionData.getTimestamp() >= BlockChain.getInstance().getNewAssetPricingTimestamp()) { // "new" asset pricing - if (!wantAssetData.getIsDivisible() && createOrderTransactionData.getPrice().stripTrailingZeros().scale() > 0) + if (!wantAssetData.getIsDivisible() && createOrderTransactionData.getWantAmount().stripTrailingZeros().scale() > 0) return ValidationResult.INVALID_RETURN; } else { // "old" asset pricing if (!wantAssetData.getIsDivisible() - && createOrderTransactionData.getAmount().multiply(createOrderTransactionData.getPrice()).stripTrailingZeros().scale() > 0) + && createOrderTransactionData.getAmount().multiply(createOrderTransactionData.getWantAmount()).stripTrailingZeros().scale() > 0) return ValidationResult.INVALID_RETURN; } @@ -166,12 +166,12 @@ public class CreateAssetOrderTransaction extends Transaction { if (createOrderTransactionData.getTimestamp() >= BlockChain.getInstance().getNewAssetPricingTimestamp()) { // "new" asset pricing: want-amount provided, unit price to be calculated - wantAmount = createOrderTransactionData.getPrice(); + wantAmount = createOrderTransactionData.getWantAmount(); unitPrice = wantAmount.setScale(Order.BD_PRICE_STORAGE_SCALE).divide(createOrderTransactionData.getAmount().setScale(Order.BD_PRICE_STORAGE_SCALE), RoundingMode.DOWN); } else { // "old" asset pricing: selling unit price provided, want-amount to be calculated - wantAmount = createOrderTransactionData.getAmount().multiply(createOrderTransactionData.getPrice()); - unitPrice = createOrderTransactionData.getPrice(); + wantAmount = createOrderTransactionData.getAmount().multiply(createOrderTransactionData.getWantAmount()); + unitPrice = createOrderTransactionData.getWantAmount(); // getWantAmount() was getPrice() in the "old" pricing scheme } // Process the order itself diff --git a/src/main/java/org/qora/transform/transaction/CreateAssetOrderTransactionTransformer.java b/src/main/java/org/qora/transform/transaction/CreateAssetOrderTransactionTransformer.java index 8597e2af..def4303e 100644 --- a/src/main/java/org/qora/transform/transaction/CreateAssetOrderTransactionTransformer.java +++ b/src/main/java/org/qora/transform/transaction/CreateAssetOrderTransactionTransformer.java @@ -88,7 +88,7 @@ public class CreateAssetOrderTransactionTransformer extends TransactionTransform Serialization.serializeBigDecimal(bytes, createOrderTransactionData.getAmount(), AMOUNT_LENGTH); // Under "new" asset pricing, this is actually the want-amount - Serialization.serializeBigDecimal(bytes, createOrderTransactionData.getPrice(), AMOUNT_LENGTH); + Serialization.serializeBigDecimal(bytes, createOrderTransactionData.getWantAmount(), AMOUNT_LENGTH); Serialization.serializeBigDecimal(bytes, createOrderTransactionData.getFee()); @@ -128,7 +128,7 @@ public class CreateAssetOrderTransactionTransformer extends TransactionTransform Serialization.serializeBigDecimal(bytes, createOrderTransactionData.getAmount(), AMOUNT_LENGTH); // This is the crucial difference - Serialization.serializeBigDecimal(bytes, createOrderTransactionData.getPrice(), FEE_LENGTH); + Serialization.serializeBigDecimal(bytes, createOrderTransactionData.getWantAmount(), FEE_LENGTH); Serialization.serializeBigDecimal(bytes, createOrderTransactionData.getFee()); diff --git a/src/test/java/org/qora/test/ATTests.java b/src/test/java/org/qora/test/ATTests.java index e759a928..b13388f3 100644 --- a/src/test/java/org/qora/test/ATTests.java +++ b/src/test/java/org/qora/test/ATTests.java @@ -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; diff --git a/src/test/java/org/qora/test/BlockTests.java b/src/test/java/org/qora/test/BlockTests.java index 6487aaa5..b228feca 100644 --- a/src/test/java/org/qora/test/BlockTests.java +++ b/src/test/java/org/qora/test/BlockTests.java @@ -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; diff --git a/src/test/java/org/qora/test/BlockchainTests.java b/src/test/java/org/qora/test/BlockchainTests.java index f80965bb..c6e45138 100644 --- a/src/test/java/org/qora/test/BlockchainTests.java +++ b/src/test/java/org/qora/test/BlockchainTests.java @@ -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 { diff --git a/src/test/java/org/qora/test/CryptoTests.java b/src/test/java/org/qora/test/CryptoTests.java index c8947c82..d81b5df9 100644 --- a/src/test/java/org/qora/test/CryptoTests.java +++ b/src/test/java/org/qora/test/CryptoTests.java @@ -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.*; diff --git a/src/test/java/org/qora/test/GenesisTests.java b/src/test/java/org/qora/test/GenesisTests.java index 59df1090..65c1d8b5 100644 --- a/src/test/java/org/qora/test/GenesisTests.java +++ b/src/test/java/org/qora/test/GenesisTests.java @@ -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.*; diff --git a/src/test/java/org/qora/test/GroupApprovalTests.java b/src/test/java/org/qora/test/GroupApprovalTests.java index 695a2163..76745de5 100644 --- a/src/test/java/org/qora/test/GroupApprovalTests.java +++ b/src/test/java/org/qora/test/GroupApprovalTests.java @@ -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; diff --git a/src/test/java/org/qora/test/LoadTests.java b/src/test/java/org/qora/test/LoadTests.java index 63b764a5..c911aa6d 100644 --- a/src/test/java/org/qora/test/LoadTests.java +++ b/src/test/java/org/qora/test/LoadTests.java @@ -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; diff --git a/src/test/java/org/qora/test/NavigationTests.java b/src/test/java/org/qora/test/NavigationTests.java index 64db0a22..fba2ecf6 100644 --- a/src/test/java/org/qora/test/NavigationTests.java +++ b/src/test/java/org/qora/test/NavigationTests.java @@ -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; diff --git a/src/test/java/org/qora/test/RepositoryTests.java b/src/test/java/org/qora/test/RepositoryTests.java index 5847086f..b7866ae0 100644 --- a/src/test/java/org/qora/test/RepositoryTests.java +++ b/src/test/java/org/qora/test/RepositoryTests.java @@ -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.*; diff --git a/src/test/java/org/qora/test/SaveTests.java b/src/test/java/org/qora/test/SaveTests.java index ce6eb556..c9898b8d 100644 --- a/src/test/java/org/qora/test/SaveTests.java +++ b/src/test/java/org/qora/test/SaveTests.java @@ -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 { diff --git a/src/test/java/org/qora/test/SerializationTests.java b/src/test/java/org/qora/test/SerializationTests.java index 8741e011..045d313e 100644 --- a/src/test/java/org/qora/test/SerializationTests.java +++ b/src/test/java/org/qora/test/SerializationTests.java @@ -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; diff --git a/src/test/java/org/qora/test/SignatureTests.java b/src/test/java/org/qora/test/SignatureTests.java index 033b87bc..f667c241 100644 --- a/src/test/java/org/qora/test/SignatureTests.java +++ b/src/test/java/org/qora/test/SignatureTests.java @@ -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; diff --git a/src/test/java/org/qora/test/TransactionTests.java b/src/test/java/org/qora/test/TransactionTests.java index e209d05f..efe6b47c 100644 --- a/src/test/java/org/qora/test/TransactionTests.java +++ b/src/test/java/org/qora/test/TransactionTests.java @@ -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; diff --git a/src/test/java/org/qora/test/assets/TradingTests.java b/src/test/java/org/qora/test/assets/TradingTests.java index f34c53b9..7662ecfa 100644 --- a/src/test/java/org/qora/test/assets/TradingTests.java +++ b/src/test/java/org/qora/test/assets/TradingTests.java @@ -4,14 +4,16 @@ 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; import org.qora.test.common.AccountUtils; import org.qora.test.common.AssetUtils; - -import static org.junit.Assert.*; +import org.qora.test.common.Common; import java.math.BigDecimal; import java.util.Map; @@ -25,6 +27,46 @@ public class TradingTests extends Common { @After public void afterTest() throws DataException { + Common.orphanCheck(); + } + + /** + * Check granularity adjustment values. + *

+ * If trading at a price of 12 eggs for 1 coin + * then trades can only happen at multiples of + * 0.000000001 or 0.00000012 depending on direction. + */ + @Test + public void testDivisibleGranularities() { + testGranularity(true, true, "12", "1", "0.00000012"); + testGranularity(true, true, "1", "12", "0.00000001"); + } + + /** + * Check granularity adjustment values. + *

+ * If trading at a price of 123 riches per 50301 rags, + * then the GCD(123, 50301) is 3 and so trades can only + * happen at multiples of (50301/3) = 16767 rags or + * (123/3) = 41 riches. + */ + @Test + public void testIndivisibleGranularities() { + testGranularity(false, false, "50301", "123", "16767"); + testGranularity(false, false, "123", "50301", "41"); + } + + private void testGranularity(boolean isOurHaveDivisible, boolean isOurWantDivisible, String theirHaveAmount, String theirWantAmount, String expectedGranularity) { + final long newPricingTimestamp = BlockChain.getInstance().getNewAssetPricingTimestamp() + 1; + + final AssetData ourHaveAssetData = new AssetData(null, null, null, 0, isOurHaveDivisible, null, 0, null); + final AssetData ourWantAssetData = new AssetData(null, null, null, 0, isOurWantDivisible, null, 0, null); + + OrderData theirOrderData = new OrderData(null, null, 0, 0, new BigDecimal(theirHaveAmount), new BigDecimal(theirWantAmount), null, newPricingTimestamp); + + BigDecimal granularity = Order.calculateAmountGranularity(ourHaveAssetData, ourWantAssetData, theirOrderData); + assertEqualBigDecimals("Granularity incorrect", new BigDecimal(expectedGranularity), granularity); } /** @@ -164,8 +206,7 @@ public class TradingTests extends Common { BigDecimal expectedFulfilled = asset113Matched2; BigDecimal actualFulfilled = repository.getAssetRepository().fromOrderId(furtherOrderId).getFulfilled(); - assertTrue(String.format("Order fulfilled incorrect: expected %s, actual %s", expectedFulfilled.toPlainString(), actualFulfilled.toPlainString()), - actualFulfilled.compareTo(expectedFulfilled) == 0); + assertEqualBigDecimals("Order fulfilled incorrect", expectedFulfilled, actualFulfilled); } } @@ -394,8 +435,7 @@ public class TradingTests extends Common { private static void assertBalance(Repository repository, String accountName, long assetId, BigDecimal expectedBalance) throws DataException { BigDecimal actualBalance = Common.getTestAccount(repository, accountName).getConfirmedBalance(assetId); - assertTrue(String.format("Test account '%s' asset %d balance incorrect: expected %s, actual %s", accountName, assetId, expectedBalance.toPlainString(), actualBalance.toPlainString()), - actualBalance.compareTo(expectedBalance) == 0); + assertEqualBigDecimals(String.format("Test account '%s' asset %d balance incorrect", accountName, assetId), expectedBalance, actualBalance); } } \ No newline at end of file diff --git a/src/test/java/org/qora/test/common/AccountUtils.java b/src/test/java/org/qora/test/common/AccountUtils.java index cba82127..63e6012a 100644 --- a/src/test/java/org/qora/test/common/AccountUtils.java +++ b/src/test/java/org/qora/test/common/AccountUtils.java @@ -6,7 +6,6 @@ import java.util.Map; import org.qora.repository.DataException; import org.qora.repository.Repository; -import org.qora.test.Common; public class AccountUtils { diff --git a/src/test/java/org/qora/test/common/AssetUtils.java b/src/test/java/org/qora/test/common/AssetUtils.java index 12cc91f8..9cca4eb1 100644 --- a/src/test/java/org/qora/test/common/AssetUtils.java +++ b/src/test/java/org/qora/test/common/AssetUtils.java @@ -10,7 +10,6 @@ import org.qora.data.transaction.TransferAssetTransactionData; import org.qora.group.Group; import org.qora.repository.DataException; import org.qora.repository.Repository; -import org.qora.test.Common; public class AssetUtils { @@ -26,7 +25,7 @@ public class AssetUtils { TransactionData transactionData = new IssueAssetTransactionData(timestamp, AssetUtils.txGroupId, reference, account.getPublicKey(), account.getAddress(), assetName, "desc", quantity, isDivisible, "{}", AssetUtils.fee); - Common.signAndForge(repository, transactionData, account); + TransactionUtils.signAndForge(repository, transactionData, account); return repository.getAssetRepository().fromAssetName(assetName).getAssetId(); } @@ -40,7 +39,7 @@ public class AssetUtils { TransactionData transactionData = new TransferAssetTransactionData(timestamp, AssetUtils.txGroupId, reference, fromAccount.getPublicKey(), toAccount.getAddress(), amount, assetId, AssetUtils.fee); - Common.signAndForge(repository, transactionData, fromAccount); + TransactionUtils.signAndForge(repository, transactionData, fromAccount); } public static byte[] createOrder(Repository repository, String accountName, long haveAssetId, long wantAssetId, BigDecimal amount, BigDecimal wantAmount) throws DataException { @@ -52,7 +51,7 @@ public class AssetUtils { // 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); - Common.signAndForge(repository, transactionData, account); + TransactionUtils.signAndForge(repository, transactionData, account); return repository.getAssetRepository().getAccountsOrders(account.getPublicKey(), null, null, null, null, true).get(0).getOrderId(); } diff --git a/src/test/java/org/qora/test/Common.java b/src/test/java/org/qora/test/common/Common.java similarity index 52% rename from src/test/java/org/qora/test/Common.java rename to src/test/java/org/qora/test/common/Common.java index fd14f9f8..578e4ddf 100644 --- a/src/test/java/org/qora/test/Common.java +++ b/src/test/java/org/qora/test/common/Common.java @@ -1,12 +1,16 @@ -package org.qora.test; +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; @@ -14,28 +18,33 @@ import org.bouncycastle.jce.provider.BouncyCastleProvider; import org.bouncycastle.jsse.provider.BouncyCastleJsseProvider; import org.junit.AfterClass; import org.junit.BeforeClass; -import org.qora.account.PrivateKeyAccount; -import org.qora.api.resource.TransactionsResource.ConfirmationStatus; +import org.qora.block.Block; import org.qora.block.BlockChain; -import org.qora.block.BlockGenerator; -import org.qora.data.transaction.TransactionData; +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; -import org.qora.test.common.TestAccount; -import org.qora.transaction.Transaction; -import org.qora.transaction.Transaction.ValidationResult; 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 initialAssets; + private static List initialGroups; + private static List 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"; @@ -53,7 +62,6 @@ public class Common { Settings.fileInstance(testSettingsUrl.getPath()); } - public static Map lastTransactionByAddress; private static Map testAccountsByName = new HashMap<>(); static { testAccountsByName.put("alice", new TestAccount(null, "alice", "A9MNsATgQgruBUjxy2rjWY36Yf19uRioKZbiLFT2P7c6")); @@ -90,35 +98,52 @@ public class Common { public static void resetBlockchain() throws DataException { BlockChain.validate(); - lastTransactionByAddress = new HashMap<>(); + 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); - try (Repository repository = RepositoryManager.getRepository()) { - for (TestAccount account : testAccountsByName.values()) { - List signatures = repository.getTransactionRepository().getSignaturesMatchingCriteria(null, null, null, account.getAddress(), ConfirmationStatus.BOTH, 1, null, true); - assertFalse(String.format("Test account '%s' should have existing transaction", account.accountName), signatures.isEmpty()); - - TransactionData transactionData = repository.getTransactionRepository().fromSignature(signatures.get(0)); - lastTransactionByAddress.put(account.getAddress(), transactionData); - } + // 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()); } } - public static void signAndForge(Repository repository, TransactionData transactionData, PrivateKeyAccount signingAccount) throws DataException { - Transaction transaction = Transaction.fromData(repository, transactionData); - transaction.sign(signingAccount); + /** 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(); + } - // Add to unconfirmed - assertTrue("Transaction's signature should be valid", transaction.isSignatureValid()); + List remainingAssets = repository.getAssetRepository().getAllAssets(); + checkOrphanedLists("asset", initialAssets, remainingAssets, AssetData::getAssetId); - ValidationResult result = transaction.isValidUnconfirmed(); - assertEquals("Transaction invalid", ValidationResult.OK, result); + List remainingGroups = repository.getGroupRepository().getAllGroups(); + checkOrphanedLists("group", initialGroups, remainingGroups, GroupData::getGroupId); - repository.getTransactionRepository().save(transactionData); - repository.getTransactionRepository().unconfirmTransaction(transactionData); - repository.saveChanges(); + List remainingBalances = repository.getAccountRepository().getAssetBalances(Collections.emptyList(), Collections.emptyList(), BalanceOrdering.ASSET_ACCOUNT, null, null, null); + checkOrphanedLists("account balance", initialBalances, remainingBalances, entry -> entry.getAssetName() + "-" + entry.getAddress()); + } + } - // Generate block - BlockGenerator.generateTestingBlock(repository, signingAccount); + private static void checkOrphanedLists(String typeName, List initial, List remaining, Function keyExtractor) { + Predicate isInitial = entry -> initial.stream().anyMatch(initialEntry -> keyExtractor.apply(initialEntry).equals(keyExtractor.apply(entry))); + Predicate 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 @@ -136,4 +161,9 @@ public class Common { 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); + } + } diff --git a/src/test/java/org/qora/test/common/TransactionUtils.java b/src/test/java/org/qora/test/common/TransactionUtils.java new file mode 100644 index 00000000..59fefccf --- /dev/null +++ b/src/test/java/org/qora/test/common/TransactionUtils.java @@ -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); + } + +}