3
0
mirror of https://github.com/Qortal/qortal.git synced 2025-02-15 03:35:49 +00:00

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.
This commit is contained in:
catbref 2019-01-24 16:42:55 +00:00
parent 782bc2000f
commit 4be58514c0
31 changed files with 1020 additions and 582 deletions

View File

@ -2,5 +2,6 @@ eclipse.preferences.version=1
encoding//src/main/java=UTF-8 encoding//src/main/java=UTF-8
encoding//src/main/resources=UTF-8 encoding//src/main/resources=UTF-8
encoding//src/test/java=UTF-8 encoding//src/test/java=UTF-8
encoding//target/generated-sources/package-info=UTF-8
encoding/<project>=UTF-8 encoding/<project>=UTF-8
encoding/src=UTF-8 encoding/src=UTF-8

View File

@ -9,17 +9,14 @@ rootLogger.appenderRef.rolling.ref = FILE
# Override HSQLDB logging level to "warn" as too much is logged at "info" # Override HSQLDB logging level to "warn" as too much is logged at "info"
logger.hsqldb.name = hsqldb.db logger.hsqldb.name = hsqldb.db
logger.hsqldb.level = warn logger.hsqldb.level = warn
logger.hsqldb.appenderRef.rolling.ref = FILE
# Override logging level for this class # Suppress extraneous Jersey warning
logger.voting.name = qora.transaction.VoteOnPollTransaction logger.jerseyInject.name = org.glassfish.jersey.internal.inject.Providers
logger.voting.level = trace logger.jerseyInject.level = error
logger.voting.appenderRef.rolling.ref = FILE
# Override logging level for this class # Debugging transaction searches
logger.assets.name = qora.assets.Order logger.txSearch.name = org.qora.repository.hsqldb.transaction.HSQLDBTransactionRepository
logger.assets.level = trace logger.txSearch.level = trace
logger.assets.appenderRef.rolling.ref = FILE
appender.console.type = Console appender.console.type = Console
appender.console.name = stdout appender.console.name = stdout

19
pom.xml
View File

@ -305,17 +305,24 @@
<artifactId>log4j-api</artifactId> <artifactId>log4j-api</artifactId>
<version>${log4j.version}</version> <version>${log4j.version}</version>
</dependency> </dependency>
<!-- Logging: slf4j used by Jetty --> <!-- redirect slf4j to log4j2 -->
<dependency>
<groupId>org.apache.logging.log4j</groupId>
<artifactId>log4j-slf4j-impl</artifactId>
<version>${log4j.version}</version>
</dependency>
<!-- redirect java.utils.logging to log4j2 -->
<dependency>
<groupId>org.apache.logging.log4j</groupId>
<artifactId>log4j-jul</artifactId>
<version>${log4j.version}</version>
</dependency>
<!-- Logging: slf4j used by Jetty/Jersey -->
<dependency> <dependency>
<groupId>org.slf4j</groupId> <groupId>org.slf4j</groupId>
<artifactId>slf4j-api</artifactId> <artifactId>slf4j-api</artifactId>
<version>${slf4j.version}</version> <version>${slf4j.version}</version>
</dependency> </dependency>
<dependency>
<groupId>org.slf4j</groupId>
<artifactId>slf4j-simple</artifactId>
<version>${slf4j.version}</version>
</dependency>
<!-- Servlet related --> <!-- Servlet related -->
<dependency> <dependency>
<groupId>javax.servlet</groupId> <groupId>javax.servlet</groupId>

View File

@ -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;
}
}

View File

@ -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<AccountBalanceData> holders;
// For JAX-RS
protected AssetWithHolders() {
}
public AssetWithHolders(AssetData assetData, List<AccountBalanceData> holders) {
this.assetData = assetData;
this.holders = holders;
}
}

View File

@ -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<TransactionData> transactions;
// For JAX-RS
protected BlockWithTransactions() {
}
public BlockWithTransactions(BlockData blockData, List<TransactionData> transactions) {
this.blockData = blockData;
this.transactions = transactions;
}
}

View File

@ -6,38 +6,31 @@ import javax.xml.bind.annotation.XmlAccessType;
import javax.xml.bind.annotation.XmlAccessorType; import javax.xml.bind.annotation.XmlAccessorType;
import javax.xml.bind.annotation.XmlElement; 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; import io.swagger.v3.oas.annotations.media.Schema;
@Schema(description = "Group info, maybe including members") @Schema(description = "Group info, maybe including members")
// All properties to be converted to JSON via JAX-RS // All properties to be converted to JSON via JAX-RS
@XmlAccessorType(XmlAccessType.FIELD) @XmlAccessorType(XmlAccessType.FIELD)
public class GroupWithMemberInfo { public class GroupMembers {
@Schema(implementation = GroupData.class, name = "group", title = "group info")
@XmlElement(name = "group")
public GroupData groupData;
Integer memberCount; Integer memberCount;
Integer adminCount;
@XmlElement(name = "admins")
public List<String> groupAdminAddresses;
@XmlAccessorType(XmlAccessType.FIELD) @XmlAccessorType(XmlAccessType.FIELD)
@Schema(description = "Member info") @Schema(description = "Member info")
public static class MemberInfo { public static class MemberInfo {
public String member; public String member;
public long joined; public Long joined;
public Boolean isAdmin;
// For JAX-RS // For JAX-RS
protected MemberInfo() { protected MemberInfo() {
} }
public MemberInfo(GroupMemberData groupMemberData) { public MemberInfo(String member, Long joined, boolean isAdmin) {
this.member = groupMemberData.getMember(); this.member = member;
this.joined = groupMemberData.getJoined(); 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<MemberInfo> groupMembers; public List<MemberInfo> groupMembers;
// For JAX-RS // For JAX-RS
protected GroupWithMemberInfo() { protected GroupMembers() {
} }
public GroupWithMemberInfo(GroupData groupData, List<String> groupAdminAddresses, List<MemberInfo> groupMembers, Integer memberCount) { public GroupMembers(List<MemberInfo> groupMembers, Integer memberCount, Integer adminCount) {
this.groupData = groupData;
this.groupAdminAddresses = groupAdminAddresses;
this.groupMembers = groupMembers; this.groupMembers = groupMembers;
this.memberCount = memberCount; this.memberCount = memberCount;
this.adminCount = adminCount;
} }
} }

View File

@ -1,21 +1,18 @@
package org.qora.api.resource; package org.qora.api.resource;
import io.swagger.v3.oas.annotations.Operation; 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.Content;
import io.swagger.v3.oas.annotations.media.Schema; import io.swagger.v3.oas.annotations.media.Schema;
import io.swagger.v3.oas.annotations.responses.ApiResponse; import io.swagger.v3.oas.annotations.responses.ApiResponse;
import io.swagger.v3.oas.annotations.tags.Tag; import io.swagger.v3.oas.annotations.tags.Tag;
import java.math.BigDecimal; import java.math.BigDecimal;
import java.util.List;
import javax.servlet.http.HttpServletRequest; import javax.servlet.http.HttpServletRequest;
import javax.ws.rs.GET; import javax.ws.rs.GET;
import javax.ws.rs.Path; import javax.ws.rs.Path;
import javax.ws.rs.PathParam; import javax.ws.rs.PathParam;
import javax.ws.rs.Produces; import javax.ws.rs.Produces;
import javax.ws.rs.QueryParam;
import javax.ws.rs.core.Context; import javax.ws.rs.core.Context;
import javax.ws.rs.core.MediaType; import javax.ws.rs.core.MediaType;
@ -26,9 +23,7 @@ import org.qora.api.ApiException;
import org.qora.api.ApiExceptionFactory; import org.qora.api.ApiExceptionFactory;
import org.qora.asset.Asset; import org.qora.asset.Asset;
import org.qora.crypto.Crypto; import org.qora.crypto.Crypto;
import org.qora.data.account.AccountBalanceData;
import org.qora.data.account.AccountData; import org.qora.data.account.AccountData;
import org.qora.data.asset.OrderData;
import org.qora.repository.DataException; import org.qora.repository.DataException;
import org.qora.repository.Repository; import org.qora.repository.Repository;
import org.qora.repository.RepositoryManager; 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<AccountBalanceData> 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 @GET
@Path("/balance/{address}/{confirmations}") @Path("/balance/{address}/{confirmations}")
@Operation( @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<OrderData> 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);
}
}
} }

View File

@ -28,16 +28,13 @@ public class AdminResource {
@GET @GET
@Path("/unused") @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 = "integer"))
@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 = "integer"))
@Parameter(in = ParameterIn.PATH, name = "otherAssetId", description = "Asset ID, 0 is native coin", schema = @Schema(type = "string", format = "byte"))
@Parameter(in = ParameterIn.PATH, name = "address", description = "an account address", example = "QgV4s3xnzLhVBEJxcYui4u4q11yhUHsd9v") @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 = "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 = "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 = "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 = "reverse", description = "Reverse 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"))
public String globalParameters() { public String globalParameters() {
return ""; return "";
} }

View File

@ -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.responses.ApiResponse;
import io.swagger.v3.oas.annotations.tags.Tag; import io.swagger.v3.oas.annotations.tags.Tag;
import java.math.BigDecimal;
import java.util.ArrayList; import java.util.ArrayList;
import java.util.List; import java.util.List;
@ -22,13 +23,15 @@ import javax.ws.rs.QueryParam;
import javax.ws.rs.core.Context; import javax.ws.rs.core.Context;
import javax.ws.rs.core.MediaType; import javax.ws.rs.core.MediaType;
import org.qora.account.Account;
import org.qora.api.ApiError; import org.qora.api.ApiError;
import org.qora.api.ApiErrors; import org.qora.api.ApiErrors;
import org.qora.api.ApiException;
import org.qora.api.ApiExceptionFactory; 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.api.model.TradeWithOrderInfo;
import org.qora.crypto.Crypto;
import org.qora.data.account.AccountBalanceData; import org.qora.data.account.AccountBalanceData;
import org.qora.data.account.AccountData;
import org.qora.data.asset.AssetData; import org.qora.data.asset.AssetData;
import org.qora.data.asset.OrderData; import org.qora.data.asset.OrderData;
import org.qora.data.asset.TradeData; import org.qora.data.asset.TradeData;
@ -47,8 +50,12 @@ import org.qora.transform.transaction.IssueAssetTransactionTransformer;
import org.qora.utils.Base58; import org.qora.utils.Base58;
@Path("/assets") @Path("/assets")
@Produces({MediaType.APPLICATION_JSON, MediaType.TEXT_PLAIN}) @Produces({
@Tag(name = "Assets") MediaType.APPLICATION_JSON, MediaType.TEXT_PLAIN
})
@Tag(
name = "Assets"
)
public class AssetsResource { public class AssetsResource {
@Context @Context
@ -60,21 +67,28 @@ public class AssetsResource {
responses = { responses = {
@ApiResponse( @ApiResponse(
description = "asset info", 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}) @ApiErrors({
public List<AssetData> getAllAssets(@Parameter(ref = "limit") @QueryParam("limit") int limit, @Parameter(ref = "offset") @QueryParam("offset") int offset) { ApiError.REPOSITORY_ISSUE
})
public List<AssetData> 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()) { try (final Repository repository = RepositoryManager.getRepository()) {
List<AssetData> assets = repository.getAssetRepository().getAllAssets(); return repository.getAssetRepository().getAllAssets(limit, offset, reverse);
// 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;
} catch (DataException e) { } catch (DataException e) {
throw ApiExceptionFactory.INSTANCE.createException(request, ApiError.REPOSITORY_ISSUE, e); throw ApiExceptionFactory.INSTANCE.createException(request, ApiError.REPOSITORY_ISSUE, e);
} }
@ -88,12 +102,18 @@ public class AssetsResource {
responses = { responses = {
@ApiResponse( @ApiResponse(
description = "asset info", 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}) @ApiErrors({
public AssetWithHolders getAssetInfo(@QueryParam("assetId") Integer assetId, @QueryParam("assetName") String assetName, @Parameter(ref = "includeHolders") @QueryParam("includeHolders") boolean includeHolders) { 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())) if (assetId == null && (assetName == null || assetName.isEmpty()))
throw ApiExceptionFactory.INSTANCE.createException(request, ApiError.INVALID_CRITERIA); throw ApiExceptionFactory.INSTANCE.createException(request, ApiError.INVALID_CRITERIA);
@ -108,31 +128,81 @@ public class AssetsResource {
if (assetData == null) if (assetData == null)
throw ApiExceptionFactory.INSTANCE.createException(request, ApiError.INVALID_ASSET_ID); throw ApiExceptionFactory.INSTANCE.createException(request, ApiError.INVALID_ASSET_ID);
List<AccountBalanceData> holders = null; return assetData;
if (includeHolders)
holders = repository.getAccountRepository().getAssetBalances(assetData.getAssetId());
return new AssetWithHolders(assetData, holders);
} catch (DataException e) { } catch (DataException e) {
throw ApiExceptionFactory.INSTANCE.createException(request, ApiError.REPOSITORY_ISSUE, e); throw ApiExceptionFactory.INSTANCE.createException(request, ApiError.REPOSITORY_ISSUE, e);
} }
} }
@GET @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<AccountBalanceData> 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( @Operation(
summary = "Asset order book", 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 = { responses = {
@ApiResponse( @ApiResponse(
description = "asset orders", 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}) @ApiErrors({
public List<OrderData> getAssetOrders(@Parameter(ref = "assetId") @PathParam("assetId") int assetId, @Parameter(ref = "otherAssetId") @PathParam("otherAssetId") int otherAssetId, ApiError.INVALID_ASSET_ID, ApiError.REPOSITORY_ISSUE
@Parameter(ref = "limit") @QueryParam("limit") int limit, @Parameter(ref = "offset") @QueryParam("offset") int offset) { })
public List<OrderData> 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()) { try (final Repository repository = RepositoryManager.getRepository()) {
if (!repository.getAssetRepository().assetExists(assetId)) if (!repository.getAssetRepository().assetExists(assetId))
throw ApiExceptionFactory.INSTANCE.createException(request, ApiError.INVALID_ASSET_ID); throw ApiExceptionFactory.INSTANCE.createException(request, ApiError.INVALID_ASSET_ID);
@ -140,36 +210,45 @@ public class AssetsResource {
if (!repository.getAssetRepository().assetExists(otherAssetId)) if (!repository.getAssetRepository().assetExists(otherAssetId))
throw ApiExceptionFactory.INSTANCE.createException(request, ApiError.INVALID_ASSET_ID); throw ApiExceptionFactory.INSTANCE.createException(request, ApiError.INVALID_ASSET_ID);
List<OrderData> orders = repository.getAssetRepository().getOpenOrders(assetId, otherAssetId); return repository.getAssetRepository().getOpenOrders(assetId, otherAssetId, limit, offset, reverse);
// 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;
} catch (DataException e) { } catch (DataException e) {
throw ApiExceptionFactory.INSTANCE.createException(request, ApiError.REPOSITORY_ISSUE, e); throw ApiExceptionFactory.INSTANCE.createException(request, ApiError.REPOSITORY_ISSUE, e);
} }
} }
@GET @GET
@Path("/trades/{assetId}/{otherAssetId}") @Path("/trades/{assetid}/{otherassetid}")
@Operation( @Operation(
summary = "Asset trades", summary = "Asset trades",
description = "Returns successful trades of {assetId} for {otherAssetId}.<br>" + description = "Returns successful trades of {assetid} for {otherassetid}.<br>" + "Does NOT include trades of {otherassetid} for {assetid}!<br>"
"Does NOT include trades of {otherAssetId} for {assetId}!<br>" + + "\"Initiating\" order is the order that caused the actual trade by matching up with the \"target\" order.",
"\"Initiating\" order is the order that caused the actual trade by matching up with the \"target\" order.",
responses = { responses = {
@ApiResponse( @ApiResponse(
description = "asset trades", 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}) @ApiErrors({
public List<TradeWithOrderInfo> getAssetTrades(@Parameter(ref = "assetId") @PathParam("assetId") int assetId, @Parameter(ref = "otherAssetId") @PathParam("otherAssetId") int otherAssetId, ApiError.INVALID_ASSET_ID, ApiError.REPOSITORY_ISSUE
@Parameter(ref = "limit") @QueryParam("limit") int limit, @Parameter(ref = "offset") @QueryParam("offset") int offset) { })
public List<TradeWithOrderInfo> 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()) { try (final Repository repository = RepositoryManager.getRepository()) {
if (!repository.getAssetRepository().assetExists(assetId)) if (!repository.getAssetRepository().assetExists(assetId))
throw ApiExceptionFactory.INSTANCE.createException(request, ApiError.INVALID_ASSET_ID); throw ApiExceptionFactory.INSTANCE.createException(request, ApiError.INVALID_ASSET_ID);
@ -177,12 +256,7 @@ public class AssetsResource {
if (!repository.getAssetRepository().assetExists(otherAssetId)) if (!repository.getAssetRepository().assetExists(otherAssetId))
throw ApiExceptionFactory.INSTANCE.createException(request, ApiError.INVALID_ASSET_ID); throw ApiExceptionFactory.INSTANCE.createException(request, ApiError.INVALID_ASSET_ID);
List<TradeData> trades = repository.getAssetRepository().getTrades(assetId, otherAssetId); List<TradeData> trades = repository.getAssetRepository().getTrades(assetId, otherAssetId, limit, offset, reverse);
// 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);
// Expanding remaining entries // Expanding remaining entries
List<TradeWithOrderInfo> fullTrades = new ArrayList<>(); List<TradeWithOrderInfo> fullTrades = new ArrayList<>();
@ -199,19 +273,25 @@ public class AssetsResource {
} }
@GET @GET
@Path("/order/{orderId}") @Path("/order/{orderid}")
@Operation( @Operation(
summary = "Fetch asset order", summary = "Fetch asset order",
description = "Returns asset order info.", description = "Returns asset order info.",
responses = { responses = {
@ApiResponse( @ApiResponse(
description = "asset order", 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}) @ApiErrors({
public OrderWithTrades getAssetOrder(@PathParam("orderId") String orderId58) { ApiError.INVALID_ORDER_ID, ApiError.ORDER_NO_EXISTS, ApiError.REPOSITORY_ISSUE
})
public OrderData getAssetOrder(@PathParam("orderid") String orderId58) {
// Decode orderID // Decode orderID
byte[] orderId; byte[] orderId;
try { try {
@ -225,9 +305,178 @@ public class AssetsResource {
if (orderData == null) if (orderData == null)
throw ApiExceptionFactory.INSTANCE.createException(request, ApiError.ORDER_NO_EXISTS); throw ApiExceptionFactory.INSTANCE.createException(request, ApiError.ORDER_NO_EXISTS);
List<TradeData> 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<TradeData> 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<AccountBalanceData> 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<OrderData> 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) { } catch (DataException e) {
throw ApiExceptionFactory.INSTANCE.createException(request, ApiError.REPOSITORY_ISSUE, e); throw ApiExceptionFactory.INSTANCE.createException(request, ApiError.REPOSITORY_ISSUE, e);
} }
@ -241,7 +490,9 @@ public class AssetsResource {
required = true, required = true,
content = @Content( content = @Content(
mediaType = MediaType.APPLICATION_JSON, mediaType = MediaType.APPLICATION_JSON,
schema = @Schema(implementation = CancelAssetOrderTransactionData.class) schema = @Schema(
implementation = CancelAssetOrderTransactionData.class
)
) )
), ),
responses = { 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) { public String cancelOrder(CancelAssetOrderTransactionData transactionData) {
try (final Repository repository = RepositoryManager.getRepository()) { try (final Repository repository = RepositoryManager.getRepository()) {
Transaction transaction = Transaction.fromData(repository, transactionData); Transaction transaction = Transaction.fromData(repository, transactionData);
@ -282,7 +535,9 @@ public class AssetsResource {
required = true, required = true,
content = @Content( content = @Content(
mediaType = MediaType.APPLICATION_JSON, mediaType = MediaType.APPLICATION_JSON,
schema = @Schema(implementation = IssueAssetTransactionData.class) schema = @Schema(
implementation = IssueAssetTransactionData.class
)
) )
), ),
responses = { 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) { public String issueAsset(IssueAssetTransactionData transactionData) {
try (final Repository repository = RepositoryManager.getRepository()) { try (final Repository repository = RepositoryManager.getRepository()) {
Transaction transaction = Transaction.fromData(repository, transactionData); Transaction transaction = Transaction.fromData(repository, transactionData);
@ -323,7 +580,9 @@ public class AssetsResource {
required = true, required = true,
content = @Content( content = @Content(
mediaType = MediaType.APPLICATION_JSON, mediaType = MediaType.APPLICATION_JSON,
schema = @Schema(implementation = CreateAssetOrderTransactionData.class) schema = @Schema(
implementation = CreateAssetOrderTransactionData.class
)
) )
), ),
responses = { 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) { public String createOrder(CreateAssetOrderTransactionData transactionData) {
try (final Repository repository = RepositoryManager.getRepository()) { try (final Repository repository = RepositoryManager.getRepository()) {
Transaction transaction = Transaction.fromData(repository, transactionData); Transaction transaction = Transaction.fromData(repository, transactionData);

View File

@ -2,6 +2,7 @@ package org.qora.api.resource;
import io.swagger.v3.oas.annotations.Operation; import io.swagger.v3.oas.annotations.Operation;
import io.swagger.v3.oas.annotations.Parameter; 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.Content;
import io.swagger.v3.oas.annotations.media.Schema; import io.swagger.v3.oas.annotations.media.Schema;
import io.swagger.v3.oas.annotations.responses.ApiResponse; 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.math.BigDecimal;
import java.util.ArrayList; import java.util.ArrayList;
import java.util.List; import java.util.List;
import java.util.stream.Collectors;
import javax.servlet.http.HttpServletRequest; import javax.servlet.http.HttpServletRequest;
@ -26,7 +26,6 @@ import org.qora.api.ApiError;
import org.qora.api.ApiErrors; import org.qora.api.ApiErrors;
import org.qora.api.ApiException; import org.qora.api.ApiException;
import org.qora.api.ApiExceptionFactory; import org.qora.api.ApiExceptionFactory;
import org.qora.api.model.BlockWithTransactions;
import org.qora.block.Block; import org.qora.block.Block;
import org.qora.data.block.BlockData; import org.qora.data.block.BlockData;
import org.qora.data.transaction.TransactionData; import org.qora.data.transaction.TransactionData;
@ -36,8 +35,12 @@ import org.qora.repository.RepositoryManager;
import org.qora.utils.Base58; import org.qora.utils.Base58;
@Path("/blocks") @Path("/blocks")
@Produces({MediaType.APPLICATION_JSON, MediaType.TEXT_PLAIN}) @Produces({
@Tag(name = "Blocks") MediaType.APPLICATION_JSON, MediaType.TEXT_PLAIN
})
@Tag(
name = "Blocks"
)
public class BlocksResource { public class BlocksResource {
@Context @Context
@ -53,14 +56,16 @@ public class BlocksResource {
description = "the block", description = "the block",
content = @Content( content = @Content(
schema = @Schema( schema = @Schema(
implementation = BlockWithTransactions.class implementation = BlockData.class
) )
) )
) )
} }
) )
@ApiErrors({ApiError.INVALID_SIGNATURE, ApiError.BLOCK_NO_EXISTS, ApiError.REPOSITORY_ISSUE}) @ApiErrors({
public BlockWithTransactions getBlock(@PathParam("signature") String signature58, @Parameter(ref = "includeTransactions") @QueryParam("includeTransactions") boolean includeTransactions) { ApiError.INVALID_SIGNATURE, ApiError.BLOCK_NO_EXISTS, ApiError.REPOSITORY_ISSUE
})
public BlockData getBlock(@PathParam("signature") String signature58) {
// Decode signature // Decode signature
byte[] signature; byte[] signature;
try { try {
@ -70,8 +75,55 @@ public class BlocksResource {
} }
try (final Repository repository = RepositoryManager.getRepository()) { try (final Repository repository = RepositoryManager.getRepository()) {
BlockData blockData = repository.getBlockRepository().fromSignature(signature); return repository.getBlockRepository().fromSignature(signature);
return packageBlockData(repository, blockData, includeTransactions); } 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<TransactionData> 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) { } catch (ApiException e) {
throw e; throw e;
} catch (DataException e) { } catch (DataException e) {
@ -89,17 +141,18 @@ public class BlocksResource {
description = "the block", description = "the block",
content = @Content( content = @Content(
schema = @Schema( schema = @Schema(
implementation = BlockWithTransactions.class implementation = BlockData.class
) )
) )
) )
} }
) )
@ApiErrors({ApiError.BLOCK_NO_EXISTS, ApiError.REPOSITORY_ISSUE}) @ApiErrors({
public BlockWithTransactions getFirstBlock(@Parameter(ref = "includeTransactions") @QueryParam("includeTransactions") boolean includeTransactions) { ApiError.BLOCK_NO_EXISTS, ApiError.REPOSITORY_ISSUE
})
public BlockData getFirstBlock() {
try (final Repository repository = RepositoryManager.getRepository()) { try (final Repository repository = RepositoryManager.getRepository()) {
BlockData blockData = repository.getBlockRepository().fromHeight(1); return repository.getBlockRepository().fromHeight(1);
return packageBlockData(repository, blockData, includeTransactions);
} catch (ApiException e) { } catch (ApiException e) {
throw e; throw e;
} catch (DataException e) { } catch (DataException e) {
@ -117,17 +170,18 @@ public class BlocksResource {
description = "the block", description = "the block",
content = @Content( content = @Content(
schema = @Schema( schema = @Schema(
implementation = BlockWithTransactions.class implementation = BlockData.class
) )
) )
) )
} }
) )
@ApiErrors({ApiError.BLOCK_NO_EXISTS, ApiError.REPOSITORY_ISSUE}) @ApiErrors({
public BlockWithTransactions getLastBlock(@Parameter(ref = "includeTransactions") @QueryParam("includeTransactions") boolean includeTransactions) { ApiError.BLOCK_NO_EXISTS, ApiError.REPOSITORY_ISSUE
})
public BlockData getLastBlock() {
try (final Repository repository = RepositoryManager.getRepository()) { try (final Repository repository = RepositoryManager.getRepository()) {
BlockData blockData = repository.getBlockRepository().getLastBlock(); return repository.getBlockRepository().getLastBlock();
return packageBlockData(repository, blockData, includeTransactions);
} catch (ApiException e) { } catch (ApiException e) {
throw e; throw e;
} catch (DataException e) { } catch (DataException e) {
@ -145,14 +199,16 @@ public class BlocksResource {
description = "the block", description = "the block",
content = @Content( content = @Content(
schema = @Schema( schema = @Schema(
implementation = BlockWithTransactions.class implementation = BlockData.class
) )
) )
) )
} }
) )
@ApiErrors({ApiError.INVALID_SIGNATURE, ApiError.BLOCK_NO_EXISTS, ApiError.REPOSITORY_ISSUE}) @ApiErrors({
public BlockWithTransactions getChild(@PathParam("signature") String signature58, @Parameter(ref = "includeTransactions") @QueryParam("includeTransactions") boolean includeTransactions) { ApiError.INVALID_SIGNATURE, ApiError.BLOCK_NO_EXISTS, ApiError.REPOSITORY_ISSUE
})
public BlockData getChild(@PathParam("signature") String signature58) {
// Decode signature // Decode signature
byte[] signature; byte[] signature;
try { try {
@ -170,8 +226,11 @@ public class BlocksResource {
BlockData childBlockData = repository.getBlockRepository().fromReference(signature); BlockData childBlockData = repository.getBlockRepository().fromReference(signature);
// Checking child exists is handled by packageBlockData() // Check child block exists
return packageBlockData(repository, childBlockData, includeTransactions); if (childBlockData == null)
throw ApiExceptionFactory.INSTANCE.createException(request, ApiError.BLOCK_NO_EXISTS);
return childBlockData;
} catch (ApiException e) { } catch (ApiException e) {
throw e; throw e;
} catch (DataException e) { } catch (DataException e) {
@ -196,7 +255,9 @@ public class BlocksResource {
) )
} }
) )
@ApiErrors({ApiError.REPOSITORY_ISSUE}) @ApiErrors({
ApiError.REPOSITORY_ISSUE
})
public BigDecimal getGeneratingBalance() { public BigDecimal getGeneratingBalance() {
try (final Repository repository = RepositoryManager.getRepository()) { try (final Repository repository = RepositoryManager.getRepository()) {
BlockData blockData = repository.getBlockRepository().getLastBlock(); 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) { public BigDecimal getGeneratingBalance(@PathParam("signature") String signature58) {
// Decode signature // Decode signature
byte[] signature; byte[] signature;
@ -269,7 +332,9 @@ public class BlocksResource {
) )
} }
) )
@ApiErrors({ApiError.REPOSITORY_ISSUE}) @ApiErrors({
ApiError.REPOSITORY_ISSUE
})
public long getTimePerBlock() { public long getTimePerBlock() {
try (final Repository repository = RepositoryManager.getRepository()) { try (final Repository repository = RepositoryManager.getRepository()) {
BlockData blockData = repository.getBlockRepository().getLastBlock(); BlockData blockData = repository.getBlockRepository().getLastBlock();
@ -319,7 +384,9 @@ public class BlocksResource {
) )
} }
) )
@ApiErrors({ApiError.REPOSITORY_ISSUE}) @ApiErrors({
ApiError.REPOSITORY_ISSUE
})
public int getHeight() { public int getHeight() {
try (final Repository repository = RepositoryManager.getRepository()) { try (final Repository repository = RepositoryManager.getRepository()) {
return repository.getBlockRepository().getBlockchainHeight(); 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) { public int getHeight(@PathParam("signature") String signature58) {
// Decode signature // Decode signature
byte[] signature; byte[] signature;
@ -382,17 +451,22 @@ public class BlocksResource {
description = "the block", description = "the block",
content = @Content( content = @Content(
schema = @Schema( schema = @Schema(
implementation = BlockWithTransactions.class implementation = BlockData.class
) )
) )
) )
} }
) )
@ApiErrors({ApiError.BLOCK_NO_EXISTS, ApiError.REPOSITORY_ISSUE}) @ApiErrors({
public BlockWithTransactions getByHeight(@PathParam("height") int height, @Parameter(ref = "includeTransactions") @QueryParam("includeTransactions") boolean includeTransactions) { ApiError.BLOCK_NO_EXISTS, ApiError.REPOSITORY_ISSUE
})
public BlockData getByHeight(@PathParam("height") int height) {
try (final Repository repository = RepositoryManager.getRepository()) { try (final Repository repository = RepositoryManager.getRepository()) {
BlockData blockData = repository.getBlockRepository().fromHeight(height); 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) { } catch (ApiException e) {
throw e; throw e;
} catch (DataException e) { } catch (DataException e) {
@ -409,19 +483,23 @@ public class BlocksResource {
@ApiResponse( @ApiResponse(
description = "blocks", description = "blocks",
content = @Content( content = @Content(
array = @ArraySchema(
schema = @Schema( schema = @Schema(
implementation = BlockWithTransactions.class implementation = BlockData.class
)
) )
) )
) )
} }
) )
@ApiErrors({ApiError.BLOCK_NO_EXISTS, ApiError.REPOSITORY_ISSUE}) @ApiErrors({
public List<BlockWithTransactions> getBlockRange(@PathParam("height") int height, @Parameter(ref = "count") @QueryParam("count") int count) { ApiError.BLOCK_NO_EXISTS, ApiError.REPOSITORY_ISSUE
boolean includeTransactions = false; })
public List<BlockData> getBlockRange(@PathParam("height") int height, @Parameter(
ref = "count"
) @QueryParam("count") int count) {
try (final Repository repository = RepositoryManager.getRepository()) { try (final Repository repository = RepositoryManager.getRepository()) {
List<BlockWithTransactions> blocks = new ArrayList<BlockWithTransactions>(); List<BlockData> blocks = new ArrayList<>();
for (/* count already set */; count > 0; --count, ++height) { for (/* count already set */; count > 0; --count, ++height) {
BlockData blockData = repository.getBlockRepository().fromHeight(height); BlockData blockData = repository.getBlockRepository().fromHeight(height);
@ -429,7 +507,7 @@ public class BlocksResource {
// Run out of blocks! // Run out of blocks!
break; break;
blocks.add(packageBlockData(repository, blockData, includeTransactions)); blocks.add(blockData);
} }
return blocks; return blocks;
@ -440,29 +518,4 @@ public class BlocksResource {
} }
} }
/**
* Returns block, optionally including transactions.
* <p>
* 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<TransactionData> 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);
}
} }

View File

@ -10,6 +10,7 @@ import io.swagger.v3.oas.annotations.responses.ApiResponse;
import io.swagger.v3.oas.annotations.tags.Tag; import io.swagger.v3.oas.annotations.tags.Tag;
import java.util.List; import java.util.List;
import java.util.function.Predicate;
import java.util.stream.Collectors; import java.util.stream.Collectors;
import javax.servlet.http.HttpServletRequest; import javax.servlet.http.HttpServletRequest;
@ -25,8 +26,8 @@ import javax.ws.rs.core.MediaType;
import org.qora.api.ApiError; import org.qora.api.ApiError;
import org.qora.api.ApiErrors; import org.qora.api.ApiErrors;
import org.qora.api.ApiExceptionFactory; import org.qora.api.ApiExceptionFactory;
import org.qora.api.model.GroupWithMemberInfo; import org.qora.api.model.GroupMembers;
import org.qora.api.model.GroupWithMemberInfo.MemberInfo; import org.qora.api.model.GroupMembers.MemberInfo;
import org.qora.crypto.Crypto; import org.qora.crypto.Crypto;
import org.qora.data.group.GroupAdminData; import org.qora.data.group.GroupAdminData;
import org.qora.data.group.GroupBanData; import org.qora.data.group.GroupBanData;
@ -86,14 +87,15 @@ public class GroupsResource {
} }
) )
@ApiErrors({ApiError.REPOSITORY_ISSUE}) @ApiErrors({ApiError.REPOSITORY_ISSUE})
public List<GroupData> getAllGroups(@Parameter(ref = "limit") @QueryParam("limit") int limit, @Parameter(ref = "offset") @QueryParam("offset") int offset) { public List<GroupData> 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()) { try (final Repository repository = RepositoryManager.getRepository()) {
List<GroupData> groups = repository.getGroupRepository().getAllGroups(); List<GroupData> groups = repository.getGroupRepository().getAllGroups(limit, offset, reverse);
// 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);
return groups; return groups;
} catch (DataException e) { } catch (DataException e) {
@ -158,7 +160,7 @@ public class GroupsResource {
} }
@GET @GET
@Path("/{groupname}") @Path("/{groupid}")
@Operation( @Operation(
summary = "Info on group", summary = "Info on group",
responses = { responses = {
@ -166,45 +168,72 @@ public class GroupsResource {
description = "group info", description = "group info",
content = @Content( content = @Content(
mediaType = MediaType.APPLICATION_JSON, mediaType = MediaType.APPLICATION_JSON,
schema = @Schema(implementation = GroupWithMemberInfo.class) schema = @Schema(implementation = GroupData.class)
) )
) )
} }
) )
@ApiErrors({ApiError.REPOSITORY_ISSUE}) @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()) { try (final Repository repository = RepositoryManager.getRepository()) {
GroupData groupData = repository.getGroupRepository().fromGroupName(groupName); GroupData groupData = repository.getGroupRepository().fromGroupId(groupId);
if (groupData == null) if (groupData == null)
throw ApiExceptionFactory.INSTANCE.createException(request, ApiError.GROUP_UNKNOWN); throw ApiExceptionFactory.INSTANCE.createException(request, ApiError.GROUP_UNKNOWN);
List<MemberInfo> members = null; return groupData;
Integer memberCount = null;
if (includeMembers) {
List<GroupMemberData> 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<GroupAdminData> groupAdmins = repository.getGroupRepository().getGroupAdmins(groupData.getGroupId());
// We only need admin addresses
List<String> adminAddresses = groupAdmins.stream().map(groupAdminData -> groupAdminData.getAdmin()).collect(Collectors.toList());
return new GroupWithMemberInfo(groupData, adminAddresses, members, memberCount);
} catch (DataException e) { } catch (DataException e) {
throw ApiExceptionFactory.INSTANCE.createException(request, ApiError.REPOSITORY_ISSUE, 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<GroupAdminData> admins = repository.getGroupRepository().getGroupAdmins(groupId, limit, offset, reverse);
// Convert form
List<MemberInfo> membersInfo = admins.stream().map(admin -> new MemberInfo(admin.getAdmin(), null, true)).collect(Collectors.toList());
return new GroupMembers(membersInfo, memberCount, adminCount);
}
final List<GroupAdminData> admins = repository.getGroupRepository().getGroupAdmins(groupId, limit, offset, reverse);
List<GroupMemberData> members = repository.getGroupRepository().getGroupMembers(groupId, limit, offset, reverse);
// Convert form
Predicate<GroupMemberData> memberIsAdmin = member -> admins.stream().anyMatch(admin -> admin.getAdmin().equals(member.getMember()));
List<MemberInfo> 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 @POST
@Path("/create") @Path("/create")

View File

@ -68,15 +68,12 @@ public class NamesResource {
} }
) )
@ApiErrors({ApiError.REPOSITORY_ISSUE}) @ApiErrors({ApiError.REPOSITORY_ISSUE})
public List<NameSummary> getAllNames(@Parameter(ref = "limit") @QueryParam("limit") int limit, @Parameter(ref = "offset") @QueryParam("offset") int offset) { public List<NameSummary> 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()) { try (final Repository repository = RepositoryManager.getRepository()) {
List<NameData> names = repository.getNameRepository().getAllNames(); List<NameData> names = repository.getNameRepository().getAllNames(limit, offset, reverse);
// 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);
// Convert to summary
return names.stream().map(nameData -> new NameSummary(nameData)).collect(Collectors.toList()); return names.stream().map(nameData -> new NameSummary(nameData)).collect(Collectors.toList());
} catch (DataException e) { } catch (DataException e) {
throw ApiExceptionFactory.INSTANCE.createException(request, ApiError.REPOSITORY_ISSUE, e); throw ApiExceptionFactory.INSTANCE.createException(request, ApiError.REPOSITORY_ISSUE, e);
@ -98,12 +95,13 @@ public class NamesResource {
} }
) )
@ApiErrors({ApiError.INVALID_ADDRESS, ApiError.REPOSITORY_ISSUE}) @ApiErrors({ApiError.INVALID_ADDRESS, ApiError.REPOSITORY_ISSUE})
public List<NameSummary> getNamesByAddress(@PathParam("address") String address) { public List<NameSummary> 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)) if (!Crypto.isValidAddress(address))
throw ApiExceptionFactory.INSTANCE.createException(request, ApiError.INVALID_ADDRESS); throw ApiExceptionFactory.INSTANCE.createException(request, ApiError.INVALID_ADDRESS);
try (final Repository repository = RepositoryManager.getRepository()) { try (final Repository repository = RepositoryManager.getRepository()) {
List<NameData> names = repository.getNameRepository().getNamesByOwner(address); List<NameData> names = repository.getNameRepository().getNamesByOwner(address, limit, offset, reverse);
return names.stream().map(nameData -> new NameSummary(nameData)).collect(Collectors.toList()); return names.stream().map(nameData -> new NameSummary(nameData)).collect(Collectors.toList());
} catch (DataException e) { } catch (DataException e) {
@ -365,16 +363,10 @@ public class NamesResource {
} }
) )
@ApiErrors({ApiError.REPOSITORY_ISSUE}) @ApiErrors({ApiError.REPOSITORY_ISSUE})
public List<NameData> getNamesForSale(@Parameter(ref = "limit") @QueryParam("limit") int limit, @Parameter(ref = "offset") @QueryParam("offset") int offset) { public List<NameData> 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()) { try (final Repository repository = RepositoryManager.getRepository()) {
List<NameData> names = repository.getNameRepository().getNamesForSale(); return repository.getNameRepository().getNamesForSale(limit, offset, reverse);
// 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;
} catch (DataException e) { } catch (DataException e) {
throw ApiExceptionFactory.INSTANCE.createException(request, ApiError.REPOSITORY_ISSUE, e); throw ApiExceptionFactory.INSTANCE.createException(request, ApiError.REPOSITORY_ISSUE, e);
} }

View File

@ -28,8 +28,6 @@ import org.qora.api.ApiErrors;
import org.qora.api.ApiException; import org.qora.api.ApiException;
import org.qora.api.ApiExceptionFactory; import org.qora.api.ApiExceptionFactory;
import org.qora.api.model.SimpleTransactionSignRequest; 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.data.transaction.TransactionData;
import org.qora.globalization.Translator; import org.qora.globalization.Translator;
import org.qora.repository.DataException; import org.qora.repository.DataException;
@ -45,8 +43,12 @@ import org.qora.utils.Base58;
import com.google.common.primitives.Bytes; import com.google.common.primitives.Bytes;
@Path("/transactions") @Path("/transactions")
@Produces({MediaType.APPLICATION_JSON, MediaType.TEXT_PLAIN}) @Produces({
@Tag(name = "Transactions") MediaType.APPLICATION_JSON, MediaType.TEXT_PLAIN
})
@Tag(
name = "Transactions"
)
public class TransactionsResource { public class TransactionsResource {
@Context @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) { public TransactionData getTransaction(@PathParam("signature") String signature58) {
byte[] signature; byte[] signature;
try { try {
@ -100,12 +104,16 @@ public class TransactionsResource {
description = "raw transaction encoded in Base58", description = "raw transaction encoded in Base58",
content = @Content( content = @Content(
mediaType = MediaType.TEXT_PLAIN, 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) { public String getRawTransaction(@PathParam("signature") String signature58) {
byte[] signature; byte[] signature;
try { try {
@ -138,21 +146,28 @@ public class TransactionsResource {
description = "Returns list of transactions", description = "Returns list of transactions",
responses = { responses = {
@ApiResponse( @ApiResponse(
description = "list of transactions", description = "the block",
content = @Content( content = @Content(
array = @ArraySchema( array = @ArraySchema(
schema = @Schema( schema = @Schema(
oneOf = { implementation = TransactionData.class
GenesisTransactionData.class, PaymentTransactionData.class
}
) )
) )
) )
) )
} }
) )
@ApiErrors({ApiError.INVALID_SIGNATURE, ApiError.BLOCK_NO_EXISTS, ApiError.REPOSITORY_ISSUE}) @ApiErrors({
public List<TransactionData> getBlockTransactions(@PathParam("signature") String signature58, @Parameter(ref = "limit") @QueryParam("limit") int limit, @Parameter(ref = "offset") @QueryParam("offset") int offset) { ApiError.INVALID_SIGNATURE, ApiError.BLOCK_NO_EXISTS, ApiError.REPOSITORY_ISSUE
})
public List<TransactionData> 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; byte[] signature;
try { try {
signature = Base58.decode(signature58); signature = Base58.decode(signature58);
@ -161,18 +176,10 @@ public class TransactionsResource {
} }
try (final Repository repository = RepositoryManager.getRepository()) { try (final Repository repository = RepositoryManager.getRepository()) {
List<TransactionData> transactions = repository.getBlockRepository().getTransactionsFromSignature(signature); if (repository.getBlockRepository().getHeightFromSignature(signature) == 0)
// check if block exists
if (transactions == null)
throw ApiExceptionFactory.INSTANCE.createException(request, ApiError.BLOCK_NO_EXISTS); throw ApiExceptionFactory.INSTANCE.createException(request, ApiError.BLOCK_NO_EXISTS);
// Pagination would take effect here (or as part of the repository access) return repository.getBlockRepository().getTransactionsFromSignature(signature, limit, offset, reverse);
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;
} catch (ApiException e) { } catch (ApiException e) {
throw e; throw e;
} catch (DataException e) { } catch (DataException e) {
@ -198,10 +205,18 @@ public class TransactionsResource {
) )
} }
) )
@ApiErrors({ApiError.REPOSITORY_ISSUE}) @ApiErrors({
public List<TransactionData> getUnconfirmedTransactions() { ApiError.REPOSITORY_ISSUE
})
public List<TransactionData> 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()) { try (final Repository repository = RepositoryManager.getRepository()) {
return repository.getTransactionRepository().getAllUnconfirmedTransactions(); return repository.getTransactionRepository().getUnconfirmedTransactions(limit, offset, reverse);
} catch (ApiException e) { } catch (ApiException e) {
throw e; throw e;
} catch (DataException e) { } catch (DataException e) {
@ -209,23 +224,17 @@ public class TransactionsResource {
} }
} }
public enum ConfirmationStatus {
CONFIRMED,
UNCONFIRMED,
BOTH;
}
@GET @GET
@Path("/search") @Path("/search")
@Operation( @Operation(
summary = "Find matching transactions", summary = "Find matching transactions",
description = "Returns transactions that match criteria. At least either txType or address must be provided.", 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.",
/*
* 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"))
* },
*/
responses = { responses = {
@ApiResponse( @ApiResponse(
description = "transactions", description = "transactions",
@ -239,30 +248,31 @@ public class TransactionsResource {
) )
} }
) )
@ApiErrors({ApiError.INVALID_CRITERIA, ApiError.REPOSITORY_ISSUE}) @ApiErrors({
ApiError.INVALID_CRITERIA, ApiError.REPOSITORY_ISSUE
})
public List<TransactionData> searchTransactions(@QueryParam("startBlock") Integer startBlock, @QueryParam("blockLimit") Integer blockLimit, public List<TransactionData> searchTransactions(@QueryParam("startBlock") Integer startBlock, @QueryParam("blockLimit") Integer blockLimit,
@QueryParam("txType") Integer txTypeNum, @QueryParam("address") String address, @Parameter( @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" ref = "limit"
) @QueryParam("limit") int limit, @Parameter( ) @QueryParam("limit") Integer limit, @Parameter(
ref = "offset" ref = "offset"
) @QueryParam("offset") int offset) { ) @QueryParam("offset") Integer offset, @Parameter(
if ((txTypeNum == null || txTypeNum == 0) && (address == null || address.isEmpty())) 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); throw ApiExceptionFactory.INSTANCE.createException(request, ApiError.INVALID_CRITERIA);
TransactionType txType = null; // You can't ask for unconfirmed and impose a block height range
if (txTypeNum != null) { if (confirmationStatus != ConfirmationStatus.CONFIRMED && (startBlock != null || blockLimit != null))
txType = TransactionType.valueOf(txTypeNum);
if (txType == null)
throw ApiExceptionFactory.INSTANCE.createException(request, ApiError.INVALID_CRITERIA); throw ApiExceptionFactory.INSTANCE.createException(request, ApiError.INVALID_CRITERIA);
}
try (final Repository repository = RepositoryManager.getRepository()) { try (final Repository repository = RepositoryManager.getRepository()) {
List<byte[]> signatures = repository.getTransactionRepository().getAllSignaturesMatchingCriteria(startBlock, blockLimit, txType, address); List<byte[]> signatures = repository.getTransactionRepository().getSignaturesMatchingCriteria(startBlock, blockLimit, txType, address,
confirmationStatus, limit, offset, reverse);
// 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);
// Expand signatures to transactions // Expand signatures to transactions
List<TransactionData> transactions = new ArrayList<TransactionData>(signatures.size()); List<TransactionData> transactions = new ArrayList<TransactionData>(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) { public String signTransaction(SimpleTransactionSignRequest signRequest) {
if (signRequest.transactionBytes.length == 0) if (signRequest.transactionBytes.length == 0)
throw ApiExceptionFactory.INSTANCE.createException(request, ApiError.JSON); 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) { public String processTransaction(String rawBytes58) {
try (final Repository repository = RepositoryManager.getRepository()) { try (final Repository repository = RepositoryManager.getRepository()) {
byte[] rawBytes = Base58.decode(rawBytes58); 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) { public TransactionData decodeTransaction(String rawBytes58, @QueryParam("ignoreValidityChecks") boolean ignoreValidityChecks) {
try (final Repository repository = RepositoryManager.getRepository()) { try (final Repository repository = RepositoryManager.getRepository()) {
byte[] rawBytes = Base58.decode(rawBytes58); byte[] rawBytes = Base58.decode(rawBytes58);

View File

@ -50,7 +50,7 @@ public class BlockGenerator extends Thread {
try (final Repository repository = RepositoryManager.getRepository()) { try (final Repository repository = RepositoryManager.getRepository()) {
if (Settings.getInstance().getWipeUnconfirmedOnStart()) { if (Settings.getInstance().getWipeUnconfirmedOnStart()) {
// Wipe existing unconfirmed transactions // Wipe existing unconfirmed transactions
List<TransactionData> unconfirmedTransactions = repository.getTransactionRepository().getAllUnconfirmedTransactions(); List<TransactionData> unconfirmedTransactions = repository.getTransactionRepository().getUnconfirmedTransactions();
for (TransactionData transactionData : unconfirmedTransactions) { for (TransactionData transactionData : unconfirmedTransactions) {
LOGGER.trace(String.format("Deleting unconfirmed transaction %s", Base58.encode(transactionData.getSignature()))); LOGGER.trace(String.format("Deleting unconfirmed transaction %s", Base58.encode(transactionData.getSignature())));

View File

@ -17,6 +17,11 @@ import org.qora.utils.Base58;
public class Controller { 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); private static final Logger LOGGER = LogManager.getLogger(Controller.class);
public static final String connectionUrl = "jdbc:hsqldb:file:db/blockchain;create=true"; public static final String connectionUrl = "jdbc:hsqldb:file:db/blockchain;create=true";

View File

@ -10,8 +10,9 @@ import javax.xml.bind.annotation.XmlElement;
import javax.xml.bind.annotation.XmlSeeAlso; import javax.xml.bind.annotation.XmlSeeAlso;
import javax.xml.bind.annotation.XmlTransient; import javax.xml.bind.annotation.XmlTransient;
import org.eclipse.persistence.oxm.annotations.XmlClassExtractor; // XXX are this still needed? see below
import org.qora.api.TransactionClassExtractor; // import org.eclipse.persistence.oxm.annotations.XmlClassExtractor;
// import org.qora.api.TransactionClassExtractor;
import org.qora.crypto.Crypto; import org.qora.crypto.Crypto;
import org.qora.transaction.Transaction.TransactionType; 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! * 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, @XmlSeeAlso({GenesisTransactionData.class, PaymentTransactionData.class, RegisterNameTransactionData.class, UpdateNameTransactionData.class,
SellNameTransactionData.class, CancelSellNameTransactionData.class, BuyNameTransactionData.class, SellNameTransactionData.class, CancelSellNameTransactionData.class, BuyNameTransactionData.class,
CreatePollTransactionData.class, VoteOnPollTransactionData.class, ArbitraryTransactionData.class, CreatePollTransactionData.class, VoteOnPollTransactionData.class, ArbitraryTransactionData.class,

View File

@ -21,9 +21,17 @@ public interface AccountRepository {
public AccountBalanceData getBalance(String address, long assetId) throws DataException; public AccountBalanceData getBalance(String address, long assetId) throws DataException;
public List<AccountBalanceData> getAllBalances(String address) throws DataException; public List<AccountBalanceData> getAllBalances(String address, Integer limit, Integer offset, Boolean reverse) throws DataException;
public List<AccountBalanceData> getAssetBalances(long assetId) throws DataException; public default List<AccountBalanceData> getAllBalances(String address) throws DataException {
return getAllBalances(address, null, null, null);
}
public List<AccountBalanceData> getAssetBalances(long assetId, Integer limit, Integer offset, Boolean reverse) throws DataException;
public default List<AccountBalanceData> getAssetBalances(long assetId) throws DataException {
return getAssetBalances(assetId, null, null, null);
}
public void save(AccountBalanceData accountBalanceData) throws DataException; public void save(AccountBalanceData accountBalanceData) throws DataException;

View File

@ -18,7 +18,11 @@ public interface AssetRepository {
public boolean assetExists(String assetName) throws DataException; public boolean assetExists(String assetName) throws DataException;
public List<AssetData> getAllAssets() throws DataException; public List<AssetData> getAllAssets(Integer limit, Integer offset, Boolean reverse) throws DataException;
public default List<AssetData> getAllAssets() throws DataException {
return getAllAssets(null, null, null);
}
// For a list of asset holders, see AccountRepository.getAssetBalances // For a list of asset holders, see AccountRepository.getAssetBalances
@ -30,9 +34,18 @@ public interface AssetRepository {
public OrderData fromOrderId(byte[] orderId) throws DataException; public OrderData fromOrderId(byte[] orderId) throws DataException;
public List<OrderData> getOpenOrders(long haveAssetId, long wantAssetId) throws DataException; public List<OrderData> getOpenOrders(long haveAssetId, long wantAssetId, Integer limit, Integer offset, Boolean reverse) throws DataException;
public List<OrderData> getAccountsOrders(byte[] publicKey, boolean includeClosed, boolean includeFulfilled) throws DataException; public default List<OrderData> getOpenOrders(long haveAssetId, long wantAssetId) throws DataException {
return getOpenOrders(haveAssetId, wantAssetId, null, null, null);
}
public List<OrderData> getAccountsOrders(byte[] publicKey, boolean includeClosed, boolean includeFulfilled, Integer limit, Integer offset, Boolean reverse)
throws DataException;
public default List<OrderData> getAccountsOrders(byte[] publicKey, boolean includeClosed, boolean includeFulfilled) throws DataException {
return getAccountsOrders(publicKey, includeClosed, includeFulfilled, null, null, null);
}
public void save(OrderData orderData) throws DataException; public void save(OrderData orderData) throws DataException;
@ -40,10 +53,18 @@ public interface AssetRepository {
// Trades // Trades
public List<TradeData> getTrades(long haveAssetId, long wantAssetId) throws DataException; public List<TradeData> getTrades(long haveAssetId, long wantAssetId, Integer limit, Integer offset, Boolean reverse) throws DataException;
public default List<TradeData> 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 */ /** Returns TradeData for trades where orderId was involved, i.e. either initiating OR target order */
public List<TradeData> getOrdersTrades(byte[] orderId) throws DataException; public List<TradeData> getOrdersTrades(byte[] orderId, Integer limit, Integer offset, Boolean reverse) throws DataException;
public default List<TradeData> getOrdersTrades(byte[] orderId) throws DataException {
return getOrdersTrades(orderId, null, null, null);
}
public void save(TradeData tradeData) throws DataException; public void save(TradeData tradeData) throws DataException;

View File

@ -59,6 +59,17 @@ public interface BlockRepository {
*/ */
public BlockData getLastBlock() throws DataException; public BlockData getLastBlock() throws DataException;
/**
* Returns block's transactions given block's signature.
* <p>
* 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<TransactionData> getTransactionsFromSignature(byte[] signature, Integer limit, Integer offset, Boolean reverse) throws DataException;
/** /**
* Returns block's transactions given block's signature. * Returns block's transactions given block's signature.
* <p> * <p>
@ -68,7 +79,9 @@ public interface BlockRepository {
* @return list of transactions, or null if block not found in blockchain. * @return list of transactions, or null if block not found in blockchain.
* @throws DataException * @throws DataException
*/ */
public List<TransactionData> getTransactionsFromSignature(byte[] signature) throws DataException; public default List<TransactionData> getTransactionsFromSignature(byte[] signature) throws DataException {
return getTransactionsFromSignature(signature, null, null, null);
}
/** /**
* Saves block into repository. * Saves block into repository.

View File

@ -21,11 +21,23 @@ public interface GroupRepository {
public boolean groupExists(String groupName) throws DataException; public boolean groupExists(String groupName) throws DataException;
public List<GroupData> getAllGroups() throws DataException; public List<GroupData> getAllGroups(Integer limit, Integer offset, Boolean reverse) throws DataException;
public List<GroupData> getGroupsByOwner(String address) throws DataException; public default List<GroupData> getAllGroups() throws DataException {
return getAllGroups(null, null, null);
}
public List<GroupData> getGroupsWithMember(String member) throws DataException; public List<GroupData> getGroupsByOwner(String address, Integer limit, Integer offset, Boolean reverse) throws DataException;
public default List<GroupData> getGroupsByOwner(String address) throws DataException {
return getGroupsByOwner(address, null, null, null);
}
public List<GroupData> getGroupsWithMember(String member, Integer limit, Integer offset, Boolean reverse) throws DataException;
public default List<GroupData> getGroupsWithMember(String member) throws DataException {
return getGroupsWithMember(member, null, null, null);
}
public void save(GroupData groupData) throws DataException; public void save(GroupData groupData) throws DataException;
@ -39,7 +51,14 @@ public interface GroupRepository {
public boolean adminExists(int groupId, String address) throws DataException; public boolean adminExists(int groupId, String address) throws DataException;
public List<GroupAdminData> getGroupAdmins(int groupId) throws DataException; public List<GroupAdminData> getGroupAdmins(int groupId, Integer limit, Integer offset, Boolean reverse) throws DataException;
public default List<GroupAdminData> 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; public void save(GroupAdminData groupAdminData) throws DataException;
@ -51,7 +70,11 @@ public interface GroupRepository {
public boolean memberExists(int groupId, String address) throws DataException; public boolean memberExists(int groupId, String address) throws DataException;
public List<GroupMemberData> getGroupMembers(int groupId) throws DataException; public List<GroupMemberData> getGroupMembers(int groupId, Integer limit, Integer offset, Boolean reverse) throws DataException;
public default List<GroupMemberData> getGroupMembers(int groupId) throws DataException {
return getGroupMembers(groupId, null, null, null);
}
/** Returns number of group members, or null if group doesn't exist */ /** Returns number of group members, or null if group doesn't exist */
public Integer countGroupMembers(int groupId) throws DataException; public Integer countGroupMembers(int groupId) throws DataException;
@ -66,9 +89,17 @@ public interface GroupRepository {
public boolean inviteExists(int groupId, String invitee) throws DataException; public boolean inviteExists(int groupId, String invitee) throws DataException;
public List<GroupInviteData> getInvitesByGroupId(int groupId) throws DataException; public List<GroupInviteData> getInvitesByGroupId(int groupId, Integer limit, Integer offset, Boolean reverse) throws DataException;
public List<GroupInviteData> getInvitesByInvitee(String invitee) throws DataException; public default List<GroupInviteData> getInvitesByGroupId(int groupId) throws DataException {
return getInvitesByGroupId(groupId, null, null, null);
}
public List<GroupInviteData> getInvitesByInvitee(String invitee, Integer limit, Integer offset, Boolean reverse) throws DataException;
public default List<GroupInviteData> getInvitesByInvitee(String invitee) throws DataException {
return getInvitesByInvitee(invitee, null, null, null);
}
public void save(GroupInviteData groupInviteData) throws DataException; public void save(GroupInviteData groupInviteData) throws DataException;
@ -80,7 +111,11 @@ public interface GroupRepository {
public boolean joinRequestExists(int groupId, String joiner) throws DataException; public boolean joinRequestExists(int groupId, String joiner) throws DataException;
public List<GroupJoinRequestData> getGroupJoinRequests(int groupId) throws DataException; public List<GroupJoinRequestData> getGroupJoinRequests(int groupId, Integer limit, Integer offset, Boolean reverse) throws DataException;
public default List<GroupJoinRequestData> getGroupJoinRequests(int groupId) throws DataException {
return getGroupJoinRequests(groupId, null, null, null);
}
public void save(GroupJoinRequestData groupJoinRequestData) throws DataException; public void save(GroupJoinRequestData groupJoinRequestData) throws DataException;
@ -92,7 +127,11 @@ public interface GroupRepository {
public boolean banExists(int groupId, String offender) throws DataException; public boolean banExists(int groupId, String offender) throws DataException;
public List<GroupBanData> getGroupBans(int groupId) throws DataException; public List<GroupBanData> getGroupBans(int groupId, Integer limit, Integer offset, Boolean reverse) throws DataException;
public default List<GroupBanData> getGroupBans(int groupId) throws DataException {
return getGroupBans(groupId, null, null, null);
}
public void save(GroupBanData groupBanData) throws DataException; public void save(GroupBanData groupBanData) throws DataException;

View File

@ -10,11 +10,23 @@ public interface NameRepository {
public boolean nameExists(String name) throws DataException; public boolean nameExists(String name) throws DataException;
public List<NameData> getAllNames() throws DataException; public List<NameData> getAllNames(Integer limit, Integer offset, Boolean reverse) throws DataException;
public List<NameData> getNamesForSale() throws DataException; public default List<NameData> getAllNames() throws DataException {
return getAllNames(null, null, null);
}
public List<NameData> getNamesByOwner(String address) throws DataException; public List<NameData> getNamesForSale(Integer limit, Integer offset, Boolean reverse) throws DataException;
public default List<NameData> getNamesForSale() throws DataException {
return getNamesForSale(null, null, null);
}
public List<NameData> getNamesByOwner(String address, Integer limit, Integer offset, Boolean reverse) throws DataException;
public default List<NameData> getNamesByOwner(String address) throws DataException {
return getNamesByOwner(address, null, null, null);
}
public void save(NameData nameData) throws DataException; public void save(NameData nameData) throws DataException;

View File

@ -2,6 +2,7 @@ package org.qora.repository;
import java.util.List; import java.util.List;
import org.qora.api.resource.TransactionsResource.ConfirmationStatus;
import org.qora.data.transaction.TransactionData; import org.qora.data.transaction.TransactionData;
import org.qora.transaction.Transaction.TransactionType; import org.qora.transaction.Transaction.TransactionType;
@ -20,7 +21,7 @@ public interface TransactionRepository {
// Transaction participants // Transaction participants
public List<byte[]> getAllSignaturesInvolvingAddress(String address) throws DataException; public List<byte[]> getSignaturesInvolvingAddress(String address) throws DataException;
public void saveParticipants(TransactionData transactionData, List<String> participants) throws DataException; public void saveParticipants(TransactionData transactionData, List<String> participants) throws DataException;
@ -28,7 +29,21 @@ public interface TransactionRepository {
// Searching transactions // Searching transactions
public List<byte[]> getAllSignaturesMatchingCriteria(Integer startBlock, Integer blockLimit, TransactionType txType, String address) throws DataException; public List<byte[]> 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.
* <p>
* 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<TransactionData> getUnconfirmedTransactions(Integer limit, Integer offset, Boolean reverse) throws DataException;
/** /**
* Returns list of unconfirmed transactions in timestamp-else-signature order. * 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. * @return list of transactions, or empty if none.
* @throws DataException * @throws DataException
*/ */
public List<TransactionData> getAllUnconfirmedTransactions() throws DataException; public default List<TransactionData> getUnconfirmedTransactions() throws DataException {
return getUnconfirmedTransactions(null, null, null);
}
/** /**
* Remove transaction from unconfirmed transactions pile. * Remove transaction from unconfirmed transactions pile.

View File

@ -94,10 +94,15 @@ public class HSQLDBAccountRepository implements AccountRepository {
} }
@Override @Override
public List<AccountBalanceData> getAllBalances(String address) throws DataException { public List<AccountBalanceData> 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<AccountBalanceData> balances = new ArrayList<AccountBalanceData>(); List<AccountBalanceData> balances = new ArrayList<AccountBalanceData>();
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) if (resultSet == null)
return balances; return balances;
@ -115,10 +120,15 @@ public class HSQLDBAccountRepository implements AccountRepository {
} }
@Override @Override
public List<AccountBalanceData> getAssetBalances(long assetId) throws DataException { public List<AccountBalanceData> 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<AccountBalanceData> balances = new ArrayList<AccountBalanceData>(); List<AccountBalanceData> balances = new ArrayList<AccountBalanceData>();
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) if (resultSet == null)
return balances; return balances;

View File

@ -83,11 +83,15 @@ public class HSQLDBAssetRepository implements AssetRepository {
} }
@Override @Override
public List<AssetData> getAllAssets() throws DataException { public List<AssetData> 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<AssetData> assets = new ArrayList<AssetData>(); List<AssetData> assets = new ArrayList<AssetData>();
try (ResultSet resultSet = this.repository try (ResultSet resultSet = this.repository.checkedExecute(sql)) {
.checkedExecute("SELECT owner, asset_id, description, quantity, is_divisible, reference, asset_name FROM Assets ORDER BY asset_id ASC")) {
if (resultSet == null) if (resultSet == null)
return assets; return assets;
@ -170,13 +174,19 @@ public class HSQLDBAssetRepository implements AssetRepository {
} }
@Override @Override
public List<OrderData> getOpenOrders(long haveAssetId, long wantAssetId) throws DataException { public List<OrderData> 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<OrderData> orders = new ArrayList<OrderData>(); List<OrderData> orders = new ArrayList<OrderData>();
try (ResultSet resultSet = this.repository.checkedExecute( try (ResultSet resultSet = this.repository.checkedExecute(sql, haveAssetId, wantAssetId)) {
"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)) {
if (resultSet == null) if (resultSet == null)
return orders; return orders;
@ -202,16 +212,18 @@ public class HSQLDBAssetRepository implements AssetRepository {
} }
@Override @Override
public List<OrderData> getAccountsOrders(byte[] publicKey, boolean includeClosed, boolean includeFulfilled) throws DataException { public List<OrderData> getAccountsOrders(byte[] publicKey, boolean includeClosed, boolean includeFulfilled, Integer limit, Integer offset, Boolean reverse) throws DataException {
List<OrderData> orders = new ArrayList<OrderData>(); String sql = "SELECT asset_order_id, have_asset_id, want_asset_id, amount, fulfilled, price, ordered, is_closed, is_fulfilled FROM AssetOrders WHERE creator = ?";
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) if (!includeClosed)
sql += " AND is_closed = FALSE"; sql += " AND is_closed = FALSE";
if (!includeFulfilled) if (!includeFulfilled)
sql += " AND is_fulfilled = FALSE"; 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<OrderData> orders = new ArrayList<OrderData>();
try (ResultSet resultSet = this.repository.checkedExecute(sql, publicKey)) { try (ResultSet resultSet = this.repository.checkedExecute(sql, publicKey)) {
if (resultSet == null) if (resultSet == null)
@ -267,13 +279,16 @@ public class HSQLDBAssetRepository implements AssetRepository {
// Trades // Trades
@Override @Override
public List<TradeData> getTrades(long haveAssetId, long wantAssetId) throws DataException { public List<TradeData> 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<TradeData> trades = new ArrayList<TradeData>(); List<TradeData> trades = new ArrayList<TradeData>();
try (ResultSet resultSet = this.repository.checkedExecute( try (ResultSet resultSet = this.repository.checkedExecute(sql, haveAssetId, wantAssetId)) {
"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)) {
if (resultSet == null) if (resultSet == null)
return trades; return trades;
@ -295,12 +310,15 @@ public class HSQLDBAssetRepository implements AssetRepository {
} }
@Override @Override
public List<TradeData> getOrdersTrades(byte[] orderId) throws DataException { public List<TradeData> 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<TradeData> trades = new ArrayList<TradeData>(); List<TradeData> trades = new ArrayList<TradeData>();
try (ResultSet resultSet = this.repository.checkedExecute( try (ResultSet resultSet = this.repository.checkedExecute(sql, orderId, orderId)) {
"SELECT initiating_order_id, target_order_id, amount, price, traded FROM AssetTrades WHERE initiating_order_id = ? OR target_order_id = ?",
orderId, orderId)) {
if (resultSet == null) if (resultSet == null)
return trades; return trades;

View File

@ -108,10 +108,15 @@ public class HSQLDBBlockRepository implements BlockRepository {
} }
@Override @Override
public List<TransactionData> getTransactionsFromSignature(byte[] signature) throws DataException { public List<TransactionData> 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<TransactionData> transactions = new ArrayList<TransactionData>(); List<TransactionData> transactions = new ArrayList<TransactionData>();
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) if (resultSet == null)
return transactions; // No transactions in this block return transactions; // No transactions in this block

View File

@ -95,11 +95,15 @@ public class HSQLDBGroupRepository implements GroupRepository {
} }
@Override @Override
public List<GroupData> getAllGroups() throws DataException { public List<GroupData> 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<GroupData> groups = new ArrayList<>(); List<GroupData> groups = new ArrayList<>();
try (ResultSet resultSet = this.repository try (ResultSet resultSet = this.repository.checkedExecute(sql)) {
.checkedExecute("SELECT group_id, owner, group_name, description, created, updated, reference, is_open FROM Groups")) {
if (resultSet == null) if (resultSet == null)
return groups; return groups;
@ -127,11 +131,15 @@ public class HSQLDBGroupRepository implements GroupRepository {
} }
@Override @Override
public List<GroupData> getGroupsByOwner(String owner) throws DataException { public List<GroupData> 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<GroupData> groups = new ArrayList<>(); List<GroupData> groups = new ArrayList<>();
try (ResultSet resultSet = this.repository try (ResultSet resultSet = this.repository.checkedExecute(sql, owner)) {
.checkedExecute("SELECT group_id, group_name, description, created, updated, reference, is_open FROM Groups WHERE owner = ?", owner)) {
if (resultSet == null) if (resultSet == null)
return groups; return groups;
@ -158,12 +166,15 @@ public class HSQLDBGroupRepository implements GroupRepository {
} }
@Override @Override
public List<GroupData> getGroupsWithMember(String member) throws DataException { public List<GroupData> 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<GroupData> groups = new ArrayList<>(); List<GroupData> groups = new ArrayList<>();
try (ResultSet resultSet = this.repository.checkedExecute( try (ResultSet resultSet = this.repository.checkedExecute(sql, member)) {
"SELECT group_id, owner, group_name, description, created, updated, reference, is_open FROM Groups JOIN GroupMembers USING (group_id) WHERE address = ?",
member)) {
if (resultSet == null) if (resultSet == null)
return groups; return groups;
@ -266,10 +277,15 @@ public class HSQLDBGroupRepository implements GroupRepository {
} }
@Override @Override
public List<GroupAdminData> getGroupAdmins(int groupId) throws DataException { public List<GroupAdminData> 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<GroupAdminData> admins = new ArrayList<>(); List<GroupAdminData> 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) if (resultSet == null)
return admins; 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 @Override
public void save(GroupAdminData groupAdminData) throws DataException { public void save(GroupAdminData groupAdminData) throws DataException {
HSQLDBSaver saveHelper = new HSQLDBSaver("GroupAdmins"); HSQLDBSaver saveHelper = new HSQLDBSaver("GroupAdmins");
@ -336,10 +367,15 @@ public class HSQLDBGroupRepository implements GroupRepository {
} }
@Override @Override
public List<GroupMemberData> getGroupMembers(int groupId) throws DataException { public List<GroupMemberData> 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<GroupMemberData> members = new ArrayList<>(); List<GroupMemberData> 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) if (resultSet == null)
return members; return members;
@ -359,13 +395,14 @@ public class HSQLDBGroupRepository implements GroupRepository {
@Override @Override
public Integer countGroupMembers(int groupId) throws DataException { 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 COUNT(*) FROM GroupMembers WHERE group_id = ?", groupId)) {
try (ResultSet resultSet = this.repository.checkedExecute("SELECT group_id, COUNT(*) FROM GroupMembers WHERE group_id = ? GROUP BY group_id", int count = resultSet.getInt(1);
groupId)) {
if (resultSet == null) if (count == 0)
// There must be at least one member: the group owner
return null; return null;
return resultSet.getInt(2); return count;
} catch (SQLException e) { } catch (SQLException e) {
throw new DataException("Unable to fetch group member count from repository", e); throw new DataException("Unable to fetch group member count from repository", e);
} }
@ -425,10 +462,15 @@ public class HSQLDBGroupRepository implements GroupRepository {
} }
@Override @Override
public List<GroupInviteData> getInvitesByGroupId(int groupId) throws DataException { public List<GroupInviteData> 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<GroupInviteData> invites = new ArrayList<>(); List<GroupInviteData> 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) if (resultSet == null)
return invites; return invites;
@ -451,10 +493,15 @@ public class HSQLDBGroupRepository implements GroupRepository {
} }
@Override @Override
public List<GroupInviteData> getInvitesByInvitee(String invitee) throws DataException { public List<GroupInviteData> 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<GroupInviteData> invites = new ArrayList<>(); List<GroupInviteData> 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) if (resultSet == null)
return invites; return invites;
@ -530,10 +577,15 @@ public class HSQLDBGroupRepository implements GroupRepository {
} }
@Override @Override
public List<GroupJoinRequestData> getGroupJoinRequests(int groupId) throws DataException { public List<GroupJoinRequestData> 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<GroupJoinRequestData> joinRequests = new ArrayList<>(); List<GroupJoinRequestData> 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) if (resultSet == null)
return joinRequests; return joinRequests;
@ -604,11 +656,15 @@ public class HSQLDBGroupRepository implements GroupRepository {
} }
@Override @Override
public List<GroupBanData> getGroupBans(int groupId) throws DataException { public List<GroupBanData> 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<GroupBanData> bans = new ArrayList<>(); List<GroupBanData> bans = new ArrayList<>();
try (ResultSet resultSet = this.repository.checkedExecute("SELECT offender, admin, banned, reason, expiry, reference FROM GroupBans WHERE group_id = ?", try (ResultSet resultSet = this.repository.checkedExecute(sql, groupId)) {
groupId)) {
if (resultSet == null) if (resultSet == null)
return bans; return bans;

View File

@ -55,11 +55,15 @@ public class HSQLDBNameRepository implements NameRepository {
} }
@Override @Override
public List<NameData> getAllNames() throws DataException { public List<NameData> 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<NameData> names = new ArrayList<>(); List<NameData> names = new ArrayList<>();
try (ResultSet resultSet = this.repository try (ResultSet resultSet = this.repository.checkedExecute(sql)) {
.checkedExecute("SELECT name, data, owner, registered, updated, reference, is_for_sale, sale_price FROM Names")) {
if (resultSet == null) if (resultSet == null)
return names; return names;
@ -87,11 +91,15 @@ public class HSQLDBNameRepository implements NameRepository {
} }
@Override @Override
public List<NameData> getNamesForSale() throws DataException { public List<NameData> 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<NameData> names = new ArrayList<>(); List<NameData> names = new ArrayList<>();
try (ResultSet resultSet = this.repository try (ResultSet resultSet = this.repository.checkedExecute(sql)) {
.checkedExecute("SELECT name, data, owner, registered, updated, reference, sale_price FROM Names WHERE is_for_sale = TRUE")) {
if (resultSet == null) if (resultSet == null)
return names; return names;
@ -119,11 +127,15 @@ public class HSQLDBNameRepository implements NameRepository {
} }
@Override @Override
public List<NameData> getNamesByOwner(String owner) throws DataException { public List<NameData> 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<NameData> names = new ArrayList<>(); List<NameData> names = new ArrayList<>();
try (ResultSet resultSet = this.repository try (ResultSet resultSet = this.repository.checkedExecute(sql, owner)) {
.checkedExecute("SELECT name, data, registered, updated, reference, is_for_sale, sale_price FROM Names WHERE owner = ?", owner)) {
if (resultSet == null) if (resultSet == null)
return names; return names;
@ -157,9 +169,9 @@ public class HSQLDBNameRepository implements NameRepository {
Long updated = nameData.getUpdated(); Long updated = nameData.getUpdated();
Timestamp updatedTimestamp = updated == null ? null : new Timestamp(updated); Timestamp updatedTimestamp = updated == null ? null : new Timestamp(updated);
saveHelper.bind("owner", nameData.getOwner()).bind("name", nameData.getName()) saveHelper.bind("owner", nameData.getOwner()).bind("name", nameData.getName()).bind("data", nameData.getData())
.bind("data", nameData.getData()).bind("registered", new Timestamp(nameData.getRegistered())).bind("updated", updatedTimestamp) .bind("registered", new Timestamp(nameData.getRegistered())).bind("updated", updatedTimestamp).bind("reference", nameData.getReference())
.bind("reference", nameData.getReference()).bind("is_for_sale", nameData.getIsForSale()).bind("sale_price", nameData.getSalePrice()); .bind("is_for_sale", nameData.getIsForSale()).bind("sale_price", nameData.getSalePrice());
try { try {
saveHelper.execute(this.repository); saveHelper.execute(this.repository);

View File

@ -287,4 +287,25 @@ public class HSQLDBRepository implements Repository {
} }
} }
/**
* Returns additional SQL "LIMIT" and "OFFSET" clauses.
* <p>
* (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;
}
} }

View File

@ -13,6 +13,7 @@ import java.util.List;
import org.apache.logging.log4j.LogManager; import org.apache.logging.log4j.LogManager;
import org.apache.logging.log4j.Logger; import org.apache.logging.log4j.Logger;
import org.qora.api.resource.TransactionsResource.ConfirmationStatus;
import org.qora.data.PaymentData; import org.qora.data.PaymentData;
import org.qora.data.transaction.TransactionData; import org.qora.data.transaction.TransactionData;
import org.qora.repository.DataException; import org.qora.repository.DataException;
@ -53,7 +54,8 @@ public class HSQLDBTransactionRepository implements TransactionRepository {
private static Class<?> getClassByTxType(TransactionType txType) { private static Class<?> getClassByTxType(TransactionType txType) {
try { 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) { } catch (ClassNotFoundException e) {
return null; return null;
} }
@ -204,7 +206,7 @@ public class HSQLDBTransactionRepository implements TransactionRepository {
} }
@Override @Override
public List<byte[]> getAllSignaturesInvolvingAddress(String address) throws DataException { public List<byte[]> getSignaturesInvolvingAddress(String address) throws DataException {
List<byte[]> signatures = new ArrayList<byte[]>(); List<byte[]> signatures = new ArrayList<byte[]>();
try (ResultSet resultSet = this.repository.checkedExecute("SELECT signature FROM TransactionRecipients WHERE participant = ?", address)) { try (ResultSet resultSet = this.repository.checkedExecute("SELECT signature FROM TransactionRecipients WHERE participant = ?", address)) {
@ -250,7 +252,8 @@ public class HSQLDBTransactionRepository implements TransactionRepository {
} }
@Override @Override
public List<byte[]> getAllSignaturesMatchingCriteria(Integer startBlock, Integer blockLimit, TransactionType txType, String address) throws DataException { public List<byte[]> getSignaturesMatchingCriteria(Integer startBlock, Integer blockLimit, TransactionType txType, String address,
ConfirmationStatus confirmationStatus, Integer limit, Integer offset, Boolean reverse) throws DataException {
List<byte[]> signatures = new ArrayList<byte[]>(); List<byte[]> signatures = new ArrayList<byte[]>();
boolean hasAddress = address != null && !address.isEmpty(); boolean hasAddress = address != null && !address.isEmpty();
@ -258,33 +261,42 @@ public class HSQLDBTransactionRepository implements TransactionRepository {
boolean hasHeightRange = startBlock != null || blockLimit != null; boolean hasHeightRange = startBlock != null || blockLimit != null;
if (hasHeightRange && startBlock == null) if (hasHeightRange && startBlock == null)
startBlock = 1; startBlock = (reverse == null || !reverse) ? 1 : this.repository.getBlockRepository().getBlockchainHeight() - blockLimit;
String signatureColumn = "NULL"; String signatureColumn = "Transactions.signature";
List<Object> bindParams = new ArrayList<Object>(); List<String> whereClauses = new ArrayList<String>();
String groupBy = ""; String groupBy = "";
List<Object> bindParams = new ArrayList<Object>();
// Table JOINs first // Tables, starting with Transactions
List<String> tableJoins = new ArrayList<String>(); String tables = "Transactions";
// Always JOIN BlockTransactions as we only ever want confirmed transactions // BlockTransactions if we want confirmed transactions
tableJoins.add("Blocks"); switch (confirmationStatus) {
tableJoins.add("BlockTransactions ON BlockTransactions.block_signature = Blocks.signature"); case BOTH:
signatureColumn = "BlockTransactions.transaction_signature"; break;
// Always JOIN Transactions as we want to order by timestamp case CONFIRMED:
tableJoins.add("Transactions ON Transactions.signature = BlockTransactions.transaction_signature"); tables += " JOIN BlockTransactions ON BlockTransactions.transaction_signature = Transactions.signature";
signatureColumn = "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) { if (hasAddress) {
tableJoins.add("TransactionParticipants ON TransactionParticipants.signature = Transactions.signature"); tables += " JOIN TransactionParticipants ON TransactionParticipants.signature = Transactions.signature";
signatureColumn = "TransactionParticipants.signature";
groupBy = " GROUP BY TransactionParticipants.signature, Transactions.creation"; groupBy = " GROUP BY TransactionParticipants.signature, Transactions.creation";
signatureColumn = "TransactionParticipants.signature";
} }
// WHERE clauses next // WHERE clauses next
List<String> whereClauses = new ArrayList<String>();
if (hasHeightRange) { if (hasHeightRange) {
whereClauses.add("Blocks.height >= " + startBlock); whereClauses.add("Blocks.height >= " + startBlock);
@ -300,7 +312,19 @@ public class HSQLDBTransactionRepository implements TransactionRepository {
bindParams.add(address); 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); LOGGER.trace(sql);
try (ResultSet resultSet = this.repository.checkedExecute(sql, bindParams.toArray())) { try (ResultSet resultSet = this.repository.checkedExecute(sql, bindParams.toArray())) {
@ -320,11 +344,19 @@ public class HSQLDBTransactionRepository implements TransactionRepository {
} }
@Override @Override
public List<TransactionData> getAllUnconfirmedTransactions() throws DataException { public List<TransactionData> 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<TransactionData> transactions = new ArrayList<TransactionData>(); List<TransactionData> transactions = new ArrayList<TransactionData>();
// Find transactions with no corresponding row in BlockTransactions // 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) if (resultSet == null)
return transactions; return transactions;

View File

@ -468,7 +468,7 @@ public abstract class Transaction {
} }
private int countUnconfirmedByCreator(PublicKeyAccount creator) throws DataException { private int countUnconfirmedByCreator(PublicKeyAccount creator) throws DataException {
List<TransactionData> unconfirmedTransactions = repository.getTransactionRepository().getAllUnconfirmedTransactions(); List<TransactionData> unconfirmedTransactions = repository.getTransactionRepository().getUnconfirmedTransactions();
int count = 0; int count = 0;
for (TransactionData transactionData : unconfirmedTransactions) { for (TransactionData transactionData : unconfirmedTransactions) {
@ -495,7 +495,7 @@ public abstract class Transaction {
public static List<TransactionData> getUnconfirmedTransactions(Repository repository) throws DataException { public static List<TransactionData> getUnconfirmedTransactions(Repository repository) throws DataException {
BlockData latestBlockData = repository.getBlockRepository().getLastBlock(); BlockData latestBlockData = repository.getBlockRepository().getLastBlock();
List<TransactionData> unconfirmedTransactions = repository.getTransactionRepository().getAllUnconfirmedTransactions(); List<TransactionData> unconfirmedTransactions = repository.getTransactionRepository().getUnconfirmedTransactions();
List<TransactionData> invalidTransactions = new ArrayList<>(); List<TransactionData> invalidTransactions = new ArrayList<>();
unconfirmedTransactions.sort(getDataComparator()); unconfirmedTransactions.sort(getDataComparator());