Add API call to support TRANSFER_ASSET + activity summary API call

This commit is contained in:
catbref 2019-03-18 11:34:46 +00:00
parent 91ee505ba9
commit 048c54fc0a
10 changed files with 285 additions and 0 deletions

View File

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

View 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() {
}
}

View File

@ -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.responses.ApiResponse;
import io.swagger.v3.oas.annotations.tags.Tag; 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.servlet.http.HttpServletRequest;
import javax.ws.rs.GET; import javax.ws.rs.GET;
import javax.ws.rs.Path; 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.Context;
import javax.ws.rs.core.MediaType; 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.Security;
import org.qora.api.model.ActivitySummary;
import org.qora.controller.Controller; import org.qora.controller.Controller;
import org.qora.repository.DataException;
import org.qora.repository.Repository;
import org.qora.repository.RepositoryManager;
@Path("/admin") @Path("/admin")
@Produces({MediaType.APPLICATION_JSON, MediaType.TEXT_PLAIN}) @Produces({MediaType.APPLICATION_JSON, MediaType.TEXT_PLAIN})
@ -87,4 +99,45 @@ public class AdminResource {
return "true"; 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);
}
}
} }

View File

@ -43,6 +43,7 @@ import org.qora.data.transaction.CancelAssetOrderTransactionData;
import org.qora.data.transaction.CreateAssetOrderTransactionData; import org.qora.data.transaction.CreateAssetOrderTransactionData;
import org.qora.data.transaction.IssueAssetTransactionData; import org.qora.data.transaction.IssueAssetTransactionData;
import org.qora.data.transaction.TransactionData; import org.qora.data.transaction.TransactionData;
import org.qora.data.transaction.TransferAssetTransactionData;
import org.qora.data.transaction.UpdateAssetTransactionData; import org.qora.data.transaction.UpdateAssetTransactionData;
import org.qora.repository.DataException; import org.qora.repository.DataException;
import org.qora.repository.Repository; 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.CancelAssetOrderTransactionTransformer;
import org.qora.transform.transaction.CreateAssetOrderTransactionTransformer; import org.qora.transform.transaction.CreateAssetOrderTransactionTransformer;
import org.qora.transform.transaction.IssueAssetTransactionTransformer; import org.qora.transform.transaction.IssueAssetTransactionTransformer;
import org.qora.transform.transaction.TransferAssetTransactionTransformer;
import org.qora.transform.transaction.UpdateAssetTransactionTransformer; import org.qora.transform.transaction.UpdateAssetTransactionTransformer;
import org.qora.utils.Base58; 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);
}
}
} }

View File

@ -25,6 +25,8 @@ public interface AssetRepository {
return getAllAssets(null, null, null); return getAllAssets(null, null, null);
} }
public List<Long> getRecentAssetIds(long start) throws DataException;
// For a list of asset holders, see AccountRepository.getAssetBalances // For a list of asset holders, see AccountRepository.getAssetBalances
public void save(AssetData assetData) throws DataException; public void save(AssetData assetData) throws DataException;

View File

@ -28,6 +28,8 @@ public interface NameRepository {
return getNamesByOwner(address, null, null, null); return getNamesByOwner(address, null, null, null);
} }
public List<String> getRecentNames(long start) throws DataException;
public void save(NameData nameData) throws DataException; public void save(NameData nameData) throws DataException;
public void delete(String name) throws DataException; public void delete(String name) throws DataException;

View File

@ -1,6 +1,7 @@
package org.qora.repository; package org.qora.repository;
import java.util.List; import java.util.List;
import java.util.Map;
import org.qora.api.resource.TransactionsResource.ConfirmationStatus; import org.qora.api.resource.TransactionsResource.ConfirmationStatus;
import org.qora.data.transaction.GroupApprovalTransactionData; import org.qora.data.transaction.GroupApprovalTransactionData;
@ -32,6 +33,18 @@ public interface TransactionRepository {
// Searching transactions // 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, public List<byte[]> getSignaturesMatchingCriteria(Integer startBlock, Integer blockLimit, TransactionType txType, String address,
ConfirmationStatus confirmationStatus, Integer limit, Integer offset, Boolean reverse) throws DataException; ConfirmationStatus confirmationStatus, Integer limit, Integer offset, Boolean reverse) throws DataException;

View File

@ -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 @Override
public void save(AssetData assetData) throws DataException { public void save(AssetData assetData) throws DataException {
HSQLDBSaver saveHelper = new HSQLDBSaver("Assets"); HSQLDBSaver saveHelper = new HSQLDBSaver("Assets");

View File

@ -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 @Override
public void save(NameData nameData) throws DataException { public void save(NameData nameData) throws DataException {
HSQLDBSaver saveHelper = new HSQLDBSaver("Names"); HSQLDBSaver saveHelper = new HSQLDBSaver("Names");

View File

@ -11,7 +11,9 @@ import java.util.ArrayList;
import java.util.Arrays; import java.util.Arrays;
import static java.util.Arrays.stream; import static java.util.Arrays.stream;
import java.util.Calendar; import java.util.Calendar;
import java.util.HashMap;
import java.util.List; import java.util.List;
import java.util.Map;
import java.util.stream.Collectors; import java.util.stream.Collectors;
import org.apache.logging.log4j.LogManager; import org.apache.logging.log4j.LogManager;
@ -112,6 +114,8 @@ public class HSQLDBTransactionRepository implements TransactionRepository {
protected HSQLDBTransactionRepository() { protected HSQLDBTransactionRepository() {
} }
// Fetching transactions / transaction height
@Override @Override
public TransactionData fromSignature(byte[] signature) throws DataException { 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 = ?", 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 @Override
public List<byte[]> getSignaturesInvolvingAddress(String address) throws DataException { public List<byte[]> getSignaturesInvolvingAddress(String address) throws DataException {
List<byte[]> signatures = new ArrayList<byte[]>(); 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 @Override
public List<byte[]> getSignaturesMatchingCriteria(Integer startBlock, Integer blockLimit, TransactionType txType, String address, public List<byte[]> getSignaturesMatchingCriteria(Integer startBlock, Integer blockLimit, TransactionType txType, String address,
ConfirmationStatus confirmationStatus, Integer limit, Integer offset, Boolean reverse) throws DataException { ConfirmationStatus confirmationStatus, Integer limit, Integer offset, Boolean reverse) throws DataException {