diff --git a/Q-Apps.md b/Q-Apps.md
index de97200d..1b4f33bb 100644
--- a/Q-Apps.md
+++ b/Q-Apps.md
@@ -583,14 +583,15 @@ let res = await qortalRequest({
```
### Send foreign coin to address
-_Requires user approval_
+_Requires user approval_
+Note: default fees can be found [here](https://github.com/Qortal/qortal-ui/blob/master/plugins/plugins/core/qdn/browser/browser.src.js#L205-L209).
```
let res = await qortalRequest({
action: "SEND_COIN",
coin: "LTC",
destinationAddress: "LSdTvMHRm8sScqwCi6x9wzYQae8JeZhx6y",
amount: 1.00000000, // 1 LTC
- fee: 0.00000020 // fee per byte
+ fee: 0.00000020 // Optional fee per byte (default fee used if omitted, recommended) - not used for QORT or ARRR
});
```
diff --git a/pom.xml b/pom.xml
index c9986fd4..7c7ac147 100644
--- a/pom.xml
+++ b/pom.xml
@@ -3,7 +3,7 @@
4.0.0
org.qortal
qortal
- 4.1.2
+ 4.1.3
jar
true
diff --git a/src/main/java/org/qortal/api/ApiService.java b/src/main/java/org/qortal/api/ApiService.java
index 059b8971..1ee733c6 100644
--- a/src/main/java/org/qortal/api/ApiService.java
+++ b/src/main/java/org/qortal/api/ApiService.java
@@ -96,7 +96,7 @@ public class ApiService {
throw new RuntimeException("Failed to start SSL API due to broken keystore");
// BouncyCastle-specific SSLContext build
- SSLContext sslContext = SSLContext.getInstance("TLS", "BCJSSE");
+ SSLContext sslContext = SSLContext.getInstance("TLSv1.3", "BCJSSE");
KeyManagerFactory keyManagerFactory = KeyManagerFactory.getInstance("PKIX", "BCJSSE");
KeyStore keyStore = KeyStore.getInstance(KeyStore.getDefaultType(), "BC");
diff --git a/src/main/java/org/qortal/api/DevProxyService.java b/src/main/java/org/qortal/api/DevProxyService.java
new file mode 100644
index 00000000..e0bf02db
--- /dev/null
+++ b/src/main/java/org/qortal/api/DevProxyService.java
@@ -0,0 +1,173 @@
+package org.qortal.api;
+
+import io.swagger.v3.jaxrs2.integration.resources.OpenApiResource;
+import org.eclipse.jetty.http.HttpVersion;
+import org.eclipse.jetty.rewrite.handler.RewriteHandler;
+import org.eclipse.jetty.server.*;
+import org.eclipse.jetty.server.handler.ErrorHandler;
+import org.eclipse.jetty.server.handler.InetAccessHandler;
+import org.eclipse.jetty.servlet.FilterHolder;
+import org.eclipse.jetty.servlet.ServletContextHandler;
+import org.eclipse.jetty.servlet.ServletHolder;
+import org.eclipse.jetty.servlets.CrossOriginFilter;
+import org.eclipse.jetty.util.ssl.SslContextFactory;
+import org.glassfish.jersey.server.ResourceConfig;
+import org.glassfish.jersey.servlet.ServletContainer;
+import org.qortal.api.resource.AnnotationPostProcessor;
+import org.qortal.api.resource.ApiDefinition;
+import org.qortal.network.Network;
+import org.qortal.repository.DataException;
+import org.qortal.settings.Settings;
+
+import javax.net.ssl.KeyManagerFactory;
+import javax.net.ssl.SSLContext;
+import java.io.InputStream;
+import java.net.InetAddress;
+import java.net.InetSocketAddress;
+import java.nio.file.Files;
+import java.nio.file.Path;
+import java.nio.file.Paths;
+import java.security.KeyStore;
+import java.security.SecureRandom;
+
+public class DevProxyService {
+
+ private static DevProxyService instance;
+
+ private final ResourceConfig config;
+ private Server server;
+
+ private DevProxyService() {
+ this.config = new ResourceConfig();
+ this.config.packages("org.qortal.api.proxy.resource", "org.qortal.api.resource");
+ this.config.register(OpenApiResource.class);
+ this.config.register(ApiDefinition.class);
+ this.config.register(AnnotationPostProcessor.class);
+ }
+
+ public static DevProxyService getInstance() {
+ if (instance == null)
+ instance = new DevProxyService();
+
+ return instance;
+ }
+
+ public Iterable> getResources() {
+ return this.config.getClasses();
+ }
+
+ public void start() throws DataException {
+ try {
+ // Create API server
+
+ // SSL support if requested
+ String keystorePathname = Settings.getInstance().getSslKeystorePathname();
+ String keystorePassword = Settings.getInstance().getSslKeystorePassword();
+
+ if (keystorePathname != null && keystorePassword != null) {
+ // SSL version
+ if (!Files.isReadable(Path.of(keystorePathname)))
+ throw new RuntimeException("Failed to start SSL API due to broken keystore");
+
+ // BouncyCastle-specific SSLContext build
+ SSLContext sslContext = SSLContext.getInstance("TLS", "BCJSSE");
+ KeyManagerFactory keyManagerFactory = KeyManagerFactory.getInstance("PKIX", "BCJSSE");
+
+ KeyStore keyStore = KeyStore.getInstance(KeyStore.getDefaultType(), "BC");
+
+ try (InputStream keystoreStream = Files.newInputStream(Paths.get(keystorePathname))) {
+ keyStore.load(keystoreStream, keystorePassword.toCharArray());
+ }
+
+ keyManagerFactory.init(keyStore, keystorePassword.toCharArray());
+ sslContext.init(keyManagerFactory.getKeyManagers(), null, new SecureRandom());
+
+ SslContextFactory.Server sslContextFactory = new SslContextFactory.Server();
+ sslContextFactory.setSslContext(sslContext);
+
+ this.server = new Server();
+
+ HttpConfiguration httpConfig = new HttpConfiguration();
+ httpConfig.setSecureScheme("https");
+ httpConfig.setSecurePort(Settings.getInstance().getDevProxyPort());
+
+ SecureRequestCustomizer src = new SecureRequestCustomizer();
+ httpConfig.addCustomizer(src);
+
+ HttpConnectionFactory httpConnectionFactory = new HttpConnectionFactory(httpConfig);
+ SslConnectionFactory sslConnectionFactory = new SslConnectionFactory(sslContextFactory, HttpVersion.HTTP_1_1.asString());
+
+ ServerConnector portUnifiedConnector = new ServerConnector(this.server,
+ new DetectorConnectionFactory(sslConnectionFactory),
+ httpConnectionFactory);
+ portUnifiedConnector.setHost(Network.getInstance().getBindAddress());
+ portUnifiedConnector.setPort(Settings.getInstance().getDevProxyPort());
+
+ this.server.addConnector(portUnifiedConnector);
+ } else {
+ // Non-SSL
+ InetAddress bindAddr = InetAddress.getByName(Network.getInstance().getBindAddress());
+ InetSocketAddress endpoint = new InetSocketAddress(bindAddr, Settings.getInstance().getDevProxyPort());
+ this.server = new Server(endpoint);
+ }
+
+ // Error handler
+ ErrorHandler errorHandler = new ApiErrorHandler();
+ this.server.setErrorHandler(errorHandler);
+
+ // Request logging
+ if (Settings.getInstance().isDevProxyLoggingEnabled()) {
+ RequestLogWriter logWriter = new RequestLogWriter("devproxy-requests.log");
+ logWriter.setAppend(true);
+ logWriter.setTimeZone("UTC");
+ RequestLog requestLog = new CustomRequestLog(logWriter, CustomRequestLog.EXTENDED_NCSA_FORMAT);
+ this.server.setRequestLog(requestLog);
+ }
+
+ // Access handler (currently no whitelist is used)
+ InetAccessHandler accessHandler = new InetAccessHandler();
+ this.server.setHandler(accessHandler);
+
+ // URL rewriting
+ RewriteHandler rewriteHandler = new RewriteHandler();
+ accessHandler.setHandler(rewriteHandler);
+
+ // Context
+ ServletContextHandler context = new ServletContextHandler(ServletContextHandler.NO_SESSIONS);
+ context.setContextPath("/");
+ rewriteHandler.setHandler(context);
+
+ // Cross-origin resource sharing
+ FilterHolder corsFilterHolder = new FilterHolder(CrossOriginFilter.class);
+ corsFilterHolder.setInitParameter(CrossOriginFilter.ALLOWED_ORIGINS_PARAM, "*");
+ corsFilterHolder.setInitParameter(CrossOriginFilter.ALLOWED_METHODS_PARAM, "GET, POST, DELETE");
+ corsFilterHolder.setInitParameter(CrossOriginFilter.CHAIN_PREFLIGHT_PARAM, "false");
+ context.addFilter(corsFilterHolder, "/*", null);
+
+ // API servlet
+ ServletContainer container = new ServletContainer(this.config);
+ ServletHolder apiServlet = new ServletHolder(container);
+ apiServlet.setInitOrder(1);
+ context.addServlet(apiServlet, "/*");
+
+ // Start server
+ this.server.start();
+ } catch (Exception e) {
+ // Failed to start
+ throw new DataException("Failed to start developer proxy", e);
+ }
+ }
+
+ public void stop() {
+ try {
+ // Stop server
+ this.server.stop();
+ } catch (Exception e) {
+ // Failed to stop
+ }
+
+ this.server = null;
+ instance = null;
+ }
+
+}
diff --git a/src/main/java/org/qortal/api/DomainMapService.java b/src/main/java/org/qortal/api/DomainMapService.java
index 3b81d94c..8b791121 100644
--- a/src/main/java/org/qortal/api/DomainMapService.java
+++ b/src/main/java/org/qortal/api/DomainMapService.java
@@ -69,7 +69,7 @@ public class DomainMapService {
throw new RuntimeException("Failed to start SSL API due to broken keystore");
// BouncyCastle-specific SSLContext build
- SSLContext sslContext = SSLContext.getInstance("TLS", "BCJSSE");
+ SSLContext sslContext = SSLContext.getInstance("TLSv1.3", "BCJSSE");
KeyManagerFactory keyManagerFactory = KeyManagerFactory.getInstance("PKIX", "BCJSSE");
KeyStore keyStore = KeyStore.getInstance(KeyStore.getDefaultType(), "BC");
diff --git a/src/main/java/org/qortal/api/GatewayService.java b/src/main/java/org/qortal/api/GatewayService.java
index 51191af3..24a7b7c9 100644
--- a/src/main/java/org/qortal/api/GatewayService.java
+++ b/src/main/java/org/qortal/api/GatewayService.java
@@ -69,7 +69,7 @@ public class GatewayService {
throw new RuntimeException("Failed to start SSL API due to broken keystore");
// BouncyCastle-specific SSLContext build
- SSLContext sslContext = SSLContext.getInstance("TLS", "BCJSSE");
+ SSLContext sslContext = SSLContext.getInstance("TLSv1.3", "BCJSSE");
KeyManagerFactory keyManagerFactory = KeyManagerFactory.getInstance("PKIX", "BCJSSE");
KeyStore keyStore = KeyStore.getInstance(KeyStore.getDefaultType(), "BC");
diff --git a/src/main/java/org/qortal/api/HTMLParser.java b/src/main/java/org/qortal/api/HTMLParser.java
index cc3102e8..f1794594 100644
--- a/src/main/java/org/qortal/api/HTMLParser.java
+++ b/src/main/java/org/qortal/api/HTMLParser.java
@@ -24,11 +24,11 @@ public class HTMLParser {
private String theme;
private boolean usingCustomRouting;
- public HTMLParser(String resourceId, String inPath, String prefix, boolean usePrefix, byte[] data,
+ public HTMLParser(String resourceId, String inPath, String prefix, boolean includeResourceIdInPrefix, byte[] data,
String qdnContext, Service service, String identifier, String theme, boolean usingCustomRouting) {
- String inPathWithoutFilename = inPath.contains("/") ? inPath.substring(0, inPath.lastIndexOf('/')) : "";
- this.qdnBase = usePrefix ? String.format("%s/%s", prefix, resourceId) : "";
- this.qdnBaseWithPath = usePrefix ? String.format("%s/%s%s", prefix, resourceId, inPathWithoutFilename) : "";
+ String inPathWithoutFilename = inPath.contains("/") ? inPath.substring(0, inPath.lastIndexOf('/')) : String.format("/%s",inPath);
+ this.qdnBase = includeResourceIdInPrefix ? String.format("%s/%s", prefix, resourceId) : prefix;
+ this.qdnBaseWithPath = includeResourceIdInPrefix ? String.format("%s/%s%s", prefix, resourceId, inPathWithoutFilename) : String.format("%s%s", prefix, inPathWithoutFilename);
this.data = data;
this.qdnContext = qdnContext;
this.resourceId = resourceId;
@@ -82,7 +82,7 @@ public class HTMLParser {
}
public static boolean isHtmlFile(String path) {
- if (path.endsWith(".html") || path.endsWith(".htm")) {
+ if (path.endsWith(".html") || path.endsWith(".htm") || path.equals("")) {
return true;
}
return false;
diff --git a/src/main/java/org/qortal/api/domainmap/resource/DomainMapResource.java b/src/main/java/org/qortal/api/domainmap/resource/DomainMapResource.java
index 4cb9f8e5..019fb753 100644
--- a/src/main/java/org/qortal/api/domainmap/resource/DomainMapResource.java
+++ b/src/main/java/org/qortal/api/domainmap/resource/DomainMapResource.java
@@ -48,10 +48,10 @@ public class DomainMapResource {
}
private HttpServletResponse get(String resourceId, ResourceIdType resourceIdType, Service service, String identifier,
- String inPath, String secret58, String prefix, boolean usePrefix, boolean async) {
+ String inPath, String secret58, String prefix, boolean includeResourceIdInPrefix, boolean async) {
ArbitraryDataRenderer renderer = new ArbitraryDataRenderer(resourceId, resourceIdType, service, identifier, inPath,
- secret58, prefix, usePrefix, async, "domainMap", request, response, context);
+ secret58, prefix, includeResourceIdInPrefix, async, "domainMap", request, response, context);
return renderer.render();
}
diff --git a/src/main/java/org/qortal/api/gateway/resource/GatewayResource.java b/src/main/java/org/qortal/api/gateway/resource/GatewayResource.java
index 15bb398e..d82fb98d 100644
--- a/src/main/java/org/qortal/api/gateway/resource/GatewayResource.java
+++ b/src/main/java/org/qortal/api/gateway/resource/GatewayResource.java
@@ -76,7 +76,7 @@ public class GatewayResource {
}
- private HttpServletResponse parsePath(String inPath, String qdnContext, String secret58, boolean usePrefix, boolean async) {
+ private HttpServletResponse parsePath(String inPath, String qdnContext, String secret58, boolean includeResourceIdInPrefix, boolean async) {
if (inPath == null || inPath.equals("")) {
// Assume not a real file
@@ -143,7 +143,7 @@ public class GatewayResource {
}
ArbitraryDataRenderer renderer = new ArbitraryDataRenderer(name, ResourceIdType.NAME, service, identifier, outPath,
- secret58, prefix, usePrefix, async, qdnContext, request, response, context);
+ secret58, prefix, includeResourceIdInPrefix, async, qdnContext, request, response, context);
return renderer.render();
}
diff --git a/src/main/java/org/qortal/api/proxy/resource/DevProxyServerResource.java b/src/main/java/org/qortal/api/proxy/resource/DevProxyServerResource.java
new file mode 100644
index 00000000..d51e6852
--- /dev/null
+++ b/src/main/java/org/qortal/api/proxy/resource/DevProxyServerResource.java
@@ -0,0 +1,142 @@
+package org.qortal.api.proxy.resource;
+
+import org.qortal.api.ApiError;
+import org.qortal.api.ApiExceptionFactory;
+import org.qortal.api.HTMLParser;
+import org.qortal.arbitrary.misc.Service;
+import org.qortal.controller.DevProxyManager;
+
+import javax.servlet.ServletContext;
+import javax.servlet.http.HttpServletRequest;
+import javax.servlet.http.HttpServletResponse;
+import javax.ws.rs.GET;
+import javax.ws.rs.Path;
+import javax.ws.rs.PathParam;
+import javax.ws.rs.core.Context;
+import java.io.ByteArrayOutputStream;
+import java.io.IOException;
+import java.io.InputStream;
+import java.net.HttpURLConnection;
+import java.net.URL;
+import java.util.Enumeration;
+
+
+@Path("/")
+public class DevProxyServerResource {
+
+ @Context HttpServletRequest request;
+ @Context HttpServletResponse response;
+ @Context ServletContext context;
+
+
+ @GET
+ public HttpServletResponse getProxyIndex() {
+ return this.proxy("/");
+ }
+
+ @GET
+ @Path("{path:.*}")
+ public HttpServletResponse getProxyPath(@PathParam("path") String inPath) {
+ return this.proxy(inPath);
+ }
+
+ private HttpServletResponse proxy(String inPath) {
+ try {
+ String source = DevProxyManager.getInstance().getSourceHostAndPort();
+
+ // Convert localhost / 127.0.0.1 to IPv6 [::1]
+ if (source.startsWith("localhost") || source.startsWith("127.0.0.1")) {
+ int port = 80;
+ String[] parts = source.split(":");
+ if (parts.length > 1) {
+ port = Integer.parseInt(parts[1]);
+ }
+ source = String.format("[::1]:%d", port);
+ }
+
+ if (!inPath.startsWith("/")) {
+ inPath = "/" + inPath;
+ }
+
+ String queryString = request.getQueryString() != null ? "?" + request.getQueryString() : "";
+
+ // Open URL
+ String urlString = String.format("http://%s%s%s", source, inPath, queryString);
+ URL url = new URL(urlString);
+ HttpURLConnection con = (HttpURLConnection) url.openConnection();
+ con.setRequestMethod(request.getMethod());
+
+ // Proxy the request headers
+ Enumeration headerNames = request.getHeaderNames();
+ while (headerNames.hasMoreElements()) {
+ String headerName = headerNames.nextElement();
+ String headerValue = request.getHeader(headerName);
+ con.setRequestProperty(headerName, headerValue);
+ }
+
+ // TODO: proxy any POST parameters from "request" to "con"
+
+ // Proxy the response code
+ int responseCode = con.getResponseCode();
+ response.setStatus(responseCode);
+
+ // Proxy the response headers
+ for (int i = 0; ; i++) {
+ String headerKey = con.getHeaderFieldKey(i);
+ String headerValue = con.getHeaderField(i);
+ if (headerKey != null && headerValue != null) {
+ response.addHeader(headerKey, headerValue);
+ continue;
+ }
+ break;
+ }
+
+ // Read the response body
+ InputStream inputStream = con.getInputStream();
+ ByteArrayOutputStream outputStream = new ByteArrayOutputStream();
+ byte[] buffer = new byte[4096];
+ int bytesRead;
+ while ((bytesRead = inputStream.read(buffer)) != -1) {
+ outputStream.write(buffer, 0, bytesRead);
+ }
+ byte[] data = outputStream.toByteArray(); // TODO: limit file size that can be read into memory
+
+ // Close the streams
+ outputStream.close();
+ inputStream.close();
+
+ // Extract filename
+ String filename = "";
+ if (inPath.contains("/")) {
+ String[] parts = inPath.split("/");
+ if (parts.length > 0) {
+ filename = parts[parts.length - 1];
+ }
+ }
+
+ // Parse and modify output if needed
+ if (HTMLParser.isHtmlFile(filename)) {
+ // HTML file - needs to be parsed
+ HTMLParser htmlParser = new HTMLParser("", inPath, "", false, data, "proxy", Service.APP, null, "light", true);
+ htmlParser.addAdditionalHeaderTags();
+ response.addHeader("Content-Security-Policy", "default-src 'self' 'unsafe-inline' 'unsafe-eval'; media-src 'self' data: blob:; img-src 'self' data: blob:; connect-src 'self' ws:; font-src 'self' data:;");
+ response.setContentType(con.getContentType());
+ response.setContentLength(htmlParser.getData().length);
+ response.getOutputStream().write(htmlParser.getData());
+ }
+ else {
+ // Regular file - can be streamed directly
+ response.addHeader("Content-Security-Policy", "default-src 'self'");
+ response.setContentType(con.getContentType());
+ response.setContentLength(data.length);
+ response.getOutputStream().write(data);
+ }
+
+ } catch (IOException e) {
+ throw ApiExceptionFactory.INSTANCE.createCustomException(request, ApiError.INVALID_CRITERIA, e.getMessage());
+ }
+
+ return response;
+ }
+
+}
diff --git a/src/main/java/org/qortal/api/resource/DeveloperResource.java b/src/main/java/org/qortal/api/resource/DeveloperResource.java
new file mode 100644
index 00000000..ba534502
--- /dev/null
+++ b/src/main/java/org/qortal/api/resource/DeveloperResource.java
@@ -0,0 +1,96 @@
+package org.qortal.api.resource;
+
+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 org.qortal.api.ApiError;
+import org.qortal.api.ApiErrors;
+import org.qortal.api.ApiExceptionFactory;
+import org.qortal.controller.DevProxyManager;
+import org.qortal.repository.DataException;
+
+import javax.servlet.ServletContext;
+import javax.servlet.http.HttpServletRequest;
+import javax.servlet.http.HttpServletResponse;
+import javax.ws.rs.POST;
+import javax.ws.rs.Path;
+import javax.ws.rs.core.Context;
+import javax.ws.rs.core.MediaType;
+
+
+@Path("/developer")
+@Tag(name = "Developer Tools")
+public class DeveloperResource {
+
+ @Context HttpServletRequest request;
+ @Context HttpServletResponse response;
+ @Context ServletContext context;
+
+
+ @POST
+ @Path("/proxy/start")
+ @Operation(
+ summary = "Start proxy server, for real time QDN app/website development",
+ requestBody = @RequestBody(
+ description = "Host and port of source webserver to be proxied",
+ required = true,
+ content = @Content(
+ mediaType = MediaType.TEXT_PLAIN,
+ schema = @Schema(
+ type = "string",
+ example = "127.0.0.1:5173"
+ )
+ )
+ ),
+ responses = {
+ @ApiResponse(
+ description = "Port number of running server",
+ content = @Content(
+ mediaType = MediaType.TEXT_PLAIN,
+ schema = @Schema(
+ type = "number"
+ )
+ )
+ )
+ }
+ )
+ @ApiErrors({ApiError.INVALID_CRITERIA})
+ public Integer startProxy(String sourceHostAndPort) {
+ // TODO: API key
+ DevProxyManager devProxyManager = DevProxyManager.getInstance();
+ try {
+ devProxyManager.setSourceHostAndPort(sourceHostAndPort);
+ devProxyManager.start();
+ return devProxyManager.getPort();
+
+ } catch (DataException e) {
+ throw ApiExceptionFactory.INSTANCE.createCustomException(request, ApiError.INVALID_CRITERIA, e.getMessage());
+ }
+ }
+
+ @POST
+ @Path("/proxy/stop")
+ @Operation(
+ summary = "Stop proxy server",
+ responses = {
+ @ApiResponse(
+ description = "true if stopped",
+ content = @Content(
+ mediaType = MediaType.TEXT_PLAIN,
+ schema = @Schema(
+ type = "boolean"
+ )
+ )
+ )
+ }
+ )
+ public boolean stopProxy() {
+ DevProxyManager devProxyManager = DevProxyManager.getInstance();
+ devProxyManager.stop();
+ return !devProxyManager.isRunning();
+ }
+
+}
\ No newline at end of file
diff --git a/src/main/java/org/qortal/api/restricted/resource/RenderResource.java b/src/main/java/org/qortal/api/restricted/resource/RenderResource.java
index 7a772f9f..92f72032 100644
--- a/src/main/java/org/qortal/api/restricted/resource/RenderResource.java
+++ b/src/main/java/org/qortal/api/restricted/resource/RenderResource.java
@@ -157,10 +157,10 @@ public class RenderResource {
private HttpServletResponse get(String resourceId, ResourceIdType resourceIdType, Service service, String identifier,
- String inPath, String secret58, String prefix, boolean usePrefix, boolean async, String theme) {
+ String inPath, String secret58, String prefix, boolean includeResourceIdInPrefix, boolean async, String theme) {
ArbitraryDataRenderer renderer = new ArbitraryDataRenderer(resourceId, resourceIdType, service, identifier, inPath,
- secret58, prefix, usePrefix, async, "render", request, response, context);
+ secret58, prefix, includeResourceIdInPrefix, async, "render", request, response, context);
if (theme != null) {
renderer.setTheme(theme);
diff --git a/src/main/java/org/qortal/arbitrary/ArbitraryDataRenderer.java b/src/main/java/org/qortal/arbitrary/ArbitraryDataRenderer.java
index 089a99ca..704533c8 100644
--- a/src/main/java/org/qortal/arbitrary/ArbitraryDataRenderer.java
+++ b/src/main/java/org/qortal/arbitrary/ArbitraryDataRenderer.java
@@ -40,7 +40,7 @@ public class ArbitraryDataRenderer {
private String inPath;
private final String secret58;
private final String prefix;
- private final boolean usePrefix;
+ private final boolean includeResourceIdInPrefix;
private final boolean async;
private final String qdnContext;
private final HttpServletRequest request;
@@ -48,7 +48,7 @@ public class ArbitraryDataRenderer {
private final ServletContext context;
public ArbitraryDataRenderer(String resourceId, ResourceIdType resourceIdType, Service service, String identifier,
- String inPath, String secret58, String prefix, boolean usePrefix, boolean async, String qdnContext,
+ String inPath, String secret58, String prefix, boolean includeResourceIdInPrefix, boolean async, String qdnContext,
HttpServletRequest request, HttpServletResponse response, ServletContext context) {
this.resourceId = resourceId;
@@ -58,7 +58,7 @@ public class ArbitraryDataRenderer {
this.inPath = inPath;
this.secret58 = secret58;
this.prefix = prefix;
- this.usePrefix = usePrefix;
+ this.includeResourceIdInPrefix = includeResourceIdInPrefix;
this.async = async;
this.qdnContext = qdnContext;
this.request = request;
@@ -159,7 +159,7 @@ public class ArbitraryDataRenderer {
if (HTMLParser.isHtmlFile(filename)) {
// HTML file - needs to be parsed
byte[] data = Files.readAllBytes(filePath); // TODO: limit file size that can be read into memory
- HTMLParser htmlParser = new HTMLParser(resourceId, inPath, prefix, usePrefix, data, qdnContext, service, identifier, theme, usingCustomRouting);
+ HTMLParser htmlParser = new HTMLParser(resourceId, inPath, prefix, includeResourceIdInPrefix, data, qdnContext, service, identifier, theme, usingCustomRouting);
htmlParser.addAdditionalHeaderTags();
response.addHeader("Content-Security-Policy", "default-src 'self' 'unsafe-inline' 'unsafe-eval'; media-src 'self' data: blob:; img-src 'self' data: blob:;");
response.setContentType(context.getMimeType(filename));
diff --git a/src/main/java/org/qortal/controller/DevProxyManager.java b/src/main/java/org/qortal/controller/DevProxyManager.java
new file mode 100644
index 00000000..a04e87ac
--- /dev/null
+++ b/src/main/java/org/qortal/controller/DevProxyManager.java
@@ -0,0 +1,74 @@
+package org.qortal.controller;
+
+import org.apache.logging.log4j.LogManager;
+import org.apache.logging.log4j.Logger;
+import org.qortal.api.DevProxyService;
+import org.qortal.repository.DataException;
+import org.qortal.settings.Settings;
+
+public class DevProxyManager {
+
+ protected static final Logger LOGGER = LogManager.getLogger(DevProxyManager.class);
+
+ private static DevProxyManager instance;
+
+ private boolean running = false;
+
+ private String sourceHostAndPort = "127.0.0.1:5173"; // Default for React/Vite
+
+ private DevProxyManager() {
+
+ }
+
+ public static DevProxyManager getInstance() {
+ if (instance == null)
+ instance = new DevProxyManager();
+
+ return instance;
+ }
+
+ public void start() throws DataException {
+ synchronized(this) {
+ if (this.running) {
+ // Already running
+ return;
+ }
+
+ LOGGER.info(String.format("Starting developer proxy service on port %d", Settings.getInstance().getDevProxyPort()));
+ DevProxyService devProxyService = DevProxyService.getInstance();
+ devProxyService.start();
+ this.running = true;
+ }
+ }
+
+ public void stop() {
+ synchronized(this) {
+ if (!this.running) {
+ // Not running
+ return;
+ }
+
+ LOGGER.info(String.format("Shutting down developer proxy service"));
+ DevProxyService devProxyService = DevProxyService.getInstance();
+ devProxyService.stop();
+ this.running = false;
+ }
+ }
+
+ public void setSourceHostAndPort(String sourceHostAndPort) {
+ this.sourceHostAndPort = sourceHostAndPort;
+ }
+
+ public String getSourceHostAndPort() {
+ return this.sourceHostAndPort;
+ }
+
+ public Integer getPort() {
+ return Settings.getInstance().getDevProxyPort();
+ }
+
+ public boolean isRunning() {
+ return this.running;
+ }
+
+}
diff --git a/src/main/java/org/qortal/crypto/TrustlessSSLSocketFactory.java b/src/main/java/org/qortal/crypto/TrustlessSSLSocketFactory.java
index aba1955e..f723e651 100644
--- a/src/main/java/org/qortal/crypto/TrustlessSSLSocketFactory.java
+++ b/src/main/java/org/qortal/crypto/TrustlessSSLSocketFactory.java
@@ -28,7 +28,7 @@ public abstract class TrustlessSSLSocketFactory {
private static final SSLContext sc;
static {
try {
- sc = SSLContext.getInstance("SSL");
+ sc = SSLContext.getInstance("TLSv1.3");
sc.init(null, TRUSTLESS_MANAGER, new java.security.SecureRandom());
} catch (Exception e) {
throw new RuntimeException(e);
diff --git a/src/main/java/org/qortal/network/Handshake.java b/src/main/java/org/qortal/network/Handshake.java
index 4500cd59..341f4e21 100644
--- a/src/main/java/org/qortal/network/Handshake.java
+++ b/src/main/java/org/qortal/network/Handshake.java
@@ -265,7 +265,7 @@ public enum Handshake {
private static final long PEER_VERSION_131 = 0x0100030001L;
/** Minimum peer version that we are allowed to communicate with */
- private static final String MIN_PEER_VERSION = "4.0.0";
+ private static final String MIN_PEER_VERSION = "4.1.1";
private static final int POW_BUFFER_SIZE_PRE_131 = 8 * 1024 * 1024; // bytes
private static final int POW_DIFFICULTY_PRE_131 = 8; // leading zero bits
diff --git a/src/main/java/org/qortal/settings/Settings.java b/src/main/java/org/qortal/settings/Settings.java
index 362227a5..c3d5a0c8 100644
--- a/src/main/java/org/qortal/settings/Settings.java
+++ b/src/main/java/org/qortal/settings/Settings.java
@@ -47,6 +47,9 @@ public class Settings {
private static final int MAINNET_GATEWAY_PORT = 80;
private static final int TESTNET_GATEWAY_PORT = 8080;
+ private static final int MAINNET_DEV_PROXY_PORT = 12393;
+ private static final int TESTNET_DEV_PROXY_PORT = 62393;
+
private static final Logger LOGGER = LogManager.getLogger(Settings.class);
private static final String SETTINGS_FILENAME = "settings.json";
@@ -107,6 +110,11 @@ public class Settings {
private boolean gatewayLoggingEnabled = false;
private boolean gatewayLoopbackEnabled = false;
+ // Developer Proxy
+ private Integer devProxyPort;
+ private boolean devProxyLoggingEnabled = false;
+
+
// Specific to this node
private boolean wipeUnconfirmedOnStart = false;
/** Maximum number of unconfirmed transactions allowed per account */
@@ -219,7 +227,7 @@ public class Settings {
public long recoveryModeTimeout = 24 * 60 * 60 * 1000L;
/** Minimum peer version number required in order to sync with them */
- private String minPeerVersion = "4.1.1";
+ private String minPeerVersion = "4.1.2";
/** Whether to allow connections with peers below minPeerVersion
* If true, we won't sync with them but they can still sync with us, and will show in the peers list
* If false, sync will be blocked both ways, and they will not appear in the peers list */
@@ -649,6 +657,18 @@ public class Settings {
}
+ public int getDevProxyPort() {
+ if (this.devProxyPort != null)
+ return this.devProxyPort;
+
+ return this.isTestNet ? TESTNET_DEV_PROXY_PORT : MAINNET_DEV_PROXY_PORT;
+ }
+
+ public boolean isDevProxyLoggingEnabled() {
+ return this.devProxyLoggingEnabled;
+ }
+
+
public boolean getWipeUnconfirmedOnStart() {
return this.wipeUnconfirmedOnStart;
}
diff --git a/src/main/resources/i18n/ApiError_jp.properties b/src/main/resources/i18n/ApiError_jp.properties
new file mode 100644
index 00000000..603914cb
--- /dev/null
+++ b/src/main/resources/i18n/ApiError_jp.properties
@@ -0,0 +1,83 @@
+#Generated by ResourceBundle Editor (http://essiembre.github.io/eclipse-rbe/)
+# Keys are from api.ApiError enum
+
+# "localeLang": "jp",
+
+### Common ###
+JSON = JSON メッセージの解析に失敗しました
+
+INSUFFICIENT_BALANCE = 残高不足
+
+UNAUTHORIZED = APIコール未承認
+
+REPOSITORY_ISSUE = リポジトリエラー
+
+NON_PRODUCTION = この APIコールはプロダクションシステムでは許可されていません
+
+BLOCKCHAIN_NEEDS_SYNC = ブロックチェーンをまず同期する必要があります
+
+NO_TIME_SYNC = 時刻が未同期
+
+### Validation ###
+INVALID_SIGNATURE = 無効な署名
+
+INVALID_ADDRESS = 無効なアドレス
+
+INVALID_PUBLIC_KEY = 無効な公開鍵
+
+INVALID_DATA = 無効なデータ
+
+INVALID_NETWORK_ADDRESS = 無効なネットワーク アドレス
+
+ADDRESS_UNKNOWN = 不明なアカウントアドレス
+
+INVALID_CRITERIA = 無効な検索条件
+
+INVALID_REFERENCE = 無効な参照
+
+TRANSFORMATION_ERROR = JSONをトランザクションに変換出来ませんでした
+
+INVALID_PRIVATE_KEY = 無効な秘密鍵
+
+INVALID_HEIGHT = 無効なブロック高
+
+CANNOT_MINT = アカウントはミント出来ません
+
+### Blocks ###
+BLOCK_UNKNOWN = 不明なブロック
+
+### Transactions ###
+TRANSACTION_UNKNOWN = 不明なトランザクション
+
+PUBLIC_KEY_NOT_FOUND = 公開鍵が見つかりません
+
+# this one is special in that caller expected to pass two additional strings, hence the two %s
+TRANSACTION_INVALID = 無効なトランザクション: %s (%s)
+
+### Naming ###
+NAME_UNKNOWN = 不明な名前
+
+### Asset ###
+INVALID_ASSET_ID = 無効なアセット ID
+
+INVALID_ORDER_ID = 無効なアセット注文 ID
+
+ORDER_UNKNOWN = 不明なアセット注文 ID
+
+### Groups ###
+GROUP_UNKNOWN = 不明なグループ
+
+### Foreign Blockchain ###
+FOREIGN_BLOCKCHAIN_NETWORK_ISSUE = 外部ブロックチェーンまたはElectrumXネットワークの問題
+
+FOREIGN_BLOCKCHAIN_BALANCE_ISSUE = 外部ブロックチェーンの残高が不足しています
+
+FOREIGN_BLOCKCHAIN_TOO_SOON = 外部ブロックチェーン トランザクションのブロードキャストが時期尚早 (ロックタイム/ブロック時間の中央値)
+
+### Trade Portal ###
+ORDER_SIZE_TOO_SMALL = 注文金額が低すぎます
+
+### Data ###
+FILE_NOT_FOUND = ファイルが見つかりません
+
+NO_REPLY = ピアが制限時間内に応答しませんでした
diff --git a/src/main/resources/i18n/SysTray_jp.properties b/src/main/resources/i18n/SysTray_jp.properties
new file mode 100644
index 00000000..c4cccb5b
--- /dev/null
+++ b/src/main/resources/i18n/SysTray_jp.properties
@@ -0,0 +1,48 @@
+#Generated by ResourceBundle Editor (http://essiembre.github.io/eclipse-rbe/)
+# SysTray pop-up menu # Japanese translation by R M 2023
+
+APPLYING_UPDATE_AND_RESTARTING = 自動更新を適用して再起動しています...
+
+AUTO_UPDATE = 自動更新
+
+BLOCK_HEIGHT = ブロック高
+
+BLOCKS_REMAINING = 残りのブロック
+
+BUILD_VERSION = ビルドバージョン
+
+CHECK_TIME_ACCURACY = 時刻の精度を確認
+
+CONNECTING = 接続中
+
+CONNECTION = 接続
+
+CONNECTIONS = 接続
+
+CREATING_BACKUP_OF_DB_FILES = データベース ファイルのバックアップを作成中...
+
+DB_BACKUP = データベースのバックアップ
+
+DB_CHECKPOINT = データベースのチェックポイント
+
+DB_MAINTENANCE = データベースのメンテナンス
+
+EXIT = 終了
+
+LITE_NODE = ライトノード
+
+MINTING_DISABLED = ミント一時中止中
+
+MINTING_ENABLED = \u2714 ミント
+
+OPEN_UI = UIを開く
+
+PERFORMING_DB_CHECKPOINT = コミットされていないデータベースの変更を保存中...
+
+PERFORMING_DB_MAINTENANCE = 定期メンテナンスを実行中...
+
+SYNCHRONIZE_CLOCK = 時刻を同期
+
+SYNCHRONIZING_BLOCKCHAIN = ブロックチェーンを同期中
+
+SYNCHRONIZING_CLOCK = 時刻を同期中
diff --git a/src/main/resources/i18n/TransactionValidity_jp.properties b/src/main/resources/i18n/TransactionValidity_jp.properties
new file mode 100644
index 00000000..9540372a
--- /dev/null
+++ b/src/main/resources/i18n/TransactionValidity_jp.properties
@@ -0,0 +1,195 @@
+#
+
+ACCOUNT_ALREADY_EXISTS = 既にアカウントは存在します
+
+ACCOUNT_CANNOT_REWARD_SHARE = アカウントは報酬シェアが出来ません
+
+ADDRESS_ABOVE_RATE_LIMIT = アドレスが指定されたレート制限に達しました
+
+ADDRESS_BLOCKED = このアドレスはブロックされています
+
+ALREADY_GROUP_ADMIN = 既ににグループ管理者です
+
+ALREADY_GROUP_MEMBER = 既にグループメンバーです
+
+ALREADY_VOTED_FOR_THAT_OPTION = 既にそのオプションに投票しています
+
+ASSET_ALREADY_EXISTS = 既にアセットは存在します
+
+ASSET_DOES_NOT_EXIST = アセットが存在しません
+
+ASSET_DOES_NOT_MATCH_AT = アセットがATのアセットと一致しません
+
+ASSET_NOT_SPENDABLE = 資産が使用不可です
+
+AT_ALREADY_EXISTS = 既にATが存在します
+
+AT_IS_FINISHED = ATが終了しました
+
+AT_UNKNOWN = 不明なAT
+
+BAN_EXISTS = 既にバンされてます
+
+BAN_UNKNOWN = 不明なバン
+
+BANNED_FROM_GROUP = グループからのバンされています
+
+BUYER_ALREADY_OWNER = 既に購入者が所有者です
+
+CLOCK_NOT_SYNCED = 時刻が未同期
+
+DUPLICATE_MESSAGE = このアドレスは重複メッセージを送信しました
+
+DUPLICATE_OPTION = 重複したオプション
+
+GROUP_ALREADY_EXISTS = 既にグループは存在します
+
+GROUP_APPROVAL_DECIDED = 既にグループの承認は決定されています
+
+GROUP_APPROVAL_NOT_REQUIRED = グループ承認が不必要
+
+GROUP_DOES_NOT_EXIST = グループが存在しません
+
+GROUP_ID_MISMATCH = グループ ID が不一致
+
+GROUP_OWNER_CANNOT_LEAVE = グループ所有者はグループを退会出来ません
+
+HAVE_EQUALS_WANT = 持っている資産は欲しい資産と同じです
+
+INCORRECT_NONCE = 不正な PoW ナンス
+
+INSUFFICIENT_FEE = 手数料が不十分です
+
+INVALID_ADDRESS = 無効なアドレス
+
+INVALID_AMOUNT = 無効な金額
+
+INVALID_ASSET_OWNER = 無効なアセット所有者
+
+INVALID_AT_TRANSACTION = 無効なATトランザクション
+
+INVALID_AT_TYPE_LENGTH = 無効なATの「タイプ」の長さです
+
+INVALID_BUT_OK = 無効だがOK
+
+INVALID_CREATION_BYTES = 無効な作成バイト数
+
+INVALID_DATA_LENGTH = 無効なデータ長
+
+INVALID_DESCRIPTION_LENGTH = 無効な概要の長さ
+
+INVALID_GROUP_APPROVAL_THRESHOLD = 無効なグループ承認のしきい値
+
+INVALID_GROUP_BLOCK_DELAY = 無効なグループ承認のブロック遅延
+
+INVALID_GROUP_ID = 無効なグループ ID
+
+INVALID_GROUP_OWNER = 無効なグループ所有者
+
+INVALID_LIFETIME = 無効な有効期間
+
+INVALID_NAME_LENGTH = 無効な名前の長さです
+
+INVALID_NAME_OWNER = 無効な名前の所有者
+
+INVALID_OPTION_LENGTH = 無効なオプションの長さ
+
+INVALID_OPTIONS_COUNT = 無効なオプションの数
+
+INVALID_ORDER_CREATOR = 無効な注文作成者
+
+INVALID_PAYMENTS_COUNT = 無効な入出金数
+
+INVALID_PUBLIC_KEY = 無効な公開鍵
+
+INVALID_QUANTITY = 無効な数量
+
+INVALID_REFERENCE = 無効な参照
+
+INVALID_RETURN = 無効な返品
+
+INVALID_REWARD_SHARE_PERCENT = 無効な報酬シェア率
+
+INVALID_SELLER = 無効な販売者
+
+INVALID_TAGS_LENGTH = 無効な「タグ」の長さ
+
+INVALID_TIMESTAMP_SIGNATURE = 無効なタイムスタンプ署名
+
+INVALID_TX_GROUP_ID = 無効なトランザクション グループ ID
+
+INVALID_VALUE_LENGTH = 無効な「値」の長さ
+
+INVITE_UNKNOWN = 不明なグループ招待
+
+JOIN_REQUEST_EXISTS = 既にグループ参加リクエストが存在します
+
+MAXIMUM_REWARD_SHARES = 既にこのアカウントの報酬シェアは最大です
+
+MISSING_CREATOR = 作成者が見つかりません
+
+MULTIPLE_NAMES_FORBIDDEN = アカウントごとに複数の登録名は禁止されています
+
+NAME_ALREADY_FOR_SALE = 既に名前は販売中です
+
+NAME_ALREADY_REGISTERED = 既に名前は登録されています
+
+NAME_BLOCKED = この名前はブロックされています
+
+NAME_DOES_NOT_EXIST = 名前は存在しません
+
+NAME_NOT_FOR_SALE = 名前は非売品です
+
+NAME_NOT_NORMALIZED = 名前は Unicode の「正規化」形式ではありません
+
+NEGATIVE_AMOUNT = 無効な/負の金額
+
+NEGATIVE_FEE = 無効な/負の料金
+
+NEGATIVE_PRICE = 無効な/負の価格
+
+NO_BALANCE = 残高が不足しています
+
+NO_BLOCKCHAIN_LOCK = ノードのブロックチェーンは現在ビジーです
+
+NO_FLAG_PERMISSION = アカウントにはその権限がありません
+
+NOT_GROUP_ADMIN = アカウントはグループ管理者ではありません
+
+NOT_GROUP_MEMBER = アカウントはグループメンバーではありません
+
+NOT_MINTING_ACCOUNT = アカウントはミント出来ません
+
+NOT_YET_RELEASED = 機能はまだリリースされていません
+
+OK = OK
+
+ORDER_ALREADY_CLOSED = 既に資産取引注文は終了しています
+
+ORDER_DOES_NOT_EXIST = 資産取引注文が存在しません
+
+POLL_ALREADY_EXISTS = 既に投票は存在します
+
+POLL_DOES_NOT_EXIST = 投票は存在しません
+
+POLL_OPTION_DOES_NOT_EXIST = 投票オプションが存在しません
+
+PUBLIC_KEY_UNKNOWN = 不明な公開鍵
+
+REWARD_SHARE_UNKNOWN = 不明な報酬シェア
+
+SELF_SHARE_EXISTS = 既に自己シェア(報酬シェア)が存在します
+
+TIMESTAMP_TOO_NEW = タイムスタンプが新しすぎます
+
+TIMESTAMP_TOO_OLD = タイムスタンプが古すぎます
+
+TOO_MANY_UNCONFIRMED = アカウントに保留中の未承認トランザクションが多すぎます
+
+TRANSACTION_ALREADY_CONFIRMED = 既にトランザクションは承認されています
+
+TRANSACTION_ALREADY_EXISTS = 既にトランザクションは存在します
+
+TRANSACTION_UNKNOWN = 不明なトランザクション
+
+TX_GROUP_ID_MISMATCH = トランザクションのグループIDが一致しません
diff --git a/src/main/resources/q-apps/q-apps.js b/src/main/resources/q-apps/q-apps.js
index e766fffd..6cc1bf08 100644
--- a/src/main/resources/q-apps/q-apps.js
+++ b/src/main/resources/q-apps/q-apps.js
@@ -472,6 +472,10 @@ function getDefaultTimeout(action) {
// Allow extra time for other actions that create transactions, even if there is no PoW
return 5 * 60 * 1000;
+ case "GET_WALLET_BALANCE":
+ // Getting a wallet balance can take a while, if there are many transactions
+ return 2 * 60 * 1000;
+
default:
break;
}