mirror of
https://github.com/Qortal/qortal.git
synced 2025-02-11 17:55:50 +00:00
added API support for signing and processing transactions
This commit is contained in:
parent
963e4c5d35
commit
107ef93b37
@ -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")
|
||||
},
|
||||
|
@ -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),
|
||||
|
@ -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);
|
||||
|
||||
|
67
src/api/NamesResource.java
Normal file
67
src/api/NamesResource.java
Normal 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);
|
||||
}
|
||||
}
|
||||
|
||||
}
|
67
src/api/PaymentsResource.java
Normal file
67
src/api/PaymentsResource.java
Normal 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);
|
||||
}
|
||||
}
|
||||
|
||||
}
|
@ -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<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;
|
||||
try {
|
||||
signature = Base58.decode(signature58);
|
||||
@ -126,7 +192,7 @@ public class TransactionsResource {
|
||||
List<TransactionData> 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<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()))
|
||||
throw ApiErrorFactory.getInstance().createError(ApiError.INVALID_CRITERIA);
|
||||
|
||||
@ -207,7 +305,7 @@ public class TransactionsResource {
|
||||
}
|
||||
|
||||
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)
|
||||
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);
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
@ -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();
|
||||
}
|
||||
|
||||
}
|
21
src/api/models/SimpleTransactionSignRequest.java
Normal file
21
src/api/models/SimpleTransactionSignRequest.java
Normal 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;
|
||||
|
||||
}
|
@ -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,
|
||||
|
@ -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;
|
||||
|
@ -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())
|
||||
|
@ -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<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
|
||||
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();
|
||||
}
|
||||
|
||||
}
|
||||
|
@ -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;
|
||||
|
@ -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");
|
||||
|
Loading…
x
Reference in New Issue
Block a user