API: basic asset info

Added repository support for asset API calls

Added /utils/seed for returning server-generated 32-byte seed
This commit is contained in:
catbref 2018-12-11 13:48:10 +00:00
parent 3829630b29
commit 2aaa199c86
14 changed files with 231 additions and 4 deletions

12
pom.xml
View File

@ -71,6 +71,18 @@
<token>https://petstore.swagger.io/v2/swagger.json</token>
<value>/openapi.json</value>
</replacement>
<replacement>
<token>Swagger UI</token>
<value>API Documentation</value>
</replacement>
<replacement>
<token>deepLinking: true,</token>
<value>
deepLinking: true,
tagsSorter: "alpha",
operationsSorter: "alpha",
</value>
</replacement>
</replacements>
</configuration>
</plugin>

View File

@ -370,7 +370,7 @@ public class AddressesResource {
@GET
@Path("/publickey/{address}")
@Operation(
summary = "Address' public key",
summary = "Get public key of address",
description = "Returns the base64-encoded account public key of the given address, or \"false\" if address not known or has no public key.",
extensions = {
@Extension(name = "translation", properties = {

View File

@ -38,6 +38,7 @@ public class AdminResource {
@Parameter(in = ParameterIn.QUERY, name = "limit", description = "Maximum number of entries to return, 0 means unlimited", schema = @Schema(type = "integer", defaultValue = "10"))
@Parameter(in = ParameterIn.QUERY, name = "offset", description = "Starting entry in results", 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"))
public String globalParameters() {
return "";
}

View File

@ -7,10 +7,11 @@ import io.swagger.v3.oas.annotations.info.Info;
import io.swagger.v3.oas.annotations.tags.Tag;
@OpenAPIDefinition(
info = @Info( title = "Qora API", description = "NOTE: byte-arrays currently returned as Base64 but this is likely to change to Base58" ),
info = @Info( title = "Qora API", description = "NOTE: byte-arrays are encoded in Base64" ),
tags = {
@Tag(name = "Addresses"),
@Tag(name = "Admin"),
@Tag(name = "Assets"),
@Tag(name = "Blocks"),
@Tag(name = "Transactions"),
@Tag(name = "Utilities")

View File

@ -29,6 +29,7 @@ public class ApiService {
this.resources = new HashSet<Class<?>>();
this.resources.add(AddressesResource.class);
this.resources.add(AdminResource.class);
this.resources.add(AssetsResource.class);
this.resources.add(BlocksResource.class);
this.resources.add(TransactionsResource.class);
this.resources.add(UtilsResource.class);

View File

@ -0,0 +1,86 @@
package api;
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;
import io.swagger.v3.oas.annotations.tags.Tag;
import repository.DataException;
import repository.Repository;
import repository.RepositoryManager;
import java.util.List;
import javax.servlet.http.HttpServletRequest;
import javax.ws.rs.GET;
import javax.ws.rs.Path;
import javax.ws.rs.Produces;
import javax.ws.rs.QueryParam;
import javax.ws.rs.core.Context;
import javax.ws.rs.core.MediaType;
import api.models.AssetWithHolders;
import data.assets.AssetData;
@Path("/assets")
@Produces({MediaType.APPLICATION_JSON, MediaType.TEXT_PLAIN})
@Tag(name = "Assets")
public class AssetsResource {
@Context
HttpServletRequest request;
@GET
@Path("/all")
@Operation(
summary = "List all known assets",
responses = {
@ApiResponse(
description = "asset info",
content = @Content(array = @ArraySchema(schema = @Schema(implementation = AssetData.class)))
)
}
)
public List<AssetData> getAllAssets() {
try (final Repository repository = RepositoryManager.getRepository()) {
return repository.getAssetRepository().getAllAssets();
} catch (DataException e) {
throw ApiErrorFactory.getInstance().createError(ApiError.REPOSITORY_ISSUE, e);
}
}
@GET
@Path("/info")
@Operation(
summary = "Info on specific asset",
responses = {
@ApiResponse(
description = "asset info",
content = @Content(array = @ArraySchema(schema = @Schema(implementation = AssetData.class)))
)
}
)
public AssetWithHolders getAssetInfo(@QueryParam("key") Integer key, @QueryParam("name") String name, @Parameter(ref = "includeHolders") @QueryParam("withHolders") boolean includeHolders) {
if (key == null && (name == null || name.isEmpty()))
throw ApiErrorFactory.getInstance().createError(ApiError.INVALID_CRITERIA);
try (final Repository repository = RepositoryManager.getRepository()) {
AssetData assetData = null;
if (key != null)
assetData = repository.getAssetRepository().fromAssetId(key);
else
assetData = repository.getAssetRepository().fromAssetName(name);
if (assetData == null)
throw ApiErrorFactory.getInstance().createError(ApiError.INVALID_ASSET_ID);
return new AssetWithHolders(repository, assetData, includeHolders);
} catch (DataException e) {
throw ApiErrorFactory.getInstance().createError(ApiError.REPOSITORY_ISSUE, e);
}
}
}

View File

@ -7,6 +7,7 @@ import io.swagger.v3.oas.annotations.responses.ApiResponse;
import io.swagger.v3.oas.annotations.tags.Tag;
import utils.Base58;
import java.security.SecureRandom;
import java.util.Base64;
import javax.servlet.http.HttpServletRequest;
@ -63,4 +64,21 @@ public class UtilsResource {
}
}
@GET
@Path("/seed")
@Operation(
summary = "Generate random 32-byte seed",
responses = {
@ApiResponse(
description = "base64 data",
content = @Content(schema = @Schema(implementation = String.class))
)
}
)
public String seed() {
byte[] seed = new byte[32];
new SecureRandom().nextBytes(seed);
return Base64.getEncoder().encodeToString(seed);
}
}

View File

@ -0,0 +1,43 @@
package api.models;
import java.util.List;
import java.util.stream.Collectors;
import javax.xml.bind.annotation.XmlElement;
import api.ApiError;
import api.ApiErrorFactory;
import data.account.AccountBalanceData;
import data.assets.AssetData;
import data.block.BlockData;
import data.transaction.TransactionData;
import io.swagger.v3.oas.annotations.media.Schema;
import qora.block.Block;
import repository.DataException;
import repository.Repository;
@Schema(description = "Asset with (optional) asset holders")
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
@SuppressWarnings("unused")
private AssetWithHolders() {
}
public AssetWithHolders(Repository repository, AssetData assetData, boolean includeHolders) throws DataException {
if (assetData == null)
throw ApiErrorFactory.getInstance().createError(ApiError.INVALID_ASSET_ID);
this.assetData = assetData;
if (includeHolders)
this.holders = repository.getAccountRepository().getAssetBalances(assetData.getAssetId());
}
}

View File

@ -1,5 +1,10 @@
package data.assets;
import javax.xml.bind.annotation.XmlAccessType;
import javax.xml.bind.annotation.XmlAccessorType;
//All properties to be converted to JSON via JAX-RS
@XmlAccessorType(XmlAccessType.FIELD)
public class AssetData {
// Properties
@ -11,6 +16,10 @@ public class AssetData {
private boolean isDivisible;
private byte[] reference;
// necessary for JAX-RS serialization
protected AssetData() {
}
// NOTE: key is Long, not long, because it can be null if asset ID/key not yet assigned.
public AssetData(Long assetId, String owner, String name, String description, long quantity, boolean isDivisible, byte[] reference) {
this.assetId = assetId;

View File

@ -23,6 +23,8 @@ public interface AccountRepository {
public List<AccountBalanceData> getAllBalances(String address) throws DataException;
public List<AccountBalanceData> getAssetBalances(long assetId) throws DataException;
public void save(AccountBalanceData accountBalanceData) throws DataException;
public void delete(String address, long assetId) throws DataException;

View File

@ -2,6 +2,7 @@ package repository;
import java.util.List;
import data.account.AccountBalanceData;
import data.assets.AssetData;
import data.assets.OrderData;
import data.assets.TradeData;
@ -18,6 +19,10 @@ public interface AssetRepository {
public boolean assetExists(String assetName) throws DataException;
public List<AssetData> getAllAssets() throws DataException;
// For a list of asset holders, see AccountRepository.getAssetBalances
public void save(AssetData assetData) throws DataException;
public void delete(long assetId) throws DataException;

View File

@ -114,6 +114,27 @@ public class HSQLDBAccountRepository implements AccountRepository {
}
}
@Override
public List<AccountBalanceData> getAssetBalances(long assetId) throws DataException {
List<AccountBalanceData> balances = new ArrayList<AccountBalanceData>();
try (ResultSet resultSet = this.repository.checkedExecute("SELECT account, balance FROM AccountBalances WHERE asset_id = ?", assetId)) {
if (resultSet == null)
return balances;
do {
String address = resultSet.getString(1);
BigDecimal balance = resultSet.getBigDecimal(2).setScale(8);
balances.add(new AccountBalanceData(address, assetId, balance));
} while (resultSet.next());
return balances;
} catch (SQLException e) {
throw new DataException("Unable to fetch asset account balances from repository", e);
}
}
@Override
public void save(AccountBalanceData accountBalanceData) throws DataException {
HSQLDBSaver saveHelper = new HSQLDBSaver("AccountBalances");

View File

@ -82,6 +82,33 @@ public class HSQLDBAssetRepository implements AssetRepository {
}
}
@Override
public List<AssetData> getAllAssets() throws DataException {
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")) {
if (resultSet == null)
return assets;
do {
String owner = resultSet.getString(1);
long assetId = resultSet.getLong(2);
String description = resultSet.getString(3);
long quantity = resultSet.getLong(4);
boolean isDivisible = resultSet.getBoolean(5);
byte[] reference = resultSet.getBytes(6);
String assetName = resultSet.getString(7);
assets.add(new AssetData(assetId, owner, assetName, description, quantity, isDivisible, reference));
} while (resultSet.next());
return assets;
} catch (SQLException e) {
throw new DataException("Unable to fetch all assets from repository", e);
}
}
@Override
public void save(AssetData assetData) throws DataException {
HSQLDBSaver saveHelper = new HSQLDBSaver("Assets");
@ -148,8 +175,7 @@ public class HSQLDBAssetRepository implements AssetRepository {
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",
+ "WHERE have_asset_id = ? AND want_asset_id = ? AND is_closed = FALSE AND is_fulfilled = FALSE ORDER BY price ASC, ordered ASC",
haveAssetId, wantAssetId)) {
if (resultSet == null)
return orders;

View File

@ -152,6 +152,8 @@ public class HSQLDBDatabaseUpdates {
stmt.execute("CREATE INDEX UnconfirmedTransactionsIndex ON UnconfirmedTransactions (creation, signature)");
// Transaction recipients
// XXX This should be transaction "participants" to allow lookup of all activity by an address!
// Could add "is_recipient" boolean flag
stmt.execute("CREATE TABLE TransactionRecipients (signature Signature, recipient QoraAddress NOT NULL, "
+ "FOREIGN KEY (signature) REFERENCES Transactions (signature) ON DELETE CASCADE)");
// Use a separate table space as this table will be very large.