diff --git a/src/main/java/org/qortal/repository/hsqldb/HSQLDBATRepository.java b/src/main/java/org/qortal/repository/hsqldb/HSQLDBATRepository.java index feae3b5c..f6de4fb4 100644 --- a/src/main/java/org/qortal/repository/hsqldb/HSQLDBATRepository.java +++ b/src/main/java/org/qortal/repository/hsqldb/HSQLDBATRepository.java @@ -1,8 +1,5 @@ package org.qortal.repository.hsqldb; -import static org.qortal.repository.hsqldb.HSQLDBRepository.getZonedTimestampMilli; -import static org.qortal.repository.hsqldb.HSQLDBRepository.toOffsetDateTime; - import java.sql.ResultSet; import java.sql.SQLException; import java.util.ArrayList; @@ -25,7 +22,7 @@ public class HSQLDBATRepository implements ATRepository { @Override public ATData fromATAddress(String atAddress) throws DataException { - String sql = "SELECT creator, creation, version, asset_id, code_bytes, code_hash, " + String sql = "SELECT creator, created_when, version, asset_id, code_bytes, code_hash, " + "is_sleeping, sleep_until_height, is_finished, had_fatal_error, " + "is_frozen, frozen_balance " + "FROM ATs " @@ -36,7 +33,7 @@ public class HSQLDBATRepository implements ATRepository { return null; byte[] creatorPublicKey = resultSet.getBytes(1); - long creation = getZonedTimestampMilli(resultSet, 2); + long created = resultSet.getLong(2); int version = resultSet.getInt(3); long assetId = resultSet.getLong(4); byte[] codeBytes = resultSet.getBytes(5); // Actually BLOB @@ -55,7 +52,7 @@ public class HSQLDBATRepository implements ATRepository { if (frozenBalance == 0 && resultSet.wasNull()) frozenBalance = null; - return new ATData(atAddress, creatorPublicKey, creation, version, assetId, codeBytes, codeHash, + return new ATData(atAddress, creatorPublicKey, created, version, assetId, codeBytes, codeHash, isSleeping, sleepUntilHeight, isFinished, hadFatalError, isFrozen, frozenBalance); } catch (SQLException e) { throw new DataException("Unable to fetch AT from repository", e); @@ -73,12 +70,12 @@ public class HSQLDBATRepository implements ATRepository { @Override public List getAllExecutableATs() throws DataException { - String sql = "SELECT AT_address, creator, creation, version, asset_id, code_bytes, code_hash, " + String sql = "SELECT AT_address, creator, created_when, version, asset_id, code_bytes, code_hash, " + "is_sleeping, sleep_until_height, had_fatal_error, " + "is_frozen, frozen_balance " + "FROM ATs " + "WHERE is_finished = false " - + "ORDER BY creation ASC"; + + "ORDER BY created_when ASC"; List executableATs = new ArrayList<>(); @@ -91,7 +88,7 @@ public class HSQLDBATRepository implements ATRepository { do { String atAddress = resultSet.getString(1); byte[] creatorPublicKey = resultSet.getBytes(2); - long creation = getZonedTimestampMilli(resultSet, 3); + long created = resultSet.getLong(3); int version = resultSet.getInt(4); long assetId = resultSet.getLong(5); byte[] codeBytes = resultSet.getBytes(6); // Actually BLOB @@ -109,7 +106,7 @@ public class HSQLDBATRepository implements ATRepository { if (frozenBalance == 0 && resultSet.wasNull()) frozenBalance = null; - ATData atData = new ATData(atAddress, creatorPublicKey, creation, version, assetId, codeBytes, codeHash, + ATData atData = new ATData(atAddress, creatorPublicKey, created, version, assetId, codeBytes, codeHash, isSleeping, sleepUntilHeight, isFinished, hadFatalError, isFrozen, frozenBalance); executableATs.add(atData); @@ -124,7 +121,7 @@ public class HSQLDBATRepository implements ATRepository { @Override public List getATsByFunctionality(byte[] codeHash, Boolean isExecutable, Integer limit, Integer offset, Boolean reverse) throws DataException { StringBuilder sql = new StringBuilder(512); - sql.append("SELECT AT_address, creator, creation, version, asset_id, code_bytes, ") + sql.append("SELECT AT_address, creator, created_when, version, asset_id, code_bytes, ") .append("is_sleeping, sleep_until_height, is_finished, had_fatal_error, ") .append("is_frozen, frozen_balance ") .append("FROM ATs ") @@ -133,7 +130,7 @@ public class HSQLDBATRepository implements ATRepository { if (isExecutable != null) sql.append("AND is_finished = ").append(isExecutable ? "false" : "true"); - sql.append(" ORDER BY creation "); + sql.append(" ORDER BY created_when "); if (reverse != null && reverse) sql.append("DESC"); @@ -148,7 +145,7 @@ public class HSQLDBATRepository implements ATRepository { do { String atAddress = resultSet.getString(1); byte[] creatorPublicKey = resultSet.getBytes(2); - long creation = getZonedTimestampMilli(resultSet, 3); + long created = resultSet.getLong(3); int version = resultSet.getInt(4); long assetId = resultSet.getLong(5); byte[] codeBytes = resultSet.getBytes(6); // Actually BLOB @@ -167,7 +164,7 @@ public class HSQLDBATRepository implements ATRepository { if (frozenBalance == 0 && resultSet.wasNull()) frozenBalance = null; - ATData atData = new ATData(atAddress, creatorPublicKey, creation, version, assetId, codeBytes, codeHash, + ATData atData = new ATData(atAddress, creatorPublicKey, created, version, assetId, codeBytes, codeHash, isSleeping, sleepUntilHeight, isFinished, hadFatalError, isFrozen, frozenBalance); matchingATs.add(atData); @@ -202,7 +199,7 @@ public class HSQLDBATRepository implements ATRepository { public void save(ATData atData) throws DataException { HSQLDBSaver saveHelper = new HSQLDBSaver("ATs"); - saveHelper.bind("AT_address", atData.getATAddress()).bind("creator", atData.getCreatorPublicKey()).bind("creation", toOffsetDateTime(atData.getCreation())) + saveHelper.bind("AT_address", atData.getATAddress()).bind("creator", atData.getCreatorPublicKey()).bind("created_when", atData.getCreation()) .bind("version", atData.getVersion()).bind("asset_id", atData.getAssetId()) .bind("code_bytes", atData.getCodeBytes()).bind("code_hash", atData.getCodeHash()) .bind("is_sleeping", atData.getIsSleeping()).bind("sleep_until_height", atData.getSleepUntilHeight()) @@ -230,7 +227,7 @@ public class HSQLDBATRepository implements ATRepository { @Override public ATStateData getATStateAtHeight(String atAddress, int height) throws DataException { - String sql = "SELECT creation, state_data, state_hash, fees, is_initial " + String sql = "SELECT created_when, state_data, state_hash, fees, is_initial " + "FROM ATStates " + "WHERE AT_address = ? AND height = ? " + "LIMIT 1"; @@ -239,13 +236,13 @@ public class HSQLDBATRepository implements ATRepository { if (resultSet == null) return null; - long creation = getZonedTimestampMilli(resultSet, 1); + long created = resultSet.getLong(1); byte[] stateData = resultSet.getBytes(2); // Actually BLOB byte[] stateHash = resultSet.getBytes(3); long fees = resultSet.getLong(4); boolean isInitial = resultSet.getBoolean(5); - return new ATStateData(atAddress, height, creation, stateData, stateHash, fees, isInitial); + return new ATStateData(atAddress, height, created, stateData, stateHash, fees, isInitial); } catch (SQLException e) { throw new DataException("Unable to fetch AT state from repository", e); } @@ -253,7 +250,7 @@ public class HSQLDBATRepository implements ATRepository { @Override public ATStateData getLatestATState(String atAddress) throws DataException { - String sql = "SELECT height, creation, state_data, state_hash, fees, is_initial " + String sql = "SELECT height, created_when, state_data, state_hash, fees, is_initial " + "FROM ATStates " + "WHERE AT_address = ? " + "ORDER BY height DESC " @@ -264,13 +261,13 @@ public class HSQLDBATRepository implements ATRepository { return null; int height = resultSet.getInt(1); - long creation = getZonedTimestampMilli(resultSet, 2); + long created = resultSet.getLong(2); byte[] stateData = resultSet.getBytes(3); // Actually BLOB byte[] stateHash = resultSet.getBytes(4); long fees = resultSet.getLong(5); boolean isInitial = resultSet.getBoolean(6); - return new ATStateData(atAddress, height, creation, stateData, stateHash, fees, isInitial); + return new ATStateData(atAddress, height, created, stateData, stateHash, fees, isInitial); } catch (SQLException e) { throw new DataException("Unable to fetch latest AT state from repository", e); } @@ -281,7 +278,7 @@ public class HSQLDBATRepository implements ATRepository { String sql = "SELECT AT_address, state_hash, fees, is_initial " + "FROM ATStates " + "WHERE height = ? " - + "ORDER BY creation ASC"; + + "ORDER BY created_when ASC"; List atStates = new ArrayList<>(); @@ -315,7 +312,7 @@ public class HSQLDBATRepository implements ATRepository { HSQLDBSaver saveHelper = new HSQLDBSaver("ATStates"); saveHelper.bind("AT_address", atStateData.getATAddress()).bind("height", atStateData.getHeight()) - .bind("creation", toOffsetDateTime(atStateData.getCreation())).bind("state_data", atStateData.getStateData()) + .bind("created_when", atStateData.getCreation()).bind("state_data", atStateData.getStateData()) .bind("state_hash", atStateData.getStateHash()).bind("fees", atStateData.getFees()) .bind("is_initial", atStateData.isInitial()); diff --git a/src/main/java/org/qortal/repository/hsqldb/HSQLDBAccountRepository.java b/src/main/java/org/qortal/repository/hsqldb/HSQLDBAccountRepository.java index 93714de1..afcd12b5 100644 --- a/src/main/java/org/qortal/repository/hsqldb/HSQLDBAccountRepository.java +++ b/src/main/java/org/qortal/repository/hsqldb/HSQLDBAccountRepository.java @@ -290,7 +290,7 @@ public class HSQLDBAccountRepository implements AccountRepository { @Override public AccountBalanceData getBalance(String address, long assetId) throws DataException { - String sql = "SELECT IFNULL(balance, 0) FROM AccountBalances WHERE account = ? AND asset_id = ? LIMIT 1"; + String sql = "SELECT balance FROM AccountBalances WHERE account = ? AND asset_id = ? LIMIT 1"; try (ResultSet resultSet = this.repository.checkedExecute(sql, address, assetId)) { if (resultSet == null) @@ -307,7 +307,8 @@ public class HSQLDBAccountRepository implements AccountRepository { @Override public List getAssetBalances(long assetId, Boolean excludeZero) throws DataException { StringBuilder sql = new StringBuilder(1024); - sql.append("SELECT account, IFNULL(balance, 0) FROM AccountBalances WHERE asset_id = ?"); + + sql.append("SELECT account, balance FROM AccountBalances WHERE asset_id = ?"); if (excludeZero != null && excludeZero) sql.append(" AND balance != 0"); @@ -334,7 +335,8 @@ public class HSQLDBAccountRepository implements AccountRepository { public List getAssetBalances(List addresses, List assetIds, BalanceOrdering balanceOrdering, Boolean excludeZero, Integer limit, Integer offset, Boolean reverse) throws DataException { StringBuilder sql = new StringBuilder(1024); - sql.append("SELECT account, asset_id, IFNULL(balance, 0), asset_name FROM "); + + sql.append("SELECT account, asset_id, balance, asset_name FROM "); final boolean haveAddresses = addresses != null && !addresses.isEmpty(); final boolean haveAssetIds = assetIds != null && !assetIds.isEmpty(); diff --git a/src/main/java/org/qortal/repository/hsqldb/HSQLDBAssetRepository.java b/src/main/java/org/qortal/repository/hsqldb/HSQLDBAssetRepository.java index 30af058b..878bab7a 100644 --- a/src/main/java/org/qortal/repository/hsqldb/HSQLDBAssetRepository.java +++ b/src/main/java/org/qortal/repository/hsqldb/HSQLDBAssetRepository.java @@ -1,8 +1,5 @@ package org.qortal.repository.hsqldb; -import static org.qortal.repository.hsqldb.HSQLDBRepository.getZonedTimestampMilli; -import static org.qortal.repository.hsqldb.HSQLDBRepository.toOffsetDateTime; - import java.sql.ResultSet; import java.sql.SQLException; import java.util.ArrayList; @@ -133,14 +130,14 @@ public class HSQLDBAssetRepository implements AssetRepository { } @Override - public List getRecentAssetIds(long start) throws DataException { + public List getRecentAssetIds(long startTimestamp) throws DataException { String sql = "SELECT asset_id FROM IssueAssetTransactions JOIN Assets USING (asset_id) " + "JOIN Transactions USING (signature) " - + "WHERE creation >= ?"; + + "WHERE created_when >= ?"; List assetIds = new ArrayList<>(); - try (ResultSet resultSet = this.repository.checkedExecute(sql, HSQLDBRepository.toOffsetDateTime(start))) { + try (ResultSet resultSet = this.repository.checkedExecute(sql, startTimestamp)) { if (resultSet == null) return assetIds; @@ -199,7 +196,8 @@ public class HSQLDBAssetRepository implements AssetRepository { @Override public OrderData fromOrderId(byte[] orderId) throws DataException { - String sql = "SELECT creator, have_asset_id, want_asset_id, amount, fulfilled, price, ordered, is_closed, is_fulfilled, HaveAsset.asset_name, WantAsset.asset_name " + String sql = "SELECT creator, have_asset_id, want_asset_id, amount, fulfilled, price, ordered_when, " + + "is_closed, is_fulfilled, HaveAsset.asset_name, WantAsset.asset_name " + "FROM AssetOrders " + "JOIN Assets AS HaveAsset ON HaveAsset.asset_id = have_asset_id " + "JOIN Assets AS WantAsset ON WantAsset.asset_id = want_asset_id " @@ -215,13 +213,14 @@ public class HSQLDBAssetRepository implements AssetRepository { long amount = resultSet.getLong(4); long fulfilled = resultSet.getLong(5); long price = resultSet.getLong(6); - long timestamp = getZonedTimestampMilli(resultSet, 7); + long timestamp = resultSet.getLong(7); boolean isClosed = resultSet.getBoolean(8); boolean isFulfilled = resultSet.getBoolean(9); String haveAssetName = resultSet.getString(10); String wantAssetName = resultSet.getString(11); - return new OrderData(orderId, creatorPublicKey, haveAssetId, wantAssetId, amount, fulfilled, price, timestamp, isClosed, isFulfilled, haveAssetName, wantAssetName); + return new OrderData(orderId, creatorPublicKey, haveAssetId, wantAssetId, amount, fulfilled, price, + timestamp, isClosed, isFulfilled, haveAssetName, wantAssetName); } catch (SQLException e) { throw new DataException("Unable to fetch asset order from repository", e); } @@ -242,14 +241,14 @@ public class HSQLDBAssetRepository implements AssetRepository { return orders; StringBuilder sql = new StringBuilder(512); - sql.append("SELECT creator, asset_order_id, amount, fulfilled, price, ordered " + "FROM AssetOrders " + sql.append("SELECT creator, asset_order_id, amount, fulfilled, price, ordered_when FROM AssetOrders " + "WHERE have_asset_id = ? AND want_asset_id = ? AND NOT is_closed AND NOT is_fulfilled "); sql.append("ORDER BY price"); if (reverse != null && reverse) sql.append(" DESC"); - sql.append(", ordered"); + sql.append(", ordered_when"); if (reverse != null && reverse) sql.append(" DESC"); @@ -265,7 +264,7 @@ public class HSQLDBAssetRepository implements AssetRepository { long amount = resultSet.getLong(3); long fulfilled = resultSet.getLong(4); long price = resultSet.getLong(5); - long timestamp = getZonedTimestampMilli(resultSet, 6); + long timestamp = resultSet.getLong(6); boolean isClosed = false; boolean isFulfilled = false; @@ -285,7 +284,7 @@ public class HSQLDBAssetRepository implements AssetRepository { List bindParams = new ArrayList<>(3); StringBuilder sql = new StringBuilder(512); - sql.append("SELECT creator, asset_order_id, amount, fulfilled, price, ordered " + "FROM AssetOrders " + sql.append("SELECT creator, asset_order_id, amount, fulfilled, price, ordered_when FROM AssetOrders " + "WHERE have_asset_id = ? AND want_asset_id = ? AND NOT is_closed AND NOT is_fulfilled "); Collections.addAll(bindParams, haveAssetId, wantAssetId); @@ -305,7 +304,7 @@ public class HSQLDBAssetRepository implements AssetRepository { if (minimumPrice != null && haveAssetId < wantAssetId) sql.append(" DESC"); - sql.append(", ordered"); + sql.append(", ordered_when"); List orders = new ArrayList<>(); @@ -319,7 +318,7 @@ public class HSQLDBAssetRepository implements AssetRepository { long amount = resultSet.getLong(3); long fulfilled = resultSet.getLong(4); long price = resultSet.getLong(5); - long timestamp = getZonedTimestampMilli(resultSet, 6); + long timestamp = resultSet.getLong(6); boolean isClosed = false; boolean isFulfilled = false; @@ -350,7 +349,7 @@ public class HSQLDBAssetRepository implements AssetRepository { return orders; StringBuilder sql = new StringBuilder(512); - sql.append("SELECT price, SUM(amount - fulfilled), MAX(ordered) " + "FROM AssetOrders " + sql.append("SELECT price, SUM(amount - fulfilled), MAX(ordered_when) FROM AssetOrders " + "WHERE have_asset_id = ? AND want_asset_id = ? AND NOT is_closed AND NOT is_fulfilled " + "GROUP BY price "); @@ -367,7 +366,7 @@ public class HSQLDBAssetRepository implements AssetRepository { do { long price = resultSet.getLong(1); long totalUnfulfilled = resultSet.getLong(2); - long timestamp = resultSet.getTimestamp(3).getTime(); + long timestamp = resultSet.getLong(3); OrderData order = new OrderData(null, null, haveAssetId, wantAssetId, totalUnfulfilled, 0L, price, timestamp, false, false, haveAssetData.getName(), wantAssetData.getName()); @@ -384,7 +383,8 @@ public class HSQLDBAssetRepository implements AssetRepository { public List getAccountsOrders(byte[] publicKey, Boolean optIsClosed, Boolean optIsFulfilled, Integer limit, Integer offset, Boolean reverse) throws DataException { StringBuilder sql = new StringBuilder(1024); - sql.append("SELECT asset_order_id, have_asset_id, want_asset_id, amount, fulfilled, price, ordered, is_closed, is_fulfilled, HaveAsset.asset_name, WantAsset.asset_name " + sql.append("SELECT asset_order_id, have_asset_id, want_asset_id, amount, fulfilled, price, ordered_when, " + + "is_closed, is_fulfilled, HaveAsset.asset_name, WantAsset.asset_name " + "FROM AssetOrders " + "JOIN Assets AS HaveAsset ON HaveAsset.asset_id = have_asset_id " + "JOIN Assets AS WantAsset ON WantAsset.asset_id = want_asset_id " @@ -400,7 +400,7 @@ public class HSQLDBAssetRepository implements AssetRepository { sql.append(optIsFulfilled ? "TRUE" : "FALSE"); } - sql.append(" ORDER BY ordered"); + sql.append(" ORDER BY ordered_when"); if (reverse != null && reverse) sql.append(" DESC"); @@ -419,7 +419,7 @@ public class HSQLDBAssetRepository implements AssetRepository { long amount = resultSet.getLong(4); long fulfilled = resultSet.getLong(5); long price = resultSet.getLong(6); - long timestamp = getZonedTimestampMilli(resultSet, 7); + long timestamp = resultSet.getLong(7); boolean isClosed = resultSet.getBoolean(8); boolean isFulfilled = resultSet.getBoolean(9); String haveAssetName = resultSet.getString(10); @@ -451,7 +451,7 @@ public class HSQLDBAssetRepository implements AssetRepository { return orders; StringBuilder sql = new StringBuilder(1024); - sql.append("SELECT asset_order_id, amount, fulfilled, price, ordered, is_closed, is_fulfilled " + sql.append("SELECT asset_order_id, amount, fulfilled, price, ordered_when, is_closed, is_fulfilled " + "FROM AssetOrders " + "WHERE creator = ? AND have_asset_id = ? AND want_asset_id = ?"); @@ -465,7 +465,7 @@ public class HSQLDBAssetRepository implements AssetRepository { sql.append(optIsFulfilled ? "TRUE" : "FALSE"); } - sql.append(" ORDER BY ordered"); + sql.append(" ORDER BY ordered_when"); if (reverse != null && reverse) sql.append(" DESC"); @@ -480,7 +480,7 @@ public class HSQLDBAssetRepository implements AssetRepository { long amount = resultSet.getLong(2); long fulfilled = resultSet.getLong(3); long price = resultSet.getLong(4); - long timestamp = getZonedTimestampMilli(resultSet, 5); + long timestamp = resultSet.getLong(5); boolean isClosed = resultSet.getBoolean(6); boolean isFulfilled = resultSet.getBoolean(7); @@ -502,7 +502,7 @@ public class HSQLDBAssetRepository implements AssetRepository { saveHelper.bind("asset_order_id", orderData.getOrderId()).bind("creator", orderData.getCreatorPublicKey()) .bind("have_asset_id", orderData.getHaveAssetId()).bind("want_asset_id", orderData.getWantAssetId()) .bind("amount", orderData.getAmount()).bind("fulfilled", orderData.getFulfilled()) - .bind("price", orderData.getPrice()).bind("ordered", toOffsetDateTime(orderData.getTimestamp())) + .bind("price", orderData.getPrice()).bind("ordered_when", orderData.getTimestamp()) .bind("is_closed", orderData.getIsClosed()).bind("is_fulfilled", orderData.getIsFulfilled()); try { @@ -538,11 +538,11 @@ public class HSQLDBAssetRepository implements AssetRepository { return trades; StringBuilder sql = new StringBuilder(512); - sql.append("SELECT initiating_order_id, target_order_id, target_amount, initiator_amount, initiator_saving, traded " + sql.append("SELECT initiating_order_id, target_order_id, target_amount, initiator_amount, initiator_saving, traded_when " + "FROM AssetOrders JOIN AssetTrades ON initiating_order_id = asset_order_id " + "WHERE have_asset_id = ? AND want_asset_id = ? "); - sql.append("ORDER BY traded"); + sql.append("ORDER BY traded_when"); if (reverse != null && reverse) sql.append(" DESC"); @@ -558,7 +558,7 @@ public class HSQLDBAssetRepository implements AssetRepository { long targetAmount = resultSet.getLong(3); long initiatorAmount = resultSet.getLong(4); long initiatorSaving = resultSet.getLong(5); - long timestamp = getZonedTimestampMilli(resultSet, 6); + long timestamp = resultSet.getLong(6); TradeData trade = new TradeData(initiatingOrderId, targetOrderId, targetAmount, initiatorAmount, initiatorSaving, timestamp, haveAssetId, haveAssetData.getName(), wantAssetId, wantAssetData.getName()); @@ -615,18 +615,18 @@ public class HSQLDBAssetRepository implements AssetRepository { tradedAssetsSubquery.append(" GROUP BY have_asset_id, want_asset_id"); // Find recent trades using "TradedAssets" assetID pairs - String recentTradesSubquery = "SELECT AssetTrades.target_amount, AssetTrades.initiator_amount, AssetTrades.traded " + String recentTradesSubquery = "SELECT AssetTrades.target_amount, AssetTrades.initiator_amount, AssetTrades.traded_when " + "FROM AssetOrders JOIN AssetTrades ON initiating_order_id = asset_order_id " + "WHERE AssetOrders.have_asset_id = TradedAssets.have_asset_id AND AssetOrders.want_asset_id = TradedAssets.want_asset_id " - + "ORDER BY traded DESC LIMIT 2"; + + "ORDER BY traded_when DESC LIMIT 2"; // Put it all together StringBuilder sql = new StringBuilder(4096); - sql.append("SELECT have_asset_id, want_asset_id, RecentTrades.target_amount, RecentTrades.initiator_amount, RecentTrades.traded FROM ("); + sql.append("SELECT have_asset_id, want_asset_id, RecentTrades.target_amount, RecentTrades.initiator_amount, RecentTrades.traded_when FROM ("); sql.append(tradedAssetsSubquery); sql.append(") AS TradedAssets, LATERAL ("); sql.append(recentTradesSubquery); - sql.append(") AS RecentTrades (target_amount, initiator_amount, traded) ORDER BY have_asset_id"); + sql.append(") AS RecentTrades (target_amount, initiator_amount, traded_when) ORDER BY have_asset_id"); if (reverse != null && reverse) sql.append(" DESC"); @@ -634,7 +634,7 @@ public class HSQLDBAssetRepository implements AssetRepository { if (reverse != null && reverse) sql.append(" DESC"); - sql.append(", RecentTrades.traded DESC "); + sql.append(", RecentTrades.traded_when DESC "); HSQLDBRepository.limitOffsetSql(sql, limit, offset); @@ -649,7 +649,7 @@ public class HSQLDBAssetRepository implements AssetRepository { long wantAssetId = resultSet.getLong(2); long otherAmount = resultSet.getLong(3); long amount = resultSet.getLong(4); - long timestamp = getZonedTimestampMilli(resultSet, 5); + long timestamp = resultSet.getLong(5); RecentTradeData recentTrade = new RecentTradeData(haveAssetId, wantAssetId, otherAmount, amount, timestamp); @@ -665,7 +665,7 @@ public class HSQLDBAssetRepository implements AssetRepository { @Override public List getOrdersTrades(byte[] orderId, Integer limit, Integer offset, Boolean reverse) throws DataException { StringBuilder sql = new StringBuilder(512); - sql.append("SELECT initiating_order_id, target_order_id, target_amount, initiator_amount, initiator_saving, traded, " + sql.append("SELECT initiating_order_id, target_order_id, target_amount, initiator_amount, initiator_saving, traded_when, " + "have_asset_id, HaveAsset.asset_name, want_asset_id, WantAsset.asset_name " + "FROM AssetTrades " + "JOIN AssetOrders ON asset_order_id = initiating_order_id " @@ -673,7 +673,7 @@ public class HSQLDBAssetRepository implements AssetRepository { + "JOIN Assets AS WantAsset ON WantAsset.asset_id = want_asset_id " + "WHERE ? IN (initiating_order_id, target_order_id) "); - sql.append("ORDER BY traded"); + sql.append("ORDER BY traded_when"); if (reverse != null && reverse) sql.append(" DESC"); @@ -691,7 +691,7 @@ public class HSQLDBAssetRepository implements AssetRepository { long targetAmount = resultSet.getLong(3); long initiatorAmount = resultSet.getLong(4); long initiatorSaving = resultSet.getLong(5); - long timestamp = getZonedTimestampMilli(resultSet, 6); + long timestamp = resultSet.getLong(6); long haveAssetId = resultSet.getLong(7); String haveAssetName = resultSet.getString(8); @@ -715,7 +715,7 @@ public class HSQLDBAssetRepository implements AssetRepository { saveHelper.bind("initiating_order_id", tradeData.getInitiator()).bind("target_order_id", tradeData.getTarget()) .bind("target_amount", tradeData.getTargetAmount()).bind("initiator_amount", tradeData.getInitiatorAmount()) - .bind("initiator_saving", tradeData.getInitiatorSaving()).bind("traded", toOffsetDateTime(tradeData.getTimestamp())); + .bind("initiator_saving", tradeData.getInitiatorSaving()).bind("traded_when", tradeData.getTimestamp()); try { saveHelper.execute(this.repository); diff --git a/src/main/java/org/qortal/repository/hsqldb/HSQLDBBlockRepository.java b/src/main/java/org/qortal/repository/hsqldb/HSQLDBBlockRepository.java index 5984edaa..fc6477ad 100644 --- a/src/main/java/org/qortal/repository/hsqldb/HSQLDBBlockRepository.java +++ b/src/main/java/org/qortal/repository/hsqldb/HSQLDBBlockRepository.java @@ -1,8 +1,5 @@ package org.qortal.repository.hsqldb; -import static org.qortal.repository.hsqldb.HSQLDBRepository.getZonedTimestampMilli; -import static org.qortal.repository.hsqldb.HSQLDBRepository.toOffsetDateTime; - import java.sql.ResultSet; import java.sql.SQLException; import java.util.ArrayList; @@ -21,7 +18,7 @@ import org.qortal.repository.TransactionRepository; public class HSQLDBBlockRepository implements BlockRepository { private static final String BLOCK_DB_COLUMNS = "version, reference, transaction_count, total_fees, " - + "transactions_signature, height, minted, minter, minter_signature, " + + "transactions_signature, height, minted_when, minter, minter_signature, " + "AT_count, AT_fees, online_accounts, online_accounts_count, online_accounts_timestamp, online_accounts_signatures"; protected HSQLDBRepository repository; @@ -41,14 +38,18 @@ public class HSQLDBBlockRepository implements BlockRepository { long totalFees = resultSet.getLong(4); byte[] transactionsSignature = resultSet.getBytes(5); int height = resultSet.getInt(6); - long timestamp = getZonedTimestampMilli(resultSet, 7); + long timestamp = resultSet.getLong(7); byte[] minterPublicKey = resultSet.getBytes(8); byte[] minterSignature = resultSet.getBytes(9); int atCount = resultSet.getInt(10); long atFees = resultSet.getLong(11); byte[] encodedOnlineAccounts = resultSet.getBytes(12); int onlineAccountsCount = resultSet.getInt(13); - Long onlineAccountsTimestamp = getZonedTimestampMilli(resultSet, 14); + + Long onlineAccountsTimestamp = resultSet.getLong(14); + if (onlineAccountsTimestamp == 0 && resultSet.wasNull()) + onlineAccountsTimestamp = null; + byte[] onlineAccountsSignatures = resultSet.getBytes(15); return new BlockData(version, reference, transactionCount, totalFees, transactionsSignature, height, timestamp, @@ -61,7 +62,9 @@ public class HSQLDBBlockRepository implements BlockRepository { @Override public BlockData fromSignature(byte[] signature) throws DataException { - try (ResultSet resultSet = this.repository.checkedExecute("SELECT " + BLOCK_DB_COLUMNS + " FROM Blocks WHERE signature = ? LIMIT 1", signature)) { + String sql = "SELECT " + BLOCK_DB_COLUMNS + " FROM Blocks WHERE signature = ? LIMIT 1"; + + try (ResultSet resultSet = this.repository.checkedExecute(sql, signature)) { return getBlockFromResultSet(resultSet); } catch (SQLException e) { throw new DataException("Error fetching block by signature from repository", e); @@ -70,7 +73,9 @@ public class HSQLDBBlockRepository implements BlockRepository { @Override public BlockData fromReference(byte[] reference) throws DataException { - try (ResultSet resultSet = this.repository.checkedExecute("SELECT " + BLOCK_DB_COLUMNS + " FROM Blocks WHERE reference = ? LIMIT 1", reference)) { + String sql = "SELECT " + BLOCK_DB_COLUMNS + " FROM Blocks WHERE reference = ? LIMIT 1"; + + try (ResultSet resultSet = this.repository.checkedExecute(sql, reference)) { return getBlockFromResultSet(resultSet); } catch (SQLException e) { throw new DataException("Error fetching block by reference from repository", e); @@ -79,7 +84,9 @@ public class HSQLDBBlockRepository implements BlockRepository { @Override public BlockData fromHeight(int height) throws DataException { - try (ResultSet resultSet = this.repository.checkedExecute("SELECT " + BLOCK_DB_COLUMNS + " FROM Blocks WHERE height = ? LIMIT 1", height)) { + String sql = "SELECT " + BLOCK_DB_COLUMNS + " FROM Blocks WHERE height = ? LIMIT 1"; + + try (ResultSet resultSet = this.repository.checkedExecute(sql, height)) { return getBlockFromResultSet(resultSet); } catch (SQLException e) { throw new DataException("Error fetching block by height from repository", e); @@ -97,7 +104,9 @@ public class HSQLDBBlockRepository implements BlockRepository { @Override public int getHeightFromSignature(byte[] signature) throws DataException { - try (ResultSet resultSet = this.repository.checkedExecute("SELECT height FROM Blocks WHERE signature = ? LIMIT 1", signature)) { + String sql = "SELECT height FROM Blocks WHERE signature = ? LIMIT 1"; + + try (ResultSet resultSet = this.repository.checkedExecute(sql, signature)) { if (resultSet == null) return 0; @@ -109,9 +118,10 @@ public class HSQLDBBlockRepository implements BlockRepository { @Override public int getHeightFromTimestamp(long timestamp) throws DataException { - // Uses (minted, height) index - try (ResultSet resultSet = this.repository.checkedExecute("SELECT height FROM Blocks WHERE minted <= ? ORDER BY minted DESC LIMIT 1", - toOffsetDateTime(timestamp))) { + // Uses (minted_when, height) index + String sql = "SELECT height FROM Blocks WHERE minted_when <= ? ORDER BY minted_when DESC LIMIT 1"; + + try (ResultSet resultSet = this.repository.checkedExecute(sql, timestamp)) { if (resultSet == null) return 0; @@ -123,7 +133,9 @@ public class HSQLDBBlockRepository implements BlockRepository { @Override public int getBlockchainHeight() throws DataException { - try (ResultSet resultSet = this.repository.checkedExecute("SELECT height FROM Blocks ORDER BY height DESC LIMIT 1")) { + String sql = "SELECT height FROM Blocks ORDER BY height DESC LIMIT 1"; + + try (ResultSet resultSet = this.repository.checkedExecute(sql)) { if (resultSet == null) return 0; @@ -135,7 +147,9 @@ public class HSQLDBBlockRepository implements BlockRepository { @Override public BlockData getLastBlock() throws DataException { - try (ResultSet resultSet = this.repository.checkedExecute("SELECT " + BLOCK_DB_COLUMNS + " FROM Blocks ORDER BY height DESC LIMIT 1")) { + String sql = "SELECT " + BLOCK_DB_COLUMNS + " FROM Blocks ORDER BY height DESC LIMIT 1"; + + try (ResultSet resultSet = this.repository.checkedExecute(sql)) { return getBlockFromResultSet(resultSet); } catch (SQLException e) { throw new DataException("Error fetching last block from repository", e); @@ -176,7 +190,9 @@ public class HSQLDBBlockRepository implements BlockRepository { public int countMintedBlocks(byte[] minterPublicKey) throws DataException { String directSql = "SELECT COUNT(*) FROM Blocks WHERE minter = ?"; - String rewardShareSql = "SELECT COUNT(*) FROM RewardShares JOIN Blocks ON minter = reward_share_public_key WHERE minter_public_key = ?"; + String rewardShareSql = "SELECT COUNT(*) FROM RewardShares " + + "JOIN Blocks ON minter = reward_share_public_key " + + "WHERE minter_public_key = ?"; int totalCount = 0; @@ -346,10 +362,10 @@ public class HSQLDBBlockRepository implements BlockRepository { @Override public int trimOldOnlineAccountsSignatures(long timestamp) throws DataException { - String sql = "UPDATE Blocks set online_accounts_signatures = NULL WHERE minted < ? AND online_accounts_signatures IS NOT NULL"; + String sql = "UPDATE Blocks set online_accounts_signatures = NULL WHERE minted_when < ? AND online_accounts_signatures IS NOT NULL"; try { - return this.repository.checkedExecuteUpdateCount(sql, toOffsetDateTime(timestamp)); + return this.repository.checkedExecuteUpdateCount(sql, timestamp); } catch (SQLException e) { throw new DataException("Unable to trim old online accounts signatures in repository", e); } @@ -377,11 +393,11 @@ public class HSQLDBBlockRepository implements BlockRepository { saveHelper.bind("signature", blockData.getSignature()).bind("version", blockData.getVersion()).bind("reference", blockData.getReference()) .bind("transaction_count", blockData.getTransactionCount()).bind("total_fees", blockData.getTotalFees()) .bind("transactions_signature", blockData.getTransactionsSignature()).bind("height", blockData.getHeight()) - .bind("minted", toOffsetDateTime(blockData.getTimestamp())) + .bind("minted_when", blockData.getTimestamp()) .bind("minter", blockData.getMinterPublicKey()).bind("minter_signature", blockData.getMinterSignature()) .bind("AT_count", blockData.getATCount()).bind("AT_fees", blockData.getATFees()) .bind("online_accounts", blockData.getEncodedOnlineAccounts()).bind("online_accounts_count", blockData.getOnlineAccountsCount()) - .bind("online_accounts_timestamp", toOffsetDateTime(blockData.getOnlineAccountsTimestamp())) + .bind("online_accounts_timestamp", blockData.getOnlineAccountsTimestamp()) .bind("online_accounts_signatures", blockData.getOnlineAccountsSignatures()); try { diff --git a/src/main/java/org/qortal/repository/hsqldb/HSQLDBChatRepository.java b/src/main/java/org/qortal/repository/hsqldb/HSQLDBChatRepository.java index 01e7cafd..7ff7df21 100644 --- a/src/main/java/org/qortal/repository/hsqldb/HSQLDBChatRepository.java +++ b/src/main/java/org/qortal/repository/hsqldb/HSQLDBChatRepository.java @@ -43,13 +43,13 @@ public class HSQLDBChatRepository implements ChatRepository { // Timestamp range if (before != null) { - whereClauses.add("Transactions.creation < ?"); - bindParams.add(HSQLDBRepository.toOffsetDateTime(before)); + whereClauses.add("Transactions.created_when < ?"); + bindParams.add(before); } if (after != null) { - whereClauses.add("Transactions.creation > ?"); - bindParams.add(HSQLDBRepository.toOffsetDateTime(after)); + whereClauses.add("Transactions.created_when > ?"); + bindParams.add(after); } if (txGroupId != null) @@ -83,7 +83,7 @@ public class HSQLDBChatRepository implements ChatRepository { } } - sql.append(" ORDER BY Transactions.creation"); + sql.append(" ORDER BY Transactions.created_when"); sql.append((reverse == null || !reverse) ? " ASC" : " DESC"); HSQLDBRepository.limitOffsetSql(sql, limit, offset); diff --git a/src/main/java/org/qortal/repository/hsqldb/HSQLDBDatabaseUpdates.java b/src/main/java/org/qortal/repository/hsqldb/HSQLDBDatabaseUpdates.java index d9ec4de8..399e0662 100644 --- a/src/main/java/org/qortal/repository/hsqldb/HSQLDBDatabaseUpdates.java +++ b/src/main/java/org/qortal/repository/hsqldb/HSQLDBDatabaseUpdates.java @@ -4,16 +4,17 @@ import java.sql.Connection; import java.sql.ResultSet; import java.sql.SQLException; import java.sql.Statement; -import java.util.List; import org.apache.logging.log4j.LogManager; import org.apache.logging.log4j.Logger; -import org.qortal.block.BlockChain; public class HSQLDBDatabaseUpdates { private static final Logger LOGGER = LogManager.getLogger(HSQLDBDatabaseUpdates.class); + private static final String TRANSACTION_KEYS = "PRIMARY KEY (signature), " + + "FOREIGN KEY (signature) REFERENCES Transactions (signature) ON DELETE CASCADE"; + /** * Apply any incremental changes to database schema. * @@ -89,74 +90,105 @@ public class HSQLDBDatabaseUpdates { stmt.execute("SET DATABASE TRANSACTION CONTROL MVCC"); // Use MVCC over default two-phase locking, a-k-a "LOCKS" stmt.execute("SET DATABASE DEFAULT TABLE TYPE CACHED"); stmt.execute("SET DATABASE COLLATION SQL_TEXT NO PAD"); // Do not pad strings to same length before comparison + stmt.execute("CREATE COLLATION SQL_TEXT_UCC_NO_PAD FOR SQL_TEXT FROM SQL_TEXT_UCC NO PAD"); stmt.execute("CREATE COLLATION SQL_TEXT_NO_PAD FOR SQL_TEXT FROM SQL_TEXT NO PAD"); + stmt.execute("SET FILES SPACE TRUE"); // Enable per-table block space within .data file, useful for CACHED table types stmt.execute("SET FILES LOB SCALE 1"); // LOB granularity is 1KB + // Slow down log fsync() calls from every 500ms to reduce I/O load + stmt.execute("SET FILES WRITE DELAY 5"); // only fsync() every 5 seconds + stmt.execute("CREATE TABLE DatabaseInfo ( version INTEGER NOT NULL )"); stmt.execute("INSERT INTO DatabaseInfo VALUES ( 0 )"); - stmt.execute("CREATE TYPE BlockSignature AS VARBINARY(128)"); - stmt.execute("CREATE TYPE Signature AS VARBINARY(64)"); - stmt.execute("CREATE TYPE QortalAddress AS VARCHAR(36)"); - stmt.execute("CREATE TYPE QortalPublicKey AS VARBINARY(32)"); - stmt.execute("CREATE TYPE QortalAmount AS BIGINT"); - stmt.execute("CREATE TYPE GenericDescription AS VARCHAR(4000)"); - stmt.execute("CREATE TYPE RegisteredName AS VARCHAR(128) COLLATE SQL_TEXT_NO_PAD"); - stmt.execute("CREATE TYPE NameData AS VARCHAR(4000)"); - stmt.execute("CREATE TYPE MessageData AS VARBINARY(4000)"); - stmt.execute("CREATE TYPE PollName AS VARCHAR(128) COLLATE SQL_TEXT_NO_PAD"); - stmt.execute("CREATE TYPE PollOption AS VARCHAR(80) COLLATE SQL_TEXT_UCC_NO_PAD"); - stmt.execute("CREATE TYPE PollOptionIndex AS INTEGER"); - stmt.execute("CREATE TYPE DataHash AS VARBINARY(32)"); + + stmt.execute("CREATE TYPE ArbitraryData AS VARBINARY(256)"); + stmt.execute("CREATE TYPE AssetData AS CLOB(400K)"); stmt.execute("CREATE TYPE AssetID AS BIGINT"); stmt.execute("CREATE TYPE AssetName AS VARCHAR(34) COLLATE SQL_TEXT_NO_PAD"); stmt.execute("CREATE TYPE AssetOrderID AS VARBINARY(64)"); - stmt.execute("CREATE TYPE ATName AS VARCHAR(32) COLLATE SQL_TEXT_UCC_NO_PAD"); - stmt.execute("CREATE TYPE ATType AS VARCHAR(32) COLLATE SQL_TEXT_UCC_NO_PAD"); - stmt.execute("CREATE TYPE ATTags AS VARCHAR(80) COLLATE SQL_TEXT_UCC_NO_PAD"); stmt.execute("CREATE TYPE ATCode AS BLOB(64K)"); // 16bit * 1 - stmt.execute("CREATE TYPE ATState AS BLOB(1M)"); // 16bit * 8 + 16bit * 4 + 16bit * 4 stmt.execute("CREATE TYPE ATCreationBytes AS BLOB(576K)"); // 16bit * 1 + 16bit * 8 + stmt.execute("CREATE TYPE ATMessage AS VARBINARY(32)"); + stmt.execute("CREATE TYPE ATName AS VARCHAR(32) COLLATE SQL_TEXT_UCC_NO_PAD"); + stmt.execute("CREATE TYPE ATState AS BLOB(1M)"); // 16bit * 8 + 16bit * 4 + 16bit * 4 + stmt.execute("CREATE TYPE ATTags AS VARCHAR(80) COLLATE SQL_TEXT_UCC_NO_PAD"); + stmt.execute("CREATE TYPE ATType AS VARCHAR(32) COLLATE SQL_TEXT_UCC_NO_PAD"); stmt.execute("CREATE TYPE ATStateHash as VARBINARY(32)"); - stmt.execute("CREATE TYPE ATMessage AS VARBINARY(256)"); + stmt.execute("CREATE TYPE BlockSignature AS VARBINARY(128)"); + stmt.execute("CREATE TYPE DataHash AS VARBINARY(32)"); + stmt.execute("CREATE TYPE EpochMillis AS BIGINT"); + stmt.execute("CREATE TYPE GenericDescription AS VARCHAR(4000)"); stmt.execute("CREATE TYPE GroupID AS INTEGER"); stmt.execute("CREATE TYPE GroupName AS VARCHAR(400) COLLATE SQL_TEXT_UCC_NO_PAD"); stmt.execute("CREATE TYPE GroupReason AS VARCHAR(128) COLLATE SQL_TEXT_UCC_NO_PAD"); + stmt.execute("CREATE TYPE MessageData AS VARBINARY(4000)"); + stmt.execute("CREATE TYPE NameData AS VARCHAR(4000)"); + stmt.execute("CREATE TYPE PollName AS VARCHAR(128) COLLATE SQL_TEXT_NO_PAD"); + stmt.execute("CREATE TYPE PollOption AS VARCHAR(80) COLLATE SQL_TEXT_UCC_NO_PAD"); + stmt.execute("CREATE TYPE PollOptionIndex AS TINYINT"); + stmt.execute("CREATE TYPE QortalAddress AS VARCHAR(36)"); + stmt.execute("CREATE TYPE QortalKeySeed AS VARBINARY(32)"); + stmt.execute("CREATE TYPE QortalPublicKey AS VARBINARY(32)"); + stmt.execute("CREATE TYPE QortalAmount AS BIGINT"); + stmt.execute("CREATE TYPE RegisteredName AS VARCHAR(128) COLLATE SQL_TEXT_NO_PAD"); stmt.execute("CREATE TYPE RewardSharePercent AS INT"); + stmt.execute("CREATE TYPE Signature AS VARBINARY(64)"); break; case 1: // Blocks stmt.execute("CREATE TABLE Blocks (signature BlockSignature, version TINYINT NOT NULL, reference BlockSignature, " + "transaction_count INTEGER NOT NULL, total_fees QortalAmount NOT NULL, transactions_signature Signature NOT NULL, " - + "height INTEGER NOT NULL, generation TIMESTAMP WITH TIME ZONE NOT NULL, " - + "generator QortalPublicKey NOT NULL, generator_signature Signature NOT NULL, AT_count INTEGER NOT NULL, AT_fees QortalAmount NOT NULL, " + + "height INTEGER NOT NULL, minted_when EpochMillis NOT NULL, " + + "minter QortalPublicKey NOT NULL, minter_signature Signature NOT NULL, AT_count INTEGER NOT NULL, AT_fees QortalAmount NOT NULL, " + + "online_accounts BLOB(1M), online_accounts_count INTEGER NOT NULL, online_accounts_timestamp EpochMillis, online_accounts_signatures BLOB(1M), " + "PRIMARY KEY (signature))"); // For finding blocks by height. stmt.execute("CREATE INDEX BlockHeightIndex ON Blocks (height)"); - // For finding blocks by the account that generated them. - stmt.execute("CREATE INDEX BlockGeneratorIndex ON Blocks (generator)"); + // For finding blocks by the account that minted them. + stmt.execute("CREATE INDEX BlockMinterIndex ON Blocks (minter)"); // For finding blocks by reference, e.g. child blocks. stmt.execute("CREATE INDEX BlockReferenceIndex ON Blocks (reference)"); - // For finding blocks by generation timestamp or finding height of latest block immediately before generation timestamp, etc. - stmt.execute("CREATE INDEX BlockGenerationHeightIndex ON Blocks (generation, height)"); + // For finding blocks by timestamp or finding height of latest block immediately before timestamp, etc. + stmt.execute("CREATE INDEX BlockTimestampHeightIndex ON Blocks (minted_when, height)"); // Use a separate table space as this table will be very large. stmt.execute("SET TABLE Blocks NEW SPACE"); + + // Table to hold next block height. + stmt.execute("CREATE TABLE NextBlockHeight (height INT NOT NULL)"); + // Initial value - should work for empty DB or populated DB. + stmt.execute("INSERT INTO NextBlockHeight VALUES (SELECT IFNULL(MAX(height), 0) + 1 FROM Blocks)"); + // We use triggers on Blocks to update a simple "next block height" table + String blockUpdateSql = "UPDATE NextBlockHeight SET height = (SELECT height + 1 FROM Blocks ORDER BY height DESC LIMIT 1)"; + stmt.execute("CREATE TRIGGER Next_block_height_insert_trigger AFTER INSERT ON Blocks " + blockUpdateSql); + stmt.execute("CREATE TRIGGER Next_block_height_update_trigger AFTER UPDATE ON Blocks " + blockUpdateSql); + stmt.execute("CREATE TRIGGER Next_block_height_delete_trigger AFTER DELETE ON Blocks " + blockUpdateSql); break; case 2: // Generic transactions (null reference, creator and milestone_block for genesis transactions) stmt.execute("CREATE TABLE Transactions (signature Signature, reference Signature, type TINYINT NOT NULL, " - + "creator QortalPublicKey NOT NULL, creation TIMESTAMP WITH TIME ZONE NOT NULL, fee QortalAmount NOT NULL, milestone_block BlockSignature, " + + "creator QortalPublicKey NOT NULL, created_when EpochMillis NOT NULL, fee QortalAmount NOT NULL, " + + "tx_group_id GroupID NOT NULL, block_height INTEGER, " + + "approval_status TINYINT NOT NULL, approval_height INTEGER, " + "PRIMARY KEY (signature))"); // For finding transactions by transaction type. stmt.execute("CREATE INDEX TransactionTypeIndex ON Transactions (type)"); // For finding transactions using creation timestamp. - stmt.execute("CREATE INDEX TransactionCreationIndex ON Transactions (creation)"); + stmt.execute("CREATE INDEX TransactionTimestampIndex ON Transactions (created_when)"); // For when a user wants to lookup ALL transactions they have created, with optional type. stmt.execute("CREATE INDEX TransactionCreatorIndex ON Transactions (creator, type)"); // For finding transactions by reference, e.g. child transactions. stmt.execute("CREATE INDEX TransactionReferenceIndex ON Transactions (reference)"); + // For finding transactions by groupID + stmt.execute("CREATE INDEX TransactionGroupIndex ON Transactions (tx_group_id)"); + // For finding transactions by block height + stmt.execute("CREATE INDEX TransactionHeightIndex on Transactions (block_height)"); + // For searching transactions based on approval status + stmt.execute("CREATE INDEX TransactionApprovalStatusIndex on Transactions (approval_status, block_height)"); + // For searching transactions based on approval height + stmt.execute("CREATE INDEX TransactionApprovalHeightIndex on Transactions (approval_height)"); // Use a separate table space as this table will be very large. stmt.execute("SET TABLE Transactions NEW SPACE"); @@ -169,828 +201,401 @@ public class HSQLDBDatabaseUpdates { // Unconfirmed transactions // We use this as searching for transactions with no corresponding mapping in BlockTransactions is much slower. - stmt.execute("CREATE TABLE UnconfirmedTransactions (signature Signature PRIMARY KEY, creation TIMESTAMP WITH TIME ZONE NOT NULL)"); + stmt.execute("CREATE TABLE UnconfirmedTransactions (signature Signature PRIMARY KEY, created_when EpochMillis NOT NULL)"); // Index to allow quick sorting by creation-else-signature - stmt.execute("CREATE INDEX UnconfirmedTransactionsIndex ON UnconfirmedTransactions (creation, signature)"); + stmt.execute("CREATE INDEX UnconfirmedTransactionsIndex ON UnconfirmedTransactions (created_when, signature)"); // Transaction participants // To allow lookup of all activity by an address - stmt.execute("CREATE TABLE TransactionParticipants (signature Signature, participant QortalAddress NOT NULL, " + stmt.execute("CREATE TABLE TransactionParticipants (signature Signature NOT NULL, participant QortalAddress NOT NULL, " + "FOREIGN KEY (signature) REFERENCES Transactions (signature) ON DELETE CASCADE)"); + // Add index to TransactionParticipants to speed up queries + stmt.execute("CREATE INDEX TransactionParticipantsAddressIndex on TransactionParticipants (participant)"); // Use a separate table space as this table will be very large. stmt.execute("SET TABLE TransactionParticipants NEW SPACE"); break; case 3: - // Genesis Transactions - stmt.execute("CREATE TABLE GenesisTransactions (signature Signature, recipient QortalAddress NOT NULL, " - + "amount QortalAmount NOT NULL, asset_id AssetID NOT NULL, PRIMARY KEY (signature), " - + "FOREIGN KEY (signature) REFERENCES Transactions (signature) ON DELETE CASCADE)"); + // Accounts + stmt.execute("CREATE TABLE Accounts (account QortalAddress, reference Signature, public_key QortalPublicKey, " + + "default_group_id GroupID NOT NULL DEFAULT 0, flags INTEGER NOT NULL DEFAULT 0, level INT NOT NULL DEFAULT 0, " + + "blocks_minted INTEGER NOT NULL DEFAULT 0, blocks_minted_adjustment INTEGER NOT NULL DEFAULT 0, " + + "PRIMARY KEY (account))"); + // For looking up an account by public key + stmt.execute("CREATE INDEX AccountPublicKeyIndex on Accounts (public_key)"); + + // Account balances + stmt.execute("CREATE TABLE AccountBalances (account QortalAddress, asset_id AssetID, balance QortalAmount NOT NULL, " + + "PRIMARY KEY (account, asset_id), FOREIGN KEY (account) REFERENCES Accounts (account) ON DELETE CASCADE)"); + // Index for speeding up fetch legacy QORA holders for Block processing + stmt.execute("CREATE INDEX AccountBalancesAssetBalanceIndex ON AccountBalances (asset_id, balance)"); + // Add CHECK constraint to account balances + stmt.execute("ALTER TABLE AccountBalances ADD CONSTRAINT CheckBalanceNotNegative CHECK (balance >= 0)"); + + // Keeping track of QORT gained from holding legacy QORA + stmt.execute("CREATE TABLE AccountQortFromQoraInfo (account QortalAddress, final_qort_from_qora QortalAmount, final_block_height INT, " + + "PRIMARY KEY (account), FOREIGN KEY (account) REFERENCES Accounts (account) ON DELETE CASCADE)"); break; case 4: - // Payment Transactions - stmt.execute("CREATE TABLE PaymentTransactions (signature Signature, sender QortalPublicKey NOT NULL, recipient QortalAddress NOT NULL, " - + "amount QortalAmount NOT NULL, PRIMARY KEY (signature), " - + "FOREIGN KEY (signature) REFERENCES Transactions (signature) ON DELETE CASCADE)"); + // Genesis Transactions + stmt.execute("CREATE TABLE GenesisTransactions (signature Signature, recipient QortalAddress NOT NULL, " + + "amount QortalAmount NOT NULL, asset_id AssetID NOT NULL, " + TRANSACTION_KEYS + ")"); + + // Genesis-block-only transaction to set/clear flags + stmt.execute("CREATE TABLE AccountFlagsTransactions (signature Signature, creator QortalPublicKey NOT NULL, target QortalAddress NOT NULL, " + + "and_mask INT NOT NULL, or_mask INT NOT NULL, xor_mask INT NOT NULL, previous_flags INT, " + TRANSACTION_KEYS + ")"); + + // Genesis-block-only transaction to set level + stmt.execute("CREATE TABLE AccountLevelTransactions (signature Signature, creator QortalPublicKey NOT NULL, target QortalAddress NOT NULL, " + + "level INT NOT NULL, " + TRANSACTION_KEYS + ")"); break; case 5: - // Register Name Transactions - stmt.execute("CREATE TABLE RegisterNameTransactions (signature Signature, registrant QortalPublicKey NOT NULL, name RegisteredName NOT NULL, " - + "owner QortalAddress NOT NULL, data NameData NOT NULL, " - + "PRIMARY KEY (signature), FOREIGN KEY (signature) REFERENCES Transactions (signature) ON DELETE CASCADE)"); + // Payments + // Arbitrary/Multi-payment/Message/Payment Transaction Payments + stmt.execute("CREATE TABLE SharedTransactionPayments (signature Signature, recipient QortalAddress NOT NULL, " + + "amount QortalAmount NOT NULL, asset_id AssetID NOT NULL, " + TRANSACTION_KEYS + ")"); + + // Payment Transactions + stmt.execute("CREATE TABLE PaymentTransactions (signature Signature, sender QortalPublicKey NOT NULL, recipient QortalAddress NOT NULL, " + + "amount QortalAmount NOT NULL, " + TRANSACTION_KEYS + ")"); + + // Multi-payment Transactions + stmt.execute("CREATE TABLE MultiPaymentTransactions (signature Signature, sender QortalPublicKey NOT NULL, " + + TRANSACTION_KEYS + ")"); break; case 6: - // Update Name Transactions - stmt.execute("CREATE TABLE UpdateNameTransactions (signature Signature, owner QortalPublicKey NOT NULL, name RegisteredName NOT NULL, " - + "new_owner QortalAddress NOT NULL, new_data NameData NOT NULL, name_reference Signature, " - + "PRIMARY KEY (signature), FOREIGN KEY (signature) REFERENCES Transactions (signature) ON DELETE CASCADE)"); + // Message Transactions + stmt.execute("CREATE TABLE MessageTransactions (signature Signature, version TINYINT NOT NULL, " + + "sender QortalPublicKey NOT NULL, recipient QortalAddress NOT NULL, " + + "is_text BOOLEAN NOT NULL, is_encrypted BOOLEAN NOT NULL, amount QortalAmount NOT NULL, asset_id AssetID NOT NULL, data MessageData NOT NULL, " + + TRANSACTION_KEYS + ")"); break; case 7: - // Sell Name Transactions - stmt.execute("CREATE TABLE SellNameTransactions (signature Signature, owner QortalPublicKey NOT NULL, name RegisteredName NOT NULL, " - + "amount QortalAmount NOT NULL, PRIMARY KEY (signature), FOREIGN KEY (signature) REFERENCES Transactions (signature) ON DELETE CASCADE)"); - break; - - case 8: - // Cancel Sell Name Transactions - stmt.execute("CREATE TABLE CancelSellNameTransactions (signature Signature, owner QortalPublicKey NOT NULL, name RegisteredName NOT NULL, " - + "PRIMARY KEY (signature), FOREIGN KEY (signature) REFERENCES Transactions (signature) ON DELETE CASCADE)"); - break; - - case 9: - // Buy Name Transactions - stmt.execute("CREATE TABLE BuyNameTransactions (signature Signature, buyer QortalPublicKey NOT NULL, name RegisteredName NOT NULL, " - + "seller QortalAddress NOT NULL, amount QortalAmount NOT NULL, name_reference Signature, " - + "PRIMARY KEY (signature), FOREIGN KEY (signature) REFERENCES Transactions (signature) ON DELETE CASCADE)"); - break; - - case 10: - // Create Poll Transactions - stmt.execute("CREATE TABLE CreatePollTransactions (signature Signature, creator QortalPublicKey NOT NULL, owner QortalAddress NOT NULL, " - + "poll_name PollName NOT NULL, description GenericDescription NOT NULL, " - + "PRIMARY KEY (signature), FOREIGN KEY (signature) REFERENCES Transactions (signature) ON DELETE CASCADE)"); - // Poll options. NB: option is implicitly NON NULL and UNIQUE due to being part of compound primary key - stmt.execute("CREATE TABLE CreatePollTransactionOptions (signature Signature, option_index TINYINT NOT NULL, option_name PollOption, " - + "PRIMARY KEY (signature, option_index), FOREIGN KEY (signature) REFERENCES CreatePollTransactions (signature) ON DELETE CASCADE)"); - // For the future: add flag to polls to allow one or multiple votes per voter - break; - - case 11: - // Vote On Poll Transactions - stmt.execute("CREATE TABLE VoteOnPollTransactions (signature Signature, voter QortalPublicKey NOT NULL, poll_name PollName NOT NULL, " - + "option_index PollOptionIndex NOT NULL, previous_option_index PollOptionIndex, " - + "PRIMARY KEY (signature), FOREIGN KEY (signature) REFERENCES Transactions (signature) ON DELETE CASCADE)"); - break; - - case 12: - // Arbitrary/Multi-payment/Message/Payment Transaction Payments - stmt.execute("CREATE TABLE SharedTransactionPayments (signature Signature, recipient QortalAddress NOT NULL, " - + "amount QortalAmount NOT NULL, asset_id AssetID NOT NULL, " - + "PRIMARY KEY (signature, recipient, asset_id), FOREIGN KEY (signature) REFERENCES Transactions (signature) ON DELETE CASCADE)"); - break; - - case 13: // Arbitrary Transactions stmt.execute("CREATE TABLE ArbitraryTransactions (signature Signature, sender QortalPublicKey NOT NULL, version TINYINT NOT NULL, " - + "service TINYINT NOT NULL, data_hash DataHash NOT NULL, " - + "PRIMARY KEY (signature), FOREIGN KEY (signature) REFERENCES Transactions (signature) ON DELETE CASCADE)"); + + "service SMALLINT NOT NULL, is_data_raw BOOLEAN NOT NULL, data ArbitraryData NOT NULL, " + + TRANSACTION_KEYS + ")"); // NB: Actual data payload stored elsewhere // For the future: data payload should be encrypted, at the very least with transaction's reference as the seed for the encryption key break; - case 14: - // Issue Asset Transactions - stmt.execute( - "CREATE TABLE IssueAssetTransactions (signature Signature, issuer QortalPublicKey NOT NULL, owner QortalAddress NOT NULL, asset_name AssetName NOT NULL, " - + "description GenericDescription NOT NULL, quantity BIGINT NOT NULL, is_divisible BOOLEAN NOT NULL, asset_id AssetID, " - + "PRIMARY KEY (signature), FOREIGN KEY (signature) REFERENCES Transactions (signature) ON DELETE CASCADE)"); - // For the future: maybe convert quantity from BIGINT to QortalAmount, regardless of divisibility + case 8: + // Name-related + stmt.execute("CREATE TABLE Names (name RegisteredName, owner QortalAddress NOT NULL, " + + "registered_when EpochMillis NOT NULL, updated_when EpochMillis, creation_group_id GroupID NOT NULL DEFAULT 0, " + + "reference Signature, is_for_sale BOOLEAN NOT NULL DEFAULT FALSE, sale_price QortalAmount, data NameData NOT NULL, " + + "PRIMARY KEY (name))"); + + // Register Name Transactions + stmt.execute("CREATE TABLE RegisterNameTransactions (signature Signature, registrant QortalPublicKey NOT NULL, name RegisteredName NOT NULL, " + + "owner QortalAddress NOT NULL, data NameData NOT NULL, " + TRANSACTION_KEYS + ")"); + + // Update Name Transactions + stmt.execute("CREATE TABLE UpdateNameTransactions (signature Signature, owner QortalPublicKey NOT NULL, name RegisteredName NOT NULL, " + + "new_owner QortalAddress NOT NULL, new_data NameData NOT NULL, name_reference Signature, " + TRANSACTION_KEYS + ")"); + + // Sell Name Transactions + stmt.execute("CREATE TABLE SellNameTransactions (signature Signature, owner QortalPublicKey NOT NULL, name RegisteredName NOT NULL, " + + "amount QortalAmount NOT NULL, " + TRANSACTION_KEYS + ")"); + + // Cancel Sell Name Transactions + stmt.execute("CREATE TABLE CancelSellNameTransactions (signature Signature, owner QortalPublicKey NOT NULL, name RegisteredName NOT NULL, " + + TRANSACTION_KEYS + ")"); + + // Buy Name Transactions + stmt.execute("CREATE TABLE BuyNameTransactions (signature Signature, buyer QortalPublicKey NOT NULL, name RegisteredName NOT NULL, " + + "seller QortalAddress NOT NULL, amount QortalAmount NOT NULL, name_reference Signature, " + TRANSACTION_KEYS + ")"); break; - case 15: - // Transfer Asset Transactions - stmt.execute("CREATE TABLE TransferAssetTransactions (signature Signature, sender QortalPublicKey NOT NULL, recipient QortalAddress NOT NULL, " - + "asset_id AssetID NOT NULL, amount QortalAmount NOT NULL," - + "PRIMARY KEY (signature), FOREIGN KEY (signature) REFERENCES Transactions (signature) ON DELETE CASCADE)"); - break; + case 9: + // Polls/voting + stmt.execute("CREATE TABLE Polls (poll_name PollName, creator QortalPublicKey NOT NULL, " + + "owner QortalAddress NOT NULL, published_when EpochMillis NOT NULL, " + + "description GenericDescription NOT NULL, " + + "PRIMARY KEY (poll_name))"); + // For when a user wants to lookup poll they own + stmt.execute("CREATE INDEX PollOwnerIndex on Polls (owner)"); - case 16: - // Create Asset Order Transactions - stmt.execute("CREATE TABLE CreateAssetOrderTransactions (signature Signature, creator QortalPublicKey NOT NULL, " - + "have_asset_id AssetID NOT NULL, amount QortalAmount NOT NULL, want_asset_id AssetID NOT NULL, price QortalAmount NOT NULL, " - + "PRIMARY KEY (signature), FOREIGN KEY (signature) REFERENCES Transactions (signature) ON DELETE CASCADE)"); - break; - - case 17: - // Cancel Asset Order Transactions - stmt.execute("CREATE TABLE CancelAssetOrderTransactions (signature Signature, creator QortalPublicKey NOT NULL, " - + "asset_order_id AssetOrderID NOT NULL, " - + "PRIMARY KEY (signature), FOREIGN KEY (signature) REFERENCES Transactions (signature) ON DELETE CASCADE)"); - break; - - case 18: - // Multi-payment Transactions - stmt.execute("CREATE TABLE MultiPaymentTransactions (signature Signature, sender QortalPublicKey NOT NULL, " - + "PRIMARY KEY (signature), FOREIGN KEY (signature) REFERENCES Transactions (signature) ON DELETE CASCADE)"); - break; - - case 19: - // Deploy CIYAM AT Transactions - stmt.execute("CREATE TABLE DeployATTransactions (signature Signature, creator QortalPublicKey NOT NULL, AT_name ATName NOT NULL, " - + "description GenericDescription NOT NULL, AT_type ATType NOT NULL, AT_tags ATTags NOT NULL, " - + "creation_bytes ATCreationBytes NOT NULL, amount QortalAmount NOT NULL, asset_id AssetID NOT NULL, AT_address QortalAddress, " - + "PRIMARY KEY (signature), FOREIGN KEY (signature) REFERENCES Transactions (signature) ON DELETE CASCADE)"); - // For looking up the Deploy AT Transaction based on deployed AT address - stmt.execute("CREATE INDEX DeployATAddressIndex on DeployATTransactions (AT_address)"); - break; - - case 20: - // Message Transactions - stmt.execute( - "CREATE TABLE MessageTransactions (signature Signature, version TINYINT NOT NULL, sender QortalPublicKey NOT NULL, recipient QortalAddress NOT NULL, " - + "is_text BOOLEAN NOT NULL, is_encrypted BOOLEAN NOT NULL, amount QortalAmount NOT NULL, asset_id AssetID NOT NULL, data MessageData NOT NULL, " - + "PRIMARY KEY (signature), FOREIGN KEY (signature) REFERENCES Transactions (signature) ON DELETE CASCADE)"); - break; - - case 21: - // Assets (including QORT coin itself) - stmt.execute("CREATE TABLE Assets (asset_id AssetID, owner QortalAddress NOT NULL, " - + "asset_name AssetName NOT NULL, description GenericDescription NOT NULL, " - + "quantity BIGINT NOT NULL, is_divisible BOOLEAN NOT NULL, reference Signature NOT NULL, PRIMARY KEY (asset_id))"); - // We need a corresponding trigger to make sure new asset_id values are assigned sequentially start from 0 - stmt.execute( - "CREATE TRIGGER Asset_ID_Trigger BEFORE INSERT ON Assets REFERENCING NEW ROW AS new_row FOR EACH ROW WHEN (new_row.asset_id IS NULL) " - + "SET new_row.asset_id = (SELECT IFNULL(MAX(asset_id) + 1, 0) FROM Assets)"); - // For when a user wants to lookup an asset by name - stmt.execute("CREATE INDEX AssetNameIndex on Assets (asset_name)"); - break; - - case 22: - // Accounts - stmt.execute("CREATE TABLE Accounts (account QortalAddress, reference Signature, public_key QortalPublicKey, PRIMARY KEY (account))"); - stmt.execute("CREATE TABLE AccountBalances (account QortalAddress, asset_id AssetID, balance QortalAmount NOT NULL, " - + "PRIMARY KEY (account, asset_id), FOREIGN KEY (account) REFERENCES Accounts (account) ON DELETE CASCADE)"); - // For looking up an account by public key - stmt.execute("CREATE INDEX AccountPublicKeyIndex on Accounts (public_key)"); - break; - - case 23: - // Asset Orders - stmt.execute( - "CREATE TABLE AssetOrders (asset_order_id AssetOrderID, creator QortalPublicKey NOT NULL, have_asset_id AssetID NOT NULL, want_asset_id AssetID NOT NULL, " - + "amount QortalAmount NOT NULL, fulfilled QortalAmount NOT NULL, price QortalAmount NOT NULL, " - + "ordered TIMESTAMP WITH TIME ZONE NOT NULL, is_closed BOOLEAN NOT NULL, is_fulfilled BOOLEAN NOT NULL, " - + "PRIMARY KEY (asset_order_id))"); - // For quick matching of orders. is_closed are is_fulfilled included so inactive orders can be filtered out. - stmt.execute("CREATE INDEX AssetOrderMatchingIndex on AssetOrders (have_asset_id, want_asset_id, is_closed, is_fulfilled, price, ordered)"); - // For when a user wants to look up their current/historic orders. is_closed included so user can filter by active/inactive orders. - stmt.execute("CREATE INDEX AssetOrderCreatorIndex on AssetOrders (creator, is_closed)"); - break; - - case 24: - // Asset Trades - stmt.execute("CREATE TABLE AssetTrades (initiating_order_id AssetOrderId NOT NULL, target_order_id AssetOrderId NOT NULL, " - + "amount QortalAmount NOT NULL, price QortalAmount NOT NULL, traded TIMESTAMP WITH TIME ZONE NOT NULL)"); - // For looking up historic trades based on orders - stmt.execute("CREATE INDEX AssetTradeBuyOrderIndex on AssetTrades (initiating_order_id, traded)"); - stmt.execute("CREATE INDEX AssetTradeSellOrderIndex on AssetTrades (target_order_id, traded)"); - break; - - case 25: - // Polls/Voting - stmt.execute( - "CREATE TABLE Polls (poll_name PollName, description GenericDescription NOT NULL, creator QortalPublicKey NOT NULL, owner QortalAddress NOT NULL, " - + "published TIMESTAMP WITH TIME ZONE NOT NULL, " + "PRIMARY KEY (poll_name))"); // Various options available on a poll - stmt.execute("CREATE TABLE PollOptions (poll_name PollName, option_index TINYINT NOT NULL, option_name PollOption, " + stmt.execute("CREATE TABLE PollOptions (poll_name PollName, option_index PollOptionIndex NOT NULL, option_name PollOption, " + "PRIMARY KEY (poll_name, option_index), FOREIGN KEY (poll_name) REFERENCES Polls (poll_name) ON DELETE CASCADE)"); + // Actual votes cast on a poll by voting users. NOTE: only one vote per user supported at this time. stmt.execute("CREATE TABLE PollVotes (poll_name PollName, voter QortalPublicKey, option_index PollOptionIndex NOT NULL, " + "PRIMARY KEY (poll_name, voter), FOREIGN KEY (poll_name) REFERENCES Polls (poll_name) ON DELETE CASCADE)"); - // For when a user wants to lookup poll they own - stmt.execute("CREATE INDEX PollOwnerIndex on Polls (owner)"); + + // Create Poll Transactions + stmt.execute("CREATE TABLE CreatePollTransactions (signature Signature, creator QortalPublicKey NOT NULL, owner QortalAddress NOT NULL, " + + "poll_name PollName NOT NULL, description GenericDescription NOT NULL, " + TRANSACTION_KEYS + ")"); + + // Poll options. NB: option is implicitly NON NULL and UNIQUE due to being part of compound primary key + stmt.execute("CREATE TABLE CreatePollTransactionOptions (signature Signature, option_index PollOptionIndex NOT NULL, option_name PollOption, " + + "PRIMARY KEY (signature, option_index), FOREIGN KEY (signature) REFERENCES CreatePollTransactions (signature) ON DELETE CASCADE)"); + // For the future: add flag to polls to allow one or multiple votes per voter + + // Vote On Poll Transactions + stmt.execute("CREATE TABLE VoteOnPollTransactions (signature Signature, voter QortalPublicKey NOT NULL, poll_name PollName NOT NULL, " + + "option_index PollOptionIndex NOT NULL, previous_option_index PollOptionIndex, " + TRANSACTION_KEYS + ")"); break; - case 26: - // Registered Names - stmt.execute("CREATE TABLE Names (name RegisteredName, data NameData NOT NULL, owner QortalAddress NOT NULL, " - + "registered TIMESTAMP WITH TIME ZONE NOT NULL, updated TIMESTAMP WITH TIME ZONE, reference Signature, is_for_sale BOOLEAN NOT NULL, sale_price QortalAmount, " - + "PRIMARY KEY (name))"); + case 10: + // Assets (including QORT coin itself) + stmt.execute("CREATE TABLE Assets (asset_id AssetID, owner QortalAddress NOT NULL, " + + "asset_name AssetName NOT NULL, description GenericDescription NOT NULL, " + + "quantity BIGINT NOT NULL, is_divisible BOOLEAN NOT NULL, " + + "is_unspendable BOOLEAN NOT NULL DEFAULT FALSE, creation_group_id GroupID NOT NULL DEFAULT 0, " + + "reference Signature NOT NULL, data AssetData NOT NULL DEFAULT '', " + + "PRIMARY KEY (asset_id))"); + // For when a user wants to lookup an asset by name + stmt.execute("CREATE INDEX AssetNameIndex on Assets (asset_name)"); + + // We need a corresponding trigger to make sure new asset_id values are assigned sequentially start from 0 + stmt.execute("CREATE TRIGGER Asset_ID_Trigger BEFORE INSERT ON Assets " + + "REFERENCING NEW ROW AS new_row FOR EACH ROW WHEN (new_row.asset_id IS NULL) " + + "SET new_row.asset_id = (SELECT IFNULL(MAX(asset_id) + 1, 0) FROM Assets)"); + + // Asset Orders + stmt.execute("CREATE TABLE AssetOrders (asset_order_id AssetOrderID, creator QortalPublicKey NOT NULL, " + + "have_asset_id AssetID NOT NULL, want_asset_id AssetID NOT NULL, " + + "amount QortalAmount NOT NULL, fulfilled QortalAmount NOT NULL, price QortalAmount NOT NULL, " + + "ordered_when EpochMillis NOT NULL, is_closed BOOLEAN NOT NULL, is_fulfilled BOOLEAN NOT NULL, " + + "PRIMARY KEY (asset_order_id))"); + // For quick matching of orders. is_closed are is_fulfilled included so inactive orders can be filtered out. + stmt.execute("CREATE INDEX AssetOrderMatchingIndex on AssetOrders (have_asset_id, want_asset_id, is_closed, is_fulfilled, price, ordered_when)"); + // For when a user wants to look up their current/historic orders. is_closed included so user can filter by active/inactive orders. + stmt.execute("CREATE INDEX AssetOrderCreatorIndex on AssetOrders (creator, is_closed)"); + + // Asset Trades + stmt.execute("CREATE TABLE AssetTrades (initiating_order_id AssetOrderId NOT NULL, target_order_id AssetOrderId NOT NULL, " + + "target_amount QortalAmount NOT NULL, initiator_amount QortalAmount NOT NULL, traded_when EpochMillis NOT NULL, " + + "initiator_saving QortalAmount NOT NULL DEFAULT 0)"); + // For looking up historic trades based on orders + stmt.execute("CREATE INDEX AssetTradeBuyOrderIndex on AssetTrades (initiating_order_id, traded_when)"); + stmt.execute("CREATE INDEX AssetTradeSellOrderIndex on AssetTrades (target_order_id, traded_when)"); + + // Issue Asset Transactions + stmt.execute("CREATE TABLE IssueAssetTransactions (signature Signature, issuer QortalPublicKey NOT NULL, owner QortalAddress NOT NULL, " + + "asset_name AssetName NOT NULL, " + + "description GenericDescription NOT NULL, quantity BIGINT NOT NULL, is_divisible BOOLEAN NOT NULL, asset_id AssetID, " + + "is_unspendable BOOLEAN NOT NULL, data AssetData NOT NULL DEFAULT '', " + TRANSACTION_KEYS + ")"); + + // Transfer Asset Transactions + stmt.execute("CREATE TABLE TransferAssetTransactions (signature Signature, sender QortalPublicKey NOT NULL, recipient QortalAddress NOT NULL, " + + "asset_id AssetID NOT NULL, amount QortalAmount NOT NULL," + TRANSACTION_KEYS + ")"); + + // Add support for UPDATE_ASSET transactions + stmt.execute("CREATE TABLE UpdateAssetTransactions (signature Signature, owner QortalPublicKey NOT NULL, asset_id AssetID NOT NULL, " + + "new_owner QortalAddress NOT NULL, new_description GenericDescription NOT NULL, new_data AssetData NOT NULL, " + + "orphan_reference Signature, " + TRANSACTION_KEYS + ")"); + + // Create Asset Order Transactions + stmt.execute("CREATE TABLE CreateAssetOrderTransactions (signature Signature, creator QortalPublicKey NOT NULL, " + + "have_asset_id AssetID NOT NULL, amount QortalAmount NOT NULL, want_asset_id AssetID NOT NULL, price QortalAmount NOT NULL, " + + TRANSACTION_KEYS + ")"); + + // Cancel Asset Order Transactions + stmt.execute("CREATE TABLE CancelAssetOrderTransactions (signature Signature, creator QortalPublicKey NOT NULL, " + + "asset_order_id AssetOrderID NOT NULL, " + TRANSACTION_KEYS + ")"); break; - case 27: + case 11: // CIYAM Automated Transactions - stmt.execute( - "CREATE TABLE ATs (AT_address QortalAddress, creator QortalPublicKey, creation TIMESTAMP WITH TIME ZONE, version INTEGER NOT NULL, " - + "asset_id AssetID NOT NULL, code_bytes ATCode NOT NULL, is_sleeping BOOLEAN NOT NULL, sleep_until_height INTEGER, " - + "is_finished BOOLEAN NOT NULL, had_fatal_error BOOLEAN NOT NULL, is_frozen BOOLEAN NOT NULL, frozen_balance QortalAmount, " - + "PRIMARY key (AT_address))"); + stmt.execute("CREATE TABLE ATs (AT_address QortalAddress, creator QortalPublicKey NOT NULL, created_when EpochMillis NOT NULL, " + + "version INTEGER NOT NULL, asset_id AssetID NOT NULL, code_bytes ATCode NOT NULL, code_hash VARBINARY(32) NOT NULL, " + + "creation_group_id GroupID NOT NULL DEFAULT 0, is_sleeping BOOLEAN NOT NULL, sleep_until_height INTEGER, " + + "is_finished BOOLEAN NOT NULL, had_fatal_error BOOLEAN NOT NULL, is_frozen BOOLEAN NOT NULL, frozen_balance QortalAmount, " + + "PRIMARY key (AT_address))"); // For finding executable ATs, ordered by creation timestamp - stmt.execute("CREATE INDEX ATIndex on ATs (is_finished, creation)"); + stmt.execute("CREATE INDEX ATIndex on ATs (is_finished, created_when)"); // For finding ATs by creator stmt.execute("CREATE INDEX ATCreatorIndex on ATs (creator)"); // AT state on a per-block basis - stmt.execute("CREATE TABLE ATStates (AT_address QortalAddress, height INTEGER NOT NULL, creation TIMESTAMP WITH TIME ZONE, " - + "state_data ATState, state_hash ATStateHash NOT NULL, fees QortalAmount NOT NULL, " + stmt.execute("CREATE TABLE ATStates (AT_address QortalAddress, height INTEGER NOT NULL, created_when EpochMillis NOT NULL, " + + "state_data ATState, state_hash ATStateHash NOT NULL, fees QortalAmount NOT NULL, is_initial BOOLEAN NOT NULL, " + "PRIMARY KEY (AT_address, height), FOREIGN KEY (AT_address) REFERENCES ATs (AT_address) ON DELETE CASCADE)"); // For finding per-block AT states, ordered by creation timestamp - stmt.execute("CREATE INDEX BlockATStateIndex on ATStates (height, creation)"); + stmt.execute("CREATE INDEX BlockATStateIndex on ATStates (height, created_when)"); + + // Deploy CIYAM AT Transactions + stmt.execute("CREATE TABLE DeployATTransactions (signature Signature, creator QortalPublicKey NOT NULL, AT_name ATName NOT NULL, " + + "description GenericDescription NOT NULL, AT_type ATType NOT NULL, AT_tags ATTags NOT NULL, " + + "creation_bytes ATCreationBytes NOT NULL, amount QortalAmount NOT NULL, asset_id AssetID NOT NULL, AT_address QortalAddress, " + + TRANSACTION_KEYS + ")"); + // For looking up the Deploy AT Transaction based on deployed AT address + stmt.execute("CREATE INDEX DeployATAddressIndex on DeployATTransactions (AT_address)"); // Generated AT Transactions - stmt.execute( - "CREATE TABLE ATTransactions (signature Signature, AT_address QortalAddress NOT NULL, recipient QortalAddress, amount QortalAmount, asset_id AssetID, message ATMessage, " - + "PRIMARY KEY (signature), FOREIGN KEY (signature) REFERENCES Transactions (signature) ON DELETE CASCADE)"); + stmt.execute("CREATE TABLE ATTransactions (signature Signature, AT_address QortalAddress NOT NULL, recipient QortalAddress, " + + "amount QortalAmount, asset_id AssetID, message ATMessage, " + + TRANSACTION_KEYS + ")"); // For finding AT Transactions generated by a specific AT stmt.execute("CREATE INDEX ATTransactionsIndex on ATTransactions (AT_address)"); break; - case 28: - // Account groups - stmt.execute( - "CREATE TABLE Groups (group_id GroupID, owner QortalAddress NOT NULL, group_name GroupName, description GenericDescription NOT NULL, " - + "created TIMESTAMP WITH TIME ZONE NOT NULL, updated TIMESTAMP WITH TIME ZONE, is_open BOOLEAN NOT NULL, " - + "reference Signature, PRIMARY KEY (group_id))"); - // We need a corresponding trigger to make sure new group_id values are assigned sequentially starting from 1 - stmt.execute( - "CREATE TRIGGER Group_ID_Trigger BEFORE INSERT ON Groups REFERENCING NEW ROW AS new_row FOR EACH ROW WHEN (new_row.group_id IS NULL) " - + "SET new_row.group_id = (SELECT IFNULL(MAX(group_id) + 1, 1) FROM Groups)"); + case 12: + // Groups + stmt.execute("CREATE TABLE Groups (group_id GroupID, owner QortalAddress NOT NULL, group_name GroupName, " + + "created_when EpochMillis NOT NULL, updated_when EpochMillis, is_open BOOLEAN NOT NULL, " + + "approval_threshold TINYINT NOT NULL, min_block_delay INTEGER NOT NULL, max_block_delay INTEGER NOT NULL, " + + "reference Signature, creation_group_id GroupID, description GenericDescription NOT NULL, PRIMARY KEY (group_id))"); // For when a user wants to lookup an group by name stmt.execute("CREATE INDEX GroupNameIndex on Groups (group_name)"); // For finding groups by owner stmt.execute("CREATE INDEX GroupOwnerIndex ON Groups (owner)"); + // We need a corresponding trigger to make sure new group_id values are assigned sequentially starting from 1 + stmt.execute("CREATE TRIGGER Group_ID_Trigger BEFORE INSERT ON Groups " + + "REFERENCING NEW ROW AS new_row FOR EACH ROW WHEN (new_row.group_id IS NULL) " + + "SET new_row.group_id = (SELECT IFNULL(MAX(group_id) + 1, 1) FROM Groups)"); + // Admins stmt.execute("CREATE TABLE GroupAdmins (group_id GroupID, admin QortalAddress, reference Signature NOT NULL, " + "PRIMARY KEY (group_id, admin), FOREIGN KEY (group_id) REFERENCES Groups (group_id) ON DELETE CASCADE)"); - // For finding groups that address administrates + // For finding groups by admin address stmt.execute("CREATE INDEX GroupAdminIndex ON GroupAdmins (admin)"); // Members - stmt.execute( - "CREATE TABLE GroupMembers (group_id GroupID, address QortalAddress, joined TIMESTAMP WITH TIME ZONE NOT NULL, reference Signature NOT NULL, " - + "PRIMARY KEY (group_id, address), FOREIGN KEY (group_id) REFERENCES Groups (group_id) ON DELETE CASCADE)"); - // For finding groups that address is member + stmt.execute("CREATE TABLE GroupMembers (group_id GroupID, address QortalAddress, " + + "joined_when EpochMillis NOT NULL, reference Signature NOT NULL, " + + "PRIMARY KEY (group_id, address), FOREIGN KEY (group_id) REFERENCES Groups (group_id) ON DELETE CASCADE)"); + // For finding groups by member address stmt.execute("CREATE INDEX GroupMemberIndex ON GroupMembers (address)"); // Invites stmt.execute("CREATE TABLE GroupInvites (group_id GroupID, inviter QortalAddress, invitee QortalAddress, " - + "expiry TIMESTAMP WITH TIME ZONE NOT NULL, reference Signature, " + + "expires_when EpochMillis NOT NULL, reference Signature, " + "PRIMARY KEY (group_id, invitee), FOREIGN KEY (group_id) REFERENCES Groups (group_id) ON DELETE CASCADE)"); // For finding invites sent by inviter stmt.execute("CREATE INDEX GroupInviteInviterIndex ON GroupInvites (inviter)"); // For finding invites by group stmt.execute("CREATE INDEX GroupInviteInviteeIndex ON GroupInvites (invitee)"); // For expiry maintenance - stmt.execute("CREATE INDEX GroupInviteExpiryIndex ON GroupInvites (expiry)"); + stmt.execute("CREATE INDEX GroupInviteExpiryIndex ON GroupInvites (expires_when)"); // Pending "join requests" - stmt.execute( - "CREATE TABLE GroupJoinRequests (group_id GroupID, joiner QortalAddress, reference Signature NOT NULL, PRIMARY KEY (group_id, joiner))"); + stmt.execute("CREATE TABLE GroupJoinRequests (group_id GroupID, joiner QortalAddress, reference Signature NOT NULL, " + + "PRIMARY KEY (group_id, joiner))"); // Bans - // NULL expiry means does not expire! - stmt.execute( - "CREATE TABLE GroupBans (group_id GroupID, offender QortalAddress, admin QortalAddress NOT NULL, banned TIMESTAMP WITH TIME ZONE NOT NULL, " - + "reason GenericDescription NOT NULL, expiry TIMESTAMP WITH TIME ZONE, reference Signature NOT NULL, " - + "PRIMARY KEY (group_id, offender), FOREIGN KEY (group_id) REFERENCES Groups (group_id) ON DELETE CASCADE)"); + // NULL expires_when means does not expire! + stmt.execute("CREATE TABLE GroupBans (group_id GroupID, offender QortalAddress, admin QortalAddress NOT NULL, " + + "banned_when EpochMillis NOT NULL, reason GenericDescription NOT NULL, expires_when EpochMillis, reference Signature NOT NULL, " + + "PRIMARY KEY (group_id, offender), FOREIGN KEY (group_id) REFERENCES Groups (group_id) ON DELETE CASCADE)"); // For expiry maintenance - stmt.execute("CREATE INDEX GroupBanExpiryIndex ON GroupBans (expiry)"); + stmt.execute("CREATE INDEX GroupBanExpiryIndex ON GroupBans (expires_when)"); break; - case 29: - // Account group transactions + case 13: + // Group transactions + // Create group stmt.execute("CREATE TABLE CreateGroupTransactions (signature Signature, creator QortalPublicKey NOT NULL, group_name GroupName NOT NULL, " - + "owner QortalAddress NOT NULL, description GenericDescription NOT NULL, is_open BOOLEAN NOT NULL, group_id GroupID, " - + "PRIMARY KEY (signature), FOREIGN KEY (signature) REFERENCES Transactions (signature) ON DELETE CASCADE)"); + + "owner QortalAddress NOT NULL, is_open BOOLEAN NOT NULL, approval_threshold TINYINT NOT NULL, " + + "min_block_delay INTEGER NOT NULL, max_block_delay INTEGER NOT NULL, group_id GroupID, description GenericDescription NOT NULL, " + + TRANSACTION_KEYS + ")"); + + // Update group stmt.execute("CREATE TABLE UpdateGroupTransactions (signature Signature, owner QortalPublicKey NOT NULL, group_id GroupID NOT NULL, " - + "new_owner QortalAddress NOT NULL, new_description GenericDescription NOT NULL, new_is_open BOOLEAN NOT NULL, group_reference Signature, " - + "PRIMARY KEY (signature), FOREIGN KEY (signature) REFERENCES Transactions (signature) ON DELETE CASCADE)"); + + "new_owner QortalAddress NOT NULL, new_is_open BOOLEAN NOT NULL, new_approval_threshold TINYINT NOT NULL, " + + "new_min_block_delay INTEGER NOT NULL, new_max_block_delay INTEGER NOT NULL, " + + "group_reference Signature, new_description GenericDescription NOT NULL, " + TRANSACTION_KEYS + ")"); - // Account group add/remove admin transactions - stmt.execute( - "CREATE TABLE AddGroupAdminTransactions (signature Signature, owner QortalPublicKey NOT NULL, group_id GroupID NOT NULL, address QortalAddress NOT NULL, " - + "PRIMARY KEY (signature), FOREIGN KEY (signature) REFERENCES Transactions (signature) ON DELETE CASCADE)"); - stmt.execute( - "CREATE TABLE RemoveGroupAdminTransactions (signature Signature, owner QortalPublicKey NOT NULL, group_id GroupID NOT NULL, admin QortalAddress NOT NULL, " - + "admin_reference Signature, PRIMARY KEY (signature), FOREIGN KEY (signature) REFERENCES Transactions (signature) ON DELETE CASCADE)"); + // Promote to admin + stmt.execute("CREATE TABLE AddGroupAdminTransactions (signature Signature, owner QortalPublicKey NOT NULL, " + + "group_id GroupID NOT NULL, address QortalAddress NOT NULL, " + TRANSACTION_KEYS + ")"); - // Account group join/leave transactions + // Demote from admin + stmt.execute("CREATE TABLE RemoveGroupAdminTransactions (signature Signature, owner QortalPublicKey NOT NULL, " + + "group_id GroupID NOT NULL, admin QortalAddress NOT NULL, admin_reference Signature, " + + TRANSACTION_KEYS + ")"); + + // Join group stmt.execute("CREATE TABLE JoinGroupTransactions (signature Signature, joiner QortalPublicKey NOT NULL, group_id GroupID NOT NULL, " - + "invite_reference Signature, PRIMARY KEY (signature), FOREIGN KEY (signature) REFERENCES Transactions (signature) ON DELETE CASCADE)"); + + "invite_reference Signature, previous_group_id GroupID, " + TRANSACTION_KEYS + ")"); + + // Leave group stmt.execute("CREATE TABLE LeaveGroupTransactions (signature Signature, leaver QortalPublicKey NOT NULL, group_id GroupID NOT NULL, " - + "member_reference Signature, admin_reference Signature, " - + "PRIMARY KEY (signature), FOREIGN KEY (signature) REFERENCES Transactions (signature) ON DELETE CASCADE)"); + + "member_reference Signature, admin_reference Signature, " + TRANSACTION_KEYS + ")"); - // Account group kick transaction - stmt.execute( - "CREATE TABLE GroupKickTransactions (signature Signature, admin QortalPublicKey NOT NULL, group_id GroupID NOT NULL, address QortalAddress NOT NULL, " - + "reason GroupReason, member_reference Signature, admin_reference Signature, join_reference Signature, " - + "PRIMARY KEY (signature), FOREIGN KEY (signature) REFERENCES Transactions (signature) ON DELETE CASCADE)"); + // Kick from group + stmt.execute("CREATE TABLE GroupKickTransactions (signature Signature, admin QortalPublicKey NOT NULL, " + + "group_id GroupID NOT NULL, address QortalAddress NOT NULL, reason GroupReason, previous_group_id GroupID, " + + "member_reference Signature, admin_reference Signature, join_reference Signature, " + TRANSACTION_KEYS + ")"); + + // Invite to group + stmt.execute("CREATE TABLE GroupInviteTransactions (signature Signature, admin QortalPublicKey NOT NULL, group_id GroupID NOT NULL, " + + "invitee QortalAddress NOT NULL, time_to_live INTEGER NOT NULL, join_reference Signature, previous_group_id GroupID, " + + TRANSACTION_KEYS + ")"); - // Account group invite/cancel-invite transactions - stmt.execute( - "CREATE TABLE GroupInviteTransactions (signature Signature, admin QortalPublicKey NOT NULL, group_id GroupID NOT NULL, invitee QortalAddress NOT NULL, " - + "time_to_live INTEGER NOT NULL, join_reference Signature, " - + "PRIMARY KEY (signature), FOREIGN KEY (signature) REFERENCES Transactions (signature) ON DELETE CASCADE)"); // Cancel group invite - stmt.execute( - "CREATE TABLE CancelGroupInviteTransactions (signature Signature, admin QortalPublicKey NOT NULL, group_id GroupID NOT NULL, invitee QortalAddress NOT NULL, " - + "invite_reference Signature, PRIMARY KEY (signature), FOREIGN KEY (signature) REFERENCES Transactions (signature) ON DELETE CASCADE)"); + stmt.execute("CREATE TABLE CancelGroupInviteTransactions (signature Signature, admin QortalPublicKey NOT NULL, group_id GroupID NOT NULL, " + + "invitee QortalAddress NOT NULL, invite_reference Signature, " + TRANSACTION_KEYS + ")"); - // Account ban/cancel-ban transactions - stmt.execute( - "CREATE TABLE GroupBanTransactions (signature Signature, admin QortalPublicKey NOT NULL, group_id GroupID NOT NULL, address QortalAddress NOT NULL, " - + "reason GroupReason, time_to_live INTEGER NOT NULL, " - + "member_reference Signature, admin_reference Signature, join_invite_reference Signature, " - + "PRIMARY KEY (signature), FOREIGN KEY (signature) REFERENCES Transactions (signature) ON DELETE CASCADE)"); - stmt.execute( - "CREATE TABLE CancelGroupBanTransactions (signature Signature, admin QortalPublicKey NOT NULL, group_id GroupID NOT NULL, address QortalAddress NOT NULL, " - + "ban_reference Signature, PRIMARY KEY (signature), FOREIGN KEY (signature) REFERENCES Transactions (signature) ON DELETE CASCADE)"); - break; + // Ban from group + stmt.execute("CREATE TABLE GroupBanTransactions (signature Signature, admin QortalPublicKey NOT NULL, group_id GroupID NOT NULL, " + + "address QortalAddress NOT NULL, reason GroupReason, time_to_live INTEGER NOT NULL, previous_group_id GroupID, " + + "member_reference Signature, admin_reference Signature, join_invite_reference Signature, " + + TRANSACTION_KEYS + ")"); - case 30: - // Networking - stmt.execute("CREATE TABLE Peers (hostname VARCHAR(255), port INTEGER, last_connected TIMESTAMP WITH TIME ZONE, last_attempted TIMESTAMP WITH TIME ZONE, " - + "last_height INTEGER, last_misbehaved TIMESTAMP WITH TIME ZONE, PRIMARY KEY (hostname, port))"); - break; + // Unban from group + stmt.execute("CREATE TABLE CancelGroupBanTransactions (signature Signature, admin QortalPublicKey NOT NULL, group_id GroupID NOT NULL, " + + "address QortalAddress NOT NULL, ban_reference Signature, " + TRANSACTION_KEYS + ")"); - case 31: - stmt.execute("SET DATABASE TRANSACTION CONTROL MVCC"); // Use MVCC over default two-phase locking, a-k-a "LOCKS" - break; - - case 32: - // Unified PeerAddress requires peer hostname & port stored as one string - stmt.execute("ALTER TABLE Peers ALTER COLUMN hostname RENAME TO address"); - // Make sure literal IPv6 addresses are enclosed in square brackets. - stmt.execute("UPDATE Peers SET address=CONCAT('[', address, ']') WHERE POSITION(':' IN address) != 0"); - stmt.execute("UPDATE Peers SET address=CONCAT(address, ':', port)"); - // We didn't name the PRIMARY KEY constraint when creating Peers table, so can't easily drop it - // Workaround is to create a new table with new constraint, drop old table, then rename. - stmt.execute("CREATE TABLE PeersTEMP AS (SELECT * FROM Peers) WITH DATA"); - stmt.execute("ALTER TABLE PeersTEMP DROP COLUMN port"); - stmt.execute("ALTER TABLE PeersTEMP ADD PRIMARY KEY (address)"); - stmt.execute("DROP TABLE Peers"); - stmt.execute("ALTER TABLE PeersTEMP RENAME TO Peers"); - break; - - case 33: - // Add groupID to all transactions - groupID 0 is default, which means groupless/no-group - stmt.execute("ALTER TABLE Transactions ADD COLUMN tx_group_id GroupID NOT NULL DEFAULT 0"); - stmt.execute("CREATE INDEX TransactionGroupIndex ON Transactions (tx_group_id)"); - - // Adding approval to group-based transactions - // Default approval threshold is 100% for existing groups but probably of no effect in production - stmt.execute("ALTER TABLE Groups ADD COLUMN approval_threshold TINYINT NOT NULL DEFAULT 100 BEFORE reference"); - stmt.execute("ALTER TABLE CreateGroupTransactions ADD COLUMN approval_threshold TINYINT NOT NULL DEFAULT 100 BEFORE group_id"); - stmt.execute("ALTER TABLE UpdateGroupTransactions ADD COLUMN new_approval_threshold TINYINT NOT NULL DEFAULT 100 BEFORE group_reference"); - - // Approval transactions themselves + // Approval transactions // "pending_signature" contains signature of pending transaction requiring approval // "prior_reference" contains signature of previous approval transaction for orphaning purposes stmt.execute("CREATE TABLE GroupApprovalTransactions (signature Signature, admin QortalPublicKey NOT NULL, pending_signature Signature NOT NULL, approval BOOLEAN NOT NULL, " - + "prior_reference Signature, PRIMARY KEY (signature), FOREIGN KEY (signature) REFERENCES Transactions (signature) ON DELETE CASCADE)"); + + "prior_reference Signature, " + TRANSACTION_KEYS + ")"); + // For finding transactions pending approval, and maybe decision by specific admin + stmt.execute("CREATE INDEX GroupApprovalLatestIndex on GroupApprovalTransactions (pending_signature, admin)"); - // Accounts have a default groupID to be used if transaction's txGroupId is 0 - stmt.execute("ALTER TABLE Accounts add default_group_id GroupID NOT NULL DEFAULT 0"); - break; - - case 34: // SET_GROUP transaction support stmt.execute("CREATE TABLE SetGroupTransactions (signature Signature, default_group_id GroupID NOT NULL, previous_default_group_id GroupID, " - + "PRIMARY KEY (signature), FOREIGN KEY (signature) REFERENCES Transactions (signature) ON DELETE CASCADE)"); + + TRANSACTION_KEYS + ")"); break; - case 35: - // Group-based transaction approval min/max block delay - stmt.execute("ALTER TABLE Groups ADD COLUMN min_block_delay INT NOT NULL DEFAULT 0 BEFORE reference"); - stmt.execute("ALTER TABLE Groups ADD COLUMN max_block_delay INT NOT NULL DEFAULT 1440 BEFORE reference"); - stmt.execute("ALTER TABLE CreateGroupTransactions ADD COLUMN min_block_delay INT NOT NULL DEFAULT 0 BEFORE group_id"); - stmt.execute("ALTER TABLE CreateGroupTransactions ADD COLUMN max_block_delay INT NOT NULL DEFAULT 1440 BEFORE group_id"); - stmt.execute("ALTER TABLE UpdateGroupTransactions ADD COLUMN new_min_block_delay INT NOT NULL DEFAULT 0 BEFORE group_reference"); - stmt.execute("ALTER TABLE UpdateGroupTransactions ADD COLUMN new_max_block_delay INT NOT NULL DEFAULT 1440 BEFORE group_reference"); + case 14: + // Networking + stmt.execute("CREATE TABLE Peers (address VARCHAR(255), last_connected EpochMillis, last_attempted EpochMillis, " + + "last_misbehaved EpochMillis, added_when EpochMillis, added_by VARCHAR(255), PRIMARY KEY (address))"); break; - case 36: - // Adding group-ness to record types that could require approval for their related transactions - // e.g. REGISTER_NAME might require approval and so Names table requires groupID - // Registered Names - stmt.execute("ALTER TABLE Names ADD COLUMN creation_group_id GroupID NOT NULL DEFAULT 0"); - // Assets aren't ever updated so don't need group-ness - // for future use: stmt.execute("ALTER TABLE Assets ADD COLUMN creation_group_id GroupID NOT NULL DEFAULT 0"); - // Polls aren't ever updated, only voted upon using option index so don't need group-ness - // for future use: stmt.execute("ALTER TABLE Polls ADD COLUMN creation_group_id GroupID NOT NULL DEFAULT 0"); - // CIYAM ATs - stmt.execute("ALTER TABLE ATs ADD COLUMN creation_group_id GroupID NOT NULL DEFAULT 0"); - // Groups can be updated but updates require approval from original groupID - stmt.execute("ALTER TABLE Groups ADD COLUMN creation_group_id GroupID NOT NULL DEFAULT 0"); - break; + case 15: + // Reward-shares + // Transaction emitted by minter announcing they are sharing with recipient + stmt.execute("CREATE TABLE RewardShareTransactions (signature Signature, minter_public_key QortalPublicKey NOT NULL, recipient QortalAddress NOT NULL, " + + "reward_share_public_key QortalPublicKey NOT NULL, share_percent RewardSharePercent NOT NULL, previous_share_percent RewardSharePercent, " + + TRANSACTION_KEYS + ")"); - case 37: - // Performance-improving INDEX - stmt.execute("CREATE INDEX IF NOT EXISTS BlockGenerationHeightIndex ON Blocks (generation, height)"); - // Asset orders now have isClosed=true when isFulfilled=true - stmt.execute("UPDATE AssetOrders SET is_closed = TRUE WHERE is_fulfilled = TRUE"); - break; - - case 38: - // Rename asset trade columns for clarity - stmt.execute("ALTER TABLE AssetTrades ALTER COLUMN amount RENAME TO target_amount"); - stmt.execute("ALTER TABLE AssetTrades ALTER COLUMN price RENAME TO initiator_amount"); - // Add support for asset "data" - typically JSON map like registered name data - stmt.execute("CREATE TYPE AssetData AS VARCHAR(4000)"); - stmt.execute("ALTER TABLE Assets ADD data AssetData NOT NULL DEFAULT '' BEFORE reference"); - stmt.execute("ALTER TABLE Assets ADD creation_group_id GroupID NOT NULL DEFAULT 0 BEFORE reference"); - // Add support for asset "data" to ISSUE_ASSET transaction - stmt.execute("ALTER TABLE IssueAssetTransactions ADD data AssetData NOT NULL DEFAULT '' BEFORE asset_id"); - // Add support for UPDATE_ASSET transactions - stmt.execute("CREATE TABLE UpdateAssetTransactions (signature Signature, owner QortalPublicKey NOT NULL, asset_id AssetID NOT NULL, " - + "new_owner QortalAddress NOT NULL, new_description GenericDescription NOT NULL, new_data AssetData NOT NULL, " - + "orphan_reference Signature, PRIMARY KEY (signature), FOREIGN KEY (signature) REFERENCES Transactions (signature) ON DELETE CASCADE)"); - // Correct Assets.reference to use ISSUE_ASSET transaction's signature instead of reference. - // This is to help UPDATE_ASSET orphaning. - stmt.execute("MERGE INTO Assets USING (SELECT asset_id, signature FROM Assets JOIN Transactions USING (reference) JOIN IssueAssetTransactions USING (signature)) AS Updates " - + "ON Assets.asset_id = Updates.asset_id WHEN MATCHED THEN UPDATE SET Assets.reference = Updates.signature"); - break; - - case 39: - // Support for automatically setting joiner's default groupID when they join a group (by JOIN_GROUP or corresponding admin's INVITE_GROUP) - stmt.execute("ALTER TABLE JoinGroupTransactions ADD previous_group_id INTEGER"); - stmt.execute("ALTER TABLE GroupInviteTransactions ADD previous_group_id INTEGER"); - // Ditto for leaving - stmt.execute("ALTER TABLE LeaveGroupTransactions ADD previous_group_id INTEGER"); - stmt.execute("ALTER TABLE GroupKickTransactions ADD previous_group_id INTEGER"); - stmt.execute("ALTER TABLE GroupBanTransactions ADD previous_group_id INTEGER"); - break; - - case 40: - // Increase asset "data" size from 4K to 400K - stmt.execute("CREATE TYPE AssetDataLob AS CLOB(400K)"); - stmt.execute("ALTER TABLE Assets ALTER COLUMN data AssetDataLob"); - stmt.execute("ALTER TABLE IssueAssetTransactions ALTER COLUMN data AssetDataLob"); - stmt.execute("ALTER TABLE UpdateAssetTransactions ALTER COLUMN new_data AssetDataLob"); - break; - - case 41: - // New asset pricing - /* - * We store "unit price" for asset orders but need enough precision to accurately - * represent fractional values without loss. - * Asset quantities can be up to either 1_000_000_000_000_000_000 (19 digits) if indivisible, - * or 10_000_000_000.00000000 (11+8 = 19 digits) if divisible. - * Two 19-digit numbers need 38 integer and 38 fractional to cover extremes of unit price. - * However, we use another 10 more fractional digits to avoid rounding issues. - * 38 integer + 48 fractional gives 86, so: DECIMAL (86, 48) - */ - // Rename price to unit_price to preserve indexes - stmt.execute("ALTER TABLE AssetOrders ALTER COLUMN price RENAME TO unit_price"); - // Adjust precision - stmt.execute("ALTER TABLE AssetOrders ALTER COLUMN unit_price DECIMAL(76,48)"); - // Add want-amount column - stmt.execute("ALTER TABLE AssetOrders ADD want_amount QortalAmount BEFORE unit_price"); - // Calculate want-amount values - stmt.execute("UPDATE AssetOrders set want_amount = amount * unit_price"); - // want-amounts all set, so disallow NULL - stmt.execute("ALTER TABLE AssetOrders ALTER COLUMN want_amount SET NOT NULL"); - // Rename corresponding column in CreateAssetOrderTransactions - stmt.execute("ALTER TABLE CreateAssetOrderTransactions ALTER COLUMN price RENAME TO want_amount"); - break; - - case 42: - // New asset pricing #2 - /* - * Use "price" (discard want-amount) but enforce pricing units in one direction - * to avoid all the reciprocal and round issues. - */ - stmt.execute("ALTER TABLE CreateAssetOrderTransactions ALTER COLUMN want_amount RENAME TO price"); - stmt.execute("ALTER TABLE AssetOrders DROP COLUMN want_amount"); - stmt.execute("ALTER TABLE AssetOrders ALTER COLUMN unit_price RENAME TO price"); - stmt.execute("ALTER TABLE AssetOrders ALTER COLUMN price QortalAmount"); - /* - * Normalize any 'old' orders to 'new' pricing. - * We must do this so that requesting open orders can be sorted by price. - */ - // Make sure new asset pricing timestamp (used below) is UTC - stmt.execute("SET TIME ZONE INTERVAL '0:00' HOUR TO MINUTE"); - // Normalize amount/fulfilled to asset with highest assetID, BEFORE price correction - stmt.execute("UPDATE AssetOrders SET amount = amount * price, fulfilled = fulfilled * price " - + "WHERE ordered < timestamp(" + 0 /* was BlockChain.getInstance().getNewAssetPricingTimestamp() */ + ") " - + "AND have_asset_id < want_asset_id"); - // Normalize price into lowest-assetID/highest-assetID price-pair, e.g. QORT/asset100 - // Note: HSQLDB uses BigDecimal's dividend.divide(divisor, RoundingMode.DOWN) too - stmt.execute("UPDATE AssetOrders SET price = CAST(1 AS QortalAmount) / price " - + "WHERE ordered < timestamp(" + 0 /* was BlockChain.getInstance().getNewAssetPricingTimestamp() */ + ") " - + "AND have_asset_id < want_asset_id"); - // Revert time zone change above - stmt.execute("SET TIME ZONE LOCAL"); - break; - - case 43: - // More work on 'new' asset pricing - refunds due to price improvement - stmt.execute("ALTER TABLE AssetTrades ADD initiator_saving QortalAmount NOT NULL DEFAULT 0"); - break; - - case 44: - // Account flags - stmt.execute("ALTER TABLE Accounts ADD COLUMN flags INT NOT NULL DEFAULT 0"); - // Corresponding transaction to set/clear flags - stmt.execute("CREATE TABLE AccountFlagsTransactions (signature Signature, creator QortalPublicKey NOT NULL, target QortalAddress NOT NULL, and_mask INT NOT NULL, or_mask INT NOT NULL, xor_mask INT NOT NULL, " - + "previous_flags INT, PRIMARY KEY (signature), FOREIGN KEY (signature) REFERENCES Transactions (signature) ON DELETE CASCADE)"); - break; - - case 45: - // Enabling other accounts to forge - // Transaction to allow one account to enable other account to forge - stmt.execute("CREATE TABLE EnableForgingTransactions (signature Signature, creator QortalPublicKey NOT NULL, target QortalAddress NOT NULL, " - + "PRIMARY KEY (signature), FOREIGN KEY (signature) REFERENCES Transactions (signature) ON DELETE CASCADE)"); - // Modification to accounts to record who enabled them to forge (useful for counting accounts and potentially orphaning) - stmt.execute("ALTER TABLE Accounts ADD COLUMN forging_enabler QortalAddress"); - break; - - case 46: - // Proxy forging - // Transaction emitted by forger announcing they are forging on behalf of recipient - stmt.execute("CREATE TABLE ProxyForgingTransactions (signature Signature, forger QortalPublicKey NOT NULL, recipient QortalAddress NOT NULL, proxy_public_key QortalPublicKey NOT NULL, share RewardSharePercent NOT NULL, " - + "previous_share RewardSharePercent, PRIMARY KEY (signature), FOREIGN KEY (signature) REFERENCES Transactions (signature) ON DELETE CASCADE)"); - // Table of current shares - stmt.execute("CREATE TABLE ProxyForgers (forger QortalPublicKey NOT NULL, recipient QortalAddress NOT NULL, proxy_public_key QortalPublicKey NOT NULL, share RewardSharePercent NOT NULL, " - + "PRIMARY KEY (forger, recipient))"); - // Proxy-forged blocks will contain proxy public key, which will be used to look up block reward sharing, so create index for those lookups - stmt.execute("CREATE INDEX ProxyForgersProxyPublicKeyIndex ON ProxyForgers (proxy_public_key)"); - break; - - case 47: - // Stash of private keys used for generating blocks. These should be proxy keys! - stmt.execute("CREATE TYPE QortalKeySeed AS VARBINARY(32)"); - stmt.execute("CREATE TABLE ForgingAccounts (forger_seed QortalKeySeed NOT NULL, PRIMARY KEY (forger_seed))"); - break; - - case 48: - // Add index to TransactionParticipants to speed up queries - stmt.execute("CREATE INDEX TransactionParticipantsAddressIndex on TransactionParticipants (participant)"); - break; - - case 49: - // Additional peer information - stmt.execute("ALTER TABLE Peers ADD COLUMN last_block_signature BlockSignature BEFORE last_misbehaved"); - stmt.execute("ALTER TABLE Peers ADD COLUMN last_block_timestamp TIMESTAMP WITH TIME ZONE BEFORE last_misbehaved"); - stmt.execute("ALTER TABLE Peers ADD COLUMN last_block_generator QortalPublicKey BEFORE last_misbehaved"); - break; - - case 50: - // Cached block height in Transactions to save loads of JOINs - stmt.execute("ALTER TABLE Transactions ADD COLUMN block_height INT"); - // Add height-based index - stmt.execute("CREATE INDEX TransactionHeightIndex on Transactions (block_height)"); - break; - - case 51: - // Transaction group-approval rework - // Add index to GroupApprovalTransactions - stmt.execute("CREATE INDEX GroupApprovalLatestIndex on GroupApprovalTransactions (pending_signature, admin)"); - // Transaction's approval status (Java enum) stored as tiny integer for efficiency - stmt.execute("ALTER TABLE Transactions ADD COLUMN approval_status TINYINT NOT NULL"); - // For searching transactions based on approval status - stmt.execute("CREATE INDEX TransactionApprovalStatusIndex on Transactions (approval_status, block_height)"); - // Height when/if transaction is finally approved - stmt.execute("ALTER TABLE Transactions ADD COLUMN approval_height INT"); - // For searching transactions based on approval height - stmt.execute("CREATE INDEX TransactionApprovalHeightIndex on Transactions (approval_height)"); - break; - - case 52: - // Arbitrary transactions changes to allow storage of very small payloads locally - stmt.execute("CREATE TYPE ArbitraryData AS VARBINARY(255)"); - stmt.execute("ALTER TABLE ArbitraryTransactions ADD COLUMN is_data_raw BOOLEAN NOT NULL"); - stmt.execute("ALTER TABLE ArbitraryTransactions ALTER COLUMN data_hash ArbitraryData"); - stmt.execute("ALTER TABLE ArbitraryTransactions ALTER COLUMN data_hash RENAME TO data"); - break; - - case 53: - // Change what we store about peers (again) - stmt.execute("ALTER TABLE Peers DROP COLUMN last_block_signature"); - stmt.execute("ALTER TABLE Peers DROP COLUMN last_block_timestamp"); - stmt.execute("ALTER TABLE Peers DROP COLUMN last_block_generator"); - stmt.execute("ALTER TABLE Peers DROP COLUMN last_height"); - stmt.execute("ALTER TABLE Peers ADD COLUMN added_when TIMESTAMP WITH TIME ZONE"); - stmt.execute("ALTER TABLE Peers ADD COLUMN added_by VARCHAR(255)"); - break; - - case 54: - // Account 'level' - stmt.execute("ALTER TABLE Accounts ADD COLUMN initial_level TINYINT NOT NULL DEFAULT 0"); - stmt.execute("ALTER TABLE Accounts ADD COLUMN level TINYINT NOT NULL DEFAULT 0"); - // Corresponding transaction to set level - stmt.execute("CREATE TABLE AccountLevelTransactions (signature Signature, creator QortalPublicKey NOT NULL, target QortalAddress NOT NULL, level INT NOT NULL, " - + "PRIMARY KEY (signature), FOREIGN KEY (signature) REFERENCES Transactions (signature) ON DELETE CASCADE)"); - break; - - case 55: - // Storage of which level 1+ accounts were 'online' for a particular block. Used to distribute block rewards. - stmt.execute("ALTER TABLE Blocks ADD COLUMN online_accounts VARBINARY(1048576)"); - stmt.execute("ALTER TABLE Blocks ADD COLUMN online_accounts_count INT NOT NULL DEFAULT 0"); - stmt.execute("ALTER TABLE Blocks ADD COLUMN online_accounts_timestamp TIMESTAMP WITH TIME ZONE"); - stmt.execute("ALTER TABLE Blocks ADD COLUMN online_accounts_signatures BLOB"); - break; - - case 56: - // Modify assets to support "unspendable" flag so we can implement the representative legacy QORA asset. - stmt.execute("ALTER TABLE Assets ADD COLUMN is_unspendable BOOLEAN NOT NULL DEFAULT FALSE BEFORE creation_group_id"); - stmt.execute("ALTER TABLE IssueAssetTransactions ADD COLUMN is_unspendable BOOLEAN NOT NULL DEFAULT FALSE BEFORE asset_id"); - break; - - case 57: - // Modify accounts to keep track of how many blocks generated - stmt.execute("ALTER TABLE Accounts ADD COLUMN blocks_generated INT NOT NULL DEFAULT 0"); - // Remove forging_enabler - stmt.execute("ALTER TABLE Accounts DROP COLUMN forging_enabler"); - // Remove corresponding ENABLE_FORGING transaction - stmt.execute("DROP TABLE EnableForgingTransactions"); - break; - - case 58: - // Refactoring to unify/clarify block forging/generation/proxy-forging to simply "minting" - // Account-related - stmt.execute("ALTER TABLE Accounts ALTER COLUMN blocks_generated RENAME TO blocks_minted"); - // "proxy-forging" is now "reward-share" - stmt.execute("ALTER TABLE ProxyForgers ALTER COLUMN proxy_public_key RENAME TO reward_share_public_key"); - stmt.execute("ALTER TABLE ProxyForgers ALTER COLUMN forger RENAME TO minter_public_key"); - stmt.execute("ALTER TABLE ProxyForgers ALTER COLUMN share RENAME TO share_percent"); - stmt.execute("ALTER TABLE ProxyForgers RENAME TO RewardShares"); + // Active reward-shares + stmt.execute("CREATE TABLE RewardShares (minter_public_key QortalPublicKey NOT NULL, minter QortalAddress NOT NULL, recipient QortalAddress NOT NULL, " + + "reward_share_public_key QortalPublicKey NOT NULL, share_percent RewardSharePercent NOT NULL, " + + "PRIMARY KEY (minter_public_key, recipient))"); + // For looking up reward-shares based on reward-share public key stmt.execute("CREATE INDEX RewardSharePublicKeyIndex ON RewardShares (reward_share_public_key)"); - stmt.execute("DROP INDEX ProxyForgersProxyPublicKeyIndex"); - // Reward-share transactions - stmt.execute("ALTER TABLE ProxyForgingTransactions ALTER COLUMN forger RENAME TO minter_public_key"); - stmt.execute("ALTER TABLE ProxyForgingTransactions ALTER COLUMN proxy_public_key RENAME TO reward_share_public_key"); - stmt.execute("ALTER TABLE ProxyForgingTransactions ALTER COLUMN share RENAME TO share_percent"); - stmt.execute("ALTER TABLE ProxyForgingTransactions ALTER COLUMN previous_share RENAME TO previous_share_percent"); - stmt.execute("ALTER TABLE ProxyForgingTransactions RENAME TO RewardShareTransactions"); - // Accounts used by BlockMinter - stmt.execute("ALTER TABLE ForgingAccounts ALTER COLUMN forger_seed RENAME TO minter_private_key"); - stmt.execute("ALTER TABLE ForgingAccounts RENAME TO MintingAccounts"); - // Blocks - stmt.execute("ALTER TABLE Blocks ALTER COLUMN generation RENAME TO minted"); - stmt.execute("ALTER TABLE Blocks ALTER COLUMN generator RENAME TO minter"); - stmt.execute("ALTER TABLE Blocks ALTER COLUMN generator_signature RENAME TO minter_signature"); - // Block-indexes - stmt.execute("CREATE INDEX BlockMinterIndex ON Blocks (minter)"); - stmt.execute("DROP INDEX BlockGeneratorIndex"); - stmt.execute("CREATE INDEX BlockMintedHeightIndex ON Blocks (minted, height)"); - stmt.execute("DROP INDEX BlockGenerationHeightIndex"); break; - case 59: - // Keeping track of QORT gained from holding legacy QORA - stmt.execute("CREATE TABLE AccountQortFromQoraInfo (account QortalAddress, final_qort_from_qora QortalAmount, final_block_height INT, " - + "PRIMARY KEY (account), FOREIGN KEY (account) REFERENCES Accounts (account) ON DELETE CASCADE)"); + case 16: + // Stash of private keys used for generating blocks. These should be proxy keys! + stmt.execute("CREATE TABLE MintingAccounts (minter_private_key QortalKeySeed NOT NULL, minter_public_key QortalPublicKey NOT NULL, PRIMARY KEY (minter_private_key))"); break; - 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 QortalAddress, asset_id AssetID, height INT DEFAULT 1, balance QortalAmount 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; - - case 61: - // Rework triggers on AccountBalances as their block-height sub-queries are too slow - stmt.execute("DROP TRIGGER Historic_account_balance_insert_trigger"); - stmt.execute("DROP TRIGGER Historic_account_balance_update_trigger"); - 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(height, 0) + 1 FROM (SELECT height FROM Blocks ORDER BY height DESC LIMIT 1) AS BlockHeights), 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(height, 0) + 1 FROM (SELECT height FROM Blocks ORDER BY height DESC LIMIT 1) AS BlockHeights), new_row.balance) " - + "ON DUPLICATE KEY UPDATE balance = new_row.balance"); - break; - - case 62: - // Rework sub-queries that need to know next block height as currently they fail for genesis block and/or are still too slow - // Table to hold next block height. - stmt.execute("CREATE TABLE NextBlockHeight (height INT NOT NULL)"); - // Initial value - should work for empty DB or populated DB. - stmt.execute("INSERT INTO NextBlockHeight VALUES (SELECT IFNULL(MAX(height), 0) + 1 FROM Blocks)"); - // We use triggers on Blocks to update a simple "next block height" table - String blockUpdateSql = "UPDATE NextBlockHeight SET height = (SELECT height + 1 FROM Blocks ORDER BY height DESC LIMIT 1)"; - stmt.execute("CREATE TRIGGER Next_block_height_insert_trigger AFTER INSERT ON Blocks " + blockUpdateSql); - stmt.execute("CREATE TRIGGER Next_block_height_update_trigger AFTER UPDATE ON Blocks " + blockUpdateSql); - stmt.execute("CREATE TRIGGER Next_block_height_delete_trigger AFTER DELETE ON Blocks " + blockUpdateSql); - // Now update previously slow/broken sub-queries - stmt.execute("DROP TRIGGER Historic_account_balance_insert_trigger"); - stmt.execute("DROP TRIGGER Historic_account_balance_update_trigger"); - 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 height from NextBlockHeight), 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 height from NextBlockHeight), new_row.balance) " - + "ON DUPLICATE KEY UPDATE balance = new_row.balance"); - break; - - case 63: - // Group invites should allow NULL expiry column - stmt.execute("ALTER TABLE GroupInvites ALTER COLUMN expiry SET NULL"); - break; - - case 64: + case 17: // TRANSFER_PRIVS transaction stmt.execute("CREATE TABLE TransferPrivsTransactions (signature Signature, sender QortalPublicKey NOT NULL, recipient QortalAddress NOT NULL, " + "previous_sender_flags INT, previous_recipient_flags INT, " + "previous_sender_blocks_minted_adjustment INT, previous_sender_blocks_minted INT, " - + "PRIMARY KEY (signature), FOREIGN KEY (signature) REFERENCES Transactions (signature) ON DELETE CASCADE)"); - - // Convert Account's "initial_level" to "blocks_minted_adjustment" - stmt.execute("ALTER TABLE Accounts ADD blocks_minted_adjustment INT NOT NULL DEFAULT 0"); - - List blocksByLevel = BlockChain.getInstance().getBlocksNeededByLevel(); - for (int bbli = 0; bbli < blocksByLevel.size(); ++bbli) - stmt.execute("UPDATE Accounts SET blocks_minted_adjustment = " + blocksByLevel.get(bbli) + " WHERE initial_level = " + (bbli + 1)); - - stmt.execute("ALTER TABLE Accounts DROP initial_level"); + + TRANSACTION_KEYS + ")"); break; - case 65: - // Add INDEX to speed up very slow "DELETE FROM HistoricAccountBalances WHERE height >= ?" - stmt.execute("CREATE INDEX IF NOT EXISTS HistoricAccountBalancesHeightIndex ON HistoricAccountBalances (height)"); - break; - - case 66: - // Add CHECK constraint to account balances - stmt.execute("ALTER TABLE AccountBalances ADD CONSTRAINT CheckBalanceNotNegative CHECK (balance >= 0)"); - break; - - case 67: - // Provide external function to convert private keys to public keys - stmt.execute("CREATE FUNCTION Ed25519_private_to_public_key (IN privateKey VARBINARY(32)) RETURNS VARBINARY(32) LANGUAGE JAVA DETERMINISTIC NO SQL EXTERNAL NAME 'CLASSPATH:org.qortal.repository.hsqldb.HSQLDBRepository.ed25519PrivateToPublicKey'"); - - // Cache minting account public keys to save us recalculating them - stmt.execute("ALTER TABLE MintingAccounts ADD minter_public_key QortalPublicKey"); - stmt.execute("UPDATE MintingAccounts SET minter_public_key = Ed25519_private_to_public_key(minter_private_key)"); - stmt.execute("ALTER TABLE MintingAccounts ALTER COLUMN minter_public_key SET NOT NULL"); - - // Provide external function to convert public keys to addresses - stmt.execute("CREATE FUNCTION Ed25519_public_key_to_address (IN privateKey VARBINARY(32)) RETURNS VARCHAR(36) LANGUAGE JAVA DETERMINISTIC NO SQL EXTERNAL NAME 'CLASSPATH:org.qortal.repository.hsqldb.HSQLDBRepository.ed25519PublicKeyToAddress'"); - - // Cache reward-share minting account's address - stmt.execute("ALTER TABLE RewardShares ADD minter QortalAddress BEFORE recipient"); - stmt.execute("UPDATE RewardShares SET minter = Ed25519_public_key_to_address(minter_public_key)"); - stmt.execute("ALTER TABLE RewardShares ALTER COLUMN minter SET NOT NULL"); - break; - - case 68: - // Slow down log fsync() calls from every 500ms to reduce I/O load - stmt.execute("SET FILES WRITE DELAY 5"); // only fsync() every 5 seconds - break; - - case 69: - // Get rid of historic account balances as they simply use up way too much space - stmt.execute("DROP TRIGGER Historic_Account_Balance_Insert_Trigger"); - stmt.execute("DROP TRIGGER Historic_Account_Balance_Update_Trigger"); - stmt.execute("DROP TABLE HistoricAccountBalances"); - // Reclaim space - stmt.execute("CHECKPOINT"); - stmt.execute("CHECKPOINT DEFRAG"); - break; - - case 70: - // Reduce space used for storing online account in Blocks - stmt.execute("ALTER TABLE Blocks ALTER COLUMN online_accounts BLOB(1M)"); - stmt.execute("ALTER TABLE Blocks ALTER COLUMN online_accounts_signatures BLOB(1M)"); - // Reclaim space - stmt.execute("CHECKPOINT"); - stmt.execute("CHECKPOINT DEFRAG"); - break; - - case 71: - // Add flag to AT state data to indicate 'initial deployment state' - stmt.execute("ALTER TABLE ATStates ADD COLUMN is_initial BOOLEAN NOT NULL DEFAULT TRUE"); - break; - - case 72: - // For ATs, add hash of code bytes to allow searching for specific function ATs, e.g. cross-chain trading - stmt.execute("ALTER TABLE ATs ADD COLUMN code_hash VARBINARY(32) NOT NULL BEFORE is_sleeping"); // Assuming something like SHA256 - break; - - case 73: + case 18: // Chat transactions stmt.execute("CREATE TABLE ChatTransactions (signature Signature, sender QortalAddress NOT NULL, nonce INT NOT NULL, recipient QortalAddress, " - + "is_text BOOLEAN NOT NULL, is_encrypted BOOLEAN NOT NULL, data MessageData NOT NULL, " - + "PRIMARY KEY (signature), FOREIGN KEY (signature) REFERENCES Transactions (signature) ON DELETE CASCADE)"); + + "is_text BOOLEAN NOT NULL, is_encrypted BOOLEAN NOT NULL, data MessageData NOT NULL, " + TRANSACTION_KEYS + ")"); break; default: @@ -1000,7 +605,7 @@ public class HSQLDBDatabaseUpdates { } // database was updated - LOGGER.info(String.format("HSQLDB repository updated to version %d", databaseVersion + 1)); + LOGGER.info(() -> String.format("HSQLDB repository updated to version %d", databaseVersion + 1)); return true; } diff --git a/src/main/java/org/qortal/repository/hsqldb/HSQLDBGroupRepository.java b/src/main/java/org/qortal/repository/hsqldb/HSQLDBGroupRepository.java index 257c61c9..460b5641 100644 --- a/src/main/java/org/qortal/repository/hsqldb/HSQLDBGroupRepository.java +++ b/src/main/java/org/qortal/repository/hsqldb/HSQLDBGroupRepository.java @@ -2,9 +2,7 @@ package org.qortal.repository.hsqldb; import java.sql.ResultSet; import java.sql.SQLException; -import java.sql.Timestamp; import java.util.ArrayList; -import java.util.Calendar; import java.util.List; import org.qortal.data.group.GroupAdminData; @@ -29,7 +27,8 @@ public class HSQLDBGroupRepository implements GroupRepository { @Override public GroupData fromGroupId(int groupId) throws DataException { - String sql = "SELECT group_name, owner, description, created, updated, reference, is_open, approval_threshold, min_block_delay, max_block_delay, creation_group_id FROM Groups WHERE group_id = ?"; + String sql = "SELECT group_name, owner, description, created_when, updated_when, reference, is_open, " + + "approval_threshold, min_block_delay, max_block_delay, creation_group_id FROM Groups WHERE group_id = ?"; try (ResultSet resultSet = this.repository.checkedExecute(sql, groupId)) { if (resultSet == null) @@ -38,11 +37,12 @@ public class HSQLDBGroupRepository implements GroupRepository { String groupName = resultSet.getString(1); String owner = resultSet.getString(2); String description = resultSet.getString(3); - long created = resultSet.getTimestamp(4, Calendar.getInstance(HSQLDBRepository.UTC)).getTime(); + long created = resultSet.getLong(4); // Special handling for possibly-NULL "updated" column - Timestamp updatedTimestamp = resultSet.getTimestamp(5, Calendar.getInstance(HSQLDBRepository.UTC)); - Long updated = updatedTimestamp == null ? null : updatedTimestamp.getTime(); + Long updated = resultSet.getLong(5); + if (updated == 0 && resultSet.wasNull()) + updated = null; byte[] reference = resultSet.getBytes(6); boolean isOpen = resultSet.getBoolean(7); @@ -54,7 +54,8 @@ public class HSQLDBGroupRepository implements GroupRepository { int creationGroupId = resultSet.getInt(11); - return new GroupData(groupId, owner, groupName, description, created, updated, isOpen, approvalThreshold, minBlockDelay, maxBlockDelay, reference, creationGroupId); + return new GroupData(groupId, owner, groupName, description, created, updated, isOpen, + approvalThreshold, minBlockDelay, maxBlockDelay, reference, creationGroupId); } catch (SQLException e) { throw new DataException("Unable to fetch group info from repository", e); } @@ -62,7 +63,8 @@ public class HSQLDBGroupRepository implements GroupRepository { @Override public GroupData fromGroupName(String groupName) throws DataException { - String sql = "SELECT group_id, owner, description, created, updated, reference, is_open, approval_threshold, min_block_delay, max_block_delay, creation_group_id FROM Groups WHERE group_name = ?"; + String sql = "SELECT group_id, owner, description, created_when, updated_when, reference, is_open, " + + "approval_threshold, min_block_delay, max_block_delay, creation_group_id FROM Groups WHERE group_name = ?"; try (ResultSet resultSet = this.repository.checkedExecute(sql, groupName)) { if (resultSet == null) @@ -71,11 +73,12 @@ public class HSQLDBGroupRepository implements GroupRepository { int groupId = resultSet.getInt(1); String owner = resultSet.getString(2); String description = resultSet.getString(3); - long created = resultSet.getTimestamp(4, Calendar.getInstance(HSQLDBRepository.UTC)).getTime(); + long created = resultSet.getLong(4); // Special handling for possibly-NULL "updated" column - Timestamp updatedTimestamp = resultSet.getTimestamp(5, Calendar.getInstance(HSQLDBRepository.UTC)); - Long updated = updatedTimestamp == null ? null : updatedTimestamp.getTime(); + Long updated = resultSet.getLong(5); + if (updated == 0 && resultSet.wasNull()) + updated = null; byte[] reference = resultSet.getBytes(6); boolean isOpen = resultSet.getBoolean(7); @@ -87,7 +90,8 @@ public class HSQLDBGroupRepository implements GroupRepository { int creationGroupId = resultSet.getInt(11); - return new GroupData(groupId, owner, groupName, description, created, updated, isOpen, approvalThreshold, minBlockDelay, maxBlockDelay, reference, creationGroupId); + return new GroupData(groupId, owner, groupName, description, created, updated, isOpen, + approvalThreshold, minBlockDelay, maxBlockDelay, reference, creationGroupId); } catch (SQLException e) { throw new DataException("Unable to fetch group info from repository", e); } @@ -114,8 +118,10 @@ public class HSQLDBGroupRepository implements GroupRepository { @Override public List getAllGroups(Integer limit, Integer offset, Boolean reverse) throws DataException { StringBuilder sql = new StringBuilder(512); - sql.append("SELECT group_id, owner, group_name, description, created, updated, reference, is_open, " + + sql.append("SELECT group_id, owner, group_name, description, created_when, updated_when, reference, is_open, " + "approval_threshold, min_block_delay, max_block_delay, creation_group_id FROM Groups ORDER BY group_name"); + if (reverse != null && reverse) sql.append(" DESC"); @@ -132,11 +138,12 @@ public class HSQLDBGroupRepository implements GroupRepository { String owner = resultSet.getString(2); String groupName = resultSet.getString(3); String description = resultSet.getString(4); - long created = resultSet.getTimestamp(5, Calendar.getInstance(HSQLDBRepository.UTC)).getTime(); + long created = resultSet.getLong(5); // Special handling for possibly-NULL "updated" column - Timestamp updatedTimestamp = resultSet.getTimestamp(6, Calendar.getInstance(HSQLDBRepository.UTC)); - Long updated = updatedTimestamp == null ? null : updatedTimestamp.getTime(); + Long updated = resultSet.getLong(6); + if (updated == 0 && resultSet.wasNull()) + updated = null; byte[] reference = resultSet.getBytes(7); boolean isOpen = resultSet.getBoolean(8); @@ -148,7 +155,8 @@ public class HSQLDBGroupRepository implements GroupRepository { int creationGroupId = resultSet.getInt(12); - groups.add(new GroupData(groupId, owner, groupName, description, created, updated, isOpen, approvalThreshold, minBlockDelay, maxBlockDelay, reference, creationGroupId)); + groups.add(new GroupData(groupId, owner, groupName, description, created, updated, isOpen, + approvalThreshold, minBlockDelay, maxBlockDelay, reference, creationGroupId)); } while (resultSet.next()); return groups; @@ -160,8 +168,10 @@ public class HSQLDBGroupRepository implements GroupRepository { @Override public List getGroupsByOwner(String owner, Integer limit, Integer offset, Boolean reverse) throws DataException { StringBuilder sql = new StringBuilder(512); - sql.append("SELECT group_id, group_name, description, created, updated, reference, is_open, " + + sql.append("SELECT group_id, group_name, description, created_when, updated_when, reference, is_open, " + "approval_threshold, min_block_delay, max_block_delay, creation_group_id FROM Groups WHERE owner = ? ORDER BY group_name"); + if (reverse != null && reverse) sql.append(" DESC"); @@ -177,11 +187,12 @@ public class HSQLDBGroupRepository implements GroupRepository { int groupId = resultSet.getInt(1); String groupName = resultSet.getString(2); String description = resultSet.getString(3); - long created = resultSet.getTimestamp(4, Calendar.getInstance(HSQLDBRepository.UTC)).getTime(); + long created = resultSet.getLong(4); // Special handling for possibly-NULL "updated" column - Timestamp updatedTimestamp = resultSet.getTimestamp(5, Calendar.getInstance(HSQLDBRepository.UTC)); - Long updated = updatedTimestamp == null ? null : updatedTimestamp.getTime(); + Long updated = resultSet.getLong(5); + if (updated == 0 && resultSet.wasNull()) + updated = null; byte[] reference = resultSet.getBytes(6); boolean isOpen = resultSet.getBoolean(7); @@ -193,7 +204,8 @@ public class HSQLDBGroupRepository implements GroupRepository { int creationGroupId = resultSet.getInt(11); - groups.add(new GroupData(groupId, owner, groupName, description, created, updated, isOpen, approvalThreshold, minBlockDelay, maxBlockDelay, reference, creationGroupId)); + groups.add(new GroupData(groupId, owner, groupName, description, created, updated, isOpen, + approvalThreshold, minBlockDelay, maxBlockDelay, reference, creationGroupId)); } while (resultSet.next()); return groups; @@ -205,11 +217,13 @@ public class HSQLDBGroupRepository implements GroupRepository { @Override public List getGroupsWithMember(String member, Integer limit, Integer offset, Boolean reverse) throws DataException { StringBuilder sql = new StringBuilder(512); - sql.append("SELECT group_id, owner, group_name, description, created, updated, reference, is_open, " + + sql.append("SELECT group_id, owner, group_name, description, created_when, updated_when, reference, is_open, " + "approval_threshold, min_block_delay, max_block_delay, creation_group_id, admin FROM Groups " + "JOIN GroupMembers USING (group_id) " + "LEFT OUTER JOIN GroupAdmins ON GroupAdmins.group_id = GroupMembers.group_id AND GroupAdmins.admin = GroupMembers.address " + "WHERE address = ? ORDER BY group_name"); + if (reverse != null && reverse) sql.append(" DESC"); @@ -226,11 +240,12 @@ public class HSQLDBGroupRepository implements GroupRepository { String owner = resultSet.getString(2); String groupName = resultSet.getString(3); String description = resultSet.getString(4); - long created = resultSet.getTimestamp(5, Calendar.getInstance(HSQLDBRepository.UTC)).getTime(); + long created = resultSet.getLong(5); // Special handling for possibly-NULL "updated" column - Timestamp updatedTimestamp = resultSet.getTimestamp(6, Calendar.getInstance(HSQLDBRepository.UTC)); - Long updated = updatedTimestamp == null ? null : updatedTimestamp.getTime(); + Long updated = resultSet.getLong(6); + if (updated == 0 && resultSet.wasNull()) + updated = null; byte[] reference = resultSet.getBytes(7); boolean isOpen = resultSet.getBoolean(8); @@ -241,10 +256,13 @@ public class HSQLDBGroupRepository implements GroupRepository { int maxBlockDelay = resultSet.getInt(11); int creationGroupId = resultSet.getInt(12); - resultSet.getString(13); + + resultSet.getString(13); // 'admin' boolean isAdmin = !resultSet.wasNull(); - GroupData groupData = new GroupData(groupId, owner, groupName, description, created, updated, isOpen, approvalThreshold, minBlockDelay, maxBlockDelay, reference, creationGroupId); + GroupData groupData = new GroupData(groupId, owner, groupName, description, created, updated, isOpen, + approvalThreshold, minBlockDelay, maxBlockDelay, reference, creationGroupId); + groupData.setIsAdmin(isAdmin); groups.add(groupData); @@ -260,12 +278,8 @@ public class HSQLDBGroupRepository implements GroupRepository { public void save(GroupData groupData) throws DataException { HSQLDBSaver saveHelper = new HSQLDBSaver("Groups"); - // Special handling for "updated" timestamp - Long updated = groupData.getUpdated(); - Timestamp updatedTimestamp = updated == null ? null : new Timestamp(updated); - saveHelper.bind("group_id", groupData.getGroupId()).bind("owner", groupData.getOwner()).bind("group_name", groupData.getGroupName()) - .bind("description", groupData.getDescription()).bind("created", new Timestamp(groupData.getCreated())).bind("updated", updatedTimestamp) + .bind("description", groupData.getDescription()).bind("created_when", groupData.getCreated()).bind("updated_when", groupData.getUpdated()) .bind("reference", groupData.getReference()).bind("is_open", groupData.getIsOpen()).bind("approval_threshold", groupData.getApprovalThreshold().value) .bind("min_block_delay", groupData.getMinimumBlockDelay()).bind("max_block_delay", groupData.getMaximumBlockDelay()) .bind("creation_group_id", groupData.getCreationGroupId()); @@ -350,7 +364,9 @@ public class HSQLDBGroupRepository implements GroupRepository { @Override public List getGroupAdmins(int groupId, Integer limit, Integer offset, Boolean reverse) throws DataException { StringBuilder sql = new StringBuilder(256); + sql.append("SELECT admin, reference FROM GroupAdmins WHERE group_id = ? ORDER BY admin"); + if (reverse != null && reverse) sql.append(" DESC"); @@ -416,14 +432,14 @@ public class HSQLDBGroupRepository implements GroupRepository { @Override public GroupMemberData getMember(int groupId, String address) throws DataException { - String sql = "SELECT address, joined, reference FROM GroupMembers WHERE group_id = ?"; + String sql = "SELECT address, joined_when, reference FROM GroupMembers WHERE group_id = ?"; try (ResultSet resultSet = this.repository.checkedExecute(sql, groupId)) { if (resultSet == null) return null; String member = resultSet.getString(1); - long joined = resultSet.getTimestamp(2, Calendar.getInstance(HSQLDBRepository.UTC)).getTime(); + long joined = resultSet.getLong(2); byte[] reference = resultSet.getBytes(3); return new GroupMemberData(groupId, member, joined, reference); @@ -444,7 +460,9 @@ public class HSQLDBGroupRepository implements GroupRepository { @Override public List getGroupMembers(int groupId, Integer limit, Integer offset, Boolean reverse) throws DataException { StringBuilder sql = new StringBuilder(256); - sql.append("SELECT address, joined, reference FROM GroupMembers WHERE group_id = ? ORDER BY address"); + + sql.append("SELECT address, joined_when, reference FROM GroupMembers WHERE group_id = ? ORDER BY address"); + if (reverse != null && reverse) sql.append(" DESC"); @@ -458,7 +476,7 @@ public class HSQLDBGroupRepository implements GroupRepository { do { String member = resultSet.getString(1); - long joined = resultSet.getTimestamp(2, Calendar.getInstance(HSQLDBRepository.UTC)).getTime(); + long joined = resultSet.getLong(2); byte[] reference = resultSet.getBytes(3); members.add(new GroupMemberData(groupId, member, joined, reference)); @@ -490,7 +508,7 @@ public class HSQLDBGroupRepository implements GroupRepository { HSQLDBSaver saveHelper = new HSQLDBSaver("GroupMembers"); saveHelper.bind("group_id", groupMemberData.getGroupId()).bind("address", groupMemberData.getMember()) - .bind("joined", new Timestamp(groupMemberData.getJoined())).bind("reference", groupMemberData.getReference()); + .bind("joined_when", groupMemberData.getJoined()).bind("reference", groupMemberData.getReference()); try { saveHelper.execute(this.repository); @@ -512,15 +530,17 @@ public class HSQLDBGroupRepository implements GroupRepository { @Override public GroupInviteData getInvite(int groupId, String invitee) throws DataException { - String sql = "SELECT inviter, expiry, reference FROM GroupInvites WHERE group_id = ? AND invitee = ?"; + String sql = "SELECT inviter, expires_when, reference FROM GroupInvites WHERE group_id = ? AND invitee = ?"; try (ResultSet resultSet = this.repository.checkedExecute(sql, groupId, invitee)) { if (resultSet == null) return null; String inviter = resultSet.getString(1); - Timestamp expiryTimestamp = resultSet.getTimestamp(2, Calendar.getInstance(HSQLDBRepository.UTC)); - Long expiry = expiryTimestamp == null ? null : expiryTimestamp.getTime(); + + Long expiry = resultSet.getLong(2); + if (expiry == 0 && resultSet.wasNull()) + expiry = null; byte[] reference = resultSet.getBytes(3); @@ -542,7 +562,9 @@ public class HSQLDBGroupRepository implements GroupRepository { @Override public List getInvitesByGroupId(int groupId, Integer limit, Integer offset, Boolean reverse) throws DataException { StringBuilder sql = new StringBuilder(256); - sql.append("SELECT inviter, invitee, expiry, reference FROM GroupInvites WHERE group_id = ? ORDER BY invitee"); + + sql.append("SELECT inviter, invitee, expires_when, reference FROM GroupInvites WHERE group_id = ? ORDER BY invitee"); + if (reverse != null && reverse) sql.append(" DESC"); @@ -558,8 +580,9 @@ public class HSQLDBGroupRepository implements GroupRepository { String inviter = resultSet.getString(1); String invitee = resultSet.getString(2); - Timestamp expiryTimestamp = resultSet.getTimestamp(3, Calendar.getInstance(HSQLDBRepository.UTC)); - Long expiry = expiryTimestamp == null ? null : expiryTimestamp.getTime(); + Long expiry = resultSet.getLong(3); + if (expiry == 0 && resultSet.wasNull()) + expiry = null; byte[] reference = resultSet.getBytes(4); @@ -575,7 +598,9 @@ public class HSQLDBGroupRepository implements GroupRepository { @Override public List getInvitesByInvitee(String invitee, Integer limit, Integer offset, Boolean reverse) throws DataException { StringBuilder sql = new StringBuilder(256); - sql.append("SELECT group_id, inviter, expiry, reference FROM GroupInvites WHERE invitee = ? ORDER BY group_id"); + + sql.append("SELECT group_id, inviter, expires_when, reference FROM GroupInvites WHERE invitee = ? ORDER BY group_id"); + if (reverse != null && reverse) sql.append(" DESC"); @@ -591,8 +616,9 @@ public class HSQLDBGroupRepository implements GroupRepository { int groupId = resultSet.getInt(1); String inviter = resultSet.getString(2); - Timestamp expiryTimestamp = resultSet.getTimestamp(3, Calendar.getInstance(HSQLDBRepository.UTC)); - Long expiry = expiryTimestamp == null ? null : expiryTimestamp.getTime(); + Long expiry = resultSet.getLong(3); + if (expiry == 0 && resultSet.wasNull()) + expiry = null; byte[] reference = resultSet.getBytes(4); @@ -609,12 +635,9 @@ public class HSQLDBGroupRepository implements GroupRepository { public void save(GroupInviteData groupInviteData) throws DataException { HSQLDBSaver saveHelper = new HSQLDBSaver("GroupInvites"); - Timestamp expiryTimestamp = null; - if (groupInviteData.getExpiry() != null) - expiryTimestamp = new Timestamp(groupInviteData.getExpiry()); - - saveHelper.bind("group_id", groupInviteData.getGroupId()).bind("inviter", groupInviteData.getInviter()).bind("invitee", groupInviteData.getInvitee()) - .bind("expiry", expiryTimestamp).bind("reference", groupInviteData.getReference()); + saveHelper.bind("group_id", groupInviteData.getGroupId()).bind("inviter", groupInviteData.getInviter()) + .bind("invitee", groupInviteData.getInvitee()).bind("expires_when", groupInviteData.getExpiry()) + .bind("reference", groupInviteData.getReference()); try { saveHelper.execute(this.repository); @@ -662,7 +685,9 @@ public class HSQLDBGroupRepository implements GroupRepository { @Override public List getGroupJoinRequests(int groupId, Integer limit, Integer offset, Boolean reverse) throws DataException { StringBuilder sql = new StringBuilder(256); + sql.append("SELECT joiner, reference FROM GroupJoinRequests WHERE group_id = ? ORDER BY joiner"); + if (reverse != null && reverse) sql.append(" DESC"); @@ -691,8 +716,8 @@ public class HSQLDBGroupRepository implements GroupRepository { public void save(GroupJoinRequestData groupJoinRequestData) throws DataException { HSQLDBSaver saveHelper = new HSQLDBSaver("GroupJoinRequests"); - saveHelper.bind("group_id", groupJoinRequestData.getGroupId()).bind("joiner", groupJoinRequestData.getJoiner()).bind("reference", - groupJoinRequestData.getReference()); + saveHelper.bind("group_id", groupJoinRequestData.getGroupId()).bind("joiner", groupJoinRequestData.getJoiner()) + .bind("reference", groupJoinRequestData.getReference()); try { saveHelper.execute(this.repository); @@ -714,15 +739,16 @@ public class HSQLDBGroupRepository implements GroupRepository { @Override public GroupBanData getBan(int groupId, String offender) throws DataException { - String sql = "SELECT admin, banned, reason, expiry, reference FROM GroupBans WHERE group_id = ? AND offender = ?"; + String sql = "SELECT admin, banned_when, reason, expires_when, reference FROM GroupBans WHERE group_id = ? AND offender = ?"; try (ResultSet resultSet = this.repository.checkedExecute(sql, groupId, offender)) { String admin = resultSet.getString(1); - long banned = resultSet.getTimestamp(2, Calendar.getInstance(HSQLDBRepository.UTC)).getTime(); + long banned = resultSet.getLong(2); String reason = resultSet.getString(3); - Timestamp expiryTimestamp = resultSet.getTimestamp(4, Calendar.getInstance(HSQLDBRepository.UTC)); - Long expiry = expiryTimestamp == null ? null : expiryTimestamp.getTime(); + Long expiry = resultSet.getLong(4); + if (expiry == 0 && resultSet.wasNull()) + expiry = null; byte[] reference = resultSet.getBytes(5); @@ -744,7 +770,9 @@ public class HSQLDBGroupRepository implements GroupRepository { @Override public List getGroupBans(int groupId, Integer limit, Integer offset, Boolean reverse) throws DataException { StringBuilder sql = new StringBuilder(256); - sql.append("SELECT offender, admin, banned, reason, expiry, reference FROM GroupBans WHERE group_id = ? ORDER BY offender"); + + sql.append("SELECT offender, admin, banned_when, reason, expires_when, reference FROM GroupBans WHERE group_id = ? ORDER BY offender"); + if (reverse != null && reverse) sql.append(" DESC"); @@ -759,11 +787,12 @@ public class HSQLDBGroupRepository implements GroupRepository { do { String offender = resultSet.getString(1); String admin = resultSet.getString(2); - long banned = resultSet.getTimestamp(3, Calendar.getInstance(HSQLDBRepository.UTC)).getTime(); + long banned = resultSet.getLong(3); String reason = resultSet.getString(4); - Timestamp expiryTimestamp = resultSet.getTimestamp(5, Calendar.getInstance(HSQLDBRepository.UTC)); - Long expiry = expiryTimestamp == null ? null : expiryTimestamp.getTime(); + Long expiry = resultSet.getLong(5); + if (expiry == 0 && resultSet.wasNull()) + expiry = null; byte[] reference = resultSet.getBytes(6); @@ -780,12 +809,8 @@ public class HSQLDBGroupRepository implements GroupRepository { public void save(GroupBanData groupBanData) throws DataException { HSQLDBSaver saveHelper = new HSQLDBSaver("GroupBans"); - Timestamp expiryTimestamp = null; - if (groupBanData.getExpiry() != null) - expiryTimestamp = new Timestamp(groupBanData.getExpiry()); - saveHelper.bind("group_id", groupBanData.getGroupId()).bind("offender", groupBanData.getOffender()).bind("admin", groupBanData.getAdmin()) - .bind("banned", new Timestamp(groupBanData.getBanned())).bind("reason", groupBanData.getReason()).bind("expiry", expiryTimestamp) + .bind("banned", groupBanData.getBanned()).bind("reason", groupBanData.getReason()).bind("expiry", groupBanData.getExpiry()) .bind("reference", groupBanData.getReference()); try { diff --git a/src/main/java/org/qortal/repository/hsqldb/HSQLDBNameRepository.java b/src/main/java/org/qortal/repository/hsqldb/HSQLDBNameRepository.java index a5b66115..3f6c9030 100644 --- a/src/main/java/org/qortal/repository/hsqldb/HSQLDBNameRepository.java +++ b/src/main/java/org/qortal/repository/hsqldb/HSQLDBNameRepository.java @@ -1,13 +1,8 @@ package org.qortal.repository.hsqldb; -import static org.qortal.repository.hsqldb.HSQLDBRepository.getZonedTimestampMilli; -import static org.qortal.repository.hsqldb.HSQLDBRepository.toOffsetDateTime; - import java.sql.ResultSet; import java.sql.SQLException; -import java.sql.Timestamp; import java.util.ArrayList; -import java.util.Calendar; import java.util.List; import org.qortal.data.naming.NameData; @@ -24,7 +19,7 @@ public class HSQLDBNameRepository implements NameRepository { @Override public NameData fromName(String name) throws DataException { - String sql = "SELECT owner, data, registered, updated, reference, is_for_sale, sale_price, creation_group_id FROM Names WHERE name = ?"; + String sql = "SELECT owner, data, registered_when, updated_when, reference, is_for_sale, sale_price, creation_group_id FROM Names WHERE name = ?"; try (ResultSet resultSet = this.repository.checkedExecute(sql, name)) { if (resultSet == null) @@ -32,11 +27,12 @@ public class HSQLDBNameRepository implements NameRepository { String owner = resultSet.getString(1); String data = resultSet.getString(2); - long registered = resultSet.getTimestamp(3, Calendar.getInstance(HSQLDBRepository.UTC)).getTime(); + long registered = resultSet.getLong(3); // Special handling for possibly-NULL "updated" column - Timestamp updatedTimestamp = resultSet.getTimestamp(4, Calendar.getInstance(HSQLDBRepository.UTC)); - Long updated = updatedTimestamp == null ? null : updatedTimestamp.getTime(); + Long updated = resultSet.getLong(4); + if (updated == 0 && resultSet.wasNull()) + updated = null; byte[] reference = resultSet.getBytes(5); boolean isForSale = resultSet.getBoolean(6); @@ -65,7 +61,9 @@ public class HSQLDBNameRepository implements NameRepository { @Override public List getAllNames(Integer limit, Integer offset, Boolean reverse) throws DataException { StringBuilder sql = new StringBuilder(256); - sql.append("SELECT name, data, owner, registered, updated, reference, is_for_sale, sale_price, creation_group_id FROM Names ORDER BY name"); + + sql.append("SELECT name, data, owner, registered_when, updated_when, reference, is_for_sale, sale_price, creation_group_id FROM Names ORDER BY name"); + if (reverse != null && reverse) sql.append(" DESC"); @@ -81,8 +79,13 @@ public class HSQLDBNameRepository implements NameRepository { String name = resultSet.getString(1); String data = resultSet.getString(2); String owner = resultSet.getString(3); - long registered = getZonedTimestampMilli(resultSet, 4); - Long updated = getZonedTimestampMilli(resultSet, 5); // can be null + long registered = resultSet.getLong(4); + + // Special handling for possibly-NULL "updated" column + Long updated = resultSet.getLong(5); + if (updated == 0 && resultSet.wasNull()) + updated = null; + byte[] reference = resultSet.getBytes(6); boolean isForSale = resultSet.getBoolean(7); @@ -104,7 +107,9 @@ public class HSQLDBNameRepository implements NameRepository { @Override public List getNamesForSale(Integer limit, Integer offset, Boolean reverse) throws DataException { StringBuilder sql = new StringBuilder(512); - sql.append("SELECT name, data, owner, registered, updated, reference, sale_price, creation_group_id FROM Names WHERE is_for_sale = TRUE ORDER BY name"); + + sql.append("SELECT name, data, owner, registered_when, updated_when, reference, sale_price, creation_group_id FROM Names WHERE is_for_sale = TRUE ORDER BY name"); + if (reverse != null && reverse) sql.append(" DESC"); @@ -120,8 +125,13 @@ public class HSQLDBNameRepository implements NameRepository { String name = resultSet.getString(1); String data = resultSet.getString(2); String owner = resultSet.getString(3); - long registered = getZonedTimestampMilli(resultSet, 4); - Long updated = getZonedTimestampMilli(resultSet, 5); // can be null + long registered = resultSet.getLong(4); + + // Special handling for possibly-NULL "updated" column + Long updated = resultSet.getLong(5); + if (updated == 0 && resultSet.wasNull()) + updated = null; + byte[] reference = resultSet.getBytes(6); boolean isForSale = true; @@ -143,7 +153,9 @@ public class HSQLDBNameRepository implements NameRepository { @Override public List getNamesByOwner(String owner, Integer limit, Integer offset, Boolean reverse) throws DataException { StringBuilder sql = new StringBuilder(512); - sql.append("SELECT name, data, registered, updated, reference, is_for_sale, sale_price, creation_group_id FROM Names WHERE owner = ? ORDER BY name"); + + sql.append("SELECT name, data, registered_when, updated_when, reference, is_for_sale, sale_price, creation_group_id FROM Names WHERE owner = ? ORDER BY name"); + if (reverse != null && reverse) sql.append(" DESC"); @@ -158,8 +170,13 @@ public class HSQLDBNameRepository implements NameRepository { do { String name = resultSet.getString(1); String data = resultSet.getString(2); - long registered = getZonedTimestampMilli(resultSet, 3); - Long updated = getZonedTimestampMilli(resultSet, 4); // can be null + long registered = resultSet.getLong(3); + + // Special handling for possibly-NULL "updated" column + Long updated = resultSet.getLong(4); + if (updated == 0 && resultSet.wasNull()) + updated = null; + byte[] reference = resultSet.getBytes(5); boolean isForSale = resultSet.getBoolean(6); @@ -179,14 +196,14 @@ public class HSQLDBNameRepository implements NameRepository { } @Override - public List getRecentNames(long start) throws DataException { + public List getRecentNames(long startTimestamp) throws DataException { String sql = "SELECT name FROM RegisterNameTransactions JOIN Names USING (name) " + "JOIN Transactions USING (signature) " - + "WHERE creation >= ?"; + + "WHERE created_when >= ?"; List names = new ArrayList<>(); - try (ResultSet resultSet = this.repository.checkedExecute(sql, HSQLDBRepository.toOffsetDateTime(start))) { + try (ResultSet resultSet = this.repository.checkedExecute(sql, startTimestamp)) { if (resultSet == null) return names; @@ -207,7 +224,7 @@ public class HSQLDBNameRepository implements NameRepository { HSQLDBSaver saveHelper = new HSQLDBSaver("Names"); saveHelper.bind("owner", nameData.getOwner()).bind("name", nameData.getName()).bind("data", nameData.getData()) - .bind("registered", toOffsetDateTime(nameData.getRegistered())).bind("updated", toOffsetDateTime(nameData.getUpdated())) + .bind("registered_when", nameData.getRegistered()).bind("updated_when", nameData.getUpdated()) .bind("reference", nameData.getReference()) .bind("is_for_sale", nameData.getIsForSale()).bind("sale_price", nameData.getSalePrice()) .bind("creation_group_id", nameData.getCreationGroupId()); diff --git a/src/main/java/org/qortal/repository/hsqldb/HSQLDBNetworkRepository.java b/src/main/java/org/qortal/repository/hsqldb/HSQLDBNetworkRepository.java index ae84c612..9f865160 100644 --- a/src/main/java/org/qortal/repository/hsqldb/HSQLDBNetworkRepository.java +++ b/src/main/java/org/qortal/repository/hsqldb/HSQLDBNetworkRepository.java @@ -33,13 +33,21 @@ public class HSQLDBNetworkRepository implements NetworkRepository { String address = resultSet.getString(1); PeerAddress peerAddress = PeerAddress.fromString(address); - Long lastConnected = HSQLDBRepository.getZonedTimestampMilli(resultSet, 2); + Long lastConnected = resultSet.getLong(2); + if (lastConnected == 0 && resultSet.wasNull()) + lastConnected = null; - Long lastAttempted = HSQLDBRepository.getZonedTimestampMilli(resultSet, 3); + Long lastAttempted = resultSet.getLong(3); + if (lastAttempted == 0 && resultSet.wasNull()) + lastAttempted = null; - Long lastMisbehaved = HSQLDBRepository.getZonedTimestampMilli(resultSet, 4); + Long lastMisbehaved = resultSet.getLong(4); + if (lastMisbehaved == 0 && resultSet.wasNull()) + lastMisbehaved = null; - Long addedWhen = HSQLDBRepository.getZonedTimestampMilli(resultSet, 5); + Long addedWhen = resultSet.getLong(5); + if (addedWhen == 0 && resultSet.wasNull()) + addedWhen = null; String addedBy = resultSet.getString(6); @@ -58,11 +66,9 @@ public class HSQLDBNetworkRepository implements NetworkRepository { public void save(PeerData peerData) throws DataException { HSQLDBSaver saveHelper = new HSQLDBSaver("Peers"); - saveHelper.bind("address", peerData.getAddress().toString()).bind("last_connected", HSQLDBRepository.toOffsetDateTime(peerData.getLastConnected())) - .bind("last_attempted", HSQLDBRepository.toOffsetDateTime(peerData.getLastAttempted())) - .bind("last_misbehaved", HSQLDBRepository.toOffsetDateTime(peerData.getLastMisbehaved())) - .bind("added_when", HSQLDBRepository.toOffsetDateTime(peerData.getAddedWhen())) - .bind("added_by", peerData.getAddedBy()); + saveHelper.bind("address", peerData.getAddress().toString()).bind("last_connected", peerData.getLastConnected()) + .bind("last_attempted", peerData.getLastAttempted()).bind("last_misbehaved", peerData.getLastMisbehaved()) + .bind("added_when", peerData.getAddedWhen()).bind("added_by", peerData.getAddedBy()); try { saveHelper.execute(this.repository); diff --git a/src/main/java/org/qortal/repository/hsqldb/HSQLDBRepository.java b/src/main/java/org/qortal/repository/hsqldb/HSQLDBRepository.java index 9e1796ea..aa365b9c 100644 --- a/src/main/java/org/qortal/repository/hsqldb/HSQLDBRepository.java +++ b/src/main/java/org/qortal/repository/hsqldb/HSQLDBRepository.java @@ -14,22 +14,16 @@ import java.sql.ResultSet; import java.sql.SQLException; import java.sql.Savepoint; import java.sql.Statement; -import java.time.Instant; -import java.time.OffsetDateTime; -import java.time.ZoneOffset; import java.util.ArrayDeque; import java.util.ArrayList; import java.util.Comparator; import java.util.Deque; import java.util.List; -import java.util.TimeZone; import java.util.regex.Matcher; import java.util.regex.Pattern; import org.apache.logging.log4j.LogManager; import org.apache.logging.log4j.Logger; -import org.qortal.account.PrivateKeyAccount; -import org.qortal.crypto.Crypto; import org.qortal.repository.ATRepository; import org.qortal.repository.AccountRepository; import org.qortal.repository.ArbitraryRepository; @@ -51,8 +45,6 @@ public class HSQLDBRepository implements Repository { private static final Logger LOGGER = LogManager.getLogger(HSQLDBRepository.class); - public static final TimeZone UTC = TimeZone.getTimeZone("UTC"); - protected Connection connection; protected Deque savepoints; protected boolean debugState = false; @@ -78,7 +70,7 @@ public class HSQLDBRepository implements Repository { try (ResultSet resultSet = stmt.getResultSet()) { if (resultSet == null || !resultSet.next()) - LOGGER.warn("Unable to fetch session ID from repository"); + throw new DataException("Unable to fetch session ID from repository"); this.sessionId = resultSet.getLong(1); } @@ -442,7 +434,7 @@ public class HSQLDBRepository implements Repository { long queryTime = System.currentTimeMillis() - beforeQuery; if (queryTime > this.slowQueryThreshold) { - LOGGER.info(String.format("HSQLDB query took %d ms: %s", queryTime, sql), new SQLException("slow query")); + LOGGER.info(() -> String.format("HSQLDB query took %d ms: %s", queryTime, sql), new SQLException("slow query")); logStatements(); } @@ -517,7 +509,7 @@ public class HSQLDBRepository implements Repository { long queryTime = System.currentTimeMillis() - beforeQuery; if (queryTime > this.slowQueryThreshold) { - LOGGER.info(String.format("HSQLDB query took %d ms: %s", queryTime, sql), new SQLException("slow query")); + LOGGER.info(() -> String.format("HSQLDB query took %d ms: %s", queryTime, sql), new SQLException("slow query")); logStatements(); } @@ -690,7 +682,7 @@ public class HSQLDBRepository implements Repository { if (this.sqlStatements == null) return; - LOGGER.info(String.format("HSQLDB SQL statements (session %d) leading up to this were:", this.sessionId)); + LOGGER.info(() -> String.format("HSQLDB SQL statements (session %d) leading up to this were:", this.sessionId)); for (String sql : this.sqlStatements) LOGGER.info(sql); @@ -752,42 +744,4 @@ public class HSQLDBRepository implements Repository { } } - // Utility methods - - public static byte[] ed25519PrivateToPublicKey(byte[] privateKey) { - if (privateKey == null) - return null; - - return PrivateKeyAccount.toPublicKey(privateKey); - } - - public static String ed25519PublicKeyToAddress(byte[] publicKey) { - if (publicKey == null) - return null; - - return Crypto.toAddress(publicKey); - } - - /** Converts milliseconds from epoch to OffsetDateTime needed for TIMESTAMP WITH TIME ZONE columns. */ - public static OffsetDateTime toOffsetDateTime(Long timestamp) { - if (timestamp == null) - return null; - - return OffsetDateTime.ofInstant(Instant.ofEpochMilli(timestamp), ZoneOffset.UTC); - } - - /** Converts OffsetDateTime from TIMESTAMP WITH TIME ZONE column to milliseconds from epoch. */ - public static long fromOffsetDateTime(OffsetDateTime offsetDateTime) { - return offsetDateTime.toInstant().toEpochMilli(); - } - - /** Returns TIMESTAMP WITH TIME ZONE column value as milliseconds from epoch, or null. */ - public static Long getZonedTimestampMilli(ResultSet resultSet, int columnIndex) throws SQLException { - OffsetDateTime offsetDateTime = resultSet.getObject(columnIndex, OffsetDateTime.class); - if (offsetDateTime == null) - return null; - - return offsetDateTime.toInstant().toEpochMilli(); - } - } \ No newline at end of file diff --git a/src/main/java/org/qortal/repository/hsqldb/HSQLDBSaver.java b/src/main/java/org/qortal/repository/hsqldb/HSQLDBSaver.java index a029a0f9..8384c22f 100644 --- a/src/main/java/org/qortal/repository/hsqldb/HSQLDBSaver.java +++ b/src/main/java/org/qortal/repository/hsqldb/HSQLDBSaver.java @@ -7,6 +7,7 @@ import java.sql.Timestamp; import java.util.ArrayList; import java.util.Calendar; import java.util.List; +import java.util.TimeZone; /** * Database helper for building, and executing, INSERT INTO ... ON DUPLICATE KEY UPDATE ... statements. @@ -20,6 +21,8 @@ import java.util.List; */ public class HSQLDBSaver { + private final Calendar utcCalendar = Calendar.getInstance(TimeZone.getTimeZone("UTC")); + private String table; private List columns = new ArrayList<>(); @@ -137,8 +140,8 @@ public class HSQLDBSaver { preparedStatement.setBigDecimal(i + this.objects.size() + 1, (BigDecimal) object); } else if (object instanceof Timestamp) { // Special treatment for Timestamps so that they are stored as UTC - preparedStatement.setTimestamp(i + 1, (Timestamp) object, Calendar.getInstance(HSQLDBRepository.UTC)); - preparedStatement.setTimestamp(i + this.objects.size() + 1, (Timestamp) object, Calendar.getInstance(HSQLDBRepository.UTC)); + preparedStatement.setTimestamp(i + 1, (Timestamp) object, utcCalendar); + preparedStatement.setTimestamp(i + this.objects.size() + 1, (Timestamp) object, utcCalendar); } else { preparedStatement.setObject(i + 1, object); preparedStatement.setObject(i + this.objects.size() + 1, object); diff --git a/src/main/java/org/qortal/repository/hsqldb/HSQLDBVotingRepository.java b/src/main/java/org/qortal/repository/hsqldb/HSQLDBVotingRepository.java index f936b6f2..447fbe4c 100644 --- a/src/main/java/org/qortal/repository/hsqldb/HSQLDBVotingRepository.java +++ b/src/main/java/org/qortal/repository/hsqldb/HSQLDBVotingRepository.java @@ -2,9 +2,7 @@ package org.qortal.repository.hsqldb; import java.sql.ResultSet; import java.sql.SQLException; -import java.sql.Timestamp; import java.util.ArrayList; -import java.util.Calendar; import java.util.List; import org.qortal.data.voting.PollData; @@ -25,7 +23,7 @@ public class HSQLDBVotingRepository implements VotingRepository { @Override public PollData fromPollName(String pollName) throws DataException { - String sql = "SELECT description, creator, owner, published FROM Polls WHERE poll_name = ?"; + String sql = "SELECT description, creator, owner, published_when FROM Polls WHERE poll_name = ?"; try (ResultSet resultSet = this.repository.checkedExecute(sql, pollName)) { if (resultSet == null) @@ -34,7 +32,7 @@ public class HSQLDBVotingRepository implements VotingRepository { String description = resultSet.getString(1); byte[] creatorPublicKey = resultSet.getBytes(2); String owner = resultSet.getString(3); - long published = resultSet.getTimestamp(4, Calendar.getInstance(HSQLDBRepository.UTC)).getTime(); + long published = resultSet.getLong(4); String optionsSql = "SELECT option_name FROM PollOptions WHERE poll_name = ? ORDER BY option_index ASC"; try (ResultSet optionsResultSet = this.repository.checkedExecute(optionsSql, pollName)) { @@ -71,7 +69,7 @@ public class HSQLDBVotingRepository implements VotingRepository { HSQLDBSaver saveHelper = new HSQLDBSaver("Polls"); saveHelper.bind("poll_name", pollData.getPollName()).bind("description", pollData.getDescription()).bind("creator", pollData.getCreatorPublicKey()) - .bind("owner", pollData.getOwner()).bind("published", new Timestamp(pollData.getPublished())); + .bind("owner", pollData.getOwner()).bind("published_when", pollData.getPublished()); try { saveHelper.execute(this.repository); diff --git a/src/main/java/org/qortal/repository/hsqldb/transaction/HSQLDBTransactionRepository.java b/src/main/java/org/qortal/repository/hsqldb/transaction/HSQLDBTransactionRepository.java index a4ca30d1..adcc09c3 100644 --- a/src/main/java/org/qortal/repository/hsqldb/transaction/HSQLDBTransactionRepository.java +++ b/src/main/java/org/qortal/repository/hsqldb/transaction/HSQLDBTransactionRepository.java @@ -1,8 +1,5 @@ package org.qortal.repository.hsqldb.transaction; -import static org.qortal.repository.hsqldb.HSQLDBRepository.getZonedTimestampMilli; -import static org.qortal.repository.hsqldb.HSQLDBRepository.toOffsetDateTime; - import static org.qortal.transaction.Transaction.TransactionType.*; import java.lang.reflect.Constructor; @@ -124,7 +121,7 @@ public class HSQLDBTransactionRepository implements TransactionRepository { @Override public TransactionData fromSignature(byte[] signature) throws DataException { - String sql = "SELECT type, reference, creator, creation, fee, tx_group_id, block_height, approval_status, approval_height " + String sql = "SELECT type, reference, creator, created_when, fee, tx_group_id, block_height, approval_status, approval_height " + "FROM Transactions WHERE signature = ?"; try (ResultSet resultSet = this.repository.checkedExecute(sql, signature)) { @@ -135,7 +132,7 @@ public class HSQLDBTransactionRepository implements TransactionRepository { byte[] reference = resultSet.getBytes(2); byte[] creatorPublicKey = resultSet.getBytes(3); - long timestamp = getZonedTimestampMilli(resultSet, 4); + long timestamp = resultSet.getLong(4); Long fee = resultSet.getLong(5); if (fee == 0 && resultSet.wasNull()) @@ -161,7 +158,7 @@ public class HSQLDBTransactionRepository implements TransactionRepository { @Override public TransactionData fromReference(byte[] reference) throws DataException { - String sql = "SELECT type, signature, creator, creation, fee, tx_group_id, block_height, approval_status, approval_height " + String sql = "SELECT type, signature, creator, created_when, fee, tx_group_id, block_height, approval_status, approval_height " + "FROM Transactions WHERE reference = ?"; try (ResultSet resultSet = this.repository.checkedExecute(sql, reference)) { @@ -172,7 +169,7 @@ public class HSQLDBTransactionRepository implements TransactionRepository { byte[] signature = resultSet.getBytes(2); byte[] creatorPublicKey = resultSet.getBytes(3); - long timestamp = getZonedTimestampMilli(resultSet, 4); + long timestamp = resultSet.getLong(4); Long fee = resultSet.getLong(5); if (fee == 0 && resultSet.wasNull()) @@ -410,7 +407,7 @@ public class HSQLDBTransactionRepository implements TransactionRepository { if (hasAddress) { tables.append(" JOIN TransactionParticipants ON TransactionParticipants.signature = Transactions.signature"); - groupBy = " GROUP BY TransactionParticipants.signature, Transactions.creation"; + groupBy = " GROUP BY TransactionParticipants.signature, Transactions.created_when"; signatureColumn = "TransactionParticipants.signature"; } @@ -497,7 +494,7 @@ public class HSQLDBTransactionRepository implements TransactionRepository { if (groupBy != null) sql.append(groupBy); - sql.append(" ORDER BY Transactions.creation"); + sql.append(" ORDER BY Transactions.created_when"); sql.append((reverse == null || !reverse) ? " ASC" : " DESC"); HSQLDBRepository.limitOffsetSql(sql, limit, offset); @@ -553,7 +550,7 @@ public class HSQLDBTransactionRepository implements TransactionRepository { // Enum int value safe to use literally sql.append(ApprovalStatus.APPROVED.value); - sql.append(" ORDER BY creation DESC LIMIT 1"); + sql.append(" ORDER BY created_when DESC LIMIT 1"); try (ResultSet resultSet = this.repository.checkedExecute(sql.toString())) { if (resultSet == null) @@ -620,7 +617,7 @@ public class HSQLDBTransactionRepository implements TransactionRepository { sql.append(" OR AssetOrders.want_asset_id = "); sql.append(assetId); - sql.append(") GROUP BY Transactions.signature, Transactions.creation ORDER BY Transactions.creation"); + sql.append(") GROUP BY Transactions.signature, Transactions.created_when ORDER BY Transactions.created_when"); sql.append((reverse == null || !reverse) ? " ASC" : " DESC"); HSQLDBRepository.limitOffsetSql(sql, limit, offset); @@ -655,7 +652,7 @@ public class HSQLDBTransactionRepository implements TransactionRepository { List bindParams = new ArrayList<>(3); StringBuilder sql = new StringBuilder(1024); - sql.append("SELECT creation, tx_group_id, reference, fee, signature, sender, block_height, approval_status, approval_height, recipient, amount, asset_name " + sql.append("SELECT created_when, tx_group_id, reference, fee, signature, sender, block_height, approval_status, approval_height, recipient, amount, asset_name " + "FROM TransferAssetTransactions JOIN Transactions USING (signature) "); if (address != null) @@ -669,7 +666,7 @@ public class HSQLDBTransactionRepository implements TransactionRepository { bindParams.add(address); } - sql.append(" ORDER by creation "); + sql.append(" ORDER by created_when "); sql.append((reverse == null || !reverse) ? "ASC" : "DESC"); HSQLDBRepository.limitOffsetSql(sql, limit, offset); @@ -681,7 +678,7 @@ public class HSQLDBTransactionRepository implements TransactionRepository { return assetTransfers; do { - long timestamp = getZonedTimestampMilli(resultSet, 1); + long timestamp = resultSet.getLong(1); int txGroupId = resultSet.getInt(2); byte[] reference = resultSet.getBytes(3); long fee = resultSet.getLong(4); @@ -729,7 +726,7 @@ public class HSQLDBTransactionRepository implements TransactionRepository { bindParams = new Object[0]; } - sql.append(" ORDER BY creation"); + sql.append(" ORDER BY created_when"); if (reverse != null && reverse) sql.append(" DESC"); @@ -867,7 +864,7 @@ public class HSQLDBTransactionRepository implements TransactionRepository { String sql = "SELECT signature FROM GroupApprovalTransactions " + "NATURAL JOIN Transactions " + "WHERE pending_signature = ? AND admin = ? AND block_height IS NOT NULL " - + "ORDER BY creation DESC, signature DESC LIMIT 1"; + + "ORDER BY created_when DESC, signature DESC LIMIT 1"; try (ResultSet resultSet = this.repository.checkedExecute(sql, pendingSignature, adminPublicKey)) { if (resultSet == null) @@ -888,7 +885,7 @@ public class HSQLDBTransactionRepository implements TransactionRepository { // Also make sure that GROUP_APPROVAL transaction's admin is still an admin of group // Sub-query SQL to find latest GroupApprovalTransaction relating to passed pending signature - String latestApprovalSql = "SELECT pending_signature, admin, approval, creation, signature FROM GroupApprovalTransactions " + String latestApprovalSql = "SELECT pending_signature, admin, approval, created_when, signature FROM GroupApprovalTransactions " + "NATURAL JOIN Transactions WHERE pending_signature = ? AND block_height IS NOT NULL"; StringBuilder sql = new StringBuilder(1024); @@ -896,7 +893,7 @@ public class HSQLDBTransactionRepository implements TransactionRepository { sql.append(latestApprovalSql); sql.append(") AS GAT LEFT OUTER JOIN ("); sql.append(latestApprovalSql); - sql.append(") AS NewerGAT ON NewerGAT.admin = GAT.admin AND (NewerGAT.creation > GAT.creation OR (NewerGAT.creation = GAT.creation AND NewerGat.signature > GAT.signature)) " + sql.append(") AS NewerGAT ON NewerGAT.admin = GAT.admin AND (NewerGAT.created_when > GAT.created_when OR (NewerGAT.created_when = GAT.created_when AND NewerGat.signature > GAT.signature)) " + "JOIN Transactions AS PendingTransactions ON PendingTransactions.signature = GAT.pending_signature " + "LEFT OUTER JOIN Accounts ON Accounts.public_key = GAT.admin " + "LEFT OUTER JOIN GroupAdmins ON GroupAdmins.admin = Accounts.account AND GroupAdmins.group_id = PendingTransactions.tx_group_id " @@ -935,7 +932,7 @@ public class HSQLDBTransactionRepository implements TransactionRepository { @Override public List getUnconfirmedTransactionSignatures() throws DataException { - String sql = "SELECT signature FROM UnconfirmedTransactions ORDER by creation DESC, signature DESC"; + String sql = "SELECT signature FROM UnconfirmedTransactions ORDER by created_when DESC, signature DESC"; List signatures = new ArrayList<>(); @@ -961,7 +958,7 @@ public class HSQLDBTransactionRepository implements TransactionRepository { StringBuilder sql = new StringBuilder(256); sql.append("SELECT signature FROM UnconfirmedTransactions "); - sql.append("ORDER BY creation"); + sql.append("ORDER BY created_when"); if (reverse != null && reverse) sql.append(" DESC"); @@ -1035,7 +1032,7 @@ public class HSQLDBTransactionRepository implements TransactionRepository { public void unconfirmTransaction(TransactionData transactionData) throws DataException { HSQLDBSaver saver = new HSQLDBSaver("UnconfirmedTransactions"); - saver.bind("signature", transactionData.getSignature()).bind("creation", toOffsetDateTime(transactionData.getTimestamp())); + saver.bind("signature", transactionData.getSignature()).bind("created_when", transactionData.getTimestamp()); try { saver.execute(repository); @@ -1052,8 +1049,8 @@ public class HSQLDBTransactionRepository implements TransactionRepository { saver.bind("signature", transactionData.getSignature()).bind("reference", transactionData.getReference()) .bind("type", transactionData.getType().value) - .bind("creator", transactionData.getCreatorPublicKey()).bind("creation", toOffsetDateTime(transactionData.getTimestamp())) - .bind("fee", transactionData.getFee()).bind("milestone_block", null).bind("tx_group_id", transactionData.getTxGroupId()) + .bind("creator", transactionData.getCreatorPublicKey()).bind("created_when", transactionData.getTimestamp()) + .bind("fee", transactionData.getFee()).bind("tx_group_id", transactionData.getTxGroupId()) .bind("approval_status", transactionData.getApprovalStatus().value); try { diff --git a/src/test/java/org/qortal/test/api/AddressesApiTests.java b/src/test/java/org/qortal/test/api/AddressesApiTests.java index f508a970..c1d28cb6 100644 --- a/src/test/java/org/qortal/test/api/AddressesApiTests.java +++ b/src/test/java/org/qortal/test/api/AddressesApiTests.java @@ -23,6 +23,11 @@ public class AddressesApiTests extends ApiCommon { assertNotNull(this.addressesResource.getAccountInfo(aliceAddress)); } + @Test + public void testGetOnlineAccounts() { + assertNotNull(this.addressesResource.getOnlineAccounts()); + } + @Test public void testGetRewardShares() { assertNotNull(this.addressesResource.getRewardShares(Collections.singletonList(aliceAddress), null, null, null, null, null)); diff --git a/src/test/java/org/qortal/test/api/AdminApiTests.java b/src/test/java/org/qortal/test/api/AdminApiTests.java index c5620e50..4aa2ca3b 100644 --- a/src/test/java/org/qortal/test/api/AdminApiTests.java +++ b/src/test/java/org/qortal/test/api/AdminApiTests.java @@ -26,4 +26,9 @@ public class AdminApiTests extends ApiCommon { assertNotNull(this.adminResource.summary()); } + @Test + public void testGetMintingAccounts() { + assertNotNull(this.adminResource.getMintingAccounts()); + } + } diff --git a/src/test/java/org/qortal/test/api/ArbitraryApiTests.java b/src/test/java/org/qortal/test/api/ArbitraryApiTests.java new file mode 100644 index 00000000..2b80fb51 --- /dev/null +++ b/src/test/java/org/qortal/test/api/ArbitraryApiTests.java @@ -0,0 +1,43 @@ +package org.qortal.test.api; + +import static org.junit.Assert.*; + +import org.junit.Before; +import org.junit.Test; +import org.qortal.api.resource.ArbitraryResource; +import org.qortal.api.resource.TransactionsResource.ConfirmationStatus; +import org.qortal.test.common.ApiCommon; + +public class ArbitraryApiTests extends ApiCommon { + + private ArbitraryResource arbitraryResource; + + @Before + public void buildResource() { + this.arbitraryResource = (ArbitraryResource) ApiCommon.buildResource(ArbitraryResource.class); + } + + @Test + public void testSearch() { + Integer[] startingBlocks = new Integer[] { null, 0, 1, 999999999 }; + Integer[] blockLimits = new Integer[] { null, 0, 1, 999999999 }; + Integer[] txGroupIds = new Integer[] { null, 0, 1, 999999999 }; + Integer[] services = new Integer[] { null, 0, 1, 999999999 }; + String[] addresses = new String[] { null, this.aliceAddress }; + ConfirmationStatus[] confirmationStatuses = new ConfirmationStatus[] { ConfirmationStatus.UNCONFIRMED, ConfirmationStatus.CONFIRMED, ConfirmationStatus.BOTH }; + + for (Integer startBlock : startingBlocks) + for (Integer blockLimit : blockLimits) + for (Integer txGroupId : txGroupIds) + for (Integer service : services) + for (String address : addresses) + for (ConfirmationStatus confirmationStatus : confirmationStatuses) { + if (confirmationStatus != ConfirmationStatus.CONFIRMED && (startBlock != null || blockLimit != null)) + continue; + + assertNotNull(this.arbitraryResource.searchTransactions(startBlock, blockLimit, txGroupId, service, address, confirmationStatus, 20, null, null)); + assertNotNull(this.arbitraryResource.searchTransactions(startBlock, blockLimit, txGroupId, service, address, confirmationStatus, 1, 1, true)); + } + } + +} diff --git a/src/test/java/org/qortal/test/api/AssetsApiTests.java b/src/test/java/org/qortal/test/api/AssetsApiTests.java index cf49ec6a..d678c1ce 100644 --- a/src/test/java/org/qortal/test/api/AssetsApiTests.java +++ b/src/test/java/org/qortal/test/api/AssetsApiTests.java @@ -12,8 +12,12 @@ import org.qortal.api.ApiError; import org.qortal.api.ApiException; import org.qortal.api.resource.AssetsResource; import org.qortal.api.resource.TransactionsResource.ConfirmationStatus; +import org.qortal.repository.Repository; +import org.qortal.repository.RepositoryManager; import org.qortal.repository.AccountRepository.BalanceOrdering; +import org.qortal.repository.DataException; import org.qortal.test.common.ApiCommon; +import org.qortal.test.common.AssetUtils; public class AssetsApiTests extends ApiCommon { @@ -22,12 +26,22 @@ public class AssetsApiTests extends ApiCommon { private AssetsResource assetsResource; @Before - public void buildResource() { + public void buildResource() throws DataException { this.assetsResource = (AssetsResource) ApiCommon.buildResource(AssetsResource.class); + + // Create some dummy data + try (final Repository repository = RepositoryManager.getRepository()) { + // Matching orders, to create a trade + AssetUtils.createOrder(repository, "alice", AssetUtils.goldAssetId, AssetUtils.otherAssetId, 1_00000000L, 1_00000000L); + AssetUtils.createOrder(repository, "bob", AssetUtils.otherAssetId, AssetUtils.goldAssetId, 1_00000000L, 1_00000000L); + + // Open order + AssetUtils.createOrder(repository, "bob", AssetUtils.otherAssetId, AssetUtils.testAssetId, 1_00000000L, 1_00000000L); + } } @Test - public void testGet() { + public void testResource() { assertNotNull(this.assetsResource); } @@ -70,8 +84,8 @@ public class AssetsApiTests extends ApiCommon { @Test public void testGetAssetBalances() { - List addresses = Arrays.asList(aliceAddress, aliceAddress); - List assetIds = Arrays.asList(1L, 2L, 3L); + List addresses = Arrays.asList(aliceAddress, bobAddress); + List assetIds = Arrays.asList(0L, 1L, 2L, 3L); for (BalanceOrdering balanceOrdering : BalanceOrdering.values()) { for (Boolean excludeZero : ALL_BOOLEAN_VALUES) { diff --git a/src/test/java/org/qortal/test/api/BlockApiTests.java b/src/test/java/org/qortal/test/api/BlockApiTests.java index 4d9d5320..09286a01 100644 --- a/src/test/java/org/qortal/test/api/BlockApiTests.java +++ b/src/test/java/org/qortal/test/api/BlockApiTests.java @@ -8,8 +8,16 @@ import java.util.List; import org.junit.Before; import org.junit.Test; +import org.qortal.account.PrivateKeyAccount; import org.qortal.api.resource.BlocksResource; +import org.qortal.block.GenesisBlock; +import org.qortal.repository.DataException; +import org.qortal.repository.Repository; +import org.qortal.repository.RepositoryManager; import org.qortal.test.common.ApiCommon; +import org.qortal.test.common.BlockUtils; +import org.qortal.test.common.Common; +import org.qortal.utils.Base58; public class BlockApiTests extends ApiCommon { @@ -21,24 +29,84 @@ public class BlockApiTests extends ApiCommon { } @Test - public void test() { + public void testResource() { assertNotNull(this.blocksResource); } @Test - public void testGetBlockMinters() { - List addresses = Arrays.asList(aliceAddress, aliceAddress); + public void testGetBlock() throws DataException { + try (final Repository repository = RepositoryManager.getRepository()) { + byte[] signatureBytes = GenesisBlock.getInstance(repository).getSignature(); + String signature = Base58.encode(signatureBytes); - assertNotNull(this.blocksResource.getBlockMinters(Collections.emptyList(), null, null, null)); - assertNotNull(this.blocksResource.getBlockMinters(addresses, null, null, null)); - assertNotNull(this.blocksResource.getBlockMinters(Collections.emptyList(), 1, 1, true)); - assertNotNull(this.blocksResource.getBlockMinters(addresses, 1, 1, true)); + assertNotNull(this.blocksResource.getBlock(signature)); + } } @Test - public void testGetBlockSummariesByMinter() { - assertNotNull(this.blocksResource.getBlockSummariesByMinter(aliceAddress, null, null, null)); - assertNotNull(this.blocksResource.getBlockSummariesByMinter(aliceAddress, 1, 1, true)); + public void testGetBlockTransactions() throws DataException { + try (final Repository repository = RepositoryManager.getRepository()) { + byte[] signatureBytes = GenesisBlock.getInstance(repository).getSignature(); + String signature = Base58.encode(signatureBytes); + + assertNotNull(this.blocksResource.getBlockTransactions(signature, null, null, null)); + assertNotNull(this.blocksResource.getBlockTransactions(signature, 1, 1, true)); + } + } + + @Test + public void testGetHeight() { + assertNotNull(this.blocksResource.getHeight()); + } + + @Test + public void testGetBlockHeight() throws DataException { + try (final Repository repository = RepositoryManager.getRepository()) { + byte[] signatureBytes = GenesisBlock.getInstance(repository).getSignature(); + String signature = Base58.encode(signatureBytes); + + assertNotNull(this.blocksResource.getHeight(signature)); + } + } + + @Test + public void testGetBlockByHeight() { + assertNotNull(this.blocksResource.getByHeight(1)); + } + + @Test + public void testGetBlockByTimestamp() { + assertNotNull(this.blocksResource.getByTimestamp(System.currentTimeMillis())); + } + + @Test + public void testGetBlockRange() { + assertNotNull(this.blocksResource.getBlockRange(1, 1)); + } + + @Test + public void testGetBlockMinters() throws DataException { + try (final Repository repository = RepositoryManager.getRepository()) { + PrivateKeyAccount mintingAccount = Common.getTestAccount(repository, "alice-reward-share"); + BlockUtils.mintBlock(repository); + + List addresses = Arrays.asList(aliceAddress, mintingAccount.getAddress(), bobAddress); + + assertNotNull(this.blocksResource.getBlockMinters(Collections.emptyList(), null, null, null)); + assertNotNull(this.blocksResource.getBlockMinters(addresses, null, null, null)); + assertNotNull(this.blocksResource.getBlockMinters(Collections.emptyList(), 1, 1, true)); + assertNotNull(this.blocksResource.getBlockMinters(addresses, 1, 1, true)); + } + } + + @Test + public void testGetBlockSummariesByMinter() throws DataException { + try (final Repository repository = RepositoryManager.getRepository()) { + BlockUtils.mintBlock(repository); + + assertNotNull(this.blocksResource.getBlockSummariesByMinter(aliceAddress, null, null, null)); + assertNotNull(this.blocksResource.getBlockSummariesByMinter(aliceAddress, 1, 1, true)); + } } } diff --git a/src/test/java/org/qortal/test/api/NamesApiTests.java b/src/test/java/org/qortal/test/api/NamesApiTests.java new file mode 100644 index 00000000..ae7248b4 --- /dev/null +++ b/src/test/java/org/qortal/test/api/NamesApiTests.java @@ -0,0 +1,91 @@ +package org.qortal.test.api; + +import static org.junit.Assert.*; + +import org.junit.Before; +import org.junit.Test; +import org.qortal.account.PrivateKeyAccount; +import org.qortal.api.resource.NamesResource; +import org.qortal.data.transaction.RegisterNameTransactionData; +import org.qortal.data.transaction.SellNameTransactionData; +import org.qortal.data.transaction.TransactionData; +import org.qortal.repository.DataException; +import org.qortal.repository.Repository; +import org.qortal.repository.RepositoryManager; +import org.qortal.test.common.ApiCommon; +import org.qortal.test.common.Common; +import org.qortal.test.common.TransactionUtils; +import org.qortal.test.common.transaction.TestTransaction; + +public class NamesApiTests extends ApiCommon { + + private NamesResource namesResource; + + @Before + public void before() throws DataException { + Common.useDefaultSettings(); + + this.namesResource = (NamesResource) ApiCommon.buildResource(NamesResource.class); + } + + @Test + public void testResource() { + assertNotNull(this.namesResource); + } + + @Test + public void testGetAllNames() { + assertNotNull(this.namesResource.getAllNames(null, null, null)); + assertNotNull(this.namesResource.getAllNames(1, 1, true)); + } + + @Test + public void testGetNamesByAddress() throws DataException { + try (final Repository repository = RepositoryManager.getRepository()) { + // Register-name + PrivateKeyAccount alice = Common.getTestAccount(repository, "alice"); + String name = "test-name"; + + RegisterNameTransactionData transactionData = new RegisterNameTransactionData(TestTransaction.generateBase(alice), alice.getAddress(), name, "{}"); + TransactionUtils.signAndMint(repository, transactionData, alice); + + assertNotNull(this.namesResource.getNamesByAddress(alice.getAddress(), null, null, null)); + assertNotNull(this.namesResource.getNamesByAddress(alice.getAddress(), 1, 1, true)); + } + } + + @Test + public void testGetName() throws DataException { + try (final Repository repository = RepositoryManager.getRepository()) { + // Register-name + PrivateKeyAccount alice = Common.getTestAccount(repository, "alice"); + String name = "test-name"; + + RegisterNameTransactionData transactionData = new RegisterNameTransactionData(TestTransaction.generateBase(alice), alice.getAddress(), name, "{}"); + TransactionUtils.signAndMint(repository, transactionData, alice); + + assertNotNull(this.namesResource.getName(name)); + } + } + + @Test + public void testGetAllAssets() throws DataException { + try (final Repository repository = RepositoryManager.getRepository()) { + // Register-name + PrivateKeyAccount alice = Common.getTestAccount(repository, "alice"); + String name = "test-name"; + long price = 1_23456789L; + + TransactionData transactionData = new RegisterNameTransactionData(TestTransaction.generateBase(alice), alice.getAddress(), name, "{}"); + TransactionUtils.signAndMint(repository, transactionData, alice); + + // Sell-name + transactionData = new SellNameTransactionData(TestTransaction.generateBase(alice), name, price); + TransactionUtils.signAndMint(repository, transactionData, alice); + + assertNotNull(this.namesResource.getNamesForSale(null, null, null)); + assertNotNull(this.namesResource.getNamesForSale(1, 1, true)); + } + } + +} diff --git a/src/test/java/org/qortal/test/common/ApiCommon.java b/src/test/java/org/qortal/test/common/ApiCommon.java index 2a6f0c37..25e6ec53 100644 --- a/src/test/java/org/qortal/test/common/ApiCommon.java +++ b/src/test/java/org/qortal/test/common/ApiCommon.java @@ -24,12 +24,14 @@ public class ApiCommon extends Common { private static final FakeRequest FAKE_REQUEST = new FakeRequest(); public String aliceAddress; + public String bobAddress; @Before public void beforeTests() throws DataException { Common.useDefaultSettings(); this.aliceAddress = Common.getTestAccount(null, "alice").getAddress(); + this.bobAddress = Common.getTestAccount(null, "bob").getAddress(); } public static Object buildResource(Class resourceClass) { diff --git a/src/test/java/org/qortal/test/naming/MiscTests.java b/src/test/java/org/qortal/test/naming/MiscTests.java new file mode 100644 index 00000000..d9cbf6fc --- /dev/null +++ b/src/test/java/org/qortal/test/naming/MiscTests.java @@ -0,0 +1,42 @@ +package org.qortal.test.naming; + +import static org.junit.Assert.*; + +import java.util.List; + +import org.junit.Before; +import org.junit.Test; +import org.qortal.account.PrivateKeyAccount; +import org.qortal.data.transaction.RegisterNameTransactionData; +import org.qortal.repository.DataException; +import org.qortal.repository.Repository; +import org.qortal.repository.RepositoryManager; +import org.qortal.test.common.Common; +import org.qortal.test.common.TransactionUtils; +import org.qortal.test.common.transaction.TestTransaction; + +public class MiscTests extends Common { + + @Before + public void beforeTest() throws DataException { + Common.useDefaultSettings(); + } + + @Test + public void testGetRecentNames() throws DataException { + try (final Repository repository = RepositoryManager.getRepository()) { + // Register-name + PrivateKeyAccount alice = Common.getTestAccount(repository, "alice"); + String name = "test-name"; + + RegisterNameTransactionData transactionData = new RegisterNameTransactionData(TestTransaction.generateBase(alice), alice.getAddress(), name, "{}"); + TransactionUtils.signAndMint(repository, transactionData, alice); + + List recentNames = repository.getNameRepository().getRecentNames(0L); + + assertNotNull(recentNames); + assertFalse(recentNames.isEmpty()); + } + } + +}