diff --git a/pom.xml b/pom.xml
index e9cbf1d4..dcbdfc8a 100644
--- a/pom.xml
+++ b/pom.xml
@@ -71,6 +71,18 @@
https://petstore.swagger.io/v2/swagger.json
/openapi.json
+
+ Swagger UI
+ API Documentation
+
+
+ deepLinking: true,
+
+ deepLinking: true,
+ tagsSorter: "alpha",
+ operationsSorter: "alpha",
+
+
diff --git a/src/api/AddressesResource.java b/src/api/AddressesResource.java
index 81b5ee10..18f28334 100644
--- a/src/api/AddressesResource.java
+++ b/src/api/AddressesResource.java
@@ -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 = {
diff --git a/src/api/AdminResource.java b/src/api/AdminResource.java
index 35346c19..2b802e0d 100644
--- a/src/api/AdminResource.java
+++ b/src/api/AdminResource.java
@@ -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 "";
}
diff --git a/src/api/ApiDefinition.java b/src/api/ApiDefinition.java
index 898f4b12..b91d1b63 100644
--- a/src/api/ApiDefinition.java
+++ b/src/api/ApiDefinition.java
@@ -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")
diff --git a/src/api/ApiService.java b/src/api/ApiService.java
index 2389eff6..71fa9850 100644
--- a/src/api/ApiService.java
+++ b/src/api/ApiService.java
@@ -29,6 +29,7 @@ public class ApiService {
this.resources = new HashSet>();
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);
diff --git a/src/api/AssetsResource.java b/src/api/AssetsResource.java
new file mode 100644
index 00000000..a670cb89
--- /dev/null
+++ b/src/api/AssetsResource.java
@@ -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 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);
+ }
+ }
+
+}
diff --git a/src/api/UtilsResource.java b/src/api/UtilsResource.java
index 01e3ad97..86602bc1 100644
--- a/src/api/UtilsResource.java
+++ b/src/api/UtilsResource.java
@@ -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);
+ }
+
}
diff --git a/src/api/models/AssetWithHolders.java b/src/api/models/AssetWithHolders.java
new file mode 100644
index 00000000..662b51b2
--- /dev/null
+++ b/src/api/models/AssetWithHolders.java
@@ -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 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());
+ }
+
+}
diff --git a/src/data/assets/AssetData.java b/src/data/assets/AssetData.java
index f4446fb2..a6c16df3 100644
--- a/src/data/assets/AssetData.java
+++ b/src/data/assets/AssetData.java
@@ -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;
diff --git a/src/repository/AccountRepository.java b/src/repository/AccountRepository.java
index 1559dc3a..99ad57ca 100644
--- a/src/repository/AccountRepository.java
+++ b/src/repository/AccountRepository.java
@@ -23,6 +23,8 @@ public interface AccountRepository {
public List getAllBalances(String address) throws DataException;
+ public List getAssetBalances(long assetId) throws DataException;
+
public void save(AccountBalanceData accountBalanceData) throws DataException;
public void delete(String address, long assetId) throws DataException;
diff --git a/src/repository/AssetRepository.java b/src/repository/AssetRepository.java
index 89e27f63..5bd5caed 100644
--- a/src/repository/AssetRepository.java
+++ b/src/repository/AssetRepository.java
@@ -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 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;
diff --git a/src/repository/hsqldb/HSQLDBAccountRepository.java b/src/repository/hsqldb/HSQLDBAccountRepository.java
index 0cbaabdb..e0c27612 100644
--- a/src/repository/hsqldb/HSQLDBAccountRepository.java
+++ b/src/repository/hsqldb/HSQLDBAccountRepository.java
@@ -114,6 +114,27 @@ public class HSQLDBAccountRepository implements AccountRepository {
}
}
+ @Override
+ public List getAssetBalances(long assetId) throws DataException {
+ List balances = new ArrayList();
+
+ 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");
diff --git a/src/repository/hsqldb/HSQLDBAssetRepository.java b/src/repository/hsqldb/HSQLDBAssetRepository.java
index 1cc32de3..741816fa 100644
--- a/src/repository/hsqldb/HSQLDBAssetRepository.java
+++ b/src/repository/hsqldb/HSQLDBAssetRepository.java
@@ -82,6 +82,33 @@ public class HSQLDBAssetRepository implements AssetRepository {
}
}
+ @Override
+ public List getAllAssets() throws DataException {
+ List assets = new ArrayList();
+
+ 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;
diff --git a/src/repository/hsqldb/HSQLDBDatabaseUpdates.java b/src/repository/hsqldb/HSQLDBDatabaseUpdates.java
index 520723eb..0dd8fc58 100644
--- a/src/repository/hsqldb/HSQLDBDatabaseUpdates.java
+++ b/src/repository/hsqldb/HSQLDBDatabaseUpdates.java
@@ -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.