diff --git a/src/api/ApiDefinition.java b/src/api/ApiDefinition.java index fbd8e3a2..31239faf 100644 --- a/src/api/ApiDefinition.java +++ b/src/api/ApiDefinition.java @@ -13,6 +13,8 @@ import io.swagger.v3.oas.annotations.tags.Tag; @Tag(name = "Admin"), @Tag(name = "Assets"), @Tag(name = "Blocks"), + @Tag(name = "Names"), + @Tag(name = "Payments"), @Tag(name = "Transactions"), @Tag(name = "Utilities") }, diff --git a/src/api/ApiError.java b/src/api/ApiError.java index a2b5ae2d..93c087c3 100644 --- a/src/api/ApiError.java +++ b/src/api/ApiError.java @@ -35,6 +35,7 @@ public enum ApiError { INVALID_NETWORK_ADDRESS(123, 404), ADDRESS_NO_EXISTS(124, 404), INVALID_CRITERIA(125, 400), + INVALID_REFERENCE(126, 400), //WALLET WALLET_NO_EXISTS(201, 404), diff --git a/src/api/ApiService.java b/src/api/ApiService.java index 71fa9850..05c9788d 100644 --- a/src/api/ApiService.java +++ b/src/api/ApiService.java @@ -31,6 +31,8 @@ public class ApiService { this.resources.add(AdminResource.class); this.resources.add(AssetsResource.class); this.resources.add(BlocksResource.class); + this.resources.add(NamesResource.class); + this.resources.add(PaymentsResource.class); this.resources.add(TransactionsResource.class); this.resources.add(UtilsResource.class); diff --git a/src/api/NamesResource.java b/src/api/NamesResource.java new file mode 100644 index 00000000..97200cec --- /dev/null +++ b/src/api/NamesResource.java @@ -0,0 +1,67 @@ +package api; + +import io.swagger.v3.oas.annotations.Operation; +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 transform.TransformationException; +import transform.transaction.RegisterNameTransactionTransformer; +import utils.Base58; + +import javax.servlet.http.HttpServletRequest; +import javax.ws.rs.POST; +import javax.ws.rs.Path; +import javax.ws.rs.Produces; +import javax.ws.rs.core.Context; +import javax.ws.rs.core.MediaType; + +import data.transaction.RegisterNameTransactionData; + +@Path("/names") +@Produces({ + MediaType.TEXT_PLAIN +}) +@Tag( + name = "Names" +) +public class NamesResource { + + @Context + HttpServletRequest request; + + @POST + @Path("/register") + @Operation( + summary = "Build raw, unsigned REGISTER_NAME transaction", + requestBody = @RequestBody( + required = true, + content = @Content( + mediaType = MediaType.APPLICATION_JSON, + schema = @Schema( + implementation = RegisterNameTransactionData.class + ) + ) + ), + responses = { + @ApiResponse( + description = "raw, unsigned REGISTER_NAME transaction encoded in Base58", + content = @Content( + schema = @Schema( + type = "string" + ) + ) + ) + } + ) + public String buildTransaction(RegisterNameTransactionData transactionData) { + try { + byte[] bytes = RegisterNameTransactionTransformer.toBytes(transactionData); + return Base58.encode(bytes); + } catch (TransformationException e) { + throw ApiErrorFactory.getInstance().createError(ApiError.UNKNOWN, e); + } + } + +} \ No newline at end of file diff --git a/src/api/PaymentsResource.java b/src/api/PaymentsResource.java new file mode 100644 index 00000000..2e0e5e4c --- /dev/null +++ b/src/api/PaymentsResource.java @@ -0,0 +1,67 @@ +package api; + +import io.swagger.v3.oas.annotations.Operation; +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 transform.TransformationException; +import transform.transaction.PaymentTransactionTransformer; +import utils.Base58; + +import javax.servlet.http.HttpServletRequest; +import javax.ws.rs.POST; +import javax.ws.rs.Path; +import javax.ws.rs.Produces; +import javax.ws.rs.core.Context; +import javax.ws.rs.core.MediaType; + +import data.transaction.PaymentTransactionData; + +@Path("/payments") +@Produces({ + MediaType.TEXT_PLAIN +}) +@Tag( + name = "Payments" +) +public class PaymentsResource { + + @Context + HttpServletRequest request; + + @POST + @Path("/pay") + @Operation( + summary = "Build raw, unsigned payment transaction", + requestBody = @RequestBody( + required = true, + content = @Content( + mediaType = MediaType.APPLICATION_JSON, + schema = @Schema( + implementation = PaymentTransactionData.class + ) + ) + ), + responses = { + @ApiResponse( + description = "raw, unsigned payment transaction encoded in Base58", + content = @Content( + schema = @Schema( + type = "string" + ) + ) + ) + } + ) + public String buildTransaction(PaymentTransactionData paymentTransactionData) { + try { + byte[] bytes = PaymentTransactionTransformer.toBytes(paymentTransactionData); + return Base58.encode(bytes); + } catch (TransformationException e) { + throw ApiErrorFactory.getInstance().createError(ApiError.UNKNOWN, e); + } + } + +} \ No newline at end of file diff --git a/src/api/TransactionsResource.java b/src/api/TransactionsResource.java index 06370c3c..ac0fe4ff 100644 --- a/src/api/TransactionsResource.java +++ b/src/api/TransactionsResource.java @@ -7,15 +7,20 @@ import io.swagger.v3.oas.annotations.extensions.ExtensionProperty; 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 qora.account.PrivateKeyAccount; +import qora.transaction.Transaction; import qora.transaction.Transaction.TransactionType; +import qora.transaction.Transaction.ValidationResult; 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; @@ -23,21 +28,37 @@ import javax.ws.rs.QueryParam; import javax.ws.rs.core.Context; import javax.ws.rs.core.MediaType; +import com.google.common.primitives.Bytes; + +import api.models.SimpleTransactionSignRequest; import data.transaction.GenesisTransactionData; import data.transaction.PaymentTransactionData; +import data.transaction.RegisterNameTransactionData; import data.transaction.TransactionData; import repository.DataException; import repository.Repository; import repository.RepositoryManager; +import transform.TransformationException; +import transform.transaction.RegisterNameTransactionTransformer; +import transform.transaction.TransactionTransformer; import utils.Base58; @Path("transactions") -@Produces({MediaType.APPLICATION_JSON, MediaType.TEXT_PLAIN}) -@Extension(name = "translation", properties = { - @ExtensionProperty(name="path", value="/Api/TransactionsResource") +@Produces({ + MediaType.APPLICATION_JSON, MediaType.TEXT_PLAIN +}) +@Extension( + name = "translation", + properties = { + @ExtensionProperty( + name = "path", + value = "/Api/TransactionsResource" + ) } ) -@Tag(name = "Transactions") +@Tag( + name = "Transactions" +) public class TransactionsResource { @Context @@ -49,18 +70,34 @@ public class TransactionsResource { summary = "Fetch transaction using transaction signature", description = "Returns transaction", extensions = { - @Extension(properties = { - @ExtensionProperty(name="apiErrors", value="[\"INVALID_SIGNATURE\", \"TRANSACTION_NO_EXISTS\"]", parseValue = true), - }) + @Extension( + properties = { + @ExtensionProperty( + name = "apiErrors", + value = "[\"INVALID_SIGNATURE\", \"TRANSACTION_NO_EXISTS\"]", + parseValue = true + ), + } + ) }, responses = { @ApiResponse( description = "a transaction", - content = @Content(schema = @Schema(implementation = TransactionData.class)), + content = @Content( + schema = @Schema( + implementation = TransactionData.class + ) + ), extensions = { - @Extension(name = "translation", properties = { - @ExtensionProperty(name="description.key", value="success_response:description") - }) + @Extension( + name = "translation", + properties = { + @ExtensionProperty( + name = "description.key", + value = "success_response:description" + ) + } + ) } ) } @@ -92,29 +129,58 @@ public class TransactionsResource { summary = "Fetch transactions using block signature", description = "Returns list of transactions", extensions = { - @Extension(name = "translation", properties = { - @ExtensionProperty(name="path", value="GET block:signature"), - @ExtensionProperty(name="description.key", value="operation:description") - }), - @Extension(properties = { - @ExtensionProperty(name="apiErrors", value="[\"INVALID_SIGNATURE\", \"BLOCK_NO_EXISTS\"]", parseValue = true), - }) + @Extension( + name = "translation", + properties = { + @ExtensionProperty( + name = "path", + value = "GET block:signature" + ), @ExtensionProperty( + name = "description.key", + value = "operation:description" + ) + } + ), @Extension( + properties = { + @ExtensionProperty( + name = "apiErrors", + value = "[\"INVALID_SIGNATURE\", \"BLOCK_NO_EXISTS\"]", + parseValue = true + ), + } + ) }, responses = { @ApiResponse( description = "list of transactions", - content = @Content(array = @ArraySchema(schema = @Schema( - oneOf = { GenesisTransactionData.class, PaymentTransactionData.class } - ))), + content = @Content( + array = @ArraySchema( + schema = @Schema( + oneOf = { + GenesisTransactionData.class, PaymentTransactionData.class + } + ) + ) + ), extensions = { - @Extension(name = "translation", properties = { - @ExtensionProperty(name="description.key", value="success_response:description") - }) + @Extension( + name = "translation", + properties = { + @ExtensionProperty( + name = "description.key", + value = "success_response:description" + ) + } + ) } ) } ) - public List getBlockTransactions(@PathParam("signature") String signature58, @Parameter(ref = "limit") @QueryParam("limit") int limit, @Parameter(ref = "offset") @QueryParam("offset") int offset) { + public List getBlockTransactions(@PathParam("signature") String signature58, @Parameter( + ref = "limit" + ) @QueryParam("limit") int limit, @Parameter( + ref = "offset" + ) @QueryParam("offset") int offset) { byte[] signature; try { signature = Base58.decode(signature58); @@ -126,7 +192,7 @@ public class TransactionsResource { List transactions = repository.getBlockRepository().getTransactionsFromSignature(signature); // check if block exists - if(transactions == null) + if (transactions == null) throw ApiErrorFactory.getInstance().createError(ApiError.BLOCK_NO_EXISTS); // Pagination would take effect here (or as part of the repository access) @@ -150,11 +216,23 @@ public class TransactionsResource { responses = { @ApiResponse( description = "transactions", - content = @Content(array = @ArraySchema(schema = @Schema(implementation = TransactionData.class))), + content = @Content( + array = @ArraySchema( + schema = @Schema( + implementation = TransactionData.class + ) + ) + ), extensions = { - @Extension(name = "translation", properties = { - @ExtensionProperty(name="description.key", value="success_response:description") - }) + @Extension( + name = "translation", + properties = { + @ExtensionProperty( + name = "description.key", + value = "success_response:description" + ) + } + ) } ) } @@ -175,27 +253,47 @@ public class TransactionsResource { summary = "Find matching transactions", description = "Returns transactions that match criteria. At least either txType or address must be provided.", /* - parameters = { - @Parameter(in = ParameterIn.QUERY, name = "txType", description = "Transaction type", schema = @Schema(type = "integer")), - @Parameter(in = ParameterIn.QUERY, name = "address", description = "Account's address", schema = @Schema(type = "string")), - @Parameter(in = ParameterIn.QUERY, name = "startBlock", description = "Start block height", schema = @Schema(type = "integer")), - @Parameter(in = ParameterIn.QUERY, name = "blockLimit", description = "Maximum number of blocks to search", schema = @Schema(type = "integer")) - }, - */ + * parameters = { + * + * @Parameter(in = ParameterIn.QUERY, name = "txType", description = "Transaction type", schema = @Schema(type = "integer")), + * + * @Parameter(in = ParameterIn.QUERY, name = "address", description = "Account's address", schema = @Schema(type = "string")), + * + * @Parameter(in = ParameterIn.QUERY, name = "startBlock", description = "Start block height", schema = @Schema(type = "integer")), + * + * @Parameter(in = ParameterIn.QUERY, name = "blockLimit", description = "Maximum number of blocks to search", schema = @Schema(type = "integer")) + * }, + */ responses = { @ApiResponse( description = "transactions", - content = @Content(array = @ArraySchema(schema = @Schema(implementation = TransactionData.class))), + content = @Content( + array = @ArraySchema( + schema = @Schema( + implementation = TransactionData.class + ) + ) + ), extensions = { - @Extension(name = "translation", properties = { - @ExtensionProperty(name="description.key", value="success_response:description") - }) + @Extension( + name = "translation", + properties = { + @ExtensionProperty( + name = "description.key", + value = "success_response:description" + ) + } + ) } ) } ) public List searchTransactions(@QueryParam("startBlock") Integer startBlock, @QueryParam("blockLimit") Integer blockLimit, - @QueryParam("txType") Integer txTypeNum, @QueryParam("address") String address, @Parameter(ref = "limit") @QueryParam("limit") int limit, @Parameter(ref = "offset") @QueryParam("offset") int offset) { + @QueryParam("txType") Integer txTypeNum, @QueryParam("address") String address, @Parameter( + ref = "limit" + ) @QueryParam("limit") int limit, @Parameter( + ref = "offset" + ) @QueryParam("offset") int offset) { if ((txTypeNum == null || txTypeNum == 0) && (address == null || address.isEmpty())) throw ApiErrorFactory.getInstance().createError(ApiError.INVALID_CRITERIA); @@ -207,7 +305,7 @@ public class TransactionsResource { } try (final Repository repository = RepositoryManager.getRepository()) { - List signatures = repository.getTransactionRepository().getAllSignaturesMatchingCriteria(startBlock, blockLimit, txType, address); + List signatures = repository.getTransactionRepository().getAllSignaturesMatchingCriteria(startBlock, blockLimit, txType, address); // Pagination would take effect here (or as part of the repository access) int fromIndex = Integer.min(offset, signatures.size()); @@ -227,4 +325,96 @@ public class TransactionsResource { } } + @POST + @Path("/sign") + @Operation( + summary = "Sign a raw, unsigned transaction", + requestBody = @RequestBody( + required = true, + content = @Content( + mediaType = MediaType.APPLICATION_JSON, + schema = @Schema( + implementation = SimpleTransactionSignRequest.class + ) + ) + ), + responses = { + @ApiResponse( + description = "raw, signed transaction encoded in Base58", + content = @Content( + mediaType = MediaType.TEXT_PLAIN, + schema = @Schema( + type = "string" + ) + ) + ) + } + ) + public String signTransaction(SimpleTransactionSignRequest signRequest) { + try { + // Append null signature on the end + byte[] rawBytes = Bytes.concat(signRequest.transactionBytes, new byte[TransactionTransformer.SIGNATURE_LENGTH]); + TransactionData transactionData = TransactionTransformer.fromBytes(rawBytes); + PrivateKeyAccount signer = new PrivateKeyAccount(null, signRequest.privateKey); + Transaction transaction = Transaction.fromData(null, transactionData); + transaction.sign(signer); + byte[] signedBytes = TransactionTransformer.toBytes(transactionData); + return Base58.encode(signedBytes); + } catch (TransformationException e) { + throw ApiErrorFactory.getInstance().createError(ApiError.UNKNOWN, e); + } + } + + @POST + @Path("/process") + @Operation( + summary = "Submit raw, signed transaction for processing and adding to blockchain", + requestBody = @RequestBody( + required = true, + content = @Content( + mediaType = MediaType.TEXT_PLAIN, + schema = @Schema( + type = "string", + description = "raw, signed transaction in base58 encoding" + ) + ) + ), + responses = { + @ApiResponse( + description = "true if accepted, false otherwise", + content = @Content( + mediaType = MediaType.TEXT_PLAIN, + schema = @Schema( + type = "string" + ) + ) + ) + } + ) + public String processTransaction(String rawBytes58) { + try (final Repository repository = RepositoryManager.getRepository()) { + byte[] rawBytes = Base58.decode(rawBytes58); + TransactionData transactionData = TransactionTransformer.fromBytes(rawBytes); + + Transaction transaction = Transaction.fromData(repository, transactionData); + if (!transaction.isSignatureValid()) + throw ApiErrorFactory.getInstance().createError(ApiError.INVALID_SIGNATURE); + + if (transaction.isValid() != ValidationResult.OK) + throw ApiErrorFactory.getInstance().createError(ApiError.INVALID_DATA); + + repository.getTransactionRepository().save(transactionData); + repository.getTransactionRepository().unconfirmTransaction(transactionData); + repository.saveChanges(); + + return "true"; + } catch (TransformationException e) { + throw ApiErrorFactory.getInstance().createError(ApiError.UNKNOWN, e); + } catch (ApiException e) { + throw e; + } catch (DataException e) { + throw ApiErrorFactory.getInstance().createError(ApiError.REPOSITORY_ISSUE, e); + } + } + } diff --git a/src/api/UtilsResource.java b/src/api/UtilsResource.java index 124b7294..224b0b0e 100644 --- a/src/api/UtilsResource.java +++ b/src/api/UtilsResource.java @@ -10,6 +10,7 @@ import qora.account.PrivateKeyAccount; import qora.crypto.Crypto; import utils.BIP39; import utils.Base58; +import utils.NTP; import java.security.SecureRandom; import java.util.Arrays; @@ -343,4 +344,23 @@ public class UtilsResource { return Base58.encode(publicKey); } + @GET + @Path("/timestamp") + @Operation( + summary = "Returns current timestamp as milliseconds from unix epoch", + responses = { + @ApiResponse( + content = @Content( + mediaType = MediaType.TEXT_PLAIN, + schema = @Schema( + type = "number" + ) + ) + ) + } + ) + public long getTimestamp() { + return NTP.getTime(); + } + } \ No newline at end of file diff --git a/src/api/models/SimpleTransactionSignRequest.java b/src/api/models/SimpleTransactionSignRequest.java new file mode 100644 index 00000000..81b014cb --- /dev/null +++ b/src/api/models/SimpleTransactionSignRequest.java @@ -0,0 +1,21 @@ +package api.models; + +import javax.xml.bind.annotation.XmlAccessType; +import javax.xml.bind.annotation.XmlAccessorType; + +import io.swagger.v3.oas.annotations.media.Schema; + +@XmlAccessorType(XmlAccessType.FIELD) +public class SimpleTransactionSignRequest { + + @Schema( + description = "signer's private key" + ) + public byte[] privateKey; + + @Schema( + description = "raw, unsigned transaction bytes" + ) + public byte[] transactionBytes; + +} diff --git a/src/data/transaction/RegisterNameTransactionData.java b/src/data/transaction/RegisterNameTransactionData.java index 6d7b31f9..4af0ee65 100644 --- a/src/data/transaction/RegisterNameTransactionData.java +++ b/src/data/transaction/RegisterNameTransactionData.java @@ -14,15 +14,20 @@ import qora.transaction.Transaction.TransactionType; public class RegisterNameTransactionData extends TransactionData { // Properties + @Schema(description = "registrant's public key", example = "2tiMr5LTpaWCgbRvkPK8TFd7k63DyHJMMFFsz9uBf1ZP") private byte[] registrantPublicKey; + @Schema(description = "new owner's address", example = "Qj2Stco8ziE3ZQN2AdpWCmkBFfYjuz8fGu") private String owner; + @Schema(description = "requested name", example = "my-name") private String name; + @Schema(description = "simple name-related info in JSON format", example = "{ \"age\": 30 }") private String data; // Constructors // For JAX-RS protected RegisterNameTransactionData() { + super(TransactionType.REGISTER_NAME); } public RegisterNameTransactionData(byte[] registrantPublicKey, String owner, String name, String data, BigDecimal fee, long timestamp, byte[] reference, diff --git a/src/data/transaction/TransactionData.java b/src/data/transaction/TransactionData.java index 2b0b2cc4..f8a62394 100644 --- a/src/data/transaction/TransactionData.java +++ b/src/data/transaction/TransactionData.java @@ -13,6 +13,8 @@ import javax.xml.bind.annotation.XmlTransient; import org.eclipse.persistence.oxm.annotations.XmlClassExtractor; import api.TransactionClassExtractor; +import io.swagger.v3.oas.annotations.media.Schema; +import io.swagger.v3.oas.annotations.media.Schema.AccessMode; import qora.crypto.Crypto; import qora.transaction.Transaction.TransactionType; @@ -34,12 +36,18 @@ import qora.transaction.Transaction.TransactionType; public abstract class TransactionData { // Properties shared with all transaction types + @Schema(accessMode = AccessMode.READ_ONLY, hidden = true) protected TransactionType type; @XmlTransient // represented in transaction-specific properties + @Schema(hidden = true) protected byte[] creatorPublicKey; + @Schema(description = "timestamp when transaction created, in milliseconds since unix epoch", example = "1545062012000") protected long timestamp; + @Schema(description = "sender's last transaction ID", example = "47fw82McxnTQ8wtTS5A51Qojhg62b8px1rF3FhJp5a3etKeb5Y2DniL4Q6E7GbVCs6BAjHVe6sA4gTPxtYzng3AX") protected byte[] reference; + @Schema(description = "fee for processing transaction", example = "1.0") protected BigDecimal fee; + @Schema(accessMode = AccessMode.READ_ONLY, description = "signature for transaction's raw bytes, using sender's private key", example = "28u54WRcMfGujtQMZ9dNKFXVqucY7XfPihXAqPFsnx853NPUwfDJy1sMH5boCkahFgjUNYqc5fkduxdBhQTKgUsC") protected byte[] signature; // Constructors @@ -48,6 +56,11 @@ public abstract class TransactionData { protected TransactionData() { } + // For JAX-RS + protected TransactionData(TransactionType type) { + this.type = type; + } + public TransactionData(TransactionType type, BigDecimal fee, byte[] creatorPublicKey, long timestamp, byte[] reference, byte[] signature) { this.fee = fee; this.type = type; diff --git a/src/qora/block/Block.java b/src/qora/block/Block.java index f670851d..570eb612 100644 --- a/src/qora/block/Block.java +++ b/src/qora/block/Block.java @@ -697,8 +697,9 @@ public class Block { return ValidationResult.TIMESTAMP_MS_INCORRECT; // Too early to forge block? - if (this.blockData.getTimestamp() < parentBlock.getBlockData().getTimestamp() + BlockChain.getInstance().getMinBlockTime()) - return ValidationResult.TIMESTAMP_TOO_SOON; + // XXX DISABLED + // if (this.blockData.getTimestamp() < parentBlock.getBlockData().getTimestamp() + BlockChain.getInstance().getMinBlockTime()) + // return ValidationResult.TIMESTAMP_TOO_SOON; // Check block version if (this.blockData.getVersion() != parentBlock.getNextBlockVersion()) diff --git a/src/qora/block/BlockGenerator.java b/src/qora/block/BlockGenerator.java index cdd54d2a..233158a1 100644 --- a/src/qora/block/BlockGenerator.java +++ b/src/qora/block/BlockGenerator.java @@ -10,6 +10,7 @@ import data.block.BlockData; import data.transaction.TransactionData; import qora.account.PrivateKeyAccount; import qora.block.Block.ValidationResult; +import qora.transaction.Transaction; import repository.BlockRepository; import repository.DataException; import repository.Repository; @@ -43,6 +44,8 @@ public class BlockGenerator extends Thread { // Main thread loop @Override public void run() { + Thread.currentThread().setName("BlockGenerator"); + try (final Repository repository = RepositoryManager.getRepository()) { generator = new PrivateKeyAccount(repository, generatorPrivateKey); @@ -106,6 +109,11 @@ public class BlockGenerator extends Thread { // Grab all unconfirmed transactions (already sorted) List unconfirmedTransactions = repository.getTransactionRepository().getAllUnconfirmedTransactions(); + // Remove transactions that have timestamp later than block's timestamp (not yet valid) + unconfirmedTransactions.removeIf(transactionData -> transactionData.getTimestamp() > newBlock.getBlockData().getTimestamp()); + // Remove transactions that have expired deadline for this block + unconfirmedTransactions.removeIf(transactionData -> Transaction.fromData(repository, transactionData).getDeadline() <= newBlock.getBlockData().getTimestamp()); + // Attempt to add transactions until block is full, or we run out for (TransactionData transactionData : unconfirmedTransactions) if (!newBlock.addTransaction(transactionData)) @@ -115,7 +123,7 @@ public class BlockGenerator extends Thread { public void shutdown() { this.running = false; // Interrupt too, absorbed by HSQLDB but could be caught by Thread.sleep() - Thread.currentThread().interrupt(); + this.interrupt(); } } diff --git a/src/repository/TransactionRepository.java b/src/repository/TransactionRepository.java index 80fb16f1..3503a7eb 100644 --- a/src/repository/TransactionRepository.java +++ b/src/repository/TransactionRepository.java @@ -46,6 +46,8 @@ public interface TransactionRepository { */ public void confirmTransaction(byte[] signature) throws DataException; + void unconfirmTransaction(TransactionData transactionData) throws DataException; + public void save(TransactionData transactionData) throws DataException; public void delete(TransactionData transactionData) throws DataException; diff --git a/src/repository/hsqldb/transaction/HSQLDBTransactionRepository.java b/src/repository/hsqldb/transaction/HSQLDBTransactionRepository.java index d4c5e55f..8dc0f8b9 100644 --- a/src/repository/hsqldb/transaction/HSQLDBTransactionRepository.java +++ b/src/repository/hsqldb/transaction/HSQLDBTransactionRepository.java @@ -407,6 +407,17 @@ public class HSQLDBTransactionRepository implements TransactionRepository { } } + @Override + public void unconfirmTransaction(TransactionData transactionData) throws DataException { + HSQLDBSaver saver = new HSQLDBSaver("UnconfirmedTransactions"); + saver.bind("signature", transactionData.getSignature()).bind("creation", new Timestamp(transactionData.getTimestamp())); + try { + saver.execute(repository); + } catch (SQLException e) { + throw new DataException("Unable to add transaction to unconfirmed transactions repository", e); + } + } + @Override public void save(TransactionData transactionData) throws DataException { HSQLDBSaver saver = new HSQLDBSaver("Transactions");