From 4d69242cdb20a0a282ec28a6227f2e4e48f6ae90 Mon Sep 17 00:00:00 2001 From: catbref Date: Mon, 4 Mar 2019 18:53:54 +0000 Subject: [PATCH] Added/improved assets-related API calls Imported Block/BlockChain fixes from "minting" branch to do with block timestamps. GET /assets/holders/{assetid} and GET /assets/address/{address} and GET /assets/balance/{assetid}/{address} all combined into GET /assets/balances?address=...&address=...&assetid=...&assetid=... New GET /assets/trades/recent?assetid=...&assetid=... that returns most recent two trades for each asset-pair. GET /assets/orders/{address}/{assetid}/{otherassetid} has includeClosed and includeFulfilled repurposed as isClosed (true/false/omitted) and isFulfilled (true/false/omitted). ALSO, Order.isClosed is now set to true when isFulfilled is set to true during processing (and correspondingly set to false during orphaning). AccountBalanceData now includes optional assetName field for use with API but generally not set for internal use. --- .../org/qora/api/resource/AssetsResource.java | 158 ++++++++---------- src/main/java/org/qora/asset/Trade.java | 8 + src/main/java/org/qora/block/Block.java | 71 ++++---- src/main/java/org/qora/block/BlockChain.java | 4 +- .../qora/data/account/AccountBalanceData.java | 13 +- .../org/qora/data/asset/RecentTradeData.java | 65 +++++++ .../qora/repository/AccountRepository.java | 12 +- .../org/qora/repository/AssetRepository.java | 11 +- .../hsqldb/HSQLDBAccountRepository.java | 62 ++++--- .../hsqldb/HSQLDBAssetRepository.java | 90 ++++++++-- 10 files changed, 311 insertions(+), 183 deletions(-) create mode 100644 src/main/java/org/qora/data/asset/RecentTradeData.java 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) { - try (final Repository repository = RepositoryManager.getRepository()) { - if (!repository.getAssetRepository().assetExists(assetId)) - throw ApiExceptionFactory.INSTANCE.createException(request, ApiError.INVALID_ASSET_ID); + if (addresses.isEmpty() && assetIds.isEmpty()) + throw ApiExceptionFactory.INSTANCE.createException(request, ApiError.INVALID_CRITERIA); - return repository.getAccountRepository().getAssetBalances(assetId, limit, offset, reverse); + for (String address : addresses) + if (!Crypto.isValidAddress(address)) + throw ApiExceptionFactory.INSTANCE.createException(request, ApiError.INVALID_ADDRESS); + + try (final Repository repository = RepositoryManager.getRepository()) { + for (long assetId : assetIds) + if (!repository.getAssetRepository().assetExists(assetId)) + throw ApiExceptionFactory.INSTANCE.createException(request, ApiError.INVALID_ASSET_ID); + + 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"; + 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 "; + + if (!addresses.isEmpty()) + sql += "account IN (" + String.join(", ", Collections.nCopies(addresses.size(), "?")) + ") "; + + if (!addresses.isEmpty() && !assetIds.isEmpty()) + sql += "AND "; + + if (!assetIds.isEmpty()) + sql += "asset_id IN (" + String.join(", ", assetIds.stream().map(assetId -> assetId.toString()).collect(Collectors.toList())) + ") "; + + sql += "ORDER BY account"; if (reverse != null && reverse) sql += " DESC"; - sql += HSQLDBRepository.limitOffsetSql(limit, offset); - List balances = new ArrayList(); - - try (ResultSet resultSet = this.repository.checkedExecute(sql, address)) { - if (resultSet == null) - return balances; - - do { - long assetId = resultSet.getLong(1); - BigDecimal balance = resultSet.getBigDecimal(2).setScale(8); - - balances.add(new AccountBalanceData(address, assetId, balance)); - } while (resultSet.next()); - - return balances; - } catch (SQLException e) { - throw new DataException("Unable to fetch account balances from repository", e); - } - } - - @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";