mirror of
https://github.com/Qortal/qortal.git
synced 2025-02-18 21:25:47 +00:00
Added "X-API-VERSION" header support in POST /transactions/process.
Default is version "1". If version "2" is specified, the API will return the full transaction JSON on success, rather than just "true". Example usage: curl -X POST "http://localhost:12391/transactions/process" -H "X-API-VERSION: 2" -d "signedTransactionBytesHere"
This commit is contained in:
parent
c5a0b00cde
commit
c310a7c5e8
@ -3,6 +3,7 @@ package org.qortal.api;
|
|||||||
import java.io.IOException;
|
import java.io.IOException;
|
||||||
import java.io.InputStream;
|
import java.io.InputStream;
|
||||||
import java.io.UnsupportedEncodingException;
|
import java.io.UnsupportedEncodingException;
|
||||||
|
import java.io.Writer;
|
||||||
import java.net.HttpURLConnection;
|
import java.net.HttpURLConnection;
|
||||||
import java.net.InetSocketAddress;
|
import java.net.InetSocketAddress;
|
||||||
import java.net.Socket;
|
import java.net.Socket;
|
||||||
@ -20,14 +21,12 @@ import javax.net.ssl.SNIHostName;
|
|||||||
import javax.net.ssl.SNIServerName;
|
import javax.net.ssl.SNIServerName;
|
||||||
import javax.net.ssl.SSLParameters;
|
import javax.net.ssl.SSLParameters;
|
||||||
import javax.net.ssl.SSLSocket;
|
import javax.net.ssl.SSLSocket;
|
||||||
import javax.xml.bind.JAXBContext;
|
import javax.xml.bind.*;
|
||||||
import javax.xml.bind.JAXBException;
|
|
||||||
import javax.xml.bind.UnmarshalException;
|
|
||||||
import javax.xml.bind.Unmarshaller;
|
|
||||||
import javax.xml.transform.stream.StreamSource;
|
import javax.xml.transform.stream.StreamSource;
|
||||||
|
|
||||||
import org.eclipse.persistence.exceptions.XMLMarshalException;
|
import org.eclipse.persistence.exceptions.XMLMarshalException;
|
||||||
import org.eclipse.persistence.jaxb.JAXBContextFactory;
|
import org.eclipse.persistence.jaxb.JAXBContextFactory;
|
||||||
|
import org.eclipse.persistence.jaxb.MarshallerProperties;
|
||||||
import org.eclipse.persistence.jaxb.UnmarshallerProperties;
|
import org.eclipse.persistence.jaxb.UnmarshallerProperties;
|
||||||
|
|
||||||
public class ApiRequest {
|
public class ApiRequest {
|
||||||
@ -107,6 +106,36 @@ public class ApiRequest {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private static Marshaller createMarshaller(Class<?> objectClass) {
|
||||||
|
try {
|
||||||
|
// Create JAXB context aware of object's class
|
||||||
|
JAXBContext jc = JAXBContextFactory.createContext(new Class[] { objectClass }, null);
|
||||||
|
|
||||||
|
// Create marshaller
|
||||||
|
Marshaller marshaller = jc.createMarshaller();
|
||||||
|
|
||||||
|
// Set the marshaller media type to JSON
|
||||||
|
marshaller.setProperty(MarshallerProperties.MEDIA_TYPE, "application/json");
|
||||||
|
|
||||||
|
// Tell marshaller not to include JSON root element in the output
|
||||||
|
marshaller.setProperty(MarshallerProperties.JSON_INCLUDE_ROOT, false);
|
||||||
|
|
||||||
|
return marshaller;
|
||||||
|
} catch (JAXBException e) {
|
||||||
|
throw new RuntimeException("Unable to create websocket marshaller", e);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public static void marshall(Writer writer, Object object) throws IOException {
|
||||||
|
Marshaller marshaller = createMarshaller(object.getClass());
|
||||||
|
|
||||||
|
try {
|
||||||
|
marshaller.marshal(object, writer);
|
||||||
|
} catch (JAXBException e) {
|
||||||
|
throw new IOException("Unable to create marshall object for websocket", e);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
public static String getParamsString(Map<String, String> params) {
|
public static String getParamsString(Map<String, String> params) {
|
||||||
StringBuilder result = new StringBuilder();
|
StringBuilder result = new StringBuilder();
|
||||||
|
|
||||||
|
@ -13,6 +13,7 @@ import java.security.SecureRandom;
|
|||||||
|
|
||||||
import javax.net.ssl.KeyManagerFactory;
|
import javax.net.ssl.KeyManagerFactory;
|
||||||
import javax.net.ssl.SSLContext;
|
import javax.net.ssl.SSLContext;
|
||||||
|
import javax.servlet.http.HttpServletRequest;
|
||||||
|
|
||||||
import org.eclipse.jetty.http.HttpVersion;
|
import org.eclipse.jetty.http.HttpVersion;
|
||||||
import org.eclipse.jetty.rewrite.handler.RedirectPatternRule;
|
import org.eclipse.jetty.rewrite.handler.RedirectPatternRule;
|
||||||
@ -50,6 +51,8 @@ public class ApiService {
|
|||||||
private Server server;
|
private Server server;
|
||||||
private ApiKey apiKey;
|
private ApiKey apiKey;
|
||||||
|
|
||||||
|
public static final String API_VERSION_HEADER = "X-API-VERSION";
|
||||||
|
|
||||||
private ApiService() {
|
private ApiService() {
|
||||||
this.config = new ResourceConfig();
|
this.config = new ResourceConfig();
|
||||||
this.config.packages("org.qortal.api.resource", "org.qortal.api.restricted.resource");
|
this.config.packages("org.qortal.api.resource", "org.qortal.api.restricted.resource");
|
||||||
@ -229,4 +232,19 @@ public class ApiService {
|
|||||||
this.server = null;
|
this.server = null;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public static int getApiVersion(HttpServletRequest request) {
|
||||||
|
// Get API version
|
||||||
|
String apiVersionString = request.getHeader(API_VERSION_HEADER);
|
||||||
|
if (apiVersionString == null) {
|
||||||
|
// Try query string - this is needed to avoid a CORS preflight. See: https://stackoverflow.com/a/43881141
|
||||||
|
apiVersionString = request.getParameter("apiVersion");
|
||||||
|
}
|
||||||
|
|
||||||
|
int apiVersion = 1;
|
||||||
|
if (apiVersionString != null) {
|
||||||
|
apiVersion = Integer.parseInt(apiVersionString);
|
||||||
|
}
|
||||||
|
return apiVersion;
|
||||||
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
@ -9,6 +9,8 @@ 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 java.io.IOException;
|
||||||
|
import java.io.StringWriter;
|
||||||
import java.lang.reflect.Constructor;
|
import java.lang.reflect.Constructor;
|
||||||
import java.lang.reflect.InvocationTargetException;
|
import java.lang.reflect.InvocationTargetException;
|
||||||
import java.util.ArrayList;
|
import java.util.ArrayList;
|
||||||
@ -18,19 +20,12 @@ import java.util.concurrent.TimeUnit;
|
|||||||
import java.util.concurrent.locks.ReentrantLock;
|
import java.util.concurrent.locks.ReentrantLock;
|
||||||
|
|
||||||
import javax.servlet.http.HttpServletRequest;
|
import javax.servlet.http.HttpServletRequest;
|
||||||
import javax.ws.rs.GET;
|
import javax.ws.rs.*;
|
||||||
import javax.ws.rs.POST;
|
|
||||||
import javax.ws.rs.Path;
|
|
||||||
import javax.ws.rs.PathParam;
|
|
||||||
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 org.qortal.account.PrivateKeyAccount;
|
import org.qortal.account.PrivateKeyAccount;
|
||||||
import org.qortal.api.ApiError;
|
import org.qortal.api.*;
|
||||||
import org.qortal.api.ApiErrors;
|
|
||||||
import org.qortal.api.ApiException;
|
|
||||||
import org.qortal.api.ApiExceptionFactory;
|
|
||||||
import org.qortal.api.model.SimpleTransactionSignRequest;
|
import org.qortal.api.model.SimpleTransactionSignRequest;
|
||||||
import org.qortal.controller.Controller;
|
import org.qortal.controller.Controller;
|
||||||
import org.qortal.controller.LiteNode;
|
import org.qortal.controller.LiteNode;
|
||||||
@ -709,7 +704,7 @@ public class TransactionsResource {
|
|||||||
),
|
),
|
||||||
responses = {
|
responses = {
|
||||||
@ApiResponse(
|
@ApiResponse(
|
||||||
description = "true if accepted, false otherwise",
|
description = "For API version 1, this returns true if accepted.\nFor API version 2, the transactionData is returned as a JSON string if accepted.",
|
||||||
content = @Content(
|
content = @Content(
|
||||||
mediaType = MediaType.TEXT_PLAIN,
|
mediaType = MediaType.TEXT_PLAIN,
|
||||||
schema = @Schema(
|
schema = @Schema(
|
||||||
@ -722,7 +717,9 @@ public class TransactionsResource {
|
|||||||
@ApiErrors({
|
@ApiErrors({
|
||||||
ApiError.BLOCKCHAIN_NEEDS_SYNC, ApiError.INVALID_SIGNATURE, ApiError.INVALID_DATA, ApiError.TRANSFORMATION_ERROR, ApiError.REPOSITORY_ISSUE
|
ApiError.BLOCKCHAIN_NEEDS_SYNC, ApiError.INVALID_SIGNATURE, ApiError.INVALID_DATA, ApiError.TRANSFORMATION_ERROR, ApiError.REPOSITORY_ISSUE
|
||||||
})
|
})
|
||||||
public String processTransaction(String rawBytes58) {
|
public String processTransaction(String rawBytes58, @HeaderParam(ApiService.API_VERSION_HEADER) String apiVersionHeader) {
|
||||||
|
int apiVersion = ApiService.getApiVersion(request);
|
||||||
|
|
||||||
// Only allow a transaction to be processed if our latest block is less than 60 minutes old
|
// Only allow a transaction to be processed if our latest block is less than 60 minutes old
|
||||||
// If older than this, we should first wait until the blockchain is synced
|
// If older than this, we should first wait until the blockchain is synced
|
||||||
final Long minLatestBlockTimestamp = NTP.getTime() - (60 * 60 * 1000L);
|
final Long minLatestBlockTimestamp = NTP.getTime() - (60 * 60 * 1000L);
|
||||||
@ -759,13 +756,27 @@ public class TransactionsResource {
|
|||||||
blockchainLock.unlock();
|
blockchainLock.unlock();
|
||||||
}
|
}
|
||||||
|
|
||||||
return "true";
|
switch (apiVersion) {
|
||||||
|
case 1:
|
||||||
|
return "true";
|
||||||
|
|
||||||
|
case 2:
|
||||||
|
default:
|
||||||
|
// Marshall transactionData to string
|
||||||
|
StringWriter stringWriter = new StringWriter();
|
||||||
|
ApiRequest.marshall(stringWriter, transactionData);
|
||||||
|
return stringWriter.toString();
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
} catch (NumberFormatException e) {
|
} catch (NumberFormatException e) {
|
||||||
throw ApiExceptionFactory.INSTANCE.createException(request, ApiError.INVALID_DATA, e);
|
throw ApiExceptionFactory.INSTANCE.createException(request, ApiError.INVALID_DATA, e);
|
||||||
} catch (DataException e) {
|
} catch (DataException e) {
|
||||||
throw ApiExceptionFactory.INSTANCE.createException(request, ApiError.REPOSITORY_ISSUE, e);
|
throw ApiExceptionFactory.INSTANCE.createException(request, ApiError.REPOSITORY_ISSUE, e);
|
||||||
} catch (InterruptedException e) {
|
} catch (InterruptedException e) {
|
||||||
throw createTransactionInvalidException(request, ValidationResult.NO_BLOCKCHAIN_LOCK);
|
throw createTransactionInvalidException(request, ValidationResult.NO_BLOCKCHAIN_LOCK);
|
||||||
|
} catch (IOException e) {
|
||||||
|
throw ApiExceptionFactory.INSTANCE.createException(request, ApiError.TRANSFORMATION_ERROR, e);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
Loading…
x
Reference in New Issue
Block a user