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:
parent
963e4c5d35
commit
107ef93b37
@ -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")
|
||||||
},
|
},
|
||||||
|
@ -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),
|
||||||
|
@ -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);
|
||||||
|
|
||||||
|
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.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);
|
||||||
@ -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);
|
||||||
|
|
||||||
@ -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 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();
|
||||||
|
}
|
||||||
|
|
||||||
}
|
}
|
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 {
|
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,
|
||||||
|
@ -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;
|
||||||
|
@ -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())
|
||||||
|
@ -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();
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
@ -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;
|
||||||
|
@ -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");
|
||||||
|
Loading…
x
Reference in New Issue
Block a user