mirror of
https://github.com/Qortal/qortal.git
synced 2025-03-16 12:12:32 +00:00
Redoing account balances with block height.
*** WARNING *** Possible block reward bug in this commit. Further investigation needed. Reverted AccountBalances back to height-less form. Added HistoricAccountBalances table that is populated via trigger on AccountBalances. This saves work when performing common requests for latest/confirmed balances, shunting the extra work to when requesting height-related account balances. Unified API call GET /addresses/balance/{address} by having address/assetId/height as query params. Simpler call for fetching legacy QORA holders during block rewarding. Improved SQL for fetching asset balances, in all conditions, e.g. with/without filtering addresses, with/without filtering assetIds, etc. Unit test for above to make sure query execution is fast enough. (At one point, some SQL query was taking 6 seconds!) Added optional 'height' Integer to AccountBalanceData, but this is not populated/used very often. HSQLDBAccountRepository.save(AccountBalanceData) now checks zero balance saves to see if the row can be deleted instead. This fixes a lot of unhappy tests that complain that there are residual account balance rows left after post-test orphaning back to genesis block. Yet more tests. Removed very old 'TransactionTests' which are mostly covered in more specific tests elsewhere. Added cancel-sell-name test from above. Fixed AssetsApiTests to check for QORT not QORA! Changed hard-coded assetIDs in test.common.AssetUtils in light of new LEGACY_QORA & QORT_FROM_QORA genesis assets. Some test blockchain config changes.
This commit is contained in:
parent
31cbc1f15b
commit
30df320e7f
@ -5,14 +5,11 @@ import java.util.List;
|
||||
|
||||
import org.apache.logging.log4j.LogManager;
|
||||
import org.apache.logging.log4j.Logger;
|
||||
import org.qora.block.Block;
|
||||
import org.qora.block.BlockChain;
|
||||
import org.qora.data.account.AccountBalanceData;
|
||||
import org.qora.data.account.AccountData;
|
||||
import org.qora.data.account.RewardShareData;
|
||||
import org.qora.data.block.BlockData;
|
||||
import org.qora.data.transaction.TransactionData;
|
||||
import org.qora.repository.BlockRepository;
|
||||
import org.qora.repository.DataException;
|
||||
import org.qora.repository.Repository;
|
||||
import org.qora.transaction.Transaction;
|
||||
@ -54,35 +51,14 @@ public class Account {
|
||||
return new AccountData(this.address);
|
||||
}
|
||||
|
||||
// Balance manipulations - assetId is 0 for QORA
|
||||
// Balance manipulations - assetId is 0 for QORT
|
||||
|
||||
public BigDecimal getBalance(long assetId, int confirmations) throws DataException {
|
||||
// Simple case: we only need balance with 1 confirmation
|
||||
if (confirmations == 1)
|
||||
return this.getConfirmedBalance(assetId);
|
||||
public BigDecimal getBalance(long assetId, int height) throws DataException {
|
||||
AccountBalanceData accountBalanceData = this.repository.getAccountRepository().getBalance(this.address, assetId, height);
|
||||
if (accountBalanceData == null)
|
||||
return BigDecimal.ZERO.setScale(8);
|
||||
|
||||
/*
|
||||
* For a balance with more confirmations work back from last block, undoing transactions involving this account, until we have processed required number
|
||||
* of blocks.
|
||||
*/
|
||||
BlockRepository blockRepository = this.repository.getBlockRepository();
|
||||
BigDecimal balance = this.getConfirmedBalance(assetId);
|
||||
BlockData blockData = blockRepository.getLastBlock();
|
||||
|
||||
// Note: "blockData.getHeight() > 1" to make sure we don't examine genesis block
|
||||
for (int i = 1; i < confirmations && blockData != null && blockData.getHeight() > 1; ++i) {
|
||||
Block block = new Block(this.repository, blockData);
|
||||
|
||||
// CIYAM AT transactions should be fetched from repository so no special handling needed here
|
||||
for (Transaction transaction : block.getTransactions())
|
||||
if (transaction.isInvolved(this))
|
||||
balance = balance.subtract(transaction.getAmount(this));
|
||||
|
||||
blockData = block.getParent();
|
||||
}
|
||||
|
||||
// Return balance
|
||||
return balance;
|
||||
return accountBalanceData.getBalance();
|
||||
}
|
||||
|
||||
public BigDecimal getConfirmedBalance(long assetId) throws DataException {
|
||||
|
@ -191,7 +191,8 @@ public class AddressesResource {
|
||||
@GET
|
||||
@Path("/balance/{address}")
|
||||
@Operation(
|
||||
summary = "Returns the confirmed balance of the given address",
|
||||
summary = "Returns account balance",
|
||||
description = "Returns account's balance, optionally of given asset and at given height",
|
||||
responses = {
|
||||
@ApiResponse(
|
||||
description = "the balance",
|
||||
@ -199,14 +200,27 @@ public class AddressesResource {
|
||||
)
|
||||
}
|
||||
)
|
||||
@ApiErrors({ApiError.INVALID_ADDRESS, ApiError.REPOSITORY_ISSUE})
|
||||
public BigDecimal getConfirmedBalance(@PathParam("address") String address) {
|
||||
@ApiErrors({ApiError.INVALID_ADDRESS, ApiError.INVALID_ASSET_ID, ApiError.INVALID_HEIGHT, ApiError.REPOSITORY_ISSUE})
|
||||
public BigDecimal getBalance(@PathParam("address") String address,
|
||||
@QueryParam("assetId") Long assetId,
|
||||
@QueryParam("height") Integer height) {
|
||||
if (!Crypto.isValidAddress(address))
|
||||
throw ApiExceptionFactory.INSTANCE.createException(request, ApiError.INVALID_ADDRESS);
|
||||
|
||||
try (final Repository repository = RepositoryManager.getRepository()) {
|
||||
Account account = new Account(repository, address);
|
||||
return account.getConfirmedBalance(Asset.QORT);
|
||||
|
||||
if (assetId == null)
|
||||
assetId = Asset.QORT;
|
||||
else if (!repository.getAssetRepository().assetExists(assetId))
|
||||
throw ApiExceptionFactory.INSTANCE.createException(request, ApiError.INVALID_ASSET_ID);
|
||||
|
||||
if (height == null)
|
||||
height = repository.getBlockRepository().getBlockchainHeight();
|
||||
else if (height <= 0)
|
||||
throw ApiExceptionFactory.INSTANCE.createException(request, ApiError.INVALID_HEIGHT);
|
||||
|
||||
return account.getBalance(assetId, height);
|
||||
} catch (ApiException e) {
|
||||
throw e;
|
||||
} catch (DataException e) {
|
||||
@ -214,21 +228,6 @@ public class AddressesResource {
|
||||
}
|
||||
}
|
||||
|
||||
@GET
|
||||
@Path("/balance/{address}/{confirmations}")
|
||||
@Operation(
|
||||
summary = "Calculates the balance of the given address for the given confirmations",
|
||||
responses = {
|
||||
@ApiResponse(
|
||||
description = "the balance",
|
||||
content = @Content(schema = @Schema(type = "string", format = "number"))
|
||||
)
|
||||
}
|
||||
)
|
||||
public String getConfirmedBalance(@PathParam("address") String address, @PathParam("confirmations") int confirmations) {
|
||||
throw new UnsupportedOperationException();
|
||||
}
|
||||
|
||||
@GET
|
||||
@Path("/publickey/{address}")
|
||||
@Operation(
|
||||
|
@ -8,7 +8,6 @@ import java.math.BigInteger;
|
||||
import java.math.RoundingMode;
|
||||
import java.util.ArrayList;
|
||||
import java.util.Arrays;
|
||||
import java.util.Collections;
|
||||
import java.util.HashMap;
|
||||
import java.util.Iterator;
|
||||
import java.util.List;
|
||||
@ -40,7 +39,6 @@ import org.qora.data.block.BlockTransactionData;
|
||||
import org.qora.data.network.OnlineAccountData;
|
||||
import org.qora.data.transaction.TransactionData;
|
||||
import org.qora.repository.ATRepository;
|
||||
import org.qora.repository.AccountRepository.BalanceOrdering;
|
||||
import org.qora.repository.DataException;
|
||||
import org.qora.repository.Repository;
|
||||
import org.qora.repository.TransactionRepository;
|
||||
@ -1621,9 +1619,7 @@ public class Block {
|
||||
BigDecimal qoraHoldersAmount = BlockChain.getInstance().getQoraHoldersShare().multiply(totalAmount).setScale(8, RoundingMode.DOWN);
|
||||
LOGGER.trace(() -> String.format("Legacy QORA holders share of %s: %s", totalAmount.toPlainString(), qoraHoldersAmount.toPlainString()));
|
||||
|
||||
List<String> assetAddresses = Collections.emptyList();
|
||||
List<Long> assetIds = Collections.singletonList(Asset.LEGACY_QORA);
|
||||
List<AccountBalanceData> qoraHolders = this.repository.getAccountRepository().getAssetBalances(assetAddresses, assetIds, BalanceOrdering.ASSET_ACCOUNT, true, null, null, null);
|
||||
List<AccountBalanceData> qoraHolders = this.repository.getAccountRepository().getAssetBalances(Asset.LEGACY_QORA, true);
|
||||
|
||||
// Filter out qoraHolders who have received max QORT due to holding legacy QORA, (ratio from blockchain config)
|
||||
BigDecimal qoraPerQortReward = BlockChain.getInstance().getQoraPerQortReward();
|
||||
@ -1708,7 +1704,12 @@ public class Block {
|
||||
}
|
||||
|
||||
qoraHolderAccount.setConfirmedBalance(Asset.QORT, qoraHolderAccount.getConfirmedBalance(Asset.QORT).add(holderReward));
|
||||
qoraHolderAccount.setConfirmedBalance(Asset.QORT_FROM_QORA, newQortFromQoraBalance);
|
||||
|
||||
if (newQortFromQoraBalance.signum() > 0)
|
||||
qoraHolderAccount.setConfirmedBalance(Asset.QORT_FROM_QORA, newQortFromQoraBalance);
|
||||
else
|
||||
// Remove QORT_FROM_QORA balance as it's zero
|
||||
qoraHolderAccount.deleteBalance(Asset.QORT_FROM_QORA);
|
||||
|
||||
sharedAmount = sharedAmount.add(holderReward);
|
||||
}
|
||||
|
@ -13,7 +13,9 @@ public class AccountBalanceData {
|
||||
private String address;
|
||||
private long assetId;
|
||||
private BigDecimal balance;
|
||||
|
||||
// Not always present:
|
||||
private Integer height;
|
||||
private String assetName;
|
||||
|
||||
// Constructors
|
||||
@ -22,15 +24,22 @@ public class AccountBalanceData {
|
||||
protected AccountBalanceData() {
|
||||
}
|
||||
|
||||
public AccountBalanceData(String address, long assetId, BigDecimal balance, String assetName) {
|
||||
public AccountBalanceData(String address, long assetId, BigDecimal balance) {
|
||||
this.address = address;
|
||||
this.assetId = assetId;
|
||||
this.balance = balance;
|
||||
this.assetName = assetName;
|
||||
}
|
||||
|
||||
public AccountBalanceData(String address, long assetId, BigDecimal balance) {
|
||||
this(address, assetId, balance, null);
|
||||
public AccountBalanceData(String address, long assetId, BigDecimal balance, int height) {
|
||||
this(address, assetId, balance);
|
||||
|
||||
this.height = height;
|
||||
}
|
||||
|
||||
public AccountBalanceData(String address, long assetId, BigDecimal balance, String assetName) {
|
||||
this(address, assetId, balance);
|
||||
|
||||
this.assetName = assetName;
|
||||
}
|
||||
|
||||
// Getters/Setters
|
||||
@ -51,6 +60,10 @@ public class AccountBalanceData {
|
||||
this.balance = balance;
|
||||
}
|
||||
|
||||
public Integer getHeight() {
|
||||
return this.height;
|
||||
}
|
||||
|
||||
public String getAssetName() {
|
||||
return this.assetName;
|
||||
}
|
||||
|
@ -89,6 +89,9 @@ public interface AccountRepository {
|
||||
/** Returns account balance data for address & assetId at (or before) passed block height. */
|
||||
public AccountBalanceData getBalance(String address, long assetId, int height) throws DataException;
|
||||
|
||||
/** Returns per-height historic balance for address & assetId. */
|
||||
public List<AccountBalanceData> getHistoricBalances(String address, long assetId) throws DataException;
|
||||
|
||||
public enum BalanceOrdering {
|
||||
ASSET_BALANCE_ACCOUNT,
|
||||
ACCOUNT_ASSET,
|
||||
|
@ -259,7 +259,7 @@ public class HSQLDBAccountRepository implements AccountRepository {
|
||||
|
||||
@Override
|
||||
public AccountBalanceData getBalance(String address, long assetId) throws DataException {
|
||||
String sql = "SELECT balance FROM AccountBalances WHERE account = ? AND asset_id = ? ORDER BY height DESC LIMIT 1";
|
||||
String sql = "SELECT IFNULL(balance, 0) FROM AccountBalances WHERE account = ? AND asset_id = ? LIMIT 1";
|
||||
|
||||
try (ResultSet resultSet = this.repository.checkedExecute(sql, address, assetId)) {
|
||||
if (resultSet == null)
|
||||
@ -275,7 +275,7 @@ public class HSQLDBAccountRepository implements AccountRepository {
|
||||
|
||||
@Override
|
||||
public AccountBalanceData getBalance(String address, long assetId, int height) throws DataException {
|
||||
String sql = "SELECT balance FROM AccountBalances WHERE account = ? AND asset_id = ? AND height <= ? ORDER BY height DESC LIMIT 1";
|
||||
String sql = "SELECT IFNULL(balance, 0) FROM HistoricAccountBalances WHERE account = ? AND asset_id = ? AND height <= ? ORDER BY height DESC LIMIT 1";
|
||||
|
||||
try (ResultSet resultSet = this.repository.checkedExecute(sql, address, assetId, height)) {
|
||||
if (resultSet == null)
|
||||
@ -289,10 +289,32 @@ public class HSQLDBAccountRepository implements AccountRepository {
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public List<AccountBalanceData> getHistoricBalances(String address, long assetId) throws DataException {
|
||||
String sql = "SELECT height, balance FROM HistoricAccountBalances WHERE account = ? AND asset_id = ? ORDER BY height DESC";
|
||||
|
||||
List<AccountBalanceData> historicBalances = new ArrayList<>();
|
||||
try (ResultSet resultSet = this.repository.checkedExecute(sql, address, assetId)) {
|
||||
if (resultSet == null)
|
||||
return historicBalances;
|
||||
|
||||
do {
|
||||
int height = resultSet.getInt(1);
|
||||
BigDecimal balance = resultSet.getBigDecimal(2);
|
||||
|
||||
historicBalances.add(new AccountBalanceData(address, assetId, balance, height));
|
||||
} while (resultSet.next());
|
||||
|
||||
return historicBalances;
|
||||
} catch (SQLException e) {
|
||||
throw new DataException("Unable to fetch historic account balances from repository", e);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public List<AccountBalanceData> getAssetBalances(long assetId, Boolean excludeZero) throws DataException {
|
||||
StringBuilder sql = new StringBuilder(1024);
|
||||
sql.append("SELECT account, IFNULL(balance, 0) FROM NewestAccountBalances WHERE asset_id = ?");
|
||||
sql.append("SELECT account, IFNULL(balance, 0) FROM AccountBalances WHERE asset_id = ?");
|
||||
|
||||
if (excludeZero != null && excludeZero)
|
||||
sql.append(" AND balance != 0");
|
||||
@ -321,76 +343,83 @@ public class HSQLDBAccountRepository implements AccountRepository {
|
||||
StringBuilder sql = new StringBuilder(1024);
|
||||
sql.append("SELECT account, asset_id, IFNULL(balance, 0), asset_name FROM ");
|
||||
|
||||
if (!addresses.isEmpty()) {
|
||||
sql.append("(VALUES ");
|
||||
final boolean haveAddresses = addresses != null && !addresses.isEmpty();
|
||||
final boolean haveAssetIds = assetIds != null && !assetIds.isEmpty();
|
||||
|
||||
final int addressesSize = addresses.size();
|
||||
for (int ai = 0; ai < addressesSize; ++ai) {
|
||||
if (ai != 0)
|
||||
sql.append(", ");
|
||||
// Fill temporary table with filtering addresses/assetIDs
|
||||
if (haveAddresses)
|
||||
HSQLDBRepository.temporaryValuesTableSql(sql, addresses.size(), "TmpAccounts", "account");
|
||||
|
||||
sql.append("(?)");
|
||||
if (haveAssetIds) {
|
||||
if (haveAddresses)
|
||||
sql.append("CROSS JOIN ");
|
||||
|
||||
HSQLDBRepository.temporaryValuesTableSql(sql, assetIds, "TmpAssetIds", "asset_id");
|
||||
}
|
||||
|
||||
if (haveAddresses || haveAssetIds) {
|
||||
// Now use temporary table to filter AccountBalances (using index) and optional zero balance exclusion
|
||||
sql.append("JOIN AccountBalances ON ");
|
||||
|
||||
if (haveAddresses)
|
||||
sql.append("AccountBalances.account = TmpAccounts.account ");
|
||||
|
||||
if (haveAssetIds) {
|
||||
if (haveAddresses)
|
||||
sql.append("AND ");
|
||||
|
||||
sql.append("AccountBalances.asset_id = TmpAssetIds.asset_id ");
|
||||
}
|
||||
|
||||
sql.append(") AS Accounts (account) ");
|
||||
sql.append("CROSS JOIN Assets LEFT OUTER JOIN NewestAccountBalances USING (asset_id, account) ");
|
||||
if (!haveAddresses || (excludeZero != null && excludeZero))
|
||||
sql.append("AND AccountBalances.balance != 0 ");
|
||||
} else {
|
||||
// Simplier, no-address query
|
||||
sql.append("NewestAccountBalances NATURAL JOIN Assets ");
|
||||
// Simpler form if no filtering
|
||||
sql.append("AccountBalances ");
|
||||
|
||||
// Zero balance exclusion comes later
|
||||
}
|
||||
|
||||
if (!assetIds.isEmpty()) {
|
||||
// longs are safe enough to use literally
|
||||
sql.append("WHERE asset_id IN (");
|
||||
// Join for asset name
|
||||
sql.append("JOIN Assets ON Assets.asset_id = AccountBalances.asset_id ");
|
||||
|
||||
final int assetIdsSize = assetIds.size();
|
||||
for (int ai = 0; ai < assetIdsSize; ++ai) {
|
||||
if (ai != 0)
|
||||
sql.append(", ");
|
||||
// Zero balance exclusion if no filtering
|
||||
if (!haveAddresses && !haveAssetIds && excludeZero != null && excludeZero)
|
||||
sql.append("WHERE AccountBalances.balance != 0 ");
|
||||
|
||||
sql.append(assetIds.get(ai));
|
||||
if (balanceOrdering != null) {
|
||||
String[] orderingColumns;
|
||||
switch (balanceOrdering) {
|
||||
case ACCOUNT_ASSET:
|
||||
orderingColumns = new String[] { "account", "asset_id" };
|
||||
break;
|
||||
|
||||
case ASSET_ACCOUNT:
|
||||
orderingColumns = new String[] { "asset_id", "account" };
|
||||
break;
|
||||
|
||||
case ASSET_BALANCE_ACCOUNT:
|
||||
orderingColumns = new String[] { "asset_id", "balance", "account" };
|
||||
break;
|
||||
|
||||
default:
|
||||
throw new DataException(String.format("Unsupported asset balance result ordering: %s", balanceOrdering.name()));
|
||||
}
|
||||
|
||||
sql.append(") ");
|
||||
}
|
||||
sql.append("ORDER BY ");
|
||||
for (int oi = 0; oi < orderingColumns.length; ++oi) {
|
||||
if (oi != 0)
|
||||
sql.append(", ");
|
||||
|
||||
// For no-address queries, or unless specifically requested, only return accounts with non-zero balance
|
||||
if (addresses.isEmpty() || (excludeZero != null && excludeZero)) {
|
||||
sql.append(assetIds.isEmpty() ? " WHERE " : " AND ");
|
||||
sql.append("balance != 0 ");
|
||||
}
|
||||
|
||||
String[] orderingColumns;
|
||||
switch (balanceOrdering) {
|
||||
case ACCOUNT_ASSET:
|
||||
orderingColumns = new String[] { "account", "asset_id" };
|
||||
break;
|
||||
|
||||
case ASSET_ACCOUNT:
|
||||
orderingColumns = new String[] { "asset_id", "account" };
|
||||
break;
|
||||
|
||||
case ASSET_BALANCE_ACCOUNT:
|
||||
orderingColumns = new String[] { "asset_id", "balance", "account" };
|
||||
break;
|
||||
|
||||
default:
|
||||
throw new DataException(String.format("Unsupported asset balance result ordering: %s", balanceOrdering.name()));
|
||||
}
|
||||
|
||||
sql.append("ORDER BY ");
|
||||
for (int oi = 0; oi < orderingColumns.length; ++oi) {
|
||||
if (oi != 0)
|
||||
sql.append(", ");
|
||||
|
||||
sql.append(orderingColumns[oi]);
|
||||
if (reverse != null && reverse)
|
||||
sql.append(" DESC");
|
||||
sql.append(orderingColumns[oi]);
|
||||
if (reverse != null && reverse)
|
||||
sql.append(" DESC");
|
||||
}
|
||||
}
|
||||
|
||||
HSQLDBRepository.limitOffsetSql(sql, limit, offset);
|
||||
|
||||
String[] addressesArray = addresses.toArray(new String[addresses.size()]);
|
||||
String[] addressesArray = addresses == null ? new String[0] : addresses.toArray(new String[addresses.size()]);
|
||||
List<AccountBalanceData> accountBalances = new ArrayList<>();
|
||||
|
||||
try (ResultSet resultSet = this.repository.checkedExecute(sql.toString(), (Object[]) addressesArray)) {
|
||||
@ -414,15 +443,41 @@ public class HSQLDBAccountRepository implements AccountRepository {
|
||||
|
||||
@Override
|
||||
public void save(AccountBalanceData accountBalanceData) throws DataException {
|
||||
// If balance is zero and there are no prior historic balance, then simply delete balances for this assetId (typically during orphaning)
|
||||
if (accountBalanceData.getBalance().signum() == 0) {
|
||||
boolean hasPriorBalances;
|
||||
try {
|
||||
hasPriorBalances = this.repository.exists("HistoricAccountBalances", "account = ? AND asset_id = ? AND height < (SELECT IFNULL(MAX(height), 1) FROM Blocks)",
|
||||
accountBalanceData.getAddress(), accountBalanceData.getAssetId());
|
||||
} catch (SQLException e) {
|
||||
throw new DataException("Unable to check for historic account balances in repository", e);
|
||||
}
|
||||
|
||||
if (!hasPriorBalances) {
|
||||
try {
|
||||
this.repository.delete("AccountBalances", "account = ? AND asset_id = ?", accountBalanceData.getAddress(), accountBalanceData.getAssetId());
|
||||
} catch (SQLException e) {
|
||||
throw new DataException("Unable to delete account balance from repository", e);
|
||||
}
|
||||
|
||||
// I don't think we need to do this as Block.orphan() would do this for us?
|
||||
try {
|
||||
this.repository.delete("HistoricAccountBalances", "account = ? AND asset_id = ?", accountBalanceData.getAddress(), accountBalanceData.getAssetId());
|
||||
} catch (SQLException e) {
|
||||
throw new DataException("Unable to delete historic account balances from repository", e);
|
||||
}
|
||||
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
HSQLDBSaver saveHelper = new HSQLDBSaver("AccountBalances");
|
||||
|
||||
saveHelper.bind("account", accountBalanceData.getAddress()).bind("asset_id", accountBalanceData.getAssetId()).bind("balance",
|
||||
accountBalanceData.getBalance());
|
||||
|
||||
try {
|
||||
// Fill in 'height'
|
||||
int height = this.repository.checkedExecute("SELECT COUNT(*) + 1 FROM Blocks").getInt(1);
|
||||
saveHelper.bind("height", height);
|
||||
// HistoricAccountBalances auto-updated via trigger
|
||||
|
||||
saveHelper.execute(this.repository);
|
||||
} catch (SQLException e) {
|
||||
@ -437,14 +492,20 @@ public class HSQLDBAccountRepository implements AccountRepository {
|
||||
} catch (SQLException e) {
|
||||
throw new DataException("Unable to delete account balance from repository", e);
|
||||
}
|
||||
|
||||
try {
|
||||
this.repository.delete("HistoricAccountBalances", "account = ? AND asset_id = ?", address, assetId);
|
||||
} catch (SQLException e) {
|
||||
throw new DataException("Unable to delete historic account balances from repository", e);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public int deleteBalancesFromHeight(int height) throws DataException {
|
||||
try {
|
||||
return this.repository.delete("AccountBalances", "height >= ?", height);
|
||||
return this.repository.delete("HistoricAccountBalances", "height >= ?", height);
|
||||
} catch (SQLException e) {
|
||||
throw new DataException("Unable to delete old account balances from repository", e);
|
||||
throw new DataException("Unable to delete historic account balances from repository", e);
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -847,21 +847,19 @@ public class HSQLDBDatabaseUpdates {
|
||||
+ "PRIMARY KEY (account), FOREIGN KEY (account) REFERENCES Accounts (account) ON DELETE CASCADE)");
|
||||
break;
|
||||
|
||||
case 60: // Adding height to account balances
|
||||
// We need to drop primary key first
|
||||
stmt.execute("ALTER TABLE AccountBalances DROP PRIMARY KEY");
|
||||
// Add height to account balances
|
||||
stmt.execute("ALTER TABLE AccountBalances ADD COLUMN height INT NOT NULL DEFAULT 0 BEFORE BALANCE");
|
||||
// Add new primary key
|
||||
stmt.execute("ALTER TABLE AccountBalances ADD PRIMARY KEY (asset_id, account, height)");
|
||||
/// Create a view for account balances at greatest height
|
||||
stmt.execute("CREATE VIEW NewestAccountBalances (account, asset_id, balance) AS "
|
||||
+ "SELECT AccountBalances.account, AccountBalances.asset_id, AccountBalances.balance FROM AccountBalances "
|
||||
+ "LEFT OUTER JOIN AccountBalances AS NewerAccountBalances "
|
||||
+ "ON NewerAccountBalances.account = AccountBalances.account "
|
||||
+ "AND NewerAccountBalances.asset_id = AccountBalances.asset_id "
|
||||
+ "AND NewerAccountBalances.height > AccountBalances.height "
|
||||
+ "WHERE NewerAccountBalances.height IS NULL");
|
||||
case 60:
|
||||
// Index for speeding up fetch legacy QORA holders for Block processing
|
||||
stmt.execute("CREATE INDEX AccountBalances_Asset_Balance_Index ON AccountBalances (asset_id, balance)");
|
||||
// Tracking height-history to account balances
|
||||
stmt.execute("CREATE TABLE HistoricAccountBalances (account QoraAddress, asset_id AssetID, height INT DEFAULT 1, balance QoraAmount NOT NULL, "
|
||||
+ "PRIMARY KEY (account, asset_id, height), FOREIGN KEY (account) REFERENCES Accounts (account) ON DELETE CASCADE)");
|
||||
// Create triggers on changes to AccountBalances rows to update historic
|
||||
stmt.execute("CREATE TRIGGER Historic_account_balance_insert_trigger AFTER INSERT ON AccountBalances REFERENCING NEW ROW AS new_row FOR EACH ROW "
|
||||
+ "INSERT INTO HistoricAccountBalances VALUES (new_row.account, new_row.asset_id, (SELECT IFNULL(MAX(height), 0) + 1 FROM Blocks), new_row.balance) "
|
||||
+ "ON DUPLICATE KEY UPDATE balance = new_row.balance");
|
||||
stmt.execute("CREATE TRIGGER Historic_account_balance_update_trigger AFTER UPDATE ON AccountBalances REFERENCING NEW ROW AS new_row FOR EACH ROW "
|
||||
+ "INSERT INTO HistoricAccountBalances VALUES (new_row.account, new_row.asset_id, (SELECT IFNULL(MAX(height), 0) + 1 FROM Blocks), new_row.balance) "
|
||||
+ "ON DUPLICATE KEY UPDATE balance = new_row.balance");
|
||||
break;
|
||||
|
||||
default:
|
||||
|
@ -577,6 +577,52 @@ public class HSQLDBRepository implements Repository {
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Appends SQL for filling a temporary VALUES table, values NOT supplied.
|
||||
* <p>
|
||||
* (Convenience method for HSQLDB repository subclasses).
|
||||
*/
|
||||
/* package */ static void temporaryValuesTableSql(StringBuilder stringBuilder, int valuesCount, String tableName, String columnName) {
|
||||
stringBuilder.append("(VALUES ");
|
||||
|
||||
for (int i = 0; i < valuesCount; ++i) {
|
||||
if (i != 0)
|
||||
stringBuilder.append(", ");
|
||||
|
||||
stringBuilder.append("(?)");
|
||||
}
|
||||
|
||||
stringBuilder.append(") AS ");
|
||||
stringBuilder.append(tableName);
|
||||
stringBuilder.append(" (");
|
||||
stringBuilder.append(columnName);
|
||||
stringBuilder.append(") ");
|
||||
}
|
||||
|
||||
/**
|
||||
* Appends SQL for filling a temporary VALUES table, literal values ARE supplied.
|
||||
* <p>
|
||||
* (Convenience method for HSQLDB repository subclasses).
|
||||
*/
|
||||
/* package */ static void temporaryValuesTableSql(StringBuilder stringBuilder, List<? extends Object> values, String tableName, String columnName) {
|
||||
stringBuilder.append("(VALUES ");
|
||||
|
||||
for (int i = 0; i < values.size(); ++i) {
|
||||
if (i != 0)
|
||||
stringBuilder.append(", ");
|
||||
|
||||
stringBuilder.append("(");
|
||||
stringBuilder.append(values.get(i));
|
||||
stringBuilder.append(")");
|
||||
}
|
||||
|
||||
stringBuilder.append(") AS ");
|
||||
stringBuilder.append(tableName);
|
||||
stringBuilder.append(" (");
|
||||
stringBuilder.append(columnName);
|
||||
stringBuilder.append(") ");
|
||||
}
|
||||
|
||||
/** Logs other HSQLDB sessions then re-throws passed exception */
|
||||
public SQLException examineException(SQLException e) throws SQLException {
|
||||
LOGGER.error(String.format("HSQLDB error (session %d): %s", this.sessionId, e.getMessage()), e);
|
||||
|
@ -1,10 +1,14 @@
|
||||
package org.qora.test;
|
||||
|
||||
import static org.junit.Assert.assertNull;
|
||||
import static org.junit.Assert.assertTrue;
|
||||
import static org.junit.Assert.*;
|
||||
|
||||
import java.math.BigDecimal;
|
||||
import java.sql.SQLException;
|
||||
import java.util.ArrayList;
|
||||
import java.util.Arrays;
|
||||
import java.util.List;
|
||||
import java.util.Random;
|
||||
import java.util.stream.Collectors;
|
||||
|
||||
import org.junit.After;
|
||||
import org.junit.Before;
|
||||
@ -15,9 +19,11 @@ import org.qora.account.PublicKeyAccount;
|
||||
import org.qora.asset.Asset;
|
||||
import org.qora.block.BlockChain;
|
||||
import org.qora.data.account.AccountBalanceData;
|
||||
import org.qora.data.account.AccountData;
|
||||
import org.qora.data.transaction.BaseTransactionData;
|
||||
import org.qora.data.transaction.PaymentTransactionData;
|
||||
import org.qora.data.transaction.TransactionData;
|
||||
import org.qora.repository.AccountRepository.BalanceOrdering;
|
||||
import org.qora.repository.DataException;
|
||||
import org.qora.repository.Repository;
|
||||
import org.qora.repository.RepositoryManager;
|
||||
@ -78,7 +84,7 @@ public class AccountBalanceTests extends Common {
|
||||
BigDecimal orphanedBalance = alice.getConfirmedBalance(Asset.QORT);
|
||||
|
||||
// Confirm post-orphan balance is same as initial
|
||||
assertTrue("Post-orphan balance should match initial", orphanedBalance.equals(initialBalance));
|
||||
assertEqualBigDecimals("Post-orphan balance should match initial", initialBalance, orphanedBalance);
|
||||
}
|
||||
}
|
||||
|
||||
@ -95,7 +101,7 @@ public class AccountBalanceTests extends Common {
|
||||
BigDecimal genesisBalance = accountBalanceData.getBalance();
|
||||
|
||||
// Confirm genesis balance is same as initial
|
||||
assertTrue("Genesis balance should match initial", genesisBalance.equals(initialBalance));
|
||||
assertEqualBigDecimals("Genesis balance should match initial", initialBalance, genesisBalance);
|
||||
}
|
||||
}
|
||||
|
||||
@ -116,7 +122,7 @@ public class AccountBalanceTests extends Common {
|
||||
|
||||
// Confirm recipient balance is zero
|
||||
BigDecimal balance = recipientAccount.getConfirmedBalance(Asset.QORT);
|
||||
assertTrue("recipient's balance should be zero", balance.signum() == 0);
|
||||
assertEqualBigDecimals("recipient's balance should be zero", BigDecimal.ZERO, balance);
|
||||
|
||||
// Send 1 QORT to recipient
|
||||
TestAccount sendingAccount = Common.getTestAccount(repository, "alice");
|
||||
@ -129,24 +135,28 @@ public class AccountBalanceTests extends Common {
|
||||
// Send more QORT to recipient
|
||||
BigDecimal amount = BigDecimal.valueOf(random.nextInt(123456));
|
||||
pay(repository, sendingAccount, recipientAccount, amount);
|
||||
BigDecimal totalAmount = BigDecimal.ONE.add(amount);
|
||||
|
||||
// Mint some more blocks
|
||||
for (int i = 0; i < 10; ++i)
|
||||
BlockUtils.mintBlock(repository);
|
||||
|
||||
// Confirm recipient balance is as expected
|
||||
BigDecimal totalAmount = amount.add(BigDecimal.ONE);
|
||||
balance = recipientAccount.getConfirmedBalance(Asset.QORT);
|
||||
assertTrue("recipient's balance incorrect", balance.compareTo(totalAmount) == 0);
|
||||
assertEqualBigDecimals("recipient's balance incorrect", totalAmount, balance);
|
||||
|
||||
List<AccountBalanceData> historicBalances = repository.getAccountRepository().getHistoricBalances(recipientAccount.getAddress(), Asset.QORT);
|
||||
for (AccountBalanceData historicBalance : historicBalances)
|
||||
System.out.println(String.format("Block %d: %s", historicBalance.getHeight(), historicBalance.getBalance().toPlainString()));
|
||||
|
||||
// Confirm balance as of 2 blocks ago
|
||||
int height = repository.getBlockRepository().getBlockchainHeight();
|
||||
balance = repository.getAccountRepository().getBalance(recipientAccount.getAddress(), Asset.QORT, height - 2).getBalance();
|
||||
assertTrue("recipient's historic balance incorrect", balance.compareTo(totalAmount) == 0);
|
||||
assertEqualBigDecimals("recipient's historic balance incorrect", totalAmount, balance);
|
||||
|
||||
// Confirm balance prior to last payment
|
||||
balance = repository.getAccountRepository().getBalance(recipientAccount.getAddress(), Asset.QORT, height - 15).getBalance();
|
||||
assertTrue("recipient's historic balance incorrect", balance.compareTo(BigDecimal.ONE) == 0);
|
||||
assertEqualBigDecimals("recipient's historic balance incorrect", BigDecimal.ONE, balance);
|
||||
|
||||
// Orphan blocks to before last payment
|
||||
BlockUtils.orphanBlocks(repository, 10 + 5);
|
||||
@ -154,7 +164,7 @@ public class AccountBalanceTests extends Common {
|
||||
// Re-check balance from (now) invalid height
|
||||
AccountBalanceData accountBalanceData = repository.getAccountRepository().getBalance(recipientAccount.getAddress(), Asset.QORT, height - 2);
|
||||
balance = accountBalanceData.getBalance();
|
||||
assertTrue("recipient's invalid-height balance should be one", balance.compareTo(BigDecimal.ONE) == 0);
|
||||
assertEqualBigDecimals("recipient's invalid-height balance should be one", BigDecimal.ONE, balance);
|
||||
|
||||
// Orphan blocks to before initial 1 QORT payment
|
||||
BlockUtils.orphanBlocks(repository, 10 + 5);
|
||||
@ -177,4 +187,94 @@ public class AccountBalanceTests extends Common {
|
||||
TransactionUtils.signAndMint(repository, transactionData, sendingAccount);
|
||||
}
|
||||
|
||||
/** Tests SQL query speed for account balance fetches. */
|
||||
@Test
|
||||
public void testRepositorySpeed() throws DataException, SQLException {
|
||||
Random random = new Random();
|
||||
final long MAX_QUERY_TIME = 100L; // ms
|
||||
|
||||
try (final Repository repository = RepositoryManager.getRepository()) {
|
||||
System.out.println("Creating random accounts...");
|
||||
|
||||
// Generate some random accounts
|
||||
List<Account> accounts = new ArrayList<>();
|
||||
for (int ai = 0; ai < 20; ++ai) {
|
||||
byte[] publicKey = new byte[32];
|
||||
random.nextBytes(publicKey);
|
||||
|
||||
PublicKeyAccount account = new PublicKeyAccount(repository, publicKey);
|
||||
accounts.add(account);
|
||||
|
||||
AccountData accountData = new AccountData(account.getAddress());
|
||||
repository.getAccountRepository().ensureAccount(accountData);
|
||||
}
|
||||
repository.saveChanges();
|
||||
|
||||
System.out.println("Creating random balances...");
|
||||
|
||||
// Fill with lots of random balances
|
||||
for (int i = 0; i < 100000; ++i) {
|
||||
Account account = accounts.get(random.nextInt(accounts.size()));
|
||||
int assetId = random.nextInt(2);
|
||||
BigDecimal balance = BigDecimal.valueOf(random.nextInt(100000));
|
||||
|
||||
AccountBalanceData accountBalanceData = new AccountBalanceData(account.getAddress(), assetId, balance);
|
||||
repository.getAccountRepository().save(accountBalanceData);
|
||||
|
||||
// Maybe mint a block to change height
|
||||
if (i > 0 && (i % 1000) == 0)
|
||||
BlockUtils.mintBlock(repository);
|
||||
}
|
||||
repository.saveChanges();
|
||||
|
||||
// Address filtering test cases
|
||||
List<String> testAddresses = accounts.stream().limit(3).map(account -> account.getAddress()).collect(Collectors.toList());
|
||||
List<List<String>> addressFilteringCases = Arrays.asList(null, testAddresses);
|
||||
|
||||
// AssetID filtering test cases
|
||||
List<List<Long>> assetIdFilteringCases = Arrays.asList(null, Arrays.asList(0L, 1L, 2L));
|
||||
|
||||
// Results ordering test cases
|
||||
List<BalanceOrdering> orderingCases = new ArrayList<>();
|
||||
orderingCases.add(null);
|
||||
orderingCases.addAll(Arrays.asList(BalanceOrdering.values()));
|
||||
|
||||
// Zero exclusion test cases
|
||||
List<Boolean> zeroExclusionCases = Arrays.asList(null, true, false);
|
||||
|
||||
// Limit test cases
|
||||
List<Integer> limitCases = Arrays.asList(null, 10);
|
||||
|
||||
// Offset test cases
|
||||
List<Integer> offsetCases = Arrays.asList(null, 10);
|
||||
|
||||
// Reverse results cases
|
||||
List<Boolean> reverseCases = Arrays.asList(null, true, false);
|
||||
|
||||
repository.setDebug(true);
|
||||
|
||||
// Test all cases
|
||||
for (List<String> addresses : addressFilteringCases)
|
||||
for (List<Long> assetIds : assetIdFilteringCases)
|
||||
for (BalanceOrdering balanceOrdering : orderingCases)
|
||||
for (Boolean excludeZero : zeroExclusionCases)
|
||||
for (Integer limit : limitCases)
|
||||
for (Integer offset : offsetCases)
|
||||
for (Boolean reverse : reverseCases) {
|
||||
repository.discardChanges();
|
||||
|
||||
System.out.println(String.format("Testing query: %s addresses, %s assetIDs, %s ordering, %b zero-exclusion, %d limit, %d offset, %b reverse",
|
||||
(addresses == null ? "no" : "with"), (assetIds == null ? "no" : "with"), balanceOrdering, excludeZero, limit, offset, reverse));
|
||||
|
||||
long before = System.currentTimeMillis();
|
||||
repository.getAccountRepository().getAssetBalances(addresses, assetIds, balanceOrdering, excludeZero, limit, offset, reverse);
|
||||
final long period = System.currentTimeMillis() - before;
|
||||
assertTrue(String.format("Query too slow: %dms", period), period < MAX_QUERY_TIME);
|
||||
}
|
||||
}
|
||||
|
||||
// Rebuild repository to avoid orphan check
|
||||
Common.useDefaultSettings();
|
||||
}
|
||||
|
||||
}
|
File diff suppressed because it is too large
Load Diff
@ -86,7 +86,7 @@ public class AssetsApiTests extends ApiCommon {
|
||||
@Test
|
||||
public void testGetAssetInfo() {
|
||||
assertNotNull(this.assetsResource.getAssetInfo((int) 0L, null));
|
||||
assertNotNull(this.assetsResource.getAssetInfo(null, "QORA"));
|
||||
assertNotNull(this.assetsResource.getAssetInfo(null, "QORT"));
|
||||
}
|
||||
|
||||
@Test
|
||||
|
@ -28,9 +28,10 @@ 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; // Owned by Alice
|
||||
public static final long otherAssetId = 2L; // Owned by Bob
|
||||
public static final long goldAssetId = 3L; // Owned by Alice
|
||||
// QORT: 0, LEGACY_QORA: 1, QORT_FROM_QORA: 2
|
||||
public static final long testAssetId = 3L; // Owned by Alice
|
||||
public static final long otherAssetId = 4L; // Owned by Bob
|
||||
public static final long goldAssetId = 5L; // Owned by Alice
|
||||
|
||||
public static long issueAsset(Repository repository, String issuerAccountName, String assetName, long quantity, boolean isDivisible) throws DataException {
|
||||
PrivateKeyAccount account = Common.getTestAccount(repository, issuerAccountName);
|
||||
|
@ -146,13 +146,13 @@ public class Common {
|
||||
}
|
||||
|
||||
List<AssetData> remainingAssets = repository.getAssetRepository().getAllAssets();
|
||||
checkOrphanedLists("asset", initialAssets, remainingAssets, AssetData::getAssetId);
|
||||
checkOrphanedLists("asset", initialAssets, remainingAssets, AssetData::getAssetId, AssetData::getAssetId);
|
||||
|
||||
List<GroupData> remainingGroups = repository.getGroupRepository().getAllGroups();
|
||||
checkOrphanedLists("group", initialGroups, remainingGroups, GroupData::getGroupId);
|
||||
checkOrphanedLists("group", initialGroups, remainingGroups, GroupData::getGroupId, GroupData::getGroupId);
|
||||
|
||||
List<AccountBalanceData> remainingBalances = repository.getAccountRepository().getAssetBalances(Collections.emptyList(), Collections.emptyList(), BalanceOrdering.ASSET_ACCOUNT, false, null, null, null);
|
||||
checkOrphanedLists("account balance", initialBalances, remainingBalances, entry -> entry.getAssetName() + "-" + entry.getAddress());
|
||||
checkOrphanedLists("account balance", initialBalances, remainingBalances, entry -> entry.getAddress() + " [" + entry.getAssetName() + "]", entry -> entry.getBalance().toPlainString());
|
||||
|
||||
assertEquals("remainingBalances is different size", initialBalances.size(), remainingBalances.size());
|
||||
// Actually compare balances
|
||||
@ -168,7 +168,7 @@ public class Common {
|
||||
}
|
||||
}
|
||||
|
||||
private static <T> void checkOrphanedLists(String typeName, List<T> initial, List<T> remaining, Function<T, ? extends Object> keyExtractor) {
|
||||
private static <T> void checkOrphanedLists(String typeName, List<T> initial, List<T> remaining, Function<T, ? extends Object> keyExtractor, Function<T, ? extends Object> valueExtractor) {
|
||||
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)));
|
||||
|
||||
@ -181,7 +181,7 @@ public class Common {
|
||||
remainingClone.removeIf(isInitial);
|
||||
|
||||
for (T remainingEntry : remainingClone)
|
||||
LOGGER.info(String.format("Non-genesis remaining entry: %s", keyExtractor.apply(remainingEntry)));
|
||||
LOGGER.info(String.format("Non-genesis remaining entry: %s = %s", keyExtractor.apply(remainingEntry), valueExtractor.apply(remainingEntry)));
|
||||
|
||||
assertTrue(String.format("Non-genesis %s remains", typeName), remainingClone.isEmpty());
|
||||
}
|
||||
|
@ -25,7 +25,7 @@ public class TransactionUtils {
|
||||
// Add to unconfirmed
|
||||
assertTrue("Transaction's signature should be valid", transaction.isSignatureValid());
|
||||
|
||||
// We might need to wait until transaction's timestamp is valid for the block we're about to generate
|
||||
// We might need to wait until transaction's timestamp is valid for the block we're about to mint
|
||||
try {
|
||||
Thread.sleep(1L);
|
||||
} catch (InterruptedException e) {
|
||||
|
@ -11,6 +11,7 @@ import org.junit.Test;
|
||||
import org.qora.account.PrivateKeyAccount;
|
||||
import org.qora.data.naming.NameData;
|
||||
import org.qora.data.transaction.BuyNameTransactionData;
|
||||
import org.qora.data.transaction.CancelSellNameTransactionData;
|
||||
import org.qora.data.transaction.RegisterNameTransactionData;
|
||||
import org.qora.data.transaction.SellNameTransactionData;
|
||||
import org.qora.repository.DataException;
|
||||
@ -21,7 +22,7 @@ import org.qora.test.common.Common;
|
||||
import org.qora.test.common.TransactionUtils;
|
||||
import org.qora.test.common.transaction.TestTransaction;
|
||||
|
||||
public class OrphaningTests extends Common {
|
||||
public class BuySellTests extends Common {
|
||||
|
||||
protected static final Random random = new Random();
|
||||
|
||||
@ -136,6 +137,31 @@ public class OrphaningTests extends Common {
|
||||
assertEqualBigDecimals("price incorrect", price, nameData.getSalePrice());
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testCancelSellName() throws DataException {
|
||||
// Register-name and sell-name
|
||||
testSellName();
|
||||
|
||||
// Cancel Sell-name
|
||||
CancelSellNameTransactionData transactionData = new CancelSellNameTransactionData(TestTransaction.generateBase(alice), name);
|
||||
TransactionUtils.signAndMint(repository, transactionData, alice);
|
||||
|
||||
NameData nameData;
|
||||
|
||||
// Check name is no longer for sale
|
||||
nameData = repository.getNameRepository().fromName(name);
|
||||
assertFalse(nameData.getIsForSale());
|
||||
// Not concerned about price
|
||||
|
||||
// Orphan cancel sell-name
|
||||
BlockUtils.orphanLastBlock(repository);
|
||||
|
||||
// Check name is for sale
|
||||
nameData = repository.getNameRepository().fromName(name);
|
||||
assertTrue(nameData.getIsForSale());
|
||||
assertEqualBigDecimals("price incorrect", price, nameData.getSalePrice());
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testBuyName() throws DataException {
|
||||
// Register-name and sell-name
|
@ -8,6 +8,7 @@
|
||||
"requireGroupForApproval": false,
|
||||
"minAccountLevelToRewardShare": 5,
|
||||
"maxRewardSharesPerMintingAccount": 20,
|
||||
"founderEffectiveMintingLevel": 10,
|
||||
"onlineAccountSignaturesMinLifetime": 3600000,
|
||||
"onlineAccountSignaturesMaxLifetime": 86400000,
|
||||
"rewardsByHeight": [
|
||||
@ -23,6 +24,7 @@
|
||||
{ "levels": [ 9, 10 ], "share": 0.25 }
|
||||
],
|
||||
"qoraHoldersShare": 0.20,
|
||||
"qoraPerQortReward": 250,
|
||||
"blocksNeededByLevel": [ 10, 20, 30, 40, 50, 60, 70, 80, 90, 100 ],
|
||||
"blockTimingsByHeight": [
|
||||
{ "height": 1, "target": 60000, "deviation": 30000, "power": 0.2 }
|
||||
@ -42,14 +44,19 @@
|
||||
"version": 4,
|
||||
"timestamp": 0,
|
||||
"transactions": [
|
||||
{ "type": "ISSUE_ASSET", "owner": "QcFmNxSArv5tWEzCtTKb2Lqc5QkKuQ7RNs", "assetName": "QORA", "description": "QORA native coin", "data": "", "quantity": 10000000000, "isDivisible": true, "fee": 0, "reference": "3Verk6ZKBJc3WTTVfxFC9icSjKdM8b92eeJEpJP8qNizG4ZszNFq8wdDYdSjJXq2iogDFR1njyhsBdVpbvDfjzU7" },
|
||||
{ "type": "ISSUE_ASSET", "owner": "QUwGVHPPxJNJ2dq95abQNe79EyBN2K26zM", "assetName": "QORT", "description": "QORT native coin", "data": "", "quantity": 0, "isDivisible": true, "fee": 0, "reference": "3Verk6ZKBJc3WTTVfxFC9icSjKdM8b92eeJEpJP8qNizG4ZszNFq8wdDYdSjJXq2iogDFR1njyhsBdVpbvDfjzU7" },
|
||||
{ "type": "ISSUE_ASSET", "owner": "QUwGVHPPxJNJ2dq95abQNe79EyBN2K26zM", "assetName": "Legacy-QORA", "description": "Representative legacy QORA", "quantity": 0, "isDivisible": true, "data": "{}", "isUnspendable": true },
|
||||
{ "type": "ISSUE_ASSET", "owner": "QUwGVHPPxJNJ2dq95abQNe79EyBN2K26zM", "assetName": "QORT-from-QORA", "description": "QORT gained from holding legacy QORA", "quantity": 0, "isDivisible": true, "data": "{}", "isUnspendable": true },
|
||||
|
||||
{ "type": "GENESIS", "recipient": "QgV4s3xnzLhVBEJxcYui4u4q11yhUHsd9v", "amount": "1000000000", "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 },
|
||||
{ "type": "ISSUE_ASSET", "owner": "QixPbJUwsaHsVEofJdozU9zgVqkK6aYhrK", "assetName": "OTHER", "description": "other test asset", "data": "", "quantity": 1000000, "isDivisible": true, "fee": 0 },
|
||||
{ "type": "ISSUE_ASSET", "owner": "QgV4s3xnzLhVBEJxcYui4u4q11yhUHsd9v", "assetName": "GOLD", "description": "gold test asset", "data": "", "quantity": 1000000, "isDivisible": true, "fee": 0 },
|
||||
|
||||
{ "type": "ACCOUNT_FLAGS", "target": "QgV4s3xnzLhVBEJxcYui4u4q11yhUHsd9v", "andMask": -1, "orMask": 1, "xorMask": 0 },
|
||||
{ "type": "REWARD_SHARE", "minterPublicKey": "2tiMr5LTpaWCgbRvkPK8TFd7k63DyHJMMFFsz9uBf1ZP", "recipient": "QgV4s3xnzLhVBEJxcYui4u4q11yhUHsd9v", "rewardSharePublicKey": "7PpfnvLSG7y4HPh8hE7KoqAjLCkv7Ui6xw4mKAkbZtox", "sharePercent": 100 }
|
||||
]
|
||||
|
@ -44,7 +44,7 @@
|
||||
"version": 4,
|
||||
"timestamp": 0,
|
||||
"transactions": [
|
||||
{ "type": "ISSUE_ASSET", "owner": "QcFmNxSArv5tWEzCtTKb2Lqc5QkKuQ7RNs", "assetName": "QORT", "description": "QORT native coin", "data": "", "quantity": 0, "isDivisible": true, "fee": 0, "reference": "3Verk6ZKBJc3WTTVfxFC9icSjKdM8b92eeJEpJP8qNizG4ZszNFq8wdDYdSjJXq2iogDFR1njyhsBdVpbvDfjzU7" },
|
||||
{ "type": "ISSUE_ASSET", "owner": "QUwGVHPPxJNJ2dq95abQNe79EyBN2K26zM", "assetName": "QORT", "description": "QORT native coin", "data": "", "quantity": 0, "isDivisible": true, "fee": 0, "reference": "3Verk6ZKBJc3WTTVfxFC9icSjKdM8b92eeJEpJP8qNizG4ZszNFq8wdDYdSjJXq2iogDFR1njyhsBdVpbvDfjzU7" },
|
||||
{ "type": "ISSUE_ASSET", "owner": "QUwGVHPPxJNJ2dq95abQNe79EyBN2K26zM", "assetName": "Legacy-QORA", "description": "Representative legacy QORA", "quantity": 0, "isDivisible": true, "data": "{}", "isUnspendable": true },
|
||||
{ "type": "ISSUE_ASSET", "owner": "QUwGVHPPxJNJ2dq95abQNe79EyBN2K26zM", "assetName": "QORT-from-QORA", "description": "QORT gained from holding legacy QORA", "quantity": 0, "isDivisible": true, "data": "{}", "isUnspendable": true },
|
||||
|
||||
|
Loading…
x
Reference in New Issue
Block a user