forked from Qortal/qortal
Add API call to support TRANSFER_ASSET + activity summary API call
This commit is contained in:
parent
91ee505ba9
commit
048c54fc0a
@ -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<TransactionCountMapXmlAdapter.StringIntegerMap, Map<TransactionType, Integer>> {
|
||||
|
||||
public static class StringIntegerMap {
|
||||
@XmlVariableNode("key")
|
||||
List<MapEntry> entries = new ArrayList<MapEntry>();
|
||||
}
|
||||
|
||||
public static class MapEntry {
|
||||
@XmlTransient
|
||||
public String key;
|
||||
|
||||
@XmlValue
|
||||
public Integer value;
|
||||
}
|
||||
|
||||
@Override
|
||||
public Map<TransactionType, Integer> unmarshal(StringIntegerMap stringIntegerMap) throws Exception {
|
||||
Map<TransactionType, Integer> 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<TransactionType, Integer> map) throws Exception {
|
||||
StringIntegerMap output = new StringIntegerMap();
|
||||
|
||||
for (Entry<TransactionType, Integer> entry : map.entrySet()) {
|
||||
MapEntry mapEntry = new MapEntry();
|
||||
mapEntry.key = entry.getKey().name();
|
||||
mapEntry.value = entry.getValue();
|
||||
output.entries.add(mapEntry);
|
||||
}
|
||||
|
||||
return output;
|
||||
}
|
||||
}
|
28
src/main/java/org/qora/api/model/ActivitySummary.java
Normal file
28
src/main/java/org/qora/api/model/ActivitySummary.java
Normal file
@ -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<TransactionType, Integer> transactionCountByType = new HashMap<>();
|
||||
|
||||
public ActivitySummary() {
|
||||
}
|
||||
|
||||
}
|
@ -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);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
|
@ -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);
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
@ -25,6 +25,8 @@ public interface AssetRepository {
|
||||
return getAllAssets(null, null, null);
|
||||
}
|
||||
|
||||
public List<Long> getRecentAssetIds(long start) throws DataException;
|
||||
|
||||
// For a list of asset holders, see AccountRepository.getAssetBalances
|
||||
|
||||
public void save(AssetData assetData) throws DataException;
|
||||
|
@ -28,6 +28,8 @@ public interface NameRepository {
|
||||
return getNamesByOwner(address, null, null, null);
|
||||
}
|
||||
|
||||
public List<String> getRecentNames(long start) throws DataException;
|
||||
|
||||
public void save(NameData nameData) throws DataException;
|
||||
|
||||
public void delete(String name) throws DataException;
|
||||
|
@ -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.
|
||||
* <p>
|
||||
* 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<TransactionType, Integer> getTransactionSummary(int startHeight, int endHeight) throws DataException;
|
||||
|
||||
public List<byte[]> getSignaturesMatchingCriteria(Integer startBlock, Integer blockLimit, TransactionType txType, String address,
|
||||
ConfirmationStatus confirmationStatus, Integer limit, Integer offset, Boolean reverse) throws DataException;
|
||||
|
||||
|
@ -126,6 +126,30 @@ public class HSQLDBAssetRepository implements AssetRepository {
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public List<Long> getRecentAssetIds(long start) throws DataException {
|
||||
String sql = "SELECT asset_id FROM IssueAssetTransactions JOIN Assets USING (asset_id) "
|
||||
+ "JOIN Transactions USING (signature) "
|
||||
+ "WHERE creation >= ?";
|
||||
|
||||
List<Long> 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");
|
||||
|
@ -165,6 +165,30 @@ public class HSQLDBNameRepository implements NameRepository {
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public List<String> getRecentNames(long start) throws DataException {
|
||||
String sql = "SELECT name FROM RegisterNameTransactions JOIN Names USING (name) "
|
||||
+ "JOIN Transactions USING (signature) "
|
||||
+ "WHERE creation >= ?";
|
||||
|
||||
List<String> 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");
|
||||
|
@ -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<byte[]> getSignaturesInvolvingAddress(String address) throws DataException {
|
||||
List<byte[]> signatures = new ArrayList<byte[]>();
|
||||
@ -318,6 +324,35 @@ public class HSQLDBTransactionRepository implements TransactionRepository {
|
||||
}
|
||||
}
|
||||
|
||||
// Searching transactions
|
||||
|
||||
@Override
|
||||
public Map<TransactionType, Integer> 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<TransactionType, Integer> 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<byte[]> getSignaturesMatchingCriteria(Integer startBlock, Integer blockLimit, TransactionType txType, String address,
|
||||
ConfirmationStatus confirmationStatus, Integer limit, Integer offset, Boolean reverse) throws DataException {
|
||||
|
Loading…
x
Reference in New Issue
Block a user