diff --git a/src/api/AddressesResource.java b/src/api/AddressesResource.java index 18f28334..f14d5480 100644 --- a/src/api/AddressesResource.java +++ b/src/api/AddressesResource.java @@ -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); diff --git a/src/api/AdminResource.java b/src/api/AdminResource.java index 2b802e0d..e701b3fb 100644 --- a/src/api/AdminResource.java +++ b/src/api/AdminResource.java @@ -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 ""; } diff --git a/src/api/AnnotationPostProcessor.java b/src/api/AnnotationPostProcessor.java index 57a052cc..4149f41b 100644 --- a/src/api/AnnotationPostProcessor.java +++ b/src/api/AnnotationPostProcessor.java @@ -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 diff --git a/src/api/AssetsResource.java b/src/api/AssetsResource.java index a670cb89..9f7cf111 100644 --- a/src/api/AssetsResource.java +++ b/src/api/AssetsResource.java @@ -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 getAllAssets() { + public List 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 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 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 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}.
" + + "Does NOT include trades of {otherAssetId} for {assetId}!
" + + "\"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 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 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 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 ""; + } + } diff --git a/src/api/TransactionsResource.java b/src/api/TransactionsResource.java index cb420212..1b022b1c 100644 --- a/src/api/TransactionsResource.java +++ b/src/api/TransactionsResource.java @@ -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; diff --git a/src/api/models/AssetWithHolders.java b/src/api/models/AssetWithHolders.java index 662b51b2..d3e0978f 100644 --- a/src/api/models/AssetWithHolders.java +++ b/src/api/models/AssetWithHolders.java @@ -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; diff --git a/src/api/models/IssueAssetRequest.java b/src/api/models/IssueAssetRequest.java new file mode 100644 index 00000000..1065dec1 --- /dev/null +++ b/src/api/models/IssueAssetRequest.java @@ -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; + +} diff --git a/src/api/models/TradeWithOrderInfo.java b/src/api/models/TradeWithOrderInfo.java new file mode 100644 index 00000000..51484352 --- /dev/null +++ b/src/api/models/TradeWithOrderInfo.java @@ -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()); + } + +} diff --git a/src/blockgenerator.java b/src/blockgenerator.java index f76c42d6..746ad8f3 100644 --- a/src/blockgenerator.java +++ b/src/blockgenerator.java @@ -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) { diff --git a/src/controller/Controller.java b/src/controller/Controller.java index 460e7368..7fe15d21 100644 --- a/src/controller/Controller.java +++ b/src/controller/Controller.java @@ -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(); diff --git a/src/data/assets/AssetData.java b/src/data/assets/AssetData.java index a6c16df3..547570eb 100644 --- a/src/data/assets/AssetData.java +++ b/src/data/assets/AssetData.java @@ -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() { } diff --git a/src/data/assets/OrderData.java b/src/data/assets/OrderData.java index c1d0e1de..26856386 100644 --- a/src/data/assets/OrderData.java +++ b/src/data/assets/OrderData.java @@ -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 { + // 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 { this(orderId, creatorPublicKey, haveAssetId, wantAssetId, amount, BigDecimal.ZERO.setScale(8), price, timestamp, false, false); } + // Getters/setters + public byte[] getOrderId() { return this.orderId; } diff --git a/src/data/assets/TradeData.java b/src/data/assets/TradeData.java index 5a14b9a4..cf69297e 100644 --- a/src/data/assets/TradeData.java +++ b/src/data/assets/TradeData.java @@ -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; diff --git a/src/orphan.java b/src/orphan.java index 33a49c94..8f548471 100644 --- a/src/orphan.java +++ b/src/orphan.java @@ -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 "); @@ -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()); diff --git a/src/qora/at/AT.java b/src/qora/at/AT.java index 232acaec..06903b7f 100644 --- a/src/qora/at/AT.java +++ b/src/qora/at/AT.java @@ -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; diff --git a/src/qora/block/Block.java b/src/qora/block/Block.java index 917e52dc..8cdb4ede 100644 --- a/src/qora/block/Block.java +++ b/src/qora/block/Block.java @@ -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 participants = transaction.getInvolvedAccounts(); + List 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 diff --git a/src/qora/transaction/ATTransaction.java b/src/qora/transaction/ATTransaction.java index c750f9e0..59d694e2 100644 --- a/src/qora/transaction/ATTransaction.java +++ b/src/qora/transaction/ATTransaction.java @@ -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 getInvolvedAccounts() throws DataException { + List participants = new ArrayList(getRecipientAccounts()); + participants.add(getATAccount()); + return participants; + } + @Override public boolean isInvolved(Account account) throws DataException { String address = account.getAddress(); diff --git a/src/qora/transaction/GenesisTransaction.java b/src/qora/transaction/GenesisTransaction.java index 3a1a83e9..fc5701e4 100644 --- a/src/qora/transaction/GenesisTransaction.java +++ b/src/qora/transaction/GenesisTransaction.java @@ -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 getInvolvedAccounts() throws DataException { + return getRecipientAccounts(); + } + @Override public boolean isInvolved(Account account) throws DataException { String address = account.getAddress(); diff --git a/src/qora/transaction/Transaction.java b/src/qora/transaction/Transaction.java index 9eaaeead..808739b9 100644 --- a/src/qora/transaction/Transaction.java +++ b/src/qora/transaction/Transaction.java @@ -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 getRecipientAccounts() throws DataException; + /** + * Returns a list of involved accounts for this transaction. + *

+ * "Involved" means sender or recipient. + * + * @return list of involved accounts, or empty list if none + * @throws DataException + */ + public List getInvolvedAccounts() throws DataException { + // Typically this is all the recipients plus the transaction creator/sender + List participants = new ArrayList(getRecipientAccounts()); + participants.add(getCreator()); + return participants; + } + /** * Returns whether passed account is an involved party in this transaction. *

@@ -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. * diff --git a/src/repository/AssetRepository.java b/src/repository/AssetRepository.java index 5bd5caed..224c3484 100644 --- a/src/repository/AssetRepository.java +++ b/src/repository/AssetRepository.java @@ -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 getTrades(long haveAssetId, long wantAssetId) throws DataException; + public List getOrdersTrades(byte[] orderId) throws DataException; public void save(TradeData tradeData) throws DataException; diff --git a/src/repository/TransactionRepository.java b/src/repository/TransactionRepository.java index c6a36e8c..80fb16f1 100644 --- a/src/repository/TransactionRepository.java +++ b/src/repository/TransactionRepository.java @@ -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 getAllSignaturesInvolvingAddress(String address) throws DataException; + public void saveParticipants(TransactionData transactionData, List participants) throws DataException; + + public void deleteParticipants(TransactionData transactionData) throws DataException; + + // Searching transactions + public List getAllSignaturesMatchingCriteria(Integer startBlock, Integer blockLimit, TransactionType txType, String address) throws DataException; /** diff --git a/src/repository/hsqldb/HSQLDBAssetRepository.java b/src/repository/hsqldb/HSQLDBAssetRepository.java index 741816fa..e1cd9279 100644 --- a/src/repository/hsqldb/HSQLDBAssetRepository.java +++ b/src/repository/hsqldb/HSQLDBAssetRepository.java @@ -228,6 +228,34 @@ public class HSQLDBAssetRepository implements AssetRepository { // Trades + @Override + public List getTrades(long haveAssetId, long wantAssetId) throws DataException { + List trades = new ArrayList(); + + 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 getOrdersTrades(byte[] initiatingOrderId) throws DataException { List trades = new ArrayList(); diff --git a/src/repository/hsqldb/HSQLDBDatabaseUpdates.java b/src/repository/hsqldb/HSQLDBDatabaseUpdates.java index 0dd8fc58..adb879b5 100644 --- a/src/repository/hsqldb/HSQLDBDatabaseUpdates.java +++ b/src/repository/hsqldb/HSQLDBDatabaseUpdates.java @@ -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; diff --git a/src/repository/hsqldb/HSQLDBRepositoryFactory.java b/src/repository/hsqldb/HSQLDBRepositoryFactory.java index ed35a067..60bfe8ae 100644 --- a/src/repository/hsqldb/HSQLDBRepositoryFactory.java +++ b/src/repository/hsqldb/HSQLDBRepositoryFactory.java @@ -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? diff --git a/src/repository/hsqldb/transaction/HSQLDBTransactionRepository.java b/src/repository/hsqldb/transaction/HSQLDBTransactionRepository.java index b2d2818e..d4c5e55f 100644 --- a/src/repository/hsqldb/transaction/HSQLDBTransactionRepository.java +++ b/src/repository/hsqldb/transaction/HSQLDBTransactionRepository.java @@ -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 getAllSignaturesInvolvingAddress(String address) throws DataException { List signatures = new ArrayList(); - // 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 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 getAllSignaturesMatchingCriteria(Integer startBlock, Integer blockLimit, TransactionType txType, String address) throws DataException { List signatures = new ArrayList(); @@ -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; diff --git a/src/transform/transaction/IssueAssetTransactionTransformer.java b/src/transform/transaction/IssueAssetTransactionTransformer.java index 43a53171..65b23e90 100644 --- a/src/transform/transaction/IssueAssetTransactionTransformer.java +++ b/src/transform/transaction/IssueAssetTransactionTransformer.java @@ -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; diff --git a/src/txhex.java b/src/txhex.java index 6dea671d..7d482eda 100644 --- a/src/txhex.java +++ b/src/txhex.java @@ -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 "); @@ -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()); diff --git a/src/v1feeder.java b/src/v1feeder.java index 1946ebe4..fc9649d4 100644 --- a/src/v1feeder.java +++ b/src/v1feeder.java @@ -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); diff --git a/tests/test/Common.java b/tests/test/Common.java index 9e196f2b..2cfa1fdc 100644 --- a/tests/test/Common.java +++ b/tests/test/Common.java @@ -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); } diff --git a/tests/test/GenesisTests.java b/tests/test/GenesisTests.java index 504c6c93..84b56f51 100644 --- a/tests/test/GenesisTests.java +++ b/tests/test/GenesisTests.java @@ -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 { diff --git a/tests/test/TransactionTests.java b/tests/test/TransactionTests.java index 25922aea..d230f16e 100644 --- a/tests/test/TransactionTests.java +++ b/tests/test/TransactionTests.java @@ -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();