From 048c54fc0ad0e0611984206f9a644d2e41a7cb41 Mon Sep 17 00:00:00 2001 From: catbref Date: Mon, 18 Mar 2019 11:34:46 +0000 Subject: [PATCH] Add API call to support TRANSFER_ASSET + activity summary API call --- .../api/TransactionCountMapXmlAdapter.java | 54 +++++++++++++++++++ .../org/qora/api/model/ActivitySummary.java | 28 ++++++++++ .../org/qora/api/resource/AdminResource.java | 53 ++++++++++++++++++ .../org/qora/api/resource/AssetsResource.java | 50 +++++++++++++++++ .../org/qora/repository/AssetRepository.java | 2 + .../org/qora/repository/NameRepository.java | 2 + .../repository/TransactionRepository.java | 13 +++++ .../hsqldb/HSQLDBAssetRepository.java | 24 +++++++++ .../hsqldb/HSQLDBNameRepository.java | 24 +++++++++ .../HSQLDBTransactionRepository.java | 35 ++++++++++++ 10 files changed, 285 insertions(+) create mode 100644 src/main/java/org/qora/api/TransactionCountMapXmlAdapter.java create mode 100644 src/main/java/org/qora/api/model/ActivitySummary.java diff --git a/src/main/java/org/qora/api/TransactionCountMapXmlAdapter.java b/src/main/java/org/qora/api/TransactionCountMapXmlAdapter.java new file mode 100644 index 00000000..506a5e78 --- /dev/null +++ b/src/main/java/org/qora/api/TransactionCountMapXmlAdapter.java @@ -0,0 +1,54 @@ +package org.qora.api; + +import java.util.ArrayList; +import java.util.HashMap; +import java.util.List; +import java.util.Map; +import java.util.Map.Entry; + +import javax.xml.bind.annotation.XmlTransient; +import javax.xml.bind.annotation.XmlValue; +import javax.xml.bind.annotation.adapters.XmlAdapter; + +import org.eclipse.persistence.oxm.annotations.XmlVariableNode; +import org.qora.transaction.Transaction.TransactionType; + +public class TransactionCountMapXmlAdapter extends XmlAdapter> { + + public static class StringIntegerMap { + @XmlVariableNode("key") + List entries = new ArrayList(); + } + + public static class MapEntry { + @XmlTransient + public String key; + + @XmlValue + public Integer value; + } + + @Override + public Map unmarshal(StringIntegerMap stringIntegerMap) throws Exception { + Map map = new HashMap<>(stringIntegerMap.entries.size()); + + for (MapEntry entry : stringIntegerMap.entries) + map.put(TransactionType.valueOf(entry.key), entry.value); + + return map; + } + + @Override + public StringIntegerMap marshal(Map map) throws Exception { + StringIntegerMap output = new StringIntegerMap(); + + for (Entry entry : map.entrySet()) { + MapEntry mapEntry = new MapEntry(); + mapEntry.key = entry.getKey().name(); + mapEntry.value = entry.getValue(); + output.entries.add(mapEntry); + } + + return output; + } +} diff --git a/src/main/java/org/qora/api/model/ActivitySummary.java b/src/main/java/org/qora/api/model/ActivitySummary.java new file mode 100644 index 00000000..62a91e22 --- /dev/null +++ b/src/main/java/org/qora/api/model/ActivitySummary.java @@ -0,0 +1,28 @@ +package org.qora.api.model; + +import java.util.HashMap; +import java.util.Map; + +import javax.xml.bind.annotation.XmlAccessType; +import javax.xml.bind.annotation.XmlAccessorType; +import javax.xml.bind.annotation.adapters.XmlJavaTypeAdapter; + +import org.qora.api.TransactionCountMapXmlAdapter; +import org.qora.transaction.Transaction.TransactionType; + +@XmlAccessorType(XmlAccessType.FIELD) +public class ActivitySummary { + + public int blockCount; + public int transactionCount; + public int assetsIssued; + public int namesRegistered; + + // Assuming TransactionType values are contiguous so 'length' equals count + @XmlJavaTypeAdapter(TransactionCountMapXmlAdapter.class) + public Map transactionCountByType = new HashMap<>(); + + public ActivitySummary() { + } + +} diff --git a/src/main/java/org/qora/api/resource/AdminResource.java b/src/main/java/org/qora/api/resource/AdminResource.java index 160324c1..17d49ad2 100644 --- a/src/main/java/org/qora/api/resource/AdminResource.java +++ b/src/main/java/org/qora/api/resource/AdminResource.java @@ -8,6 +8,11 @@ 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.time.LocalDate; +import java.time.LocalTime; +import java.time.OffsetDateTime; +import java.time.ZoneOffset; + import javax.servlet.http.HttpServletRequest; import javax.ws.rs.GET; import javax.ws.rs.Path; @@ -15,8 +20,15 @@ import javax.ws.rs.Produces; import javax.ws.rs.core.Context; 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.Security; +import org.qora.api.model.ActivitySummary; import org.qora.controller.Controller; +import org.qora.repository.DataException; +import org.qora.repository.Repository; +import org.qora.repository.RepositoryManager; @Path("/admin") @Produces({MediaType.APPLICATION_JSON, MediaType.TEXT_PLAIN}) @@ -87,4 +99,45 @@ public class AdminResource { return "true"; } + @GET + @Path("/summary") + @Operation( + summary = "Summary of activity since midnight, UTC", + responses = { + @ApiResponse( + content = @Content(schema = @Schema(implementation = ActivitySummary.class)) + ) + } + ) + @ApiErrors({ApiError.REPOSITORY_ISSUE}) + public ActivitySummary summary() { + ActivitySummary summary = new ActivitySummary(); + + LocalDate date = LocalDate.now(); + LocalTime time = LocalTime.of(0, 0); + ZoneOffset offset = ZoneOffset.UTC; + long start = OffsetDateTime.of(date, time, offset).toInstant().toEpochMilli(); + + try (final Repository repository = RepositoryManager.getRepository()) { + int startHeight = repository.getBlockRepository().getHeightFromTimestamp(start); + int endHeight = repository.getBlockRepository().getBlockchainHeight(); + + summary.blockCount = endHeight - startHeight; + + summary.transactionCountByType = repository.getTransactionRepository().getTransactionSummary(startHeight + 1, endHeight); + + for (Integer count : summary.transactionCountByType.values()) + summary.transactionCount += count; + + summary.assetsIssued = repository.getAssetRepository().getRecentAssetIds(start).size(); + + summary.namesRegistered = repository.getNameRepository().getRecentNames(start).size(); + + return summary; + } catch (DataException e) { + throw ApiExceptionFactory.INSTANCE.createException(request, ApiError.REPOSITORY_ISSUE, e); + } + + } + } diff --git a/src/main/java/org/qora/api/resource/AssetsResource.java b/src/main/java/org/qora/api/resource/AssetsResource.java index 20a78f2d..d1eaa280 100644 --- a/src/main/java/org/qora/api/resource/AssetsResource.java +++ b/src/main/java/org/qora/api/resource/AssetsResource.java @@ -43,6 +43,7 @@ import org.qora.data.transaction.CancelAssetOrderTransactionData; import org.qora.data.transaction.CreateAssetOrderTransactionData; import org.qora.data.transaction.IssueAssetTransactionData; import org.qora.data.transaction.TransactionData; +import org.qora.data.transaction.TransferAssetTransactionData; import org.qora.data.transaction.UpdateAssetTransactionData; import org.qora.repository.DataException; import org.qora.repository.Repository; @@ -54,6 +55,7 @@ import org.qora.transform.TransformationException; import org.qora.transform.transaction.CancelAssetOrderTransactionTransformer; import org.qora.transform.transaction.CreateAssetOrderTransactionTransformer; import org.qora.transform.transaction.IssueAssetTransactionTransformer; +import org.qora.transform.transaction.TransferAssetTransactionTransformer; import org.qora.transform.transaction.UpdateAssetTransactionTransformer; import org.qora.utils.Base58; @@ -813,4 +815,52 @@ public class AssetsResource { } } + @POST + @Path("/transfer") + @Operation( + summary = "Transfer quantity of asset", + requestBody = @RequestBody( + required = true, + content = @Content( + mediaType = MediaType.APPLICATION_JSON, + schema = @Schema( + implementation = TransferAssetTransactionData.class + ) + ) + ), + responses = { + @ApiResponse( + description = "raw, unsigned, TRANSFER_ASSET transaction encoded in Base58", + content = @Content( + mediaType = MediaType.TEXT_PLAIN, + schema = @Schema( + type = "string" + ) + ) + ) + } + ) + @ApiErrors({ + ApiError.NON_PRODUCTION, ApiError.TRANSFORMATION_ERROR, ApiError.REPOSITORY_ISSUE, ApiError.TRANSACTION_INVALID + }) + public String transferAsset(TransferAssetTransactionData transactionData) { + if (Settings.getInstance().isApiRestricted()) + throw ApiExceptionFactory.INSTANCE.createException(request, ApiError.NON_PRODUCTION); + + try (final Repository repository = RepositoryManager.getRepository()) { + Transaction transaction = Transaction.fromData(repository, transactionData); + + ValidationResult result = transaction.isValidUnconfirmed(); + if (result != ValidationResult.OK) + throw TransactionsResource.createTransactionInvalidException(request, result); + + byte[] bytes = TransferAssetTransactionTransformer.toBytes(transactionData); + return Base58.encode(bytes); + } catch (TransformationException e) { + throw ApiExceptionFactory.INSTANCE.createException(request, ApiError.TRANSFORMATION_ERROR, e); + } catch (DataException e) { + throw ApiExceptionFactory.INSTANCE.createException(request, ApiError.REPOSITORY_ISSUE, e); + } + } + } diff --git a/src/main/java/org/qora/repository/AssetRepository.java b/src/main/java/org/qora/repository/AssetRepository.java index 3dbc9a23..60328d30 100644 --- a/src/main/java/org/qora/repository/AssetRepository.java +++ b/src/main/java/org/qora/repository/AssetRepository.java @@ -25,6 +25,8 @@ public interface AssetRepository { return getAllAssets(null, null, null); } + public List getRecentAssetIds(long start) throws DataException; + // For a list of asset holders, see AccountRepository.getAssetBalances public void save(AssetData assetData) throws DataException; diff --git a/src/main/java/org/qora/repository/NameRepository.java b/src/main/java/org/qora/repository/NameRepository.java index 3b22f7f6..b96b3054 100644 --- a/src/main/java/org/qora/repository/NameRepository.java +++ b/src/main/java/org/qora/repository/NameRepository.java @@ -28,6 +28,8 @@ public interface NameRepository { return getNamesByOwner(address, null, null, null); } + public List getRecentNames(long start) throws DataException; + public void save(NameData nameData) throws DataException; public void delete(String name) throws DataException; diff --git a/src/main/java/org/qora/repository/TransactionRepository.java b/src/main/java/org/qora/repository/TransactionRepository.java index 7a5794ee..5a63027c 100644 --- a/src/main/java/org/qora/repository/TransactionRepository.java +++ b/src/main/java/org/qora/repository/TransactionRepository.java @@ -1,6 +1,7 @@ package org.qora.repository; import java.util.List; +import java.util.Map; import org.qora.api.resource.TransactionsResource.ConfirmationStatus; import org.qora.data.transaction.GroupApprovalTransactionData; @@ -32,6 +33,18 @@ public interface TransactionRepository { // Searching transactions + /** + * Returns number of each transaction type in blocks from startHeight to endHeight inclusive. + *

+ * Note: endHeight >= startHeight + * + * @param startHeight height of first block to check + * @param endHeight height of last block to check + * @return transaction counts, indexed by transaction type value + * @throws DataException + */ + public Map getTransactionSummary(int startHeight, int endHeight) throws DataException; + public List getSignaturesMatchingCriteria(Integer startBlock, Integer blockLimit, TransactionType txType, String address, ConfirmationStatus confirmationStatus, Integer limit, Integer offset, Boolean reverse) throws DataException; diff --git a/src/main/java/org/qora/repository/hsqldb/HSQLDBAssetRepository.java b/src/main/java/org/qora/repository/hsqldb/HSQLDBAssetRepository.java index 939f6e11..4d5fd243 100644 --- a/src/main/java/org/qora/repository/hsqldb/HSQLDBAssetRepository.java +++ b/src/main/java/org/qora/repository/hsqldb/HSQLDBAssetRepository.java @@ -126,6 +126,30 @@ public class HSQLDBAssetRepository implements AssetRepository { } } + @Override + public List getRecentAssetIds(long start) throws DataException { + String sql = "SELECT asset_id FROM IssueAssetTransactions JOIN Assets USING (asset_id) " + + "JOIN Transactions USING (signature) " + + "WHERE creation >= ?"; + + List assetIds = new ArrayList<>(); + + try (ResultSet resultSet = this.repository.checkedExecute(sql, HSQLDBRepository.toOffsetDateTime(start))) { + if (resultSet == null) + return assetIds; + + do { + long assetId = resultSet.getLong(1); + + assetIds.add(assetId); + } while (resultSet.next()); + + return assetIds; + } catch (SQLException e) { + throw new DataException("Unable to fetch recent asset IDs from repository", e); + } + } + @Override public void save(AssetData assetData) throws DataException { HSQLDBSaver saveHelper = new HSQLDBSaver("Assets"); diff --git a/src/main/java/org/qora/repository/hsqldb/HSQLDBNameRepository.java b/src/main/java/org/qora/repository/hsqldb/HSQLDBNameRepository.java index ce127e3a..5b48e035 100644 --- a/src/main/java/org/qora/repository/hsqldb/HSQLDBNameRepository.java +++ b/src/main/java/org/qora/repository/hsqldb/HSQLDBNameRepository.java @@ -165,6 +165,30 @@ public class HSQLDBNameRepository implements NameRepository { } } + @Override + public List getRecentNames(long start) throws DataException { + String sql = "SELECT name FROM RegisterNameTransactions JOIN Names USING (name) " + + "JOIN Transactions USING (signature) " + + "WHERE creation >= ?"; + + List names = new ArrayList<>(); + + try (ResultSet resultSet = this.repository.checkedExecute(sql, HSQLDBRepository.toOffsetDateTime(start))) { + if (resultSet == null) + return names; + + do { + String name = resultSet.getString(1); + + names.add(name); + } while (resultSet.next()); + + return names; + } catch (SQLException e) { + throw new DataException("Unable to fetch recent names from repository", e); + } + } + @Override public void save(NameData nameData) throws DataException { HSQLDBSaver saveHelper = new HSQLDBSaver("Names"); diff --git a/src/main/java/org/qora/repository/hsqldb/transaction/HSQLDBTransactionRepository.java b/src/main/java/org/qora/repository/hsqldb/transaction/HSQLDBTransactionRepository.java index a13c6e3a..993b20a0 100644 --- a/src/main/java/org/qora/repository/hsqldb/transaction/HSQLDBTransactionRepository.java +++ b/src/main/java/org/qora/repository/hsqldb/transaction/HSQLDBTransactionRepository.java @@ -11,7 +11,9 @@ import java.util.ArrayList; import java.util.Arrays; import static java.util.Arrays.stream; import java.util.Calendar; +import java.util.HashMap; import java.util.List; +import java.util.Map; import java.util.stream.Collectors; import org.apache.logging.log4j.LogManager; @@ -112,6 +114,8 @@ public class HSQLDBTransactionRepository implements TransactionRepository { protected HSQLDBTransactionRepository() { } + // Fetching transactions / transaction height + @Override public TransactionData fromSignature(byte[] signature) throws DataException { try (ResultSet resultSet = this.repository.checkedExecute("SELECT type, reference, creator, creation, fee, tx_group_id FROM Transactions WHERE signature = ?", @@ -272,6 +276,8 @@ public class HSQLDBTransactionRepository implements TransactionRepository { } } + // Transaction participants + @Override public List getSignaturesInvolvingAddress(String address) throws DataException { List signatures = new ArrayList(); @@ -318,6 +324,35 @@ public class HSQLDBTransactionRepository implements TransactionRepository { } } + // Searching transactions + + @Override + public Map getTransactionSummary(int startHeight, int endHeight) throws DataException { + String sql = "SELECT type, COUNT(signature) FROM Transactions " + + "JOIN BlockTransactions ON transaction_signature = signature " + + "JOIN Blocks ON Blocks.signature = block_signature " + + "WHERE height BETWEEN ? AND ? " + + "GROUP BY type"; + + Map transactionCounts = new HashMap<>(); + + try (ResultSet resultSet = this.repository.checkedExecute(sql, startHeight, endHeight)) { + if (resultSet == null) + return transactionCounts; + + do { + int type = resultSet.getInt(1); + int count = resultSet.getInt(2); + + transactionCounts.put(TransactionType.valueOf(type), count); + } while (resultSet.next()); + + return transactionCounts; + } catch (SQLException e) { + throw new DataException("Unable to fetch transaction counts from repository", e); + } + } + @Override public List getSignaturesMatchingCriteria(Integer startBlock, Integer blockLimit, TransactionType txType, String address, ConfirmationStatus confirmationStatus, Integer limit, Integer offset, Boolean reverse) throws DataException {