From 4be58514c0b8648ae39e7253923ff2616cc80602 Mon Sep 17 00:00:00 2001 From: catbref Date: Thu, 24 Jan 2019 16:42:55 +0000 Subject: [PATCH] Unify API calls that return lists + offload pagination to repository API calls that return lists now take limit, offset and reverse params. API calls that used to return data & optional list (e.g. blockWithTransactions) now only return base data. The optional lists can be fetched via a different API call. Also: SLF4J now routes logging to log4j2 so start up output cleaned up. Suppressed extraneous Jersey warning about Providers during start-up injection. --- .settings/org.eclipse.core.resources.prefs | 1 + log4j2.properties | 15 +- pom.xml | 19 +- .../java/org/qora/api/UnmarshalListener.java | 18 - .../org/qora/api/model/AssetWithHolders.java | 34 -- .../qora/api/model/BlockWithTransactions.java | 34 -- ...pWithMemberInfo.java => GroupMembers.java} | 30 +- .../qora/api/resource/AddressesResource.java | 92 ----- .../org/qora/api/resource/AdminResource.java | 9 +- .../org/qora/api/resource/AssetsResource.java | 391 +++++++++++++++--- .../org/qora/api/resource/BlocksResource.java | 185 ++++++--- .../org/qora/api/resource/GroupsResource.java | 99 +++-- .../org/qora/api/resource/NamesResource.java | 28 +- .../api/resource/TransactionsResource.java | 136 +++--- .../java/org/qora/block/BlockGenerator.java | 2 +- .../java/org/qora/controller/Controller.java | 5 + .../data/transaction/TransactionData.java | 8 +- .../qora/repository/AccountRepository.java | 12 +- .../org/qora/repository/AssetRepository.java | 31 +- .../org/qora/repository/BlockRepository.java | 15 +- .../org/qora/repository/GroupRepository.java | 57 ++- .../org/qora/repository/NameRepository.java | 18 +- .../repository/TransactionRepository.java | 23 +- .../hsqldb/HSQLDBAccountRepository.java | 18 +- .../hsqldb/HSQLDBAssetRepository.java | 64 +-- .../hsqldb/HSQLDBBlockRepository.java | 9 +- .../hsqldb/HSQLDBGroupRepository.java | 112 +++-- .../hsqldb/HSQLDBNameRepository.java | 36 +- .../repository/hsqldb/HSQLDBRepository.java | 21 + .../HSQLDBTransactionRepository.java | 76 +++- .../org/qora/transaction/Transaction.java | 4 +- 31 files changed, 1020 insertions(+), 582 deletions(-) delete mode 100644 src/main/java/org/qora/api/UnmarshalListener.java delete mode 100644 src/main/java/org/qora/api/model/AssetWithHolders.java delete mode 100644 src/main/java/org/qora/api/model/BlockWithTransactions.java rename src/main/java/org/qora/api/model/{GroupWithMemberInfo.java => GroupMembers.java} (51%) diff --git a/.settings/org.eclipse.core.resources.prefs b/.settings/org.eclipse.core.resources.prefs index ed7df2b3..62e13849 100644 --- a/.settings/org.eclipse.core.resources.prefs +++ b/.settings/org.eclipse.core.resources.prefs @@ -2,5 +2,6 @@ eclipse.preferences.version=1 encoding//src/main/java=UTF-8 encoding//src/main/resources=UTF-8 encoding//src/test/java=UTF-8 +encoding//target/generated-sources/package-info=UTF-8 encoding/=UTF-8 encoding/src=UTF-8 diff --git a/log4j2.properties b/log4j2.properties index 187ef94a..b5c3718f 100644 --- a/log4j2.properties +++ b/log4j2.properties @@ -9,17 +9,14 @@ rootLogger.appenderRef.rolling.ref = FILE # Override HSQLDB logging level to "warn" as too much is logged at "info" logger.hsqldb.name = hsqldb.db logger.hsqldb.level = warn -logger.hsqldb.appenderRef.rolling.ref = FILE -# Override logging level for this class -logger.voting.name = qora.transaction.VoteOnPollTransaction -logger.voting.level = trace -logger.voting.appenderRef.rolling.ref = FILE +# Suppress extraneous Jersey warning +logger.jerseyInject.name = org.glassfish.jersey.internal.inject.Providers +logger.jerseyInject.level = error -# Override logging level for this class -logger.assets.name = qora.assets.Order -logger.assets.level = trace -logger.assets.appenderRef.rolling.ref = FILE +# Debugging transaction searches +logger.txSearch.name = org.qora.repository.hsqldb.transaction.HSQLDBTransactionRepository +logger.txSearch.level = trace appender.console.type = Console appender.console.name = stdout diff --git a/pom.xml b/pom.xml index 36d73da7..08de2ea7 100644 --- a/pom.xml +++ b/pom.xml @@ -305,17 +305,24 @@ log4j-api ${log4j.version} - + + + org.apache.logging.log4j + log4j-slf4j-impl + ${log4j.version} + + + + org.apache.logging.log4j + log4j-jul + ${log4j.version} + + org.slf4j slf4j-api ${slf4j.version} - - org.slf4j - slf4j-simple - ${slf4j.version} - javax.servlet diff --git a/src/main/java/org/qora/api/UnmarshalListener.java b/src/main/java/org/qora/api/UnmarshalListener.java deleted file mode 100644 index aa4ae071..00000000 --- a/src/main/java/org/qora/api/UnmarshalListener.java +++ /dev/null @@ -1,18 +0,0 @@ -package org.qora.api; - -import javax.xml.bind.Unmarshaller.Listener; - -import org.qora.data.transaction.TransactionData; - -public class UnmarshalListener extends Listener { - - @Override - public void afterUnmarshal(Object target, Object parent) { - if (!(target instanceof TransactionData)) - return; - - // do something - return; - } - -} diff --git a/src/main/java/org/qora/api/model/AssetWithHolders.java b/src/main/java/org/qora/api/model/AssetWithHolders.java deleted file mode 100644 index 3daa9ba3..00000000 --- a/src/main/java/org/qora/api/model/AssetWithHolders.java +++ /dev/null @@ -1,34 +0,0 @@ -package org.qora.api.model; - -import java.util.List; - -import javax.xml.bind.annotation.XmlAccessType; -import javax.xml.bind.annotation.XmlAccessorType; -import javax.xml.bind.annotation.XmlElement; - -import org.qora.data.account.AccountBalanceData; -import org.qora.data.asset.AssetData; - -import io.swagger.v3.oas.annotations.media.Schema; - -@Schema(description = "Asset info, maybe including asset holders") -// All properties to be converted to JSON via JAX-RS -@XmlAccessorType(XmlAccessType.FIELD) -public class AssetWithHolders { - - @Schema(implementation = AssetData.class, name = "asset", title = "asset data") - @XmlElement(name = "asset") - public AssetData assetData; - - public List holders; - - // For JAX-RS - protected AssetWithHolders() { - } - - public AssetWithHolders(AssetData assetData, List holders) { - this.assetData = assetData; - this.holders = holders; - } - -} diff --git a/src/main/java/org/qora/api/model/BlockWithTransactions.java b/src/main/java/org/qora/api/model/BlockWithTransactions.java deleted file mode 100644 index 277f184b..00000000 --- a/src/main/java/org/qora/api/model/BlockWithTransactions.java +++ /dev/null @@ -1,34 +0,0 @@ -package org.qora.api.model; - -import java.util.List; - -import javax.xml.bind.annotation.XmlAccessType; -import javax.xml.bind.annotation.XmlAccessorType; -import javax.xml.bind.annotation.XmlElement; - -import org.qora.data.block.BlockData; -import org.qora.data.transaction.TransactionData; - -import io.swagger.v3.oas.annotations.media.Schema; - -@Schema(description = "Block info, maybe including transactions") -// All properties to be converted to JSON via JAX-RS -@XmlAccessorType(XmlAccessType.FIELD) -public class BlockWithTransactions { - - @Schema(implementation = BlockData.class, name = "block", title = "block data") - @XmlElement(name = "block") - public BlockData blockData; - - public List transactions; - - // For JAX-RS - protected BlockWithTransactions() { - } - - public BlockWithTransactions(BlockData blockData, List transactions) { - this.blockData = blockData; - this.transactions = transactions; - } - -} diff --git a/src/main/java/org/qora/api/model/GroupWithMemberInfo.java b/src/main/java/org/qora/api/model/GroupMembers.java similarity index 51% rename from src/main/java/org/qora/api/model/GroupWithMemberInfo.java rename to src/main/java/org/qora/api/model/GroupMembers.java index 25d523d2..2437eae6 100644 --- a/src/main/java/org/qora/api/model/GroupWithMemberInfo.java +++ b/src/main/java/org/qora/api/model/GroupMembers.java @@ -6,38 +6,31 @@ import javax.xml.bind.annotation.XmlAccessType; import javax.xml.bind.annotation.XmlAccessorType; import javax.xml.bind.annotation.XmlElement; -import org.qora.data.group.GroupData; -import org.qora.data.group.GroupMemberData; - import io.swagger.v3.oas.annotations.media.Schema; @Schema(description = "Group info, maybe including members") // All properties to be converted to JSON via JAX-RS @XmlAccessorType(XmlAccessType.FIELD) -public class GroupWithMemberInfo { - - @Schema(implementation = GroupData.class, name = "group", title = "group info") - @XmlElement(name = "group") - public GroupData groupData; +public class GroupMembers { Integer memberCount; - - @XmlElement(name = "admins") - public List groupAdminAddresses; + Integer adminCount; @XmlAccessorType(XmlAccessType.FIELD) @Schema(description = "Member info") public static class MemberInfo { public String member; - public long joined; + public Long joined; + public Boolean isAdmin; // For JAX-RS protected MemberInfo() { } - public MemberInfo(GroupMemberData groupMemberData) { - this.member = groupMemberData.getMember(); - this.joined = groupMemberData.getJoined(); + public MemberInfo(String member, Long joined, boolean isAdmin) { + this.member = member; + this.joined = joined; + this.isAdmin = isAdmin ? true : null; // null so field is not displayed by API } } @@ -45,14 +38,13 @@ public class GroupWithMemberInfo { public List groupMembers; // For JAX-RS - protected GroupWithMemberInfo() { + protected GroupMembers() { } - public GroupWithMemberInfo(GroupData groupData, List groupAdminAddresses, List groupMembers, Integer memberCount) { - this.groupData = groupData; - this.groupAdminAddresses = groupAdminAddresses; + public GroupMembers(List groupMembers, Integer memberCount, Integer adminCount) { this.groupMembers = groupMembers; this.memberCount = memberCount; + this.adminCount = adminCount; } } diff --git a/src/main/java/org/qora/api/resource/AddressesResource.java b/src/main/java/org/qora/api/resource/AddressesResource.java index e3f774d4..041ef063 100644 --- a/src/main/java/org/qora/api/resource/AddressesResource.java +++ b/src/main/java/org/qora/api/resource/AddressesResource.java @@ -1,21 +1,18 @@ package org.qora.api.resource; import io.swagger.v3.oas.annotations.Operation; -import io.swagger.v3.oas.annotations.media.ArraySchema; import io.swagger.v3.oas.annotations.media.Content; import io.swagger.v3.oas.annotations.media.Schema; import io.swagger.v3.oas.annotations.responses.ApiResponse; import io.swagger.v3.oas.annotations.tags.Tag; import java.math.BigDecimal; -import java.util.List; import javax.servlet.http.HttpServletRequest; import javax.ws.rs.GET; import javax.ws.rs.Path; import javax.ws.rs.PathParam; import javax.ws.rs.Produces; -import javax.ws.rs.QueryParam; import javax.ws.rs.core.Context; import javax.ws.rs.core.MediaType; @@ -26,9 +23,7 @@ import org.qora.api.ApiException; import org.qora.api.ApiExceptionFactory; 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.OrderData; import org.qora.repository.DataException; import org.qora.repository.Repository; import org.qora.repository.RepositoryManager; @@ -145,59 +140,6 @@ public class AddressesResource { } } - @GET - @Path("/assetbalance/{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("/assets/{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 getAssets(@PathParam("address") String address) { - if (!Crypto.isValidAddress(address)) - throw ApiExceptionFactory.INSTANCE.createException(request, ApiError.INVALID_ADDRESS); - - try (final Repository repository = RepositoryManager.getRepository()) { - return repository.getAccountRepository().getAllBalances(address); - } catch (ApiException e) { - throw e; - } catch (DataException e) { - throw ApiExceptionFactory.INSTANCE.createException(request, ApiError.REPOSITORY_ISSUE, e); - } - } - @GET @Path("/balance/{address}/{confirmations}") @Operation( @@ -283,38 +225,4 @@ public class AddressesResource { } } - @GET - @Path("/assetorders/{address}") - @Operation( - summary = "Asset orders created by this address", - responses = { - @ApiResponse( - description = "Asset orders", - content = @Content(array = @ArraySchema(schema = @Schema(implementation = OrderData.class))) - ) - } - ) - @ApiErrors({ApiError.INVALID_ADDRESS, ApiError.ADDRESS_NO_EXISTS, ApiError.REPOSITORY_ISSUE}) - public List getAssetOrders(@PathParam("address") String address, @QueryParam("includeClosed") boolean includeClosed, @QueryParam("includeFulfilled") boolean includeFulfilled) { - if (!Crypto.isValidAddress(address)) - throw ApiExceptionFactory.INSTANCE.createException(request, ApiError.INVALID_ADDRESS); - - try (final Repository repository = RepositoryManager.getRepository()) { - AccountData accountData = repository.getAccountRepository().getAccount(address); - - if (accountData == null) - throw ApiExceptionFactory.INSTANCE.createException(request, ApiError.ADDRESS_NO_EXISTS); - - byte[] publicKey = accountData.getPublicKey(); - if (publicKey == null) - throw ApiExceptionFactory.INSTANCE.createException(request, ApiError.ADDRESS_NO_EXISTS); - - return repository.getAssetRepository().getAccountsOrders(publicKey, includeClosed, includeFulfilled); - } catch (ApiException e) { - throw e; - } catch (DataException e) { - throw ApiExceptionFactory.INSTANCE.createException(request, ApiError.REPOSITORY_ISSUE, e); - } - } - } diff --git a/src/main/java/org/qora/api/resource/AdminResource.java b/src/main/java/org/qora/api/resource/AdminResource.java index 140a6120..51bf2fac 100644 --- a/src/main/java/org/qora/api/resource/AdminResource.java +++ b/src/main/java/org/qora/api/resource/AdminResource.java @@ -28,16 +28,13 @@ public class AdminResource { @GET @Path("/unused") - @Parameter(in = ParameterIn.PATH, name = "blockSignature", description = "Block signature", schema = @Schema(type = "string", format = "byte"), example = "very_long_block_signature_in_base58") - @Parameter(in = ParameterIn.PATH, name = "assetId", description = "Asset ID, 0 is native coin", schema = @Schema(type = "string", format = "byte")) - @Parameter(in = ParameterIn.PATH, name = "otherAssetId", description = "Asset ID, 0 is native coin", schema = @Schema(type = "string", format = "byte")) + @Parameter(in = ParameterIn.PATH, name = "assetid", description = "Asset ID, 0 is native coin", schema = @Schema(type = "integer")) + @Parameter(in = ParameterIn.PATH, name = "otherassetid", description = "Asset ID, 0 is native coin", schema = @Schema(type = "integer")) @Parameter(in = ParameterIn.PATH, name = "address", description = "an account address", example = "QgV4s3xnzLhVBEJxcYui4u4q11yhUHsd9v") @Parameter(in = ParameterIn.QUERY, name = "count", description = "Maximum number of entries to return, 0 means none", schema = @Schema(type = "integer", defaultValue = "20")) @Parameter(in = ParameterIn.QUERY, name = "limit", description = "Maximum number of entries to return, 0 means unlimited", schema = @Schema(type = "integer", defaultValue = "20")) @Parameter(in = ParameterIn.QUERY, name = "offset", description = "Starting entry in results, 0 is first entry", schema = @Schema(type = "integer")) - @Parameter(in = ParameterIn.QUERY, name = "includeTransactions", description = "Include associated transactions in results", schema = @Schema(type = "boolean")) - @Parameter(in = ParameterIn.QUERY, name = "includeHolders", description = "Include asset holders in results", schema = @Schema(type = "boolean")) - @Parameter(in = ParameterIn.QUERY, name = "queryAssetId", description = "Asset ID, 0 is native coin", schema = @Schema(type = "string", format = "byte")) + @Parameter(in = ParameterIn.QUERY, name = "reverse", description = "Reverse results", schema = @Schema(type = "boolean")) public String globalParameters() { return ""; } diff --git a/src/main/java/org/qora/api/resource/AssetsResource.java b/src/main/java/org/qora/api/resource/AssetsResource.java index 060df6bd..9ea8b33b 100644 --- a/src/main/java/org/qora/api/resource/AssetsResource.java +++ b/src/main/java/org/qora/api/resource/AssetsResource.java @@ -9,6 +9,7 @@ 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; @@ -22,13 +23,15 @@ 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; import org.qora.api.ApiExceptionFactory; -import org.qora.api.model.AssetWithHolders; -import org.qora.api.model.OrderWithTrades; import org.qora.api.model.TradeWithOrderInfo; +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.TradeData; @@ -47,8 +50,12 @@ import org.qora.transform.transaction.IssueAssetTransactionTransformer; import org.qora.utils.Base58; @Path("/assets") -@Produces({MediaType.APPLICATION_JSON, MediaType.TEXT_PLAIN}) -@Tag(name = "Assets") +@Produces({ + MediaType.APPLICATION_JSON, MediaType.TEXT_PLAIN +}) +@Tag( + name = "Assets" +) public class AssetsResource { @Context @@ -60,21 +67,28 @@ public class AssetsResource { responses = { @ApiResponse( description = "asset info", - content = @Content(array = @ArraySchema(schema = @Schema(implementation = AssetData.class))) + content = @Content( + array = @ArraySchema( + schema = @Schema( + implementation = AssetData.class + ) + ) + ) ) } ) - @ApiErrors({ApiError.REPOSITORY_ISSUE}) - public List getAllAssets(@Parameter(ref = "limit") @QueryParam("limit") int limit, @Parameter(ref = "offset") @QueryParam("offset") int offset) { + @ApiErrors({ + ApiError.REPOSITORY_ISSUE + }) + public List getAllAssets(@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()) { - List assets = repository.getAssetRepository().getAllAssets(); - - // Pagination would take effect here (or as part of the repository access) - int fromIndex = Integer.min(offset, assets.size()); - int toIndex = limit == 0 ? assets.size() : Integer.min(fromIndex + limit, assets.size()); - assets = assets.subList(fromIndex, toIndex); - - return assets; + return repository.getAssetRepository().getAllAssets(limit, offset, reverse); } catch (DataException e) { throw ApiExceptionFactory.INSTANCE.createException(request, ApiError.REPOSITORY_ISSUE, e); } @@ -88,12 +102,18 @@ public class AssetsResource { responses = { @ApiResponse( description = "asset info", - content = @Content(array = @ArraySchema(schema = @Schema(implementation = AssetWithHolders.class))) + content = @Content( + schema = @Schema( + implementation = AssetData.class + ) + ) ) } ) - @ApiErrors({ApiError.INVALID_CRITERIA, ApiError.INVALID_ASSET_ID, ApiError.REPOSITORY_ISSUE}) - public AssetWithHolders getAssetInfo(@QueryParam("assetId") Integer assetId, @QueryParam("assetName") String assetName, @Parameter(ref = "includeHolders") @QueryParam("includeHolders") boolean includeHolders) { + @ApiErrors({ + ApiError.INVALID_CRITERIA, ApiError.INVALID_ASSET_ID, ApiError.REPOSITORY_ISSUE + }) + public AssetData getAssetInfo(@QueryParam("assetId") Integer assetId, @QueryParam("assetName") String assetName) { if (assetId == null && (assetName == null || assetName.isEmpty())) throw ApiExceptionFactory.INSTANCE.createException(request, ApiError.INVALID_CRITERIA); @@ -108,31 +128,81 @@ public class AssetsResource { if (assetData == null) throw ApiExceptionFactory.INSTANCE.createException(request, ApiError.INVALID_ASSET_ID); - List holders = null; - if (includeHolders) - holders = repository.getAccountRepository().getAssetBalances(assetData.getAssetId()); - - return new AssetWithHolders(assetData, holders); + return assetData; } catch (DataException e) { throw ApiExceptionFactory.INSTANCE.createException(request, ApiError.REPOSITORY_ISSUE, e); } } @GET - @Path("/orderbook/{assetId}/{otherAssetId}") + @Path("/holders/{assetid}") + @Operation( + summary = "List holders of an asset", + responses = { + @ApiResponse( + description = "asset holders", + content = @Content( + array = @ArraySchema( + schema = @Schema( + implementation = AccountBalanceData.class + ) + ) + ) + ) + } + ) + @ApiErrors({ + ApiError.INVALID_CRITERIA, ApiError.INVALID_ASSET_ID, ApiError.REPOSITORY_ISSUE + }) + public List getAssetHolders(@PathParam("assetid") int assetId, @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); + + return repository.getAccountRepository().getAssetBalances(assetId, limit, offset, reverse); + } catch (DataException e) { + throw ApiExceptionFactory.INSTANCE.createException(request, ApiError.REPOSITORY_ISSUE, e); + } + } + + @GET + @Path("/orderbook/{assetid}/{otherassetid}") @Operation( summary = "Asset order book", - description = "Returns open orders, offering {assetId} for {otherAssetId} in return.", + description = "Returns open orders, offering {assetid} for {otherassetid} in return.", responses = { @ApiResponse( description = "asset orders", - content = @Content(array = @ArraySchema(schema = @Schema(implementation = OrderData.class))) + content = @Content( + array = @ArraySchema( + schema = @Schema( + implementation = OrderData.class + ) + ) + ) ) } ) - @ApiErrors({ApiError.INVALID_ASSET_ID, ApiError.REPOSITORY_ISSUE}) - public List getAssetOrders(@Parameter(ref = "assetId") @PathParam("assetId") int assetId, @Parameter(ref = "otherAssetId") @PathParam("otherAssetId") int otherAssetId, - @Parameter(ref = "limit") @QueryParam("limit") int limit, @Parameter(ref = "offset") @QueryParam("offset") int offset) { + @ApiErrors({ + ApiError.INVALID_ASSET_ID, ApiError.REPOSITORY_ISSUE + }) + public List getAssetOrders(@Parameter( + ref = "assetid" + ) @PathParam("assetid") int assetId, @Parameter( + ref = "otherassetid" + ) @PathParam("otherassetid") int 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()) { if (!repository.getAssetRepository().assetExists(assetId)) throw ApiExceptionFactory.INSTANCE.createException(request, ApiError.INVALID_ASSET_ID); @@ -140,36 +210,45 @@ public class AssetsResource { if (!repository.getAssetRepository().assetExists(otherAssetId)) throw ApiExceptionFactory.INSTANCE.createException(request, ApiError.INVALID_ASSET_ID); - List orders = repository.getAssetRepository().getOpenOrders(assetId, otherAssetId); - - // Pagination would take effect here (or as part of the repository access) - int fromIndex = Integer.min(offset, orders.size()); - int toIndex = limit == 0 ? orders.size() : Integer.min(fromIndex + limit, orders.size()); - orders = orders.subList(fromIndex, toIndex); - - return orders; + return repository.getAssetRepository().getOpenOrders(assetId, otherAssetId, limit, offset, reverse); } catch (DataException e) { throw ApiExceptionFactory.INSTANCE.createException(request, ApiError.REPOSITORY_ISSUE, e); } } @GET - @Path("/trades/{assetId}/{otherAssetId}") + @Path("/trades/{assetid}/{otherassetid}") @Operation( summary = "Asset trades", - description = "Returns successful trades of {assetId} for {otherAssetId}.
" + - "Does NOT include trades of {otherAssetId} for {assetId}!
" + - "\"Initiating\" order is the order that caused the actual trade by matching up with the \"target\" order.", + description = "Returns successful trades of {assetid} for {otherassetid}.
" + "Does NOT include trades of {otherassetid} for {assetid}!
" + + "\"Initiating\" order is the order that caused the actual trade by matching up with the \"target\" order.", responses = { @ApiResponse( description = "asset trades", - content = @Content(array = @ArraySchema(schema = @Schema(implementation = TradeWithOrderInfo.class))) + content = @Content( + array = @ArraySchema( + schema = @Schema( + implementation = TradeWithOrderInfo.class + ) + ) + ) ) } ) - @ApiErrors({ApiError.INVALID_ASSET_ID, ApiError.REPOSITORY_ISSUE}) - public List getAssetTrades(@Parameter(ref = "assetId") @PathParam("assetId") int assetId, @Parameter(ref = "otherAssetId") @PathParam("otherAssetId") int otherAssetId, - @Parameter(ref = "limit") @QueryParam("limit") int limit, @Parameter(ref = "offset") @QueryParam("offset") int offset) { + @ApiErrors({ + ApiError.INVALID_ASSET_ID, ApiError.REPOSITORY_ISSUE + }) + public List getAssetTrades(@Parameter( + ref = "assetid" + ) @PathParam("assetid") int assetId, @Parameter( + ref = "otherassetid" + ) @PathParam("otherassetid") int 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()) { if (!repository.getAssetRepository().assetExists(assetId)) throw ApiExceptionFactory.INSTANCE.createException(request, ApiError.INVALID_ASSET_ID); @@ -177,12 +256,7 @@ public class AssetsResource { if (!repository.getAssetRepository().assetExists(otherAssetId)) throw ApiExceptionFactory.INSTANCE.createException(request, ApiError.INVALID_ASSET_ID); - List trades = repository.getAssetRepository().getTrades(assetId, otherAssetId); - - // Pagination would take effect here (or as part of the repository access) - int fromIndex = Integer.min(offset, trades.size()); - int toIndex = limit == 0 ? trades.size() : Integer.min(fromIndex + limit, trades.size()); - trades = trades.subList(fromIndex, toIndex); + List trades = repository.getAssetRepository().getTrades(assetId, otherAssetId, limit, offset, reverse); // Expanding remaining entries List fullTrades = new ArrayList<>(); @@ -199,19 +273,25 @@ public class AssetsResource { } @GET - @Path("/order/{orderId}") + @Path("/order/{orderid}") @Operation( summary = "Fetch asset order", description = "Returns asset order info.", responses = { @ApiResponse( description = "asset order", - content = @Content(schema = @Schema(implementation = OrderData.class)) + content = @Content( + schema = @Schema( + implementation = OrderData.class + ) + ) ) } ) - @ApiErrors({ApiError.INVALID_ORDER_ID, ApiError.ORDER_NO_EXISTS, ApiError.REPOSITORY_ISSUE}) - public OrderWithTrades getAssetOrder(@PathParam("orderId") String orderId58) { + @ApiErrors({ + ApiError.INVALID_ORDER_ID, ApiError.ORDER_NO_EXISTS, ApiError.REPOSITORY_ISSUE + }) + public OrderData getAssetOrder(@PathParam("orderid") String orderId58) { // Decode orderID byte[] orderId; try { @@ -223,11 +303,180 @@ public class AssetsResource { try (final Repository repository = RepositoryManager.getRepository()) { OrderData orderData = repository.getAssetRepository().fromOrderId(orderId); if (orderData == null) - throw ApiExceptionFactory.INSTANCE.createException(request, ApiError.ORDER_NO_EXISTS); + throw ApiExceptionFactory.INSTANCE.createException(request, ApiError.ORDER_NO_EXISTS); - List trades = repository.getAssetRepository().getOrdersTrades(orderId); + return orderData; + } catch (DataException e) { + throw ApiExceptionFactory.INSTANCE.createException(request, ApiError.REPOSITORY_ISSUE, e); + } + } - return new OrderWithTrades(orderData, trades); + @GET + @Path("/order/{orderid}/trades") + @Operation( + summary = "Fetch asset order's matching trades", + description = "Returns asset order trades", + responses = { + @ApiResponse( + description = "asset trades", + content = @Content( + array = @ArraySchema( + schema = @Schema( + implementation = TradeData.class + ) + ) + ) + ) + } + ) + @ApiErrors({ + ApiError.INVALID_ORDER_ID, ApiError.ORDER_NO_EXISTS, ApiError.REPOSITORY_ISSUE + }) + public List getAssetOrderTrades(@PathParam("orderid") String orderId58, @Parameter( + ref = "limit" + ) @QueryParam("limit") Integer limit, @Parameter( + ref = "offset" + ) @QueryParam("offset") Integer offset, @Parameter( + ref = "reverse" + ) @QueryParam("reverse") Boolean reverse) { + // Decode orderID + byte[] orderId; + try { + orderId = Base58.decode(orderId58); + } catch (NumberFormatException e) { + throw ApiExceptionFactory.INSTANCE.createException(request, ApiError.INVALID_ORDER_ID, e); + } + + try (final Repository repository = RepositoryManager.getRepository()) { + OrderData orderData = repository.getAssetRepository().fromOrderId(orderId); + if (orderData == null) + throw ApiExceptionFactory.INSTANCE.createException(request, ApiError.ORDER_NO_EXISTS); + + return repository.getAssetRepository().getOrdersTrades(orderId, limit, offset, reverse); + } catch (DataException e) { + throw ApiExceptionFactory.INSTANCE.createException(request, ApiError.REPOSITORY_ISSUE, e); + } + } + + @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 getAssets(@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( + summary = "Asset orders created by this address", + responses = { + @ApiResponse( + description = "Asset orders", + content = @Content( + array = @ArraySchema( + schema = @Schema( + implementation = OrderData.class + ) + ) + ) + ) + } + ) + @ApiErrors({ + ApiError.INVALID_ADDRESS, ApiError.ADDRESS_NO_EXISTS, ApiError.REPOSITORY_ISSUE + }) + public List getAssetOrders(@PathParam("address") String address, @QueryParam("includeClosed") boolean includeClosed, + @QueryParam("includeFulfilled") boolean includeFulfilled, @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()) { + AccountData accountData = repository.getAccountRepository().getAccount(address); + + if (accountData == null) + throw ApiExceptionFactory.INSTANCE.createException(request, ApiError.ADDRESS_NO_EXISTS); + + byte[] publicKey = accountData.getPublicKey(); + if (publicKey == null) + throw ApiExceptionFactory.INSTANCE.createException(request, ApiError.ADDRESS_NO_EXISTS); + + return repository.getAssetRepository().getAccountsOrders(publicKey, includeClosed, includeFulfilled, limit, offset, reverse); + } catch (ApiException e) { + throw e; } catch (DataException e) { throw ApiExceptionFactory.INSTANCE.createException(request, ApiError.REPOSITORY_ISSUE, e); } @@ -237,11 +486,13 @@ public class AssetsResource { @Path("/order/delete") @Operation( summary = "Cancel existing asset order", - requestBody = @RequestBody( + requestBody = @RequestBody( required = true, content = @Content( mediaType = MediaType.APPLICATION_JSON, - schema = @Schema(implementation = CancelAssetOrderTransactionData.class) + schema = @Schema( + implementation = CancelAssetOrderTransactionData.class + ) ) ), responses = { @@ -256,7 +507,9 @@ public class AssetsResource { ) } ) - @ApiErrors({ApiError.TRANSFORMATION_ERROR, ApiError.REPOSITORY_ISSUE, ApiError.TRANSACTION_INVALID}) + @ApiErrors({ + ApiError.TRANSFORMATION_ERROR, ApiError.REPOSITORY_ISSUE, ApiError.TRANSACTION_INVALID + }) public String cancelOrder(CancelAssetOrderTransactionData transactionData) { try (final Repository repository = RepositoryManager.getRepository()) { Transaction transaction = Transaction.fromData(repository, transactionData); @@ -282,7 +535,9 @@ public class AssetsResource { required = true, content = @Content( mediaType = MediaType.APPLICATION_JSON, - schema = @Schema(implementation = IssueAssetTransactionData.class) + schema = @Schema( + implementation = IssueAssetTransactionData.class + ) ) ), responses = { @@ -297,7 +552,9 @@ public class AssetsResource { ) } ) - @ApiErrors({ApiError.TRANSFORMATION_ERROR, ApiError.REPOSITORY_ISSUE, ApiError.TRANSACTION_INVALID}) + @ApiErrors({ + ApiError.TRANSFORMATION_ERROR, ApiError.REPOSITORY_ISSUE, ApiError.TRANSACTION_INVALID + }) public String issueAsset(IssueAssetTransactionData transactionData) { try (final Repository repository = RepositoryManager.getRepository()) { Transaction transaction = Transaction.fromData(repository, transactionData); @@ -323,7 +580,9 @@ public class AssetsResource { required = true, content = @Content( mediaType = MediaType.APPLICATION_JSON, - schema = @Schema(implementation = CreateAssetOrderTransactionData.class) + schema = @Schema( + implementation = CreateAssetOrderTransactionData.class + ) ) ), responses = { @@ -338,7 +597,9 @@ public class AssetsResource { ) } ) - @ApiErrors({ApiError.TRANSFORMATION_ERROR, ApiError.REPOSITORY_ISSUE, ApiError.TRANSACTION_INVALID}) + @ApiErrors({ + ApiError.TRANSFORMATION_ERROR, ApiError.REPOSITORY_ISSUE, ApiError.TRANSACTION_INVALID + }) public String createOrder(CreateAssetOrderTransactionData transactionData) { try (final Repository repository = RepositoryManager.getRepository()) { Transaction transaction = Transaction.fromData(repository, transactionData); diff --git a/src/main/java/org/qora/api/resource/BlocksResource.java b/src/main/java/org/qora/api/resource/BlocksResource.java index c64be1e1..68aac5be 100644 --- a/src/main/java/org/qora/api/resource/BlocksResource.java +++ b/src/main/java/org/qora/api/resource/BlocksResource.java @@ -2,6 +2,7 @@ package org.qora.api.resource; import io.swagger.v3.oas.annotations.Operation; import io.swagger.v3.oas.annotations.Parameter; +import io.swagger.v3.oas.annotations.media.ArraySchema; import io.swagger.v3.oas.annotations.media.Content; import io.swagger.v3.oas.annotations.media.Schema; import io.swagger.v3.oas.annotations.responses.ApiResponse; @@ -10,7 +11,6 @@ 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; import javax.servlet.http.HttpServletRequest; @@ -26,7 +26,6 @@ import org.qora.api.ApiError; import org.qora.api.ApiErrors; import org.qora.api.ApiException; import org.qora.api.ApiExceptionFactory; -import org.qora.api.model.BlockWithTransactions; import org.qora.block.Block; import org.qora.data.block.BlockData; import org.qora.data.transaction.TransactionData; @@ -36,8 +35,12 @@ import org.qora.repository.RepositoryManager; import org.qora.utils.Base58; @Path("/blocks") -@Produces({MediaType.APPLICATION_JSON, MediaType.TEXT_PLAIN}) -@Tag(name = "Blocks") +@Produces({ + MediaType.APPLICATION_JSON, MediaType.TEXT_PLAIN +}) +@Tag( + name = "Blocks" +) public class BlocksResource { @Context @@ -53,14 +56,16 @@ public class BlocksResource { description = "the block", content = @Content( schema = @Schema( - implementation = BlockWithTransactions.class + implementation = BlockData.class ) ) ) } ) - @ApiErrors({ApiError.INVALID_SIGNATURE, ApiError.BLOCK_NO_EXISTS, ApiError.REPOSITORY_ISSUE}) - public BlockWithTransactions getBlock(@PathParam("signature") String signature58, @Parameter(ref = "includeTransactions") @QueryParam("includeTransactions") boolean includeTransactions) { + @ApiErrors({ + ApiError.INVALID_SIGNATURE, ApiError.BLOCK_NO_EXISTS, ApiError.REPOSITORY_ISSUE + }) + public BlockData getBlock(@PathParam("signature") String signature58) { // Decode signature byte[] signature; try { @@ -70,8 +75,55 @@ public class BlocksResource { } try (final Repository repository = RepositoryManager.getRepository()) { - BlockData blockData = repository.getBlockRepository().fromSignature(signature); - return packageBlockData(repository, blockData, includeTransactions); + return repository.getBlockRepository().fromSignature(signature); + } catch (ApiException e) { + throw e; + } catch (DataException e) { + throw ApiExceptionFactory.INSTANCE.createException(request, ApiError.REPOSITORY_ISSUE, e); + } + } + + @GET + @Path("/signature/{signature}/transactions") + @Operation( + summary = "Fetch block using base58 signature", + description = "Returns the block that matches the given signature", + responses = { + @ApiResponse( + description = "the block", + content = @Content( + array = @ArraySchema( + schema = @Schema( + implementation = TransactionData.class + ) + ) + ) + ) + } + ) + @ApiErrors({ + ApiError.INVALID_SIGNATURE, ApiError.BLOCK_NO_EXISTS, ApiError.REPOSITORY_ISSUE + }) + public List getBlockTransactions(@PathParam("signature") String signature58, @Parameter( + ref = "limit" + ) @QueryParam("limit") Integer limit, @Parameter( + ref = "offset" + ) @QueryParam("offset") Integer offset, @Parameter( + ref = "reverse" + ) @QueryParam("reverse") Boolean reverse) { + // Decode signature + byte[] signature; + try { + signature = Base58.decode(signature58); + } catch (NumberFormatException e) { + throw ApiExceptionFactory.INSTANCE.createException(request, ApiError.INVALID_SIGNATURE, e); + } + + try (final Repository repository = RepositoryManager.getRepository()) { + if (repository.getBlockRepository().getHeightFromSignature(signature) == 0) + throw ApiExceptionFactory.INSTANCE.createException(request, ApiError.BLOCK_NO_EXISTS); + + return repository.getBlockRepository().getTransactionsFromSignature(signature, limit, offset, reverse); } catch (ApiException e) { throw e; } catch (DataException e) { @@ -89,17 +141,18 @@ public class BlocksResource { description = "the block", content = @Content( schema = @Schema( - implementation = BlockWithTransactions.class + implementation = BlockData.class ) ) ) } ) - @ApiErrors({ApiError.BLOCK_NO_EXISTS, ApiError.REPOSITORY_ISSUE}) - public BlockWithTransactions getFirstBlock(@Parameter(ref = "includeTransactions") @QueryParam("includeTransactions") boolean includeTransactions) { + @ApiErrors({ + ApiError.BLOCK_NO_EXISTS, ApiError.REPOSITORY_ISSUE + }) + public BlockData getFirstBlock() { try (final Repository repository = RepositoryManager.getRepository()) { - BlockData blockData = repository.getBlockRepository().fromHeight(1); - return packageBlockData(repository, blockData, includeTransactions); + return repository.getBlockRepository().fromHeight(1); } catch (ApiException e) { throw e; } catch (DataException e) { @@ -117,17 +170,18 @@ public class BlocksResource { description = "the block", content = @Content( schema = @Schema( - implementation = BlockWithTransactions.class + implementation = BlockData.class ) ) ) } ) - @ApiErrors({ApiError.BLOCK_NO_EXISTS, ApiError.REPOSITORY_ISSUE}) - public BlockWithTransactions getLastBlock(@Parameter(ref = "includeTransactions") @QueryParam("includeTransactions") boolean includeTransactions) { + @ApiErrors({ + ApiError.BLOCK_NO_EXISTS, ApiError.REPOSITORY_ISSUE + }) + public BlockData getLastBlock() { try (final Repository repository = RepositoryManager.getRepository()) { - BlockData blockData = repository.getBlockRepository().getLastBlock(); - return packageBlockData(repository, blockData, includeTransactions); + return repository.getBlockRepository().getLastBlock(); } catch (ApiException e) { throw e; } catch (DataException e) { @@ -145,14 +199,16 @@ public class BlocksResource { description = "the block", content = @Content( schema = @Schema( - implementation = BlockWithTransactions.class + implementation = BlockData.class ) ) ) } ) - @ApiErrors({ApiError.INVALID_SIGNATURE, ApiError.BLOCK_NO_EXISTS, ApiError.REPOSITORY_ISSUE}) - public BlockWithTransactions getChild(@PathParam("signature") String signature58, @Parameter(ref = "includeTransactions") @QueryParam("includeTransactions") boolean includeTransactions) { + @ApiErrors({ + ApiError.INVALID_SIGNATURE, ApiError.BLOCK_NO_EXISTS, ApiError.REPOSITORY_ISSUE + }) + public BlockData getChild(@PathParam("signature") String signature58) { // Decode signature byte[] signature; try { @@ -170,8 +226,11 @@ public class BlocksResource { BlockData childBlockData = repository.getBlockRepository().fromReference(signature); - // Checking child exists is handled by packageBlockData() - return packageBlockData(repository, childBlockData, includeTransactions); + // Check child block exists + if (childBlockData == null) + throw ApiExceptionFactory.INSTANCE.createException(request, ApiError.BLOCK_NO_EXISTS); + + return childBlockData; } catch (ApiException e) { throw e; } catch (DataException e) { @@ -196,7 +255,9 @@ public class BlocksResource { ) } ) - @ApiErrors({ApiError.REPOSITORY_ISSUE}) + @ApiErrors({ + ApiError.REPOSITORY_ISSUE + }) public BigDecimal getGeneratingBalance() { try (final Repository repository = RepositoryManager.getRepository()) { BlockData blockData = repository.getBlockRepository().getLastBlock(); @@ -226,7 +287,9 @@ public class BlocksResource { ) } ) - @ApiErrors({ApiError.INVALID_SIGNATURE, ApiError.BLOCK_NO_EXISTS, ApiError.REPOSITORY_ISSUE}) + @ApiErrors({ + ApiError.INVALID_SIGNATURE, ApiError.BLOCK_NO_EXISTS, ApiError.REPOSITORY_ISSUE + }) public BigDecimal getGeneratingBalance(@PathParam("signature") String signature58) { // Decode signature byte[] signature; @@ -269,7 +332,9 @@ public class BlocksResource { ) } ) - @ApiErrors({ApiError.REPOSITORY_ISSUE}) + @ApiErrors({ + ApiError.REPOSITORY_ISSUE + }) public long getTimePerBlock() { try (final Repository repository = RepositoryManager.getRepository()) { BlockData blockData = repository.getBlockRepository().getLastBlock(); @@ -319,7 +384,9 @@ public class BlocksResource { ) } ) - @ApiErrors({ApiError.REPOSITORY_ISSUE}) + @ApiErrors({ + ApiError.REPOSITORY_ISSUE + }) public int getHeight() { try (final Repository repository = RepositoryManager.getRepository()) { return repository.getBlockRepository().getBlockchainHeight(); @@ -347,7 +414,9 @@ public class BlocksResource { ) } ) - @ApiErrors({ApiError.INVALID_SIGNATURE, ApiError.BLOCK_NO_EXISTS, ApiError.REPOSITORY_ISSUE}) + @ApiErrors({ + ApiError.INVALID_SIGNATURE, ApiError.BLOCK_NO_EXISTS, ApiError.REPOSITORY_ISSUE + }) public int getHeight(@PathParam("signature") String signature58) { // Decode signature byte[] signature; @@ -382,17 +451,22 @@ public class BlocksResource { description = "the block", content = @Content( schema = @Schema( - implementation = BlockWithTransactions.class + implementation = BlockData.class ) ) ) } ) - @ApiErrors({ApiError.BLOCK_NO_EXISTS, ApiError.REPOSITORY_ISSUE}) - public BlockWithTransactions getByHeight(@PathParam("height") int height, @Parameter(ref = "includeTransactions") @QueryParam("includeTransactions") boolean includeTransactions) { + @ApiErrors({ + ApiError.BLOCK_NO_EXISTS, ApiError.REPOSITORY_ISSUE + }) + public BlockData getByHeight(@PathParam("height") int height) { try (final Repository repository = RepositoryManager.getRepository()) { BlockData blockData = repository.getBlockRepository().fromHeight(height); - return packageBlockData(repository, blockData, includeTransactions); + if (blockData == null) + throw ApiExceptionFactory.INSTANCE.createException(request, ApiError.BLOCK_NO_EXISTS); + + return blockData; } catch (ApiException e) { throw e; } catch (DataException e) { @@ -409,19 +483,23 @@ public class BlocksResource { @ApiResponse( description = "blocks", content = @Content( - schema = @Schema( - implementation = BlockWithTransactions.class + array = @ArraySchema( + schema = @Schema( + implementation = BlockData.class + ) ) ) ) } ) - @ApiErrors({ApiError.BLOCK_NO_EXISTS, ApiError.REPOSITORY_ISSUE}) - public List getBlockRange(@PathParam("height") int height, @Parameter(ref = "count") @QueryParam("count") int count) { - boolean includeTransactions = false; - + @ApiErrors({ + ApiError.BLOCK_NO_EXISTS, ApiError.REPOSITORY_ISSUE + }) + public List getBlockRange(@PathParam("height") int height, @Parameter( + ref = "count" + ) @QueryParam("count") int count) { try (final Repository repository = RepositoryManager.getRepository()) { - List blocks = new ArrayList(); + List blocks = new ArrayList<>(); for (/* count already set */; count > 0; --count, ++height) { BlockData blockData = repository.getBlockRepository().fromHeight(height); @@ -429,7 +507,7 @@ public class BlocksResource { // Run out of blocks! break; - blocks.add(packageBlockData(repository, blockData, includeTransactions)); + blocks.add(blockData); } return blocks; @@ -440,29 +518,4 @@ public class BlocksResource { } } - /** - * Returns block, optionally including transactions. - *

- * Throws ApiException using ApiError.BLOCK_NO_EXISTS if blockData is null. - * - * @param repository - * @param blockData - * @param includeTransactions - * @return packaged block, with optional transactions - * @throws DataException - * @throws ApiException ApiError.BLOCK_NO_EXISTS - */ - private BlockWithTransactions packageBlockData(Repository repository, BlockData blockData, boolean includeTransactions) throws DataException { - if (blockData == null) - throw ApiExceptionFactory.INSTANCE.createException(request, ApiError.BLOCK_NO_EXISTS); - - List transactions = null; - if (includeTransactions) { - Block block = new Block(repository, blockData); - transactions = block.getTransactions().stream().map(transaction -> transaction.getTransactionData()).collect(Collectors.toList()); - } - - return new BlockWithTransactions(blockData, transactions); - } - } diff --git a/src/main/java/org/qora/api/resource/GroupsResource.java b/src/main/java/org/qora/api/resource/GroupsResource.java index cbf523b5..66d2e7cf 100644 --- a/src/main/java/org/qora/api/resource/GroupsResource.java +++ b/src/main/java/org/qora/api/resource/GroupsResource.java @@ -10,6 +10,7 @@ import io.swagger.v3.oas.annotations.responses.ApiResponse; import io.swagger.v3.oas.annotations.tags.Tag; import java.util.List; +import java.util.function.Predicate; import java.util.stream.Collectors; import javax.servlet.http.HttpServletRequest; @@ -25,8 +26,8 @@ import javax.ws.rs.core.MediaType; import org.qora.api.ApiError; import org.qora.api.ApiErrors; import org.qora.api.ApiExceptionFactory; -import org.qora.api.model.GroupWithMemberInfo; -import org.qora.api.model.GroupWithMemberInfo.MemberInfo; +import org.qora.api.model.GroupMembers; +import org.qora.api.model.GroupMembers.MemberInfo; import org.qora.crypto.Crypto; import org.qora.data.group.GroupAdminData; import org.qora.data.group.GroupBanData; @@ -86,14 +87,15 @@ public class GroupsResource { } ) @ApiErrors({ApiError.REPOSITORY_ISSUE}) - public List getAllGroups(@Parameter(ref = "limit") @QueryParam("limit") int limit, @Parameter(ref = "offset") @QueryParam("offset") int offset) { + public List getAllGroups(@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()) { - List groups = repository.getGroupRepository().getAllGroups(); - - // Pagination would take effect here (or as part of the repository access) - int fromIndex = Integer.min(offset, groups.size()); - int toIndex = limit == 0 ? groups.size() : Integer.min(fromIndex + limit, groups.size()); - groups = groups.subList(fromIndex, toIndex); + List groups = repository.getGroupRepository().getAllGroups(limit, offset, reverse); return groups; } catch (DataException e) { @@ -158,7 +160,7 @@ public class GroupsResource { } @GET - @Path("/{groupname}") + @Path("/{groupid}") @Operation( summary = "Info on group", responses = { @@ -166,45 +168,72 @@ public class GroupsResource { description = "group info", content = @Content( mediaType = MediaType.APPLICATION_JSON, - schema = @Schema(implementation = GroupWithMemberInfo.class) + schema = @Schema(implementation = GroupData.class) ) ) } ) @ApiErrors({ApiError.REPOSITORY_ISSUE}) - public GroupWithMemberInfo getGroup(@PathParam("groupname") String groupName, @QueryParam("includeMembers") boolean includeMembers) { + public GroupData getGroupData(@PathParam("groupid") int groupId) { try (final Repository repository = RepositoryManager.getRepository()) { - GroupData groupData = repository.getGroupRepository().fromGroupName(groupName); + GroupData groupData = repository.getGroupRepository().fromGroupId(groupId); if (groupData == null) throw ApiExceptionFactory.INSTANCE.createException(request, ApiError.GROUP_UNKNOWN); - List members = null; - Integer memberCount = null; - - if (includeMembers) { - List groupMembers = repository.getGroupRepository().getGroupMembers(groupData.getGroupId()); - - // Convert to MemberInfo - members = groupMembers.stream().map(groupMemberData -> new MemberInfo(groupMemberData)).collect(Collectors.toList()); - - memberCount = members.size(); - } else { - // Just count members instead - memberCount = repository.getGroupRepository().countGroupMembers(groupData.getGroupId()); - } - - // Always include admins - List groupAdmins = repository.getGroupRepository().getGroupAdmins(groupData.getGroupId()); - - // We only need admin addresses - List adminAddresses = groupAdmins.stream().map(groupAdminData -> groupAdminData.getAdmin()).collect(Collectors.toList()); - - return new GroupWithMemberInfo(groupData, adminAddresses, members, memberCount); + return groupData; } catch (DataException e) { throw ApiExceptionFactory.INSTANCE.createException(request, ApiError.REPOSITORY_ISSUE, e); } } + @GET + @Path("/members/{groupid}") + @Operation( + summary = "List group members", + responses = { + @ApiResponse( + description = "group info", + content = @Content( + mediaType = MediaType.APPLICATION_JSON, + schema = @Schema(implementation = GroupMembers.class) + ) + ) + } + ) + @ApiErrors({ApiError.REPOSITORY_ISSUE}) + public GroupMembers getGroup(@PathParam("groupid") int groupId, @QueryParam("onlyAdmins") Boolean onlyAdmins, + @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.getGroupRepository().groupExists(groupId)) + throw ApiExceptionFactory.INSTANCE.createException(request, ApiError.GROUP_UNKNOWN); + + int adminCount = repository.getGroupRepository().countGroupAdmins(groupId); + int memberCount = repository.getGroupRepository().countGroupMembers(groupId); + + if (onlyAdmins != null && onlyAdmins) { + // Shortcut + List admins = repository.getGroupRepository().getGroupAdmins(groupId, limit, offset, reverse); + + // Convert form + List membersInfo = admins.stream().map(admin -> new MemberInfo(admin.getAdmin(), null, true)).collect(Collectors.toList()); + + return new GroupMembers(membersInfo, memberCount, adminCount); + } + + final List admins = repository.getGroupRepository().getGroupAdmins(groupId, limit, offset, reverse); + + List members = repository.getGroupRepository().getGroupMembers(groupId, limit, offset, reverse); + + // Convert form + Predicate memberIsAdmin = member -> admins.stream().anyMatch(admin -> admin.getAdmin().equals(member.getMember())); + List membersInfo = members.stream().map(member -> new MemberInfo(member.getMember(), member.getJoined(), memberIsAdmin.test(member))).collect(Collectors.toList()); + + return new GroupMembers(membersInfo, memberCount, adminCount); + } catch (DataException e) { + throw ApiExceptionFactory.INSTANCE.createException(request, ApiError.REPOSITORY_ISSUE, e); + } + } @POST @Path("/create") diff --git a/src/main/java/org/qora/api/resource/NamesResource.java b/src/main/java/org/qora/api/resource/NamesResource.java index 0ecac9ae..95d19396 100644 --- a/src/main/java/org/qora/api/resource/NamesResource.java +++ b/src/main/java/org/qora/api/resource/NamesResource.java @@ -68,15 +68,12 @@ public class NamesResource { } ) @ApiErrors({ApiError.REPOSITORY_ISSUE}) - public List getAllNames(@Parameter(ref = "limit") @QueryParam("limit") int limit, @Parameter(ref = "offset") @QueryParam("offset") int offset) { + public List getAllNames(@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()) { - List names = repository.getNameRepository().getAllNames(); - - // Pagination would take effect here (or as part of the repository access) - int fromIndex = Integer.min(offset, names.size()); - int toIndex = limit == 0 ? names.size() : Integer.min(fromIndex + limit, names.size()); - names = names.subList(fromIndex, toIndex); + List names = repository.getNameRepository().getAllNames(limit, offset, reverse); + // Convert to summary return names.stream().map(nameData -> new NameSummary(nameData)).collect(Collectors.toList()); } catch (DataException e) { throw ApiExceptionFactory.INSTANCE.createException(request, ApiError.REPOSITORY_ISSUE, e); @@ -98,12 +95,13 @@ public class NamesResource { } ) @ApiErrors({ApiError.INVALID_ADDRESS, ApiError.REPOSITORY_ISSUE}) - public List getNamesByAddress(@PathParam("address") String address) { + public List getNamesByAddress(@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()) { - List names = repository.getNameRepository().getNamesByOwner(address); + List names = repository.getNameRepository().getNamesByOwner(address, limit, offset, reverse); return names.stream().map(nameData -> new NameSummary(nameData)).collect(Collectors.toList()); } catch (DataException e) { @@ -365,16 +363,10 @@ public class NamesResource { } ) @ApiErrors({ApiError.REPOSITORY_ISSUE}) - public List getNamesForSale(@Parameter(ref = "limit") @QueryParam("limit") int limit, @Parameter(ref = "offset") @QueryParam("offset") int offset) { + public List getNamesForSale(@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()) { - List names = repository.getNameRepository().getNamesForSale(); - - // Pagination would take effect here (or as part of the repository access) - int fromIndex = Integer.min(offset, names.size()); - int toIndex = limit == 0 ? names.size() : Integer.min(fromIndex + limit, names.size()); - names = names.subList(fromIndex, toIndex); - - return names; + return repository.getNameRepository().getNamesForSale(limit, offset, reverse); } catch (DataException e) { throw ApiExceptionFactory.INSTANCE.createException(request, ApiError.REPOSITORY_ISSUE, e); } diff --git a/src/main/java/org/qora/api/resource/TransactionsResource.java b/src/main/java/org/qora/api/resource/TransactionsResource.java index b3d165cf..278c255d 100644 --- a/src/main/java/org/qora/api/resource/TransactionsResource.java +++ b/src/main/java/org/qora/api/resource/TransactionsResource.java @@ -28,8 +28,6 @@ import org.qora.api.ApiErrors; import org.qora.api.ApiException; import org.qora.api.ApiExceptionFactory; import org.qora.api.model.SimpleTransactionSignRequest; -import org.qora.data.transaction.GenesisTransactionData; -import org.qora.data.transaction.PaymentTransactionData; import org.qora.data.transaction.TransactionData; import org.qora.globalization.Translator; import org.qora.repository.DataException; @@ -45,8 +43,12 @@ import org.qora.utils.Base58; import com.google.common.primitives.Bytes; @Path("/transactions") -@Produces({MediaType.APPLICATION_JSON, MediaType.TEXT_PLAIN}) -@Tag(name = "Transactions") +@Produces({ + MediaType.APPLICATION_JSON, MediaType.TEXT_PLAIN +}) +@Tag( + name = "Transactions" +) public class TransactionsResource { @Context @@ -68,7 +70,9 @@ public class TransactionsResource { ) } ) - @ApiErrors({ApiError.INVALID_SIGNATURE, ApiError.TRANSACTION_NO_EXISTS, ApiError.REPOSITORY_ISSUE}) + @ApiErrors({ + ApiError.INVALID_SIGNATURE, ApiError.TRANSACTION_NO_EXISTS, ApiError.REPOSITORY_ISSUE + }) public TransactionData getTransaction(@PathParam("signature") String signature58) { byte[] signature; try { @@ -100,12 +104,16 @@ public class TransactionsResource { description = "raw transaction encoded in Base58", content = @Content( mediaType = MediaType.TEXT_PLAIN, - schema = @Schema(type = "string") + schema = @Schema( + type = "string" + ) ) ) } ) - @ApiErrors({ApiError.INVALID_SIGNATURE, ApiError.TRANSACTION_NO_EXISTS, ApiError.REPOSITORY_ISSUE, ApiError.TRANSFORMATION_ERROR}) + @ApiErrors({ + ApiError.INVALID_SIGNATURE, ApiError.TRANSACTION_NO_EXISTS, ApiError.REPOSITORY_ISSUE, ApiError.TRANSFORMATION_ERROR + }) public String getRawTransaction(@PathParam("signature") String signature58) { byte[] signature; try { @@ -138,21 +146,28 @@ public class TransactionsResource { description = "Returns list of transactions", responses = { @ApiResponse( - description = "list of transactions", + description = "the block", content = @Content( array = @ArraySchema( schema = @Schema( - oneOf = { - GenesisTransactionData.class, PaymentTransactionData.class - } + implementation = TransactionData.class ) ) ) ) } ) - @ApiErrors({ApiError.INVALID_SIGNATURE, ApiError.BLOCK_NO_EXISTS, ApiError.REPOSITORY_ISSUE}) - public List getBlockTransactions(@PathParam("signature") String signature58, @Parameter(ref = "limit") @QueryParam("limit") int limit, @Parameter(ref = "offset") @QueryParam("offset") int offset) { + @ApiErrors({ + ApiError.INVALID_SIGNATURE, ApiError.BLOCK_NO_EXISTS, ApiError.REPOSITORY_ISSUE + }) + public List getBlockTransactions(@PathParam("signature") String signature58, @Parameter( + ref = "limit" + ) @QueryParam("limit") Integer limit, @Parameter( + ref = "offset" + ) @QueryParam("offset") Integer offset, @Parameter( + ref = "reverse" + ) @QueryParam("reverse") Boolean reverse) { + // Decode signature byte[] signature; try { signature = Base58.decode(signature58); @@ -161,18 +176,10 @@ public class TransactionsResource { } try (final Repository repository = RepositoryManager.getRepository()) { - List transactions = repository.getBlockRepository().getTransactionsFromSignature(signature); - - // check if block exists - if (transactions == null) + if (repository.getBlockRepository().getHeightFromSignature(signature) == 0) throw ApiExceptionFactory.INSTANCE.createException(request, ApiError.BLOCK_NO_EXISTS); - // Pagination would take effect here (or as part of the repository access) - int fromIndex = Integer.min(offset, transactions.size()); - int toIndex = limit == 0 ? transactions.size() : Integer.min(fromIndex + limit, transactions.size()); - transactions = transactions.subList(fromIndex, toIndex); - - return transactions; + return repository.getBlockRepository().getTransactionsFromSignature(signature, limit, offset, reverse); } catch (ApiException e) { throw e; } catch (DataException e) { @@ -198,10 +205,18 @@ public class TransactionsResource { ) } ) - @ApiErrors({ApiError.REPOSITORY_ISSUE}) - public List getUnconfirmedTransactions() { + @ApiErrors({ + ApiError.REPOSITORY_ISSUE + }) + public List getUnconfirmedTransactions(@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()) { - return repository.getTransactionRepository().getAllUnconfirmedTransactions(); + return repository.getTransactionRepository().getUnconfirmedTransactions(limit, offset, reverse); } catch (ApiException e) { throw e; } catch (DataException e) { @@ -209,23 +224,17 @@ public class TransactionsResource { } } + public enum ConfirmationStatus { + CONFIRMED, + UNCONFIRMED, + BOTH; + } + @GET @Path("/search") @Operation( summary = "Find matching transactions", - description = "Returns transactions that match criteria. At least either txType or address must be provided.", - /* - * parameters = { - * - * @Parameter(in = ParameterIn.QUERY, name = "txType", description = "Transaction type", schema = @Schema(type = "integer")), - * - * @Parameter(in = ParameterIn.QUERY, name = "address", description = "Account's address", schema = @Schema(type = "string")), - * - * @Parameter(in = ParameterIn.QUERY, name = "startBlock", description = "Start block height", schema = @Schema(type = "integer")), - * - * @Parameter(in = ParameterIn.QUERY, name = "blockLimit", description = "Maximum number of blocks to search", schema = @Schema(type = "integer")) - * }, - */ + description = "Returns transactions that match criteria. At least either txType or address or limit <= 20 must be provided. Block height ranges allowed when searching CONFIRMED transactions ONLY.", responses = { @ApiResponse( description = "transactions", @@ -239,30 +248,31 @@ public class TransactionsResource { ) } ) - @ApiErrors({ApiError.INVALID_CRITERIA, ApiError.REPOSITORY_ISSUE}) - public List searchTransactions(@QueryParam("startBlock") Integer startBlock, @QueryParam("blockLimit") Integer blockLimit, - @QueryParam("txType") Integer txTypeNum, @QueryParam("address") String address, @Parameter( + @ApiErrors({ + ApiError.INVALID_CRITERIA, ApiError.REPOSITORY_ISSUE + }) + public List searchTransactions(@QueryParam("startBlock") Integer startBlock, @QueryParam("blockLimit") Integer blockLimit, + @QueryParam("txType") TransactionType txType, @QueryParam("address") String address, @Parameter( + description = "whether to include confirmed, unconfirmed or both", + required = true + ) @QueryParam("confirmationStatus") ConfirmationStatus confirmationStatus, @Parameter( ref = "limit" - ) @QueryParam("limit") int limit, @Parameter( + ) @QueryParam("limit") Integer limit, @Parameter( ref = "offset" - ) @QueryParam("offset") int offset) { - if ((txTypeNum == null || txTypeNum == 0) && (address == null || address.isEmpty())) + ) @QueryParam("offset") Integer offset, @Parameter( + ref = "reverse" + ) @QueryParam("reverse") Boolean reverse) { + // Must have at least one of txType / address / limit <= 20 + if (txType == null && (address == null || address.isEmpty()) && (limit == null || limit > 20)) throw ApiExceptionFactory.INSTANCE.createException(request, ApiError.INVALID_CRITERIA); - TransactionType txType = null; - if (txTypeNum != null) { - txType = TransactionType.valueOf(txTypeNum); - if (txType == null) - throw ApiExceptionFactory.INSTANCE.createException(request, ApiError.INVALID_CRITERIA); - } + // You can't ask for unconfirmed and impose a block height range + if (confirmationStatus != ConfirmationStatus.CONFIRMED && (startBlock != null || blockLimit != null)) + throw ApiExceptionFactory.INSTANCE.createException(request, ApiError.INVALID_CRITERIA); try (final Repository repository = RepositoryManager.getRepository()) { - List signatures = repository.getTransactionRepository().getAllSignaturesMatchingCriteria(startBlock, blockLimit, txType, address); - - // Pagination would take effect here (or as part of the repository access) - int fromIndex = Integer.min(offset, signatures.size()); - int toIndex = limit == 0 ? signatures.size() : Integer.min(fromIndex + limit, signatures.size()); - signatures = signatures.subList(fromIndex, toIndex); + List signatures = repository.getTransactionRepository().getSignaturesMatchingCriteria(startBlock, blockLimit, txType, address, + confirmationStatus, limit, offset, reverse); // Expand signatures to transactions List transactions = new ArrayList(signatures.size()); @@ -302,7 +312,9 @@ public class TransactionsResource { ) } ) - @ApiErrors({ApiError.INVALID_PRIVATE_KEY, ApiError.TRANSFORMATION_ERROR}) + @ApiErrors({ + ApiError.INVALID_PRIVATE_KEY, ApiError.TRANSFORMATION_ERROR + }) public String signTransaction(SimpleTransactionSignRequest signRequest) { if (signRequest.transactionBytes.length == 0) throw ApiExceptionFactory.INSTANCE.createException(request, ApiError.JSON); @@ -356,7 +368,9 @@ public class TransactionsResource { ) } ) - @ApiErrors({ApiError.INVALID_SIGNATURE, ApiError.INVALID_DATA, ApiError.TRANSFORMATION_ERROR, ApiError.REPOSITORY_ISSUE}) + @ApiErrors({ + ApiError.INVALID_SIGNATURE, ApiError.INVALID_DATA, ApiError.TRANSFORMATION_ERROR, ApiError.REPOSITORY_ISSUE + }) public String processTransaction(String rawBytes58) { try (final Repository repository = RepositoryManager.getRepository()) { byte[] rawBytes = Base58.decode(rawBytes58); @@ -412,7 +426,9 @@ public class TransactionsResource { ) } ) - @ApiErrors({ApiError.INVALID_SIGNATURE, ApiError.INVALID_DATA, ApiError.TRANSACTION_INVALID, ApiError.TRANSFORMATION_ERROR, ApiError.REPOSITORY_ISSUE}) + @ApiErrors({ + ApiError.INVALID_SIGNATURE, ApiError.INVALID_DATA, ApiError.TRANSACTION_INVALID, ApiError.TRANSFORMATION_ERROR, ApiError.REPOSITORY_ISSUE + }) public TransactionData decodeTransaction(String rawBytes58, @QueryParam("ignoreValidityChecks") boolean ignoreValidityChecks) { try (final Repository repository = RepositoryManager.getRepository()) { byte[] rawBytes = Base58.decode(rawBytes58); diff --git a/src/main/java/org/qora/block/BlockGenerator.java b/src/main/java/org/qora/block/BlockGenerator.java index 54b757ba..ee8c3ddd 100644 --- a/src/main/java/org/qora/block/BlockGenerator.java +++ b/src/main/java/org/qora/block/BlockGenerator.java @@ -50,7 +50,7 @@ public class BlockGenerator extends Thread { try (final Repository repository = RepositoryManager.getRepository()) { if (Settings.getInstance().getWipeUnconfirmedOnStart()) { // Wipe existing unconfirmed transactions - List unconfirmedTransactions = repository.getTransactionRepository().getAllUnconfirmedTransactions(); + List unconfirmedTransactions = repository.getTransactionRepository().getUnconfirmedTransactions(); for (TransactionData transactionData : unconfirmedTransactions) { LOGGER.trace(String.format("Deleting unconfirmed transaction %s", Base58.encode(transactionData.getSignature()))); diff --git a/src/main/java/org/qora/controller/Controller.java b/src/main/java/org/qora/controller/Controller.java index a1d99ab3..1198b7d2 100644 --- a/src/main/java/org/qora/controller/Controller.java +++ b/src/main/java/org/qora/controller/Controller.java @@ -17,6 +17,11 @@ import org.qora.utils.Base58; public class Controller { + static { + // This must go before any calls to LogManager/Logger + System.setProperty("java.util.logging.manager", "org.apache.logging.log4j.jul.LogManager"); + } + private static final Logger LOGGER = LogManager.getLogger(Controller.class); public static final String connectionUrl = "jdbc:hsqldb:file:db/blockchain;create=true"; diff --git a/src/main/java/org/qora/data/transaction/TransactionData.java b/src/main/java/org/qora/data/transaction/TransactionData.java index 3d953dde..37666ecf 100644 --- a/src/main/java/org/qora/data/transaction/TransactionData.java +++ b/src/main/java/org/qora/data/transaction/TransactionData.java @@ -10,8 +10,9 @@ import javax.xml.bind.annotation.XmlElement; import javax.xml.bind.annotation.XmlSeeAlso; import javax.xml.bind.annotation.XmlTransient; -import org.eclipse.persistence.oxm.annotations.XmlClassExtractor; -import org.qora.api.TransactionClassExtractor; +// XXX are this still needed? see below +// import org.eclipse.persistence.oxm.annotations.XmlClassExtractor; +// import org.qora.api.TransactionClassExtractor; import org.qora.crypto.Crypto; import org.qora.transaction.Transaction.TransactionType; @@ -26,7 +27,8 @@ import io.swagger.v3.oas.annotations.media.Schema.AccessMode; * then chances are that class is missing a no-argument constructor! */ -@XmlClassExtractor(TransactionClassExtractor.class) +// XXX is this still in use? +// @XmlClassExtractor(TransactionClassExtractor.class) @XmlSeeAlso({GenesisTransactionData.class, PaymentTransactionData.class, RegisterNameTransactionData.class, UpdateNameTransactionData.class, SellNameTransactionData.class, CancelSellNameTransactionData.class, BuyNameTransactionData.class, CreatePollTransactionData.class, VoteOnPollTransactionData.class, ArbitraryTransactionData.class, diff --git a/src/main/java/org/qora/repository/AccountRepository.java b/src/main/java/org/qora/repository/AccountRepository.java index 6b94cec3..dde2738d 100644 --- a/src/main/java/org/qora/repository/AccountRepository.java +++ b/src/main/java/org/qora/repository/AccountRepository.java @@ -21,9 +21,17 @@ public interface AccountRepository { public AccountBalanceData getBalance(String address, long assetId) throws DataException; - public List getAllBalances(String address) throws DataException; + public List getAllBalances(String address, Integer limit, Integer offset, Boolean reverse) throws DataException; - public List getAssetBalances(long assetId) 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 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 ec4b7ece..6d718577 100644 --- a/src/main/java/org/qora/repository/AssetRepository.java +++ b/src/main/java/org/qora/repository/AssetRepository.java @@ -18,7 +18,11 @@ public interface AssetRepository { public boolean assetExists(String assetName) throws DataException; - public List getAllAssets() throws DataException; + public List getAllAssets(Integer limit, Integer offset, Boolean reverse) throws DataException; + + public default List getAllAssets() throws DataException { + return getAllAssets(null, null, null); + } // For a list of asset holders, see AccountRepository.getAssetBalances @@ -30,9 +34,18 @@ public interface AssetRepository { public OrderData fromOrderId(byte[] orderId) throws DataException; - public List getOpenOrders(long haveAssetId, long wantAssetId) throws DataException; + public List getOpenOrders(long haveAssetId, long wantAssetId, Integer limit, Integer offset, Boolean reverse) throws DataException; - public List getAccountsOrders(byte[] publicKey, boolean includeClosed, boolean includeFulfilled) throws DataException; + public default List getOpenOrders(long haveAssetId, long wantAssetId) throws DataException { + return getOpenOrders(haveAssetId, wantAssetId, null, null, null); + } + + public List getAccountsOrders(byte[] publicKey, boolean includeClosed, boolean includeFulfilled, 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 void save(OrderData orderData) throws DataException; @@ -40,10 +53,18 @@ public interface AssetRepository { // Trades - public List getTrades(long haveAssetId, long wantAssetId) throws DataException; + public List getTrades(long haveAssetId, long wantAssetId, Integer limit, Integer offset, Boolean reverse) throws DataException; + + public default List getTrades(long haveAssetId, long wantAssetId) throws DataException { + return getTrades(haveAssetId, wantAssetId, null, null, null); + } /** Returns TradeData for trades where orderId was involved, i.e. either initiating OR target order */ - public List getOrdersTrades(byte[] orderId) throws DataException; + public List getOrdersTrades(byte[] orderId, Integer limit, Integer offset, Boolean reverse) throws DataException; + + public default List getOrdersTrades(byte[] orderId) throws DataException { + return getOrdersTrades(orderId, null, null, null); + } public void save(TradeData tradeData) throws DataException; diff --git a/src/main/java/org/qora/repository/BlockRepository.java b/src/main/java/org/qora/repository/BlockRepository.java index 9eb6286b..65b85512 100644 --- a/src/main/java/org/qora/repository/BlockRepository.java +++ b/src/main/java/org/qora/repository/BlockRepository.java @@ -59,6 +59,17 @@ public interface BlockRepository { */ public BlockData getLastBlock() throws DataException; + /** + * Returns block's transactions given block's signature. + *

+ * This is typically used by API to fetch a block's transactions. + * + * @param signature + * @return list of transactions, or null if block not found in blockchain. + * @throws DataException + */ + public List getTransactionsFromSignature(byte[] signature, Integer limit, Integer offset, Boolean reverse) throws DataException; + /** * Returns block's transactions given block's signature. *

@@ -68,7 +79,9 @@ public interface BlockRepository { * @return list of transactions, or null if block not found in blockchain. * @throws DataException */ - public List getTransactionsFromSignature(byte[] signature) throws DataException; + public default List getTransactionsFromSignature(byte[] signature) throws DataException { + return getTransactionsFromSignature(signature, null, null, null); + } /** * Saves block into repository. diff --git a/src/main/java/org/qora/repository/GroupRepository.java b/src/main/java/org/qora/repository/GroupRepository.java index 6d35df89..bc8d57ec 100644 --- a/src/main/java/org/qora/repository/GroupRepository.java +++ b/src/main/java/org/qora/repository/GroupRepository.java @@ -21,11 +21,23 @@ public interface GroupRepository { public boolean groupExists(String groupName) throws DataException; - public List getAllGroups() throws DataException; + public List getAllGroups(Integer limit, Integer offset, Boolean reverse) throws DataException; - public List getGroupsByOwner(String address) throws DataException; + public default List getAllGroups() throws DataException { + return getAllGroups(null, null, null); + } - public List getGroupsWithMember(String member) throws DataException; + public List getGroupsByOwner(String address, Integer limit, Integer offset, Boolean reverse) throws DataException; + + public default List getGroupsByOwner(String address) throws DataException { + return getGroupsByOwner(address, null, null, null); + } + + public List getGroupsWithMember(String member, Integer limit, Integer offset, Boolean reverse) throws DataException; + + public default List getGroupsWithMember(String member) throws DataException { + return getGroupsWithMember(member, null, null, null); + } public void save(GroupData groupData) throws DataException; @@ -39,7 +51,14 @@ public interface GroupRepository { public boolean adminExists(int groupId, String address) throws DataException; - public List getGroupAdmins(int groupId) throws DataException; + public List getGroupAdmins(int groupId, Integer limit, Integer offset, Boolean reverse) throws DataException; + + public default List getGroupAdmins(int groupId) throws DataException { + return getGroupAdmins(groupId, null, null, null); + } + + /** Returns number of group admins, or null if group doesn't exist */ + public Integer countGroupAdmins(int groupId) throws DataException; public void save(GroupAdminData groupAdminData) throws DataException; @@ -51,7 +70,11 @@ public interface GroupRepository { public boolean memberExists(int groupId, String address) throws DataException; - public List getGroupMembers(int groupId) throws DataException; + public List getGroupMembers(int groupId, Integer limit, Integer offset, Boolean reverse) throws DataException; + + public default List getGroupMembers(int groupId) throws DataException { + return getGroupMembers(groupId, null, null, null); + } /** Returns number of group members, or null if group doesn't exist */ public Integer countGroupMembers(int groupId) throws DataException; @@ -66,9 +89,17 @@ public interface GroupRepository { public boolean inviteExists(int groupId, String invitee) throws DataException; - public List getInvitesByGroupId(int groupId) throws DataException; + public List getInvitesByGroupId(int groupId, Integer limit, Integer offset, Boolean reverse) throws DataException; - public List getInvitesByInvitee(String invitee) throws DataException; + public default List getInvitesByGroupId(int groupId) throws DataException { + return getInvitesByGroupId(groupId, null, null, null); + } + + public List getInvitesByInvitee(String invitee, Integer limit, Integer offset, Boolean reverse) throws DataException; + + public default List getInvitesByInvitee(String invitee) throws DataException { + return getInvitesByInvitee(invitee, null, null, null); + } public void save(GroupInviteData groupInviteData) throws DataException; @@ -80,7 +111,11 @@ public interface GroupRepository { public boolean joinRequestExists(int groupId, String joiner) throws DataException; - public List getGroupJoinRequests(int groupId) throws DataException; + public List getGroupJoinRequests(int groupId, Integer limit, Integer offset, Boolean reverse) throws DataException; + + public default List getGroupJoinRequests(int groupId) throws DataException { + return getGroupJoinRequests(groupId, null, null, null); + } public void save(GroupJoinRequestData groupJoinRequestData) throws DataException; @@ -92,7 +127,11 @@ public interface GroupRepository { public boolean banExists(int groupId, String offender) throws DataException; - public List getGroupBans(int groupId) throws DataException; + public List getGroupBans(int groupId, Integer limit, Integer offset, Boolean reverse) throws DataException; + + public default List getGroupBans(int groupId) throws DataException { + return getGroupBans(groupId, null, null, null); + } public void save(GroupBanData groupBanData) throws DataException; diff --git a/src/main/java/org/qora/repository/NameRepository.java b/src/main/java/org/qora/repository/NameRepository.java index 563cc3bf..3b22f7f6 100644 --- a/src/main/java/org/qora/repository/NameRepository.java +++ b/src/main/java/org/qora/repository/NameRepository.java @@ -10,11 +10,23 @@ public interface NameRepository { public boolean nameExists(String name) throws DataException; - public List getAllNames() throws DataException; + public List getAllNames(Integer limit, Integer offset, Boolean reverse) throws DataException; - public List getNamesForSale() throws DataException; + public default List getAllNames() throws DataException { + return getAllNames(null, null, null); + } - public List getNamesByOwner(String address) throws DataException; + public List getNamesForSale(Integer limit, Integer offset, Boolean reverse) throws DataException; + + public default List getNamesForSale() throws DataException { + return getNamesForSale(null, null, null); + } + + public List getNamesByOwner(String address, Integer limit, Integer offset, Boolean reverse) throws DataException; + + public default List getNamesByOwner(String address) throws DataException { + return getNamesByOwner(address, null, null, null); + } public void save(NameData nameData) throws DataException; diff --git a/src/main/java/org/qora/repository/TransactionRepository.java b/src/main/java/org/qora/repository/TransactionRepository.java index afaffe69..de4260dc 100644 --- a/src/main/java/org/qora/repository/TransactionRepository.java +++ b/src/main/java/org/qora/repository/TransactionRepository.java @@ -2,6 +2,7 @@ package org.qora.repository; import java.util.List; +import org.qora.api.resource.TransactionsResource.ConfirmationStatus; import org.qora.data.transaction.TransactionData; import org.qora.transaction.Transaction.TransactionType; @@ -20,7 +21,7 @@ public interface TransactionRepository { // Transaction participants - public List getAllSignaturesInvolvingAddress(String address) throws DataException; + public List getSignaturesInvolvingAddress(String address) throws DataException; public void saveParticipants(TransactionData transactionData, List participants) throws DataException; @@ -28,7 +29,21 @@ public interface TransactionRepository { // Searching transactions - public List getAllSignaturesMatchingCriteria(Integer startBlock, Integer blockLimit, TransactionType txType, String address) throws DataException; + public List getSignaturesMatchingCriteria(Integer startBlock, Integer blockLimit, TransactionType txType, String address, + ConfirmationStatus confirmationStatus, Integer limit, Integer offset, Boolean reverse) throws DataException; + + /** + * Returns list of unconfirmed transactions in timestamp-else-signature order. + *

+ * This is typically called by the API. + * + * @param limit + * @param offset + * @param reverse + * @return list of transactions, or empty if none. + * @throws DataException + */ + public List getUnconfirmedTransactions(Integer limit, Integer offset, Boolean reverse) throws DataException; /** * Returns list of unconfirmed transactions in timestamp-else-signature order. @@ -36,7 +51,9 @@ public interface TransactionRepository { * @return list of transactions, or empty if none. * @throws DataException */ - public List getAllUnconfirmedTransactions() throws DataException; + public default List getUnconfirmedTransactions() throws DataException { + return getUnconfirmedTransactions(null, null, null); + } /** * Remove transaction from unconfirmed transactions pile. diff --git a/src/main/java/org/qora/repository/hsqldb/HSQLDBAccountRepository.java b/src/main/java/org/qora/repository/hsqldb/HSQLDBAccountRepository.java index a28af6cf..9eaf6726 100644 --- a/src/main/java/org/qora/repository/hsqldb/HSQLDBAccountRepository.java +++ b/src/main/java/org/qora/repository/hsqldb/HSQLDBAccountRepository.java @@ -94,10 +94,15 @@ public class HSQLDBAccountRepository implements AccountRepository { } @Override - public List getAllBalances(String address) throws DataException { + 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); + List balances = new ArrayList(); - try (ResultSet resultSet = this.repository.checkedExecute("SELECT asset_id, balance FROM AccountBalances WHERE account = ?", address)) { + try (ResultSet resultSet = this.repository.checkedExecute(sql, address)) { if (resultSet == null) return balances; @@ -115,10 +120,15 @@ public class HSQLDBAccountRepository implements AccountRepository { } @Override - public List getAssetBalances(long assetId) throws DataException { + 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"; + if (reverse != null && reverse) + sql += " DESC"; + sql += HSQLDBRepository.limitOffsetSql(limit, offset); + List balances = new ArrayList(); - try (ResultSet resultSet = this.repository.checkedExecute("SELECT account, balance FROM AccountBalances WHERE asset_id = ?", assetId)) { + try (ResultSet resultSet = this.repository.checkedExecute(sql, assetId)) { if (resultSet == null) return balances; diff --git a/src/main/java/org/qora/repository/hsqldb/HSQLDBAssetRepository.java b/src/main/java/org/qora/repository/hsqldb/HSQLDBAssetRepository.java index dca24911..63f1e706 100644 --- a/src/main/java/org/qora/repository/hsqldb/HSQLDBAssetRepository.java +++ b/src/main/java/org/qora/repository/hsqldb/HSQLDBAssetRepository.java @@ -83,11 +83,15 @@ public class HSQLDBAssetRepository implements AssetRepository { } @Override - public List getAllAssets() throws DataException { + public List getAllAssets(Integer limit, Integer offset, Boolean reverse) throws DataException { + String sql = "SELECT owner, asset_id, description, quantity, is_divisible, reference, asset_name FROM Assets ORDER BY asset_name"; + if (reverse != null && reverse) + sql += " DESC"; + sql += HSQLDBRepository.limitOffsetSql(limit, offset); + List assets = new ArrayList(); - try (ResultSet resultSet = this.repository - .checkedExecute("SELECT owner, asset_id, description, quantity, is_divisible, reference, asset_name FROM Assets ORDER BY asset_id ASC")) { + try (ResultSet resultSet = this.repository.checkedExecute(sql)) { if (resultSet == null) return assets; @@ -170,13 +174,19 @@ public class HSQLDBAssetRepository implements AssetRepository { } @Override - public List getOpenOrders(long haveAssetId, long wantAssetId) throws DataException { + public List getOpenOrders(long haveAssetId, long wantAssetId, Integer limit, Integer offset, Boolean reverse) throws DataException { + String sql = "SELECT creator, asset_order_id, amount, fulfilled, price, ordered FROM AssetOrders " + + "WHERE have_asset_id = ? AND want_asset_id = ? AND is_closed = FALSE AND is_fulfilled = FALSE ORDER BY price"; + if (reverse != null && reverse) + sql += " DESC"; + sql += ", ordered"; + if (reverse != null && reverse) + sql += " DESC"; + sql += HSQLDBRepository.limitOffsetSql(limit, offset); + List orders = new ArrayList(); - try (ResultSet resultSet = this.repository.checkedExecute( - "SELECT creator, asset_order_id, amount, fulfilled, price, ordered FROM AssetOrders " - + "WHERE have_asset_id = ? AND want_asset_id = ? AND is_closed = FALSE AND is_fulfilled = FALSE ORDER BY price ASC, ordered ASC", - haveAssetId, wantAssetId)) { + try (ResultSet resultSet = this.repository.checkedExecute(sql, haveAssetId, wantAssetId)) { if (resultSet == null) return orders; @@ -202,16 +212,18 @@ public class HSQLDBAssetRepository implements AssetRepository { } @Override - public List getAccountsOrders(byte[] publicKey, boolean includeClosed, boolean includeFulfilled) throws DataException { - List orders = new ArrayList(); - - String sql = "SELECT asset_order_id, have_asset_id, want_asset_id, amount, fulfilled, price, ordered, is_closed, is_fulfilled " - + "FROM AssetOrders WHERE creator = ?"; + public List getAccountsOrders(byte[] publicKey, boolean includeClosed, boolean includeFulfilled, 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"; - sql += " ORDER BY ordered ASC"; + sql += " ORDER BY ordered"; + if (reverse != null && reverse) + sql += " DESC"; + sql += HSQLDBRepository.limitOffsetSql(limit, offset); + + List orders = new ArrayList(); try (ResultSet resultSet = this.repository.checkedExecute(sql, publicKey)) { if (resultSet == null) @@ -267,13 +279,16 @@ public class HSQLDBAssetRepository implements AssetRepository { // Trades @Override - public List getTrades(long haveAssetId, long wantAssetId) throws DataException { + public List getTrades(long haveAssetId, long wantAssetId, Integer limit, Integer offset, Boolean reverse) throws DataException { + String sql = "SELECT initiating_order_id, target_order_id, AssetTrades.amount, AssetTrades.price, traded FROM AssetOrders JOIN AssetTrades ON initiating_order_id = asset_order_id " + + "WHERE have_asset_id = ? AND want_asset_id = ? ORDER BY traded"; + if (reverse != null && reverse) + sql += " DESC"; + sql += HSQLDBRepository.limitOffsetSql(limit, offset); + List trades = new ArrayList(); - try (ResultSet resultSet = this.repository.checkedExecute( - "SELECT initiating_order_id, target_order_id, AssetTrades.amount, AssetTrades.price, traded FROM AssetOrders JOIN AssetTrades ON initiating_order_id = asset_order_id " - + "WHERE have_asset_id = ? AND want_asset_id = ? ORDER BY traded ASC", - haveAssetId, wantAssetId)) { + try (ResultSet resultSet = this.repository.checkedExecute(sql, haveAssetId, wantAssetId)) { if (resultSet == null) return trades; @@ -295,12 +310,15 @@ public class HSQLDBAssetRepository implements AssetRepository { } @Override - public List getOrdersTrades(byte[] orderId) throws DataException { + 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"; + if (reverse != null && reverse) + sql += " DESC"; + sql += HSQLDBRepository.limitOffsetSql(limit, offset); + List trades = new ArrayList(); - try (ResultSet resultSet = this.repository.checkedExecute( - "SELECT initiating_order_id, target_order_id, amount, price, traded FROM AssetTrades WHERE initiating_order_id = ? OR target_order_id = ?", - orderId, orderId)) { + try (ResultSet resultSet = this.repository.checkedExecute(sql, orderId, orderId)) { if (resultSet == null) return trades; diff --git a/src/main/java/org/qora/repository/hsqldb/HSQLDBBlockRepository.java b/src/main/java/org/qora/repository/hsqldb/HSQLDBBlockRepository.java index 4e3cefbf..1f92f831 100644 --- a/src/main/java/org/qora/repository/hsqldb/HSQLDBBlockRepository.java +++ b/src/main/java/org/qora/repository/hsqldb/HSQLDBBlockRepository.java @@ -108,10 +108,15 @@ public class HSQLDBBlockRepository implements BlockRepository { } @Override - public List getTransactionsFromSignature(byte[] signature) throws DataException { + public List getTransactionsFromSignature(byte[] signature, Integer limit, Integer offset, Boolean reverse) throws DataException { + String sql = "SELECT transaction_signature FROM BlockTransactions WHERE block_signature = ? ORDER BY sequence"; + if (reverse != null && reverse) + sql += " DESC"; + sql += HSQLDBRepository.limitOffsetSql(limit, offset); + List transactions = new ArrayList(); - try (ResultSet resultSet = this.repository.checkedExecute("SELECT transaction_signature FROM BlockTransactions WHERE block_signature = ?", signature)) { + try (ResultSet resultSet = this.repository.checkedExecute(sql, signature)) { if (resultSet == null) return transactions; // No transactions in this block diff --git a/src/main/java/org/qora/repository/hsqldb/HSQLDBGroupRepository.java b/src/main/java/org/qora/repository/hsqldb/HSQLDBGroupRepository.java index a6a5d4a0..e4795234 100644 --- a/src/main/java/org/qora/repository/hsqldb/HSQLDBGroupRepository.java +++ b/src/main/java/org/qora/repository/hsqldb/HSQLDBGroupRepository.java @@ -95,11 +95,15 @@ public class HSQLDBGroupRepository implements GroupRepository { } @Override - public List getAllGroups() throws DataException { + public List getAllGroups(Integer limit, Integer offset, Boolean reverse) throws DataException { + String sql = "SELECT group_id, owner, group_name, description, created, updated, reference, is_open FROM Groups ORDER BY group_name"; + if (reverse != null && reverse) + sql += " DESC"; + sql += HSQLDBRepository.limitOffsetSql(limit, offset); + List groups = new ArrayList<>(); - try (ResultSet resultSet = this.repository - .checkedExecute("SELECT group_id, owner, group_name, description, created, updated, reference, is_open FROM Groups")) { + try (ResultSet resultSet = this.repository.checkedExecute(sql)) { if (resultSet == null) return groups; @@ -127,11 +131,15 @@ public class HSQLDBGroupRepository implements GroupRepository { } @Override - public List getGroupsByOwner(String owner) throws DataException { + public List getGroupsByOwner(String owner, Integer limit, Integer offset, Boolean reverse) throws DataException { + String sql = "SELECT group_id, group_name, description, created, updated, reference, is_open FROM Groups WHERE owner = ? ORDER BY group_name"; + if (reverse != null && reverse) + sql += " DESC"; + sql += HSQLDBRepository.limitOffsetSql(limit, offset); + List groups = new ArrayList<>(); - try (ResultSet resultSet = this.repository - .checkedExecute("SELECT group_id, group_name, description, created, updated, reference, is_open FROM Groups WHERE owner = ?", owner)) { + try (ResultSet resultSet = this.repository.checkedExecute(sql, owner)) { if (resultSet == null) return groups; @@ -158,12 +166,15 @@ public class HSQLDBGroupRepository implements GroupRepository { } @Override - public List getGroupsWithMember(String member) throws DataException { + public List getGroupsWithMember(String member, Integer limit, Integer offset, Boolean reverse) throws DataException { + String sql = "SELECT group_id, owner, group_name, description, created, updated, reference, is_open FROM Groups JOIN GroupMembers USING (group_id) WHERE address = ? ORDER BY group_name"; + if (reverse != null && reverse) + sql += " DESC"; + sql += HSQLDBRepository.limitOffsetSql(limit, offset); + List groups = new ArrayList<>(); - try (ResultSet resultSet = this.repository.checkedExecute( - "SELECT group_id, owner, group_name, description, created, updated, reference, is_open FROM Groups JOIN GroupMembers USING (group_id) WHERE address = ?", - member)) { + try (ResultSet resultSet = this.repository.checkedExecute(sql, member)) { if (resultSet == null) return groups; @@ -266,10 +277,15 @@ public class HSQLDBGroupRepository implements GroupRepository { } @Override - public List getGroupAdmins(int groupId) throws DataException { + public List getGroupAdmins(int groupId, Integer limit, Integer offset, Boolean reverse) throws DataException { + String sql = "SELECT admin, reference FROM GroupAdmins WHERE group_id = ? ORDER BY admin"; + if (reverse != null && reverse) + sql += " DESC"; + sql += HSQLDBRepository.limitOffsetSql(limit, offset); + List admins = new ArrayList<>(); - try (ResultSet resultSet = this.repository.checkedExecute("SELECT admin, reference FROM GroupAdmins WHERE group_id = ?", groupId)) { + try (ResultSet resultSet = this.repository.checkedExecute(sql, groupId)) { if (resultSet == null) return admins; @@ -286,6 +302,21 @@ public class HSQLDBGroupRepository implements GroupRepository { } } + @Override + public Integer countGroupAdmins(int groupId) throws DataException { + try (ResultSet resultSet = this.repository.checkedExecute("SELECT COUNT(*) FROM GroupAdmins WHERE group_id = ?", groupId)) { + int count = resultSet.getInt(1); + + if (count == 0) + // There must be at least one admin: the group owner + return null; + + return count; + } catch (SQLException e) { + throw new DataException("Unable to fetch group admin count from repository", e); + } + } + @Override public void save(GroupAdminData groupAdminData) throws DataException { HSQLDBSaver saveHelper = new HSQLDBSaver("GroupAdmins"); @@ -336,10 +367,15 @@ public class HSQLDBGroupRepository implements GroupRepository { } @Override - public List getGroupMembers(int groupId) throws DataException { + public List getGroupMembers(int groupId, Integer limit, Integer offset, Boolean reverse) throws DataException { + String sql = "SELECT address, joined, reference FROM GroupMembers WHERE group_id = ? ORDER BY address"; + if (reverse != null && reverse) + sql += " DESC"; + sql += HSQLDBRepository.limitOffsetSql(limit, offset); + List members = new ArrayList<>(); - try (ResultSet resultSet = this.repository.checkedExecute("SELECT address, joined, reference FROM GroupMembers WHERE group_id = ?", groupId)) { + try (ResultSet resultSet = this.repository.checkedExecute(sql, groupId)) { if (resultSet == null) return members; @@ -359,13 +395,14 @@ public class HSQLDBGroupRepository implements GroupRepository { @Override public Integer countGroupMembers(int groupId) throws DataException { - // "GROUP BY" clause required to avoid error "expression not in aggregate or GROUP BY columns: PUBLIC.GROUPS.GROUP_ID" - try (ResultSet resultSet = this.repository.checkedExecute("SELECT group_id, COUNT(*) FROM GroupMembers WHERE group_id = ? GROUP BY group_id", - groupId)) { - if (resultSet == null) + try (ResultSet resultSet = this.repository.checkedExecute("SELECT COUNT(*) FROM GroupMembers WHERE group_id = ?", groupId)) { + int count = resultSet.getInt(1); + + if (count == 0) + // There must be at least one member: the group owner return null; - return resultSet.getInt(2); + return count; } catch (SQLException e) { throw new DataException("Unable to fetch group member count from repository", e); } @@ -425,10 +462,15 @@ public class HSQLDBGroupRepository implements GroupRepository { } @Override - public List getInvitesByGroupId(int groupId) throws DataException { + public List getInvitesByGroupId(int groupId, Integer limit, Integer offset, Boolean reverse) throws DataException { + String sql = "SELECT inviter, invitee, expiry, reference FROM GroupInvites WHERE group_id = ? ORDER BY invitee"; + if (reverse != null && reverse) + sql += " DESC"; + sql += HSQLDBRepository.limitOffsetSql(limit, offset); + List invites = new ArrayList<>(); - try (ResultSet resultSet = this.repository.checkedExecute("SELECT inviter, invitee, expiry, reference FROM GroupInvites WHERE group_id = ?", groupId)) { + try (ResultSet resultSet = this.repository.checkedExecute(sql, groupId)) { if (resultSet == null) return invites; @@ -451,10 +493,15 @@ public class HSQLDBGroupRepository implements GroupRepository { } @Override - public List getInvitesByInvitee(String invitee) throws DataException { + public List getInvitesByInvitee(String invitee, Integer limit, Integer offset, Boolean reverse) throws DataException { + String sql = "SELECT group_id, inviter, expiry, reference FROM GroupInvites WHERE invitee = ? ORDER BY group_id"; + if (reverse != null && reverse) + sql += " DESC"; + sql += HSQLDBRepository.limitOffsetSql(limit, offset); + List invites = new ArrayList<>(); - try (ResultSet resultSet = this.repository.checkedExecute("SELECT group_id, inviter, expiry, reference FROM GroupInvites WHERE invitee = ?", invitee)) { + try (ResultSet resultSet = this.repository.checkedExecute(sql, invitee)) { if (resultSet == null) return invites; @@ -530,10 +577,15 @@ public class HSQLDBGroupRepository implements GroupRepository { } @Override - public List getGroupJoinRequests(int groupId) throws DataException { + public List getGroupJoinRequests(int groupId, Integer limit, Integer offset, Boolean reverse) throws DataException { + String sql = "SELECT joiner, reference FROM GroupJoinRequests WHERE group_id = ? ORDER BY joiner"; + if (reverse != null && reverse) + sql += " DESC"; + sql += HSQLDBRepository.limitOffsetSql(limit, offset); + List joinRequests = new ArrayList<>(); - try (ResultSet resultSet = this.repository.checkedExecute("SELECT joiner, reference FROM GroupJoinRequests WHERE group_id = ?", groupId)) { + try (ResultSet resultSet = this.repository.checkedExecute(sql, groupId)) { if (resultSet == null) return joinRequests; @@ -604,11 +656,15 @@ public class HSQLDBGroupRepository implements GroupRepository { } @Override - public List getGroupBans(int groupId) throws DataException { + public List getGroupBans(int groupId, Integer limit, Integer offset, Boolean reverse) throws DataException { + String sql = "SELECT offender, admin, banned, reason, expiry, reference FROM GroupBans WHERE group_id = ? ORDER BY offender"; + if (reverse != null && reverse) + sql += " DESC"; + sql += HSQLDBRepository.limitOffsetSql(limit, offset); + List bans = new ArrayList<>(); - try (ResultSet resultSet = this.repository.checkedExecute("SELECT offender, admin, banned, reason, expiry, reference FROM GroupBans WHERE group_id = ?", - groupId)) { + try (ResultSet resultSet = this.repository.checkedExecute(sql, groupId)) { if (resultSet == null) return bans; diff --git a/src/main/java/org/qora/repository/hsqldb/HSQLDBNameRepository.java b/src/main/java/org/qora/repository/hsqldb/HSQLDBNameRepository.java index 26a3fed9..26446b0c 100644 --- a/src/main/java/org/qora/repository/hsqldb/HSQLDBNameRepository.java +++ b/src/main/java/org/qora/repository/hsqldb/HSQLDBNameRepository.java @@ -55,11 +55,15 @@ public class HSQLDBNameRepository implements NameRepository { } @Override - public List getAllNames() throws DataException { + public List getAllNames(Integer limit, Integer offset, Boolean reverse) throws DataException { + String sql = "SELECT name, data, owner, registered, updated, reference, is_for_sale, sale_price FROM Names ORDER BY name"; + if (reverse != null && reverse) + sql += " DESC"; + sql += HSQLDBRepository.limitOffsetSql(limit, offset); + List names = new ArrayList<>(); - try (ResultSet resultSet = this.repository - .checkedExecute("SELECT name, data, owner, registered, updated, reference, is_for_sale, sale_price FROM Names")) { + try (ResultSet resultSet = this.repository.checkedExecute(sql)) { if (resultSet == null) return names; @@ -87,11 +91,15 @@ public class HSQLDBNameRepository implements NameRepository { } @Override - public List getNamesForSale() throws DataException { + public List getNamesForSale(Integer limit, Integer offset, Boolean reverse) throws DataException { + String sql = "SELECT name, data, owner, registered, updated, reference, sale_price FROM Names WHERE is_for_sale = TRUE ORDER BY name"; + if (reverse != null && reverse) + sql += " DESC"; + sql += HSQLDBRepository.limitOffsetSql(limit, offset); + List names = new ArrayList<>(); - try (ResultSet resultSet = this.repository - .checkedExecute("SELECT name, data, owner, registered, updated, reference, sale_price FROM Names WHERE is_for_sale = TRUE")) { + try (ResultSet resultSet = this.repository.checkedExecute(sql)) { if (resultSet == null) return names; @@ -119,11 +127,15 @@ public class HSQLDBNameRepository implements NameRepository { } @Override - public List getNamesByOwner(String owner) throws DataException { + public List getNamesByOwner(String owner, Integer limit, Integer offset, Boolean reverse) throws DataException { + String sql = "SELECT name, data, registered, updated, reference, is_for_sale, sale_price FROM Names WHERE owner = ? ORDER BY name"; + if (reverse != null && reverse) + sql += " DESC"; + sql += HSQLDBRepository.limitOffsetSql(limit, offset); + List names = new ArrayList<>(); - try (ResultSet resultSet = this.repository - .checkedExecute("SELECT name, data, registered, updated, reference, is_for_sale, sale_price FROM Names WHERE owner = ?", owner)) { + try (ResultSet resultSet = this.repository.checkedExecute(sql, owner)) { if (resultSet == null) return names; @@ -157,9 +169,9 @@ public class HSQLDBNameRepository implements NameRepository { Long updated = nameData.getUpdated(); Timestamp updatedTimestamp = updated == null ? null : new Timestamp(updated); - saveHelper.bind("owner", nameData.getOwner()).bind("name", nameData.getName()) - .bind("data", nameData.getData()).bind("registered", new Timestamp(nameData.getRegistered())).bind("updated", updatedTimestamp) - .bind("reference", nameData.getReference()).bind("is_for_sale", nameData.getIsForSale()).bind("sale_price", nameData.getSalePrice()); + saveHelper.bind("owner", nameData.getOwner()).bind("name", nameData.getName()).bind("data", nameData.getData()) + .bind("registered", new Timestamp(nameData.getRegistered())).bind("updated", updatedTimestamp).bind("reference", nameData.getReference()) + .bind("is_for_sale", nameData.getIsForSale()).bind("sale_price", nameData.getSalePrice()); try { saveHelper.execute(this.repository); diff --git a/src/main/java/org/qora/repository/hsqldb/HSQLDBRepository.java b/src/main/java/org/qora/repository/hsqldb/HSQLDBRepository.java index 94082f44..3b7ddd7a 100644 --- a/src/main/java/org/qora/repository/hsqldb/HSQLDBRepository.java +++ b/src/main/java/org/qora/repository/hsqldb/HSQLDBRepository.java @@ -287,4 +287,25 @@ public class HSQLDBRepository implements Repository { } } + /** + * Returns additional SQL "LIMIT" and "OFFSET" clauses. + *

+ * (Convenience method for HSQLDB repository subclasses). + * + * @param limit + * @param offset + * @return SQL string, potentially empty but never null + */ + public static String limitOffsetSql(Integer limit, Integer offset) { + String sql = ""; + + if (limit != null && limit > 0) + sql += " LIMIT " + limit; + + if (offset != null) + sql += " OFFSET " + offset; + + return sql; + } + } diff --git a/src/main/java/org/qora/repository/hsqldb/transaction/HSQLDBTransactionRepository.java b/src/main/java/org/qora/repository/hsqldb/transaction/HSQLDBTransactionRepository.java index 569ce8a4..21cd49e3 100644 --- a/src/main/java/org/qora/repository/hsqldb/transaction/HSQLDBTransactionRepository.java +++ b/src/main/java/org/qora/repository/hsqldb/transaction/HSQLDBTransactionRepository.java @@ -13,6 +13,7 @@ import java.util.List; import org.apache.logging.log4j.LogManager; import org.apache.logging.log4j.Logger; +import org.qora.api.resource.TransactionsResource.ConfirmationStatus; import org.qora.data.PaymentData; import org.qora.data.transaction.TransactionData; import org.qora.repository.DataException; @@ -53,7 +54,8 @@ public class HSQLDBTransactionRepository implements TransactionRepository { private static Class getClassByTxType(TransactionType txType) { try { - return Class.forName(String.join("", HSQLDBTransactionRepository.class.getPackage().getName(), ".", "HSQLDB", txType.className, "TransactionRepository")); + return Class.forName( + String.join("", HSQLDBTransactionRepository.class.getPackage().getName(), ".", "HSQLDB", txType.className, "TransactionRepository")); } catch (ClassNotFoundException e) { return null; } @@ -204,7 +206,7 @@ public class HSQLDBTransactionRepository implements TransactionRepository { } @Override - public List getAllSignaturesInvolvingAddress(String address) throws DataException { + public List getSignaturesInvolvingAddress(String address) throws DataException { List signatures = new ArrayList(); try (ResultSet resultSet = this.repository.checkedExecute("SELECT signature FROM TransactionRecipients WHERE participant = ?", address)) { @@ -250,7 +252,8 @@ public class HSQLDBTransactionRepository implements TransactionRepository { } @Override - public List getAllSignaturesMatchingCriteria(Integer startBlock, Integer blockLimit, TransactionType txType, String address) throws DataException { + public List getSignaturesMatchingCriteria(Integer startBlock, Integer blockLimit, TransactionType txType, String address, + ConfirmationStatus confirmationStatus, Integer limit, Integer offset, Boolean reverse) throws DataException { List signatures = new ArrayList(); boolean hasAddress = address != null && !address.isEmpty(); @@ -258,33 +261,42 @@ public class HSQLDBTransactionRepository implements TransactionRepository { boolean hasHeightRange = startBlock != null || blockLimit != null; if (hasHeightRange && startBlock == null) - startBlock = 1; + startBlock = (reverse == null || !reverse) ? 1 : this.repository.getBlockRepository().getBlockchainHeight() - blockLimit; - String signatureColumn = "NULL"; - List bindParams = new ArrayList(); + String signatureColumn = "Transactions.signature"; + List whereClauses = new ArrayList(); String groupBy = ""; + List bindParams = new ArrayList(); - // Table JOINs first - List tableJoins = new ArrayList(); + // Tables, starting with Transactions + String tables = "Transactions"; - // Always JOIN BlockTransactions as we only ever want confirmed transactions - tableJoins.add("Blocks"); - tableJoins.add("BlockTransactions ON BlockTransactions.block_signature = Blocks.signature"); - signatureColumn = "BlockTransactions.transaction_signature"; + // BlockTransactions if we want confirmed transactions + switch (confirmationStatus) { + case BOTH: + break; - // Always JOIN Transactions as we want to order by timestamp - tableJoins.add("Transactions ON Transactions.signature = BlockTransactions.transaction_signature"); - signatureColumn = "Transactions.signature"; + case CONFIRMED: + tables += " JOIN BlockTransactions ON BlockTransactions.transaction_signature = Transactions.signature"; + + if (hasHeightRange) + tables += " JOIN Blocks ON Blocks.signature = BlockTransactions.block_signature"; + + break; + + case UNCONFIRMED: + tables += " LEFT OUTER JOIN BlockTransactions ON BlockTransactions.transaction_signature = Transactions.signature"; + whereClauses.add("BlockTransactions.transaction_signature IS NULL"); + break; + } if (hasAddress) { - tableJoins.add("TransactionParticipants ON TransactionParticipants.signature = Transactions.signature"); - signatureColumn = "TransactionParticipants.signature"; + tables += " JOIN TransactionParticipants ON TransactionParticipants.signature = Transactions.signature"; groupBy = " GROUP BY TransactionParticipants.signature, Transactions.creation"; + signatureColumn = "TransactionParticipants.signature"; } // WHERE clauses next - List whereClauses = new ArrayList(); - if (hasHeightRange) { whereClauses.add("Blocks.height >= " + startBlock); @@ -300,7 +312,19 @@ public class HSQLDBTransactionRepository implements TransactionRepository { bindParams.add(address); } - String sql = "SELECT " + signatureColumn + " FROM " + String.join(" JOIN ", tableJoins) + " WHERE " + String.join(" AND ", whereClauses) + groupBy + " ORDER BY Transactions.creation ASC"; + String sql = "SELECT " + signatureColumn + " FROM " + tables; + + if (!whereClauses.isEmpty()) + sql += " WHERE " + String.join(" AND ", whereClauses); + + if (!groupBy.isEmpty()) + sql += groupBy; + + sql += " ORDER BY Transactions.creation"; + sql += (reverse == null || !reverse) ? " ASC" : " DESC"; + + sql += HSQLDBRepository.limitOffsetSql(limit, offset); + LOGGER.trace(sql); try (ResultSet resultSet = this.repository.checkedExecute(sql, bindParams.toArray())) { @@ -320,11 +344,19 @@ public class HSQLDBTransactionRepository implements TransactionRepository { } @Override - public List getAllUnconfirmedTransactions() throws DataException { + public List getUnconfirmedTransactions(Integer limit, Integer offset, Boolean reverse) throws DataException { + String sql = "SELECT signature FROM UnconfirmedTransactions ORDER BY creation"; + if (reverse != null && reverse) + sql += " DESC"; + sql += ", signature"; + if (reverse != null && reverse) + sql += " DESC"; + sql += HSQLDBRepository.limitOffsetSql(limit, offset); + List transactions = new ArrayList(); // Find transactions with no corresponding row in BlockTransactions - try (ResultSet resultSet = this.repository.checkedExecute("SELECT signature FROM UnconfirmedTransactions ORDER BY creation ASC, signature ASC")) { + try (ResultSet resultSet = this.repository.checkedExecute(sql)) { if (resultSet == null) return transactions; diff --git a/src/main/java/org/qora/transaction/Transaction.java b/src/main/java/org/qora/transaction/Transaction.java index efc23512..a68a4b85 100644 --- a/src/main/java/org/qora/transaction/Transaction.java +++ b/src/main/java/org/qora/transaction/Transaction.java @@ -468,7 +468,7 @@ public abstract class Transaction { } private int countUnconfirmedByCreator(PublicKeyAccount creator) throws DataException { - List unconfirmedTransactions = repository.getTransactionRepository().getAllUnconfirmedTransactions(); + List unconfirmedTransactions = repository.getTransactionRepository().getUnconfirmedTransactions(); int count = 0; for (TransactionData transactionData : unconfirmedTransactions) { @@ -495,7 +495,7 @@ public abstract class Transaction { public static List getUnconfirmedTransactions(Repository repository) throws DataException { BlockData latestBlockData = repository.getBlockRepository().getLastBlock(); - List unconfirmedTransactions = repository.getTransactionRepository().getAllUnconfirmedTransactions(); + List unconfirmedTransactions = repository.getTransactionRepository().getUnconfirmedTransactions(); List invalidTransactions = new ArrayList<>(); unconfirmedTransactions.sort(getDataComparator());