3
0
mirror of https://github.com/Qortal/qortal.git synced 2025-02-14 11:15: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/resources=UTF-8
encoding//src/test/java=UTF-8
encoding//target/generated-sources/package-info=UTF-8
encoding/<project>=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"
logger.hsqldb.name = hsqldb.db
logger.hsqldb.level = warn
logger.hsqldb.appenderRef.rolling.ref = FILE
# Override logging level for this class
logger.voting.name = qora.transaction.VoteOnPollTransaction
logger.voting.level = trace
logger.voting.appenderRef.rolling.ref = FILE
# Suppress extraneous Jersey warning
logger.jerseyInject.name = org.glassfish.jersey.internal.inject.Providers
logger.jerseyInject.level = error
# Override logging level for this class
logger.assets.name = qora.assets.Order
logger.assets.level = trace
logger.assets.appenderRef.rolling.ref = FILE
# Debugging transaction searches
logger.txSearch.name = org.qora.repository.hsqldb.transaction.HSQLDBTransactionRepository
logger.txSearch.level = trace
appender.console.type = Console
appender.console.name = stdout

19
pom.xml
View File

@ -305,17 +305,24 @@
<artifactId>log4j-api</artifactId>
<version>${log4j.version}</version>
</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>
<groupId>org.slf4j</groupId>
<artifactId>slf4j-api</artifactId>
<version>${slf4j.version}</version>
</dependency>
<dependency>
<groupId>org.slf4j</groupId>
<artifactId>slf4j-simple</artifactId>
<version>${slf4j.version}</version>
</dependency>
<!-- Servlet related -->
<dependency>
<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.XmlElement;
import org.qora.data.group.GroupData;
import org.qora.data.group.GroupMemberData;
import io.swagger.v3.oas.annotations.media.Schema;
@Schema(description = "Group info, maybe including members")
// All properties to be converted to JSON via JAX-RS
@XmlAccessorType(XmlAccessType.FIELD)
public class GroupWithMemberInfo {
@Schema(implementation = GroupData.class, name = "group", title = "group info")
@XmlElement(name = "group")
public GroupData groupData;
public class GroupMembers {
Integer memberCount;
@XmlElement(name = "admins")
public List<String> groupAdminAddresses;
Integer adminCount;
@XmlAccessorType(XmlAccessType.FIELD)
@Schema(description = "Member info")
public static class MemberInfo {
public String member;
public long joined;
public Long joined;
public Boolean isAdmin;
// For JAX-RS
protected MemberInfo() {
}
public MemberInfo(GroupMemberData groupMemberData) {
this.member = groupMemberData.getMember();
this.joined = groupMemberData.getJoined();
public MemberInfo(String member, Long joined, boolean isAdmin) {
this.member = member;
this.joined = joined;
this.isAdmin = isAdmin ? true : null; // null so field is not displayed by API
}
}
@ -45,14 +38,13 @@ public class GroupWithMemberInfo {
public List<MemberInfo> groupMembers;
// For JAX-RS
protected GroupWithMemberInfo() {
protected GroupMembers() {
}
public GroupWithMemberInfo(GroupData groupData, List<String> groupAdminAddresses, List<MemberInfo> groupMembers, Integer memberCount) {
this.groupData = groupData;
this.groupAdminAddresses = groupAdminAddresses;
public GroupMembers(List<MemberInfo> groupMembers, Integer memberCount, Integer adminCount) {
this.groupMembers = groupMembers;
this.memberCount = memberCount;
this.adminCount = adminCount;
}
}

View File

@ -1,21 +1,18 @@
package org.qora.api.resource;
import io.swagger.v3.oas.annotations.Operation;
import io.swagger.v3.oas.annotations.media.ArraySchema;
import io.swagger.v3.oas.annotations.media.Content;
import io.swagger.v3.oas.annotations.media.Schema;
import io.swagger.v3.oas.annotations.responses.ApiResponse;
import io.swagger.v3.oas.annotations.tags.Tag;
import java.math.BigDecimal;
import java.util.List;
import javax.servlet.http.HttpServletRequest;
import javax.ws.rs.GET;
import javax.ws.rs.Path;
import javax.ws.rs.PathParam;
import javax.ws.rs.Produces;
import javax.ws.rs.QueryParam;
import javax.ws.rs.core.Context;
import javax.ws.rs.core.MediaType;
@ -26,9 +23,7 @@ import org.qora.api.ApiException;
import org.qora.api.ApiExceptionFactory;
import org.qora.asset.Asset;
import org.qora.crypto.Crypto;
import org.qora.data.account.AccountBalanceData;
import org.qora.data.account.AccountData;
import org.qora.data.asset.OrderData;
import org.qora.repository.DataException;
import org.qora.repository.Repository;
import org.qora.repository.RepositoryManager;
@ -145,59 +140,6 @@ public class AddressesResource {
}
}
@GET
@Path("/assetbalance/{assetid}/{address}")
@Operation(
summary = "Asset-specific balance request",
description = "Returns the confirmed balance of the given address for the given asset key.",
responses = {
@ApiResponse(
description = "the balance",
content = @Content(mediaType = MediaType.TEXT_PLAIN, schema = @Schema(type = "string", format = "number"))
)
}
)
@ApiErrors({ApiError.INVALID_ADDRESS, ApiError.REPOSITORY_ISSUE})
public BigDecimal getAssetBalance(@PathParam("assetid") long assetid, @PathParam("address") String address) {
if (!Crypto.isValidAddress(address))
throw ApiExceptionFactory.INSTANCE.createException(request, ApiError.INVALID_ADDRESS);
try (final Repository repository = RepositoryManager.getRepository()) {
Account account = new Account(repository, address);
return account.getConfirmedBalance(assetid);
} catch (ApiException e) {
throw e;
} catch (DataException e) {
throw ApiExceptionFactory.INSTANCE.createException(request, ApiError.REPOSITORY_ISSUE, e);
}
}
@GET
@Path("/assets/{address}")
@Operation(
summary = "All assets owned by this address",
description = "Returns the list of assets for this address, with balances.",
responses = {
@ApiResponse(
description = "the list of assets",
content = @Content(array = @ArraySchema(schema = @Schema(implementation = AccountBalanceData.class)))
)
}
)
@ApiErrors({ApiError.INVALID_ADDRESS, ApiError.REPOSITORY_ISSUE})
public List<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
@Path("/balance/{address}/{confirmations}")
@Operation(
@ -283,38 +225,4 @@ public class AddressesResource {
}
}
@GET
@Path("/assetorders/{address}")
@Operation(
summary = "Asset orders created by this address",
responses = {
@ApiResponse(
description = "Asset orders",
content = @Content(array = @ArraySchema(schema = @Schema(implementation = OrderData.class)))
)
}
)
@ApiErrors({ApiError.INVALID_ADDRESS, ApiError.ADDRESS_NO_EXISTS, ApiError.REPOSITORY_ISSUE})
public List<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
@Path("/unused")
@Parameter(in = ParameterIn.PATH, name = "blockSignature", description = "Block signature", schema = @Schema(type = "string", format = "byte"), example = "very_long_block_signature_in_base58")
@Parameter(in = ParameterIn.PATH, name = "assetId", description = "Asset ID, 0 is native coin", schema = @Schema(type = "string", format = "byte"))
@Parameter(in = ParameterIn.PATH, name = "otherAssetId", description = "Asset ID, 0 is native coin", schema = @Schema(type = "string", format = "byte"))
@Parameter(in = ParameterIn.PATH, name = "assetid", description = "Asset ID, 0 is native coin", schema = @Schema(type = "integer"))
@Parameter(in = ParameterIn.PATH, name = "otherassetid", description = "Asset ID, 0 is native coin", schema = @Schema(type = "integer"))
@Parameter(in = ParameterIn.PATH, name = "address", description = "an account address", example = "QgV4s3xnzLhVBEJxcYui4u4q11yhUHsd9v")
@Parameter(in = ParameterIn.QUERY, name = "count", description = "Maximum number of entries to return, 0 means none", schema = @Schema(type = "integer", defaultValue = "20"))
@Parameter(in = ParameterIn.QUERY, name = "limit", description = "Maximum number of entries to return, 0 means unlimited", schema = @Schema(type = "integer", defaultValue = "20"))
@Parameter(in = ParameterIn.QUERY, name = "offset", description = "Starting entry in results, 0 is first entry", schema = @Schema(type = "integer"))
@Parameter(in = ParameterIn.QUERY, name = "includeTransactions", description = "Include associated transactions in results", schema = @Schema(type = "boolean"))
@Parameter(in = ParameterIn.QUERY, name = "includeHolders", description = "Include asset holders in results", schema = @Schema(type = "boolean"))
@Parameter(in = ParameterIn.QUERY, name = "queryAssetId", description = "Asset ID, 0 is native coin", schema = @Schema(type = "string", format = "byte"))
@Parameter(in = ParameterIn.QUERY, name = "reverse", description = "Reverse results", schema = @Schema(type = "boolean"))
public String globalParameters() {
return "";
}

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.tags.Tag;
import java.math.BigDecimal;
import java.util.ArrayList;
import java.util.List;
@ -22,13 +23,15 @@ import javax.ws.rs.QueryParam;
import javax.ws.rs.core.Context;
import javax.ws.rs.core.MediaType;
import org.qora.account.Account;
import org.qora.api.ApiError;
import org.qora.api.ApiErrors;
import org.qora.api.ApiException;
import org.qora.api.ApiExceptionFactory;
import org.qora.api.model.AssetWithHolders;
import org.qora.api.model.OrderWithTrades;
import org.qora.api.model.TradeWithOrderInfo;
import org.qora.crypto.Crypto;
import org.qora.data.account.AccountBalanceData;
import org.qora.data.account.AccountData;
import org.qora.data.asset.AssetData;
import org.qora.data.asset.OrderData;
import org.qora.data.asset.TradeData;
@ -47,8 +50,12 @@ import org.qora.transform.transaction.IssueAssetTransactionTransformer;
import org.qora.utils.Base58;
@Path("/assets")
@Produces({MediaType.APPLICATION_JSON, MediaType.TEXT_PLAIN})
@Tag(name = "Assets")
@Produces({
MediaType.APPLICATION_JSON, MediaType.TEXT_PLAIN
})
@Tag(
name = "Assets"
)
public class AssetsResource {
@Context
@ -60,21 +67,28 @@ public class AssetsResource {
responses = {
@ApiResponse(
description = "asset info",
content = @Content(array = @ArraySchema(schema = @Schema(implementation = AssetData.class)))
content = @Content(
array = @ArraySchema(
schema = @Schema(
implementation = AssetData.class
)
)
)
)
}
)
@ApiErrors({ApiError.REPOSITORY_ISSUE})
public List<AssetData> getAllAssets(@Parameter(ref = "limit") @QueryParam("limit") int limit, @Parameter(ref = "offset") @QueryParam("offset") int offset) {
@ApiErrors({
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()) {
List<AssetData> assets = repository.getAssetRepository().getAllAssets();
// Pagination would take effect here (or as part of the repository access)
int fromIndex = Integer.min(offset, assets.size());
int toIndex = limit == 0 ? assets.size() : Integer.min(fromIndex + limit, assets.size());
assets = assets.subList(fromIndex, toIndex);
return assets;
return repository.getAssetRepository().getAllAssets(limit, offset, reverse);
} catch (DataException e) {
throw ApiExceptionFactory.INSTANCE.createException(request, ApiError.REPOSITORY_ISSUE, e);
}
@ -88,12 +102,18 @@ public class AssetsResource {
responses = {
@ApiResponse(
description = "asset info",
content = @Content(array = @ArraySchema(schema = @Schema(implementation = AssetWithHolders.class)))
content = @Content(
schema = @Schema(
implementation = AssetData.class
)
)
)
}
)
@ApiErrors({ApiError.INVALID_CRITERIA, ApiError.INVALID_ASSET_ID, ApiError.REPOSITORY_ISSUE})
public AssetWithHolders getAssetInfo(@QueryParam("assetId") Integer assetId, @QueryParam("assetName") String assetName, @Parameter(ref = "includeHolders") @QueryParam("includeHolders") boolean includeHolders) {
@ApiErrors({
ApiError.INVALID_CRITERIA, ApiError.INVALID_ASSET_ID, ApiError.REPOSITORY_ISSUE
})
public AssetData getAssetInfo(@QueryParam("assetId") Integer assetId, @QueryParam("assetName") String assetName) {
if (assetId == null && (assetName == null || assetName.isEmpty()))
throw ApiExceptionFactory.INSTANCE.createException(request, ApiError.INVALID_CRITERIA);
@ -108,31 +128,81 @@ public class AssetsResource {
if (assetData == null)
throw ApiExceptionFactory.INSTANCE.createException(request, ApiError.INVALID_ASSET_ID);
List<AccountBalanceData> holders = null;
if (includeHolders)
holders = repository.getAccountRepository().getAssetBalances(assetData.getAssetId());
return new AssetWithHolders(assetData, holders);
return assetData;
} catch (DataException e) {
throw ApiExceptionFactory.INSTANCE.createException(request, ApiError.REPOSITORY_ISSUE, e);
}
}
@GET
@Path("/orderbook/{assetId}/{otherAssetId}")
@Path("/holders/{assetid}")
@Operation(
summary = "List holders of an asset",
responses = {
@ApiResponse(
description = "asset holders",
content = @Content(
array = @ArraySchema(
schema = @Schema(
implementation = AccountBalanceData.class
)
)
)
)
}
)
@ApiErrors({
ApiError.INVALID_CRITERIA, ApiError.INVALID_ASSET_ID, ApiError.REPOSITORY_ISSUE
})
public List<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(
summary = "Asset order book",
description = "Returns open orders, offering {assetId} for {otherAssetId} in return.",
description = "Returns open orders, offering {assetid} for {otherassetid} in return.",
responses = {
@ApiResponse(
description = "asset orders",
content = @Content(array = @ArraySchema(schema = @Schema(implementation = OrderData.class)))
content = @Content(
array = @ArraySchema(
schema = @Schema(
implementation = OrderData.class
)
)
)
)
}
)
@ApiErrors({ApiError.INVALID_ASSET_ID, ApiError.REPOSITORY_ISSUE})
public List<OrderData> getAssetOrders(@Parameter(ref = "assetId") @PathParam("assetId") int assetId, @Parameter(ref = "otherAssetId") @PathParam("otherAssetId") int otherAssetId,
@Parameter(ref = "limit") @QueryParam("limit") int limit, @Parameter(ref = "offset") @QueryParam("offset") int offset) {
@ApiErrors({
ApiError.INVALID_ASSET_ID, ApiError.REPOSITORY_ISSUE
})
public List<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()) {
if (!repository.getAssetRepository().assetExists(assetId))
throw ApiExceptionFactory.INSTANCE.createException(request, ApiError.INVALID_ASSET_ID);
@ -140,36 +210,45 @@ public class AssetsResource {
if (!repository.getAssetRepository().assetExists(otherAssetId))
throw ApiExceptionFactory.INSTANCE.createException(request, ApiError.INVALID_ASSET_ID);
List<OrderData> orders = repository.getAssetRepository().getOpenOrders(assetId, otherAssetId);
// Pagination would take effect here (or as part of the repository access)
int fromIndex = Integer.min(offset, orders.size());
int toIndex = limit == 0 ? orders.size() : Integer.min(fromIndex + limit, orders.size());
orders = orders.subList(fromIndex, toIndex);
return orders;
return repository.getAssetRepository().getOpenOrders(assetId, otherAssetId, limit, offset, reverse);
} catch (DataException e) {
throw ApiExceptionFactory.INSTANCE.createException(request, ApiError.REPOSITORY_ISSUE, e);
}
}
@GET
@Path("/trades/{assetId}/{otherAssetId}")
@Path("/trades/{assetid}/{otherassetid}")
@Operation(
summary = "Asset trades",
description = "Returns successful trades of {assetId} for {otherAssetId}.<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.",
description = "Returns successful trades of {assetid} for {otherassetid}.<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.",
responses = {
@ApiResponse(
description = "asset trades",
content = @Content(array = @ArraySchema(schema = @Schema(implementation = TradeWithOrderInfo.class)))
content = @Content(
array = @ArraySchema(
schema = @Schema(
implementation = TradeWithOrderInfo.class
)
)
)
)
}
)
@ApiErrors({ApiError.INVALID_ASSET_ID, ApiError.REPOSITORY_ISSUE})
public List<TradeWithOrderInfo> getAssetTrades(@Parameter(ref = "assetId") @PathParam("assetId") int assetId, @Parameter(ref = "otherAssetId") @PathParam("otherAssetId") int otherAssetId,
@Parameter(ref = "limit") @QueryParam("limit") int limit, @Parameter(ref = "offset") @QueryParam("offset") int offset) {
@ApiErrors({
ApiError.INVALID_ASSET_ID, ApiError.REPOSITORY_ISSUE
})
public List<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()) {
if (!repository.getAssetRepository().assetExists(assetId))
throw ApiExceptionFactory.INSTANCE.createException(request, ApiError.INVALID_ASSET_ID);
@ -177,12 +256,7 @@ public class AssetsResource {
if (!repository.getAssetRepository().assetExists(otherAssetId))
throw ApiExceptionFactory.INSTANCE.createException(request, ApiError.INVALID_ASSET_ID);
List<TradeData> trades = repository.getAssetRepository().getTrades(assetId, otherAssetId);
// Pagination would take effect here (or as part of the repository access)
int fromIndex = Integer.min(offset, trades.size());
int toIndex = limit == 0 ? trades.size() : Integer.min(fromIndex + limit, trades.size());
trades = trades.subList(fromIndex, toIndex);
List<TradeData> trades = repository.getAssetRepository().getTrades(assetId, otherAssetId, limit, offset, reverse);
// Expanding remaining entries
List<TradeWithOrderInfo> fullTrades = new ArrayList<>();
@ -199,19 +273,25 @@ public class AssetsResource {
}
@GET
@Path("/order/{orderId}")
@Path("/order/{orderid}")
@Operation(
summary = "Fetch asset order",
description = "Returns asset order info.",
responses = {
@ApiResponse(
description = "asset order",
content = @Content(schema = @Schema(implementation = OrderData.class))
content = @Content(
schema = @Schema(
implementation = OrderData.class
)
)
)
}
)
@ApiErrors({ApiError.INVALID_ORDER_ID, ApiError.ORDER_NO_EXISTS, ApiError.REPOSITORY_ISSUE})
public OrderWithTrades getAssetOrder(@PathParam("orderId") String orderId58) {
@ApiErrors({
ApiError.INVALID_ORDER_ID, ApiError.ORDER_NO_EXISTS, ApiError.REPOSITORY_ISSUE
})
public OrderData getAssetOrder(@PathParam("orderid") String orderId58) {
// Decode orderID
byte[] orderId;
try {
@ -223,11 +303,180 @@ public class AssetsResource {
try (final Repository repository = RepositoryManager.getRepository()) {
OrderData orderData = repository.getAssetRepository().fromOrderId(orderId);
if (orderData == null)
throw ApiExceptionFactory.INSTANCE.createException(request, ApiError.ORDER_NO_EXISTS);
throw ApiExceptionFactory.INSTANCE.createException(request, ApiError.ORDER_NO_EXISTS);
List<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) {
throw ApiExceptionFactory.INSTANCE.createException(request, ApiError.REPOSITORY_ISSUE, e);
}
@ -237,11 +486,13 @@ public class AssetsResource {
@Path("/order/delete")
@Operation(
summary = "Cancel existing asset order",
requestBody = @RequestBody(
requestBody = @RequestBody(
required = true,
content = @Content(
mediaType = MediaType.APPLICATION_JSON,
schema = @Schema(implementation = CancelAssetOrderTransactionData.class)
schema = @Schema(
implementation = CancelAssetOrderTransactionData.class
)
)
),
responses = {
@ -256,7 +507,9 @@ public class AssetsResource {
)
}
)
@ApiErrors({ApiError.TRANSFORMATION_ERROR, ApiError.REPOSITORY_ISSUE, ApiError.TRANSACTION_INVALID})
@ApiErrors({
ApiError.TRANSFORMATION_ERROR, ApiError.REPOSITORY_ISSUE, ApiError.TRANSACTION_INVALID
})
public String cancelOrder(CancelAssetOrderTransactionData transactionData) {
try (final Repository repository = RepositoryManager.getRepository()) {
Transaction transaction = Transaction.fromData(repository, transactionData);
@ -282,7 +535,9 @@ public class AssetsResource {
required = true,
content = @Content(
mediaType = MediaType.APPLICATION_JSON,
schema = @Schema(implementation = IssueAssetTransactionData.class)
schema = @Schema(
implementation = IssueAssetTransactionData.class
)
)
),
responses = {
@ -297,7 +552,9 @@ public class AssetsResource {
)
}
)
@ApiErrors({ApiError.TRANSFORMATION_ERROR, ApiError.REPOSITORY_ISSUE, ApiError.TRANSACTION_INVALID})
@ApiErrors({
ApiError.TRANSFORMATION_ERROR, ApiError.REPOSITORY_ISSUE, ApiError.TRANSACTION_INVALID
})
public String issueAsset(IssueAssetTransactionData transactionData) {
try (final Repository repository = RepositoryManager.getRepository()) {
Transaction transaction = Transaction.fromData(repository, transactionData);
@ -323,7 +580,9 @@ public class AssetsResource {
required = true,
content = @Content(
mediaType = MediaType.APPLICATION_JSON,
schema = @Schema(implementation = CreateAssetOrderTransactionData.class)
schema = @Schema(
implementation = CreateAssetOrderTransactionData.class
)
)
),
responses = {
@ -338,7 +597,9 @@ public class AssetsResource {
)
}
)
@ApiErrors({ApiError.TRANSFORMATION_ERROR, ApiError.REPOSITORY_ISSUE, ApiError.TRANSACTION_INVALID})
@ApiErrors({
ApiError.TRANSFORMATION_ERROR, ApiError.REPOSITORY_ISSUE, ApiError.TRANSACTION_INVALID
})
public String createOrder(CreateAssetOrderTransactionData transactionData) {
try (final Repository repository = RepositoryManager.getRepository()) {
Transaction transaction = Transaction.fromData(repository, transactionData);

View File

@ -2,6 +2,7 @@ package org.qora.api.resource;
import io.swagger.v3.oas.annotations.Operation;
import io.swagger.v3.oas.annotations.Parameter;
import io.swagger.v3.oas.annotations.media.ArraySchema;
import io.swagger.v3.oas.annotations.media.Content;
import io.swagger.v3.oas.annotations.media.Schema;
import io.swagger.v3.oas.annotations.responses.ApiResponse;
@ -10,7 +11,6 @@ import io.swagger.v3.oas.annotations.tags.Tag;
import java.math.BigDecimal;
import java.util.ArrayList;
import java.util.List;
import java.util.stream.Collectors;
import javax.servlet.http.HttpServletRequest;
@ -26,7 +26,6 @@ import org.qora.api.ApiError;
import org.qora.api.ApiErrors;
import org.qora.api.ApiException;
import org.qora.api.ApiExceptionFactory;
import org.qora.api.model.BlockWithTransactions;
import org.qora.block.Block;
import org.qora.data.block.BlockData;
import org.qora.data.transaction.TransactionData;
@ -36,8 +35,12 @@ import org.qora.repository.RepositoryManager;
import org.qora.utils.Base58;
@Path("/blocks")
@Produces({MediaType.APPLICATION_JSON, MediaType.TEXT_PLAIN})
@Tag(name = "Blocks")
@Produces({
MediaType.APPLICATION_JSON, MediaType.TEXT_PLAIN
})
@Tag(
name = "Blocks"
)
public class BlocksResource {
@Context
@ -53,14 +56,16 @@ public class BlocksResource {
description = "the block",
content = @Content(
schema = @Schema(
implementation = BlockWithTransactions.class
implementation = BlockData.class
)
)
)
}
)
@ApiErrors({ApiError.INVALID_SIGNATURE, ApiError.BLOCK_NO_EXISTS, ApiError.REPOSITORY_ISSUE})
public BlockWithTransactions getBlock(@PathParam("signature") String signature58, @Parameter(ref = "includeTransactions") @QueryParam("includeTransactions") boolean includeTransactions) {
@ApiErrors({
ApiError.INVALID_SIGNATURE, ApiError.BLOCK_NO_EXISTS, ApiError.REPOSITORY_ISSUE
})
public BlockData getBlock(@PathParam("signature") String signature58) {
// Decode signature
byte[] signature;
try {
@ -70,8 +75,55 @@ public class BlocksResource {
}
try (final Repository repository = RepositoryManager.getRepository()) {
BlockData blockData = repository.getBlockRepository().fromSignature(signature);
return packageBlockData(repository, blockData, includeTransactions);
return repository.getBlockRepository().fromSignature(signature);
} catch (ApiException e) {
throw e;
} catch (DataException e) {
throw ApiExceptionFactory.INSTANCE.createException(request, ApiError.REPOSITORY_ISSUE, e);
}
}
@GET
@Path("/signature/{signature}/transactions")
@Operation(
summary = "Fetch block using base58 signature",
description = "Returns the block that matches the given signature",
responses = {
@ApiResponse(
description = "the block",
content = @Content(
array = @ArraySchema(
schema = @Schema(
implementation = TransactionData.class
)
)
)
)
}
)
@ApiErrors({
ApiError.INVALID_SIGNATURE, ApiError.BLOCK_NO_EXISTS, ApiError.REPOSITORY_ISSUE
})
public List<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) {
throw e;
} catch (DataException e) {
@ -89,17 +141,18 @@ public class BlocksResource {
description = "the block",
content = @Content(
schema = @Schema(
implementation = BlockWithTransactions.class
implementation = BlockData.class
)
)
)
}
)
@ApiErrors({ApiError.BLOCK_NO_EXISTS, ApiError.REPOSITORY_ISSUE})
public BlockWithTransactions getFirstBlock(@Parameter(ref = "includeTransactions") @QueryParam("includeTransactions") boolean includeTransactions) {
@ApiErrors({
ApiError.BLOCK_NO_EXISTS, ApiError.REPOSITORY_ISSUE
})
public BlockData getFirstBlock() {
try (final Repository repository = RepositoryManager.getRepository()) {
BlockData blockData = repository.getBlockRepository().fromHeight(1);
return packageBlockData(repository, blockData, includeTransactions);
return repository.getBlockRepository().fromHeight(1);
} catch (ApiException e) {
throw e;
} catch (DataException e) {
@ -117,17 +170,18 @@ public class BlocksResource {
description = "the block",
content = @Content(
schema = @Schema(
implementation = BlockWithTransactions.class
implementation = BlockData.class
)
)
)
}
)
@ApiErrors({ApiError.BLOCK_NO_EXISTS, ApiError.REPOSITORY_ISSUE})
public BlockWithTransactions getLastBlock(@Parameter(ref = "includeTransactions") @QueryParam("includeTransactions") boolean includeTransactions) {
@ApiErrors({
ApiError.BLOCK_NO_EXISTS, ApiError.REPOSITORY_ISSUE
})
public BlockData getLastBlock() {
try (final Repository repository = RepositoryManager.getRepository()) {
BlockData blockData = repository.getBlockRepository().getLastBlock();
return packageBlockData(repository, blockData, includeTransactions);
return repository.getBlockRepository().getLastBlock();
} catch (ApiException e) {
throw e;
} catch (DataException e) {
@ -145,14 +199,16 @@ public class BlocksResource {
description = "the block",
content = @Content(
schema = @Schema(
implementation = BlockWithTransactions.class
implementation = BlockData.class
)
)
)
}
)
@ApiErrors({ApiError.INVALID_SIGNATURE, ApiError.BLOCK_NO_EXISTS, ApiError.REPOSITORY_ISSUE})
public BlockWithTransactions getChild(@PathParam("signature") String signature58, @Parameter(ref = "includeTransactions") @QueryParam("includeTransactions") boolean includeTransactions) {
@ApiErrors({
ApiError.INVALID_SIGNATURE, ApiError.BLOCK_NO_EXISTS, ApiError.REPOSITORY_ISSUE
})
public BlockData getChild(@PathParam("signature") String signature58) {
// Decode signature
byte[] signature;
try {
@ -170,8 +226,11 @@ public class BlocksResource {
BlockData childBlockData = repository.getBlockRepository().fromReference(signature);
// Checking child exists is handled by packageBlockData()
return packageBlockData(repository, childBlockData, includeTransactions);
// Check child block exists
if (childBlockData == null)
throw ApiExceptionFactory.INSTANCE.createException(request, ApiError.BLOCK_NO_EXISTS);
return childBlockData;
} catch (ApiException e) {
throw e;
} catch (DataException e) {
@ -196,7 +255,9 @@ public class BlocksResource {
)
}
)
@ApiErrors({ApiError.REPOSITORY_ISSUE})
@ApiErrors({
ApiError.REPOSITORY_ISSUE
})
public BigDecimal getGeneratingBalance() {
try (final Repository repository = RepositoryManager.getRepository()) {
BlockData blockData = repository.getBlockRepository().getLastBlock();
@ -226,7 +287,9 @@ public class BlocksResource {
)
}
)
@ApiErrors({ApiError.INVALID_SIGNATURE, ApiError.BLOCK_NO_EXISTS, ApiError.REPOSITORY_ISSUE})
@ApiErrors({
ApiError.INVALID_SIGNATURE, ApiError.BLOCK_NO_EXISTS, ApiError.REPOSITORY_ISSUE
})
public BigDecimal getGeneratingBalance(@PathParam("signature") String signature58) {
// Decode signature
byte[] signature;
@ -269,7 +332,9 @@ public class BlocksResource {
)
}
)
@ApiErrors({ApiError.REPOSITORY_ISSUE})
@ApiErrors({
ApiError.REPOSITORY_ISSUE
})
public long getTimePerBlock() {
try (final Repository repository = RepositoryManager.getRepository()) {
BlockData blockData = repository.getBlockRepository().getLastBlock();
@ -319,7 +384,9 @@ public class BlocksResource {
)
}
)
@ApiErrors({ApiError.REPOSITORY_ISSUE})
@ApiErrors({
ApiError.REPOSITORY_ISSUE
})
public int getHeight() {
try (final Repository repository = RepositoryManager.getRepository()) {
return repository.getBlockRepository().getBlockchainHeight();
@ -347,7 +414,9 @@ public class BlocksResource {
)
}
)
@ApiErrors({ApiError.INVALID_SIGNATURE, ApiError.BLOCK_NO_EXISTS, ApiError.REPOSITORY_ISSUE})
@ApiErrors({
ApiError.INVALID_SIGNATURE, ApiError.BLOCK_NO_EXISTS, ApiError.REPOSITORY_ISSUE
})
public int getHeight(@PathParam("signature") String signature58) {
// Decode signature
byte[] signature;
@ -382,17 +451,22 @@ public class BlocksResource {
description = "the block",
content = @Content(
schema = @Schema(
implementation = BlockWithTransactions.class
implementation = BlockData.class
)
)
)
}
)
@ApiErrors({ApiError.BLOCK_NO_EXISTS, ApiError.REPOSITORY_ISSUE})
public BlockWithTransactions getByHeight(@PathParam("height") int height, @Parameter(ref = "includeTransactions") @QueryParam("includeTransactions") boolean includeTransactions) {
@ApiErrors({
ApiError.BLOCK_NO_EXISTS, ApiError.REPOSITORY_ISSUE
})
public BlockData getByHeight(@PathParam("height") int height) {
try (final Repository repository = RepositoryManager.getRepository()) {
BlockData blockData = repository.getBlockRepository().fromHeight(height);
return packageBlockData(repository, blockData, includeTransactions);
if (blockData == null)
throw ApiExceptionFactory.INSTANCE.createException(request, ApiError.BLOCK_NO_EXISTS);
return blockData;
} catch (ApiException e) {
throw e;
} catch (DataException e) {
@ -409,19 +483,23 @@ public class BlocksResource {
@ApiResponse(
description = "blocks",
content = @Content(
schema = @Schema(
implementation = BlockWithTransactions.class
array = @ArraySchema(
schema = @Schema(
implementation = BlockData.class
)
)
)
)
}
)
@ApiErrors({ApiError.BLOCK_NO_EXISTS, ApiError.REPOSITORY_ISSUE})
public List<BlockWithTransactions> getBlockRange(@PathParam("height") int height, @Parameter(ref = "count") @QueryParam("count") int count) {
boolean includeTransactions = false;
@ApiErrors({
ApiError.BLOCK_NO_EXISTS, ApiError.REPOSITORY_ISSUE
})
public List<BlockData> getBlockRange(@PathParam("height") int height, @Parameter(
ref = "count"
) @QueryParam("count") int count) {
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) {
BlockData blockData = repository.getBlockRepository().fromHeight(height);
@ -429,7 +507,7 @@ public class BlocksResource {
// Run out of blocks!
break;
blocks.add(packageBlockData(repository, blockData, includeTransactions));
blocks.add(blockData);
}
return blocks;
@ -440,29 +518,4 @@ public class BlocksResource {
}
}
/**
* Returns block, optionally including transactions.
* <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 java.util.List;
import java.util.function.Predicate;
import java.util.stream.Collectors;
import javax.servlet.http.HttpServletRequest;
@ -25,8 +26,8 @@ import javax.ws.rs.core.MediaType;
import org.qora.api.ApiError;
import org.qora.api.ApiErrors;
import org.qora.api.ApiExceptionFactory;
import org.qora.api.model.GroupWithMemberInfo;
import org.qora.api.model.GroupWithMemberInfo.MemberInfo;
import org.qora.api.model.GroupMembers;
import org.qora.api.model.GroupMembers.MemberInfo;
import org.qora.crypto.Crypto;
import org.qora.data.group.GroupAdminData;
import org.qora.data.group.GroupBanData;
@ -86,14 +87,15 @@ public class GroupsResource {
}
)
@ApiErrors({ApiError.REPOSITORY_ISSUE})
public List<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()) {
List<GroupData> groups = repository.getGroupRepository().getAllGroups();
// Pagination would take effect here (or as part of the repository access)
int fromIndex = Integer.min(offset, groups.size());
int toIndex = limit == 0 ? groups.size() : Integer.min(fromIndex + limit, groups.size());
groups = groups.subList(fromIndex, toIndex);
List<GroupData> groups = repository.getGroupRepository().getAllGroups(limit, offset, reverse);
return groups;
} catch (DataException e) {
@ -158,7 +160,7 @@ public class GroupsResource {
}
@GET
@Path("/{groupname}")
@Path("/{groupid}")
@Operation(
summary = "Info on group",
responses = {
@ -166,45 +168,72 @@ public class GroupsResource {
description = "group info",
content = @Content(
mediaType = MediaType.APPLICATION_JSON,
schema = @Schema(implementation = GroupWithMemberInfo.class)
schema = @Schema(implementation = GroupData.class)
)
)
}
)
@ApiErrors({ApiError.REPOSITORY_ISSUE})
public GroupWithMemberInfo getGroup(@PathParam("groupname") String groupName, @QueryParam("includeMembers") boolean includeMembers) {
public GroupData getGroupData(@PathParam("groupid") int groupId) {
try (final Repository repository = RepositoryManager.getRepository()) {
GroupData groupData = repository.getGroupRepository().fromGroupName(groupName);
GroupData groupData = repository.getGroupRepository().fromGroupId(groupId);
if (groupData == null)
throw ApiExceptionFactory.INSTANCE.createException(request, ApiError.GROUP_UNKNOWN);
List<MemberInfo> members = null;
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);
return groupData;
} catch (DataException e) {
throw ApiExceptionFactory.INSTANCE.createException(request, ApiError.REPOSITORY_ISSUE, e);
}
}
@GET
@Path("/members/{groupid}")
@Operation(
summary = "List group members",
responses = {
@ApiResponse(
description = "group info",
content = @Content(
mediaType = MediaType.APPLICATION_JSON,
schema = @Schema(implementation = GroupMembers.class)
)
)
}
)
@ApiErrors({ApiError.REPOSITORY_ISSUE})
public GroupMembers getGroup(@PathParam("groupid") int groupId, @QueryParam("onlyAdmins") Boolean onlyAdmins,
@Parameter(ref = "limit") @QueryParam("limit") Integer limit, @Parameter(ref = "offset") @QueryParam("offset") Integer offset,
@Parameter(ref="reverse") @QueryParam("reverse") Boolean reverse) {
try (final Repository repository = RepositoryManager.getRepository()) {
if (!repository.getGroupRepository().groupExists(groupId))
throw ApiExceptionFactory.INSTANCE.createException(request, ApiError.GROUP_UNKNOWN);
int adminCount = repository.getGroupRepository().countGroupAdmins(groupId);
int memberCount = repository.getGroupRepository().countGroupMembers(groupId);
if (onlyAdmins != null && onlyAdmins) {
// Shortcut
List<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
@Path("/create")

View File

@ -68,15 +68,12 @@ public class NamesResource {
}
)
@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()) {
List<NameData> names = repository.getNameRepository().getAllNames();
// Pagination would take effect here (or as part of the repository access)
int fromIndex = Integer.min(offset, names.size());
int toIndex = limit == 0 ? names.size() : Integer.min(fromIndex + limit, names.size());
names = names.subList(fromIndex, toIndex);
List<NameData> names = repository.getNameRepository().getAllNames(limit, offset, reverse);
// Convert to summary
return names.stream().map(nameData -> new NameSummary(nameData)).collect(Collectors.toList());
} catch (DataException e) {
throw ApiExceptionFactory.INSTANCE.createException(request, ApiError.REPOSITORY_ISSUE, e);
@ -98,12 +95,13 @@ public class NamesResource {
}
)
@ApiErrors({ApiError.INVALID_ADDRESS, ApiError.REPOSITORY_ISSUE})
public List<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))
throw ApiExceptionFactory.INSTANCE.createException(request, ApiError.INVALID_ADDRESS);
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());
} catch (DataException e) {
@ -365,16 +363,10 @@ public class NamesResource {
}
)
@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()) {
List<NameData> names = repository.getNameRepository().getNamesForSale();
// Pagination would take effect here (or as part of the repository access)
int fromIndex = Integer.min(offset, names.size());
int toIndex = limit == 0 ? names.size() : Integer.min(fromIndex + limit, names.size());
names = names.subList(fromIndex, toIndex);
return names;
return repository.getNameRepository().getNamesForSale(limit, offset, reverse);
} catch (DataException e) {
throw ApiExceptionFactory.INSTANCE.createException(request, ApiError.REPOSITORY_ISSUE, e);
}

View File

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

View File

@ -50,7 +50,7 @@ public class BlockGenerator extends Thread {
try (final Repository repository = RepositoryManager.getRepository()) {
if (Settings.getInstance().getWipeUnconfirmedOnStart()) {
// Wipe existing unconfirmed transactions
List<TransactionData> unconfirmedTransactions = repository.getTransactionRepository().getAllUnconfirmedTransactions();
List<TransactionData> unconfirmedTransactions = repository.getTransactionRepository().getUnconfirmedTransactions();
for (TransactionData transactionData : unconfirmedTransactions) {
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 {
static {
// This must go before any calls to LogManager/Logger
System.setProperty("java.util.logging.manager", "org.apache.logging.log4j.jul.LogManager");
}
private static final Logger LOGGER = LogManager.getLogger(Controller.class);
public static final String connectionUrl = "jdbc:hsqldb:file:db/blockchain;create=true";

View File

@ -10,8 +10,9 @@ import javax.xml.bind.annotation.XmlElement;
import javax.xml.bind.annotation.XmlSeeAlso;
import javax.xml.bind.annotation.XmlTransient;
import org.eclipse.persistence.oxm.annotations.XmlClassExtractor;
import org.qora.api.TransactionClassExtractor;
// XXX are this still needed? see below
// import org.eclipse.persistence.oxm.annotations.XmlClassExtractor;
// import org.qora.api.TransactionClassExtractor;
import org.qora.crypto.Crypto;
import org.qora.transaction.Transaction.TransactionType;
@ -26,7 +27,8 @@ import io.swagger.v3.oas.annotations.media.Schema.AccessMode;
* then chances are that class is missing a no-argument constructor!
*/
@XmlClassExtractor(TransactionClassExtractor.class)
// XXX is this still in use?
// @XmlClassExtractor(TransactionClassExtractor.class)
@XmlSeeAlso({GenesisTransactionData.class, PaymentTransactionData.class, RegisterNameTransactionData.class, UpdateNameTransactionData.class,
SellNameTransactionData.class, CancelSellNameTransactionData.class, BuyNameTransactionData.class,
CreatePollTransactionData.class, VoteOnPollTransactionData.class, ArbitraryTransactionData.class,

View File

@ -21,9 +21,17 @@ public interface AccountRepository {
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;

View File

@ -18,7 +18,11 @@ public interface AssetRepository {
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
@ -30,9 +34,18 @@ public interface AssetRepository {
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;
@ -40,10 +53,18 @@ public interface AssetRepository {
// 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 */
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;

View File

@ -59,6 +59,17 @@ public interface BlockRepository {
*/
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.
* <p>
@ -68,7 +79,9 @@ public interface BlockRepository {
* @return list of transactions, or null if block not found in blockchain.
* @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.

View File

@ -21,11 +21,23 @@ public interface GroupRepository {
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;
@ -39,7 +51,14 @@ public interface GroupRepository {
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;
@ -51,7 +70,11 @@ public interface GroupRepository {
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 */
public Integer countGroupMembers(int groupId) throws DataException;
@ -66,9 +89,17 @@ public interface GroupRepository {
public boolean inviteExists(int groupId, String invitee) throws DataException;
public List<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;
@ -80,7 +111,11 @@ public interface GroupRepository {
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;
@ -92,7 +127,11 @@ public interface GroupRepository {
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;

View File

@ -10,11 +10,23 @@ public interface NameRepository {
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;

View File

@ -2,6 +2,7 @@ package org.qora.repository;
import java.util.List;
import org.qora.api.resource.TransactionsResource.ConfirmationStatus;
import org.qora.data.transaction.TransactionData;
import org.qora.transaction.Transaction.TransactionType;
@ -20,7 +21,7 @@ public interface TransactionRepository {
// Transaction participants
public List<byte[]> getAllSignaturesInvolvingAddress(String address) throws DataException;
public List<byte[]> getSignaturesInvolvingAddress(String address) throws DataException;
public void saveParticipants(TransactionData transactionData, List<String> participants) throws DataException;
@ -28,7 +29,21 @@ public interface TransactionRepository {
// 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.
@ -36,7 +51,9 @@ public interface TransactionRepository {
* @return list of transactions, or empty if none.
* @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.

View File

@ -94,10 +94,15 @@ public class HSQLDBAccountRepository implements AccountRepository {
}
@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>();
try (ResultSet resultSet = this.repository.checkedExecute("SELECT asset_id, balance FROM AccountBalances WHERE account = ?", address)) {
try (ResultSet resultSet = this.repository.checkedExecute(sql, address)) {
if (resultSet == null)
return balances;
@ -115,10 +120,15 @@ public class HSQLDBAccountRepository implements AccountRepository {
}
@Override
public List<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>();
try (ResultSet resultSet = this.repository.checkedExecute("SELECT account, balance FROM AccountBalances WHERE asset_id = ?", assetId)) {
try (ResultSet resultSet = this.repository.checkedExecute(sql, assetId)) {
if (resultSet == null)
return balances;

View File

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

View File

@ -108,10 +108,15 @@ public class HSQLDBBlockRepository implements BlockRepository {
}
@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>();
try (ResultSet resultSet = this.repository.checkedExecute("SELECT transaction_signature FROM BlockTransactions WHERE block_signature = ?", signature)) {
try (ResultSet resultSet = this.repository.checkedExecute(sql, signature)) {
if (resultSet == null)
return transactions; // No transactions in this block

View File

@ -95,11 +95,15 @@ public class HSQLDBGroupRepository implements GroupRepository {
}
@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<>();
try (ResultSet resultSet = this.repository
.checkedExecute("SELECT group_id, owner, group_name, description, created, updated, reference, is_open FROM Groups")) {
try (ResultSet resultSet = this.repository.checkedExecute(sql)) {
if (resultSet == null)
return groups;
@ -127,11 +131,15 @@ public class HSQLDBGroupRepository implements GroupRepository {
}
@Override
public List<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<>();
try (ResultSet resultSet = this.repository
.checkedExecute("SELECT group_id, group_name, description, created, updated, reference, is_open FROM Groups WHERE owner = ?", owner)) {
try (ResultSet resultSet = this.repository.checkedExecute(sql, owner)) {
if (resultSet == null)
return groups;
@ -158,12 +166,15 @@ public class HSQLDBGroupRepository implements GroupRepository {
}
@Override
public List<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<>();
try (ResultSet resultSet = this.repository.checkedExecute(
"SELECT group_id, owner, group_name, description, created, updated, reference, is_open FROM Groups JOIN GroupMembers USING (group_id) WHERE address = ?",
member)) {
try (ResultSet resultSet = this.repository.checkedExecute(sql, member)) {
if (resultSet == null)
return groups;
@ -266,10 +277,15 @@ public class HSQLDBGroupRepository implements GroupRepository {
}
@Override
public List<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<>();
try (ResultSet resultSet = this.repository.checkedExecute("SELECT admin, reference FROM GroupAdmins WHERE group_id = ?", groupId)) {
try (ResultSet resultSet = this.repository.checkedExecute(sql, groupId)) {
if (resultSet == null)
return admins;
@ -286,6 +302,21 @@ public class HSQLDBGroupRepository implements GroupRepository {
}
}
@Override
public Integer countGroupAdmins(int groupId) throws DataException {
try (ResultSet resultSet = this.repository.checkedExecute("SELECT COUNT(*) FROM GroupAdmins WHERE group_id = ?", groupId)) {
int count = resultSet.getInt(1);
if (count == 0)
// There must be at least one admin: the group owner
return null;
return count;
} catch (SQLException e) {
throw new DataException("Unable to fetch group admin count from repository", e);
}
}
@Override
public void save(GroupAdminData groupAdminData) throws DataException {
HSQLDBSaver saveHelper = new HSQLDBSaver("GroupAdmins");
@ -336,10 +367,15 @@ public class HSQLDBGroupRepository implements GroupRepository {
}
@Override
public List<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<>();
try (ResultSet resultSet = this.repository.checkedExecute("SELECT address, joined, reference FROM GroupMembers WHERE group_id = ?", groupId)) {
try (ResultSet resultSet = this.repository.checkedExecute(sql, groupId)) {
if (resultSet == null)
return members;
@ -359,13 +395,14 @@ public class HSQLDBGroupRepository implements GroupRepository {
@Override
public Integer countGroupMembers(int groupId) throws DataException {
// "GROUP BY" clause required to avoid error "expression not in aggregate or GROUP BY columns: PUBLIC.GROUPS.GROUP_ID"
try (ResultSet resultSet = this.repository.checkedExecute("SELECT group_id, COUNT(*) FROM GroupMembers WHERE group_id = ? GROUP BY group_id",
groupId)) {
if (resultSet == null)
try (ResultSet resultSet = this.repository.checkedExecute("SELECT COUNT(*) FROM GroupMembers WHERE group_id = ?", groupId)) {
int count = resultSet.getInt(1);
if (count == 0)
// There must be at least one member: the group owner
return null;
return resultSet.getInt(2);
return count;
} catch (SQLException e) {
throw new DataException("Unable to fetch group member count from repository", e);
}
@ -425,10 +462,15 @@ public class HSQLDBGroupRepository implements GroupRepository {
}
@Override
public List<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<>();
try (ResultSet resultSet = this.repository.checkedExecute("SELECT inviter, invitee, expiry, reference FROM GroupInvites WHERE group_id = ?", groupId)) {
try (ResultSet resultSet = this.repository.checkedExecute(sql, groupId)) {
if (resultSet == null)
return invites;
@ -451,10 +493,15 @@ public class HSQLDBGroupRepository implements GroupRepository {
}
@Override
public List<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<>();
try (ResultSet resultSet = this.repository.checkedExecute("SELECT group_id, inviter, expiry, reference FROM GroupInvites WHERE invitee = ?", invitee)) {
try (ResultSet resultSet = this.repository.checkedExecute(sql, invitee)) {
if (resultSet == null)
return invites;
@ -530,10 +577,15 @@ public class HSQLDBGroupRepository implements GroupRepository {
}
@Override
public List<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<>();
try (ResultSet resultSet = this.repository.checkedExecute("SELECT joiner, reference FROM GroupJoinRequests WHERE group_id = ?", groupId)) {
try (ResultSet resultSet = this.repository.checkedExecute(sql, groupId)) {
if (resultSet == null)
return joinRequests;
@ -604,11 +656,15 @@ public class HSQLDBGroupRepository implements GroupRepository {
}
@Override
public List<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<>();
try (ResultSet resultSet = this.repository.checkedExecute("SELECT offender, admin, banned, reason, expiry, reference FROM GroupBans WHERE group_id = ?",
groupId)) {
try (ResultSet resultSet = this.repository.checkedExecute(sql, groupId)) {
if (resultSet == null)
return bans;

View File

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

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

View File

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