Browse Source

Added/improved assets-related API calls

Imported Block/BlockChain fixes from "minting" branch to do
with block timestamps.

GET /assets/holders/{assetid}
and
GET /assets/address/{address}
and
GET /assets/balance/{assetid}/{address}
all combined into
GET /assets/balances?address=...&address=...&assetid=...&assetid=...

New GET /assets/trades/recent?assetid=...&assetid=...
that returns most recent two trades for each asset-pair.

GET /assets/orders/{address}/{assetid}/{otherassetid} has
includeClosed and includeFulfilled repurposed as
isClosed (true/false/omitted) and isFulfilled (true/false/omitted).

ALSO, Order.isClosed is now set to true when isFulfilled is set to true
during processing (and correspondingly set to false during orphaning).

AccountBalanceData now includes optional assetName field for use with API
but generally not set for internal use.
pull/67/head
catbref 6 years ago
parent
commit
4d69242cdb
  1. 152
      src/main/java/org/qora/api/resource/AssetsResource.java
  2. 8
      src/main/java/org/qora/asset/Trade.java
  3. 67
      src/main/java/org/qora/block/Block.java
  4. 4
      src/main/java/org/qora/block/BlockChain.java
  5. 13
      src/main/java/org/qora/data/account/AccountBalanceData.java
  6. 65
      src/main/java/org/qora/data/asset/RecentTradeData.java
  7. 12
      src/main/java/org/qora/repository/AccountRepository.java
  8. 11
      src/main/java/org/qora/repository/AssetRepository.java
  9. 56
      src/main/java/org/qora/repository/hsqldb/HSQLDBAccountRepository.java
  10. 90
      src/main/java/org/qora/repository/hsqldb/HSQLDBAssetRepository.java

152
src/main/java/org/qora/api/resource/AssetsResource.java

@ -9,7 +9,6 @@ import io.swagger.v3.oas.annotations.parameters.RequestBody;
import io.swagger.v3.oas.annotations.responses.ApiResponse;
import io.swagger.v3.oas.annotations.tags.Tag;
import java.math.BigDecimal;
import java.util.ArrayList;
import java.util.List;
import java.util.stream.Collectors;
@ -24,7 +23,6 @@ import javax.ws.rs.QueryParam;
import javax.ws.rs.core.Context;
import javax.ws.rs.core.MediaType;
import org.qora.account.Account;
import org.qora.api.ApiError;
import org.qora.api.ApiErrors;
import org.qora.api.ApiException;
@ -32,11 +30,13 @@ import org.qora.api.ApiExceptionFactory;
import org.qora.api.model.AggregatedOrder;
import org.qora.api.model.TradeWithOrderInfo;
import org.qora.api.resource.TransactionsResource.ConfirmationStatus;
import org.qora.asset.Asset;
import org.qora.crypto.Crypto;
import org.qora.data.account.AccountBalanceData;
import org.qora.data.account.AccountData;
import org.qora.data.asset.AssetData;
import org.qora.data.asset.OrderData;
import org.qora.data.asset.RecentTradeData;
import org.qora.data.asset.TradeData;
import org.qora.data.transaction.CancelAssetOrderTransactionData;
import org.qora.data.transaction.CreateAssetOrderTransactionData;
@ -140,12 +140,12 @@ public class AssetsResource {
}
@GET
@Path("/holders/{assetid}")
@Path("/balances")
@Operation(
summary = "List holders of an asset",
summary = "Asset balances owned by addresses and/or filtered to subset of assetIDs",
description = "Returns asset balances for these addresses/assetIDs, with balances. At least one address or assetID must be supplied.",
responses = {
@ApiResponse(
description = "asset holders",
content = @Content(
array = @ArraySchema(
schema = @Schema(
@ -157,20 +157,30 @@ public class AssetsResource {
}
)
@ApiErrors({
ApiError.INVALID_CRITERIA, ApiError.INVALID_ASSET_ID, ApiError.REPOSITORY_ISSUE
ApiError.INVALID_ADDRESS, ApiError.INVALID_CRITERIA, ApiError.INVALID_ASSET_ID, ApiError.REPOSITORY_ISSUE
})
public List<AccountBalanceData> getAssetHolders(@PathParam("assetid") int assetId, @Parameter(
public List<AccountBalanceData> getAssetBalances(@QueryParam("address") List<String> addresses, @QueryParam("assetid") List<Long> assetIds, @Parameter(
ref = "limit"
) @QueryParam("limit") Integer limit, @Parameter(
ref = "offset"
) @QueryParam("offset") Integer offset, @Parameter(
ref = "reverse"
) @QueryParam("reverse") Boolean reverse) {
if (addresses.isEmpty() && assetIds.isEmpty())
throw ApiExceptionFactory.INSTANCE.createException(request, ApiError.INVALID_CRITERIA);
for (String address : addresses)
if (!Crypto.isValidAddress(address))
throw ApiExceptionFactory.INSTANCE.createException(request, ApiError.INVALID_ADDRESS);
try (final Repository repository = RepositoryManager.getRepository()) {
for (long assetId : assetIds)
if (!repository.getAssetRepository().assetExists(assetId))
throw ApiExceptionFactory.INSTANCE.createException(request, ApiError.INVALID_ASSET_ID);
return repository.getAccountRepository().getAssetBalances(assetId, limit, offset, reverse);
return repository.getAccountRepository().getAssetBalances(addresses, assetIds, limit, offset, reverse);
} catch (ApiException e) {
throw e;
} catch (DataException e) {
throw ApiExceptionFactory.INSTANCE.createException(request, ApiError.REPOSITORY_ISSUE, e);
}
@ -269,6 +279,51 @@ public class AssetsResource {
}
}
@GET
@Path("/trades/recent")
@Operation(
summary = "Most recent asset trades",
description = "Returns list of most recent two asset trades for each assetID passed. Other assetID optional.",
responses = {
@ApiResponse(
description = "asset trades",
content = @Content(
array = @ArraySchema(
schema = @Schema(
implementation = RecentTradeData.class
)
)
)
)
}
)
@ApiErrors({
ApiError.INVALID_ASSET_ID, ApiError.REPOSITORY_ISSUE
})
public List<RecentTradeData> getRecentTrades(@QueryParam("assetid") List<Long> assetIds, @QueryParam("otherassetid") Long otherAssetId, @Parameter(
ref = "limit"
) @QueryParam("limit") Integer limit, @Parameter(
ref = "offset"
) @QueryParam("offset") Integer offset, @Parameter(
ref = "reverse"
) @QueryParam("reverse") Boolean reverse) {
try (final Repository repository = RepositoryManager.getRepository()) {
for (long assetId : assetIds)
if (!repository.getAssetRepository().assetExists(assetId))
throw ApiExceptionFactory.INSTANCE.createException(request, ApiError.INVALID_ASSET_ID);
if (otherAssetId == null)
otherAssetId = Asset.QORA;
else
if (!repository.getAssetRepository().assetExists(otherAssetId))
throw ApiExceptionFactory.INSTANCE.createException(request, ApiError.INVALID_ASSET_ID);
return repository.getAssetRepository().getRecentTrades(assetIds, otherAssetId, limit, offset, reverse);
} catch (DataException e) {
throw ApiExceptionFactory.INSTANCE.createException(request, ApiError.REPOSITORY_ISSUE, e);
}
}
@GET
@Path("/trades/{assetid}/{otherassetid}")
@Operation(
@ -411,81 +466,6 @@ public class AssetsResource {
}
}
@GET
@Path("/address/{address}")
@Operation(
summary = "All assets owned by this address",
description = "Returns the list of assets for this address, with balances.",
responses = {
@ApiResponse(
description = "the list of assets",
content = @Content(
array = @ArraySchema(
schema = @Schema(
implementation = AccountBalanceData.class
)
)
)
)
}
)
@ApiErrors({
ApiError.INVALID_ADDRESS, ApiError.REPOSITORY_ISSUE
})
public List<AccountBalanceData> getOwnedAssets(@PathParam("address") String address, @Parameter(
ref = "limit"
) @QueryParam("limit") Integer limit, @Parameter(
ref = "offset"
) @QueryParam("offset") Integer offset, @Parameter(
ref = "reverse"
) @QueryParam("reverse") Boolean reverse) {
if (!Crypto.isValidAddress(address))
throw ApiExceptionFactory.INSTANCE.createException(request, ApiError.INVALID_ADDRESS);
try (final Repository repository = RepositoryManager.getRepository()) {
return repository.getAccountRepository().getAllBalances(address, limit, offset, reverse);
} catch (ApiException e) {
throw e;
} catch (DataException e) {
throw ApiExceptionFactory.INSTANCE.createException(request, ApiError.REPOSITORY_ISSUE, e);
}
}
@GET
@Path("/balance/{assetid}/{address}")
@Operation(
summary = "Asset-specific balance request",
description = "Returns the confirmed balance of the given address for the given asset key.",
responses = {
@ApiResponse(
description = "the balance",
content = @Content(
mediaType = MediaType.TEXT_PLAIN,
schema = @Schema(
type = "string",
format = "number"
)
)
)
}
)
@ApiErrors({
ApiError.INVALID_ADDRESS, ApiError.REPOSITORY_ISSUE
})
public BigDecimal getAssetBalance(@PathParam("assetid") long assetid, @PathParam("address") String address) {
if (!Crypto.isValidAddress(address))
throw ApiExceptionFactory.INSTANCE.createException(request, ApiError.INVALID_ADDRESS);
try (final Repository repository = RepositoryManager.getRepository()) {
Account account = new Account(repository, address);
return account.getConfirmedBalance(assetid);
} catch (ApiException e) {
throw e;
} catch (DataException e) {
throw ApiExceptionFactory.INSTANCE.createException(request, ApiError.REPOSITORY_ISSUE, e);
}
}
@GET
@Path("/orders/{address}")
@Operation(
@ -555,8 +535,8 @@ public class AssetsResource {
@ApiErrors({
ApiError.INVALID_ADDRESS, ApiError.ADDRESS_NO_EXISTS, ApiError.REPOSITORY_ISSUE
})
public List<OrderData> getAccountAssetPairOrders(@PathParam("address") String address, @PathParam("assetid") int assetId, @PathParam("otherassetid") int otherAssetId, @QueryParam("includeClosed") boolean includeClosed,
@QueryParam("includeFulfilled") boolean includeFulfilled, @Parameter(
public List<OrderData> getAccountAssetPairOrders(@PathParam("address") String address, @PathParam("assetid") int assetId,
@PathParam("otherassetid") int otherAssetId, @QueryParam("isClosed") Boolean isClosed, @QueryParam("isFulfilled") Boolean isFulfilled, @Parameter(
ref = "limit"
) @QueryParam("limit") Integer limit, @Parameter(
ref = "offset"
@ -582,7 +562,7 @@ public class AssetsResource {
if (!repository.getAssetRepository().assetExists(otherAssetId))
throw ApiExceptionFactory.INSTANCE.createException(request, ApiError.INVALID_ASSET_ID);
return repository.getAssetRepository().getAccountsOrders(publicKey, assetId, otherAssetId, includeClosed, includeFulfilled, limit, offset, reverse);
return repository.getAssetRepository().getAccountsOrders(publicKey, assetId, otherAssetId, isClosed, isFulfilled, limit, offset, reverse);
} catch (ApiException e) {
throw e;
} catch (DataException e) {

8
src/main/java/org/qora/asset/Trade.java

@ -33,11 +33,15 @@ public class Trade {
OrderData initiatingOrder = assetRepository.fromOrderId(this.tradeData.getInitiator());
initiatingOrder.setFulfilled(initiatingOrder.getFulfilled().add(tradeData.getPrice()));
initiatingOrder.setIsFulfilled(Order.isFulfilled(initiatingOrder));
// Set isClosed to true if isFulfilled now true
initiatingOrder.setIsClosed(initiatingOrder.getIsFulfilled());
assetRepository.save(initiatingOrder);
OrderData targetOrder = assetRepository.fromOrderId(this.tradeData.getTarget());
targetOrder.setFulfilled(targetOrder.getFulfilled().add(tradeData.getAmount()));
targetOrder.setIsFulfilled(Order.isFulfilled(targetOrder));
// Set isClosed to true if isFulfilled now true
targetOrder.setIsClosed(targetOrder.getIsFulfilled());
assetRepository.save(targetOrder);
// Actually transfer asset balances
@ -57,11 +61,15 @@ public class Trade {
OrderData initiatingOrder = assetRepository.fromOrderId(this.tradeData.getInitiator());
initiatingOrder.setFulfilled(initiatingOrder.getFulfilled().subtract(tradeData.getPrice()));
initiatingOrder.setIsFulfilled(Order.isFulfilled(initiatingOrder));
// Set isClosed to false if isFulfilled now false
initiatingOrder.setIsClosed(initiatingOrder.getIsFulfilled());
assetRepository.save(initiatingOrder);
OrderData targetOrder = assetRepository.fromOrderId(this.tradeData.getTarget());
targetOrder.setFulfilled(targetOrder.getFulfilled().subtract(tradeData.getAmount()));
targetOrder.setIsFulfilled(Order.isFulfilled(targetOrder));
// Set isClosed to false if isFulfilled now false
targetOrder.setIsClosed(targetOrder.getIsFulfilled());
assetRepository.save(targetOrder);
// Reverse asset transfers

67
src/main/java/org/qora/block/Block.java

@ -210,9 +210,6 @@ public class Block {
}
long timestamp = parentBlock.calcNextBlockTimestamp(version, generatorSignature, generator);
long maximumTimestamp = parentBlock.getBlockData().getTimestamp() + BlockChain.getInstance().getMaxBlockTime();
if (timestamp > maximumTimestamp)
timestamp = maximumTimestamp;
int transactionCount = 0;
byte[] transactionsSignature = null;
@ -783,28 +780,10 @@ public class Block {
if (this.blockData.getGeneratingBalance().compareTo(parentBlock.calcNextBlockGeneratingBalance()) != 0)
return ValidationResult.GENERATING_BALANCE_INCORRECT;
// XXX Block.isValid generator check relaxation?? blockchain config option?
// After maximum block period, then generator checks are relaxed
if (this.blockData.getTimestamp() < parentBlock.getBlockData().getTimestamp() + BlockChain.getInstance().getMaxBlockTime()) {
// Check generator is allowed to forge this block
BigInteger hashValue = this.calcBlockHash();
BigInteger target = parentBlock.calcGeneratorsTarget(this.generator);
// Multiply target by guesses
long guesses = (this.blockData.getTimestamp() - parentBlockData.getTimestamp()) / 1000;
BigInteger lowerTarget = target.multiply(BigInteger.valueOf(guesses - 1));
target = target.multiply(BigInteger.valueOf(guesses));
// Generator's target must exceed block's hashValue threshold
if (hashValue.compareTo(target) >= 0)
if (!isGeneratorValidToForge(parentBlock))
return ValidationResult.GENERATOR_NOT_ACCEPTED;
// Odd gen1 comment: "CHECK IF FIRST BLOCK OF USER"
// Each second elapsed allows generator to test a new "target" window against hashValue
if (hashValue.compareTo(lowerTarget) < 0)
return ValidationResult.GENERATOR_NOT_ACCEPTED;
}
// CIYAM ATs
if (this.blockData.getATCount() != 0) {
// Locally generated AT states should be valid so no need to re-execute them
@ -816,8 +795,6 @@ public class Block {
} else {
// Generate local AT states for comparison
this.executeATs();
// XXX do we need to revalidate signatures if transactions list has changed?
}
// Check locally generated AT states against ones received from elsewhere
@ -887,7 +864,6 @@ public class Block {
}
}
} catch (DataException e) {
// XXX why was this TRANSACTION_TIMESTAMP_INVALID?
return ValidationResult.TRANSACTION_INVALID;
} finally {
// Rollback repository changes made by test-processing transactions above
@ -960,8 +936,31 @@ public class Block {
this.blockData.setTransactionCount(this.blockData.getTransactionCount() + 1);
// We've added transactions, so recalculate transactions signature
// XXX surely this breaks Block.isSignatureValid which is called before we are?
// calcTransactionsSignature();
calcTransactionsSignature();
}
/** Returns whether block's generator is actually allowed to forge this block. */
protected boolean isGeneratorValidToForge(Block parentBlock) throws DataException {
BlockData parentBlockData = parentBlock.getBlockData();
BigInteger hashValue = this.calcBlockHash();
BigInteger target = parentBlock.calcGeneratorsTarget(this.generator);
// Multiply target by guesses
long guesses = (this.blockData.getTimestamp() - parentBlockData.getTimestamp()) / 1000;
BigInteger lowerTarget = target.multiply(BigInteger.valueOf(guesses - 1));
target = target.multiply(BigInteger.valueOf(guesses));
// Generator's target must exceed block's hashValue threshold
if (hashValue.compareTo(target) >= 0)
return false;
// Odd gen1 comment: "CHECK IF FIRST BLOCK OF USER"
// Each second elapsed allows generator to test a new "target" window against hashValue
if (hashValue.compareTo(lowerTarget) < 0)
return false;
return true;
}
/**
@ -981,6 +980,9 @@ public class Block {
if (blockFee.compareTo(BigDecimal.ZERO) > 0)
this.generator.setConfirmedBalance(Asset.QORA, this.generator.getConfirmedBalance(Asset.QORA).add(blockFee));
// Block rewards go here
processBlockRewards();
// Process AT fees and save AT states into repository
ATRepository atRepository = this.repository.getATRepository();
for (ATStateData atState : this.getATStates()) {
@ -1020,6 +1022,10 @@ public class Block {
}
}
protected void processBlockRewards() throws DataException {
// NOP for vanilla qora-core
}
/**
* Removes block from blockchain undoing transactions and adding them to unconfirmed pile.
*
@ -1045,6 +1051,9 @@ public class Block {
this.repository.getTransactionRepository().deleteParticipants(transaction.getTransactionData());
}
// Block rewards removed here
orphanBlockRewards();
// If fees are non-zero then remove fees from generator's balance
BigDecimal blockFee = this.blockData.getTotalFees();
if (blockFee.compareTo(BigDecimal.ZERO) > 0)
@ -1065,6 +1074,10 @@ public class Block {
this.repository.getBlockRepository().delete(this.blockData);
}
protected void orphanBlockRewards() throws DataException {
// NOP for vanilla qora-core
}
/**
* Return Qora balance adjusted to within min/max limits.
*/

4
src/main/java/org/qora/block/BlockChain.java

@ -56,9 +56,9 @@ public class BlockChain {
/** Number of blocks between recalculating block's generating balance. */
private int blockDifficultyInterval;
/** Minimum target time between blocks, in milliseconds. */
/** Minimum target time between blocks, in seconds. */
private long minBlockTime;
/** Maximum target time between blocks, in milliseconds. */
/** Maximum target time between blocks, in seconds. */
private long maxBlockTime;
/** Maximum acceptable timestamp disagreement offset in milliseconds. */
private long blockTimestampMargin;

13
src/main/java/org/qora/data/account/AccountBalanceData.java

@ -13,6 +13,8 @@ public class AccountBalanceData {
private String address;
private long assetId;
private BigDecimal balance;
// Not always present:
private String assetName;
// Constructors
@ -20,10 +22,15 @@ public class AccountBalanceData {
protected AccountBalanceData() {
}
public AccountBalanceData(String address, long assetId, BigDecimal balance) {
public AccountBalanceData(String address, long assetId, BigDecimal balance, String assetName) {
this.address = address;
this.assetId = assetId;
this.balance = balance;
this.assetName = assetName;
}
public AccountBalanceData(String address, long assetId, BigDecimal balance) {
this(address, assetId, balance, null);
}
// Getters/Setters
@ -44,4 +51,8 @@ public class AccountBalanceData {
this.balance = balance;
}
public String getAssetName() {
return this.assetName;
}
}

65
src/main/java/org/qora/data/asset/RecentTradeData.java

@ -0,0 +1,65 @@
package org.qora.data.asset;
import java.math.BigDecimal;
import javax.xml.bind.annotation.XmlAccessType;
import javax.xml.bind.annotation.XmlAccessorType;
import javax.xml.bind.annotation.XmlElement;
import io.swagger.v3.oas.annotations.media.Schema;
// All properties to be converted to JSON via JAXB
@XmlAccessorType(XmlAccessType.FIELD)
public class RecentTradeData {
// Properties
private long assetId;
private long otherAssetId;
private BigDecimal amount;
private BigDecimal price;
@Schema(
description = "when trade happened"
)
private long timestamp;
// Constructors
// necessary for JAXB serialization
protected RecentTradeData() {
}
public RecentTradeData(long assetId, long otherAssetId, BigDecimal amount, BigDecimal price, long timestamp) {
this.assetId = assetId;
this.otherAssetId = otherAssetId;
this.amount = amount;
this.price = price;
this.timestamp = timestamp;
}
// Getters/setters
public long getAssetId() {
return this.assetId;
}
public long getOtherAssetId() {
return this.otherAssetId;
}
public BigDecimal getAmount() {
return this.amount;
}
public BigDecimal getPrice() {
return this.price;
}
public long getTimestamp() {
return this.timestamp;
}
}

12
src/main/java/org/qora/repository/AccountRepository.java

@ -45,17 +45,7 @@ public interface AccountRepository {
public AccountBalanceData getBalance(String address, long assetId) throws DataException;
public List<AccountBalanceData> getAllBalances(String address, Integer limit, Integer offset, Boolean reverse) throws DataException;
public default List<AccountBalanceData> getAllBalances(String address) throws DataException {
return getAllBalances(address, null, null, null);
}
public List<AccountBalanceData> getAssetBalances(long assetId, Integer limit, Integer offset, Boolean reverse) throws DataException;
public default List<AccountBalanceData> getAssetBalances(long assetId) throws DataException {
return getAssetBalances(assetId, null, null, null);
}
public List<AccountBalanceData> getAssetBalances(List<String> addresses, List<Long> assetIds, Integer limit, Integer offset, Boolean reverse) throws DataException;
public void save(AccountBalanceData accountBalanceData) throws DataException;

11
src/main/java/org/qora/repository/AssetRepository.java

@ -4,6 +4,7 @@ import java.util.List;
import org.qora.data.asset.AssetData;
import org.qora.data.asset.OrderData;
import org.qora.data.asset.RecentTradeData;
import org.qora.data.asset.TradeData;
public interface AssetRepository {
@ -42,14 +43,14 @@ public interface AssetRepository {
public List<OrderData> getAggregatedOpenOrders(long haveAssetId, long wantAssetId, Integer limit, Integer offset, Boolean reverse) throws DataException;
public List<OrderData> getAccountsOrders(byte[] publicKey, boolean includeClosed, boolean includeFulfilled, Integer limit, Integer offset, Boolean reverse)
public List<OrderData> getAccountsOrders(byte[] publicKey, Boolean optIsClosed, Boolean optIsFulfilled, Integer limit, Integer offset, Boolean reverse)
throws DataException;
public List<OrderData> getAccountsOrders(byte[] publicKey, long haveAssetId, long wantAssetId, boolean includeClosed, boolean includeFulfilled,
public List<OrderData> getAccountsOrders(byte[] publicKey, long haveAssetId, long wantAssetId, Boolean optIsClosed, Boolean optIsFulfilled,
Integer limit, Integer offset, Boolean reverse) throws DataException;
public default List<OrderData> getAccountsOrders(byte[] publicKey, boolean includeClosed, boolean includeFulfilled) throws DataException {
return getAccountsOrders(publicKey, includeClosed, includeFulfilled, null, null, null);
public default List<OrderData> getAccountsOrders(byte[] publicKey, Boolean optIsClosed, Boolean optIsFulfilled) throws DataException {
return getAccountsOrders(publicKey, optIsClosed, optIsFulfilled, null, null, null);
}
public void save(OrderData orderData) throws DataException;
@ -64,6 +65,8 @@ public interface AssetRepository {
return getTrades(haveAssetId, wantAssetId, null, null, null);
}
public List<RecentTradeData> getRecentTrades(List<Long> assetIds, Long otherAssetId, Integer limit, Integer offset, Boolean reverse) throws DataException;
/** Returns TradeData for trades where orderId was involved, i.e. either initiating OR target order */
public List<TradeData> getOrdersTrades(byte[] orderId, Integer limit, Integer offset, Boolean reverse) throws DataException;

56
src/main/java/org/qora/repository/hsqldb/HSQLDBAccountRepository.java

@ -4,7 +4,9 @@ import java.math.BigDecimal;
import java.sql.ResultSet;
import java.sql.SQLException;
import java.util.ArrayList;
import java.util.Collections;
import java.util.List;
import java.util.stream.Collectors;
import org.qora.data.account.AccountBalanceData;
import org.qora.data.account.AccountData;
@ -141,54 +143,48 @@ public class HSQLDBAccountRepository implements AccountRepository {
}
@Override
public List<AccountBalanceData> getAllBalances(String address, Integer limit, Integer offset, Boolean reverse) throws DataException {
String sql = "SELECT asset_id, balance FROM AccountBalances WHERE account = ? ORDER BY asset_id";
if (reverse != null && reverse)
sql += " DESC";
sql += HSQLDBRepository.limitOffsetSql(limit, offset);
public List<AccountBalanceData> getAssetBalances(List<String> addresses, List<Long> assetIds, Integer limit, Integer offset, Boolean reverse)
throws DataException {
String sql = "SELECT account, asset_id, balance, asset_name FROM AccountBalances NATURAL JOIN Assets " + "WHERE ";
List<AccountBalanceData> balances = new ArrayList<AccountBalanceData>();
if (!addresses.isEmpty())
sql += "account IN (" + String.join(", ", Collections.nCopies(addresses.size(), "?")) + ") ";
try (ResultSet resultSet = this.repository.checkedExecute(sql, address)) {
if (resultSet == null)
return balances;
if (!addresses.isEmpty() && !assetIds.isEmpty())
sql += "AND ";
do {
long assetId = resultSet.getLong(1);
BigDecimal balance = resultSet.getBigDecimal(2).setScale(8);
balances.add(new AccountBalanceData(address, assetId, balance));
} while (resultSet.next());
if (!assetIds.isEmpty())
sql += "asset_id IN (" + String.join(", ", assetIds.stream().map(assetId -> assetId.toString()).collect(Collectors.toList())) + ") ";
return balances;
} catch (SQLException e) {
throw new DataException("Unable to fetch account balances from repository", e);
}
}
sql += "ORDER BY account";
if (reverse != null && reverse)
sql += " DESC";
@Override
public List<AccountBalanceData> getAssetBalances(long assetId, Integer limit, Integer offset, Boolean reverse) throws DataException {
String sql = "SELECT account, balance FROM AccountBalances WHERE asset_id = ? ORDER BY account";
sql += ", asset_id";
if (reverse != null && reverse)
sql += " DESC";
sql += HSQLDBRepository.limitOffsetSql(limit, offset);
List<AccountBalanceData> balances = new ArrayList<AccountBalanceData>();
String[] addressesArray = addresses.toArray(new String[addresses.size()]);
List<AccountBalanceData> accountBalances = new ArrayList<>();
try (ResultSet resultSet = this.repository.checkedExecute(sql, assetId)) {
try (ResultSet resultSet = this.repository.checkedExecute(sql, (Object[]) addressesArray)) {
if (resultSet == null)
return balances;
return accountBalances;
do {
String address = resultSet.getString(1);
BigDecimal balance = resultSet.getBigDecimal(2).setScale(8);
long assetId = resultSet.getLong(2);
BigDecimal balance = resultSet.getBigDecimal(3).setScale(8);
String assetName = resultSet.getString(4);
balances.add(new AccountBalanceData(address, assetId, balance));
accountBalances.add(new AccountBalanceData(address, assetId, balance, assetName));
} while (resultSet.next());
return balances;
return accountBalances;
} catch (SQLException e) {
throw new DataException("Unable to fetch asset account balances from repository", e);
throw new DataException("Unable to fetch asset balances from repository", e);
}
}

90
src/main/java/org/qora/repository/hsqldb/HSQLDBAssetRepository.java

@ -6,10 +6,12 @@ import java.sql.SQLException;
import java.sql.Timestamp;
import java.util.ArrayList;
import java.util.Calendar;
import java.util.Collections;
import java.util.List;
import org.qora.data.asset.AssetData;
import org.qora.data.asset.OrderData;
import org.qora.data.asset.RecentTradeData;
import org.qora.data.asset.TradeData;
import org.qora.repository.AssetRepository;
import org.qora.repository.DataException;
@ -241,12 +243,13 @@ public class HSQLDBAssetRepository implements AssetRepository {
}
@Override
public List<OrderData> getAccountsOrders(byte[] publicKey, boolean includeClosed, boolean includeFulfilled, Integer limit, Integer offset, Boolean reverse) throws DataException {
public List<OrderData> getAccountsOrders(byte[] publicKey, Boolean optIsClosed, Boolean optIsFulfilled, Integer limit, Integer offset, Boolean reverse)
throws DataException {
String sql = "SELECT asset_order_id, have_asset_id, want_asset_id, amount, fulfilled, price, ordered, is_closed, is_fulfilled FROM AssetOrders WHERE creator = ?";
if (!includeClosed)
sql += " AND is_closed = FALSE";
if (!includeFulfilled)
sql += " AND is_fulfilled = FALSE";
if (optIsClosed != null)
sql += " AND is_closed = " + (optIsClosed ? "TRUE" : "FALSE");
if (optIsFulfilled != null)
sql += " AND is_fulfilled = " + (optIsFulfilled ? "TRUE" : "FALSE");
sql += " ORDER BY ordered";
if (reverse != null && reverse)
sql += " DESC";
@ -269,8 +272,7 @@ public class HSQLDBAssetRepository implements AssetRepository {
boolean isClosed = resultSet.getBoolean(8);
boolean isFulfilled = resultSet.getBoolean(9);
OrderData order = new OrderData(orderId, publicKey, haveAssetId, wantAssetId, amount, fulfilled, price, timestamp, isClosed,
isFulfilled);
OrderData order = new OrderData(orderId, publicKey, haveAssetId, wantAssetId, amount, fulfilled, price, timestamp, isClosed, isFulfilled);
orders.add(order);
} while (resultSet.next());
@ -281,12 +283,13 @@ public class HSQLDBAssetRepository implements AssetRepository {
}
@Override
public List<OrderData> getAccountsOrders(byte[] publicKey, long haveAssetId, long wantAssetId, boolean includeClosed, boolean includeFulfilled, Integer limit, Integer offset, Boolean reverse) throws DataException {
public List<OrderData> getAccountsOrders(byte[] publicKey, long haveAssetId, long wantAssetId, Boolean optIsClosed, Boolean optIsFulfilled, Integer limit,
Integer offset, Boolean reverse) throws DataException {
String sql = "SELECT asset_order_id, amount, fulfilled, price, ordered, is_closed, is_fulfilled FROM AssetOrders WHERE creator = ? AND have_asset_id = ? AND want_asset_id = ?";
if (!includeClosed)
sql += " AND is_closed = FALSE";
if (!includeFulfilled)
sql += " AND is_fulfilled = FALSE";
if (optIsClosed != null)
sql += " AND is_closed = " + (optIsClosed ? "TRUE" : "FALSE");
if (optIsFulfilled != null)
sql += " AND is_fulfilled = " + (optIsFulfilled ? "TRUE" : "FALSE");
sql += " ORDER BY ordered";
if (reverse != null && reverse)
sql += " DESC";
@ -307,8 +310,7 @@ public class HSQLDBAssetRepository implements AssetRepository {
boolean isClosed = resultSet.getBoolean(6);
boolean isFulfilled = resultSet.getBoolean(7);
OrderData order = new OrderData(orderId, publicKey, haveAssetId, wantAssetId, amount, fulfilled, price, timestamp, isClosed,
isFulfilled);
OrderData order = new OrderData(orderId, publicKey, haveAssetId, wantAssetId, amount, fulfilled, price, timestamp, isClosed, isFulfilled);
orders.add(order);
} while (resultSet.next());
@ -376,6 +378,66 @@ public class HSQLDBAssetRepository implements AssetRepository {
}
}
@Override
public List<RecentTradeData> getRecentTrades(List<Long> assetIds, Long otherAssetId, Integer limit, Integer offset, Boolean reverse) throws DataException {
// Find assetID pairs that have actually been traded
String tradedAssetsSubquery = "SELECT have_asset_id, want_asset_id " + "FROM AssetTrades JOIN AssetOrders ON asset_order_id = initiating_order_id ";
// Optionally limit traded assetID pairs
if (!assetIds.isEmpty())
tradedAssetsSubquery += "WHERE have_asset_id IN (" + String.join(", ", Collections.nCopies(assetIds.size(), "?")) + ")";
if (otherAssetId != null) {
tradedAssetsSubquery += assetIds.isEmpty() ? " WHERE " : " AND ";
tradedAssetsSubquery += "want_asset_id = " + otherAssetId.toString();
}
tradedAssetsSubquery += " GROUP BY have_asset_id, want_asset_id";
// Find recent trades using "TradedAssets" assetID pairs
String recentTradesSubquery = "SELECT AssetTrades.amount, AssetTrades.price, AssetTrades.traded "
+ "FROM AssetOrders JOIN AssetTrades ON initiating_order_id = asset_order_id "
+ "WHERE AssetOrders.have_asset_id = TradedAssets.have_asset_id AND AssetOrders.want_asset_id = TradedAssets.want_asset_id "
+ "ORDER BY traded DESC LIMIT 2";
// Put it all together
String sql = "SELECT have_asset_id, want_asset_id, RecentTrades.amount, RecentTrades.price, RecentTrades.traded " + "FROM (" + tradedAssetsSubquery
+ ") AS TradedAssets " + ", LATERAL (" + recentTradesSubquery + ") AS RecentTrades (amount, price, traded) " + "ORDER BY have_asset_id";
if (reverse != null && reverse)
sql += " DESC";
sql += ", want_asset_id";
if (reverse != null && reverse)
sql += " DESC";
sql += ", RecentTrades.traded DESC ";
sql += HSQLDBRepository.limitOffsetSql(limit, offset);
Long[] assetIdsArray = assetIds.toArray(new Long[assetIds.size()]);
List<RecentTradeData> recentTrades = new ArrayList<>();
try (ResultSet resultSet = this.repository.checkedExecute(sql, (Object[]) assetIdsArray)) {
if (resultSet == null)
return recentTrades;
do {
long haveAssetId = resultSet.getLong(1);
long wantAssetId = resultSet.getLong(2);
BigDecimal amount = resultSet.getBigDecimal(3);
BigDecimal price = resultSet.getBigDecimal(4);
long timestamp = resultSet.getTimestamp(5, Calendar.getInstance(HSQLDBRepository.UTC)).getTime();
RecentTradeData recentTrade = new RecentTradeData(haveAssetId, wantAssetId, amount, price, timestamp);
recentTrades.add(recentTrade);
} while (resultSet.next());
return recentTrades;
} catch (SQLException e) {
throw new DataException("Unable to fetch recent asset trades from repository", e);
}
}
@Override
public List<TradeData> getOrdersTrades(byte[] orderId, Integer limit, Integer offset, Boolean reverse) throws DataException {
String sql = "SELECT initiating_order_id, target_order_id, amount, price, traded FROM AssetTrades WHERE initiating_order_id = ? OR target_order_id = ? ORDER BY traded";

Loading…
Cancel
Save