API, HSQLDB

Added more global parameters to /admin/unused API endpoint (formally /admin/dud)
and also managed to remove /admin/unused from API documentation UI.

Added results slicing to /assets/all

Added /assets/orderbook API call that returns open asset orders

Added /assets/trades that returns successful asset trades

Added POST /assets/issue stub

Unified HSQLDB connectionUrl to public variable inside Controller class.

Can't deploy v1 ATs with isFinished=true flag as that prevents later
transactions sending messages (during import of v1 chain).
Some future hard-fork code will need to set all v1 ATs to "finished".

Changed DB's "TransactionRecipients" to "TransactionParticipants" to
properly support API call to find all transactions 'involving' a
specific address. Support code needed in Block and Transaction with
some transaction-specific overrides for Genesis and AT transactions.

Removed old, deprecated calls from Transaction/TransactionRepository

Moved HSQLDB database properties from connection URL to explicit
SQL statements in HSQLDBDatabaseUpdates. They didn't work in
connection URL during DB creation anyway.

Retrofitted HSQLDB Accounts table with public_key column instead of
rebuilding it later.

Fixed incorrect comments in IssueAssetTransactionTransformer regarding
v1 serialization for signing.

Re-imported v1 chain to test latest changes.
This commit is contained in:
catbref 2018-12-12 12:13:06 +00:00
parent 2aaa199c86
commit cfd8b53fc1
31 changed files with 398 additions and 141 deletions

View File

@ -26,7 +26,6 @@ import javax.ws.rs.core.MediaType;
import data.account.AccountBalanceData;
import data.account.AccountData;
import qora.account.Account;
import qora.account.PublicKeyAccount;
import qora.assets.Asset;
import qora.crypto.Crypto;
import repository.DataException;
@ -82,9 +81,7 @@ public class AddressesResource {
)
}
)
public String getLastReference(
@Parameter(description = "a base64-encoded address", required = true) @PathParam("address") String address
) {
public String getLastReference(@Parameter(ref = "address") @PathParam("address") String address) {
if (!Crypto.isValidAddress(address))
throw this.apiErrorFactory.createError(ApiError.INVALID_ADDRESS);

View File

@ -32,13 +32,17 @@ public class AdminResource {
HttpServletRequest request;
@GET
@Path("/dud")
@Parameter(name = "blockSignature", description = "Block signature", schema = @Schema(type = "string", format = "byte", minLength = 84, maxLength=88))
@Parameter(in = ParameterIn.QUERY, name = "count", description = "Maximum number of entries to return", schema = @Schema(type = "integer", defaultValue = "10"))
@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"))
@Path("/unused")
@Parameter(in = ParameterIn.PATH, name = "blockSignature", description = "Block signature", schema = @Schema(type = "string", format = "byte"), example = "ZZZZ==")
@Parameter(in = ParameterIn.PATH, name = "assetId", description = "Asset ID, 0 is native coin", schema = @Schema(type = "string", format = "byte"))
@Parameter(in = ParameterIn.PATH, name = "otherAssetId", description = "Asset ID, 0 is native coin", schema = @Schema(type = "string", format = "byte"))
@Parameter(in = ParameterIn.PATH, name = "address", description = "an account address", example = "QRHDHASWAXarqTvB2X4SNtJCWbxGf68M2o")
@Parameter(in = ParameterIn.QUERY, name = "count", description = "Maximum number of entries to return, 0 means none", schema = @Schema(type = "integer", defaultValue = "20"))
@Parameter(in = ParameterIn.QUERY, name = "limit", description = "Maximum number of entries to return, 0 means unlimited", schema = @Schema(type = "integer", defaultValue = "20"))
@Parameter(in = ParameterIn.QUERY, name = "offset", description = "Starting entry in results, 0 is first entry", 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"))
@Parameter(in = ParameterIn.QUERY, name = "queryAssetId", description = "Asset ID, 0 is native coin", schema = @Schema(type = "string", format = "byte"))
public String globalParameters() {
return "";
}

View File

@ -55,12 +55,14 @@ public class AnnotationPostProcessor implements ReaderListener {
@Override
public void afterScan(Reader reader, OpenAPI openAPI) {
// Populate Components section with reusable parameters, like "limit" and "offset"
// We take the reusable parameters from AdminResource.globalParameters path "/admin/dud"
// We take the reusable parameters from AdminResource.globalParameters path "/admin/unused"
Components components = openAPI.getComponents();
PathItem globalParametersPathItem = openAPI.getPaths().get("/admin/dud");
if (globalParametersPathItem != null)
PathItem globalParametersPathItem = openAPI.getPaths().get("/admin/unused");
if (globalParametersPathItem != null) {
for (Parameter parameter : globalParametersPathItem.getGet().getParameters())
components.addParameters(parameter.getName(), parameter);
openAPI.getPaths().remove("/admin/unused");
}
// use context path and keys from "x-translation" extension annotations
// to translate supported annotations and finally remove "x-translation" extensions

View File

@ -5,24 +5,32 @@ 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.parameters.RequestBody;
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.ArrayList;
import java.util.List;
import javax.servlet.http.HttpServletRequest;
import javax.ws.rs.GET;
import javax.ws.rs.POST;
import javax.ws.rs.Path;
import javax.ws.rs.PathParam;
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 api.models.IssueAssetRequest;
import api.models.TradeWithOrderInfo;
import data.assets.AssetData;
import data.assets.OrderData;
import data.assets.TradeData;
@Path("/assets")
@Produces({MediaType.APPLICATION_JSON, MediaType.TEXT_PLAIN})
@ -43,9 +51,16 @@ public class AssetsResource {
)
}
)
public List<AssetData> getAllAssets() {
public List<AssetData> getAllAssets(@Parameter(ref = "limit") @QueryParam("limit") int limit, @Parameter(ref = "offset") @QueryParam("offset") int offset) {
try (final Repository repository = RepositoryManager.getRepository()) {
return repository.getAssetRepository().getAllAssets();
List<AssetData> assets = repository.getAssetRepository().getAllAssets();
// Pagination would take effect here (or as part of the repository access)
int fromIndex = Integer.min(offset, assets.size());
int toIndex = limit == 0 ? assets.size() : Integer.min(fromIndex + limit, assets.size());
assets = assets.subList(fromIndex, toIndex);
return assets;
} catch (DataException e) {
throw ApiErrorFactory.getInstance().createError(ApiError.REPOSITORY_ISSUE, e);
}
@ -55,24 +70,25 @@ public class AssetsResource {
@Path("/info")
@Operation(
summary = "Info on specific asset",
description = "Supply either assetId OR assetName. (If both supplied, assetId takes priority).",
responses = {
@ApiResponse(
description = "asset info",
content = @Content(array = @ArraySchema(schema = @Schema(implementation = AssetData.class)))
content = @Content(array = @ArraySchema(schema = @Schema(implementation = AssetWithHolders.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()))
public AssetWithHolders getAssetInfo(@QueryParam("assetId") Integer assetId, @QueryParam("assetName") String assetName, @Parameter(ref = "includeHolders") @QueryParam("withHolders") boolean includeHolders) {
if (assetId == null && (assetName == null || assetName.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);
if (assetId != null)
assetData = repository.getAssetRepository().fromAssetId(assetId);
else
assetData = repository.getAssetRepository().fromAssetName(name);
assetData = repository.getAssetRepository().fromAssetName(assetName);
if (assetData == null)
throw ApiErrorFactory.getInstance().createError(ApiError.INVALID_ASSET_ID);
@ -83,4 +99,98 @@ public class AssetsResource {
}
}
@GET
@Path("/orderbook/{assetId}/{otherAssetId}")
@Operation(
summary = "Asset order book",
description = "Returns open orders, offering {assetId} for {otherAssetId} in return.",
responses = {
@ApiResponse(
description = "asset orders",
content = @Content(array = @ArraySchema(schema = @Schema(implementation = OrderData.class)))
)
}
)
public List<OrderData> getAssetOrders(@Parameter(ref = "assetId") @PathParam("assetId") int assetId, @Parameter(ref = "otherAssetId") @PathParam("otherAssetId") int otherAssetId,
@Parameter(ref = "limit") @QueryParam("limit") int limit, @Parameter(ref = "offset") @QueryParam("offset") int offset) {
try (final Repository repository = RepositoryManager.getRepository()) {
if (!repository.getAssetRepository().assetExists(assetId))
throw ApiErrorFactory.getInstance().createError(ApiError.INVALID_ASSET_ID);
if (!repository.getAssetRepository().assetExists(otherAssetId))
throw ApiErrorFactory.getInstance().createError(ApiError.INVALID_ASSET_ID);
List<OrderData> orders = repository.getAssetRepository().getOpenOrders(assetId, otherAssetId);
// Pagination would take effect here (or as part of the repository access)
int fromIndex = Integer.min(offset, orders.size());
int toIndex = limit == 0 ? orders.size() : Integer.min(fromIndex + limit, orders.size());
orders = orders.subList(fromIndex, toIndex);
return orders;
} catch (DataException e) {
throw ApiErrorFactory.getInstance().createError(ApiError.REPOSITORY_ISSUE, e);
}
}
@GET
@Path("/trades/{assetId}/{otherAssetId}")
@Operation(
summary = "Asset trades",
description = "Returns successful trades of {assetId} for {otherAssetId}.<br>" +
"Does NOT include trades of {otherAssetId} for {assetId}!<br>" +
"\"Initiating\" order is the order that caused the actual trade by matching up with the \"target\" order.",
responses = {
@ApiResponse(
description = "asset trades",
content = @Content(array = @ArraySchema(schema = @Schema(implementation = TradeWithOrderInfo.class)))
)
}
)
public List<TradeWithOrderInfo> getAssetTrades(@Parameter(ref = "assetId") @PathParam("assetId") int assetId, @Parameter(ref = "otherAssetId") @PathParam("otherAssetId") int otherAssetId,
@Parameter(ref = "limit") @QueryParam("limit") int limit, @Parameter(ref = "offset") @QueryParam("offset") int offset) {
try (final Repository repository = RepositoryManager.getRepository()) {
if (!repository.getAssetRepository().assetExists(assetId))
throw ApiErrorFactory.getInstance().createError(ApiError.INVALID_ASSET_ID);
if (!repository.getAssetRepository().assetExists(otherAssetId))
throw ApiErrorFactory.getInstance().createError(ApiError.INVALID_ASSET_ID);
List<TradeData> trades = repository.getAssetRepository().getTrades(assetId, otherAssetId);
// Pagination would take effect here (or as part of the repository access)
int fromIndex = Integer.min(offset, trades.size());
int toIndex = limit == 0 ? trades.size() : Integer.min(fromIndex + limit, trades.size());
trades = trades.subList(fromIndex, toIndex);
// Expanding remaining entries
List<TradeWithOrderInfo> fullTrades = new ArrayList<>();
for (TradeData trade : trades)
fullTrades.add(new TradeWithOrderInfo(repository, trade));
return fullTrades;
} catch (DataException e) {
throw ApiErrorFactory.getInstance().createError(ApiError.REPOSITORY_ISSUE, e);
}
}
@POST
@Path("/issue")
@Operation(
summary = "Issue new asset",
requestBody = @RequestBody(
required = true,
content = @Content(
mediaType = "application/json",
schema = @Schema(implementation = IssueAssetRequest.class)
)
)
)
public String issueAsset(IssueAssetRequest issueAssetRequest) {
// required: issuer (pubkey), name, description, quantity, isDivisible, fee
// optional: reference
// returns: raw tx
return "";
}
}

View File

@ -3,7 +3,6 @@ package api;
import globalization.Translator;
import io.swagger.v3.oas.annotations.Operation;
import io.swagger.v3.oas.annotations.Parameter;
import io.swagger.v3.oas.annotations.enums.ParameterIn;
import io.swagger.v3.oas.annotations.extensions.Extension;
import io.swagger.v3.oas.annotations.extensions.ExtensionProperty;
import io.swagger.v3.oas.annotations.media.ArraySchema;
@ -11,7 +10,6 @@ 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 qora.crypto.Crypto;
import qora.transaction.Transaction.TransactionType;
import java.util.ArrayList;

View File

@ -1,7 +1,6 @@
package api.models;
import java.util.List;
import java.util.stream.Collectors;
import javax.xml.bind.annotation.XmlElement;
@ -9,10 +8,7 @@ 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;

View File

@ -0,0 +1,26 @@
package api.models;
import java.math.BigDecimal;
import io.swagger.v3.oas.annotations.media.Schema;
public class IssueAssetRequest {
@Schema(description = "asset issuer's public key")
public byte[] issuer;
@Schema(description = "asset name - must be lowercase", example = "my-asset123")
public String name;
@Schema(description = "asset description")
public String description;
public BigDecimal quantity;
public boolean isDivisible;
public BigDecimal fee;
public byte[] reference;
}

View File

@ -0,0 +1,37 @@
package api.models;
import javax.xml.bind.annotation.XmlElement;
import data.assets.OrderData;
import data.assets.TradeData;
import io.swagger.v3.oas.annotations.media.Schema;
import repository.DataException;
import repository.Repository;
@Schema(description = "Asset trade, with order info")
public class TradeWithOrderInfo {
@Schema(implementation = TradeData.class, name = "trade", title = "trade data")
@XmlElement(name = "trade")
public TradeData tradeData;
@Schema(implementation = OrderData.class, name = "order", title = "order data")
@XmlElement(name = "initiatingOrder")
public OrderData initiatingOrderData;
@Schema(implementation = OrderData.class, name = "order", title = "order data")
@XmlElement(name = "targetOrder")
public OrderData targetOrderData;
// For JAX-RS
protected TradeWithOrderInfo() {
}
public TradeWithOrderInfo(Repository repository, TradeData tradeData) throws DataException {
this.tradeData = tradeData;
this.initiatingOrderData = repository.getAssetRepository().fromOrderId(tradeData.getInitiator());
this.targetOrderData = repository.getAssetRepository().fromOrderId(tradeData.getTarget());
}
}

View File

@ -14,7 +14,7 @@ import utils.Base58;
public class blockgenerator {
private static final Logger LOGGER = LogManager.getLogger(blockgenerator.class);
public static final String connectionUrl = "jdbc:hsqldb:file:db/test;create=true";
public static final String connectionUrl = "jdbc:hsqldb:file:db/blockchain;create=true";
public static void main(String[] args) {
if (args.length != 1) {

View File

@ -13,7 +13,7 @@ public class Controller {
private static final Logger LOGGER = LogManager.getLogger(Controller.class);
private static final String connectionUrl = "jdbc:hsqldb:file:db/test;create=true";
public static final String connectionUrl = "jdbc:hsqldb:file:db/blockchain;create=true";
public static final long startTime = System.currentTimeMillis();
private static final Object shutdownLock = new Object();

View File

@ -3,7 +3,7 @@ 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
// All properties to be converted to JSON via JAX-RS
@XmlAccessorType(XmlAccessType.FIELD)
public class AssetData {
@ -16,6 +16,8 @@ public class AssetData {
private boolean isDivisible;
private byte[] reference;
// Constructors
// necessary for JAX-RS serialization
protected AssetData() {
}

View File

@ -2,19 +2,48 @@ package data.assets;
import java.math.BigDecimal;
import javax.xml.bind.annotation.XmlAccessType;
import javax.xml.bind.annotation.XmlAccessorType;
import io.swagger.v3.oas.annotations.media.Schema;
// All properties to be converted to JSON via JAX-RS
@XmlAccessorType(XmlAccessType.FIELD)
public class OrderData implements Comparable<OrderData> {
// Properties
private byte[] orderId;
private byte[] creatorPublicKey;
@Schema(description = "asset on offer to give by order creator")
private long haveAssetId;
@Schema(description = "asset wanted to receive by order creator")
private long wantAssetId;
@Schema(description = "amount of \"have\" asset to trade")
private BigDecimal amount;
private BigDecimal fulfilled;
@Schema(description = "amount of \"want\" asset to receive per unit of \"have\" asset traded")
private BigDecimal price;
@Schema(description = "how much \"have\" asset has traded")
private BigDecimal fulfilled;
private long timestamp;
@Schema(description = "has this order been cancelled for further trades?")
private boolean isClosed;
@Schema(description = "has this order been fully traded?")
private boolean isFulfilled;
// Constructors
// necessary for JAX-RS serialization
protected OrderData() {
}
public OrderData(byte[] orderId, byte[] creatorPublicKey, long haveAssetId, long wantAssetId, BigDecimal amount, BigDecimal fulfilled, BigDecimal price,
long timestamp, boolean isClosed, boolean isFulfilled) {
this.orderId = orderId;
@ -33,6 +62,8 @@ public class OrderData implements Comparable<OrderData> {
this(orderId, creatorPublicKey, haveAssetId, wantAssetId, amount, BigDecimal.ZERO.setScale(8), price, timestamp, false, false);
}
// Getters/setters
public byte[] getOrderId() {
return this.orderId;
}

View File

@ -2,17 +2,42 @@ package data.assets;
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 JAX-RS
@XmlAccessorType(XmlAccessType.FIELD)
public class TradeData {
// Properties
@Schema(name = "initiatingOrderId", description = "ID of order that caused trade")
@XmlElement(name = "initiatingOrderId")
private byte[] initiator;
@Schema(name = "targetOrderId", description = "ID of order that matched")
@XmlElement(name = "targetOrderId")
private byte[] target;
@Schema(name = "targetAmount", description = "amount traded from target order")
@XmlElement(name = "targetAmount")
private BigDecimal amount;
@Schema(name = "initiatorAmount", description = "amount traded from initiating order")
@XmlElement(name = "initiatorAmount")
private BigDecimal price;
@Schema(description = "when trade happened")
private long timestamp;
// Constructors
// necessary for JAX-RS serialization
protected TradeData() {
}
public TradeData(byte[] initiator, byte[] target, BigDecimal amount, BigDecimal price, long timestamp) {
this.initiator = initiator;
this.target = target;

View File

@ -1,3 +1,4 @@
import controller.Controller;
import data.block.BlockData;
import qora.block.Block;
import qora.block.BlockChain;
@ -9,8 +10,6 @@ import repository.hsqldb.HSQLDBRepositoryFactory;
public class orphan {
public static final String connectionUrl = "jdbc:hsqldb:file:db/test;create=true";
public static void main(String[] args) {
if (args.length == 0) {
System.err.println("usage: orphan <new-blockchain-tip-height>");
@ -20,7 +19,7 @@ public class orphan {
int targetHeight = Integer.parseInt(args[0]);
try {
RepositoryFactory repositoryFactory = new HSQLDBRepositoryFactory(connectionUrl);
RepositoryFactory repositoryFactory = new HSQLDBRepositoryFactory(Controller.connectionUrl);
RepositoryManager.setRepositoryFactory(repositoryFactory);
} catch (DataException e) {
System.err.println("Couldn't connect to repository: " + e.getMessage());

View File

@ -61,7 +61,8 @@ public class AT {
this.atStateData = new ATStateData(atAddress, height, creation, stateData, stateHash, BigDecimal.ZERO.setScale(8));
} else {
// Legacy v1 AT
// We deploy these in 'dead' state as they will never be run on Qora2
// We would deploy these in 'dead' state as they will never be run on Qora2
// but this breaks import from Qora1 so something else will have to mark them dead at hard-fork
// Extract code bytes length
ByteBuffer byteBuffer = ByteBuffer.wrap(deployATTransactionData.getCreationBytes());
@ -89,10 +90,10 @@ public class AT {
byte[] codeBytes = new byte[codeLen];
byteBuffer.get(codeBytes);
// Create AT but in dead state
// Create AT
boolean isSleeping = false;
Integer sleepUntilHeight = null;
boolean isFinished = true;
boolean isFinished = false;
boolean hadFatalError = false;
boolean isFrozen = false;
Long frozenBalance = null;

View File

@ -9,6 +9,7 @@ import java.util.ArrayList;
import java.util.Arrays;
import java.util.List;
import java.util.Map;
import java.util.stream.Collectors;
import org.apache.logging.log4j.LogManager;
import org.apache.logging.log4j.Logger;
@ -887,6 +888,7 @@ public class Block {
this.repository.getBlockRepository().save(this.blockData);
// Link transactions to this block, thus removing them from unconfirmed transactions list.
// Also update "transaction participants" in repository for "transactions involving X" support in API
for (int sequence = 0; sequence < transactions.size(); ++sequence) {
Transaction transaction = transactions.get(sequence);
@ -897,6 +899,10 @@ public class Block {
// No longer unconfirmed
this.repository.getTransactionRepository().confirmTransaction(transaction.getTransactionData().getSignature());
List<Account> participants = transaction.getInvolvedAccounts();
List<String> participantAddresses = participants.stream().map(account -> account.getAddress()).collect(Collectors.toList());
this.repository.getTransactionRepository().saveParticipants(transaction.getTransactionData(), participantAddresses);
}
}
@ -918,6 +924,8 @@ public class Block {
BlockTransactionData blockTransactionData = new BlockTransactionData(this.getSignature(), sequence,
transaction.getTransactionData().getSignature());
this.repository.getBlockRepository().delete(blockTransactionData);
this.repository.getTransactionRepository().deleteParticipants(transaction.getTransactionData());
}
// If fees are non-zero then remove fees from generator's balance

View File

@ -1,6 +1,7 @@
package qora.transaction;
import java.math.BigDecimal;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collections;
import java.util.List;
@ -53,6 +54,14 @@ public class ATTransaction extends Transaction {
return Collections.singletonList(new Account(this.repository, this.atTransactionData.getRecipient()));
}
/** For AT-Transactions, the use the AT address instead of transaction creator (which is genesis account) */
@Override
public List<Account> getInvolvedAccounts() throws DataException {
List<Account> participants = new ArrayList<Account>(getRecipientAccounts());
participants.add(getATAccount());
return participants;
}
@Override
public boolean isInvolved(Account account) throws DataException {
String address = account.getAddress();

View File

@ -41,6 +41,12 @@ public class GenesisTransaction extends Transaction {
return Collections.singletonList(new Account(this.repository, genesisTransactionData.getRecipient()));
}
/** For Genesis Transactions, do not include transaction creator (which is genesis account) */
@Override
public List<Account> getInvolvedAccounts() throws DataException {
return getRecipientAccounts();
}
@Override
public boolean isInvolved(Account account) throws DataException {
String address = account.getAddress();

View File

@ -3,6 +3,7 @@ package qora.transaction;
import java.math.BigDecimal;
import java.math.BigInteger;
import java.math.MathContext;
import java.util.ArrayList;
import java.util.Comparator;
import java.util.List;
import java.util.Map;
@ -10,7 +11,6 @@ import java.util.Map;
import static java.util.Arrays.stream;
import static java.util.stream.Collectors.toMap;
import data.block.BlockData;
import data.transaction.TransactionData;
import qora.account.Account;
import qora.account.PrivateKeyAccount;
@ -314,6 +314,21 @@ public abstract class Transaction {
*/
public abstract List<Account> getRecipientAccounts() throws DataException;
/**
* Returns a list of involved accounts for this transaction.
* <p>
* "Involved" means sender or recipient.
*
* @return list of involved accounts, or empty list if none
* @throws DataException
*/
public List<Account> getInvolvedAccounts() throws DataException {
// Typically this is all the recipients plus the transaction creator/sender
List<Account> participants = new ArrayList<Account>(getRecipientAccounts());
participants.add(getCreator());
return participants;
}
/**
* Returns whether passed account is an involved party in this transaction.
* <p>
@ -349,17 +364,6 @@ public abstract class Transaction {
return new PublicKeyAccount(this.repository, this.transactionData.getCreatorPublicKey());
}
/**
* Load encapsulating block's data from repository, if any
*
* @return BlockData, or null if transaction is not in a Block
* @throws DataException
*/
@Deprecated
public BlockData getBlock() throws DataException {
return this.repository.getTransactionRepository().getBlockDataFromSignature(this.transactionData.getSignature());
}
/**
* Load parent's transaction data from repository via this transaction's reference.
*

View File

@ -2,7 +2,6 @@ package repository;
import java.util.List;
import data.account.AccountBalanceData;
import data.assets.AssetData;
import data.assets.OrderData;
import data.assets.TradeData;
@ -39,6 +38,8 @@ public interface AssetRepository {
// Trades
public List<TradeData> getTrades(long haveAssetId, long wantAssetId) throws DataException;
public List<TradeData> getOrdersTrades(byte[] orderId) throws DataException;
public void save(TradeData tradeData) throws DataException;

View File

@ -5,10 +5,10 @@ import qora.transaction.Transaction.TransactionType;
import java.util.List;
import data.block.BlockData;
public interface TransactionRepository {
// Fetching transactions / transaction height
public TransactionData fromSignature(byte[] signature) throws DataException;
public TransactionData fromReference(byte[] reference) throws DataException;
@ -18,11 +18,16 @@ public interface TransactionRepository {
/** Returns block height containing transaction or 0 if not in a block or transaction doesn't exist */
public int getHeightFromSignature(byte[] signature) throws DataException;
@Deprecated
public BlockData getBlockDataFromSignature(byte[] signature) throws DataException;
// Transaction participants
public List<byte[]> getAllSignaturesInvolvingAddress(String address) throws DataException;
public void saveParticipants(TransactionData transactionData, List<String> participants) throws DataException;
public void deleteParticipants(TransactionData transactionData) throws DataException;
// Searching transactions
public List<byte[]> getAllSignaturesMatchingCriteria(Integer startBlock, Integer blockLimit, TransactionType txType, String address) throws DataException;
/**

View File

@ -228,6 +228,34 @@ public class HSQLDBAssetRepository implements AssetRepository {
// Trades
@Override
public List<TradeData> getTrades(long haveAssetId, long wantAssetId) throws DataException {
List<TradeData> trades = new ArrayList<TradeData>();
try (ResultSet resultSet = this.repository.checkedExecute(
"SELECT initiating_order_id, target_order_id, AssetTrades.amount, AssetTrades.price, traded FROM AssetOrders JOIN AssetTrades ON initiating_order_id = asset_order_id "
+ "WHERE have_asset_id = ? AND want_asset_id = ? ORDER BY traded ASC",
haveAssetId, wantAssetId)) {
if (resultSet == null)
return trades;
do {
byte[] initiatingOrderId = resultSet.getBytes(1);
byte[] targetOrderId = resultSet.getBytes(2);
BigDecimal amount = resultSet.getBigDecimal(3);
BigDecimal price = resultSet.getBigDecimal(4);
long timestamp = resultSet.getTimestamp(5, Calendar.getInstance(HSQLDBRepository.UTC)).getTime();
TradeData trade = new TradeData(initiatingOrderId, targetOrderId, amount, price, timestamp);
trades.add(trade);
} while (resultSet.next());
return trades;
} catch (SQLException e) {
throw new DataException("Unable to fetch asset trades from repository", e);
}
}
@Override
public List<TradeData> getOrdersTrades(byte[] initiatingOrderId) throws DataException {
List<TradeData> trades = new ArrayList<TradeData>();

View File

@ -5,8 +5,6 @@ import java.sql.ResultSet;
import java.sql.SQLException;
import java.sql.Statement;
import qora.crypto.Crypto;
public class HSQLDBDatabaseUpdates {
/**
@ -75,8 +73,11 @@ public class HSQLDBDatabaseUpdates {
switch (databaseVersion) {
case 0:
// create from new
stmt.execute("SET DATABASE SQL NAMES TRUE"); // SQL keywords cannot be used as DB object names, e.g. table names
stmt.execute("SET DATABASE SQL SYNTAX MYS TRUE"); // Required for our use of INSERT ... ON DUPLICATE KEY UPDATE ... syntax
stmt.execute("SET DATABASE SQL RESTRICT EXEC TRUE"); // No multiple-statement execute() or DDL/DML executeQuery()
stmt.execute("SET DATABASE DEFAULT TABLE TYPE CACHED");
stmt.execute("SET DATABASE COLLATION SQL_TEXT NO PAD");
stmt.execute("SET DATABASE COLLATION SQL_TEXT NO PAD"); // Do not pad strings to same length before comparison
stmt.execute("CREATE COLLATION SQL_TEXT_UCC_NO_PAD FOR SQL_TEXT FROM SQL_TEXT_UCC NO PAD");
stmt.execute("CREATE COLLATION SQL_TEXT_NO_PAD FOR SQL_TEXT FROM SQL_TEXT NO PAD");
stmt.execute("SET FILES SPACE TRUE"); // Enable per-table block space within .data file, useful for CACHED table types
@ -151,13 +152,12 @@ public class HSQLDBDatabaseUpdates {
// Index to allow quick sorting by creation-else-signature
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, "
// Transaction participants
// To allow lookup of all activity by an address
stmt.execute("CREATE TABLE TransactionParticipants (signature Signature, participant QoraAddress NOT NULL, "
+ "FOREIGN KEY (signature) REFERENCES Transactions (signature) ON DELETE CASCADE)");
// Use a separate table space as this table will be very large.
stmt.execute("SET TABLE TransactionRecipients NEW SPACE");
stmt.execute("SET TABLE TransactionParticipants NEW SPACE");
break;
case 3:
@ -310,9 +310,11 @@ public class HSQLDBDatabaseUpdates {
case 22:
// Accounts
stmt.execute("CREATE TABLE Accounts (account QoraAddress, reference Signature, PRIMARY KEY (account))");
stmt.execute("CREATE TABLE Accounts (account QoraAddress, reference Signature, public_key QoraPublicKey, PRIMARY KEY (account))");
stmt.execute("CREATE TABLE AccountBalances (account QoraAddress, asset_id AssetID, balance QoraAmount NOT NULL, "
+ "PRIMARY KEY (account, asset_id), FOREIGN KEY (account) REFERENCES Accounts (account) ON DELETE CASCADE)");
// For looking up an account by public key
stmt.execute("CREATE INDEX AccountPublicKeyIndex on Accounts (public_key)");
break;
case 23:
@ -387,35 +389,6 @@ public class HSQLDBDatabaseUpdates {
stmt.execute("CREATE INDEX ATTransactionsIndex on ATTransactions (AT_address)");
break;
case 28:
// Associate public keys with accounts
stmt.execute("ALTER TABLE Accounts add public_key QoraPublicKey");
// For looking up an account by public key
stmt.execute("CREATE INDEX AccountPublicKeyIndex on Accounts (public_key)");
// Do not call close() on this as connection did not come from pool!
HSQLDBRepository repository = new HSQLDBRepository(connection);
try (ResultSet resultSet = repository.checkedExecute("SELECT DISTINCT creator from Transactions")) {
if (resultSet == null) {
repository = null;
break;
}
do {
byte[] publicKey = resultSet.getBytes(1);
String address = Crypto.toAddress(publicKey);
HSQLDBSaver saveHelper = new HSQLDBSaver("Accounts");
saveHelper.bind("account", address).bind("public_key", publicKey);
saveHelper.execute(repository);
} while (resultSet.next());
}
repository = null;
break;
default:
// nothing to do
return false;

View File

@ -31,10 +31,6 @@ public class HSQLDBRepositoryFactory implements RepositoryFactory {
Properties properties = new Properties();
properties.setProperty("close_result", "true"); // Auto-close old ResultSet if Statement creates new ResultSet
properties.setProperty("sql.strict_exec", "true"); // No multi-SQL execute() or DDL/DML executeQuery()
properties.setProperty("sql.enforce_names", "true"); // SQL keywords cannot be used as DB object names, e.g. table names
properties.setProperty("sql.syntax_mys", "true"); // Required for our use of INSERT ... ON DUPLICATE KEY UPDATE ... syntax
properties.setProperty("sql.pad_space", "false"); // Do not pad strings to same length before comparison
this.connectionPool.setProperties(properties);
// Perform DB updates?

View File

@ -7,12 +7,8 @@ import java.sql.Timestamp;
import java.util.ArrayList;
import java.util.Calendar;
import java.util.List;
import java.util.StringJoiner;
import com.google.common.base.Strings;
import data.PaymentData;
import data.block.BlockData;
import data.transaction.TransactionData;
import qora.transaction.Transaction.TransactionType;
import repository.DataException;
@ -249,31 +245,11 @@ public class HSQLDBTransactionRepository implements TransactionRepository {
}
}
@Override
public BlockData getBlockDataFromSignature(byte[] signature) throws DataException {
if (signature == null)
return null;
// Fetch block signature (if any)
try (ResultSet resultSet = this.repository.checkedExecute("SELECT block_signature FROM BlockTransactions WHERE transaction_signature = ? LIMIT 1",
signature)) {
if (resultSet == null)
return null;
byte[] blockSignature = resultSet.getBytes(1);
return this.repository.getBlockRepository().fromSignature(blockSignature);
} catch (SQLException | DataException e) {
throw new DataException("Unable to fetch transaction's block from repository", e);
}
}
@Override
public List<byte[]> getAllSignaturesInvolvingAddress(String address) throws DataException {
List<byte[]> signatures = new ArrayList<byte[]>();
// XXX We need a table for all parties involved in a transaction, not just recipients
try (ResultSet resultSet = this.repository.checkedExecute("SELECT signature FROM TransactionRecipients WHERE recipient = ?", address)) {
try (ResultSet resultSet = this.repository.checkedExecute("SELECT signature FROM TransactionRecipients WHERE participant = ?", address)) {
if (resultSet == null)
return signatures;
@ -289,6 +265,32 @@ public class HSQLDBTransactionRepository implements TransactionRepository {
}
}
@Override
public void saveParticipants(TransactionData transactionData, List<String> participants) throws DataException {
byte[] signature = transactionData.getSignature();
try {
for (String participant : participants) {
HSQLDBSaver saver = new HSQLDBSaver("TransactionParticipants");
saver.bind("signature", signature).bind("participant", participant);
saver.execute(this.repository);
}
} catch (SQLException e) {
throw new DataException("Unable to save transaction participant into repository", e);
}
}
@Override
public void deleteParticipants(TransactionData transactionData) throws DataException {
try {
this.repository.delete("TransactionParticipants", "signature = ?", transactionData.getSignature());
} catch (SQLException e) {
throw new DataException("Unable to delete transaction participants from repository", e);
}
}
@Override
public List<byte[]> getAllSignaturesMatchingCriteria(Integer startBlock, Integer blockLimit, TransactionType txType, String address) throws DataException {
List<byte[]> signatures = new ArrayList<byte[]>();
@ -323,13 +325,13 @@ public class HSQLDBTransactionRepository implements TransactionRepository {
if (hasAddress) {
if (hasTxType)
tableJoins.add("TransactionRecipients ON TransactionRecipients.signature = Transactions.signature");
tableJoins.add("TransactionParticipants ON TransactionParticipants.signature = Transactions.signature");
else if (hasHeightRange)
tableJoins.add("TransactionRecipients ON TransactionRecipients.signature = BlockTransactions.transaction_signature");
tableJoins.add("TransactionParticipants ON TransactionParticipants.signature = BlockTransactions.transaction_signature");
else
tableJoins.add("TransactionRecipients");
tableJoins.add("TransactionParticipants");
signatureColumn = "TransactionRecipients.signature";
signatureColumn = "TransactionParticipants.signature";
}
// WHERE clauses next
@ -346,14 +348,13 @@ public class HSQLDBTransactionRepository implements TransactionRepository {
whereClauses.add("Transactions.type = " + txType.value);
if (hasAddress) {
whereClauses.add("TransactionRecipients.recipient = ?");
whereClauses.add("TransactionParticipants.participant = ?");
bindParams.add(address);
}
String sql = "SELECT " + signatureColumn + " FROM " + String.join(" JOIN ", tableJoins) + " WHERE " + String.join(" AND ", whereClauses);
System.out.println("Transaction search SQL:\n" + sql);
// XXX We need a table for all parties involved in a transaction, not just recipients
try (ResultSet resultSet = this.repository.checkedExecute(sql, bindParams.toArray())) {
if (resultSet == null)
return signatures;

View File

@ -120,8 +120,7 @@ public class IssueAssetTransactionTransformer extends TransactionTransformer {
}
/**
* In Qora v1, the bytes used for verification have transaction type set to REGISTER_NAME_TRANSACTION so we need to test for v1-ness and adjust the bytes
* accordingly.
* In Qora v1, the bytes used for verification have asset's reference zeroed so we need to test for v1-ness and adjust the bytes accordingly.
*
* @param transactionData
* @return byte[]
@ -136,8 +135,8 @@ public class IssueAssetTransactionTransformer extends TransactionTransformer {
// Special v1 version
// Zero duplicate signature/reference
int start = bytes.length - SIGNATURE_LENGTH - BIG_DECIMAL_LENGTH;
int end = start + SIGNATURE_LENGTH;
int start = bytes.length - ASSET_REFERENCE_LENGTH - FEE_LENGTH; // before asset reference (and fee)
int end = start + ASSET_REFERENCE_LENGTH;
Arrays.fill(bytes, start, end, (byte) 0);
return bytes;

View File

@ -1,5 +1,6 @@
import com.google.common.hash.HashCode;
import controller.Controller;
import data.transaction.TransactionData;
import qora.block.BlockChain;
import repository.DataException;
@ -13,8 +14,6 @@ import utils.Base58;
public class txhex {
public static final String connectionUrl = "jdbc:hsqldb:file:db/test;create=true";
public static void main(String[] args) {
if (args.length == 0) {
System.err.println("usage: txhex <base58-tx-signature>");
@ -24,7 +23,7 @@ public class txhex {
byte[] signature = Base58.decode(args[0]);
try {
RepositoryFactory repositoryFactory = new HSQLDBRepositoryFactory(connectionUrl);
RepositoryFactory repositoryFactory = new HSQLDBRepositoryFactory(Controller.connectionUrl);
RepositoryManager.setRepositoryFactory(repositoryFactory);
} catch (DataException e) {
System.err.println("Couldn't connect to repository: " + e.getMessage());

View File

@ -31,6 +31,7 @@ import com.google.common.hash.HashCode;
import com.google.common.primitives.Bytes;
import com.google.common.primitives.Ints;
import controller.Controller;
import data.at.ATData;
import data.at.ATStateData;
import data.block.BlockData;
@ -56,7 +57,6 @@ import utils.Triple;
public class v1feeder extends Thread {
private static final Logger LOGGER = LogManager.getLogger(v1feeder.class);
public static final String connectionUrl = "jdbc:hsqldb:file:db/test;create=true";
private static final int INACTIVITY_TIMEOUT = 60 * 1000; // milliseconds
private static final int CONNECTION_TIMEOUT = 2 * 1000; // milliseconds
@ -529,7 +529,7 @@ public class v1feeder extends Thread {
readLegacyATs(legacyATPathname);
try {
RepositoryFactory repositoryFactory = new HSQLDBRepositoryFactory(connectionUrl);
RepositoryFactory repositoryFactory = new HSQLDBRepositoryFactory(Controller.connectionUrl);
RepositoryManager.setRepositoryFactory(repositoryFactory);
} catch (DataException e) {
LOGGER.error("Couldn't connect to repository", e);

View File

@ -1,6 +1,9 @@
package test;
import org.junit.jupiter.api.BeforeAll;
import controller.Controller;
import org.junit.jupiter.api.AfterAll;
import repository.DataException;
@ -10,12 +13,9 @@ import repository.hsqldb.HSQLDBRepositoryFactory;
public class Common {
// public static final String connectionUrl = "jdbc:hsqldb:file:db/test;create=true;close_result=true;sql.strict_exec=true;sql.enforce_names=true;sql.syntax_mys=true;sql.pad_space=false";
public static final String connectionUrl = "jdbc:hsqldb:file:db/test;create=true";
@BeforeAll
public static void setRepository() throws DataException {
RepositoryFactory repositoryFactory = new HSQLDBRepositoryFactory(connectionUrl);
RepositoryFactory repositoryFactory = new HSQLDBRepositoryFactory(Controller.connectionUrl);
RepositoryManager.setRepositoryFactory(repositoryFactory);
}

View File

@ -23,7 +23,7 @@ import repository.hsqldb.HSQLDBRepositoryFactory;
// Don't extend Common as we want an in-memory database
public class GenesisTests {
public static final String connectionUrl = "jdbc:hsqldb:mem:db/test;create=true;close_result=true;sql.strict_exec=true;sql.enforce_names=true;sql.syntax_mys=true";
public static final String connectionUrl = "jdbc:hsqldb:mem:db/blockchain;create=true";
@BeforeAll
public static void setRepository() throws DataException {

View File

@ -74,7 +74,7 @@ import settings.Settings;
// Don't extend Common as we want to use an in-memory database
public class TransactionTests {
private static final String connectionUrl = "jdbc:hsqldb:mem:db/test;create=true;close_result=true;sql.strict_exec=true;sql.enforce_names=true;sql.syntax_mys=true";
private static final String connectionUrl = "jdbc:hsqldb:mem:db/blockchain;create=true";
private static final byte[] generatorSeed = HashCode.fromString("0123456789abcdeffedcba98765432100123456789abcdeffedcba9876543210").asBytes();
private static final byte[] senderSeed = HashCode.fromString("0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef").asBytes();