mirror of
https://github.com/Qortal/qortal.git
synced 2025-07-23 04:36:50 +00:00
Interim commit on new asset trading schema
Better order matching, especially in situations where inexact fractional representations (e.g. 1/12) or rounding issues might occur. Also better matching with indivisible assets. Essentially change ordering from have-amount & price to have-amount and want-return, leaving unit price to be calculated internally to a finer degree (in some cases to 48 decimal points). Corresponding unit tests to cover both legacy and new scenarios. Support for tests to switch between blockchain configs. "New" pricing schema is its own 'feature trigger' independent from general qorav2 switch. Safety checks added during trading process. HSQLDB schema changes (will probably need careful conflict resolution on merge). Still to do: API changes etc.
This commit is contained in:
@@ -7,6 +7,7 @@ import java.security.Security;
|
||||
import java.util.HashMap;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.stream.Collectors;
|
||||
|
||||
import org.bitcoinj.core.Base58;
|
||||
import org.bouncycastle.jce.provider.BouncyCastleProvider;
|
||||
@@ -33,7 +34,7 @@ public class Common {
|
||||
public static final String testConnectionUrl = "jdbc:hsqldb:mem:testdb";
|
||||
// public static final String testConnectionUrl = "jdbc:hsqldb:file:testdb/blockchain;create=true";
|
||||
|
||||
public static final String testSettingsFilename = "test-settings.json";
|
||||
public static final String testSettingsFilename = "test-settings-v2.json";
|
||||
|
||||
public static final byte[] v2testPrivateKey = Base58.decode("A9MNsATgQgruBUjxy2rjWY36Yf19uRioKZbiLFT2P7c6");
|
||||
public static final byte[] v2testPublicKey = Base58.decode("2tiMr5LTpaWCgbRvkPK8TFd7k63DyHJMMFFsz9uBf1ZP");
|
||||
@@ -53,24 +54,48 @@ public class Common {
|
||||
}
|
||||
|
||||
public static Map<String, TransactionData> lastTransactionByAddress;
|
||||
public static Map<String, TestAccount> testAccountsByName = new HashMap<>();
|
||||
private static Map<String, TestAccount> testAccountsByName = new HashMap<>();
|
||||
static {
|
||||
testAccountsByName.put("main", new TestAccount("A9MNsATgQgruBUjxy2rjWY36Yf19uRioKZbiLFT2P7c6"));
|
||||
testAccountsByName.put("dummy", new TestAccount("AdTd9SUEYSdTW8mgK3Gu72K97bCHGdUwi2VvLNjUohot"));
|
||||
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 PrivateKeyAccount getTestAccount(Repository repository, String name) {
|
||||
return new PrivateKeyAccount(repository, testAccountsByName.get(name).getSeed());
|
||||
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();
|
||||
|
||||
lastTransactionByAddress = new HashMap<>();
|
||||
|
||||
try (Repository repository = RepositoryManager.getRepository()) {
|
||||
for (TestAccount account : testAccountsByName.values()) {
|
||||
List<byte[]> signatures = repository.getTransactionRepository().getSignaturesMatchingCriteria(null, null, null, account.getAddress(), ConfirmationStatus.BOTH, 1, null, true);
|
||||
assertFalse("Test account should have existing transaction", signatures.isEmpty());
|
||||
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);
|
||||
|
@@ -989,7 +989,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);
|
||||
|
||||
|
@@ -1,5 +1,6 @@
|
||||
package org.qora.test.assets;
|
||||
|
||||
import org.junit.After;
|
||||
import org.junit.Before;
|
||||
import org.junit.Test;
|
||||
import org.qora.asset.Asset;
|
||||
@@ -7,48 +8,394 @@ 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 java.math.BigDecimal;
|
||||
import java.util.Map;
|
||||
|
||||
public class TradingTests extends Common {
|
||||
|
||||
@Before
|
||||
public void beforeTest() throws DataException {
|
||||
Common.resetBlockchain();
|
||||
Common.useDefaultSettings();
|
||||
}
|
||||
|
||||
/*
|
||||
* Check full matching of orders with prices that
|
||||
* can't be represented in floating binary.
|
||||
*
|
||||
* For example, sell 1 GOLD for 12 QORA so
|
||||
* price is 1/12 or 0.083...
|
||||
@After
|
||||
public void afterTest() throws DataException {
|
||||
}
|
||||
|
||||
/**
|
||||
* 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 testNonExactFraction() throws DataException {
|
||||
final long qoraAmount = 24L;
|
||||
final long otherAmount = 2L;
|
||||
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 long transferAmount = 100L;
|
||||
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()) {
|
||||
// Create initial order
|
||||
AssetUtils.createOrder(repository, "main", Asset.QORA, AssetUtils.testAssetId, qoraAmount, otherAmount);
|
||||
Map<String, Map<Long, BigDecimal>> initialBalances = AccountUtils.getBalances(repository, asset112Id, asset113Id);
|
||||
|
||||
// Give 100 asset to other account so they can create order
|
||||
AssetUtils.transferAsset(repository, "main", "dummy", AssetUtils.testAssetId, transferAmount);
|
||||
|
||||
// Create matching order
|
||||
AssetUtils.createOrder(repository, "dummy", AssetUtils.testAssetId, Asset.QORA, otherAmount, qoraAmount);
|
||||
// Create further order
|
||||
byte[] furtherOrderId = AssetUtils.createOrder(repository, "alice", asset113Id, asset112Id, asset113Amount2, asset112Price2);
|
||||
|
||||
// Check balances to check expected outcome
|
||||
BigDecimal actualAmount = Common.getTestAccount(repository, "dummy").getConfirmedBalance(AssetUtils.testAssetId);
|
||||
BigDecimal expectedAmount = BigDecimal.valueOf(transferAmount - otherAmount).setScale(8);
|
||||
assertTrue("dummy account's asset balance incorrect", actualAmount.compareTo(expectedAmount) == 0);
|
||||
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();
|
||||
assertTrue(String.format("Order fulfilled incorrect: expected %s, actual %s", expectedFulfilled.toPlainString(), actualFulfilled.toPlainString()),
|
||||
actualFulfilled.compareTo(expectedFulfilled) == 0);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 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);
|
||||
|
||||
assertTrue(String.format("Test account '%s' asset %d balance incorrect: expected %s, actual %s", accountName, assetId, expectedBalance.toPlainString(), actualBalance.toPlainString()),
|
||||
actualBalance.compareTo(expectedBalance) == 0);
|
||||
}
|
||||
|
||||
}
|
33
src/test/java/org/qora/test/common/AccountUtils.java
Normal file
33
src/test/java/org/qora/test/common/AccountUtils.java
Normal file
@@ -0,0 +1,33 @@
|
||||
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;
|
||||
import org.qora.test.Common;
|
||||
|
||||
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;
|
||||
}
|
||||
|
||||
}
|
@@ -4,6 +4,7 @@ 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;
|
||||
@@ -17,30 +18,43 @@ public class AssetUtils {
|
||||
public static final BigDecimal fee = BigDecimal.ONE.setScale(8);
|
||||
public static final long testAssetId = 1L;
|
||||
|
||||
public static void transferAsset(Repository repository, String fromAccountName, String toAccountName, long assetId, long amount) throws DataException {
|
||||
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);
|
||||
|
||||
Common.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 transferAssetTransactionData = new TransferAssetTransactionData(timestamp, AssetUtils.txGroupId, reference, fromAccount.getPublicKey(), toAccount.getAddress(), BigDecimal.valueOf(amount), assetId, AssetUtils.fee);
|
||||
TransactionData transactionData = new TransferAssetTransactionData(timestamp, AssetUtils.txGroupId, reference, fromAccount.getPublicKey(), toAccount.getAddress(), amount, assetId, AssetUtils.fee);
|
||||
|
||||
Common.signAndForge(repository, transferAssetTransactionData, fromAccount);
|
||||
Common.signAndForge(repository, transactionData, fromAccount);
|
||||
}
|
||||
|
||||
public static void createOrder(Repository repository, String accountName, long haveAssetId, long wantAssetId, long haveAmount, long wantAmount) throws DataException {
|
||||
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;
|
||||
BigDecimal amount = BigDecimal.valueOf(haveAmount);
|
||||
BigDecimal price = BigDecimal.valueOf(wantAmount);
|
||||
|
||||
// Note: "price" is not the same in V2 as in V1
|
||||
TransactionData initialOrderTransactionData = new CreateAssetOrderTransactionData(timestamp, txGroupId, reference, account.getPublicKey(), haveAssetId, wantAssetId, amount, price, fee);
|
||||
TransactionData transactionData = new CreateAssetOrderTransactionData(timestamp, txGroupId, reference, account.getPublicKey(), haveAssetId, wantAssetId, amount, wantAmount, fee);
|
||||
|
||||
Common.signAndForge(repository, initialOrderTransactionData, account);
|
||||
Common.signAndForge(repository, transactionData, account);
|
||||
|
||||
return repository.getAssetRepository().getAccountsOrders(account.getPublicKey(), null, null, null, null, true).get(0).getOrderId();
|
||||
}
|
||||
|
||||
}
|
||||
|
@@ -1,10 +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 TestAccount(String privateKey) {
|
||||
super(null, Base58.decode(privateKey));
|
||||
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));
|
||||
}
|
||||
}
|
||||
|
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
|
||||
}
|
||||
}
|
@@ -16,8 +16,10 @@
|
||||
{ "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": "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": 1000, "isDivisible": true, "fee": 0 }
|
||||
{ "type": "ISSUE_ASSET", "owner": "QgV4s3xnzLhVBEJxcYui4u4q11yhUHsd9v", "assetName": "test", "description": "test asset", "data": "", "quantity": 1000000, "isDivisible": true, "fee": 0 }
|
||||
]
|
||||
},
|
||||
"featureTriggers": {
|
||||
@@ -27,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
|
||||
}
|
Reference in New Issue
Block a user