diff --git a/src/main/java/org/qora/api/resource/AssetsResource.java b/src/main/java/org/qora/api/resource/AssetsResource.java index f1f37296..05c13d87 100644 --- a/src/main/java/org/qora/api/resource/AssetsResource.java +++ b/src/main/java/org/qora/api/resource/AssetsResource.java @@ -9,7 +9,6 @@ import io.swagger.v3.oas.annotations.parameters.RequestBody; import io.swagger.v3.oas.annotations.responses.ApiResponse; import io.swagger.v3.oas.annotations.tags.Tag; -import java.math.BigDecimal; import java.util.ArrayList; import java.util.List; import java.util.stream.Collectors; @@ -24,7 +23,6 @@ import javax.ws.rs.QueryParam; import javax.ws.rs.core.Context; import javax.ws.rs.core.MediaType; -import org.qora.account.Account; import org.qora.api.ApiError; import org.qora.api.ApiErrors; import org.qora.api.ApiException; @@ -32,11 +30,13 @@ import org.qora.api.ApiExceptionFactory; import org.qora.api.model.AggregatedOrder; import org.qora.api.model.TradeWithOrderInfo; import org.qora.api.resource.TransactionsResource.ConfirmationStatus; +import org.qora.asset.Asset; import org.qora.crypto.Crypto; import org.qora.data.account.AccountBalanceData; import org.qora.data.account.AccountData; import org.qora.data.asset.AssetData; import org.qora.data.asset.OrderData; +import org.qora.data.asset.RecentTradeData; import org.qora.data.asset.TradeData; import org.qora.data.transaction.CancelAssetOrderTransactionData; import org.qora.data.transaction.CreateAssetOrderTransactionData; @@ -140,12 +140,12 @@ public class AssetsResource { } @GET - @Path("/holders/{assetid}") + @Path("/balances") @Operation( - summary = "List holders of an asset", + summary = "Asset balances owned by addresses and/or filtered to subset of assetIDs", + description = "Returns asset balances for these addresses/assetIDs, with balances. At least one address or assetID must be supplied.", responses = { @ApiResponse( - description = "asset holders", content = @Content( array = @ArraySchema( schema = @Schema( @@ -157,20 +157,30 @@ public class AssetsResource { } ) @ApiErrors({ - ApiError.INVALID_CRITERIA, ApiError.INVALID_ASSET_ID, ApiError.REPOSITORY_ISSUE + ApiError.INVALID_ADDRESS, ApiError.INVALID_CRITERIA, ApiError.INVALID_ASSET_ID, ApiError.REPOSITORY_ISSUE }) - public List getAssetHolders(@PathParam("assetid") int assetId, @Parameter( + public List getAssetBalances(@QueryParam("address") List addresses, @QueryParam("assetid") List assetIds, @Parameter( ref = "limit" ) @QueryParam("limit") Integer limit, @Parameter( ref = "offset" ) @QueryParam("offset") Integer offset, @Parameter( ref = "reverse" ) @QueryParam("reverse") Boolean reverse) { + if (addresses.isEmpty() && assetIds.isEmpty()) + throw ApiExceptionFactory.INSTANCE.createException(request, ApiError.INVALID_CRITERIA); + + for (String address : addresses) + if (!Crypto.isValidAddress(address)) + throw ApiExceptionFactory.INSTANCE.createException(request, ApiError.INVALID_ADDRESS); + try (final Repository repository = RepositoryManager.getRepository()) { - if (!repository.getAssetRepository().assetExists(assetId)) - throw ApiExceptionFactory.INSTANCE.createException(request, ApiError.INVALID_ASSET_ID); + for (long assetId : assetIds) + if (!repository.getAssetRepository().assetExists(assetId)) + throw ApiExceptionFactory.INSTANCE.createException(request, ApiError.INVALID_ASSET_ID); - return repository.getAccountRepository().getAssetBalances(assetId, limit, offset, reverse); + return repository.getAccountRepository().getAssetBalances(addresses, assetIds, limit, offset, reverse); + } catch (ApiException e) { + throw e; } catch (DataException e) { throw ApiExceptionFactory.INSTANCE.createException(request, ApiError.REPOSITORY_ISSUE, e); } @@ -269,6 +279,51 @@ public class AssetsResource { } } + @GET + @Path("/trades/recent") + @Operation( + summary = "Most recent asset trades", + description = "Returns list of most recent two asset trades for each assetID passed. Other assetID optional.", + responses = { + @ApiResponse( + description = "asset trades", + content = @Content( + array = @ArraySchema( + schema = @Schema( + implementation = RecentTradeData.class + ) + ) + ) + ) + } + ) + @ApiErrors({ + ApiError.INVALID_ASSET_ID, ApiError.REPOSITORY_ISSUE + }) + public List getRecentTrades(@QueryParam("assetid") List assetIds, @QueryParam("otherassetid") Long otherAssetId, @Parameter( + ref = "limit" + ) @QueryParam("limit") Integer limit, @Parameter( + ref = "offset" + ) @QueryParam("offset") Integer offset, @Parameter( + ref = "reverse" + ) @QueryParam("reverse") Boolean reverse) { + try (final Repository repository = RepositoryManager.getRepository()) { + for (long assetId : assetIds) + if (!repository.getAssetRepository().assetExists(assetId)) + throw ApiExceptionFactory.INSTANCE.createException(request, ApiError.INVALID_ASSET_ID); + + if (otherAssetId == null) + otherAssetId = Asset.QORA; + else + if (!repository.getAssetRepository().assetExists(otherAssetId)) + throw ApiExceptionFactory.INSTANCE.createException(request, ApiError.INVALID_ASSET_ID); + + return repository.getAssetRepository().getRecentTrades(assetIds, otherAssetId, limit, offset, reverse); + } catch (DataException e) { + throw ApiExceptionFactory.INSTANCE.createException(request, ApiError.REPOSITORY_ISSUE, e); + } + } + @GET @Path("/trades/{assetid}/{otherassetid}") @Operation( @@ -411,81 +466,6 @@ public class AssetsResource { } } - @GET - @Path("/address/{address}") - @Operation( - summary = "All assets owned by this address", - description = "Returns the list of assets for this address, with balances.", - responses = { - @ApiResponse( - description = "the list of assets", - content = @Content( - array = @ArraySchema( - schema = @Schema( - implementation = AccountBalanceData.class - ) - ) - ) - ) - } - ) - @ApiErrors({ - ApiError.INVALID_ADDRESS, ApiError.REPOSITORY_ISSUE - }) - public List getOwnedAssets(@PathParam("address") String address, @Parameter( - ref = "limit" - ) @QueryParam("limit") Integer limit, @Parameter( - ref = "offset" - ) @QueryParam("offset") Integer offset, @Parameter( - ref = "reverse" - ) @QueryParam("reverse") Boolean reverse) { - if (!Crypto.isValidAddress(address)) - throw ApiExceptionFactory.INSTANCE.createException(request, ApiError.INVALID_ADDRESS); - - try (final Repository repository = RepositoryManager.getRepository()) { - return repository.getAccountRepository().getAllBalances(address, limit, offset, reverse); - } catch (ApiException e) { - throw e; - } catch (DataException e) { - throw ApiExceptionFactory.INSTANCE.createException(request, ApiError.REPOSITORY_ISSUE, e); - } - } - - @GET - @Path("/balance/{assetid}/{address}") - @Operation( - summary = "Asset-specific balance request", - description = "Returns the confirmed balance of the given address for the given asset key.", - responses = { - @ApiResponse( - description = "the balance", - content = @Content( - mediaType = MediaType.TEXT_PLAIN, - schema = @Schema( - type = "string", - format = "number" - ) - ) - ) - } - ) - @ApiErrors({ - ApiError.INVALID_ADDRESS, ApiError.REPOSITORY_ISSUE - }) - public BigDecimal getAssetBalance(@PathParam("assetid") long assetid, @PathParam("address") String address) { - if (!Crypto.isValidAddress(address)) - throw ApiExceptionFactory.INSTANCE.createException(request, ApiError.INVALID_ADDRESS); - - try (final Repository repository = RepositoryManager.getRepository()) { - Account account = new Account(repository, address); - return account.getConfirmedBalance(assetid); - } catch (ApiException e) { - throw e; - } catch (DataException e) { - throw ApiExceptionFactory.INSTANCE.createException(request, ApiError.REPOSITORY_ISSUE, e); - } - } - @GET @Path("/orders/{address}") @Operation( @@ -555,8 +535,8 @@ public class AssetsResource { @ApiErrors({ ApiError.INVALID_ADDRESS, ApiError.ADDRESS_NO_EXISTS, ApiError.REPOSITORY_ISSUE }) - public List getAccountAssetPairOrders(@PathParam("address") String address, @PathParam("assetid") int assetId, @PathParam("otherassetid") int otherAssetId, @QueryParam("includeClosed") boolean includeClosed, - @QueryParam("includeFulfilled") boolean includeFulfilled, @Parameter( + public List getAccountAssetPairOrders(@PathParam("address") String address, @PathParam("assetid") int assetId, + @PathParam("otherassetid") int otherAssetId, @QueryParam("isClosed") Boolean isClosed, @QueryParam("isFulfilled") Boolean isFulfilled, @Parameter( ref = "limit" ) @QueryParam("limit") Integer limit, @Parameter( ref = "offset" @@ -582,7 +562,7 @@ public class AssetsResource { if (!repository.getAssetRepository().assetExists(otherAssetId)) throw ApiExceptionFactory.INSTANCE.createException(request, ApiError.INVALID_ASSET_ID); - return repository.getAssetRepository().getAccountsOrders(publicKey, assetId, otherAssetId, includeClosed, includeFulfilled, limit, offset, reverse); + return repository.getAssetRepository().getAccountsOrders(publicKey, assetId, otherAssetId, isClosed, isFulfilled, limit, offset, reverse); } catch (ApiException e) { throw e; } catch (DataException e) { diff --git a/src/main/java/org/qora/asset/Trade.java b/src/main/java/org/qora/asset/Trade.java index 94d48c5d..1fe54873 100644 --- a/src/main/java/org/qora/asset/Trade.java +++ b/src/main/java/org/qora/asset/Trade.java @@ -33,11 +33,15 @@ public class Trade { OrderData initiatingOrder = assetRepository.fromOrderId(this.tradeData.getInitiator()); initiatingOrder.setFulfilled(initiatingOrder.getFulfilled().add(tradeData.getPrice())); initiatingOrder.setIsFulfilled(Order.isFulfilled(initiatingOrder)); + // Set isClosed to true if isFulfilled now true + initiatingOrder.setIsClosed(initiatingOrder.getIsFulfilled()); assetRepository.save(initiatingOrder); OrderData targetOrder = assetRepository.fromOrderId(this.tradeData.getTarget()); targetOrder.setFulfilled(targetOrder.getFulfilled().add(tradeData.getAmount())); targetOrder.setIsFulfilled(Order.isFulfilled(targetOrder)); + // Set isClosed to true if isFulfilled now true + targetOrder.setIsClosed(targetOrder.getIsFulfilled()); assetRepository.save(targetOrder); // Actually transfer asset balances @@ -57,11 +61,15 @@ public class Trade { OrderData initiatingOrder = assetRepository.fromOrderId(this.tradeData.getInitiator()); initiatingOrder.setFulfilled(initiatingOrder.getFulfilled().subtract(tradeData.getPrice())); initiatingOrder.setIsFulfilled(Order.isFulfilled(initiatingOrder)); + // Set isClosed to false if isFulfilled now false + initiatingOrder.setIsClosed(initiatingOrder.getIsFulfilled()); assetRepository.save(initiatingOrder); OrderData targetOrder = assetRepository.fromOrderId(this.tradeData.getTarget()); targetOrder.setFulfilled(targetOrder.getFulfilled().subtract(tradeData.getAmount())); targetOrder.setIsFulfilled(Order.isFulfilled(targetOrder)); + // Set isClosed to false if isFulfilled now false + targetOrder.setIsClosed(targetOrder.getIsFulfilled()); assetRepository.save(targetOrder); // Reverse asset transfers diff --git a/src/main/java/org/qora/block/Block.java b/src/main/java/org/qora/block/Block.java index 10947b88..ada5cfc5 100644 --- a/src/main/java/org/qora/block/Block.java +++ b/src/main/java/org/qora/block/Block.java @@ -210,9 +210,6 @@ public class Block { } long timestamp = parentBlock.calcNextBlockTimestamp(version, generatorSignature, generator); - long maximumTimestamp = parentBlock.getBlockData().getTimestamp() + BlockChain.getInstance().getMaxBlockTime(); - if (timestamp > maximumTimestamp) - timestamp = maximumTimestamp; int transactionCount = 0; byte[] transactionsSignature = null; @@ -783,27 +780,9 @@ public class Block { if (this.blockData.getGeneratingBalance().compareTo(parentBlock.calcNextBlockGeneratingBalance()) != 0) return ValidationResult.GENERATING_BALANCE_INCORRECT; - // XXX Block.isValid generator check relaxation?? blockchain config option? - // After maximum block period, then generator checks are relaxed - if (this.blockData.getTimestamp() < parentBlock.getBlockData().getTimestamp() + BlockChain.getInstance().getMaxBlockTime()) { - // Check generator is allowed to forge this block - BigInteger hashValue = this.calcBlockHash(); - BigInteger target = parentBlock.calcGeneratorsTarget(this.generator); - - // Multiply target by guesses - long guesses = (this.blockData.getTimestamp() - parentBlockData.getTimestamp()) / 1000; - BigInteger lowerTarget = target.multiply(BigInteger.valueOf(guesses - 1)); - target = target.multiply(BigInteger.valueOf(guesses)); - - // Generator's target must exceed block's hashValue threshold - if (hashValue.compareTo(target) >= 0) - return ValidationResult.GENERATOR_NOT_ACCEPTED; - - // Odd gen1 comment: "CHECK IF FIRST BLOCK OF USER" - // Each second elapsed allows generator to test a new "target" window against hashValue - if (hashValue.compareTo(lowerTarget) < 0) - return ValidationResult.GENERATOR_NOT_ACCEPTED; - } + // Check generator is allowed to forge this block + if (!isGeneratorValidToForge(parentBlock)) + return ValidationResult.GENERATOR_NOT_ACCEPTED; // CIYAM ATs if (this.blockData.getATCount() != 0) { @@ -816,8 +795,6 @@ public class Block { } else { // Generate local AT states for comparison this.executeATs(); - - // XXX do we need to revalidate signatures if transactions list has changed? } // Check locally generated AT states against ones received from elsewhere @@ -887,7 +864,6 @@ public class Block { } } } catch (DataException e) { - // XXX why was this TRANSACTION_TIMESTAMP_INVALID? return ValidationResult.TRANSACTION_INVALID; } finally { // Rollback repository changes made by test-processing transactions above @@ -960,10 +936,33 @@ public class Block { this.blockData.setTransactionCount(this.blockData.getTransactionCount() + 1); // We've added transactions, so recalculate transactions signature - // XXX surely this breaks Block.isSignatureValid which is called before we are? - // calcTransactionsSignature(); + calcTransactionsSignature(); } + /** Returns whether block's generator is actually allowed to forge this block. */ + protected boolean isGeneratorValidToForge(Block parentBlock) throws DataException { + BlockData parentBlockData = parentBlock.getBlockData(); + + BigInteger hashValue = this.calcBlockHash(); + BigInteger target = parentBlock.calcGeneratorsTarget(this.generator); + + // Multiply target by guesses + long guesses = (this.blockData.getTimestamp() - parentBlockData.getTimestamp()) / 1000; + BigInteger lowerTarget = target.multiply(BigInteger.valueOf(guesses - 1)); + target = target.multiply(BigInteger.valueOf(guesses)); + + // Generator's target must exceed block's hashValue threshold + if (hashValue.compareTo(target) >= 0) + return false; + + // Odd gen1 comment: "CHECK IF FIRST BLOCK OF USER" + // Each second elapsed allows generator to test a new "target" window against hashValue + if (hashValue.compareTo(lowerTarget) < 0) + return false; + + return true; + } + /** * Process block, and its transactions, adding them to the blockchain. * @@ -981,6 +980,9 @@ public class Block { if (blockFee.compareTo(BigDecimal.ZERO) > 0) this.generator.setConfirmedBalance(Asset.QORA, this.generator.getConfirmedBalance(Asset.QORA).add(blockFee)); + // Block rewards go here + processBlockRewards(); + // Process AT fees and save AT states into repository ATRepository atRepository = this.repository.getATRepository(); for (ATStateData atState : this.getATStates()) { @@ -1020,6 +1022,10 @@ public class Block { } } + protected void processBlockRewards() throws DataException { + // NOP for vanilla qora-core + } + /** * Removes block from blockchain undoing transactions and adding them to unconfirmed pile. * @@ -1045,6 +1051,9 @@ public class Block { this.repository.getTransactionRepository().deleteParticipants(transaction.getTransactionData()); } + // Block rewards removed here + orphanBlockRewards(); + // If fees are non-zero then remove fees from generator's balance BigDecimal blockFee = this.blockData.getTotalFees(); if (blockFee.compareTo(BigDecimal.ZERO) > 0) @@ -1065,6 +1074,10 @@ public class Block { this.repository.getBlockRepository().delete(this.blockData); } + protected void orphanBlockRewards() throws DataException { + // NOP for vanilla qora-core + } + /** * Return Qora balance adjusted to within min/max limits. */ diff --git a/src/main/java/org/qora/block/BlockChain.java b/src/main/java/org/qora/block/BlockChain.java index c008ff5d..af313024 100644 --- a/src/main/java/org/qora/block/BlockChain.java +++ b/src/main/java/org/qora/block/BlockChain.java @@ -56,9 +56,9 @@ public class BlockChain { /** Number of blocks between recalculating block's generating balance. */ private int blockDifficultyInterval; - /** Minimum target time between blocks, in milliseconds. */ + /** Minimum target time between blocks, in seconds. */ private long minBlockTime; - /** Maximum target time between blocks, in milliseconds. */ + /** Maximum target time between blocks, in seconds. */ private long maxBlockTime; /** Maximum acceptable timestamp disagreement offset in milliseconds. */ private long blockTimestampMargin; diff --git a/src/main/java/org/qora/data/account/AccountBalanceData.java b/src/main/java/org/qora/data/account/AccountBalanceData.java index 1f1ed3e4..9244a447 100644 --- a/src/main/java/org/qora/data/account/AccountBalanceData.java +++ b/src/main/java/org/qora/data/account/AccountBalanceData.java @@ -13,6 +13,8 @@ public class AccountBalanceData { private String address; private long assetId; private BigDecimal balance; + // Not always present: + private String assetName; // Constructors @@ -20,10 +22,15 @@ public class AccountBalanceData { protected AccountBalanceData() { } - public AccountBalanceData(String address, long assetId, BigDecimal balance) { + public AccountBalanceData(String address, long assetId, BigDecimal balance, String assetName) { this.address = address; this.assetId = assetId; this.balance = balance; + this.assetName = assetName; + } + + public AccountBalanceData(String address, long assetId, BigDecimal balance) { + this(address, assetId, balance, null); } // Getters/Setters @@ -44,4 +51,8 @@ public class AccountBalanceData { this.balance = balance; } + public String getAssetName() { + return this.assetName; + } + } diff --git a/src/main/java/org/qora/data/asset/RecentTradeData.java b/src/main/java/org/qora/data/asset/RecentTradeData.java new file mode 100644 index 00000000..41031e69 --- /dev/null +++ b/src/main/java/org/qora/data/asset/RecentTradeData.java @@ -0,0 +1,65 @@ +package org.qora.data.asset; + +import java.math.BigDecimal; + +import javax.xml.bind.annotation.XmlAccessType; +import javax.xml.bind.annotation.XmlAccessorType; +import javax.xml.bind.annotation.XmlElement; + +import io.swagger.v3.oas.annotations.media.Schema; + +// All properties to be converted to JSON via JAXB +@XmlAccessorType(XmlAccessType.FIELD) +public class RecentTradeData { + + // Properties + private long assetId; + + private long otherAssetId; + + private BigDecimal amount; + + private BigDecimal price; + + @Schema( + description = "when trade happened" + ) + private long timestamp; + + // Constructors + + // necessary for JAXB serialization + protected RecentTradeData() { + } + + public RecentTradeData(long assetId, long otherAssetId, BigDecimal amount, BigDecimal price, long timestamp) { + this.assetId = assetId; + this.otherAssetId = otherAssetId; + this.amount = amount; + this.price = price; + this.timestamp = timestamp; + } + + // Getters/setters + + public long getAssetId() { + return this.assetId; + } + + public long getOtherAssetId() { + return this.otherAssetId; + } + + public BigDecimal getAmount() { + return this.amount; + } + + public BigDecimal getPrice() { + return this.price; + } + + public long getTimestamp() { + return this.timestamp; + } + +} diff --git a/src/main/java/org/qora/repository/AccountRepository.java b/src/main/java/org/qora/repository/AccountRepository.java index eac15023..270df953 100644 --- a/src/main/java/org/qora/repository/AccountRepository.java +++ b/src/main/java/org/qora/repository/AccountRepository.java @@ -45,17 +45,7 @@ public interface AccountRepository { public AccountBalanceData getBalance(String address, long assetId) throws DataException; - public List getAllBalances(String address, Integer limit, Integer offset, Boolean reverse) throws DataException; - - public default List getAllBalances(String address) throws DataException { - return getAllBalances(address, null, null, null); - } - - public List getAssetBalances(long assetId, Integer limit, Integer offset, Boolean reverse) throws DataException; - - public default List getAssetBalances(long assetId) throws DataException { - return getAssetBalances(assetId, null, null, null); - } + public List getAssetBalances(List addresses, List assetIds, Integer limit, Integer offset, Boolean reverse) throws DataException; public void save(AccountBalanceData accountBalanceData) throws DataException; diff --git a/src/main/java/org/qora/repository/AssetRepository.java b/src/main/java/org/qora/repository/AssetRepository.java index 3996e2c6..3963872c 100644 --- a/src/main/java/org/qora/repository/AssetRepository.java +++ b/src/main/java/org/qora/repository/AssetRepository.java @@ -4,6 +4,7 @@ import java.util.List; import org.qora.data.asset.AssetData; import org.qora.data.asset.OrderData; +import org.qora.data.asset.RecentTradeData; import org.qora.data.asset.TradeData; public interface AssetRepository { @@ -42,14 +43,14 @@ public interface AssetRepository { public List getAggregatedOpenOrders(long haveAssetId, long wantAssetId, Integer limit, Integer offset, Boolean reverse) throws DataException; - public List getAccountsOrders(byte[] publicKey, boolean includeClosed, boolean includeFulfilled, Integer limit, Integer offset, Boolean reverse) + public List getAccountsOrders(byte[] publicKey, Boolean optIsClosed, Boolean optIsFulfilled, Integer limit, Integer offset, Boolean reverse) throws DataException; - public List getAccountsOrders(byte[] publicKey, long haveAssetId, long wantAssetId, boolean includeClosed, boolean includeFulfilled, + public List getAccountsOrders(byte[] publicKey, long haveAssetId, long wantAssetId, Boolean optIsClosed, Boolean optIsFulfilled, Integer limit, Integer offset, Boolean reverse) throws DataException; - public default List getAccountsOrders(byte[] publicKey, boolean includeClosed, boolean includeFulfilled) throws DataException { - return getAccountsOrders(publicKey, includeClosed, includeFulfilled, null, null, null); + public default List getAccountsOrders(byte[] publicKey, Boolean optIsClosed, Boolean optIsFulfilled) throws DataException { + return getAccountsOrders(publicKey, optIsClosed, optIsFulfilled, null, null, null); } public void save(OrderData orderData) throws DataException; @@ -64,6 +65,8 @@ public interface AssetRepository { return getTrades(haveAssetId, wantAssetId, null, null, null); } + public List getRecentTrades(List assetIds, Long otherAssetId, Integer limit, Integer offset, Boolean reverse) throws DataException; + /** Returns TradeData for trades where orderId was involved, i.e. either initiating OR target order */ public List getOrdersTrades(byte[] orderId, Integer limit, Integer offset, Boolean reverse) throws DataException; diff --git a/src/main/java/org/qora/repository/hsqldb/HSQLDBAccountRepository.java b/src/main/java/org/qora/repository/hsqldb/HSQLDBAccountRepository.java index a432b0aa..001ce37c 100644 --- a/src/main/java/org/qora/repository/hsqldb/HSQLDBAccountRepository.java +++ b/src/main/java/org/qora/repository/hsqldb/HSQLDBAccountRepository.java @@ -4,7 +4,9 @@ import java.math.BigDecimal; import java.sql.ResultSet; import java.sql.SQLException; import java.util.ArrayList; +import java.util.Collections; import java.util.List; +import java.util.stream.Collectors; import org.qora.data.account.AccountBalanceData; import org.qora.data.account.AccountData; @@ -56,7 +58,7 @@ public class HSQLDBAccountRepository implements AccountRepository { return null; // Column is NOT NULL so this should never implicitly convert to 0 - return resultSet.getInt(1); + return resultSet.getInt(1); } catch (SQLException e) { throw new DataException("Unable to fetch account's default groupID from repository", e); } @@ -141,54 +143,48 @@ public class HSQLDBAccountRepository implements AccountRepository { } @Override - public List getAllBalances(String address, Integer limit, Integer offset, Boolean reverse) throws DataException { - String sql = "SELECT asset_id, balance FROM AccountBalances WHERE account = ? ORDER BY asset_id"; - if (reverse != null && reverse) - sql += " DESC"; - sql += HSQLDBRepository.limitOffsetSql(limit, offset); + public List getAssetBalances(List addresses, List assetIds, Integer limit, Integer offset, Boolean reverse) + throws DataException { + String sql = "SELECT account, asset_id, balance, asset_name FROM AccountBalances NATURAL JOIN Assets " + "WHERE "; - List balances = new ArrayList(); + if (!addresses.isEmpty()) + sql += "account IN (" + String.join(", ", Collections.nCopies(addresses.size(), "?")) + ") "; - try (ResultSet resultSet = this.repository.checkedExecute(sql, address)) { - if (resultSet == null) - return balances; + if (!addresses.isEmpty() && !assetIds.isEmpty()) + sql += "AND "; - do { - long assetId = resultSet.getLong(1); - BigDecimal balance = resultSet.getBigDecimal(2).setScale(8); - - balances.add(new AccountBalanceData(address, assetId, balance)); - } while (resultSet.next()); + if (!assetIds.isEmpty()) + sql += "asset_id IN (" + String.join(", ", assetIds.stream().map(assetId -> assetId.toString()).collect(Collectors.toList())) + ") "; - return balances; - } catch (SQLException e) { - throw new DataException("Unable to fetch account balances from repository", e); - } - } + sql += "ORDER BY account"; + if (reverse != null && reverse) + sql += " DESC"; - @Override - public List getAssetBalances(long assetId, Integer limit, Integer offset, Boolean reverse) throws DataException { - String sql = "SELECT account, balance FROM AccountBalances WHERE asset_id = ? ORDER BY account"; + sql += ", asset_id"; if (reverse != null && reverse) sql += " DESC"; + sql += HSQLDBRepository.limitOffsetSql(limit, offset); - List balances = new ArrayList(); + String[] addressesArray = addresses.toArray(new String[addresses.size()]); + List accountBalances = new ArrayList<>(); - try (ResultSet resultSet = this.repository.checkedExecute(sql, assetId)) { + try (ResultSet resultSet = this.repository.checkedExecute(sql, (Object[]) addressesArray)) { if (resultSet == null) - return balances; + return accountBalances; do { String address = resultSet.getString(1); - BigDecimal balance = resultSet.getBigDecimal(2).setScale(8); + long assetId = resultSet.getLong(2); + BigDecimal balance = resultSet.getBigDecimal(3).setScale(8); + String assetName = resultSet.getString(4); - balances.add(new AccountBalanceData(address, assetId, balance)); + accountBalances.add(new AccountBalanceData(address, assetId, balance, assetName)); } while (resultSet.next()); - return balances; + return accountBalances; } catch (SQLException e) { - throw new DataException("Unable to fetch asset account balances from repository", e); + throw new DataException("Unable to fetch asset balances from repository", e); } } diff --git a/src/main/java/org/qora/repository/hsqldb/HSQLDBAssetRepository.java b/src/main/java/org/qora/repository/hsqldb/HSQLDBAssetRepository.java index 0a5aca53..0659ca03 100644 --- a/src/main/java/org/qora/repository/hsqldb/HSQLDBAssetRepository.java +++ b/src/main/java/org/qora/repository/hsqldb/HSQLDBAssetRepository.java @@ -6,10 +6,12 @@ import java.sql.SQLException; import java.sql.Timestamp; import java.util.ArrayList; import java.util.Calendar; +import java.util.Collections; import java.util.List; import org.qora.data.asset.AssetData; import org.qora.data.asset.OrderData; +import org.qora.data.asset.RecentTradeData; import org.qora.data.asset.TradeData; import org.qora.repository.AssetRepository; import org.qora.repository.DataException; @@ -241,12 +243,13 @@ public class HSQLDBAssetRepository implements AssetRepository { } @Override - public List getAccountsOrders(byte[] publicKey, boolean includeClosed, boolean includeFulfilled, Integer limit, Integer offset, Boolean reverse) throws DataException { + public List getAccountsOrders(byte[] publicKey, Boolean optIsClosed, Boolean optIsFulfilled, Integer limit, Integer offset, Boolean reverse) + throws DataException { String sql = "SELECT asset_order_id, have_asset_id, want_asset_id, amount, fulfilled, price, ordered, is_closed, is_fulfilled FROM AssetOrders WHERE creator = ?"; - if (!includeClosed) - sql += " AND is_closed = FALSE"; - if (!includeFulfilled) - sql += " AND is_fulfilled = FALSE"; + if (optIsClosed != null) + sql += " AND is_closed = " + (optIsClosed ? "TRUE" : "FALSE"); + if (optIsFulfilled != null) + sql += " AND is_fulfilled = " + (optIsFulfilled ? "TRUE" : "FALSE"); sql += " ORDER BY ordered"; if (reverse != null && reverse) sql += " DESC"; @@ -269,8 +272,7 @@ public class HSQLDBAssetRepository implements AssetRepository { boolean isClosed = resultSet.getBoolean(8); boolean isFulfilled = resultSet.getBoolean(9); - OrderData order = new OrderData(orderId, publicKey, haveAssetId, wantAssetId, amount, fulfilled, price, timestamp, isClosed, - isFulfilled); + OrderData order = new OrderData(orderId, publicKey, haveAssetId, wantAssetId, amount, fulfilled, price, timestamp, isClosed, isFulfilled); orders.add(order); } while (resultSet.next()); @@ -281,12 +283,13 @@ public class HSQLDBAssetRepository implements AssetRepository { } @Override - public List getAccountsOrders(byte[] publicKey, long haveAssetId, long wantAssetId, boolean includeClosed, boolean includeFulfilled, Integer limit, Integer offset, Boolean reverse) throws DataException { + public List getAccountsOrders(byte[] publicKey, long haveAssetId, long wantAssetId, Boolean optIsClosed, Boolean optIsFulfilled, Integer limit, + Integer offset, Boolean reverse) throws DataException { String sql = "SELECT asset_order_id, amount, fulfilled, price, ordered, is_closed, is_fulfilled FROM AssetOrders WHERE creator = ? AND have_asset_id = ? AND want_asset_id = ?"; - if (!includeClosed) - sql += " AND is_closed = FALSE"; - if (!includeFulfilled) - sql += " AND is_fulfilled = FALSE"; + if (optIsClosed != null) + sql += " AND is_closed = " + (optIsClosed ? "TRUE" : "FALSE"); + if (optIsFulfilled != null) + sql += " AND is_fulfilled = " + (optIsFulfilled ? "TRUE" : "FALSE"); sql += " ORDER BY ordered"; if (reverse != null && reverse) sql += " DESC"; @@ -307,8 +310,7 @@ public class HSQLDBAssetRepository implements AssetRepository { boolean isClosed = resultSet.getBoolean(6); boolean isFulfilled = resultSet.getBoolean(7); - OrderData order = new OrderData(orderId, publicKey, haveAssetId, wantAssetId, amount, fulfilled, price, timestamp, isClosed, - isFulfilled); + OrderData order = new OrderData(orderId, publicKey, haveAssetId, wantAssetId, amount, fulfilled, price, timestamp, isClosed, isFulfilled); orders.add(order); } while (resultSet.next()); @@ -376,6 +378,66 @@ public class HSQLDBAssetRepository implements AssetRepository { } } + @Override + public List getRecentTrades(List assetIds, Long otherAssetId, Integer limit, Integer offset, Boolean reverse) throws DataException { + // Find assetID pairs that have actually been traded + String tradedAssetsSubquery = "SELECT have_asset_id, want_asset_id " + "FROM AssetTrades JOIN AssetOrders ON asset_order_id = initiating_order_id "; + + // Optionally limit traded assetID pairs + if (!assetIds.isEmpty()) + tradedAssetsSubquery += "WHERE have_asset_id IN (" + String.join(", ", Collections.nCopies(assetIds.size(), "?")) + ")"; + + if (otherAssetId != null) { + tradedAssetsSubquery += assetIds.isEmpty() ? " WHERE " : " AND "; + tradedAssetsSubquery += "want_asset_id = " + otherAssetId.toString(); + } + + tradedAssetsSubquery += " GROUP BY have_asset_id, want_asset_id"; + + // Find recent trades using "TradedAssets" assetID pairs + String recentTradesSubquery = "SELECT AssetTrades.amount, AssetTrades.price, AssetTrades.traded " + + "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"; + + // Put it all together + String sql = "SELECT have_asset_id, want_asset_id, RecentTrades.amount, RecentTrades.price, RecentTrades.traded " + "FROM (" + tradedAssetsSubquery + + ") AS TradedAssets " + ", LATERAL (" + recentTradesSubquery + ") AS RecentTrades (amount, price, traded) " + "ORDER BY have_asset_id"; + if (reverse != null && reverse) + sql += " DESC"; + + sql += ", want_asset_id"; + if (reverse != null && reverse) + sql += " DESC"; + + sql += ", RecentTrades.traded DESC "; + + sql += HSQLDBRepository.limitOffsetSql(limit, offset); + + Long[] assetIdsArray = assetIds.toArray(new Long[assetIds.size()]); + List recentTrades = new ArrayList<>(); + + try (ResultSet resultSet = this.repository.checkedExecute(sql, (Object[]) assetIdsArray)) { + if (resultSet == null) + return recentTrades; + + do { + long haveAssetId = resultSet.getLong(1); + long wantAssetId = resultSet.getLong(2); + BigDecimal amount = resultSet.getBigDecimal(3); + BigDecimal price = resultSet.getBigDecimal(4); + long timestamp = resultSet.getTimestamp(5, Calendar.getInstance(HSQLDBRepository.UTC)).getTime(); + + RecentTradeData recentTrade = new RecentTradeData(haveAssetId, wantAssetId, amount, price, timestamp); + recentTrades.add(recentTrade); + } while (resultSet.next()); + + return recentTrades; + } catch (SQLException e) { + throw new DataException("Unable to fetch recent asset trades from repository", e); + } + } + @Override public List getOrdersTrades(byte[] orderId, Integer limit, Integer offset, Boolean reverse) throws DataException { String sql = "SELECT initiating_order_id, target_order_id, amount, price, traded FROM AssetTrades WHERE initiating_order_id = ? OR target_order_id = ? ORDER BY traded";