3
0
mirror of https://github.com/Qortal/qortal.git synced 2025-02-12 02:05:50 +00:00

added API support for signing and processing transactions

This commit is contained in:
catbref 2018-12-17 17:22:05 +00:00
parent 963e4c5d35
commit 107ef93b37
14 changed files with 456 additions and 46 deletions

View File

@ -13,6 +13,8 @@ import io.swagger.v3.oas.annotations.tags.Tag;
@Tag(name = "Admin"), @Tag(name = "Admin"),
@Tag(name = "Assets"), @Tag(name = "Assets"),
@Tag(name = "Blocks"), @Tag(name = "Blocks"),
@Tag(name = "Names"),
@Tag(name = "Payments"),
@Tag(name = "Transactions"), @Tag(name = "Transactions"),
@Tag(name = "Utilities") @Tag(name = "Utilities")
}, },

View File

@ -35,6 +35,7 @@ public enum ApiError {
INVALID_NETWORK_ADDRESS(123, 404), INVALID_NETWORK_ADDRESS(123, 404),
ADDRESS_NO_EXISTS(124, 404), ADDRESS_NO_EXISTS(124, 404),
INVALID_CRITERIA(125, 400), INVALID_CRITERIA(125, 400),
INVALID_REFERENCE(126, 400),
//WALLET //WALLET
WALLET_NO_EXISTS(201, 404), WALLET_NO_EXISTS(201, 404),

View File

@ -31,6 +31,8 @@ public class ApiService {
this.resources.add(AdminResource.class); this.resources.add(AdminResource.class);
this.resources.add(AssetsResource.class); this.resources.add(AssetsResource.class);
this.resources.add(BlocksResource.class); this.resources.add(BlocksResource.class);
this.resources.add(NamesResource.class);
this.resources.add(PaymentsResource.class);
this.resources.add(TransactionsResource.class); this.resources.add(TransactionsResource.class);
this.resources.add(UtilsResource.class); this.resources.add(UtilsResource.class);

View File

@ -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);
}
}
}

View File

@ -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);
}
}
}

View File

@ -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.ArraySchema;
import io.swagger.v3.oas.annotations.media.Content; import io.swagger.v3.oas.annotations.media.Content;
import io.swagger.v3.oas.annotations.media.Schema; 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.responses.ApiResponse;
import io.swagger.v3.oas.annotations.tags.Tag; 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.TransactionType;
import qora.transaction.Transaction.ValidationResult;
import java.util.ArrayList; import java.util.ArrayList;
import java.util.List; import java.util.List;
import javax.servlet.http.HttpServletRequest; import javax.servlet.http.HttpServletRequest;
import javax.ws.rs.GET; import javax.ws.rs.GET;
import javax.ws.rs.POST;
import javax.ws.rs.Path; import javax.ws.rs.Path;
import javax.ws.rs.PathParam; import javax.ws.rs.PathParam;
import javax.ws.rs.Produces; 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.Context;
import javax.ws.rs.core.MediaType; import javax.ws.rs.core.MediaType;
import com.google.common.primitives.Bytes;
import api.models.SimpleTransactionSignRequest;
import data.transaction.GenesisTransactionData; import data.transaction.GenesisTransactionData;
import data.transaction.PaymentTransactionData; import data.transaction.PaymentTransactionData;
import data.transaction.RegisterNameTransactionData;
import data.transaction.TransactionData; import data.transaction.TransactionData;
import repository.DataException; import repository.DataException;
import repository.Repository; import repository.Repository;
import repository.RepositoryManager; import repository.RepositoryManager;
import transform.TransformationException;
import transform.transaction.RegisterNameTransactionTransformer;
import transform.transaction.TransactionTransformer;
import utils.Base58; import utils.Base58;
@Path("transactions") @Path("transactions")
@Produces({MediaType.APPLICATION_JSON, MediaType.TEXT_PLAIN}) @Produces({
@Extension(name = "translation", properties = { MediaType.APPLICATION_JSON, MediaType.TEXT_PLAIN
@ExtensionProperty(name="path", value="/Api/TransactionsResource") })
@Extension(
name = "translation",
properties = {
@ExtensionProperty(
name = "path",
value = "/Api/TransactionsResource"
)
} }
) )
@Tag(name = "Transactions") @Tag(
name = "Transactions"
)
public class TransactionsResource { public class TransactionsResource {
@Context @Context
@ -49,18 +70,34 @@ public class TransactionsResource {
summary = "Fetch transaction using transaction signature", summary = "Fetch transaction using transaction signature",
description = "Returns transaction", description = "Returns transaction",
extensions = { extensions = {
@Extension(properties = { @Extension(
@ExtensionProperty(name="apiErrors", value="[\"INVALID_SIGNATURE\", \"TRANSACTION_NO_EXISTS\"]", parseValue = true), properties = {
}) @ExtensionProperty(
name = "apiErrors",
value = "[\"INVALID_SIGNATURE\", \"TRANSACTION_NO_EXISTS\"]",
parseValue = true
),
}
)
}, },
responses = { responses = {
@ApiResponse( @ApiResponse(
description = "a transaction", description = "a transaction",
content = @Content(schema = @Schema(implementation = TransactionData.class)), content = @Content(
schema = @Schema(
implementation = TransactionData.class
)
),
extensions = { extensions = {
@Extension(name = "translation", properties = { @Extension(
@ExtensionProperty(name="description.key", value="success_response:description") name = "translation",
}) properties = {
@ExtensionProperty(
name = "description.key",
value = "success_response:description"
)
}
)
} }
) )
} }
@ -92,29 +129,58 @@ public class TransactionsResource {
summary = "Fetch transactions using block signature", summary = "Fetch transactions using block signature",
description = "Returns list of transactions", description = "Returns list of transactions",
extensions = { extensions = {
@Extension(name = "translation", properties = { @Extension(
@ExtensionProperty(name="path", value="GET block:signature"), name = "translation",
@ExtensionProperty(name="description.key", value="operation:description") properties = {
}), @ExtensionProperty(
@Extension(properties = { name = "path",
@ExtensionProperty(name="apiErrors", value="[\"INVALID_SIGNATURE\", \"BLOCK_NO_EXISTS\"]", parseValue = true), 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 = { responses = {
@ApiResponse( @ApiResponse(
description = "list of transactions", description = "list of transactions",
content = @Content(array = @ArraySchema(schema = @Schema( content = @Content(
oneOf = { GenesisTransactionData.class, PaymentTransactionData.class } array = @ArraySchema(
))), schema = @Schema(
oneOf = {
GenesisTransactionData.class, PaymentTransactionData.class
}
)
)
),
extensions = { extensions = {
@Extension(name = "translation", properties = { @Extension(
@ExtensionProperty(name="description.key", value="success_response:description") name = "translation",
}) properties = {
@ExtensionProperty(
name = "description.key",
value = "success_response:description"
)
}
)
} }
) )
} }
) )
public List<TransactionData> getBlockTransactions(@PathParam("signature") String signature58, @Parameter(ref = "limit") @QueryParam("limit") int limit, @Parameter(ref = "offset") @QueryParam("offset") int offset) { public List<TransactionData> getBlockTransactions(@PathParam("signature") String signature58, @Parameter(
ref = "limit"
) @QueryParam("limit") int limit, @Parameter(
ref = "offset"
) @QueryParam("offset") int offset) {
byte[] signature; byte[] signature;
try { try {
signature = Base58.decode(signature58); signature = Base58.decode(signature58);
@ -126,7 +192,7 @@ public class TransactionsResource {
List<TransactionData> transactions = repository.getBlockRepository().getTransactionsFromSignature(signature); List<TransactionData> transactions = repository.getBlockRepository().getTransactionsFromSignature(signature);
// check if block exists // check if block exists
if(transactions == null) if (transactions == null)
throw ApiErrorFactory.getInstance().createError(ApiError.BLOCK_NO_EXISTS); throw ApiErrorFactory.getInstance().createError(ApiError.BLOCK_NO_EXISTS);
// Pagination would take effect here (or as part of the repository access) // Pagination would take effect here (or as part of the repository access)
@ -150,11 +216,23 @@ public class TransactionsResource {
responses = { responses = {
@ApiResponse( @ApiResponse(
description = "transactions", description = "transactions",
content = @Content(array = @ArraySchema(schema = @Schema(implementation = TransactionData.class))), content = @Content(
array = @ArraySchema(
schema = @Schema(
implementation = TransactionData.class
)
)
),
extensions = { extensions = {
@Extension(name = "translation", properties = { @Extension(
@ExtensionProperty(name="description.key", value="success_response:description") name = "translation",
}) properties = {
@ExtensionProperty(
name = "description.key",
value = "success_response:description"
)
}
)
} }
) )
} }
@ -175,27 +253,47 @@ public class TransactionsResource {
summary = "Find matching transactions", summary = "Find matching transactions",
description = "Returns transactions that match criteria. At least either txType or address must be provided.", description = "Returns transactions that match criteria. At least either txType or address must be provided.",
/* /*
parameters = { * 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 = "txType", description = "Transaction type", schema = @Schema(type = "integer")),
@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")) * @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 = { responses = {
@ApiResponse( @ApiResponse(
description = "transactions", description = "transactions",
content = @Content(array = @ArraySchema(schema = @Schema(implementation = TransactionData.class))), content = @Content(
array = @ArraySchema(
schema = @Schema(
implementation = TransactionData.class
)
)
),
extensions = { extensions = {
@Extension(name = "translation", properties = { @Extension(
@ExtensionProperty(name="description.key", value="success_response:description") name = "translation",
}) properties = {
@ExtensionProperty(
name = "description.key",
value = "success_response:description"
)
}
)
} }
) )
} }
) )
public List<TransactionData> searchTransactions(@QueryParam("startBlock") Integer startBlock, @QueryParam("blockLimit") Integer blockLimit, public List<TransactionData> 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())) if ((txTypeNum == null || txTypeNum == 0) && (address == null || address.isEmpty()))
throw ApiErrorFactory.getInstance().createError(ApiError.INVALID_CRITERIA); throw ApiErrorFactory.getInstance().createError(ApiError.INVALID_CRITERIA);
@ -207,7 +305,7 @@ public class TransactionsResource {
} }
try (final Repository repository = RepositoryManager.getRepository()) { try (final Repository repository = RepositoryManager.getRepository()) {
List<byte[]> signatures = repository.getTransactionRepository().getAllSignaturesMatchingCriteria(startBlock, blockLimit, txType, address); List<byte[]> signatures = repository.getTransactionRepository().getAllSignaturesMatchingCriteria(startBlock, blockLimit, txType, address);
// Pagination would take effect here (or as part of the repository access) // Pagination would take effect here (or as part of the repository access)
int fromIndex = Integer.min(offset, signatures.size()); 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);
}
}
} }

View File

@ -10,6 +10,7 @@ import qora.account.PrivateKeyAccount;
import qora.crypto.Crypto; import qora.crypto.Crypto;
import utils.BIP39; import utils.BIP39;
import utils.Base58; import utils.Base58;
import utils.NTP;
import java.security.SecureRandom; import java.security.SecureRandom;
import java.util.Arrays; import java.util.Arrays;
@ -343,4 +344,23 @@ public class UtilsResource {
return Base58.encode(publicKey); 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();
}
} }

View File

@ -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;
}

View File

@ -14,15 +14,20 @@ import qora.transaction.Transaction.TransactionType;
public class RegisterNameTransactionData extends TransactionData { public class RegisterNameTransactionData extends TransactionData {
// Properties // Properties
@Schema(description = "registrant's public key", example = "2tiMr5LTpaWCgbRvkPK8TFd7k63DyHJMMFFsz9uBf1ZP")
private byte[] registrantPublicKey; private byte[] registrantPublicKey;
@Schema(description = "new owner's address", example = "Qj2Stco8ziE3ZQN2AdpWCmkBFfYjuz8fGu")
private String owner; private String owner;
@Schema(description = "requested name", example = "my-name")
private String name; private String name;
@Schema(description = "simple name-related info in JSON format", example = "{ \"age\": 30 }")
private String data; private String data;
// Constructors // Constructors
// For JAX-RS // For JAX-RS
protected RegisterNameTransactionData() { protected RegisterNameTransactionData() {
super(TransactionType.REGISTER_NAME);
} }
public RegisterNameTransactionData(byte[] registrantPublicKey, String owner, String name, String data, BigDecimal fee, long timestamp, byte[] reference, public RegisterNameTransactionData(byte[] registrantPublicKey, String owner, String name, String data, BigDecimal fee, long timestamp, byte[] reference,

View File

@ -13,6 +13,8 @@ import javax.xml.bind.annotation.XmlTransient;
import org.eclipse.persistence.oxm.annotations.XmlClassExtractor; import org.eclipse.persistence.oxm.annotations.XmlClassExtractor;
import api.TransactionClassExtractor; 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.crypto.Crypto;
import qora.transaction.Transaction.TransactionType; import qora.transaction.Transaction.TransactionType;
@ -34,12 +36,18 @@ import qora.transaction.Transaction.TransactionType;
public abstract class TransactionData { public abstract class TransactionData {
// Properties shared with all transaction types // Properties shared with all transaction types
@Schema(accessMode = AccessMode.READ_ONLY, hidden = true)
protected TransactionType type; protected TransactionType type;
@XmlTransient // represented in transaction-specific properties @XmlTransient // represented in transaction-specific properties
@Schema(hidden = true)
protected byte[] creatorPublicKey; protected byte[] creatorPublicKey;
@Schema(description = "timestamp when transaction created, in milliseconds since unix epoch", example = "1545062012000")
protected long timestamp; protected long timestamp;
@Schema(description = "sender's last transaction ID", example = "47fw82McxnTQ8wtTS5A51Qojhg62b8px1rF3FhJp5a3etKeb5Y2DniL4Q6E7GbVCs6BAjHVe6sA4gTPxtYzng3AX")
protected byte[] reference; protected byte[] reference;
@Schema(description = "fee for processing transaction", example = "1.0")
protected BigDecimal fee; 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; protected byte[] signature;
// Constructors // Constructors
@ -48,6 +56,11 @@ public abstract class TransactionData {
protected 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) { public TransactionData(TransactionType type, BigDecimal fee, byte[] creatorPublicKey, long timestamp, byte[] reference, byte[] signature) {
this.fee = fee; this.fee = fee;
this.type = type; this.type = type;

View File

@ -697,8 +697,9 @@ public class Block {
return ValidationResult.TIMESTAMP_MS_INCORRECT; return ValidationResult.TIMESTAMP_MS_INCORRECT;
// Too early to forge block? // Too early to forge block?
if (this.blockData.getTimestamp() < parentBlock.getBlockData().getTimestamp() + BlockChain.getInstance().getMinBlockTime()) // XXX DISABLED
return ValidationResult.TIMESTAMP_TOO_SOON; // if (this.blockData.getTimestamp() < parentBlock.getBlockData().getTimestamp() + BlockChain.getInstance().getMinBlockTime())
// return ValidationResult.TIMESTAMP_TOO_SOON;
// Check block version // Check block version
if (this.blockData.getVersion() != parentBlock.getNextBlockVersion()) if (this.blockData.getVersion() != parentBlock.getNextBlockVersion())

View File

@ -10,6 +10,7 @@ import data.block.BlockData;
import data.transaction.TransactionData; import data.transaction.TransactionData;
import qora.account.PrivateKeyAccount; import qora.account.PrivateKeyAccount;
import qora.block.Block.ValidationResult; import qora.block.Block.ValidationResult;
import qora.transaction.Transaction;
import repository.BlockRepository; import repository.BlockRepository;
import repository.DataException; import repository.DataException;
import repository.Repository; import repository.Repository;
@ -43,6 +44,8 @@ public class BlockGenerator extends Thread {
// Main thread loop // Main thread loop
@Override @Override
public void run() { public void run() {
Thread.currentThread().setName("BlockGenerator");
try (final Repository repository = RepositoryManager.getRepository()) { try (final Repository repository = RepositoryManager.getRepository()) {
generator = new PrivateKeyAccount(repository, generatorPrivateKey); generator = new PrivateKeyAccount(repository, generatorPrivateKey);
@ -106,6 +109,11 @@ public class BlockGenerator extends Thread {
// Grab all unconfirmed transactions (already sorted) // Grab all unconfirmed transactions (already sorted)
List<TransactionData> unconfirmedTransactions = repository.getTransactionRepository().getAllUnconfirmedTransactions(); List<TransactionData> 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 // Attempt to add transactions until block is full, or we run out
for (TransactionData transactionData : unconfirmedTransactions) for (TransactionData transactionData : unconfirmedTransactions)
if (!newBlock.addTransaction(transactionData)) if (!newBlock.addTransaction(transactionData))
@ -115,7 +123,7 @@ public class BlockGenerator extends Thread {
public void shutdown() { public void shutdown() {
this.running = false; this.running = false;
// Interrupt too, absorbed by HSQLDB but could be caught by Thread.sleep() // Interrupt too, absorbed by HSQLDB but could be caught by Thread.sleep()
Thread.currentThread().interrupt(); this.interrupt();
} }
} }

View File

@ -46,6 +46,8 @@ public interface TransactionRepository {
*/ */
public void confirmTransaction(byte[] signature) throws DataException; public void confirmTransaction(byte[] signature) throws DataException;
void unconfirmTransaction(TransactionData transactionData) throws DataException;
public void save(TransactionData transactionData) throws DataException; public void save(TransactionData transactionData) throws DataException;
public void delete(TransactionData transactionData) throws DataException; public void delete(TransactionData transactionData) throws DataException;

View File

@ -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 @Override
public void save(TransactionData transactionData) throws DataException { public void save(TransactionData transactionData) throws DataException {
HSQLDBSaver saver = new HSQLDBSaver("Transactions"); HSQLDBSaver saver = new HSQLDBSaver("Transactions");