Merge pull request #186 from jschulthess/reticulum

Initial (almost) phase-1 Reticulum implementation
This commit is contained in:
crowetic 2024-03-25 09:18:55 -07:00 committed by GitHub
commit 4df05364f5
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
138 changed files with 8556 additions and 716 deletions

View File

@ -8,7 +8,7 @@
* Build auto-update download: `tools/build-auto-update.sh` - uploads auto-update file into new git branch
* Restart local node
* Publish auto-update transaction using *private key* for **non-admin** member of "dev" group:
`tools/publish-auto-update.sh non-admin-dev-member-private-key-in-base58`
`tools/publish-auto-update.pl non-admin-dev-member-private-key-in-base58`
* Wait for auto-update `ARBITRARY` transaction to be confirmed into a block
* Have "dev" group admins 'approve' auto-update using `tools/approve-auto-update.sh`
This tool will prompt for *private key* of **admin** of "dev" group

View File

@ -0,0 +1,24 @@
<?xml version="1.0" encoding="UTF-8"?>
<metadata modelVersion="1.1.0">
<groupId>io.reticulum</groupId>
<artifactId>reticulum-network-stack</artifactId>
<version>1.0-SNAPSHOT</version>
<versioning>
<snapshot>
<localCopy>true</localCopy>
</snapshot>
<lastUpdated>20240324170649</lastUpdated>
<snapshotVersions>
<snapshotVersion>
<extension>jar</extension>
<value>1.0-SNAPSHOT</value>
<updated>20240324170649</updated>
</snapshotVersion>
<snapshotVersion>
<extension>pom</extension>
<value>1.0-SNAPSHOT</value>
<updated>20240324170649</updated>
</snapshotVersion>
</snapshotVersions>
</versioning>
</metadata>

View File

@ -0,0 +1,9 @@
<?xml version="1.0" encoding="UTF-8"?>
<project xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 https://maven.apache.org/xsd/maven-4.0.0.xsd" xmlns="http://maven.apache.org/POM/4.0.0"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance">
<modelVersion>4.0.0</modelVersion>
<groupId>io.reticulum</groupId>
<artifactId>reticulum-network-stack</artifactId>
<version>1.0-SNAPSHOT</version>
<description>POM was created from install:install-file</description>
</project>

View File

@ -0,0 +1,11 @@
<?xml version="1.0" encoding="UTF-8"?>
<metadata>
<groupId>io.reticulum</groupId>
<artifactId>reticulum-network-stack</artifactId>
<versioning>
<versions>
<version>1.0-SNAPSHOT</version>
</versions>
<lastUpdated>20240324170649</lastUpdated>
</versioning>
</metadata>

Binary file not shown.

View File

@ -10,14 +10,13 @@
<properties>
<project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
<skipTests>false</skipTests>
<maven-compiler-plugin.version>3.8.1</maven-compiler-plugin.version>
<maven-source-plugin.version>3.2.0</maven-source-plugin.version>
<maven-javadoc-plugin.version>3.3.1</maven-javadoc-plugin.version>
<maven-surefire-plugin.version>3.0.0-M4</maven-surefire-plugin.version>
<maven-jar-plugin.version>3.2.0</maven-jar-plugin.version>
<bouncycastle.version>1.64</bouncycastle.version>
<bouncycastle.version>1.69</bouncycastle.version>
<junit.version>4.13.2</junit.version>
<maven-compiler-plugin.version>3.11.0</maven-compiler-plugin.version>
<maven-jar-plugin.version>3.3.0</maven-jar-plugin.version>
<maven-javadoc-plugin.version>3.6.3</maven-javadoc-plugin.version>
<maven-source-plugin.version>3.3.0</maven-source-plugin.version>
<maven-surefire-plugin.version>3.2.2</maven-surefire-plugin.version>
</properties>
<build>
@ -117,7 +116,7 @@
<dependency>
<groupId>junit</groupId>
<artifactId>junit</artifactId>
<version>4.13</version>
<version>${junit.version}</version>
<scope>test</scope>
</dependency>
</dependencies>

View File

@ -5,14 +5,11 @@
<versioning>
<release>1.4.1</release>
<versions>
<version>1.3.4</version>
<version>1.3.5</version>
<version>1.3.6</version>
<version>1.3.7</version>
<version>1.3.8</version>
<version>1.4.0</version>
<version>1.4.1</version>
</versions>
<lastUpdated>20230821074325</lastUpdated>
<lastUpdated>20231212092227</lastUpdated>
</versioning>
</metadata>

145
pom.xml
View File

@ -3,59 +3,65 @@
<modelVersion>4.0.0</modelVersion>
<groupId>org.qortal</groupId>
<artifactId>qortal</artifactId>
<version>4.4.0</version>
<version>4.5.1</version>
<packaging>jar</packaging>
<properties>
<!--
<maven.compiler.source>11</maven.compiler.source>
<maven.compiler.target>11</maven.compiler.target>
-->
<skipTests>true</skipTests>
<altcoinj.version>7dc8c6f</altcoinj.version>
<bitcoinj.version>0.15.10</bitcoinj.version>
<bouncycastle.version>1.69</bouncycastle.version>
<build-helper-maven-plugin.version>3.4.0</build-helper-maven-plugin.version>
<build-helper-maven-plugin.version>3.5.0</build-helper-maven-plugin.version>
<build.timestamp>${maven.build.timestamp}</build.timestamp>
<ciyam-at.version>1.4.1</ciyam-at.version>
<commons-net.version>3.8.0</commons-net.version>
<commons-text.version>1.11.0</commons-text.version>
<commons-io.version>2.11.0</commons-io.version>
<commons-compress.version>1.24.0</commons-compress.version>
<commons-lang3.version>3.13.0</commons-lang3.version>
<commons-compress.version>1.25.0</commons-compress.version>
<commons-lang3.version>3.14.0</commons-lang3.version>
<dagger.version>1.2.2</dagger.version>
<extendedset.version>0.12.3</extendedset.version>
<git-commit-id-plugin.version>4.9.10</git-commit-id-plugin.version>
<grpc.version>1.59.0</grpc.version>
<guava.version>32.1.3-jre</guava.version>
<grpc.version>1.61.1</grpc.version>
<guava.version>33.0.0-jre</guava.version>
<hamcrest-library.version>2.2</hamcrest-library.version>
<homoglyph.version>1.2.1</homoglyph.version>
<hsqldb.version>2.5.1</hsqldb.version>
<icu4j.version>74.1</icu4j.version>
<icu4j.version>74.2</icu4j.version>
<java-diff-utils.version>4.12</java-diff-utils.version>
<javax.servlet-api.version>4.0.1</javax.servlet-api.version>
<jaxb-runtime.version>2.3.9</jaxb-runtime.version>
<jersey.version>2.41</jersey.version>
<jetty.version>9.4.53.v20231009</jetty.version>
<jetty.version>9.4.54.v20240208</jetty.version>
<json-simple.version>1.1.1</json-simple.version>
<json.version>20231013</json.version>
<jsoup.version>1.16.2</jsoup.version>
<json.version>20240205</json.version>
<jsoup.version>1.17.2</jsoup.version>
<junit-jupiter-engine.version>5.10.0</junit-jupiter-engine.version>
<lifecycle-mapping.version>1.0.0</lifecycle-mapping.version>
<log4j.version>2.21.1</log4j.version>
<log4j.version>2.22.1</log4j.version>
<mail.version>1.5.0-b01</mail.version>
<maven-compiler-plugin.version>3.11.0</maven-compiler-plugin.version>
<maven-compiler-plugin.version>3.12.1</maven-compiler-plugin.version>
<maven-jar-plugin.version>3.3.0</maven-jar-plugin.version>
<maven-resources-plugin.version>3.3.1</maven-resources-plugin.version>
<maven-shade-plugin.version>3.5.1</maven-shade-plugin.version>
<maven-surefire-plugin.version>3.2.2</maven-surefire-plugin.version>
<maven-surefire-plugin.version>3.2.5</maven-surefire-plugin.version>
<package-info-maven-plugin.version>1.1.0</package-info-maven-plugin.version>
<project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
<protobuf.version>3.24.4</protobuf.version>
<protobuf.version>3.25.2</protobuf.version>
<replacer.version>1.5.3</replacer.version>
<reproducible-build-maven-plugin.version>0.16</reproducible-build-maven-plugin.version>
<simplemagic.version>1.17</simplemagic.version>
<slf4j.version>1.7.36</slf4j.version>
<swagger-api.version>2.0.10</swagger-api.version>
<swagger-ui.version>5.9.0</swagger-ui.version>
<swagger-ui.version>5.10.3</swagger-ui.version>
<upnp.version>1.2</upnp.version>
<versions-maven-plugin.version>2.16.1</versions-maven-plugin.version>
<versions-maven-plugin.version>2.16.2</versions-maven-plugin.version>
<xz.version>1.9</xz.version>
<lombok.version>1.18.30</lombok.version>
<jackson.version>2.14.3</jackson.version>
</properties>
<build>
<sourceDirectory>src/main/java</sourceDirectory>
@ -103,6 +109,14 @@
<outputDirectory>${project.build.directory}/swagger-ui.unpacked</outputDirectory>
</configuration>
</execution>
<!-- same as: mvn clean package dependency:copy-dependencies
<execution>
<phase>prepare-package</phase>
<goals>
<goal>copy-dependencies</goal>
</goals>
</execution>
-->
</executions>
</plugin>
<plugin>
@ -373,6 +387,11 @@
<version>${maven-surefire-plugin.version}</version>
<configuration>
<skipTests>${skipTests}</skipTests>
<!-- for Java >=17.x unit testing ("higher restrictions for some internal modules") -->
<argLine>
--add-opens=java.base/java.lang=ALL-UNNAMED
--add-opens=java.base/java.util=ALL-UNNAMED
</argLine>
</configuration>
</plugin>
</plugins>
@ -441,6 +460,10 @@
<repository>
<id>jitpack.io</id>
<url>https://jitpack.io</url>
<snapshots>
<enabled>true</enabled>
<updatePolicy>always</updatePolicy>
</snapshots>
</repository>
</repositories>
<dependencies>
@ -780,5 +803,95 @@
<artifactId>jaxb-runtime</artifactId>
<version>${jaxb-runtime.version}</version>
</dependency>
<dependency>
<groupId>io.reticulum</groupId>
<artifactId>reticulum-network-stack</artifactId>
<version>1.0-SNAPSHOT</version>
</dependency>
<!-- reticulum_network_stack -->
<!-- https://mvnrepository.com/artifact/com.fasterxml.jackson.dataformat/jackson-dataformat-yaml -->
<dependency>
<groupId>com.fasterxml.jackson.dataformat</groupId>
<artifactId>jackson-dataformat-yaml</artifactId>
<version>${jackson.version}</version>
</dependency>
<dependency>
<groupId>com.fasterxml.jackson.core</groupId>
<artifactId>jackson-databind</artifactId>
<version>${jackson.version}</version>
</dependency>
<dependency>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
<version>${lombok.version}</version>
<scope>provided</scope>
</dependency>
<dependency>
<groupId>commons-codec</groupId>
<artifactId>commons-codec</artifactId>
<version>1.15</version>
</dependency>
<dependency>
<groupId>org.apache.commons</groupId>
<artifactId>commons-collections4</artifactId>
<version>4.4</version>
</dependency>
<dependency>
<groupId>org.msgpack</groupId>
<artifactId>jackson-dataformat-msgpack</artifactId>
<version>0.9.3</version>
</dependency>
<!-- https://mvnrepository.com/artifact/org.slf4j/slf4j-api -->
<!--
<dependency>
<groupId>org.slf4j</groupId>
<artifactId>slf4j-api</artifactId>
<version>2.0.9</version>
</dependency>
-->
<!-- https://mvnrepository.com/artifact/io.netty/netty-all -->
<dependency>
<groupId>io.netty</groupId>
<artifactId>netty-all</artifactId>
<version>4.1.92.Final</version>
</dependency>
<dependency>
<groupId>org.bouncycastle</groupId>
<artifactId>bcpkix-jdk15on</artifactId>
<version>1.69</version>
</dependency>
<dependency>
<groupId>com.macasaet.fernet</groupId>
<artifactId>fernet-java8</artifactId>
<version>1.4.2</version>
</dependency>
<dependency>
<groupId>org.apache.commons</groupId>
<artifactId>commons-compress</artifactId>
<version>1.25.0</version>
</dependency>
<dependency>
<groupId>com.igormaznitsa</groupId>
<artifactId>jbbp</artifactId>
<version>2.0.4</version>
</dependency>
<dependency>
<groupId>com.github.seancfoley</groupId>
<artifactId>ipaddress</artifactId>
<version>5.4.0</version>
</dependency>
<dependency>
<groupId>org.msgpack</groupId>
<artifactId>msgpack-core</artifactId>
<version>0.9.6</version>
</dependency>
<!--
<dependency>
<groupId>org.mockito</groupId>
<artifactId>mockito-junit-jupiter</artifactId>
<version>5.2.0</version>
<scope>test</scope>
</dependency>
-->
</dependencies>
</project>

View File

@ -0,0 +1,227 @@
package org.qortal;
import org.apache.logging.log4j.LogManager;
import org.apache.logging.log4j.Logger;
import org.bouncycastle.jce.provider.BouncyCastleProvider;
import org.bouncycastle.jsse.provider.BouncyCastleJsseProvider;
import org.qortal.api.ApiKey;
import org.qortal.api.ApiRequest;
import org.qortal.controller.BootstrapNode;
import org.qortal.settings.Settings;
import java.io.IOException;
import java.lang.management.ManagementFactory;
import java.nio.file.FileVisitResult;
import java.nio.file.Files;
import java.nio.file.Path;
import java.nio.file.Paths;
import java.nio.file.SimpleFileVisitor;
import java.nio.file.attribute.BasicFileAttributes;
import java.security.Security;
import java.util.*;
import java.util.stream.Collectors;
import static org.qortal.controller.BootstrapNode.AGENTLIB_JVM_HOLDER_ARG;
public class ApplyBootstrap {
static {
// This static block will be called before others if using ApplyBootstrap.main()
// Log into different files for bootstrap - this has to be before LogManger.getLogger() calls
System.setProperty("log4j2.filenameTemplate", "log-apply-bootstrap.txt");
// This must go before any calls to LogManager/Logger
System.setProperty("java.util.logging.manager", "org.apache.logging.log4j.jul.LogManager");
}
private static final Logger LOGGER = LogManager.getLogger(ApplyBootstrap.class);
private static final String JAR_FILENAME = BootstrapNode.JAR_FILENAME;
private static final String WINDOWS_EXE_LAUNCHER = "qortal.exe";
private static final String JAVA_TOOL_OPTIONS_NAME = "JAVA_TOOL_OPTIONS";
private static final String JAVA_TOOL_OPTIONS_VALUE = "";
private static final long CHECK_INTERVAL = 15 * 1000L; // ms
private static final int MAX_ATTEMPTS = 20;
public static void main(String[] args) {
Security.insertProviderAt(new BouncyCastleProvider(), 0);
Security.insertProviderAt(new BouncyCastleJsseProvider(), 1);
// Load/check settings, which potentially sets up blockchain config, etc.
if (args.length > 0)
Settings.fileInstance(args[0]);
else
Settings.getInstance();
LOGGER.info("Applying bootstrap...");
// Shutdown node using API
if (!shutdownNode())
return;
// Delete db
deleteDB();
// Restart node
restartNode(args);
LOGGER.info("Bootstrapping...");
}
private static boolean shutdownNode() {
String baseUri = "http://localhost:" + Settings.getInstance().getApiPort() + "/";
LOGGER.info(() -> String.format("Shutting down node using API via %s", baseUri));
// The /admin/stop endpoint requires an API key, which may or may not be already generated
boolean apiKeyNewlyGenerated = false;
ApiKey apiKey = null;
try {
apiKey = new ApiKey();
if (!apiKey.generated()) {
apiKey.generate();
apiKeyNewlyGenerated = true;
LOGGER.info("Generated API key");
}
} catch (IOException e) {
LOGGER.info("Error loading API key: {}", e.getMessage());
}
// Create GET params
Map<String, String> params = new HashMap<>();
if (apiKey != null) {
params.put("apiKey", apiKey.toString());
}
// Attempt to stop the node
int attempt;
for (attempt = 0; attempt < MAX_ATTEMPTS; ++attempt) {
final int attemptForLogging = attempt;
LOGGER.info(() -> String.format("Attempt #%d out of %d to shutdown node", attemptForLogging + 1, MAX_ATTEMPTS));
String response = ApiRequest.perform(baseUri + "admin/stop", params);
if (response == null) {
// No response - consider node shut down
if (apiKeyNewlyGenerated) {
// API key was newly generated for bootstrapping node, so we need to remove it
ApplyBootstrap.removeGeneratedApiKey();
}
return true;
}
LOGGER.info(() -> String.format("Response from API: %s", response));
try {
Thread.sleep(CHECK_INTERVAL);
} catch (InterruptedException e) {
// We still need to check...
break;
}
}
if (apiKeyNewlyGenerated) {
// API key was newly generated for bootstrapping node, so we need to remove it
ApplyBootstrap.removeGeneratedApiKey();
}
if (attempt == MAX_ATTEMPTS) {
LOGGER.error("Failed to shutdown node - giving up");
return false;
}
return true;
}
private static void removeGeneratedApiKey() {
try {
LOGGER.info("Removing newly generated API key...");
// Delete the API key since it was only generated for bootstrapping node
ApiKey apiKey = new ApiKey();
apiKey.delete();
} catch (IOException e) {
LOGGER.info("Error loading or deleting API key: {}", e.getMessage());
}
}
private static void deleteDB() {
// Get the repository path from settings
String repositoryPath = Settings.getInstance().getRepositoryPath();
LOGGER.debug(String.format("Repository path: %s", repositoryPath));
try {
Path directory = Paths.get(repositoryPath);
Files.walkFileTree(directory, new SimpleFileVisitor<Path>() {
@Override
public FileVisitResult visitFile(Path file, BasicFileAttributes attrs) throws IOException {
Files.delete(file);
return FileVisitResult.CONTINUE;
}
@Override
public FileVisitResult postVisitDirectory(Path dir, IOException exc) throws IOException {
Files.delete(dir);
return FileVisitResult.CONTINUE;
}
});
} catch (IOException e) {
LOGGER.error("Error deleting DB: {}", e.getMessage());
}
}
private static void restartNode(String[] args) {
String javaHome = System.getProperty("java.home");
LOGGER.debug(() -> String.format("Java home: %s", javaHome));
Path javaBinary = Paths.get(javaHome, "bin", "java");
LOGGER.debug(() -> String.format("Java binary: %s", javaBinary));
Path exeLauncher = Paths.get(WINDOWS_EXE_LAUNCHER);
LOGGER.debug(() -> String.format("Windows EXE launcher: %s", exeLauncher));
List<String> javaCmd;
if (Files.exists(exeLauncher)) {
javaCmd = Arrays.asList(exeLauncher.toString());
} else {
javaCmd = new ArrayList<>();
// Java runtime binary itself
javaCmd.add(javaBinary.toString());
// JVM arguments
javaCmd.addAll(ManagementFactory.getRuntimeMXBean().getInputArguments());
// Reapply any retained, but disabled, -agentlib JVM arg
javaCmd = javaCmd.stream()
.map(arg -> arg.replace(AGENTLIB_JVM_HOLDER_ARG, "-agentlib"))
.collect(Collectors.toList());
// Call mainClass in JAR
javaCmd.addAll(Arrays.asList("-jar", JAR_FILENAME));
// Add saved command-line args
javaCmd.addAll(Arrays.asList(args));
}
try {
LOGGER.info(String.format("Restarting node with: %s", String.join(" ", javaCmd)));
ProcessBuilder processBuilder = new ProcessBuilder(javaCmd);
if (Files.exists(exeLauncher)) {
LOGGER.debug(() -> String.format("Setting env %s to %s", JAVA_TOOL_OPTIONS_NAME, JAVA_TOOL_OPTIONS_VALUE));
processBuilder.environment().put(JAVA_TOOL_OPTIONS_NAME, JAVA_TOOL_OPTIONS_VALUE);
}
// New process will inherit our stdout and stderr
processBuilder.redirectOutput(ProcessBuilder.Redirect.INHERIT);
processBuilder.redirectError(ProcessBuilder.Redirect.INHERIT);
Process process = processBuilder.start();
// Nothing to pipe to new process, so close output stream (process's stdin)
process.getOutputStream().close();
} catch (Exception e) {
LOGGER.error(String.format("Failed to restart node (BAD): %s", e.getMessage()));
}
}
}

View File

@ -0,0 +1,196 @@
package org.qortal;
import org.apache.logging.log4j.LogManager;
import org.apache.logging.log4j.Logger;
import org.bouncycastle.jce.provider.BouncyCastleProvider;
import org.bouncycastle.jsse.provider.BouncyCastleJsseProvider;
import org.qortal.api.ApiKey;
import org.qortal.api.ApiRequest;
import org.qortal.controller.RestartNode;
import org.qortal.settings.Settings;
import java.io.IOException;
import java.lang.management.ManagementFactory;
import java.nio.file.Files;
import java.nio.file.Path;
import java.nio.file.Paths;
import java.security.Security;
import java.util.*;
import java.util.stream.Collectors;
import static org.qortal.controller.RestartNode.AGENTLIB_JVM_HOLDER_ARG;
public class ApplyRestart {
static {
// This static block will be called before others if using ApplyRestart.main()
// Log into different files for restart node - this has to be before LogManger.getLogger() calls
System.setProperty("log4j2.filenameTemplate", "log-apply-restart.txt");
// This must go before any calls to LogManager/Logger
System.setProperty("java.util.logging.manager", "org.apache.logging.log4j.jul.LogManager");
}
private static final Logger LOGGER = LogManager.getLogger(ApplyRestart.class);
private static final String JAR_FILENAME = RestartNode.JAR_FILENAME;
private static final String WINDOWS_EXE_LAUNCHER = "qortal.exe";
private static final String JAVA_TOOL_OPTIONS_NAME = "JAVA_TOOL_OPTIONS";
private static final String JAVA_TOOL_OPTIONS_VALUE = "";
private static final long CHECK_INTERVAL = 10 * 1000L; // ms
private static final int MAX_ATTEMPTS = 12;
public static void main(String[] args) {
Security.insertProviderAt(new BouncyCastleProvider(), 0);
Security.insertProviderAt(new BouncyCastleJsseProvider(), 1);
// Load/check settings, which potentially sets up blockchain config, etc.
if (args.length > 0)
Settings.fileInstance(args[0]);
else
Settings.getInstance();
LOGGER.info("Applying restart...");
// Shutdown node using API
if (!shutdownNode())
return;
// Restart node
restartNode(args);
LOGGER.info("Restarting...");
}
private static boolean shutdownNode() {
String baseUri = "http://localhost:" + Settings.getInstance().getApiPort() + "/";
LOGGER.info(() -> String.format("Shutting down node using API via %s", baseUri));
// The /admin/stop endpoint requires an API key, which may or may not be already generated
boolean apiKeyNewlyGenerated = false;
ApiKey apiKey = null;
try {
apiKey = new ApiKey();
if (!apiKey.generated()) {
apiKey.generate();
apiKeyNewlyGenerated = true;
LOGGER.info("Generated API key");
}
} catch (IOException e) {
LOGGER.info("Error loading API key: {}", e.getMessage());
}
// Create GET params
Map<String, String> params = new HashMap<>();
if (apiKey != null) {
params.put("apiKey", apiKey.toString());
}
// Attempt to stop the node
int attempt;
for (attempt = 0; attempt < MAX_ATTEMPTS; ++attempt) {
final int attemptForLogging = attempt;
LOGGER.info(() -> String.format("Attempt #%d out of %d to shutdown node", attemptForLogging + 1, MAX_ATTEMPTS));
String response = ApiRequest.perform(baseUri + "admin/stop", params);
if (response == null) {
// No response - consider node shut down
if (apiKeyNewlyGenerated) {
// API key was newly generated for restarting node, so we need to remove it
ApplyRestart.removeGeneratedApiKey();
}
return true;
}
LOGGER.info(() -> String.format("Response from API: %s", response));
try {
Thread.sleep(CHECK_INTERVAL);
} catch (InterruptedException e) {
// We still need to check...
break;
}
}
if (apiKeyNewlyGenerated) {
// API key was newly generated for restarting node, so we need to remove it
ApplyRestart.removeGeneratedApiKey();
}
if (attempt == MAX_ATTEMPTS) {
LOGGER.error("Failed to shutdown node - giving up");
return false;
}
return true;
}
private static void removeGeneratedApiKey() {
try {
LOGGER.info("Removing newly generated API key...");
// Delete the API key since it was only generated for restarting node
ApiKey apiKey = new ApiKey();
apiKey.delete();
} catch (IOException e) {
LOGGER.info("Error loading or deleting API key: {}", e.getMessage());
}
}
private static void restartNode(String[] args) {
String javaHome = System.getProperty("java.home");
LOGGER.debug(() -> String.format("Java home: %s", javaHome));
Path javaBinary = Paths.get(javaHome, "bin", "java");
LOGGER.debug(() -> String.format("Java binary: %s", javaBinary));
Path exeLauncher = Paths.get(WINDOWS_EXE_LAUNCHER);
LOGGER.debug(() -> String.format("Windows EXE launcher: %s", exeLauncher));
List<String> javaCmd;
if (Files.exists(exeLauncher)) {
javaCmd = Arrays.asList(exeLauncher.toString());
} else {
javaCmd = new ArrayList<>();
// Java runtime binary itself
javaCmd.add(javaBinary.toString());
// JVM arguments
javaCmd.addAll(ManagementFactory.getRuntimeMXBean().getInputArguments());
// Reapply any retained, but disabled, -agentlib JVM arg
javaCmd = javaCmd.stream()
.map(arg -> arg.replace(AGENTLIB_JVM_HOLDER_ARG, "-agentlib"))
.collect(Collectors.toList());
// Call mainClass in JAR
javaCmd.addAll(Arrays.asList("-jar", JAR_FILENAME));
// Add saved command-line args
javaCmd.addAll(Arrays.asList(args));
}
try {
LOGGER.debug(String.format("Restarting node with: %s", String.join(" ", javaCmd)));
ProcessBuilder processBuilder = new ProcessBuilder(javaCmd);
if (Files.exists(exeLauncher)) {
LOGGER.debug(() -> String.format("Setting env %s to %s", JAVA_TOOL_OPTIONS_NAME, JAVA_TOOL_OPTIONS_VALUE));
processBuilder.environment().put(JAVA_TOOL_OPTIONS_NAME, JAVA_TOOL_OPTIONS_VALUE);
}
// New process will inherit our stdout and stderr
processBuilder.redirectOutput(ProcessBuilder.Redirect.INHERIT);
processBuilder.redirectError(ProcessBuilder.Redirect.INHERIT);
Process process = processBuilder.start();
// Nothing to pipe to new process, so close output stream (process's stdin)
process.getOutputStream().close();
} catch (Exception e) {
LOGGER.error(String.format("Failed to restart node (BAD): %s", e.getMessage()));
}
}
}

View File

@ -38,7 +38,7 @@ public class ApplyUpdate {
private static final String NEW_JAR_FILENAME = AutoUpdate.NEW_JAR_FILENAME;
private static final String WINDOWS_EXE_LAUNCHER = "qortal.exe";
private static final String JAVA_TOOL_OPTIONS_NAME = "JAVA_TOOL_OPTIONS";
private static final String JAVA_TOOL_OPTIONS_VALUE = "-XX:MaxRAMFraction=4";
private static final String JAVA_TOOL_OPTIONS_VALUE = "";
private static final long CHECK_INTERVAL = 30 * 1000L; // ms
private static final int MAX_ATTEMPTS = 12;
@ -139,7 +139,7 @@ public class ApplyUpdate {
apiKey.delete();
} catch (IOException e) {
LOGGER.info("Error loading or deleting API key: {}", e.getMessage());
LOGGER.error("Error loading or deleting API key: {}", e.getMessage());
}
}
@ -181,13 +181,13 @@ public class ApplyUpdate {
private static void restartNode(String[] args) {
String javaHome = System.getProperty("java.home");
LOGGER.info(() -> String.format("Java home: %s", javaHome));
LOGGER.debug(() -> String.format("Java home: %s", javaHome));
Path javaBinary = Paths.get(javaHome, "bin", "java");
LOGGER.info(() -> String.format("Java binary: %s", javaBinary));
LOGGER.debug(() -> String.format("Java binary: %s", javaBinary));
Path exeLauncher = Paths.get(WINDOWS_EXE_LAUNCHER);
LOGGER.info(() -> String.format("Windows EXE launcher: %s", exeLauncher));
LOGGER.debug(() -> String.format("Windows EXE launcher: %s", exeLauncher));
List<String> javaCmd;
if (Files.exists(exeLauncher)) {
@ -213,12 +213,12 @@ public class ApplyUpdate {
}
try {
LOGGER.info(String.format("Restarting node with: %s", String.join(" ", javaCmd)));
LOGGER.debug(String.format("Restarting node with: %s", String.join(" ", javaCmd)));
ProcessBuilder processBuilder = new ProcessBuilder(javaCmd);
if (Files.exists(exeLauncher)) {
LOGGER.info(() -> String.format("Setting env %s to %s", JAVA_TOOL_OPTIONS_NAME, JAVA_TOOL_OPTIONS_VALUE));
LOGGER.debug(() -> String.format("Setting env %s to %s", JAVA_TOOL_OPTIONS_NAME, JAVA_TOOL_OPTIONS_VALUE));
processBuilder.environment().put(JAVA_TOOL_OPTIONS_NAME, JAVA_TOOL_OPTIONS_VALUE);
}

View File

@ -363,5 +363,4 @@ public class SelfSponsorshipAlgoV1 {
return transactionDataList;
}
}
}

View File

@ -0,0 +1,249 @@
package org.qortal.account;
import org.qortal.api.resource.TransactionsResource;
import org.qortal.asset.Asset;
import org.qortal.block.BlockChain;
import org.qortal.data.account.AccountData;
import org.qortal.data.transaction.*;
import org.qortal.repository.DataException;
import org.qortal.repository.Repository;
import org.qortal.transaction.Transaction.TransactionType;
import java.util.*;
public class SelfSponsorshipAlgoV2 {
private final long snapshotTimestampV1 = BlockChain.getInstance().getSelfSponsorshipAlgoV1SnapshotTimestamp();
private final long snapshotTimestampV2 = BlockChain.getInstance().getSelfSponsorshipAlgoV2SnapshotTimestamp();
private final long referenceTimestamp = BlockChain.getInstance().getReferenceTimestampBlock();
private final boolean override;
private final Repository repository;
private final String address;
private int recentAssetSendCount = 0;
private int recentSponsorshipCount = 0;
private final Set<String> assetAddresses = new LinkedHashSet<>();
private final Set<String> penaltyAddresses = new LinkedHashSet<>();
private final Set<String> sponsorAddresses = new LinkedHashSet<>();
private List<RewardShareTransactionData> sponsorshipRewardShares = new ArrayList<>();
private List<TransferAssetTransactionData> transferAssetForAddress = new ArrayList<>();
public SelfSponsorshipAlgoV2(Repository repository, String address, boolean override) {
this.repository = repository;
this.address = address;
this.override = override;
}
public String getAddress() {
return this.address;
}
public Set<String> getPenaltyAddresses() {
return this.penaltyAddresses;
}
public void run() throws DataException {
if (!override) {
this.getAccountPrivs(this.address);
}
if (override) {
this.fetchTransferAssetForAddress(this.address);
this.findRecentAssetSendCount();
if (this.recentAssetSendCount >= 6) {
this.penaltyAddresses.add(this.address);
this.penaltyAddresses.addAll(this.assetAddresses);
}
}
}
private void getAccountPrivs(String address) throws DataException {
AccountData accountData = this.repository.getAccountRepository().getAccount(address);
List<TransactionData> transferPrivsTransactions = fetchTransferPrivsForAddress(address);
transferPrivsTransactions.removeIf(t -> t.getTimestamp() > this.referenceTimestamp || accountData.getAddress().equals(t.getRecipient()));
if (transferPrivsTransactions.isEmpty()) {
// Nothing to do
return;
}
for (TransactionData transactionData : transferPrivsTransactions) {
TransferPrivsTransactionData transferPrivsTransactionData = (TransferPrivsTransactionData) transactionData;
this.penaltyAddresses.add(transferPrivsTransactionData.getRecipient());
this.fetchSponsorshipRewardShares(transferPrivsTransactionData.getRecipient());
this.findRecentSponsorshipCount();
if (this.recentSponsorshipCount >= 1) {
this.penaltyAddresses.addAll(this.sponsorAddresses);
}
String newAddress = this.getDestinationAccount(transferPrivsTransactionData.getRecipient());
while (newAddress != null) {
// Found destination account
this.penaltyAddresses.add(newAddress);
this.fetchSponsorshipRewardShares(newAddress);
this.findRecentSponsorshipCount();
if (this.recentSponsorshipCount >= 1) {
this.penaltyAddresses.addAll(this.sponsorAddresses);
}
newAddress = this.getDestinationAccount(newAddress);
}
}
}
private String getDestinationAccount(String address) throws DataException {
AccountData accountData = this.repository.getAccountRepository().getAccount(address);
List<TransactionData> transferPrivsTransactions = fetchTransferPrivsForAddress(address);
transferPrivsTransactions.removeIf(t -> t.getTimestamp() > this.referenceTimestamp || accountData.getAddress().equals(t.getRecipient()));
if (transferPrivsTransactions.isEmpty()) {
return null;
}
if (accountData == null) {
return null;
}
for (TransactionData transactionData : transferPrivsTransactions) {
TransferPrivsTransactionData transferPrivsTransactionData = (TransferPrivsTransactionData) transactionData;
if (Arrays.equals(transferPrivsTransactionData.getSenderPublicKey(), accountData.getPublicKey())) {
return transferPrivsTransactionData.getRecipient();
}
}
return null;
}
private void fetchSponsorshipRewardShares(String address) throws DataException {
AccountData accountDataRs = this.repository.getAccountRepository().getAccount(address);
List<RewardShareTransactionData> sponsorshipRewardShares = new ArrayList<>();
// Define relevant transactions
List<TransactionType> txTypes = List.of(TransactionType.REWARD_SHARE);
List<TransactionData> transactionDataList = fetchTransactions(repository, txTypes, address, false);
for (TransactionData transactionData : transactionDataList) {
if (transactionData.getType() != TransactionType.REWARD_SHARE) {
continue;
}
RewardShareTransactionData rewardShareTransactionData = (RewardShareTransactionData) transactionData;
// Skip removals
if (rewardShareTransactionData.getSharePercent() < 0) {
continue;
}
// Skip if not sponsored by this account
if (!Arrays.equals(rewardShareTransactionData.getCreatorPublicKey(), accountDataRs.getPublicKey())) {
continue;
}
// Skip self shares
if (Objects.equals(rewardShareTransactionData.getRecipient(), address)) {
continue;
}
boolean duplicateFound = false;
for (RewardShareTransactionData existingRewardShare : sponsorshipRewardShares) {
if (Objects.equals(existingRewardShare.getRecipient(), rewardShareTransactionData.getRecipient())) {
// Duplicate
duplicateFound = true;
break;
}
}
if (!duplicateFound) {
sponsorshipRewardShares.add(rewardShareTransactionData);
this.sponsorAddresses.add(rewardShareTransactionData.getRecipient());
}
}
this.sponsorshipRewardShares = sponsorshipRewardShares;
}
private void fetchTransferAssetForAddress(String address) throws DataException {
List<TransferAssetTransactionData> transferAssetForAddress = new ArrayList<>();
// Define relevant transactions
List<TransactionType> txTypes = List.of(TransactionType.TRANSFER_ASSET);
List<TransactionData> transactionDataList = fetchTransactions(repository, txTypes, address, false);
transactionDataList.removeIf(t -> t.getTimestamp() <= this.snapshotTimestampV1 || t.getTimestamp() >= this.snapshotTimestampV2);
for (TransactionData transactionData : transactionDataList) {
if (transactionData.getType() != TransactionType.TRANSFER_ASSET) {
continue;
}
TransferAssetTransactionData transferAssetTransactionData = (TransferAssetTransactionData) transactionData;
if (transferAssetTransactionData.getAssetId() == Asset.QORT) {
if (!Objects.equals(transferAssetTransactionData.getRecipient(), address)) {
// Outgoing transfer asset for this account
transferAssetForAddress.add(transferAssetTransactionData);
this.assetAddresses.add(transferAssetTransactionData.getRecipient());
}
}
}
this.transferAssetForAddress = transferAssetForAddress;
}
private void findRecentSponsorshipCount() {
int recentSponsorshipCount = 0;
for (RewardShareTransactionData rewardShare : sponsorshipRewardShares) {
if (rewardShare.getTimestamp() >= this.snapshotTimestampV1) {
recentSponsorshipCount++;
}
}
this.recentSponsorshipCount = recentSponsorshipCount;
}
private void findRecentAssetSendCount() {
int recentAssetSendCount = 0;
for (TransferAssetTransactionData assetSend : transferAssetForAddress) {
if (assetSend.getTimestamp() >= this.snapshotTimestampV1) {
recentAssetSendCount++;
}
}
this.recentAssetSendCount = recentAssetSendCount;
}
private List<TransactionData> fetchTransferPrivsForAddress(String address) throws DataException {
return fetchTransactions(repository,
List.of(TransactionType.TRANSFER_PRIVS),
address, true);
}
private static List<TransactionData> fetchTransactions(Repository repository, List<TransactionType> txTypes, String address, boolean reverse) throws DataException {
// Fetch all relevant transactions for this account
List<byte[]> signatures = repository.getTransactionRepository()
.getSignaturesMatchingCriteria(null, null, null, txTypes,
null, null, address, TransactionsResource.ConfirmationStatus.CONFIRMED,
null, null, reverse);
List<TransactionData> transactionDataList = new ArrayList<>();
for (byte[] signature : signatures) {
// Fetch transaction data
TransactionData transactionData = repository.getTransactionRepository().fromSignature(signature);
if (transactionData == null) {
continue;
}
transactionDataList.add(transactionData);
}
return transactionDataList;
}
}

View File

@ -0,0 +1,370 @@
package org.qortal.account;
import org.qortal.api.resource.TransactionsResource;
import org.qortal.asset.Asset;
import org.qortal.data.account.AccountData;
import org.qortal.data.naming.NameData;
import org.qortal.data.transaction.*;
import org.qortal.repository.DataException;
import org.qortal.repository.Repository;
import org.qortal.transaction.Transaction.TransactionType;
import java.util.*;
import java.util.stream.Collectors;
public class SelfSponsorshipAlgoV3 {
private final Repository repository;
private final String address;
private final AccountData accountData;
private final long snapshotTimestampV1;
private final long snapshotTimestampV3;
private final boolean override;
private int registeredNameCount = 0;
private int suspiciousCount = 0;
private int suspiciousPercent = 0;
private int consolidationCount = 0;
private int bulkIssuanceCount = 0;
private int recentSponsorshipCount = 0;
private List<RewardShareTransactionData> sponsorshipRewardShares = new ArrayList<>();
private final Map<String, List<TransactionData>> paymentsByAddress = new HashMap<>();
private final Set<String> sponsees = new LinkedHashSet<>();
private Set<String> consolidatedAddresses = new LinkedHashSet<>();
private final Set<String> zeroTransactionAddreses = new LinkedHashSet<>();
private final Set<String> penaltyAddresses = new LinkedHashSet<>();
public SelfSponsorshipAlgoV3(Repository repository, String address, long snapshotTimestampV1, long snapshotTimestampV3, boolean override) throws DataException {
this.repository = repository;
this.address = address;
this.accountData = this.repository.getAccountRepository().getAccount(this.address);
this.snapshotTimestampV1 = snapshotTimestampV1;
this.snapshotTimestampV3 = snapshotTimestampV3;
this.override = override;
}
public String getAddress() {
return this.address;
}
public Set<String> getPenaltyAddresses() {
return this.penaltyAddresses;
}
public void run() throws DataException {
if (this.accountData == null) {
// Nothing to do
return;
}
this.fetchSponsorshipRewardShares();
if (this.sponsorshipRewardShares.isEmpty()) {
// Nothing to do
return;
}
this.findConsolidatedRewards();
this.findBulkIssuance();
this.findRegisteredNameCount();
this.findRecentSponsorshipCount();
int score = this.calculateScore();
if (score <= 0 && !override) {
return;
}
String newAddress = this.getDestinationAccount(this.address);
while (newAddress != null) {
// Found destination account
this.penaltyAddresses.add(newAddress);
// Run algo for this address, but in "override" mode because it has already been flagged
SelfSponsorshipAlgoV3 algoV3 = new SelfSponsorshipAlgoV3(this.repository, newAddress, this.snapshotTimestampV1, this.snapshotTimestampV3, true);
algoV3.run();
this.penaltyAddresses.addAll(algoV3.getPenaltyAddresses());
newAddress = this.getDestinationAccount(newAddress);
}
this.penaltyAddresses.add(this.address);
if (this.override || this.recentSponsorshipCount < 20) {
this.penaltyAddresses.addAll(this.consolidatedAddresses);
this.penaltyAddresses.addAll(this.zeroTransactionAddreses);
}
else {
this.penaltyAddresses.addAll(this.sponsees);
}
}
private String getDestinationAccount(String address) throws DataException {
List<TransactionData> transferPrivsTransactions = fetchTransferPrivsForAddress(address);
if (transferPrivsTransactions.isEmpty()) {
// No TRANSFER_PRIVS transactions for this address
return null;
}
AccountData accountData = this.repository.getAccountRepository().getAccount(address);
if (accountData == null) {
return null;
}
for (TransactionData transactionData : transferPrivsTransactions) {
TransferPrivsTransactionData transferPrivsTransactionData = (TransferPrivsTransactionData) transactionData;
if (Arrays.equals(transferPrivsTransactionData.getSenderPublicKey(), accountData.getPublicKey())) {
return transferPrivsTransactionData.getRecipient();
}
}
return null;
}
private void findConsolidatedRewards() throws DataException {
List<String> sponseesThatSentRewards = new ArrayList<>();
Map<String, Integer> paymentRecipients = new HashMap<>();
// Collect outgoing payments of each sponsee
for (String sponseeAddress : this.sponsees) {
// Firstly fetch all payments for address, since the functions below depend on this data
this.fetchPaymentsForAddress(sponseeAddress);
// Check if the address has zero relevant transactions
if (this.hasZeroTransactions(sponseeAddress)) {
this.zeroTransactionAddreses.add(sponseeAddress);
}
// Get payment recipients
List<String> allPaymentRecipients = this.fetchOutgoingPaymentRecipientsForAddress(sponseeAddress);
if (allPaymentRecipients.isEmpty()) {
continue;
}
sponseesThatSentRewards.add(sponseeAddress);
List<String> addressesPaidByThisSponsee = new ArrayList<>();
for (String paymentRecipient : allPaymentRecipients) {
if (addressesPaidByThisSponsee.contains(paymentRecipient)) {
// We already tracked this association - don't allow multiple to stack up
continue;
}
addressesPaidByThisSponsee.add(paymentRecipient);
// Increment count for this recipient, or initialize to 1 if not present
if (paymentRecipients.computeIfPresent(paymentRecipient, (k, v) -> v + 1) == null) {
paymentRecipients.put(paymentRecipient, 1);
}
}
}
// Exclude addresses with a low number of payments
Map<String, Integer> filteredPaymentRecipients = paymentRecipients.entrySet().stream()
.filter(p -> p.getValue() != null && p.getValue() >= 10)
.collect(Collectors.toMap(Map.Entry::getKey, Map.Entry::getValue));
// Now check how many sponsees have sent to this subset of addresses
Map<String, Integer> sponseesThatConsolidatedRewards = new HashMap<>();
for (String sponseeAddress : sponseesThatSentRewards) {
List<String> allPaymentRecipients = this.fetchOutgoingPaymentRecipientsForAddress(sponseeAddress);
// Remove any that aren't to one of the flagged recipients (i.e. consolidation)
allPaymentRecipients.removeIf(r -> !filteredPaymentRecipients.containsKey(r));
int count = allPaymentRecipients.size();
if (count == 0) {
continue;
}
if (sponseesThatConsolidatedRewards.computeIfPresent(sponseeAddress, (k, v) -> v + count) == null) {
sponseesThatConsolidatedRewards.put(sponseeAddress, count);
}
}
// Remove sponsees that have only sent a low number of payments to the filtered addresses
Map<String, Integer> filteredSponseesThatConsolidatedRewards = sponseesThatConsolidatedRewards.entrySet().stream()
.filter(p -> p.getValue() != null && p.getValue() >= 2)
.collect(Collectors.toMap(Map.Entry::getKey, Map.Entry::getValue));
this.consolidationCount = sponseesThatConsolidatedRewards.size();
this.consolidatedAddresses = new LinkedHashSet<>(filteredSponseesThatConsolidatedRewards.keySet());
this.suspiciousCount = this.consolidationCount + this.zeroTransactionAddreses.size();
this.suspiciousPercent = (int)(this.suspiciousCount / (float) this.sponsees.size() * 100);
}
private void findBulkIssuance() {
Long lastTimestamp = null;
for (RewardShareTransactionData rewardShareTransactionData : sponsorshipRewardShares) {
long timestamp = rewardShareTransactionData.getTimestamp();
if (timestamp >= this.snapshotTimestampV3) {
continue;
}
if (lastTimestamp != null) {
if (timestamp - lastTimestamp < 3*60*1000L) {
this.bulkIssuanceCount++;
}
}
lastTimestamp = timestamp;
}
}
private void findRegisteredNameCount() throws DataException {
int registeredNameCount = 0;
for (String sponseeAddress : sponsees) {
List<NameData> names = repository.getNameRepository().getNamesByOwner(sponseeAddress);
for (NameData name : names) {
if (name.getRegistered() < this.snapshotTimestampV3) {
registeredNameCount++;
break;
}
}
}
this.registeredNameCount = registeredNameCount;
}
private void findRecentSponsorshipCount() {
int recentSponsorshipCount = 0;
for (RewardShareTransactionData rewardShare : sponsorshipRewardShares) {
if (rewardShare.getTimestamp() >= this.snapshotTimestampV1) {
recentSponsorshipCount++;
}
}
this.recentSponsorshipCount = recentSponsorshipCount;
}
private int calculateScore() {
final int suspiciousMultiplier = (this.suspiciousCount >= 100) ? this.suspiciousPercent : 1;
final int nameMultiplier = (this.sponsees.size() >= 25 && this.registeredNameCount <= 1) ? 21 :
(this.sponsees.size() >= 15 && this.registeredNameCount <= 1) ? 11 :
(this.sponsees.size() >= 5 && this.registeredNameCount <= 1) ? 5 : 1;
final int consolidationMultiplier = Math.max(this.consolidationCount, 1);
final int bulkIssuanceMultiplier = Math.max(this.bulkIssuanceCount / 2, 1);
final int offset = 20;
return suspiciousMultiplier * nameMultiplier * consolidationMultiplier * bulkIssuanceMultiplier - offset;
}
private void fetchSponsorshipRewardShares() throws DataException {
List<RewardShareTransactionData> sponsorshipRewardShares = new ArrayList<>();
// Define relevant transactions
List<TransactionType> txTypes = List.of(TransactionType.REWARD_SHARE);
List<TransactionData> transactionDataList = fetchTransactions(repository, txTypes, this.address, false);
transactionDataList.removeIf(t -> t.getTimestamp() <= this.snapshotTimestampV1 || t.getTimestamp() >= this.snapshotTimestampV3);
for (TransactionData transactionData : transactionDataList) {
if (transactionData.getType() != TransactionType.REWARD_SHARE) {
continue;
}
RewardShareTransactionData rewardShareTransactionData = (RewardShareTransactionData) transactionData;
// Skip removals
if (rewardShareTransactionData.getSharePercent() < 0) {
continue;
}
// Skip if not sponsored by this account
if (!Arrays.equals(rewardShareTransactionData.getCreatorPublicKey(), accountData.getPublicKey())) {
continue;
}
// Skip self shares
if (Objects.equals(rewardShareTransactionData.getRecipient(), this.address)) {
continue;
}
boolean duplicateFound = false;
for (RewardShareTransactionData existingRewardShare : sponsorshipRewardShares) {
if (Objects.equals(existingRewardShare.getRecipient(), rewardShareTransactionData.getRecipient())) {
// Duplicate
duplicateFound = true;
break;
}
}
if (!duplicateFound) {
sponsorshipRewardShares.add(rewardShareTransactionData);
this.sponsees.add(rewardShareTransactionData.getRecipient());
}
}
this.sponsorshipRewardShares = sponsorshipRewardShares;
}
private List<TransactionData> fetchTransferPrivsForAddress(String address) throws DataException {
return fetchTransactions(repository,
List.of(TransactionType.TRANSFER_PRIVS),
address, true);
}
private void fetchPaymentsForAddress(String address) throws DataException {
List<TransactionData> payments = fetchTransactions(repository,
Arrays.asList(TransactionType.PAYMENT, TransactionType.TRANSFER_ASSET),
address, false);
this.paymentsByAddress.put(address, payments);
}
private List<String> fetchOutgoingPaymentRecipientsForAddress(String address) {
List<String> outgoingPaymentRecipients = new ArrayList<>();
List<TransactionData> transactionDataList = this.paymentsByAddress.get(address);
if (transactionDataList == null) transactionDataList = new ArrayList<>();
transactionDataList.removeIf(t -> t.getTimestamp() <= this.snapshotTimestampV1 || t.getTimestamp() >= this.snapshotTimestampV3);
for (TransactionData transactionData : transactionDataList) {
switch (transactionData.getType()) {
case PAYMENT:
PaymentTransactionData paymentTransactionData = (PaymentTransactionData) transactionData;
if (!Objects.equals(paymentTransactionData.getRecipient(), address)) {
// Outgoing payment from this account
outgoingPaymentRecipients.add(paymentTransactionData.getRecipient());
}
break;
case TRANSFER_ASSET:
TransferAssetTransactionData transferAssetTransactionData = (TransferAssetTransactionData) transactionData;
if (transferAssetTransactionData.getAssetId() == Asset.QORT) {
if (!Objects.equals(transferAssetTransactionData.getRecipient(), address)) {
// Outgoing payment from this account
outgoingPaymentRecipients.add(transferAssetTransactionData.getRecipient());
}
}
break;
default:
break;
}
}
return outgoingPaymentRecipients;
}
private boolean hasZeroTransactions(String address) {
List<TransactionData> transactionDataList = this.paymentsByAddress.get(address);
if (transactionDataList == null) {
return true;
}
transactionDataList.removeIf(t -> t.getTimestamp() <= this.snapshotTimestampV1 || t.getTimestamp() >= this.snapshotTimestampV3);
return transactionDataList.size() == 0;
}
private static List<TransactionData> fetchTransactions(Repository repository, List<TransactionType> txTypes, String address, boolean reverse) throws DataException {
// Fetch all relevant transactions for this account
List<byte[]> signatures = repository.getTransactionRepository()
.getSignaturesMatchingCriteria(null, null, null, txTypes,
null, null, address, TransactionsResource.ConfirmationStatus.CONFIRMED,
null, null, reverse);
List<TransactionData> transactionDataList = new ArrayList<>();
for (byte[] signature : signatures) {
// Fetch transaction data
TransactionData transactionData = repository.getTransactionRepository().fromSignature(signature);
if (transactionData == null) {
continue;
}
transactionDataList.add(transactionData);
}
return transactionDataList;
}
}

View File

@ -20,17 +20,25 @@ public class PollVotes {
@Schema(description = "Total number of votes")
public Integer totalVotes;
@Schema(description = "Total weight of votes")
public Integer totalWeight;
@Schema(description = "List of vote counts for each option")
public List<OptionCount> voteCounts;
@Schema(description = "List of vote weights for each option")
public List<OptionWeight> voteWeights;
// For JAX-RS
protected PollVotes() {
}
public PollVotes(List<VoteOnPollData> votes, Integer totalVotes, List<OptionCount> voteCounts) {
public PollVotes(List<VoteOnPollData> votes, Integer totalVotes, Integer totalWeight, List<OptionCount> voteCounts, List<OptionWeight> voteWeights) {
this.votes = votes;
this.totalVotes = totalVotes;
this.totalWeight = totalWeight;
this.voteCounts = voteCounts;
this.voteWeights = voteWeights;
}
@Schema(description = "Vote info")
@ -52,4 +60,24 @@ public class PollVotes {
this.voteCount = voteCount;
}
}
@Schema(description = "Vote weights")
// All properties to be converted to JSON via JAX-RS
@XmlAccessorType(XmlAccessType.FIELD)
public static class OptionWeight {
@Schema(description = "Option name")
public String optionName;
@Schema(description = "Vote weight")
public Integer voteWeight;
// For JAX-RS
protected OptionWeight() {
}
public OptionWeight(String optionName, Integer voteWeight) {
this.optionName = optionName;
this.voteWeight = voteWeight;
}
}
}

View File

@ -8,6 +8,7 @@ import io.swagger.v3.oas.annotations.parameters.RequestBody;
import io.swagger.v3.oas.annotations.responses.ApiResponse;
import io.swagger.v3.oas.annotations.security.SecurityRequirement;
import io.swagger.v3.oas.annotations.tags.Tag;
import org.bitcoinj.core.Coin;
import org.bitcoinj.core.Transaction;
import org.qortal.api.ApiError;
import org.qortal.api.ApiErrors;
@ -19,6 +20,7 @@ import org.qortal.crosschain.AddressInfo;
import org.qortal.crosschain.Bitcoin;
import org.qortal.crosschain.ForeignBlockchainException;
import org.qortal.crosschain.SimpleTransaction;
import org.qortal.crosschain.ServerConfigurationInfo;
import javax.servlet.http.HttpServletRequest;
import javax.ws.rs.GET;
@ -188,45 +190,6 @@ public class CrossChainBitcoinResource {
}
}
@POST
@Path("/unusedaddress")
@Operation(
summary = "Returns first unused address for hierarchical, deterministic BIP32 wallet",
description = "Supply BIP32 'm' private/public key in base58, starting with 'xprv'/'xpub' for mainnet, 'tprv'/'tpub' for testnet",
requestBody = @RequestBody(
required = true,
content = @Content(
mediaType = MediaType.TEXT_PLAIN,
schema = @Schema(
type = "string",
description = "BIP32 'm' private/public key in base58",
example = "tpubD6NzVbkrYhZ4XTPc4btCZ6SMgn8CxmWkj6VBVZ1tfcJfMq4UwAjZbG8U74gGSypL9XBYk2R2BLbDBe8pcEyBKM1edsGQEPKXNbEskZozeZc"
)
)
),
responses = {
@ApiResponse(
content = @Content(array = @ArraySchema( schema = @Schema( implementation = SimpleTransaction.class ) ) )
)
}
)
@ApiErrors({ApiError.INVALID_PRIVATE_KEY, ApiError.FOREIGN_BLOCKCHAIN_NETWORK_ISSUE})
@SecurityRequirement(name = "apiKey")
public String getUnusedBitcoinReceiveAddress(@HeaderParam(Security.API_KEY_HEADER) String apiKey, String key58) {
Security.checkApiCallAllowed(request);
Bitcoin bitcoin = Bitcoin.getInstance();
if (!bitcoin.isValidDeterministicKey(key58))
throw ApiExceptionFactory.INSTANCE.createException(request, ApiError.INVALID_PRIVATE_KEY);
try {
return bitcoin.getUnusedReceiveAddress(key58);
} catch (ForeignBlockchainException e) {
throw ApiExceptionFactory.INSTANCE.createException(request, ApiError.FOREIGN_BLOCKCHAIN_NETWORK_ISSUE);
}
}
@POST
@Path("/send")
@Operation(
@ -283,4 +246,137 @@ public class CrossChainBitcoinResource {
return spendTransaction.getTxId().toString();
}
@GET
@Path("/serverinfos")
@Operation(
summary = "Returns current Bitcoin server configuration",
description = "Returns current Bitcoin server locations and use status",
responses = {
@ApiResponse(
content = @Content(
mediaType = MediaType.APPLICATION_JSON,
schema = @Schema(
implementation = ServerConfigurationInfo.class
)
)
)
}
)
public ServerConfigurationInfo getServerConfiguration() {
return CrossChainUtils.buildServerConfigurationInfo(Bitcoin.getInstance());
}
@GET
@Path("/feekb")
@Operation(
summary = "Returns Bitcoin fee per Kb.",
description = "Returns Bitcoin fee per Kb.",
responses = {
@ApiResponse(
content = @Content(
schema = @Schema(
type = "number"
)
)
)
}
)
public String getBitcoinFeePerKb() {
Bitcoin bitcoin = Bitcoin.getInstance();
return String.valueOf(bitcoin.getFeePerKb().value);
}
@POST
@Path("/updatefeekb")
@Operation(
summary = "Sets Bitcoin fee per Kb.",
description = "Sets Bitcoin fee per Kb.",
requestBody = @RequestBody(
required = true,
content = @Content(
mediaType = MediaType.TEXT_PLAIN,
schema = @Schema(
type = "number",
description = "the fee per Kb",
example = "100"
)
)
),
responses = {
@ApiResponse(
content = @Content(mediaType = MediaType.TEXT_PLAIN, schema = @Schema(type = "number", description = "fee"))
)
}
)
@ApiErrors({ApiError.INVALID_PRIVATE_KEY, ApiError.INVALID_CRITERIA})
public String setBitcoinFeePerKb(@HeaderParam(Security.API_KEY_HEADER) String apiKey, String fee) {
Security.checkApiCallAllowed(request);
Bitcoin bitcoin = Bitcoin.getInstance();
try {
return CrossChainUtils.setFeePerKb(bitcoin, fee);
} catch (IllegalArgumentException e) {
throw ApiExceptionFactory.INSTANCE.createException(request, ApiError.INVALID_CRITERIA);
}
}
@GET
@Path("/feeceiling")
@Operation(
summary = "Returns Bitcoin fee per Kb.",
description = "Returns Bitcoin fee per Kb.",
responses = {
@ApiResponse(
content = @Content(
schema = @Schema(
type = "number"
)
)
)
}
)
public String getBitcoinFeeCeiling() {
Bitcoin bitcoin = Bitcoin.getInstance();
return String.valueOf(bitcoin.getFeeCeiling());
}
@POST
@Path("/updatefeeceiling")
@Operation(
summary = "Sets Bitcoin fee ceiling.",
description = "Sets Bitcoin fee ceiling.",
requestBody = @RequestBody(
required = true,
content = @Content(
mediaType = MediaType.TEXT_PLAIN,
schema = @Schema(
type = "number",
description = "the fee",
example = "100"
)
)
),
responses = {
@ApiResponse(
content = @Content(mediaType = MediaType.TEXT_PLAIN, schema = @Schema(type = "number", description = "fee"))
)
}
)
@ApiErrors({ApiError.INVALID_PRIVATE_KEY, ApiError.INVALID_CRITERIA})
public String setBitcoinFeeCeiling(@HeaderParam(Security.API_KEY_HEADER) String apiKey, String fee) {
Security.checkApiCallAllowed(request);
Bitcoin bitcoin = Bitcoin.getInstance();
try {
return CrossChainUtils.setFeeCeiling(bitcoin, fee);
}
catch (IllegalArgumentException e) {
throw ApiExceptionFactory.INSTANCE.createException(request, ApiError.INVALID_CRITERIA);
}
}
}

View File

@ -19,6 +19,7 @@ import org.qortal.crosschain.AddressInfo;
import org.qortal.crosschain.Digibyte;
import org.qortal.crosschain.ForeignBlockchainException;
import org.qortal.crosschain.SimpleTransaction;
import org.qortal.crosschain.ServerConfigurationInfo;
import javax.servlet.http.HttpServletRequest;
import javax.ws.rs.GET;
@ -188,45 +189,6 @@ public class CrossChainDigibyteResource {
}
}
@POST
@Path("/unusedaddress")
@Operation(
summary = "Returns first unused address for hierarchical, deterministic BIP32 wallet",
description = "Supply BIP32 'm' private/public key in base58, starting with 'xprv'/'xpub' for mainnet, 'tprv'/'tpub' for testnet",
requestBody = @RequestBody(
required = true,
content = @Content(
mediaType = MediaType.TEXT_PLAIN,
schema = @Schema(
type = "string",
description = "BIP32 'm' private/public key in base58",
example = "tpubD6NzVbkrYhZ4XTPc4btCZ6SMgn8CxmWkj6VBVZ1tfcJfMq4UwAjZbG8U74gGSypL9XBYk2R2BLbDBe8pcEyBKM1edsGQEPKXNbEskZozeZc"
)
)
),
responses = {
@ApiResponse(
content = @Content(array = @ArraySchema( schema = @Schema( implementation = SimpleTransaction.class ) ) )
)
}
)
@ApiErrors({ApiError.INVALID_PRIVATE_KEY, ApiError.FOREIGN_BLOCKCHAIN_NETWORK_ISSUE})
@SecurityRequirement(name = "apiKey")
public String getUnusedDigibyteReceiveAddress(@HeaderParam(Security.API_KEY_HEADER) String apiKey, String key58) {
Security.checkApiCallAllowed(request);
Digibyte digibyte = Digibyte.getInstance();
if (!digibyte.isValidDeterministicKey(key58))
throw ApiExceptionFactory.INSTANCE.createException(request, ApiError.INVALID_PRIVATE_KEY);
try {
return digibyte.getUnusedReceiveAddress(key58);
} catch (ForeignBlockchainException e) {
throw ApiExceptionFactory.INSTANCE.createException(request, ApiError.FOREIGN_BLOCKCHAIN_NETWORK_ISSUE);
}
}
@POST
@Path("/send")
@Operation(
@ -283,4 +245,137 @@ public class CrossChainDigibyteResource {
return spendTransaction.getTxId().toString();
}
}
@GET
@Path("/serverinfos")
@Operation(
summary = "Returns current Digibyte server configuration",
description = "Returns current Digibyte server locations and use status",
responses = {
@ApiResponse(
content = @Content(
mediaType = MediaType.APPLICATION_JSON,
schema = @Schema(
implementation = ServerConfigurationInfo.class
)
)
)
}
)
public ServerConfigurationInfo getServerConfiguration() {
return CrossChainUtils.buildServerConfigurationInfo(Digibyte.getInstance());
}
@GET
@Path("/feekb")
@Operation(
summary = "Returns Digibyte fee per Kb.",
description = "Returns Digibyte fee per Kb.",
responses = {
@ApiResponse(
content = @Content(
schema = @Schema(
type = "number"
)
)
)
}
)
public String getDigibyteFeePerKb() {
Digibyte digibyte = Digibyte.getInstance();
return String.valueOf(digibyte.getFeePerKb().value);
}
@POST
@Path("/updatefeekb")
@Operation(
summary = "Sets Digibyte fee per Kb.",
description = "Sets Digibyte fee per Kb.",
requestBody = @RequestBody(
required = true,
content = @Content(
mediaType = MediaType.TEXT_PLAIN,
schema = @Schema(
type = "number",
description = "the fee per Kb",
example = "100"
)
)
),
responses = {
@ApiResponse(
content = @Content(mediaType = MediaType.TEXT_PLAIN, schema = @Schema(type = "number", description = "fee"))
)
}
)
@ApiErrors({ApiError.INVALID_PRIVATE_KEY, ApiError.INVALID_CRITERIA})
public String setDigibyteFeePerKb(@HeaderParam(Security.API_KEY_HEADER) String apiKey, String fee) {
Security.checkApiCallAllowed(request);
Digibyte digibyte = Digibyte.getInstance();
try {
return CrossChainUtils.setFeePerKb(digibyte, fee);
} catch (IllegalArgumentException e) {
throw ApiExceptionFactory.INSTANCE.createException(request, ApiError.INVALID_CRITERIA);
}
}
@GET
@Path("/feeceiling")
@Operation(
summary = "Returns Digibyte fee per Kb.",
description = "Returns Digibyte fee per Kb.",
responses = {
@ApiResponse(
content = @Content(
schema = @Schema(
type = "number"
)
)
)
}
)
public String getDigibyteFeeCeiling() {
Digibyte digibyte = Digibyte.getInstance();
return String.valueOf(digibyte.getFeeCeiling());
}
@POST
@Path("/updatefeeceiling")
@Operation(
summary = "Sets Digibyte fee ceiling.",
description = "Sets Digibyte fee ceiling.",
requestBody = @RequestBody(
required = true,
content = @Content(
mediaType = MediaType.TEXT_PLAIN,
schema = @Schema(
type = "number",
description = "the fee",
example = "100"
)
)
),
responses = {
@ApiResponse(
content = @Content(mediaType = MediaType.TEXT_PLAIN, schema = @Schema(type = "number", description = "fee"))
)
}
)
@ApiErrors({ApiError.INVALID_PRIVATE_KEY, ApiError.INVALID_CRITERIA})
public String setDigibyteFeeCeiling(@HeaderParam(Security.API_KEY_HEADER) String apiKey, String fee) {
Security.checkApiCallAllowed(request);
Digibyte digibyte = Digibyte.getInstance();
try {
return CrossChainUtils.setFeeCeiling(digibyte, fee);
}
catch (IllegalArgumentException e) {
throw ApiExceptionFactory.INSTANCE.createException(request, ApiError.INVALID_CRITERIA);
}
}
}

View File

@ -19,6 +19,7 @@ import org.qortal.crosschain.AddressInfo;
import org.qortal.crosschain.Dogecoin;
import org.qortal.crosschain.ForeignBlockchainException;
import org.qortal.crosschain.SimpleTransaction;
import org.qortal.crosschain.ServerConfigurationInfo;
import javax.servlet.http.HttpServletRequest;
import javax.ws.rs.GET;
@ -188,45 +189,6 @@ public class CrossChainDogecoinResource {
}
}
@POST
@Path("/unusedaddress")
@Operation(
summary = "Returns first unused address for hierarchical, deterministic BIP32 wallet",
description = "Supply BIP32 'm' private/public key in base58, starting with 'xprv'/'xpub' for mainnet, 'tprv'/'tpub' for testnet",
requestBody = @RequestBody(
required = true,
content = @Content(
mediaType = MediaType.TEXT_PLAIN,
schema = @Schema(
type = "string",
description = "BIP32 'm' private/public key in base58",
example = "tpubD6NzVbkrYhZ4XTPc4btCZ6SMgn8CxmWkj6VBVZ1tfcJfMq4UwAjZbG8U74gGSypL9XBYk2R2BLbDBe8pcEyBKM1edsGQEPKXNbEskZozeZc"
)
)
),
responses = {
@ApiResponse(
content = @Content(array = @ArraySchema( schema = @Schema( implementation = SimpleTransaction.class ) ) )
)
}
)
@ApiErrors({ApiError.INVALID_PRIVATE_KEY, ApiError.FOREIGN_BLOCKCHAIN_NETWORK_ISSUE})
@SecurityRequirement(name = "apiKey")
public String getUnusedDogecoinReceiveAddress(@HeaderParam(Security.API_KEY_HEADER) String apiKey, String key58) {
Security.checkApiCallAllowed(request);
Dogecoin dogecoin = Dogecoin.getInstance();
if (!dogecoin.isValidDeterministicKey(key58))
throw ApiExceptionFactory.INSTANCE.createException(request, ApiError.INVALID_PRIVATE_KEY);
try {
return dogecoin.getUnusedReceiveAddress(key58);
} catch (ForeignBlockchainException e) {
throw ApiExceptionFactory.INSTANCE.createException(request, ApiError.FOREIGN_BLOCKCHAIN_NETWORK_ISSUE);
}
}
@POST
@Path("/send")
@Operation(
@ -283,4 +245,137 @@ public class CrossChainDogecoinResource {
return spendTransaction.getTxId().toString();
}
}
@GET
@Path("/serverinfos")
@Operation(
summary = "Returns current Dogecoin server configuration",
description = "Returns current Dogecoin server locations and use status",
responses = {
@ApiResponse(
content = @Content(
mediaType = MediaType.APPLICATION_JSON,
schema = @Schema(
implementation = ServerConfigurationInfo.class
)
)
)
}
)
public ServerConfigurationInfo getServerConfiguration() {
return CrossChainUtils.buildServerConfigurationInfo(Dogecoin.getInstance());
}
@GET
@Path("/feekb")
@Operation(
summary = "Returns Dogecoin fee per Kb.",
description = "Returns Dogecoin fee per Kb.",
responses = {
@ApiResponse(
content = @Content(
schema = @Schema(
type = "number"
)
)
)
}
)
public String getDogecoinFeePerKb() {
Dogecoin dogecoin = Dogecoin.getInstance();
return String.valueOf(dogecoin.getFeePerKb().value);
}
@POST
@Path("/updatefeekb")
@Operation(
summary = "Sets Dogecoin fee per Kb.",
description = "Sets Dogecoin fee per Kb.",
requestBody = @RequestBody(
required = true,
content = @Content(
mediaType = MediaType.TEXT_PLAIN,
schema = @Schema(
type = "number",
description = "the fee per Kb",
example = "100"
)
)
),
responses = {
@ApiResponse(
content = @Content(mediaType = MediaType.TEXT_PLAIN, schema = @Schema(type = "number", description = "fee"))
)
}
)
@ApiErrors({ApiError.INVALID_PRIVATE_KEY, ApiError.INVALID_CRITERIA})
public String setDogecoinFeePerKb(@HeaderParam(Security.API_KEY_HEADER) String apiKey, String fee) {
Security.checkApiCallAllowed(request);
Dogecoin dogecoin = Dogecoin.getInstance();
try {
return CrossChainUtils.setFeePerKb(dogecoin, fee);
} catch (IllegalArgumentException e) {
throw ApiExceptionFactory.INSTANCE.createException(request, ApiError.INVALID_CRITERIA);
}
}
@GET
@Path("/feeceiling")
@Operation(
summary = "Returns Dogecoin fee per Kb.",
description = "Returns Dogecoin fee per Kb.",
responses = {
@ApiResponse(
content = @Content(
schema = @Schema(
type = "number"
)
)
)
}
)
public String getDogecoinFeeCeiling() {
Dogecoin dogecoin = Dogecoin.getInstance();
return String.valueOf(dogecoin.getFeeCeiling());
}
@POST
@Path("/updatefeeceiling")
@Operation(
summary = "Sets Dogecoin fee ceiling.",
description = "Sets Dogecoin fee ceiling.",
requestBody = @RequestBody(
required = true,
content = @Content(
mediaType = MediaType.TEXT_PLAIN,
schema = @Schema(
type = "number",
description = "the fee",
example = "100"
)
)
),
responses = {
@ApiResponse(
content = @Content(mediaType = MediaType.TEXT_PLAIN, schema = @Schema(type = "number", description = "fee"))
)
}
)
@ApiErrors({ApiError.INVALID_PRIVATE_KEY, ApiError.INVALID_CRITERIA})
public String setDogecoinFeeCeiling(@HeaderParam(Security.API_KEY_HEADER) String apiKey, String fee) {
Security.checkApiCallAllowed(request);
Dogecoin dogecoin = Dogecoin.getInstance();
try {
return CrossChainUtils.setFeeCeiling(dogecoin, fee);
}
catch (IllegalArgumentException e) {
throw ApiExceptionFactory.INSTANCE.createException(request, ApiError.INVALID_CRITERIA);
}
}
}

View File

@ -19,6 +19,7 @@ import org.qortal.crosschain.AddressInfo;
import org.qortal.crosschain.ForeignBlockchainException;
import org.qortal.crosschain.Litecoin;
import org.qortal.crosschain.SimpleTransaction;
import org.qortal.crosschain.ServerConfigurationInfo;
import javax.servlet.http.HttpServletRequest;
import javax.ws.rs.GET;
@ -188,45 +189,6 @@ public class CrossChainLitecoinResource {
}
}
@POST
@Path("/unusedaddress")
@Operation(
summary = "Returns first unused address for hierarchical, deterministic BIP32 wallet",
description = "Supply BIP32 'm' private/public key in base58, starting with 'xprv'/'xpub' for mainnet, 'tprv'/'tpub' for testnet",
requestBody = @RequestBody(
required = true,
content = @Content(
mediaType = MediaType.TEXT_PLAIN,
schema = @Schema(
type = "string",
description = "BIP32 'm' private/public key in base58",
example = "tpubD6NzVbkrYhZ4XTPc4btCZ6SMgn8CxmWkj6VBVZ1tfcJfMq4UwAjZbG8U74gGSypL9XBYk2R2BLbDBe8pcEyBKM1edsGQEPKXNbEskZozeZc"
)
)
),
responses = {
@ApiResponse(
content = @Content(array = @ArraySchema( schema = @Schema( implementation = SimpleTransaction.class ) ) )
)
}
)
@ApiErrors({ApiError.INVALID_PRIVATE_KEY, ApiError.FOREIGN_BLOCKCHAIN_NETWORK_ISSUE})
@SecurityRequirement(name = "apiKey")
public String getUnusedLitecoinReceiveAddress(@HeaderParam(Security.API_KEY_HEADER) String apiKey, String key58) {
Security.checkApiCallAllowed(request);
Litecoin litecoin = Litecoin.getInstance();
if (!litecoin.isValidDeterministicKey(key58))
throw ApiExceptionFactory.INSTANCE.createException(request, ApiError.INVALID_PRIVATE_KEY);
try {
return litecoin.getUnusedReceiveAddress(key58);
} catch (ForeignBlockchainException e) {
throw ApiExceptionFactory.INSTANCE.createException(request, ApiError.FOREIGN_BLOCKCHAIN_NETWORK_ISSUE);
}
}
@POST
@Path("/send")
@Operation(
@ -283,4 +245,176 @@ public class CrossChainLitecoinResource {
return spendTransaction.getTxId().toString();
}
}
@GET
@Path("/serverinfos")
@Operation(
summary = "Returns current Litecoin server configuration",
description = "Returns current Litecoin server locations and use status",
responses = {
@ApiResponse(
content = @Content(
mediaType = MediaType.APPLICATION_JSON,
schema = @Schema(
implementation = ServerConfigurationInfo.class
)
)
)
}
)
public ServerConfigurationInfo getServerConfiguration() {
return CrossChainUtils.buildServerConfigurationInfo(Litecoin.getInstance());
}
@POST
@Path("/repair")
@Operation(
summary = "Sends all coins in wallet to primary receive address",
description = "Supply BIP32 'm' private/public key in base58, starting with 'xprv'/'xpub' for mainnet, 'tprv'/'tpub' for testnet",
requestBody = @RequestBody(
required = true,
content = @Content(
mediaType = MediaType.TEXT_PLAIN,
schema = @Schema(
type = "string",
description = "BIP32 'm' private/public key in base58",
example = "tpubD6NzVbkrYhZ4XTPc4btCZ6SMgn8CxmWkj6VBVZ1tfcJfMq4UwAjZbG8U74gGSypL9XBYk2R2BLbDBe8pcEyBKM1edsGQEPKXNbEskZozeZc"
)
)
),
responses = {
@ApiResponse(
content = @Content(mediaType = MediaType.TEXT_PLAIN, schema = @Schema(type = "string", description = "transaction hash"))
)
}
)
@ApiErrors({ApiError.INVALID_PRIVATE_KEY, ApiError.FOREIGN_BLOCKCHAIN_NETWORK_ISSUE})
@SecurityRequirement(name = "apiKey")
public String repairOldWallet(@HeaderParam(Security.API_KEY_HEADER) String apiKey, String key58) {
Security.checkApiCallAllowed(request);
Litecoin litecoin = Litecoin.getInstance();
if (!litecoin.isValidDeterministicKey(key58))
throw ApiExceptionFactory.INSTANCE.createException(request, ApiError.INVALID_PRIVATE_KEY);
try {
return litecoin.repairOldWallet(key58);
} catch (ForeignBlockchainException e) {
throw ApiExceptionFactory.INSTANCE.createException(request, ApiError.FOREIGN_BLOCKCHAIN_NETWORK_ISSUE);
}
}
@GET
@Path("/feekb")
@Operation(
summary = "Returns Litecoin fee per Kb.",
description = "Returns Litecoin fee per Kb.",
responses = {
@ApiResponse(
content = @Content(
schema = @Schema(
type = "number"
)
)
)
}
)
public String getLitecoinFeePerKb() {
Litecoin litecoin = Litecoin.getInstance();
return String.valueOf(litecoin.getFeePerKb().value);
}
@POST
@Path("/updatefeekb")
@Operation(
summary = "Sets Litecoin fee per Kb.",
description = "Sets Litecoin fee per Kb.",
requestBody = @RequestBody(
required = true,
content = @Content(
mediaType = MediaType.TEXT_PLAIN,
schema = @Schema(
type = "number",
description = "the fee per Kb",
example = "100"
)
)
),
responses = {
@ApiResponse(
content = @Content(mediaType = MediaType.TEXT_PLAIN, schema = @Schema(type = "number", description = "fee"))
)
}
)
@ApiErrors({ApiError.INVALID_PRIVATE_KEY, ApiError.INVALID_CRITERIA})
public String setLitecoinFeePerKb(@HeaderParam(Security.API_KEY_HEADER) String apiKey, String fee) {
Security.checkApiCallAllowed(request);
Litecoin litecoin = Litecoin.getInstance();
try {
return CrossChainUtils.setFeePerKb(litecoin, fee);
} catch (IllegalArgumentException e) {
throw ApiExceptionFactory.INSTANCE.createException(request, ApiError.INVALID_CRITERIA);
}
}
@GET
@Path("/feeceiling")
@Operation(
summary = "Returns Litecoin fee per Kb.",
description = "Returns Litecoin fee per Kb.",
responses = {
@ApiResponse(
content = @Content(
schema = @Schema(
type = "number"
)
)
)
}
)
public String getLitecoinFeeCeiling() {
Litecoin litecoin = Litecoin.getInstance();
return String.valueOf(litecoin.getFeeCeiling());
}
@POST
@Path("/updatefeeceiling")
@Operation(
summary = "Sets Litecoin fee ceiling.",
description = "Sets Litecoin fee ceiling.",
requestBody = @RequestBody(
required = true,
content = @Content(
mediaType = MediaType.TEXT_PLAIN,
schema = @Schema(
type = "number",
description = "the fee",
example = "100"
)
)
),
responses = {
@ApiResponse(
content = @Content(mediaType = MediaType.TEXT_PLAIN, schema = @Schema(type = "number", description = "fee"))
)
}
)
@ApiErrors({ApiError.INVALID_PRIVATE_KEY, ApiError.INVALID_CRITERIA})
public String setLitecoinFeeCeiling(@HeaderParam(Security.API_KEY_HEADER) String apiKey, String fee) {
Security.checkApiCallAllowed(request);
Litecoin litecoin = Litecoin.getInstance();
try {
return CrossChainUtils.setFeeCeiling(litecoin, fee);
}
catch (IllegalArgumentException e) {
throw ApiExceptionFactory.INSTANCE.createException(request, ApiError.INVALID_CRITERIA);
}
}
}

View File

@ -16,6 +16,7 @@ import org.qortal.api.model.crosschain.PirateChainSendRequest;
import org.qortal.crosschain.ForeignBlockchainException;
import org.qortal.crosschain.PirateChain;
import org.qortal.crosschain.SimpleTransaction;
import org.qortal.crosschain.ServerConfigurationInfo;
import javax.servlet.http.HttpServletRequest;
import javax.ws.rs.GET;
@ -329,4 +330,138 @@ public class CrossChainPirateChainResource {
throw ApiExceptionFactory.INSTANCE.createCustomException(request, ApiError.FOREIGN_BLOCKCHAIN_NETWORK_ISSUE, e.getMessage());
}
}
@GET
@Path("/serverinfos")
@Operation(
summary = "Returns current PirateChain server configuration",
description = "Returns current PirateChain server locations and use status",
responses = {
@ApiResponse(
content = @Content(
mediaType = MediaType.APPLICATION_JSON,
schema = @Schema(
implementation = ServerConfigurationInfo.class
)
)
)
}
)
public ServerConfigurationInfo getServerConfiguration() {
return CrossChainUtils.buildServerConfigurationInfo(PirateChain.getInstance());
}
@GET
@Path("/feekb")
@Operation(
summary = "Returns PirateChain fee per Kb.",
description = "Returns PirateChain fee per Kb.",
responses = {
@ApiResponse(
content = @Content(
schema = @Schema(
type = "number"
)
)
)
}
)
public String getPirateChainFeePerKb() {
PirateChain pirateChain = PirateChain.getInstance();
return String.valueOf(pirateChain.getFeePerKb().value);
}
@POST
@Path("/updatefeekb")
@Operation(
summary = "Sets PirateChain fee per Kb.",
description = "Sets PirateChain fee per Kb.",
requestBody = @RequestBody(
required = true,
content = @Content(
mediaType = MediaType.TEXT_PLAIN,
schema = @Schema(
type = "number",
description = "the fee per Kb",
example = "100"
)
)
),
responses = {
@ApiResponse(
content = @Content(mediaType = MediaType.TEXT_PLAIN, schema = @Schema(type = "number", description = "fee"))
)
}
)
@ApiErrors({ApiError.INVALID_PRIVATE_KEY, ApiError.INVALID_CRITERIA})
public String setPirateChainFeePerKb(@HeaderParam(Security.API_KEY_HEADER) String apiKey, String fee) {
Security.checkApiCallAllowed(request);
PirateChain pirateChain = PirateChain.getInstance();
try {
return CrossChainUtils.setFeePerKb(pirateChain, fee);
} catch (IllegalArgumentException e) {
throw ApiExceptionFactory.INSTANCE.createException(request, ApiError.INVALID_CRITERIA);
}
}
@GET
@Path("/feeceiling")
@Operation(
summary = "Returns PirateChain fee per Kb.",
description = "Returns PirateChain fee per Kb.",
responses = {
@ApiResponse(
content = @Content(
schema = @Schema(
type = "number"
)
)
)
}
)
public String getPirateChainFeeCeiling() {
PirateChain pirateChain = PirateChain.getInstance();
return String.valueOf(pirateChain.getFeeCeiling());
}
@POST
@Path("/updatefeeceiling")
@Operation(
summary = "Sets PirateChain fee ceiling.",
description = "Sets PirateChain fee ceiling.",
requestBody = @RequestBody(
required = true,
content = @Content(
mediaType = MediaType.TEXT_PLAIN,
schema = @Schema(
type = "number",
description = "the fee",
example = "100"
)
)
),
responses = {
@ApiResponse(
content = @Content(mediaType = MediaType.TEXT_PLAIN, schema = @Schema(type = "number", description = "fee"))
)
}
)
@ApiErrors({ApiError.INVALID_PRIVATE_KEY, ApiError.INVALID_CRITERIA})
public String setPirateChainFeeCeiling(@HeaderParam(Security.API_KEY_HEADER) String apiKey, String fee) {
Security.checkApiCallAllowed(request);
PirateChain pirateChain = PirateChain.getInstance();
try {
return CrossChainUtils.setFeeCeiling(pirateChain, fee);
}
catch (IllegalArgumentException e) {
throw ApiExceptionFactory.INSTANCE.createException(request, ApiError.INVALID_CRITERIA);
}
}
}

View File

@ -19,6 +19,7 @@ import org.qortal.crosschain.AddressInfo;
import org.qortal.crosschain.ForeignBlockchainException;
import org.qortal.crosschain.Ravencoin;
import org.qortal.crosschain.SimpleTransaction;
import org.qortal.crosschain.ServerConfigurationInfo;
import javax.servlet.http.HttpServletRequest;
import javax.ws.rs.GET;
@ -188,45 +189,6 @@ public class CrossChainRavencoinResource {
}
}
@POST
@Path("/unusedaddress")
@Operation(
summary = "Returns first unused address for hierarchical, deterministic BIP32 wallet",
description = "Supply BIP32 'm' private/public key in base58, starting with 'xprv'/'xpub' for mainnet, 'tprv'/'tpub' for testnet",
requestBody = @RequestBody(
required = true,
content = @Content(
mediaType = MediaType.TEXT_PLAIN,
schema = @Schema(
type = "string",
description = "BIP32 'm' private/public key in base58",
example = "tpubD6NzVbkrYhZ4XTPc4btCZ6SMgn8CxmWkj6VBVZ1tfcJfMq4UwAjZbG8U74gGSypL9XBYk2R2BLbDBe8pcEyBKM1edsGQEPKXNbEskZozeZc"
)
)
),
responses = {
@ApiResponse(
content = @Content(array = @ArraySchema( schema = @Schema( implementation = SimpleTransaction.class ) ) )
)
}
)
@ApiErrors({ApiError.INVALID_PRIVATE_KEY, ApiError.FOREIGN_BLOCKCHAIN_NETWORK_ISSUE})
@SecurityRequirement(name = "apiKey")
public String getUnusedRavencoinReceiveAddress(@HeaderParam(Security.API_KEY_HEADER) String apiKey, String key58) {
Security.checkApiCallAllowed(request);
Ravencoin ravencoin = Ravencoin.getInstance();
if (!ravencoin.isValidDeterministicKey(key58))
throw ApiExceptionFactory.INSTANCE.createException(request, ApiError.INVALID_PRIVATE_KEY);
try {
return ravencoin.getUnusedReceiveAddress(key58);
} catch (ForeignBlockchainException e) {
throw ApiExceptionFactory.INSTANCE.createException(request, ApiError.FOREIGN_BLOCKCHAIN_NETWORK_ISSUE);
}
}
@POST
@Path("/send")
@Operation(
@ -283,4 +245,137 @@ public class CrossChainRavencoinResource {
return spendTransaction.getTxId().toString();
}
@GET
@Path("/serverinfos")
@Operation(
summary = "Returns current Ravencoin server configuration",
description = "Returns current Ravencoin server locations and use status",
responses = {
@ApiResponse(
content = @Content(
mediaType = MediaType.APPLICATION_JSON,
schema = @Schema(
implementation = ServerConfigurationInfo.class
)
)
)
}
)
public ServerConfigurationInfo getServerConfiguration() {
return CrossChainUtils.buildServerConfigurationInfo(Ravencoin.getInstance());
}
@GET
@Path("/feekb")
@Operation(
summary = "Returns Ravencoin fee per Kb.",
description = "Returns Ravencoin fee per Kb.",
responses = {
@ApiResponse(
content = @Content(
schema = @Schema(
type = "number"
)
)
)
}
)
public String getRavencoinFeePerKb() {
Ravencoin ravencoin = Ravencoin.getInstance();
return String.valueOf(ravencoin.getFeePerKb().value);
}
@POST
@Path("/updatefeekb")
@Operation(
summary = "Sets Ravencoin fee per Kb.",
description = "Sets Ravencoin fee per Kb.",
requestBody = @RequestBody(
required = true,
content = @Content(
mediaType = MediaType.TEXT_PLAIN,
schema = @Schema(
type = "number",
description = "the fee per Kb",
example = "100"
)
)
),
responses = {
@ApiResponse(
content = @Content(mediaType = MediaType.TEXT_PLAIN, schema = @Schema(type = "number", description = "fee"))
)
}
)
@ApiErrors({ApiError.INVALID_PRIVATE_KEY, ApiError.INVALID_CRITERIA})
public String setRavencoinFeePerKb(@HeaderParam(Security.API_KEY_HEADER) String apiKey, String fee) {
Security.checkApiCallAllowed(request);
Ravencoin ravencoin = Ravencoin.getInstance();
try {
return CrossChainUtils.setFeePerKb(ravencoin, fee);
} catch (IllegalArgumentException e) {
throw ApiExceptionFactory.INSTANCE.createException(request, ApiError.INVALID_CRITERIA);
}
}
@GET
@Path("/feeceiling")
@Operation(
summary = "Returns Ravencoin fee per Kb.",
description = "Returns Ravencoin fee per Kb.",
responses = {
@ApiResponse(
content = @Content(
schema = @Schema(
type = "number"
)
)
)
}
)
public String getRavencoinFeeCeiling() {
Ravencoin ravencoin = Ravencoin.getInstance();
return String.valueOf(ravencoin.getFeeCeiling());
}
@POST
@Path("/updatefeeceiling")
@Operation(
summary = "Sets Ravencoin fee ceiling.",
description = "Sets Ravencoin fee ceiling.",
requestBody = @RequestBody(
required = true,
content = @Content(
mediaType = MediaType.TEXT_PLAIN,
schema = @Schema(
type = "number",
description = "the fee",
example = "100"
)
)
),
responses = {
@ApiResponse(
content = @Content(mediaType = MediaType.TEXT_PLAIN, schema = @Schema(type = "number", description = "fee"))
)
}
)
@ApiErrors({ApiError.INVALID_PRIVATE_KEY, ApiError.INVALID_CRITERIA})
public String setRavencoinFeeCeiling(@HeaderParam(Security.API_KEY_HEADER) String apiKey, String fee) {
Security.checkApiCallAllowed(request);
Ravencoin ravencoin = Ravencoin.getInstance();
try {
return CrossChainUtils.setFeeCeiling(ravencoin, fee);
}
catch (IllegalArgumentException e) {
throw ApiExceptionFactory.INSTANCE.createException(request, ApiError.INVALID_CRITERIA);
}
}
}

View File

@ -19,11 +19,14 @@ import org.qortal.api.model.CrossChainTradeSummary;
import org.qortal.controller.tradebot.TradeBot;
import org.qortal.crosschain.ACCT;
import org.qortal.crosschain.AcctMode;
import org.qortal.crosschain.Bitcoiny;
import org.qortal.crosschain.ForeignBlockchainException;
import org.qortal.crosschain.SupportedBlockchain;
import org.qortal.crypto.Crypto;
import org.qortal.data.at.ATData;
import org.qortal.data.at.ATStateData;
import org.qortal.data.crosschain.CrossChainTradeData;
import org.qortal.data.crosschain.TransactionSummary;
import org.qortal.data.transaction.BaseTransactionData;
import org.qortal.data.transaction.MessageTransactionData;
import org.qortal.data.transaction.TransactionData;
@ -47,6 +50,7 @@ import javax.ws.rs.core.Context;
import javax.ws.rs.core.MediaType;
import java.util.*;
import java.util.function.Supplier;
import java.util.stream.Collectors;
@Path("/crosschain")
@Tag(name = "Cross-Chain")
@ -497,6 +501,111 @@ public class CrossChainResource {
}
}
@POST
@Path("/p2sh")
@Operation(
summary = "Returns P2SH Address",
description = "Get the P2SH address to lock foreign coin in a cross chain trade for QORT",
requestBody = @RequestBody(
required = true,
content = @Content(
mediaType = MediaType.TEXT_PLAIN,
schema = @Schema(
type = "string",
description = "the AT address",
example = "AKFnu9yBp7tUAc5HAphhfCxRZTYoeKXgUy"
)
)
),
responses = {
@ApiResponse(
content = @Content(mediaType = MediaType.TEXT_PLAIN, schema = @Schema(type = "string", description = "address"))
)
}
)
@ApiErrors({ApiError.ADDRESS_UNKNOWN, ApiError.INVALID_CRITERIA})
@SecurityRequirement(name = "apiKey")
public String getForeignP2SH(@HeaderParam(Security.API_KEY_HEADER) String apiKey, String atAddress) {
Security.checkApiCallAllowed(request);
try (final Repository repository = RepositoryManager.getRepository()) {
ATData atData = repository.getATRepository().fromATAddress(atAddress);
if (atData == null)
throw ApiExceptionFactory.INSTANCE.createException(request, ApiError.ADDRESS_UNKNOWN);
ACCT acct = SupportedBlockchain.getAcctByCodeHash(atData.getCodeHash());
if( acct == null || !(acct.getBlockchain() instanceof Bitcoiny) )
throw ApiExceptionFactory.INSTANCE.createException(request, ApiError.INVALID_CRITERIA);
Bitcoiny bitcoiny = (Bitcoiny) acct.getBlockchain();
CrossChainTradeData crossChainTradeData = acct.populateTradeData(repository, atData);
Optional<String> p2sh
= CrossChainUtils.getP2ShAddressForAT(atAddress, repository, bitcoiny, crossChainTradeData);
if(p2sh.isPresent()){
return p2sh.get();
}
else{
throw ApiExceptionFactory.INSTANCE.createException(request, ApiError.ADDRESS_UNKNOWN);
}
}
catch (DataException e) {
throw ApiExceptionFactory.INSTANCE.createCustomException(request, ApiError.REPOSITORY_ISSUE, e.getMessage());
}
}
@POST
@Path("/txactivity")
@Operation(
summary = "Returns Foreign Transaction Activity",
description = "Get the activity related to foreign coin trading",
responses = {
@ApiResponse(
content = @Content(
array = @ArraySchema(
schema = @Schema(
implementation = TransactionSummary.class
)
)
)
)
}
)
@ApiErrors({ApiError.INVALID_CRITERIA, ApiError.REPOSITORY_ISSUE, ApiError.FOREIGN_BLOCKCHAIN_NETWORK_ISSUE})
@SecurityRequirement(name = "apiKey")
public List<TransactionSummary> getForeignTransactionActivity(@HeaderParam(Security.API_KEY_HEADER) String apiKey, @Parameter(
description = "Limit to specific blockchain",
example = "LITECOIN",
schema = @Schema(implementation = SupportedBlockchain.class)
) @QueryParam("foreignBlockchain") SupportedBlockchain foreignBlockchain) {
Security.checkApiCallAllowed(request);
if (!(foreignBlockchain.getInstance() instanceof Bitcoiny))
throw ApiExceptionFactory.INSTANCE.createException(request, ApiError.INVALID_CRITERIA);
Bitcoiny bitcoiny = (Bitcoiny) foreignBlockchain.getInstance() ;
org.bitcoinj.core.Context.propagate( bitcoiny.getBitcoinjContext() );
try (final Repository repository = RepositoryManager.getRepository()) {
// sort from last lock to first lock
return CrossChainUtils
.getForeignTradeSummaries(foreignBlockchain, repository, bitcoiny).stream()
.sorted(Comparator.comparing(TransactionSummary::getLockingTimestamp).reversed())
.collect(Collectors.toList());
}
catch (DataException e) {
throw ApiExceptionFactory.INSTANCE.createCustomException(request, ApiError.REPOSITORY_ISSUE, e.getMessage());
}
catch (ForeignBlockchainException e) {
throw ApiExceptionFactory.INSTANCE.createCustomException(request, ApiError.FOREIGN_BLOCKCHAIN_NETWORK_ISSUE, e.getMessage());
}
}
private ATData fetchAtDataWithChecking(Repository repository, String atAddress) throws DataException {
ATData atData = repository.getATRepository().fromATAddress(atAddress);
if (atData == null)

View File

@ -0,0 +1,420 @@
package org.qortal.api.resource;
import org.apache.logging.log4j.LogManager;
import org.apache.logging.log4j.Logger;
import org.bitcoinj.core.Address;
import org.bitcoinj.core.Coin;
import org.bitcoinj.script.Script;
import org.bitcoinj.script.ScriptBuilder;
import org.qortal.crosschain.*;
import org.qortal.data.at.ATData;
import org.qortal.data.crosschain.AtomicTransactionData;
import org.qortal.data.crosschain.CrossChainTradeData;
import org.qortal.data.crosschain.TradeBotData;
import org.qortal.data.crosschain.TransactionSummary;
import org.qortal.repository.DataException;
import org.qortal.repository.Repository;
import java.util.ArrayList;
import java.util.Collection;
import java.util.List;
import java.util.Optional;
import java.util.HashMap;
import java.util.Map;
import java.util.stream.Collectors;
public class CrossChainUtils {
private static final Logger LOGGER = LogManager.getLogger(CrossChainUtils.class);
public static ServerConfigurationInfo buildServerConfigurationInfo(Bitcoiny blockchain) {
BitcoinyBlockchainProvider blockchainProvider = blockchain.getBlockchainProvider();
ChainableServer currentServer = blockchainProvider.getCurrentServer();
return new ServerConfigurationInfo(
buildInfos(blockchainProvider.getServers(), currentServer),
buildInfos(blockchainProvider.getRemainingServers(), currentServer),
buildInfos(blockchainProvider.getUselessServers(), currentServer)
);
}
public static ServerInfo buildInfo(ChainableServer server, boolean isCurrent) {
return new ServerInfo(
server.averageResponseTime(),
server.getHostName(),
server.getPort(),
server.getConnectionType().toString(),
isCurrent);
}
public static List<ServerInfo> buildInfos(Collection<ChainableServer> servers, ChainableServer currentServer) {
List<ServerInfo> infos = new ArrayList<>( servers.size() );
for( ChainableServer server : servers )
{
infos.add(buildInfo(server, server.equals(currentServer)));
}
return infos;
}
/**
* Set Fee Per Kb
*
* @param bitcoiny the blockchain support
* @param fee the fee in satoshis
*
* @return the fee if valid
*
* @throws IllegalArgumentException if invalid
*/
public static String setFeePerKb(Bitcoiny bitcoiny, String fee) throws IllegalArgumentException {
long satoshis = Long.parseLong(fee);
if( satoshis < 0 ) throw new IllegalArgumentException("can't set fee to negative number");
bitcoiny.setFeePerKb(Coin.valueOf(satoshis) );
return String.valueOf(bitcoiny.getFeePerKb().value);
}
/**
* Set Fee Ceiling
*
* @param bitcoiny the blockchain support
* @param fee the fee in satoshis
*
* @return the fee if valid
*
* @throws IllegalArgumentException if invalid
*/
public static String setFeeCeiling(Bitcoiny bitcoiny, String fee) throws IllegalArgumentException{
long satoshis = Long.parseLong(fee);
if( satoshis < 0 ) throw new IllegalArgumentException("can't set fee to negative number");
bitcoiny.setFeeCeiling( Long.parseLong(fee));
return String.valueOf(bitcoiny.getFeeCeiling());
}
/**
* Get P2Sh Address For AT
*
* @param atAddress the AT address
* @param repository the repository
* @param bitcoiny the blockchain data
* @param crossChainTradeData the trade data
*
* @return the p2sh address for the trade, if there is one
*
* @throws DataException
*/
public static Optional<String> getP2ShAddressForAT(
String atAddress,
Repository repository,
Bitcoiny bitcoiny,
CrossChainTradeData crossChainTradeData) throws DataException {
// get the trade bot data for the AT address
Optional<TradeBotData> tradeBotDataOptional
= repository.getCrossChainRepository()
.getAllTradeBotData().stream()
.filter(data -> data.getAtAddress().equals(atAddress))
.findFirst();
if( tradeBotDataOptional.isEmpty() )
return Optional.empty();
TradeBotData tradeBotData = tradeBotDataOptional.get();
// return the p2sh address from the trade bot
return getP2ShFromTradeBot(bitcoiny, crossChainTradeData, tradeBotData);
}
/**
* Get Foreign Trade Summaries
*
* @param foreignBlockchain the blockchain traded on
* @param repository the repository
* @param bitcoiny data for the blockchain trade on
* @return
* @throws DataException
* @throws ForeignBlockchainException
*/
public static List<TransactionSummary> getForeignTradeSummaries(
SupportedBlockchain foreignBlockchain,
Repository repository,
Bitcoiny bitcoiny) throws DataException, ForeignBlockchainException {
// get all the AT address for the given blockchain
List<String> atAddresses
= repository.getCrossChainRepository().getAllTradeBotData().stream()
.filter(data -> foreignBlockchain.name().toLowerCase().equals(data.getForeignBlockchain().toLowerCase()))
//.filter( data -> data.getForeignKey().equals( xpriv )) // TODO
.map(data -> data.getAtAddress())
.collect(Collectors.toList());
List<TransactionSummary> summaries = new ArrayList<>( atAddresses.size() * 2 );
// for each AT address, gather the data and get foreign trade summary
for( String atAddress: atAddresses) {
ATData atData = repository.getATRepository().fromATAddress(atAddress);
CrossChainTradeData crossChainTradeData = foreignBlockchain.getLatestAcct().populateTradeData(repository, atData);
Optional<String> address = getP2ShAddressForAT(atAddress,repository, bitcoiny, crossChainTradeData);
if( address.isPresent()){
summaries.add( getForeignTradeSummary( bitcoiny, address.get(), atAddress ) );
}
}
return summaries;
}
/**
* Get P2Sh From Trade Bot
*
* Get P2Sh address from the trade bot
*
* @param bitcoiny the blockchain for the trade
* @param crossChainTradeData the cross cahin data for the trade
* @param tradeBotData the data from the trade bot
*
* @return the address, original format
*/
private static Optional<String> getP2ShFromTradeBot(
Bitcoiny bitcoiny,
CrossChainTradeData crossChainTradeData,
TradeBotData tradeBotData) {
// Pirate Chain does not support this
if( SupportedBlockchain.PIRATECHAIN.name().equals(tradeBotData.getForeignBlockchain())) return Optional.empty();
// need to get the trade PKH from the trade bot
if( tradeBotData.getTradeForeignPublicKeyHash() == null ) return Optional.empty();
// need to get the lock time from the trade bot
if( tradeBotData.getLockTimeA() == null ) return Optional.empty();
// need to get the creator PKH from the trade bot
if( crossChainTradeData.creatorForeignPKH == null ) return Optional.empty();
// need to get the secret from the trade bot
if( tradeBotData.getHashOfSecret() == null ) return Optional.empty();
// if we have the necessary data from the trade bot,
// then build the redeem script necessary to facilitate the trade
byte[] redeemScriptBytes
= BitcoinyHTLC.buildScript(
tradeBotData.getTradeForeignPublicKeyHash(),
tradeBotData.getLockTimeA(),
crossChainTradeData.creatorForeignPKH,
tradeBotData.getHashOfSecret()
);
String p2shAddress = bitcoiny.deriveP2shAddress(redeemScriptBytes);
return Optional.of(p2shAddress);
}
/**
* Get Foreign Trade Summary
*
* @param bitcoiny the blockchain the trade occurred on
* @param p2shAddress the p2sh address
* @param atAddress the AT address the p2sh address is derived from
*
* @return the summary
*
* @throws ForeignBlockchainException
*/
public static TransactionSummary getForeignTradeSummary(Bitcoiny bitcoiny, String p2shAddress, String atAddress)
throws ForeignBlockchainException {
Script outputScript = ScriptBuilder.createOutputScript(
Address.fromString(bitcoiny.getNetworkParameters(), p2shAddress));
List<TransactionHash> hashes
= bitcoiny.getAddressTransactions( outputScript.getProgram(), true);
TransactionSummary summary;
if(hashes.isEmpty()){
summary
= new TransactionSummary(
atAddress,
p2shAddress,
"N/A",
"N/A",
0,
0,
0,
0,
"N/A",
0,
0,
0,
0);
}
else if( hashes.size() == 1) {
AtomicTransactionData data = buildTransactionData(bitcoiny, hashes.get(0));
summary = new TransactionSummary(
atAddress,
p2shAddress,
"N/A",
data.hash.txHash,
data.timestamp,
data.totalAmount,
getTotalInput(bitcoiny, data.inputs) - data.totalAmount,
data.size,
"N/A",
0,
0,
0,
0);
}
// otherwise assuming there is 2 and only 2 hashes
else {
List<AtomicTransactionData> atomicTransactionDataList = new ArrayList<>(2);
// hashes -> data
for( TransactionHash hash : hashes){
atomicTransactionDataList.add(buildTransactionData(bitcoiny,hash));
}
// sort the transaction data by time
List<AtomicTransactionData> sorted
= atomicTransactionDataList.stream()
.sorted((data1, data2) -> data1.timestamp.compareTo(data2.timestamp))
.collect(Collectors.toList());
// build the summary using the first 2 transactions
summary = buildForeignTradeSummary(atAddress, p2shAddress, sorted.get(0), sorted.get(1), bitcoiny);
}
return summary;
}
/**
* Build Foreign Trade Summary
*
* @param p2shValue the p2sh address, original format
* @param lockingTransaction the transaction lock the foreighn coin
* @param unlockingTransaction the transaction to unlock the foreign coin
* @param bitcoiny the blockchain the trade occurred on
*
* @return
*
* @throws ForeignBlockchainException
*/
private static TransactionSummary buildForeignTradeSummary(
String atAddress,
String p2shValue,
AtomicTransactionData lockingTransaction,
AtomicTransactionData unlockingTransaction,
Bitcoiny bitcoiny) throws ForeignBlockchainException {
// get sum of the relevant inputs for each transaction
long lockingTotalInput = getTotalInput(bitcoiny, lockingTransaction.inputs);
long unlockingTotalInput = getTotalInput(bitcoiny, unlockingTransaction.inputs);
// find the address that has output that matches the total input
Optional<Map.Entry<List<String>, Long>> addressValue
= lockingTransaction.valueByAddress.entrySet().stream()
.filter(entry -> entry.getValue() == unlockingTotalInput).findFirst();
// set that matching address, if found
String p2shAddress;
if( addressValue.isPresent() && addressValue.get().getKey().size() == 1 ){
p2shAddress = addressValue.get().getKey().get(0);
}
else {
p2shAddress = "N/A";
}
// build summaries with prepared values
// the fees are the total amount subtracted by the total transaction input
return new TransactionSummary(
atAddress,
p2shValue,
p2shAddress,
lockingTransaction.hash.txHash,
lockingTransaction.timestamp,
lockingTransaction.totalAmount,
lockingTotalInput - lockingTransaction.totalAmount,
lockingTransaction.size,
unlockingTransaction.hash.txHash,
unlockingTransaction.timestamp,
unlockingTransaction.totalAmount,
unlockingTotalInput - unlockingTransaction.totalAmount,
unlockingTransaction.size
);
}
/**
* Build Transaction Data
*
* @param bitcoiny the coin for the transaction
* @param hash the hash for the transaction
*
* @return the data for the transaction
*
* @throws ForeignBlockchainException
*/
private static AtomicTransactionData buildTransactionData( Bitcoiny bitcoiny, TransactionHash hash)
throws ForeignBlockchainException {
BitcoinyTransaction transaction = bitcoiny.getTransaction(hash.txHash);
// destination address list -> value
Map<List<String>, Long> valueByAddress = new HashMap<>();
// for each output in the transaction, index by address list
for( BitcoinyTransaction.Output output : transaction.outputs) {
valueByAddress.put(output.addresses, output.value);
}
return new AtomicTransactionData(
hash,
transaction.timestamp,
transaction.inputs,
valueByAddress,
transaction.totalAmount,
transaction.size);
}
/**
* Get Total Input
*
* Get the sum of all the inputs used in a list of inputs.
*
* @param bitcoiny the coin the inputs belong to
* @param inputs the inputs
*
* @return the sum
*
* @throws ForeignBlockchainException
*/
private static long getTotalInput(Bitcoiny bitcoiny, List<BitcoinyTransaction.Input> inputs)
throws ForeignBlockchainException {
long totalInputOut = 0;
// for each input, add to total input,
// get the indexed transaction output value and add to total value
for( BitcoinyTransaction.Input input : inputs){
BitcoinyTransaction inputOut = bitcoiny.getTransaction(input.outputTxHash);
BitcoinyTransaction.Output output = inputOut.outputs.get(input.outputVout);
totalInputOut += output.value;
}
return totalInputOut;
}
}

View File

@ -13,6 +13,8 @@ import org.qortal.api.ApiErrors;
import org.qortal.api.ApiException;
import org.qortal.api.ApiExceptionFactory;
import org.qortal.api.model.PollVotes;
import org.qortal.crypto.Crypto;
import org.qortal.data.account.AccountData;
import org.qortal.data.transaction.CreatePollTransactionData;
import org.qortal.data.transaction.VoteOnPollTransactionData;
import org.qortal.data.voting.PollData;
@ -129,12 +131,25 @@ public class PollsResource {
for (PollOptionData optionData : pollData.getPollOptions()) {
voteCountMap.put(optionData.getOptionName(), 0);
}
// Initialize map for counting vote weights
Map<String, Integer> voteWeightMap = new HashMap<>();
for (PollOptionData optionData : pollData.getPollOptions()) {
voteWeightMap.put(optionData.getOptionName(), 0);
}
int totalVotes = 0;
int totalWeight = 0;
for (VoteOnPollData vote : votes) {
String voter = Crypto.toAddress(vote.getVoterPublicKey());
AccountData voterData = repository.getAccountRepository().getAccount(voter);
int voteWeight = voterData.getBlocksMinted() + voterData.getBlocksMintedPenalty();
if (voteWeight < 0) voteWeight = 0;
totalWeight += voteWeight;
String selectedOption = pollData.getPollOptions().get(vote.getOptionIndex()).getOptionName();
if (voteCountMap.containsKey(selectedOption)) {
voteCountMap.put(selectedOption, voteCountMap.get(selectedOption) + 1);
voteWeightMap.put(selectedOption, voteWeightMap.get(selectedOption) + voteWeight);
totalVotes++;
}
}
@ -143,11 +158,15 @@ public class PollsResource {
List<PollVotes.OptionCount> voteCounts = voteCountMap.entrySet().stream()
.map(entry -> new PollVotes.OptionCount(entry.getKey(), entry.getValue()))
.collect(Collectors.toList());
// Convert map to list of WeightInfo
List<PollVotes.OptionWeight> voteWeights = voteWeightMap.entrySet().stream()
.map(entry -> new PollVotes.OptionWeight(entry.getKey(), entry.getValue()))
.collect(Collectors.toList());
if (onlyCounts != null && onlyCounts) {
return new PollVotes(null, totalVotes, voteCounts);
return new PollVotes(null, totalVotes, totalWeight, voteCounts, voteWeights);
} else {
return new PollVotes(votes, totalVotes, voteCounts);
return new PollVotes(votes, totalVotes, totalWeight, voteCounts, voteWeights);
}
} catch (ApiException e) {
throw e;

View File

@ -24,8 +24,9 @@ import org.qortal.api.model.ActivitySummary;
import org.qortal.api.model.NodeInfo;
import org.qortal.api.model.NodeStatus;
import org.qortal.block.BlockChain;
import org.qortal.controller.AutoUpdate;
import org.qortal.controller.BootstrapNode;
import org.qortal.controller.Controller;
import org.qortal.controller.RestartNode;
import org.qortal.controller.Synchronizer;
import org.qortal.controller.Synchronizer.SynchronizationResult;
import org.qortal.controller.repository.BlockArchiveRebuilder;
@ -250,7 +251,38 @@ public class AdminResource {
// Not important
}
AutoUpdate.attemptRestart();
RestartNode.attemptToRestart();
}).start();
return "true";
}
@GET
@Path("/bootstrap")
@Operation(
summary = "Bootstrap",
description = "Delete and download new database archive",
responses = {
@ApiResponse(
description = "\"true\"",
content = @Content(mediaType = MediaType.TEXT_PLAIN, schema = @Schema(type = "string"))
)
}
)
@SecurityRequirement(name = "apiKey")
public String bootstrap(@HeaderParam(Security.API_KEY_HEADER) String apiKey) {
Security.checkApiCallAllowed(request);
new Thread(() -> {
// Short sleep to allow HTTP response body to be emitted
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
// Not important
}
BootstrapNode.attemptToBootstrap();
}).start();

View File

@ -522,6 +522,10 @@ public class QortalATAPI extends API {
/** Returns AT account's lastReference */
private byte[] getLastReference() {
// If we have transactions already, then use signature from last transaction
if (!this.transactions.isEmpty())
return this.transactions.get(this.transactions.size() - 1).getTransactionData().getSignature();
try {
// Look up AT's account's last reference from repository
Account atAccount = this.getATAccount();

View File

@ -1061,8 +1061,10 @@ public class Block {
return ValidationResult.MINTER_NOT_ACCEPTED;
long expectedTimestamp = calcTimestamp(parentBlockData, this.blockData.getMinterPublicKey(), minterLevel);
if (this.blockData.getTimestamp() != expectedTimestamp)
if (this.blockData.getTimestamp() != expectedTimestamp) {
LOGGER.debug(String.format("timestamp mismatch! block had %s but we expected %s", this.blockData.getTimestamp(), expectedTimestamp));
return ValidationResult.TIMESTAMP_INCORRECT;
}
return ValidationResult.OK;
}
@ -1309,6 +1311,9 @@ public class Block {
if (!transaction.isConfirmable()) {
return ValidationResult.TRANSACTION_NOT_CONFIRMABLE;
}
if (!transaction.isConfirmableAtHeight(this.blockData.getHeight())) {
return ValidationResult.TRANSACTION_NOT_CONFIRMABLE;
}
}
// Check transaction isn't already included in a block
@ -1545,12 +1550,22 @@ public class Block {
processBlockRewards();
}
if (this.blockData.getHeight() == 212937)
if (this.blockData.getHeight() == 212937) {
// Apply fix for block 212937
Block212937.processFix(this);
}
else if (this.blockData.getHeight() == BlockChain.getInstance().getSelfSponsorshipAlgoV1Height())
if (this.blockData.getHeight() == BlockChain.getInstance().getSelfSponsorshipAlgoV1Height()) {
SelfSponsorshipAlgoV1Block.processAccountPenalties(this);
}
if (this.blockData.getHeight() == BlockChain.getInstance().getSelfSponsorshipAlgoV2Height()) {
SelfSponsorshipAlgoV2Block.processAccountPenalties(this);
}
if (this.blockData.getHeight() == BlockChain.getInstance().getSelfSponsorshipAlgoV3Height()) {
SelfSponsorshipAlgoV3Block.processAccountPenalties(this);
}
}
// We're about to (test-)process a batch of transactions,
@ -1835,13 +1850,23 @@ public class Block {
// Invalidate expandedAccounts as they may have changed due to orphaning TRANSFER_PRIVS transactions, etc.
this.cachedExpandedAccounts = null;
if (this.blockData.getHeight() == 212937)
if (this.blockData.getHeight() == 212937) {
// Revert fix for block 212937
Block212937.orphanFix(this);
}
else if (this.blockData.getHeight() == BlockChain.getInstance().getSelfSponsorshipAlgoV1Height())
if (this.blockData.getHeight() == BlockChain.getInstance().getSelfSponsorshipAlgoV1Height()) {
SelfSponsorshipAlgoV1Block.orphanAccountPenalties(this);
}
if (this.blockData.getHeight() == BlockChain.getInstance().getSelfSponsorshipAlgoV2Height()) {
SelfSponsorshipAlgoV2Block.orphanAccountPenalties(this);
}
if (this.blockData.getHeight() == BlockChain.getInstance().getSelfSponsorshipAlgoV3Height()) {
SelfSponsorshipAlgoV3Block.orphanAccountPenalties(this);
}
// Account levels and block rewards are only processed/orphaned on block reward distribution blocks
if (this.isRewardDistributionBlock()) {
// Block rewards, including transaction fees, removed after transactions undone
@ -2088,7 +2113,7 @@ public class Block {
return Block.isOnlineAccountsBlock(this.getBlockData().getHeight());
}
private static boolean isOnlineAccountsBlock(int height) {
public static boolean isOnlineAccountsBlock(int height) {
// After feature trigger, only certain blocks contain online accounts
if (height >= BlockChain.getInstance().getBlockRewardBatchStartHeight()) {
final int leadingBlockCount = BlockChain.getInstance().getBlockRewardBatchAccountsBlockCount();
@ -2539,5 +2564,4 @@ public class Block {
LOGGER.info(() -> String.format("Unable to log block debugging info: %s", e.getMessage()));
}
}
}

View File

@ -73,9 +73,14 @@ public class BlockChain {
increaseOnlineAccountsDifficultyTimestamp,
onlineAccountMinterLevelValidationHeight,
selfSponsorshipAlgoV1Height,
selfSponsorshipAlgoV2Height,
selfSponsorshipAlgoV3Height,
feeValidationFixTimestamp,
chatReferenceTimestamp,
arbitraryOptionalFeeTimestamp;
arbitraryOptionalFeeTimestamp,
unconfirmableRewardSharesHeight,
disableTransferPrivsTimestamp,
enableTransferPrivsTimestamp
}
// Custom transaction fees
@ -198,6 +203,7 @@ public class BlockChain {
/** Minimum time to retain online account signatures (ms) for block validity checks. */
private long onlineAccountSignaturesMinLifetime;
/** Maximum time to retain online account signatures (ms) for block validity checks, to allow for clock variance. */
private long onlineAccountSignaturesMaxLifetime;
@ -208,6 +214,15 @@ public class BlockChain {
/** Snapshot timestamp for self sponsorship algo V1 */
private long selfSponsorshipAlgoV1SnapshotTimestamp;
/** Snapshot timestamp for self sponsorship algo V2 */
private long selfSponsorshipAlgoV2SnapshotTimestamp;
/** Snapshot timestamp for self sponsorship algo V3 */
private long selfSponsorshipAlgoV3SnapshotTimestamp;
/** Reference timestamp for self sponsorship algo V1 block height */
private long referenceTimestampBlock;
/** Feature-trigger timestamp to modify behaviour of various transactions that support mempow */
private long mempowTransactionUpdatesTimestamp;
@ -224,6 +239,8 @@ public class BlockChain {
* data and to base online accounts decisions on. */
private int blockRewardBatchAccountsBlockCount;
private String penaltyFixHash;
/** Max reward shares by block height */
public static class MaxRewardSharesByTimestamp {
public long timestamp;
@ -266,7 +283,7 @@ public class BlockChain {
try {
// Create JAXB context aware of Settings
jc = JAXBContextFactory.createContext(new Class[] {
BlockChain.class, GenesisBlock.GenesisInfo.class
BlockChain.class, GenesisBlock.GenesisInfo.class
}, null);
// Create unmarshaller
@ -394,12 +411,29 @@ public class BlockChain {
return this.blockRewardBatchAccountsBlockCount;
}
public String getPenaltyFixHash() {
return this.penaltyFixHash;
}
// Self sponsorship algo
// Self sponsorship algo V1
public long getSelfSponsorshipAlgoV1SnapshotTimestamp() {
return this.selfSponsorshipAlgoV1SnapshotTimestamp;
}
// Self sponsorship algo V2
public long getSelfSponsorshipAlgoV2SnapshotTimestamp() {
return this.selfSponsorshipAlgoV2SnapshotTimestamp;
}
// Self sponsorship algo V3
public long getSelfSponsorshipAlgoV3SnapshotTimestamp() {
return this.selfSponsorshipAlgoV3SnapshotTimestamp;
}
// Self sponsorship algo V3
public long getReferenceTimestampBlock() {
return this.referenceTimestampBlock;
}
// Feature-trigger timestamp to modify behaviour of various transactions that support mempow
public long getMemPoWTransactionUpdatesTimestamp() {
return this.mempowTransactionUpdatesTimestamp;
@ -540,6 +574,14 @@ public class BlockChain {
return this.featureTriggers.get(FeatureTrigger.selfSponsorshipAlgoV1Height.name()).intValue();
}
public int getSelfSponsorshipAlgoV2Height() {
return this.featureTriggers.get(FeatureTrigger.selfSponsorshipAlgoV2Height.name()).intValue();
}
public int getSelfSponsorshipAlgoV3Height() {
return this.featureTriggers.get(FeatureTrigger.selfSponsorshipAlgoV3Height.name()).intValue();
}
public long getOnlineAccountMinterLevelValidationHeight() {
return this.featureTriggers.get(FeatureTrigger.onlineAccountMinterLevelValidationHeight.name()).intValue();
}
@ -556,6 +598,17 @@ public class BlockChain {
return this.featureTriggers.get(FeatureTrigger.arbitraryOptionalFeeTimestamp.name()).longValue();
}
public int getUnconfirmableRewardSharesHeight() {
return this.featureTriggers.get(FeatureTrigger.unconfirmableRewardSharesHeight.name()).intValue();
}
public long getDisableTransferPrivsTimestamp() {
return this.featureTriggers.get(FeatureTrigger.disableTransferPrivsTimestamp.name()).longValue();
}
public long getEnableTransferPrivsTimestamp() {
return this.featureTriggers.get(FeatureTrigger.enableTransferPrivsTimestamp.name()).longValue();
}
// More complex getters for aspects that change by height or timestamp
@ -742,7 +795,7 @@ public class BlockChain {
/**
* Some sort of start-up/initialization/checking method.
*
*
* @throws SQLException
*/
public static void validate() throws DataException {

View File

@ -28,7 +28,6 @@ public final class SelfSponsorshipAlgoV1Block {
private static final Logger LOGGER = LogManager.getLogger(SelfSponsorshipAlgoV1Block.class);
private SelfSponsorshipAlgoV1Block() {
/* Do not instantiate */
}
@ -133,4 +132,4 @@ public final class SelfSponsorshipAlgoV1Block {
return Base58.encode(Crypto.digest(StringUtils.join(penaltyAddresses).getBytes(StandardCharsets.UTF_8)));
}
}
}

View File

@ -0,0 +1,143 @@
package org.qortal.block;
import org.apache.commons.lang3.StringUtils;
import org.apache.logging.log4j.LogManager;
import org.apache.logging.log4j.Logger;
import org.qortal.account.SelfSponsorshipAlgoV2;
import org.qortal.api.model.AccountPenaltyStats;
import org.qortal.crypto.Crypto;
import org.qortal.data.account.AccountData;
import org.qortal.data.account.AccountPenaltyData;
import org.qortal.repository.DataException;
import org.qortal.repository.Repository;
import org.qortal.utils.Base58;
import java.nio.charset.StandardCharsets;
import java.util.*;
import java.util.stream.Collectors;
/**
* Self Sponsorship AlgoV2 Block
* <p>
* Selected block for the initial run on the "self sponsorship detection algorithm"
*/
public final class SelfSponsorshipAlgoV2Block {
private static final Logger LOGGER = LogManager.getLogger(SelfSponsorshipAlgoV2Block.class);
private SelfSponsorshipAlgoV2Block() {
/* Do not instantiate */
}
public static void processAccountPenalties(Block block) throws DataException {
LOGGER.info("Process Self Sponsorship Algo V2 - this will take a while...");
logPenaltyStats(block.repository);
long startTime = System.currentTimeMillis();
Set<AccountPenaltyData> penalties = getAccountPenalties(block.repository, -5000000);
block.repository.getAccountRepository().updateBlocksMintedPenalties(penalties);
long totalTime = System.currentTimeMillis() - startTime;
String hash = getHash(penalties.stream().map(p -> p.getAddress()).collect(Collectors.toList()));
LOGGER.info("{} penalty addresses processed (hash: {}). Total time taken: {} seconds", penalties.size(), hash, (int)(totalTime / 1000.0f));
logPenaltyStats(block.repository);
int updatedCount = updateAccountLevels(block.repository, penalties);
LOGGER.info("Account levels updated for {} penalty addresses", updatedCount);
}
public static void orphanAccountPenalties(Block block) throws DataException {
LOGGER.info("Orphan Self Sponsorship Algo V2 - this will take a while...");
logPenaltyStats(block.repository);
long startTime = System.currentTimeMillis();
Set<AccountPenaltyData> penalties = getAccountPenalties(block.repository, 5000000);
block.repository.getAccountRepository().updateBlocksMintedPenalties(penalties);
long totalTime = System.currentTimeMillis() - startTime;
String hash = getHash(penalties.stream().map(p -> p.getAddress()).collect(Collectors.toList()));
LOGGER.info("{} penalty addresses orphaned (hash: {}). Total time taken: {} seconds", penalties.size(), hash, (int)(totalTime / 1000.0f));
logPenaltyStats(block.repository);
int updatedCount = updateAccountLevels(block.repository, penalties);
LOGGER.info("Account levels updated for {} penalty addresses", updatedCount);
}
private static Set<AccountPenaltyData> getAccountPenalties(Repository repository, int penalty) throws DataException {
Set<AccountPenaltyData> penalties = new LinkedHashSet<>();
List<AccountData> penalizedAddresses = repository.getAccountRepository().getPenaltyAccounts();
List<String> assetAddresses = repository.getTransactionRepository().getConfirmedTransferAssetCreators();
for (AccountData penalizedAddress : penalizedAddresses) {
//System.out.println(String.format("address: %s", address));
SelfSponsorshipAlgoV2 selfSponsorshipAlgoV2 = new SelfSponsorshipAlgoV2(repository, penalizedAddress.getAddress(), false);
selfSponsorshipAlgoV2.run();
//System.out.println(String.format("Penalty addresses: %d", selfSponsorshipAlgoV2.getPenaltyAddresses().size()));
for (String penaltyAddress : selfSponsorshipAlgoV2.getPenaltyAddresses()) {
penalties.add(new AccountPenaltyData(penaltyAddress, penalty));
}
}
for (String assetAddress : assetAddresses) {
//System.out.println(String.format("address: %s", address));
SelfSponsorshipAlgoV2 selfSponsorshipAlgoV2 = new SelfSponsorshipAlgoV2(repository, assetAddress, true);
selfSponsorshipAlgoV2.run();
//System.out.println(String.format("Penalty addresses: %d", selfSponsorshipAlgoV2.getPenaltyAddresses().size()));
for (String penaltyAddress : selfSponsorshipAlgoV2.getPenaltyAddresses()) {
penalties.add(new AccountPenaltyData(penaltyAddress, penalty));
}
}
return penalties;
}
private static int updateAccountLevels(Repository repository, Set<AccountPenaltyData> accountPenalties) throws DataException {
final List<Integer> cumulativeBlocksByLevel = BlockChain.getInstance().getCumulativeBlocksByLevel();
final int maximumLevel = cumulativeBlocksByLevel.size() - 1;
int updatedCount = 0;
for (AccountPenaltyData penaltyData : accountPenalties) {
AccountData accountData = repository.getAccountRepository().getAccount(penaltyData.getAddress());
final int effectiveBlocksMinted = accountData.getBlocksMinted() + accountData.getBlocksMintedAdjustment() + accountData.getBlocksMintedPenalty();
// Shortcut for penalties
if (effectiveBlocksMinted < 0) {
accountData.setLevel(0);
repository.getAccountRepository().setLevel(accountData);
updatedCount++;
LOGGER.trace(() -> String.format("Block minter %s dropped to level %d", accountData.getAddress(), accountData.getLevel()));
continue;
}
for (int newLevel = maximumLevel; newLevel >= 0; --newLevel) {
if (effectiveBlocksMinted >= cumulativeBlocksByLevel.get(newLevel)) {
accountData.setLevel(newLevel);
repository.getAccountRepository().setLevel(accountData);
updatedCount++;
LOGGER.trace(() -> String.format("Block minter %s increased to level %d", accountData.getAddress(), accountData.getLevel()));
break;
}
}
}
return updatedCount;
}
private static void logPenaltyStats(Repository repository) {
try {
LOGGER.info(getPenaltyStats(repository));
} catch (DataException e) {}
}
private static AccountPenaltyStats getPenaltyStats(Repository repository) throws DataException {
List<AccountData> accounts = repository.getAccountRepository().getPenaltyAccounts();
return AccountPenaltyStats.fromAccounts(accounts);
}
public static String getHash(List<String> penaltyAddresses) {
if (penaltyAddresses == null || penaltyAddresses.isEmpty()) {
return null;
}
Collections.sort(penaltyAddresses);
return Base58.encode(Crypto.digest(StringUtils.join(penaltyAddresses).getBytes(StandardCharsets.UTF_8)));
}
}

View File

@ -0,0 +1,136 @@
package org.qortal.block;
import org.apache.commons.lang3.StringUtils;
import org.apache.logging.log4j.LogManager;
import org.apache.logging.log4j.Logger;
import org.qortal.account.SelfSponsorshipAlgoV3;
import org.qortal.api.model.AccountPenaltyStats;
import org.qortal.crypto.Crypto;
import org.qortal.data.account.AccountData;
import org.qortal.data.account.AccountPenaltyData;
import org.qortal.repository.DataException;
import org.qortal.repository.Repository;
import org.qortal.utils.Base58;
import java.nio.charset.StandardCharsets;
import java.util.Collections;
import java.util.LinkedHashSet;
import java.util.List;
import java.util.Set;
import java.util.stream.Collectors;
/**
* Self Sponsorship AlgoV3 Block
* <p>
* Selected block for the initial run on the "self sponsorship detection algorithm"
*/
public final class SelfSponsorshipAlgoV3Block {
private static final Logger LOGGER = LogManager.getLogger(SelfSponsorshipAlgoV3Block.class);
private SelfSponsorshipAlgoV3Block() {
/* Do not instantiate */
}
public static void processAccountPenalties(Block block) throws DataException {
LOGGER.info("Process Self Sponsorship Algo V3 - this will take a while...");
logPenaltyStats(block.repository);
long startTime = System.currentTimeMillis();
Set<AccountPenaltyData> penalties = getAccountPenalties(block.repository, -5000000);
block.repository.getAccountRepository().updateBlocksMintedPenalties(penalties);
long totalTime = System.currentTimeMillis() - startTime;
String hash = getHash(penalties.stream().map(p -> p.getAddress()).collect(Collectors.toList()));
LOGGER.info("{} penalty addresses processed (hash: {}). Total time taken: {} seconds", penalties.size(), hash, (int)(totalTime / 1000.0f));
logPenaltyStats(block.repository);
int updatedCount = updateAccountLevels(block.repository, penalties);
LOGGER.info("Account levels updated for {} penalty addresses", updatedCount);
}
public static void orphanAccountPenalties(Block block) throws DataException {
LOGGER.info("Orphan Self Sponsorship Algo V3 - this will take a while...");
logPenaltyStats(block.repository);
long startTime = System.currentTimeMillis();
Set<AccountPenaltyData> penalties = getAccountPenalties(block.repository, 5000000);
block.repository.getAccountRepository().updateBlocksMintedPenalties(penalties);
long totalTime = System.currentTimeMillis() - startTime;
String hash = getHash(penalties.stream().map(p -> p.getAddress()).collect(Collectors.toList()));
LOGGER.info("{} penalty addresses orphaned (hash: {}). Total time taken: {} seconds", penalties.size(), hash, (int)(totalTime / 1000.0f));
logPenaltyStats(block.repository);
int updatedCount = updateAccountLevels(block.repository, penalties);
LOGGER.info("Account levels updated for {} penalty addresses", updatedCount);
}
public static Set<AccountPenaltyData> getAccountPenalties(Repository repository, int penalty) throws DataException {
final long snapshotTimestampV1 = BlockChain.getInstance().getSelfSponsorshipAlgoV1SnapshotTimestamp();
final long snapshotTimestampV3 = BlockChain.getInstance().getSelfSponsorshipAlgoV3SnapshotTimestamp();
Set<AccountPenaltyData> penalties = new LinkedHashSet<>();
List<String> addresses = repository.getTransactionRepository().getConfirmedRewardShareCreatorsExcludingSelfShares();
for (String address : addresses) {
//System.out.println(String.format("address: %s", address));
SelfSponsorshipAlgoV3 selfSponsorshipAlgoV3 = new SelfSponsorshipAlgoV3(repository, address, snapshotTimestampV1, snapshotTimestampV3, false);
selfSponsorshipAlgoV3.run();
//System.out.println(String.format("Penalty addresses: %d", selfSponsorshipAlgoV3.getPenaltyAddresses().size()));
for (String penaltyAddress : selfSponsorshipAlgoV3.getPenaltyAddresses()) {
penalties.add(new AccountPenaltyData(penaltyAddress, penalty));
}
}
return penalties;
}
private static int updateAccountLevels(Repository repository, Set<AccountPenaltyData> accountPenalties) throws DataException {
final List<Integer> cumulativeBlocksByLevel = BlockChain.getInstance().getCumulativeBlocksByLevel();
final int maximumLevel = cumulativeBlocksByLevel.size() - 1;
int updatedCount = 0;
for (AccountPenaltyData penaltyData : accountPenalties) {
AccountData accountData = repository.getAccountRepository().getAccount(penaltyData.getAddress());
final int effectiveBlocksMinted = accountData.getBlocksMinted() + accountData.getBlocksMintedAdjustment() + accountData.getBlocksMintedPenalty();
// Shortcut for penalties
if (effectiveBlocksMinted < 0) {
accountData.setLevel(0);
repository.getAccountRepository().setLevel(accountData);
updatedCount++;
LOGGER.trace(() -> String.format("Block minter %s dropped to level %d", accountData.getAddress(), accountData.getLevel()));
continue;
}
for (int newLevel = maximumLevel; newLevel >= 0; --newLevel) {
if (effectiveBlocksMinted >= cumulativeBlocksByLevel.get(newLevel)) {
accountData.setLevel(newLevel);
repository.getAccountRepository().setLevel(accountData);
updatedCount++;
LOGGER.trace(() -> String.format("Block minter %s increased to level %d", accountData.getAddress(), accountData.getLevel()));
break;
}
}
}
return updatedCount;
}
private static void logPenaltyStats(Repository repository) {
try {
LOGGER.info(getPenaltyStats(repository));
} catch (DataException e) {}
}
private static AccountPenaltyStats getPenaltyStats(Repository repository) throws DataException {
List<AccountData> accounts = repository.getAccountRepository().getPenaltyAccounts();
return AccountPenaltyStats.fromAccounts(accounts);
}
public static String getHash(List<String> penaltyAddresses) {
if (penaltyAddresses == null || penaltyAddresses.isEmpty()) {
return null;
}
Collections.sort(penaltyAddresses);
return Base58.encode(Crypto.digest(StringUtils.join(penaltyAddresses).getBytes(StandardCharsets.UTF_8)));
}
}

View File

@ -291,78 +291,4 @@ public class AutoUpdate extends Thread {
return true; // repo was okay, even if applying update failed
}
}
public static boolean attemptRestart() {
LOGGER.info(String.format("Restarting node..."));
// Give repository a chance to backup in case things go badly wrong (if enabled)
if (Settings.getInstance().getRepositoryBackupInterval() > 0) {
try {
// Timeout if the database isn't ready for backing up after 60 seconds
long timeout = 60 * 1000L;
RepositoryManager.backup(true, "backup", timeout);
} catch (TimeoutException e) {
LOGGER.info("Attempt to backup repository failed due to timeout: {}", e.getMessage());
// Continue with the node restart anyway...
}
}
// Call ApplyUpdate to end this process (unlocking current JAR so it can be replaced)
String javaHome = System.getProperty("java.home");
LOGGER.debug(String.format("Java home: %s", javaHome));
Path javaBinary = Paths.get(javaHome, "bin", "java");
LOGGER.debug(String.format("Java binary: %s", javaBinary));
try {
List<String> javaCmd = new ArrayList<>();
// Java runtime binary itself
javaCmd.add(javaBinary.toString());
// JVM arguments
javaCmd.addAll(ManagementFactory.getRuntimeMXBean().getInputArguments());
// Disable, but retain, any -agentlib JVM arg as sub-process might fail if it tries to reuse same port
javaCmd = javaCmd.stream()
.map(arg -> arg.replace("-agentlib", AGENTLIB_JVM_HOLDER_ARG))
.collect(Collectors.toList());
// Remove JNI options as they won't be supported by command-line 'java'
// These are typically added by the AdvancedInstaller Java launcher EXE
javaCmd.removeAll(Arrays.asList("abort", "exit", "vfprintf"));
// Call ApplyUpdate using JAR
javaCmd.addAll(Arrays.asList("-cp", JAR_FILENAME, ApplyUpdate.class.getCanonicalName()));
// Add command-line args saved from start-up
String[] savedArgs = Controller.getInstance().getSavedArgs();
if (savedArgs != null)
javaCmd.addAll(Arrays.asList(savedArgs));
LOGGER.info(String.format("Restarting node with: %s", String.join(" ", javaCmd)));
SysTray.getInstance().showMessage(Translator.INSTANCE.translate("SysTray", "AUTO_UPDATE"), //TODO
Translator.INSTANCE.translate("SysTray", "APPLYING_UPDATE_AND_RESTARTING"), //TODO
MessageType.INFO);
ProcessBuilder processBuilder = new ProcessBuilder(javaCmd);
// New process will inherit our stdout and stderr
processBuilder.redirectOutput(ProcessBuilder.Redirect.INHERIT);
processBuilder.redirectError(ProcessBuilder.Redirect.INHERIT);
Process process = processBuilder.start();
// Nothing to pipe to new process, so close output stream (process's stdin)
process.getOutputStream().close();
return true; // restarting node OK
} catch (Exception e) {
LOGGER.error(String.format("Failed to restart node: %s", e.getMessage()));
return true; // repo was okay, even if applying update failed
}
}
}

View File

@ -474,6 +474,7 @@ public class BlockMinter extends Thread {
Iterator<TransactionData> unconfirmedTransactionsIterator = unconfirmedTransactions.iterator();
final long newBlockTimestamp = newBlock.getBlockData().getTimestamp();
final int newBlockHeight = newBlock.getBlockData().getHeight();
while (unconfirmedTransactionsIterator.hasNext()) {
TransactionData transactionData = unconfirmedTransactionsIterator.next();
@ -481,6 +482,12 @@ public class BlockMinter extends Thread {
// Ignore transactions that have expired before this block - they will be cleaned up later
if (transactionData.getTimestamp() > newBlockTimestamp || Transaction.getDeadline(transactionData) <= newBlockTimestamp)
unconfirmedTransactionsIterator.remove();
// Ignore transactions that are unconfirmable at this block height
Transaction transaction = Transaction.fromData(repository, transactionData);
if (!transaction.isConfirmableAtHeight(newBlockHeight)) {
unconfirmedTransactionsIterator.remove();
}
}
// Sign to create block's signature, needed by Block.isValid()

View File

@ -0,0 +1,103 @@
package org.qortal.controller;
import org.apache.logging.log4j.LogManager;
import org.apache.logging.log4j.Logger;
import org.qortal.ApplyBootstrap;
import org.qortal.globalization.Translator;
import org.qortal.gui.SysTray;
import org.qortal.repository.RepositoryManager;
import org.qortal.settings.Settings;
import java.awt.TrayIcon.MessageType;
import java.lang.management.ManagementFactory;
import java.nio.file.Path;
import java.nio.file.Paths;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.List;
import java.util.concurrent.TimeoutException;
import java.util.stream.Collectors;
/* NOTE: It is CRITICAL that we use OpenJDK and not Java SE because our uber jar repacks BouncyCastle which, in turn, unsigns BC causing it to be rejected as a security provider by Java SE. */
public class BootstrapNode {
public static final String JAR_FILENAME = "qortal.jar";
public static final String AGENTLIB_JVM_HOLDER_ARG = "-DQORTAL_agentlib=";
private static final Logger LOGGER = LogManager.getLogger(BootstrapNode.class);
public static boolean attemptToBootstrap() {
LOGGER.info(String.format("Bootstrapping node..."));
// Give repository a chance to backup in case things go badly wrong (if enabled)
if (Settings.getInstance().getRepositoryBackupInterval() > 0) {
try {
// Timeout if the database isn't ready for backing up after 60 seconds
long timeout = 60 * 1000L;
RepositoryManager.backup(true, "backup", timeout);
} catch (TimeoutException e) {
LOGGER.info("Attempt to backup repository failed due to timeout: {}", e.getMessage());
// Continue with the bootstrap anyway...
}
}
// Call ApplyBootstrap to end this process
String javaHome = System.getProperty("java.home");
LOGGER.debug(String.format("Java home: %s", javaHome));
Path javaBinary = Paths.get(javaHome, "bin", "java");
LOGGER.debug(String.format("Java binary: %s", javaBinary));
try {
List<String> javaCmd = new ArrayList<>();
// Java runtime binary itself
javaCmd.add(javaBinary.toString());
// JVM arguments
javaCmd.addAll(ManagementFactory.getRuntimeMXBean().getInputArguments());
// Disable, but retain, any -agentlib JVM arg as sub-process might fail if it tries to reuse same port
javaCmd = javaCmd.stream()
.map(arg -> arg.replace("-agentlib", AGENTLIB_JVM_HOLDER_ARG))
.collect(Collectors.toList());
// Remove JNI options as they won't be supported by command-line 'java'
// These are typically added by the AdvancedInstaller Java launcher EXE
javaCmd.removeAll(Arrays.asList("abort", "exit", "vfprintf"));
// Call ApplyBootstrap using JAR
javaCmd.addAll(Arrays.asList("-cp", JAR_FILENAME, ApplyBootstrap.class.getCanonicalName()));
// Add command-line args saved from start-up
String[] savedArgs = Controller.getInstance().getSavedArgs();
if (savedArgs != null)
javaCmd.addAll(Arrays.asList(savedArgs));
LOGGER.info(String.format("Restarting node with: %s", String.join(" ", javaCmd)));
SysTray.getInstance().showMessage(Translator.INSTANCE.translate("SysTray", "BOOTSTRAP_NODE"),
Translator.INSTANCE.translate("SysTray", "APPLYING_BOOTSTRAP_AND_RESTARTING"),
MessageType.INFO);
ProcessBuilder processBuilder = new ProcessBuilder(javaCmd);
// New process will inherit our stdout and stderr
processBuilder.redirectOutput(ProcessBuilder.Redirect.INHERIT);
processBuilder.redirectError(ProcessBuilder.Redirect.INHERIT);
Process process = processBuilder.start();
// Nothing to pipe to new process, so close output stream (process's stdin)
process.getOutputStream().close();
return true; // restarting node OK
} catch (Exception e) {
LOGGER.error(String.format("Failed to restart node: %s", e.getMessage()));
return true; // repo was okay, even if applying bootstrap failed
}
}
}

View File

@ -31,6 +31,7 @@ import org.qortal.globalization.Translator;
import org.qortal.gui.Gui;
import org.qortal.gui.SysTray;
import org.qortal.network.Network;
import org.qortal.network.RNSNetwork;
import org.qortal.network.Peer;
import org.qortal.network.message.*;
import org.qortal.repository.*;
@ -115,6 +116,7 @@ public class Controller extends Thread {
private long repositoryCheckpointTimestamp = startTime; // ms
private long prunePeersTimestamp = startTime; // ms
private long ntpCheckTimestamp = startTime; // ms
private long pruneRNSPeersTimestamp = startTime; // ms
private long deleteExpiredTimestamp = startTime + DELETE_EXPIRED_INTERVAL; // ms
/** Whether we can mint new blocks, as reported by BlockMinter. */
@ -481,6 +483,15 @@ public class Controller extends Thread {
return; // Not System.exit() so that GUI can display error
}
LOGGER.info("Starting Reticulum");
try {
RNSNetwork rns = RNSNetwork.getInstance();
rns.start();
LOGGER.debug("Reticulum instance: {}", rns.toString());
} catch (IOException | DataException e) {
LOGGER.error("Unable to start Reticulum", e);
}
Runtime.getRuntime().addShutdownHook(new Thread() {
@Override
public void run() {
@ -582,6 +593,8 @@ public class Controller extends Thread {
final long repositoryCheckpointInterval = Settings.getInstance().getRepositoryCheckpointInterval();
long repositoryMaintenanceInterval = getRandomRepositoryMaintenanceInterval();
final long prunePeersInterval = 5 * 60 * 1000L; // Every 5 minutes
//final long pruneRNSPeersInterval = 5 * 60 * 1000L; // Every 5 minutes
final long pruneRNSPeersInterval = 1 * 60 * 1000L; // Every 1 minute (during development)
// Start executor service for trimming or pruning
PruneManager.getInstance().start();
@ -690,6 +703,18 @@ public class Controller extends Thread {
}
}
// Q: Do we need global pruning?
if (now >= pruneRNSPeersTimestamp + pruneRNSPeersInterval) {
pruneRNSPeersTimestamp = now + pruneRNSPeersInterval;
try {
LOGGER.debug("Pruning Reticulum peers...");
RNSNetwork.getInstance().prunePeers();
} catch (DataException e) {
LOGGER.warn(String.format("Repository issue when trying to prune Reticulum peers: %s", e.getMessage()));
}
}
// Delete expired transactions
if (now >= deleteExpiredTimestamp) {
deleteExpiredTimestamp = now + DELETE_EXPIRED_INTERVAL;
@ -988,6 +1013,9 @@ public class Controller extends Thread {
LOGGER.info("Shutting down networking");
Network.getInstance().shutdown();
LOGGER.info("Shutting down Reticulum");
RNSNetwork.getInstance().shutdown();
LOGGER.info("Shutting down controller");
this.interrupt();
try {

View File

@ -0,0 +1,102 @@
package org.qortal.controller;
import org.apache.logging.log4j.LogManager;
import org.apache.logging.log4j.Logger;
import org.qortal.ApplyRestart;;
import org.qortal.globalization.Translator;
import org.qortal.gui.SysTray;
import org.qortal.repository.RepositoryManager;
import org.qortal.settings.Settings;
import java.awt.TrayIcon.MessageType;
import java.lang.management.ManagementFactory;
import java.nio.file.Path;
import java.nio.file.Paths;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.List;
import java.util.concurrent.TimeoutException;
import java.util.stream.Collectors;
/* NOTE: It is CRITICAL that we use OpenJDK and not Java SE because our uber jar repacks BouncyCastle which, in turn, unsigns BC causing it to be rejected as a security provider by Java SE. */
public class RestartNode {
public static final String JAR_FILENAME = "qortal.jar";
public static final String AGENTLIB_JVM_HOLDER_ARG = "-DQORTAL_agentlib=";
private static final Logger LOGGER = LogManager.getLogger(RestartNode.class);
public static boolean attemptToRestart() {
LOGGER.info(String.format("Restarting node..."));
// Give repository a chance to backup in case things go badly wrong (if enabled)
if (Settings.getInstance().getRepositoryBackupInterval() > 0) {
try {
// Timeout if the database isn't ready for backing up after 60 seconds
long timeout = 60 * 1000L;
RepositoryManager.backup(true, "backup", timeout);
} catch (TimeoutException e) {
LOGGER.info("Attempt to backup repository failed due to timeout: {}", e.getMessage());
// Continue with the node restart anyway...
}
}
// Call ApplyRestart to end this process
String javaHome = System.getProperty("java.home");
LOGGER.debug(String.format("Java home: %s", javaHome));
Path javaBinary = Paths.get(javaHome, "bin", "java");
LOGGER.debug(String.format("Java binary: %s", javaBinary));
try {
List<String> javaCmd = new ArrayList<>();
// Java runtime binary itself
javaCmd.add(javaBinary.toString());
// JVM arguments
javaCmd.addAll(ManagementFactory.getRuntimeMXBean().getInputArguments());
// Disable, but retain, any -agentlib JVM arg as sub-process might fail if it tries to reuse same port
javaCmd = javaCmd.stream()
.map(arg -> arg.replace("-agentlib", AGENTLIB_JVM_HOLDER_ARG))
.collect(Collectors.toList());
// Remove JNI options as they won't be supported by command-line 'java'
// These are typically added by the AdvancedInstaller Java launcher EXE
javaCmd.removeAll(Arrays.asList("abort", "exit", "vfprintf"));
// Call ApplyRestart using JAR
javaCmd.addAll(Arrays.asList("-cp", JAR_FILENAME, ApplyRestart.class.getCanonicalName()));
// Add command-line args saved from start-up
String[] savedArgs = Controller.getInstance().getSavedArgs();
if (savedArgs != null)
javaCmd.addAll(Arrays.asList(savedArgs));
LOGGER.debug(String.format("Restarting node with: %s", String.join(" ", javaCmd)));
SysTray.getInstance().showMessage(Translator.INSTANCE.translate("SysTray", "RESTARTING_NODE"),
Translator.INSTANCE.translate("SysTray", "APPLYING_RESTARTING_NODE"),
MessageType.INFO);
ProcessBuilder processBuilder = new ProcessBuilder(javaCmd);
// New process will inherit our stdout and stderr
processBuilder.redirectOutput(ProcessBuilder.Redirect.INHERIT);
processBuilder.redirectError(ProcessBuilder.Redirect.INHERIT);
Process process = processBuilder.start();
// Nothing to pipe to new process, so close output stream (process's stdin)
process.getOutputStream().close();
return true; // restarting node OK
} catch (Exception e) {
LOGGER.error(String.format("Failed to restart node: %s", e.getMessage()));
return true; // repo was okay, even if applying restart failed
}
}
}

View File

@ -7,7 +7,7 @@ import org.bitcoinj.params.MainNetParams;
import org.bitcoinj.params.RegTestParams;
import org.bitcoinj.params.TestNet3Params;
import org.qortal.crosschain.ElectrumX.Server;
import org.qortal.crosschain.ElectrumX.Server.ConnectionType;
import org.qortal.crosschain.ChainableServer.ConnectionType;
import org.qortal.settings.Settings;
import java.util.Arrays;
@ -22,8 +22,6 @@ public class Bitcoin extends Bitcoiny {
private static final long MINIMUM_ORDER_AMOUNT = 100000; // 0.001 BTC minimum order, due to high fees
// Temporary values until a dynamic fee system is written.
private static final long OLD_FEE_AMOUNT = 4_000L; // Not 5000 so that existing P2SH-B can output 1000, avoiding dust issue, leaving 4000 for fees.
private static final long NEW_FEE_TIMESTAMP = 1598280000000L; // milliseconds since epoch
private static final long NEW_FEE_AMOUNT = 6_000L;
private static final long NON_MAINNET_FEE = 1000L; // enough for TESTNET3 and should be OK for REGTEST
@ -46,74 +44,62 @@ public class Bitcoin extends Bitcoiny {
return Arrays.asList(
// Servers chosen on NO BASIS WHATSOEVER from various sources!
// Status verified at https://1209k.com/bitcoin-eye/ele.php?chain=btc
new Server("104.248.139.211", Server.ConnectionType.SSL, 50002),
new Server("104.198.149.61", Server.ConnectionType.SSL, 50002),
new Server("128.0.190.26", Server.ConnectionType.SSL, 50002),
new Server("142.93.6.38", Server.ConnectionType.SSL, 50002),
new Server("157.245.172.236", Server.ConnectionType.SSL, 50002),
new Server("167.172.226.175", Server.ConnectionType.SSL, 50002),
new Server("167.172.42.31", Server.ConnectionType.SSL, 50002),
new Server("178.62.80.20", Server.ConnectionType.SSL, 50002),
new Server("185.64.116.15", Server.ConnectionType.SSL, 50002),
new Server("188.165.206.215", Server.ConnectionType.SSL, 50002),
new Server("188.165.211.112", Server.ConnectionType.SSL, 50002),
new Server("2azzarita.hopto.org", Server.ConnectionType.SSL, 50002),
new Server("2electrumx.hopto.me", Server.ConnectionType.SSL, 56022),
new Server("2ex.digitaleveryware.com", Server.ConnectionType.SSL, 50002),
new Server("65.39.140.37", Server.ConnectionType.SSL, 50002),
new Server("68.183.188.105", Server.ConnectionType.SSL, 50002),
new Server("71.73.14.254", Server.ConnectionType.SSL, 50002),
new Server("94.23.247.135", Server.ConnectionType.SSL, 50002),
new Server("assuredly.not.fyi", Server.ConnectionType.SSL, 50002),
new Server("ax101.blockeng.ch", Server.ConnectionType.SSL, 50002),
new Server("ax102.blockeng.ch", Server.ConnectionType.SSL, 50002),
new Server("260.whyza.net", Server.ConnectionType.SSL, 50002),
new Server("34.136.93.37", Server.ConnectionType.SSL, 50002),
new Server("34.67.22.216", Server.ConnectionType.SSL, 50002),
new Server("34.68.133.78", Server.ConnectionType.SSL, 50002),
new Server("alviss.coinjoined.com", Server.ConnectionType.SSL, 50002),
new Server("b.1209k.com", Server.ConnectionType.SSL, 50002),
new Server("b6.1209k.com", Server.ConnectionType.SSL, 50002),
new Server("bitcoin.dermichi.com", Server.ConnectionType.SSL, 50002),
new Server("bitcoin.lu.ke", Server.ConnectionType.SSL, 50002),
new Server("bitcoin.lukechilds.co", Server.ConnectionType.SSL, 50002),
new Server("blkhub.net", Server.ConnectionType.SSL, 50002),
new Server("btc.electroncash.dk", Server.ConnectionType.SSL, 60002),
new Server("btc.aftrek.org", Server.ConnectionType.SSL, 50002),
new Server("btc.hodler.ninja", Server.ConnectionType.SSL, 50002),
new Server("btc.ocf.sh", Server.ConnectionType.SSL, 50002),
new Server("btce.iiiiiii.biz", Server.ConnectionType.SSL, 50002),
new Server("caleb.vegas", Server.ConnectionType.SSL, 50002),
new Server("d762li0k0g.d.firewalla.org", Server.ConnectionType.SSL, 50002),
new Server("de.poiuty.com", Server.ConnectionType.SSL, 50002),
new Server("dijon.anties.org", Server.ConnectionType.SSL, 50002),
new Server("eai.coincited.net", Server.ConnectionType.SSL, 50002),
new Server("electrum.bhoovd.com", Server.ConnectionType.SSL, 50002),
new Server("electrum.bitaroo.net", Server.ConnectionType.SSL, 50002),
new Server("electrum.bitcoinlizard.net", Server.ConnectionType.SSL, 50002),
new Server("electrum.blockstream.info", Server.ConnectionType.SSL, 50002),
new Server("electrum.bitrefill.com", Server.ConnectionType.SSL, 50002),
new Server("electrum.brainshome.de", Server.ConnectionType.SSL, 50002),
new Server("electrum.emzy.de", Server.ConnectionType.SSL, 50002),
new Server("electrum.exan.tech", Server.ConnectionType.SSL, 50002),
new Server("electrum.kcicom.net", Server.ConnectionType.SSL, 50002),
new Server("electrum.kendigisland.xyz", Server.ConnectionType.SSL, 50002),
new Server("electrum.mmitech.info", Server.ConnectionType.SSL, 50002),
new Server("electrum.petrkr.net", Server.ConnectionType.SSL, 50002),
new Server("electrum.stippy.com", Server.ConnectionType.SSL, 50002),
new Server("electrum.thomasfischbach.de", Server.ConnectionType.SSL, 50002),
new Server("electrum-btc.leblancnet.us", Server.ConnectionType.SSL, 50002),
new Server("electrum0.snel.it", Server.ConnectionType.SSL, 50002),
new Server("electrum1.cipig.net", Server.ConnectionType.SSL, 50002),
new Server("electrum2.cipig.net", Server.ConnectionType.SSL, 50002),
new Server("electrum3.cipig.net", Server.ConnectionType.SSL, 50002),
new Server("electrumx.alexridevski.net", Server.ConnectionType.SSL, 50002),
new Server("electrum1.cipig.net", Server.ConnectionType.SSL, 20000),
new Server("electrum2.cipig.net", Server.ConnectionType.SSL, 20000),
new Server("electrum3.cipig.net", Server.ConnectionType.SSL, 20000),
new Server("electrumx.blockfinance-eco.li", Server.ConnectionType.SSL, 50002),
new Server("electrumx.indoor.app", Server.ConnectionType.SSL, 50002),
new Server("electrumx.iodata.org", Server.ConnectionType.SSL, 50002),
new Server("electrumx-core.1209k.com", Server.ConnectionType.SSL, 50002),
new Server("elx.bitske.com", Server.ConnectionType.SSL, 50002),
new Server("ex03.axalgo.com", Server.ConnectionType.SSL, 50002),
new Server("ex05.axalgo.com", Server.ConnectionType.SSL, 50002),
new Server("ex07.axalgo.com", Server.ConnectionType.SSL, 50002),
new Server("exs.dyshek.org", Server.ConnectionType.SSL, 50002),
new Server("fortress.qtornado.com", Server.ConnectionType.SSL, 50002),
new Server("fulcrum.grey.pw", Server.ConnectionType.SSL, 50002),
new Server("fulcrum.sethforprivacy.com", Server.ConnectionType.SSL, 51002),
new Server("guichet.centure.cc", Server.ConnectionType.SSL, 50002),
new Server("hodl.artyomk13.me", Server.ConnectionType.SSL, 50002),
new Server("hodlers.beer", Server.ConnectionType.SSL, 50002),
new Server("kareoke.qoppa.org", Server.ConnectionType.SSL, 50002),
new Server("kirsche.emzy.de", Server.ConnectionType.SSL, 50002),
new Server("kittyserver.ddnsfree.com", Server.ConnectionType.SSL, 50002),
new Server("lille.anties.org", Server.ConnectionType.SSL, 50002),
new Server("marseille.anties.org", Server.ConnectionType.SSL, 50002),
new Server("node1.btccuracao.com", Server.ConnectionType.SSL, 50002),
new Server("osr1ex1.compumundohipermegared.one", Server.ConnectionType.SSL, 50002),
new Server("smmalis37.ddns.net", Server.ConnectionType.SSL, 50002),
new Server("ulrichard.ch", Server.ConnectionType.SSL, 50002),
new Server("vmd104012.contaboserver.net", Server.ConnectionType.SSL, 50002),
new Server("vmd104014.contaboserver.net", Server.ConnectionType.SSL, 50002),
new Server("paris.anties.org", Server.ConnectionType.SSL, 50002),
new Server("ragtor.duckdns.org", Server.ConnectionType.SSL, 50002),
new Server("stavver.dyshek.org", Server.ConnectionType.SSL, 50002),
new Server("vmd63185.contaboserver.net", Server.ConnectionType.SSL, 50002),
new Server("vmd71287.contaboserver.net", Server.ConnectionType.SSL, 50002),
new Server("vmd84592.contaboserver.net", Server.ConnectionType.SSL, 50002),
new Server("xtrum.com", Server.ConnectionType.SSL, 50002)
);
}
@ -125,11 +111,7 @@ public class Bitcoin extends Bitcoiny {
@Override
public long getP2shFee(Long timestamp) {
// TODO: This will need to be replaced with something better in the near future!
if (timestamp != null && timestamp < NEW_FEE_TIMESTAMP)
return OLD_FEE_AMOUNT;
return NEW_FEE_AMOUNT;
return this.getFeeCeiling();
}
},
TEST3 {
@ -141,12 +123,17 @@ public class Bitcoin extends Bitcoiny {
@Override
public Collection<ElectrumX.Server> getServers() {
return Arrays.asList(
new Server("tn.not.fyi", Server.ConnectionType.SSL, 55002),
new Server("electrumx-test.1209k.com", Server.ConnectionType.SSL, 50002),
new Server("testnet.qtornado.com", Server.ConnectionType.SSL, 51002),
new Server("testnet.aranguren.org", Server.ConnectionType.TCP, 51001),
new Server("bitcoin.devmole.eu", Server.ConnectionType.TCP, 5000),
new Server("bitcoin.stagemole.eu", Server.ConnectionType.TCP, 5000),
new Server("blockstream.info", Server.ConnectionType.SSL, 993),
new Server("electrum.blockstream.info", Server.ConnectionType.SSL, 60002),
new Server("electrum1.cipig.net", Server.ConnectionType.TCP, 10068),
new Server("electrum2.cipig.net", Server.ConnectionType.TCP, 10068),
new Server("electrum3.cipig.net", Server.ConnectionType.TCP, 10068),
new Server("testnet.aranguren.org", Server.ConnectionType.SSL, 51002),
new Server("testnet.hsmiths.com", Server.ConnectionType.SSL, 53012)
new Server("testnet.hsmiths.com", Server.ConnectionType.SSL, 53012),
new Server("testnet.qtornado.com", Server.ConnectionType.SSL, 51002),
new Server("v22019051929289916.bestsrv.de", Server.ConnectionType.SSL, 50002)
);
}
@ -186,6 +173,16 @@ public class Bitcoin extends Bitcoiny {
}
};
private long feeCeiling = NEW_FEE_AMOUNT;
public long getFeeCeiling() {
return feeCeiling;
}
public void setFeeCeiling(long feeCeiling) {
this.feeCeiling = feeCeiling;
}
public abstract NetworkParameters getParams();
public abstract Collection<ElectrumX.Server> getServers();
public abstract String getGenesisHash();
@ -199,7 +196,7 @@ public class Bitcoin extends Bitcoiny {
// Constructors and instance
private Bitcoin(BitcoinNet bitcoinNet, BitcoinyBlockchainProvider blockchain, Context bitcoinjContext, String currencyCode) {
super(blockchain, bitcoinjContext, currencyCode);
super(blockchain, bitcoinjContext, currencyCode, bitcoinjContext.getFeePerKb());
this.bitcoinNet = bitcoinNet;
LOGGER.info(() -> String.format("Starting Bitcoin support using %s", this.bitcoinNet.name()));
@ -244,6 +241,16 @@ public class Bitcoin extends Bitcoiny {
return this.bitcoinNet.getP2shFee(timestamp);
}
@Override
public long getFeeCeiling() {
return this.bitcoinNet.getFeeCeiling();
}
@Override
public void setFeeCeiling(long fee) {
this.bitcoinNet.setFeeCeiling( fee );
}
/**
* Returns bitcoinj transaction sending <tt>amount</tt> to <tt>recipient</tt> using 20 sat/byte fee.
*

View File

@ -11,6 +11,7 @@ import org.bitcoinj.crypto.DeterministicKey;
import org.bitcoinj.script.Script.ScriptType;
import org.bitcoinj.script.ScriptBuilder;
import org.bitcoinj.wallet.DeterministicKeyChain;
import org.bitcoinj.wallet.KeyChain;
import org.bitcoinj.wallet.SendRequest;
import org.bitcoinj.wallet.Wallet;
import org.qortal.api.model.SimpleForeignTransaction;
@ -52,12 +53,15 @@ public abstract class Bitcoiny implements ForeignBlockchain {
/** Byte offset into raw block headers to block timestamp. */
private static final int TIMESTAMP_OFFSET = 4 + 32 + 32;
protected Coin feePerKb;
// Constructors and instance
protected Bitcoiny(BitcoinyBlockchainProvider blockchainProvider, Context bitcoinjContext, String currencyCode) {
protected Bitcoiny(BitcoinyBlockchainProvider blockchainProvider, Context bitcoinjContext, String currencyCode, Coin feePerKb) {
this.blockchainProvider = blockchainProvider;
this.bitcoinjContext = bitcoinjContext;
this.currencyCode = currencyCode;
this.feePerKb = feePerKb;
this.params = this.bitcoinjContext.getParams();
}
@ -166,7 +170,11 @@ public abstract class Bitcoiny implements ForeignBlockchain {
/** Returns fee per transaction KB. To be overridden for testnet/regtest. */
public Coin getFeePerKb() {
return this.bitcoinjContext.getFeePerKb();
return this.feePerKb;
}
public void setFeePerKb(Coin feePerKb) {
this.feePerKb = feePerKb;
}
/** Returns minimum order size in sats. To be overridden for coins that need to restrict order size. */
@ -504,7 +512,7 @@ public abstract class Bitcoiny implements ForeignBlockchain {
List<String> candidates = this.getSpendingCandidateAddresses(key58);
for(DeterministicKey key : getWalletKeys(key58)) {
for(DeterministicKey key : getOldWalletKeys(key58)) {
infos.add(buildAddressInfo(key, candidates));
}
@ -591,11 +599,23 @@ public abstract class Bitcoiny implements ForeignBlockchain {
}
}
private List<DeterministicKey> getWalletKeys(String key58) throws ForeignBlockchainException {
/**
* Get Old Wallet Keys
*
* Get wallet keys using the old key generation algorithm. This is used for diagnosing and repairing wallets
* created before 2024.
*
* @param masterPrivateKey
*
* @return the keys
*
* @throws ForeignBlockchainException
*/
private List<DeterministicKey> getOldWalletKeys(String masterPrivateKey) throws ForeignBlockchainException {
synchronized (this) {
Context.propagate(bitcoinjContext);
Wallet wallet = walletFromDeterministicKey58(key58);
Wallet wallet = walletFromDeterministicKey58(masterPrivateKey);
DeterministicKeyChain keyChain = wallet.getActiveKeyChain();
keyChain.setLookaheadSize(Bitcoiny.WALLET_KEY_LOOKAHEAD_INCREMENT);
@ -720,7 +740,7 @@ public abstract class Bitcoiny implements ForeignBlockchain {
}
/**
* Returns first unused receive address given 'm' BIP32 key.
* Returns first unused receive address given a BIP32 key.
*
* @param key58 BIP32/HD extended Bitcoin private/public key
* @return P2PKH address
@ -732,68 +752,22 @@ public abstract class Bitcoiny implements ForeignBlockchain {
Wallet wallet = walletFromDeterministicKey58(key58);
DeterministicKeyChain keyChain = wallet.getActiveKeyChain();
keyChain.setLookaheadSize(Bitcoiny.WALLET_KEY_LOOKAHEAD_INCREMENT);
keyChain.maybeLookAhead();
final int keyChainPathSize = keyChain.getAccountPath().size();
List<DeterministicKey> keys = new ArrayList<>(keyChain.getLeafKeys());
int ki = 0;
do {
for (; ki < keys.size(); ++ki) {
DeterministicKey dKey = keys.get(ki);
List<ChildNumber> dKeyPath = dKey.getPath();
// the next receive funds address
Address address = Address.fromKey(this.params, keyChain.getKey(KeyChain.KeyPurpose.RECEIVE_FUNDS), ScriptType.P2PKH);
// If keyChain is based on 'm', then make sure dKey is m/0/ki - i.e. a 'receive' address, not 'change' (m/1/ki)
if (dKeyPath.size() != keyChainPathSize + 2 || dKeyPath.get(dKeyPath.size() - 2) != ChildNumber.ZERO)
continue;
// if zero transactions, return address
if( 0 == getAddressTransactions(ScriptBuilder.createOutputScript(address).getProgram(), true).size() )
return address.toString();
// Check unspent
Address address = Address.fromKey(this.params, dKey, ScriptType.P2PKH);
byte[] script = ScriptBuilder.createOutputScript(address).getProgram();
List<UnspentOutput> unspentOutputs = this.blockchainProvider.getUnspentOutputs(script, false);
/*
* If there are no unspent outputs then either:
* a) all the outputs have been spent
* b) address has never been used
*
* For case (a) we want to remember not to check this address (key) again.
*/
if (unspentOutputs.isEmpty()) {
// If this is a known key that has been spent before, then we can skip asking for transaction history
if (this.spentKeys.contains(dKey)) {
wallet.getActiveKeyChain().markKeyAsUsed(dKey);
continue;
}
// Ask for transaction history - if it's empty then key has never been used
List<TransactionHash> historicTransactionHashes = this.blockchainProvider.getAddressTransactions(script, false);
if (!historicTransactionHashes.isEmpty()) {
// Fully spent key - case (a)
this.spentKeys.add(dKey);
wallet.getActiveKeyChain().markKeyAsUsed(dKey);
continue;
}
// Key never been used - case (b)
return address.toString();
}
// Key has unspent outputs, hence used, so no good to us
this.spentKeys.remove(dKey);
}
// Generate some more keys
keys.addAll(generateMoreKeys(keyChain));
// Process new keys
// else try the next receive funds address
} while (true);
}
public abstract long getFeeCeiling();
public abstract void setFeeCeiling(long fee);
// UTXOProvider support
static class WalletAwareUTXOProvider implements UTXOProvider {
@ -1047,4 +1021,52 @@ public abstract class Bitcoiny implements ForeignBlockchain {
return Wallet.fromWatchingKeyB58(this.params, key58, DeterministicHierarchy.BIP32_STANDARDISATION_TIME_SECS);
}
/**
* Repair Wallet
*
* Repair wallets generated before 2024 by moving all the address balances to the first address.
*
* @param privateMasterKey
*
* @return the transaction Id of the spend operation that moves the balances or the exception name if an exception
* is thrown
*
* @throws ForeignBlockchainException
*/
public String repairOldWallet(String privateMasterKey) throws ForeignBlockchainException {
// create a deterministic wallet to satisfy the bitcoinj API
Wallet wallet = Wallet.createDeterministic(this.bitcoinjContext, ScriptType.P2PKH);
// use the blockchain resources of this instance for UTXO provision
wallet.setUTXOProvider(new BitcoinyUTXOProvider( this ));
// import in each that is generated using the old key generation algorithm
List<DeterministicKey> walletKeys = getOldWalletKeys(privateMasterKey);
for( DeterministicKey key : walletKeys) {
wallet.importKey(ECKey.fromPrivate(key.getPrivKey()));
}
// get the primary receive address
Address firstAddress = Address.fromKey(this.params, walletKeys.get(0), ScriptType.P2PKH);
// send all the imported coins to the primary receive address
SendRequest sendRequest = SendRequest.emptyWallet(firstAddress);
sendRequest.feePerKb = this.getFeePerKb();
try {
// allow the wallet to build the send request transaction and broadcast
wallet.completeTx(sendRequest);
broadcastTransaction(sendRequest.tx);
// return the transaction Id
return sendRequest.tx.getTxId().toString();
}
catch( Exception e ) {
// log error and return exception name
LOGGER.error(e.getMessage(), e);
return e.getClass().getSimpleName();
}
}
}

View File

@ -3,6 +3,7 @@ package org.qortal.crosschain;
import cash.z.wallet.sdk.rpc.CompactFormats.CompactBlock;
import java.util.List;
import java.util.Set;
public abstract class BitcoinyBlockchainProvider {
@ -59,4 +60,11 @@ public abstract class BitcoinyBlockchainProvider {
/** Broadcasts raw, serialized, transaction bytes to network, returning success/failure. */
public abstract void broadcastTransaction(byte[] rawTransaction) throws ForeignBlockchainException;
public abstract Set<ChainableServer> getServers();
public abstract List<ChainableServer> getRemainingServers();
public abstract Set<ChainableServer> getUselessServers();
public abstract ChainableServer getCurrentServer();
}

View File

@ -0,0 +1,80 @@
package org.qortal.crosschain;
import org.bitcoinj.core.*;
import org.bitcoinj.script.Script;
import org.bitcoinj.script.ScriptBuilder;
import java.util.ArrayList;
import java.util.List;
/**
* Class BitcoinyUTXOProvider
*
* Uses Bitcoiny resources for UTXO provision.
*/
public class BitcoinyUTXOProvider implements UTXOProvider {
private Bitcoiny bitcoiny;
public BitcoinyUTXOProvider(Bitcoiny bitcoiny) {
this.bitcoiny = bitcoiny;
}
@Override
public List<UTXO> getOpenTransactionOutputs(List<ECKey> keys) throws UTXOProviderException {
try {
List<UTXO> utxos = new ArrayList<>();
for( ECKey key : keys) {
Address address = Address.fromKey(this.bitcoiny.params, key, Script.ScriptType.P2PKH);
byte[] script = ScriptBuilder.createOutputScript(address).getProgram();
// collection UTXO's for all confirmed unspent outputs
for (UnspentOutput output : this.bitcoiny.blockchainProvider.getUnspentOutputs(script, false)) {
utxos.add(toUTXO(output));
}
}
return utxos;
} catch (ForeignBlockchainException e) {
throw new UTXOProviderException(e);
}
}
/**
* Convert Unspent Output to a UTXO
*
* @param unspentOutput
*
* @return the UTXO
*
* @throws ForeignBlockchainException
*/
private UTXO toUTXO(UnspentOutput unspentOutput) throws ForeignBlockchainException {
List<TransactionOutput> transactionOutputs = this.bitcoiny.getOutputs(unspentOutput.hash);
TransactionOutput transactionOutput = transactionOutputs.get(unspentOutput.index);
return new UTXO(
Sha256Hash.wrap(unspentOutput.hash),
unspentOutput.index,
Coin.valueOf(unspentOutput.value),
unspentOutput.height,
false,
transactionOutput.getScriptPubKey()
);
}
@Override
public int getChainHeadHeight() throws UTXOProviderException {
try {
return this.bitcoiny.blockchainProvider.getCurrentHeight();
} catch (ForeignBlockchainException e) {
throw new UTXOProviderException(e);
}
}
@Override
public NetworkParameters getParams() {
return this.bitcoiny.params;
}
}

View File

@ -0,0 +1,15 @@
package org.qortal.crosschain;
public interface ChainableServer {
public void addResponseTime(long responseTime);
public long averageResponseTime();
public String getHostName();
public int getPort();
public ConnectionType getConnectionType();
public enum ConnectionType {TCP, SSL}
}

View File

@ -7,7 +7,7 @@ import org.bitcoinj.params.RegTestParams;
import org.bitcoinj.params.TestNet3Params;
import org.libdohj.params.DigibyteMainNetParams;
import org.qortal.crosschain.ElectrumX.Server;
import org.qortal.crosschain.ElectrumX.Server.ConnectionType;
import org.qortal.crosschain.ChainableServer.ConnectionType;
import org.qortal.settings.Settings;
import java.util.Arrays;
@ -46,10 +46,6 @@ public class Digibyte extends Bitcoiny {
// Servers chosen on NO BASIS WHATSOEVER from various sources!
// Status verified at https://1209k.com/bitcoin-eye/ele.php?chain=dgb
new Server("electrum.qortal.link", Server.ConnectionType.SSL, 55002),
new Server("electrum1-dgb.qortal.online", Server.ConnectionType.SSL, 50002),
new Server("electrum2-dgb.qortal.online", Server.ConnectionType.SSL, 50002),
new Server("electrum3-dgb.qortal.online", Server.ConnectionType.SSL, 40002),
new Server("electrum4-dgb.qortal.online", Server.ConnectionType.SSL, 40002),
new Server("electrum1.cipig.net", Server.ConnectionType.SSL, 20059),
new Server("electrum2.cipig.net", Server.ConnectionType.SSL, 20059),
new Server("electrum3.cipig.net", Server.ConnectionType.SSL, 20059)
@ -63,8 +59,7 @@ public class Digibyte extends Bitcoiny {
@Override
public long getP2shFee(Long timestamp) {
// TODO: This will need to be replaced with something better in the near future!
return MAINNET_FEE;
return this.getFeeCeiling();
}
},
TEST3 {
@ -114,6 +109,16 @@ public class Digibyte extends Bitcoiny {
}
};
private long feeCeiling = MAINNET_FEE;
public long getFeeCeiling() {
return feeCeiling;
}
public void setFeeCeiling(long feeCeiling) {
this.feeCeiling = feeCeiling;
}
public abstract NetworkParameters getParams();
public abstract Collection<Server> getServers();
public abstract String getGenesisHash();
@ -127,7 +132,7 @@ public class Digibyte extends Bitcoiny {
// Constructors and instance
private Digibyte(DigibyteNet digibyteNet, BitcoinyBlockchainProvider blockchain, Context bitcoinjContext, String currencyCode) {
super(blockchain, bitcoinjContext, currencyCode);
super(blockchain, bitcoinjContext, currencyCode, DEFAULT_FEE_PER_KB);
this.digibyteNet = digibyteNet;
LOGGER.info(() -> String.format("Starting Digibyte support using %s", this.digibyteNet.name()));
@ -156,11 +161,6 @@ public class Digibyte extends Bitcoiny {
// Actual useful methods for use by other classes
@Override
public Coin getFeePerKb() {
return DEFAULT_FEE_PER_KB;
}
@Override
public long getMinimumOrderAmount() {
return MINIMUM_ORDER_AMOUNT;
@ -177,4 +177,14 @@ public class Digibyte extends Bitcoiny {
return this.digibyteNet.getP2shFee(timestamp);
}
@Override
public long getFeeCeiling() {
return this.digibyteNet.getFeeCeiling();
}
@Override
public void setFeeCeiling(long fee) {
this.digibyteNet.setFeeCeiling( fee );
}
}

View File

@ -6,7 +6,7 @@ import org.bitcoinj.core.NetworkParameters;
import org.libdohj.params.DogecoinMainNetParams;
import org.libdohj.params.DogecoinTestNet3Params;
import org.qortal.crosschain.ElectrumX.Server;
import org.qortal.crosschain.ElectrumX.Server.ConnectionType;
import org.qortal.crosschain.ChainableServer.ConnectionType;
import org.qortal.settings.Settings;
import java.util.Arrays;
@ -45,11 +45,8 @@ public class Dogecoin extends Bitcoiny {
return Arrays.asList(
// Servers chosen on NO BASIS WHATSOEVER from various sources!
// Status verified at https://1209k.com/bitcoin-eye/ele.php?chain=doge
new Server("dogecoin.stackwallet.com", Server.ConnectionType.SSL, 50022),
new Server("electrum.qortal.link", Server.ConnectionType.SSL, 54002),
new Server("electrum1-doge.qortal.online", Server.ConnectionType.SSL, 50002),
new Server("electrum2-doge.qortal.online", Server.ConnectionType.SSL, 50002),
new Server("electrum3-doge.qortal.online", Server.ConnectionType.SSL, 30002),
new Server("electrum4-doge.qortal.online", Server.ConnectionType.SSL, 30002),
new Server("electrum1.cipig.net", Server.ConnectionType.SSL, 20060),
new Server("electrum2.cipig.net", Server.ConnectionType.SSL, 20060),
new Server("electrum3.cipig.net", Server.ConnectionType.SSL, 20060)
@ -63,8 +60,7 @@ public class Dogecoin extends Bitcoiny {
@Override
public long getP2shFee(Long timestamp) {
// TODO: This will need to be replaced with something better in the near future!
return MAINNET_FEE;
return this.getFeeCeiling();
}
},
TEST3 {
@ -114,6 +110,16 @@ public class Dogecoin extends Bitcoiny {
}
};
private long feeCeiling = MAINNET_FEE;
public long getFeeCeiling() {
return feeCeiling;
}
public void setFeeCeiling(long feeCeiling) {
this.feeCeiling = feeCeiling;
}
public abstract NetworkParameters getParams();
public abstract Collection<Server> getServers();
public abstract String getGenesisHash();
@ -127,7 +133,7 @@ public class Dogecoin extends Bitcoiny {
// Constructors and instance
private Dogecoin(DogecoinNet dogecoinNet, BitcoinyBlockchainProvider blockchain, Context bitcoinjContext, String currencyCode) {
super(blockchain, bitcoinjContext, currencyCode);
super(blockchain, bitcoinjContext, currencyCode, DEFAULT_FEE_PER_KB);
this.dogecoinNet = dogecoinNet;
LOGGER.info(() -> String.format("Starting Dogecoin support using %s", this.dogecoinNet.name()));
@ -156,11 +162,6 @@ public class Dogecoin extends Bitcoiny {
// Actual useful methods for use by other classes
@Override
public Coin getFeePerKb() {
return DEFAULT_FEE_PER_KB;
}
@Override
public long getMinimumOrderAmount() {
return MINIMUM_ORDER_AMOUNT;
@ -177,4 +178,14 @@ public class Dogecoin extends Bitcoiny {
return this.dogecoinNet.getP2shFee(timestamp);
}
@Override
public long getFeeCeiling() {
return this.dogecoinNet.getFeeCeiling();
}
@Override
public void setFeeCeiling(long fee) {
this.dogecoinNet.setFeeCeiling( fee );
}
}

View File

@ -43,12 +43,11 @@ public class ElectrumX extends BitcoinyBlockchainProvider {
private static final String VERBOSE_TRANSACTIONS_UNSUPPORTED_MESSAGE = "verbose transactions are currently unsupported";
private static final int RESPONSE_TIME_READINGS = 5;
private static final long MAX_AVG_RESPONSE_TIME = 1000L; // ms
private static final long MAX_AVG_RESPONSE_TIME = 2000L; // ms
public static class Server {
public static class Server implements ChainableServer {
String hostname;
public enum ConnectionType { TCP, SSL }
ConnectionType connectionType;
int port;
@ -60,6 +59,7 @@ public class ElectrumX extends BitcoinyBlockchainProvider {
this.port = port;
}
@Override
public void addResponseTime(long responseTime) {
while (this.responseTimes.size() > RESPONSE_TIME_READINGS) {
this.responseTimes.remove(0);
@ -67,6 +67,7 @@ public class ElectrumX extends BitcoinyBlockchainProvider {
this.responseTimes.add(responseTime);
}
@Override
public long averageResponseTime() {
if (this.responseTimes.size() < RESPONSE_TIME_READINGS) {
// Not enough readings yet
@ -79,6 +80,21 @@ public class ElectrumX extends BitcoinyBlockchainProvider {
return 0L;
}
@Override
public String getHostName() {
return this.hostname;
}
@Override
public int getPort() {
return this.port;
}
@Override
public ConnectionType getConnectionType() {
return this.connectionType;
}
@Override
public boolean equals(Object other) {
if (other == this)
@ -104,9 +120,9 @@ public class ElectrumX extends BitcoinyBlockchainProvider {
return String.format("%s:%s:%d", this.connectionType.name(), this.hostname, this.port);
}
}
private Set<Server> servers = new HashSet<>();
private List<Server> remainingServers = new ArrayList<>();
private Set<Server> uselessServers = Collections.synchronizedSet(new HashSet<>());
private Set<ChainableServer> servers = new HashSet<>();
private List<ChainableServer> remainingServers = new ArrayList<>();
private Set<ChainableServer> uselessServers = Collections.synchronizedSet(new HashSet<>());
private final String netId;
private final String expectedGenesisHash;
@ -114,7 +130,7 @@ public class ElectrumX extends BitcoinyBlockchainProvider {
private Bitcoiny blockchain;
private final Object serverLock = new Object();
private Server currentServer;
private ChainableServer currentServer;
private Socket socket;
private Scanner scanner;
private int nextId = 1;
@ -638,7 +654,7 @@ public class ElectrumX extends BitcoinyBlockchainProvider {
if (!this.remainingServers.isEmpty()) {
long averageResponseTime = this.currentServer.averageResponseTime();
if (averageResponseTime > MAX_AVG_RESPONSE_TIME) {
LOGGER.info("Slow average response time {}ms from {} - trying another server...", averageResponseTime, this.currentServer.hostname);
LOGGER.info("Slow average response time {}ms from {} - trying another server...", averageResponseTime, this.currentServer.getHostName());
this.closeServer();
break;
}
@ -663,20 +679,20 @@ public class ElectrumX extends BitcoinyBlockchainProvider {
return true;
while (!this.remainingServers.isEmpty()) {
Server server = this.remainingServers.remove(RANDOM.nextInt(this.remainingServers.size()));
ChainableServer server = this.remainingServers.remove(RANDOM.nextInt(this.remainingServers.size()));
LOGGER.trace(() -> String.format("Connecting to %s", server));
try {
SocketAddress endpoint = new InetSocketAddress(server.hostname, server.port);
SocketAddress endpoint = new InetSocketAddress(server.getHostName(), server.getPort());
int timeout = 5000; // ms
this.socket = new Socket();
this.socket.connect(endpoint, timeout);
this.socket.setTcpNoDelay(true);
if (server.connectionType == Server.ConnectionType.SSL) {
if (server.getConnectionType() == Server.ConnectionType.SSL) {
SSLSocketFactory factory = TrustlessSSLSocketFactory.getSocketFactory();
this.socket = factory.createSocket(this.socket, server.hostname, server.port, true);
this.socket = factory.createSocket(this.socket, server.getHostName(), server.getPort(), true);
}
this.scanner = new Scanner(this.socket.getInputStream());
@ -832,7 +848,7 @@ public class ElectrumX extends BitcoinyBlockchainProvider {
* Closes connection to <tt>server</tt> if it is currently connected server.
* @param server
*/
private void closeServer(Server server) {
private void closeServer(ChainableServer server) {
synchronized (this.serverLock) {
if (this.currentServer == null || !this.currentServer.equals(server))
return;
@ -857,4 +873,24 @@ public class ElectrumX extends BitcoinyBlockchainProvider {
}
}
@Override
public Set<ChainableServer> getServers() {
LOGGER.info("getting servers");
return servers;
}
@Override
public List<ChainableServer> getRemainingServers() {
return remainingServers;
}
@Override
public Set<ChainableServer> getUselessServers() {
return uselessServers;
}
@Override
public ChainableServer getCurrentServer() {
return currentServer;
}
}

View File

@ -7,7 +7,7 @@ import org.libdohj.params.LitecoinMainNetParams;
import org.libdohj.params.LitecoinRegTestParams;
import org.libdohj.params.LitecoinTestNet3Params;
import org.qortal.crosschain.ElectrumX.Server;
import org.qortal.crosschain.ElectrumX.Server.ConnectionType;
import org.qortal.crosschain.ChainableServer.ConnectionType;
import org.qortal.settings.Settings;
import java.util.Arrays;
@ -45,13 +45,9 @@ public class Litecoin extends Bitcoiny {
return Arrays.asList(
// Servers chosen on NO BASIS WHATSOEVER from various sources!
// Status verified at https://1209k.com/bitcoin-eye/ele.php?chain=ltc
new Server("electrum.qortal.link", Server.ConnectionType.SSL, 50002),
new Server("electrum1-ltc.qortal.online", Server.ConnectionType.SSL, 50002),
new Server("electrum2-ltc.qortal.online", Server.ConnectionType.SSL, 50002),
new Server("electrum3-ltc.qortal.online", Server.ConnectionType.SSL, 20002),
new Server("electrum4-ltc.qortal.online", Server.ConnectionType.SSL, 20002),
new Server("backup.electrum-ltc.org", Server.ConnectionType.SSL, 443),
new Server("electrum.ltc.xurious.com", Server.ConnectionType.SSL, 50002),
new Server("electrum.qortal.link", Server.ConnectionType.SSL, 50002),
new Server("electrum-ltc.petrkr.net", Server.ConnectionType.SSL, 60002),
new Server("electrum1.cipig.net", Server.ConnectionType.SSL, 20063),
new Server("electrum2.cipig.net", Server.ConnectionType.SSL, 20063),
@ -67,8 +63,7 @@ public class Litecoin extends Bitcoiny {
@Override
public long getP2shFee(Long timestamp) {
// TODO: This will need to be replaced with something better in the near future!
return MAINNET_FEE;
return this.getFeeCeiling();
}
},
TEST3 {
@ -80,9 +75,7 @@ public class Litecoin extends Bitcoiny {
@Override
public Collection<ElectrumX.Server> getServers() {
return Arrays.asList(
new Server("electrum-ltc.bysh.me", Server.ConnectionType.TCP, 51001),
new Server("electrum-ltc.bysh.me", Server.ConnectionType.SSL, 51002),
new Server("electrum.ltc.xurious.com", Server.ConnectionType.TCP, 51001),
new Server("electrum.ltc.xurious.com", Server.ConnectionType.SSL, 51002)
);
}
@ -123,6 +116,16 @@ public class Litecoin extends Bitcoiny {
}
};
private long feeCeiling = MAINNET_FEE;
public long getFeeCeiling() {
return feeCeiling;
}
public void setFeeCeiling(long feeCeiling) {
this.feeCeiling = feeCeiling;
}
public abstract NetworkParameters getParams();
public abstract Collection<ElectrumX.Server> getServers();
public abstract String getGenesisHash();
@ -136,7 +139,7 @@ public class Litecoin extends Bitcoiny {
// Constructors and instance
private Litecoin(LitecoinNet litecoinNet, BitcoinyBlockchainProvider blockchain, Context bitcoinjContext, String currencyCode) {
super(blockchain, bitcoinjContext, currencyCode);
super(blockchain, bitcoinjContext, currencyCode, DEFAULT_FEE_PER_KB);
this.litecoinNet = litecoinNet;
LOGGER.info(() -> String.format("Starting Litecoin support using %s", this.litecoinNet.name()));
@ -165,12 +168,6 @@ public class Litecoin extends Bitcoiny {
// Actual useful methods for use by other classes
/** Default Litecoin fee is lower than Bitcoin: only 10sats/byte. */
@Override
public Coin getFeePerKb() {
return DEFAULT_FEE_PER_KB;
}
@Override
public long getMinimumOrderAmount() {
return MINIMUM_ORDER_AMOUNT;
@ -187,4 +184,14 @@ public class Litecoin extends Bitcoiny {
return this.litecoinNet.getP2shFee(timestamp);
}
@Override
public long getFeeCeiling() {
return this.litecoinNet.getFeeCeiling();
}
@Override
public void setFeeCeiling(long fee) {
this.litecoinNet.setFeeCeiling( fee );
}
}

View File

@ -13,7 +13,7 @@ import org.libdohj.params.PirateChainMainNetParams;
import org.qortal.api.model.crosschain.PirateChainSendRequest;
import org.qortal.controller.PirateChainWalletController;
import org.qortal.crosschain.PirateLightClient.Server;
import org.qortal.crosschain.PirateLightClient.Server.ConnectionType;
import org.qortal.crosschain.ChainableServer.ConnectionType;
import org.qortal.crypto.Crypto;
import org.qortal.settings.Settings;
import org.qortal.transform.TransformationException;
@ -51,12 +51,12 @@ public class PirateChain extends Bitcoiny {
public Collection<Server> getServers() {
return Arrays.asList(
// Servers chosen on NO BASIS WHATSOEVER from various sources!
new Server("lightd.pirate.black", Server.ConnectionType.SSL, 443),
new Server("wallet-arrr1.qortal.online", Server.ConnectionType.SSL, 443),
new Server("wallet-arrr2.qortal.online", Server.ConnectionType.SSL, 443),
new Server("wallet-arrr3.qortal.online", Server.ConnectionType.SSL, 443),
new Server("wallet-arrr4.qortal.online", Server.ConnectionType.SSL, 443),
new Server("wallet-arrr5.qortal.online", Server.ConnectionType.SSL, 443),
new Server("lightd.pirate.black", Server.ConnectionType.SSL, 443)
new Server("wallet-arrr5.qortal.online", Server.ConnectionType.SSL, 443)
);
}
@ -67,8 +67,7 @@ public class PirateChain extends Bitcoiny {
@Override
public long getP2shFee(Long timestamp) {
// TODO: This will need to be replaced with something better in the near future!
return MAINNET_FEE;
return this.getFeeCeiling();
}
},
TEST3 {
@ -118,6 +117,16 @@ public class PirateChain extends Bitcoiny {
}
};
private long feeCeiling = MAINNET_FEE;
public long getFeeCeiling() {
return feeCeiling;
}
public void setFeeCeiling(long feeCeiling) {
this.feeCeiling = feeCeiling;
}
public abstract NetworkParameters getParams();
public abstract Collection<Server> getServers();
public abstract String getGenesisHash();
@ -131,7 +140,7 @@ public class PirateChain extends Bitcoiny {
// Constructors and instance
private PirateChain(PirateChainNet pirateChainNet, BitcoinyBlockchainProvider blockchain, Context bitcoinjContext, String currencyCode) {
super(blockchain, bitcoinjContext, currencyCode);
super(blockchain, bitcoinjContext, currencyCode, DEFAULT_FEE_PER_KB);
this.pirateChainNet = pirateChainNet;
LOGGER.info(() -> String.format("Starting Pirate Chain support using %s", this.pirateChainNet.name()));
@ -160,12 +169,6 @@ public class PirateChain extends Bitcoiny {
// Actual useful methods for use by other classes
/** Default Litecoin fee is lower than Bitcoin: only 10sats/byte. */
@Override
public Coin getFeePerKb() {
return DEFAULT_FEE_PER_KB;
}
@Override
public long getMinimumOrderAmount() {
return MINIMUM_ORDER_AMOUNT;
@ -182,6 +185,16 @@ public class PirateChain extends Bitcoiny {
return this.pirateChainNet.getP2shFee(timestamp);
}
@Override
public long getFeeCeiling() {
return this.pirateChainNet.getFeeCeiling();
}
@Override
public void setFeeCeiling(long fee) {
this.pirateChainNet.setFeeCeiling( fee );
}
/**
* Returns confirmed balance, based on passed payment script.
* <p>

View File

@ -30,10 +30,9 @@ public class PirateLightClient extends BitcoinyBlockchainProvider {
private static final int RESPONSE_TIME_READINGS = 5;
private static final long MAX_AVG_RESPONSE_TIME = 500L; // ms
public static class Server {
public static class Server implements ChainableServer{
String hostname;
public enum ConnectionType { TCP, SSL }
ConnectionType connectionType;
int port;
@ -64,6 +63,21 @@ public class PirateLightClient extends BitcoinyBlockchainProvider {
return 0L;
}
@Override
public String getHostName() {
return this.hostname;
}
@Override
public int getPort() {
return this.port;
}
@Override
public ChainableServer.ConnectionType getConnectionType() {
return this.connectionType;
}
@Override
public boolean equals(Object other) {
if (other == this)
@ -89,9 +103,9 @@ public class PirateLightClient extends BitcoinyBlockchainProvider {
return String.format("%s:%s:%d", this.connectionType.name(), this.hostname, this.port);
}
}
private Set<Server> servers = new HashSet<>();
private List<Server> remainingServers = new ArrayList<>();
private Set<Server> uselessServers = Collections.synchronizedSet(new HashSet<>());
private Set<ChainableServer> servers = new HashSet<>();
private List<ChainableServer> remainingServers = new ArrayList<>();
private Set<ChainableServer> uselessServers = Collections.synchronizedSet(new HashSet<>());
private final String netId;
private final String expectedGenesisHash;
@ -99,7 +113,7 @@ public class PirateLightClient extends BitcoinyBlockchainProvider {
private Bitcoiny blockchain;
private final Object serverLock = new Object();
private Server currentServer;
private ChainableServer currentServer;
private ManagedChannel channel;
private int nextId = 1;
@ -525,6 +539,24 @@ public class PirateLightClient extends BitcoinyBlockchainProvider {
throw new ForeignBlockchainException.NetworkException(String.format("Unexpected error code from Pirate Chain broadcastTransaction gRPC: %d", sendResponse.getErrorCode()));
}
@Override
public Set<ChainableServer> getServers() {
return this.servers;
}
@Override
public List<ChainableServer> getRemainingServers() {
return this.remainingServers;
}
@Override
public Set<ChainableServer> getUselessServers() {
return this.uselessServers;
}
@Override
public ChainableServer getCurrentServer() { return this.currentServer; }
// Class-private utility methods
@ -544,7 +576,7 @@ public class PirateLightClient extends BitcoinyBlockchainProvider {
if (!this.remainingServers.isEmpty()) {
long averageResponseTime = this.currentServer.averageResponseTime();
if (averageResponseTime > MAX_AVG_RESPONSE_TIME) {
LOGGER.info("Slow average response time {}ms from {} - trying another server...", averageResponseTime, this.currentServer.hostname);
LOGGER.info("Slow average response time {}ms from {} - trying another server...", averageResponseTime, this.currentServer.getHostName());
this.closeServer();
continue;
}
@ -568,11 +600,11 @@ public class PirateLightClient extends BitcoinyBlockchainProvider {
return true;
while (!this.remainingServers.isEmpty()) {
Server server = this.remainingServers.remove(RANDOM.nextInt(this.remainingServers.size()));
ChainableServer server = this.remainingServers.remove(RANDOM.nextInt(this.remainingServers.size()));
LOGGER.trace(() -> String.format("Connecting to %s", server));
try {
this.channel = ManagedChannelBuilder.forAddress(server.hostname, server.port).build();
this.channel = ManagedChannelBuilder.forAddress(server.getHostName(), server.getPort()).build();
CompactTxStreamerGrpc.CompactTxStreamerBlockingStub stub = CompactTxStreamerGrpc.newBlockingStub(this.channel);
LightdInfo lightdInfo = stub.getLightdInfo(Empty.newBuilder().build());
@ -604,7 +636,7 @@ public class PirateLightClient extends BitcoinyBlockchainProvider {
* Closes connection to <tt>server</tt> if it is currently connected server.
* @param server
*/
private void closeServer(Server server) {
private void closeServer(ChainableServer server) {
synchronized (this.serverLock) {
if (this.currentServer == null || !this.currentServer.equals(server) || this.channel == null) {
return;

View File

@ -7,7 +7,7 @@ import org.bitcoinj.params.RegTestParams;
import org.bitcoinj.params.TestNet3Params;
import org.libdohj.params.RavencoinMainNetParams;
import org.qortal.crosschain.ElectrumX.Server;
import org.qortal.crosschain.ElectrumX.Server.ConnectionType;
import org.qortal.crosschain.ChainableServer.ConnectionType;
import org.qortal.settings.Settings;
import java.util.Arrays;
@ -46,10 +46,6 @@ public class Ravencoin extends Bitcoiny {
// Servers chosen on NO BASIS WHATSOEVER from various sources!
// Status verified at https://1209k.com/bitcoin-eye/ele.php?chain=rvn
new Server("electrum.qortal.link", Server.ConnectionType.SSL, 56002),
new Server("electrum1-rvn.qortal.online", Server.ConnectionType.SSL, 50002),
new Server("electrum2-rvn.qortal.online", Server.ConnectionType.SSL, 50002),
new Server("electrum3-rvn.qortal.online", Server.ConnectionType.SSL, 50002),
new Server("electrum4-rvn.qortal.online", Server.ConnectionType.SSL, 50002),
new Server("electrum1.cipig.net", Server.ConnectionType.SSL, 20051),
new Server("electrum2.cipig.net", Server.ConnectionType.SSL, 20051),
new Server("electrum3.cipig.net", Server.ConnectionType.SSL, 20051),
@ -65,8 +61,7 @@ public class Ravencoin extends Bitcoiny {
@Override
public long getP2shFee(Long timestamp) {
// TODO: This will need to be replaced with something better in the near future!
return MAINNET_FEE;
return this.getFeeCeiling();
}
},
TEST3 {
@ -116,6 +111,16 @@ public class Ravencoin extends Bitcoiny {
}
};
private long feeCeiling = MAINNET_FEE;
public long getFeeCeiling() {
return feeCeiling;
}
public void setFeeCeiling(long feeCeiling) {
this.feeCeiling = feeCeiling;
}
public abstract NetworkParameters getParams();
public abstract Collection<Server> getServers();
public abstract String getGenesisHash();
@ -129,7 +134,7 @@ public class Ravencoin extends Bitcoiny {
// Constructors and instance
private Ravencoin(RavencoinNet ravencoinNet, BitcoinyBlockchainProvider blockchain, Context bitcoinjContext, String currencyCode) {
super(blockchain, bitcoinjContext, currencyCode);
super(blockchain, bitcoinjContext, currencyCode, DEFAULT_FEE_PER_KB);
this.ravencoinNet = ravencoinNet;
LOGGER.info(() -> String.format("Starting Ravencoin support using %s", this.ravencoinNet.name()));
@ -158,11 +163,6 @@ public class Ravencoin extends Bitcoiny {
// Actual useful methods for use by other classes
@Override
public Coin getFeePerKb() {
return DEFAULT_FEE_PER_KB;
}
@Override
public long getMinimumOrderAmount() {
return MINIMUM_ORDER_AMOUNT;
@ -179,4 +179,14 @@ public class Ravencoin extends Bitcoiny {
return this.ravencoinNet.getP2shFee(timestamp);
}
@Override
public long getFeeCeiling() {
return this.ravencoinNet.getFeeCeiling();
}
@Override
public void setFeeCeiling(long fee) {
this.ravencoinNet.setFeeCeiling( fee );
}
}

View File

@ -0,0 +1,60 @@
package org.qortal.crosschain;
import javax.xml.bind.annotation.XmlAccessType;
import javax.xml.bind.annotation.XmlAccessorType;
import java.util.List;
import java.util.Objects;
@XmlAccessorType(XmlAccessType.FIELD)
public class ServerConfigurationInfo {
private List<ServerInfo> servers;
private List<ServerInfo> remainingServers;
private List<ServerInfo> uselessServers;
public ServerConfigurationInfo() {
}
public ServerConfigurationInfo(
List<ServerInfo> servers,
List<ServerInfo> remainingServers,
List<ServerInfo> uselessServers) {
this.servers = servers;
this.remainingServers = remainingServers;
this.uselessServers = uselessServers;
}
public List<ServerInfo> getServers() {
return servers;
}
public List<ServerInfo> getRemainingServers() {
return remainingServers;
}
public List<ServerInfo> getUselessServers() {
return uselessServers;
}
@Override
public boolean equals(Object o) {
if (this == o) return true;
if (o == null || getClass() != o.getClass()) return false;
ServerConfigurationInfo that = (ServerConfigurationInfo) o;
return Objects.equals(servers, that.servers) && Objects.equals(remainingServers, that.remainingServers) && Objects.equals(uselessServers, that.uselessServers);
}
@Override
public int hashCode() {
return Objects.hash(servers, remainingServers, uselessServers);
}
@Override
public String toString() {
return "ServerConfigurationInfo{" +
"servers=" + servers +
", remainingServers=" + remainingServers +
", uselessServers=" + uselessServers +
'}';
}
}

View File

@ -0,0 +1,74 @@
package org.qortal.crosschain;
import javax.xml.bind.annotation.XmlAccessType;
import javax.xml.bind.annotation.XmlAccessorType;
import java.util.Objects;
@XmlAccessorType(XmlAccessType.FIELD)
public class ServerInfo {
private long averageResponseTime;
private String hostName;
private int port;
private String connectionType;
private boolean isCurrent;
public ServerInfo() {
}
public ServerInfo(long averageResponseTime, String hostName, int port, String connectionType, boolean isCurrent) {
this.averageResponseTime = averageResponseTime;
this.hostName = hostName;
this.port = port;
this.connectionType = connectionType;
this.isCurrent = isCurrent;
}
public long getAverageResponseTime() {
return averageResponseTime;
}
public String getHostName() {
return hostName;
}
public int getPort() {
return port;
}
public String getConnectionType() {
return connectionType;
}
public boolean isCurrent() {
return isCurrent;
}
@Override
public boolean equals(Object o) {
if (this == o) return true;
if (o == null || getClass() != o.getClass()) return false;
ServerInfo that = (ServerInfo) o;
return averageResponseTime == that.averageResponseTime && port == that.port && isCurrent == that.isCurrent && Objects.equals(hostName, that.hostName) && Objects.equals(connectionType, that.connectionType);
}
@Override
public int hashCode() {
return Objects.hash(averageResponseTime, hostName, port, connectionType, isCurrent);
}
@Override
public String toString() {
return "ServerInfo{" +
"averageResponseTime=" + averageResponseTime +
", hostName='" + hostName + '\'' +
", port=" + port +
", connectionType='" + connectionType + '\'' +
", isCurrent=" + isCurrent +
'}';
}
}

View File

@ -1,33 +1,33 @@
package org.qortal.crypto;
import javax.net.ssl.SSLContext;
import javax.net.ssl.SSLSocketFactory;
import javax.net.ssl.TrustManager;
import javax.net.ssl.X509TrustManager;
import javax.net.ssl.*;
import java.security.cert.X509Certificate;
public abstract class TrustlessSSLSocketFactory {
// Create a trust manager that does not validate certificate chains
/**
* Creates a SSLSocketFactory that ignore certificate chain validation because ElectrumX servers use mostly
* self signed certificates.
*/
private static final TrustManager[] TRUSTLESS_MANAGER = new TrustManager[] {
new X509TrustManager() {
public java.security.cert.X509Certificate[] getAcceptedIssuers() {
return new X509Certificate[0];
public X509Certificate[] getAcceptedIssuers() {
return null;
}
public void checkClientTrusted(java.security.cert.X509Certificate[] certs, String authType) {
public void checkClientTrusted(X509Certificate[] certs, String authType) {
}
public void checkServerTrusted(java.security.cert.X509Certificate[] certs, String authType) {
public void checkServerTrusted(X509Certificate[] certs, String authType) {
}
}
};
// Install the all-trusting trust manager
/**
* Install the all-trusting trust manager.
*/
private static final SSLContext sc;
static {
try {
sc = SSLContext.getInstance("TLSv1.3");
sc = SSLContext.getInstance("SSL");
sc.init(null, TRUSTLESS_MANAGER, new java.security.SecureRandom());
} catch (Exception e) {
throw new RuntimeException(e);
@ -37,5 +37,4 @@ public abstract class TrustlessSSLSocketFactory {
public static SSLSocketFactory getSocketFactory() {
return sc.getSocketFactory();
}
}

View File

@ -0,0 +1,32 @@
package org.qortal.data.crosschain;
import org.qortal.crosschain.BitcoinyTransaction;
import org.qortal.crosschain.TransactionHash;
import java.util.List;
import java.util.Map;
public class AtomicTransactionData {
public final TransactionHash hash;
public final Integer timestamp;
public final List<BitcoinyTransaction.Input> inputs;
public final Map<List<String>, Long> valueByAddress;
public final long totalAmount;
public final int size;
public AtomicTransactionData(
TransactionHash hash,
Integer timestamp,
List<BitcoinyTransaction.Input> inputs,
Map<List<String>, Long> valueByAddress,
long totalAmount,
int size) {
this.hash = hash;
this.timestamp = timestamp;
this.inputs = inputs;
this.valueByAddress = valueByAddress;
this.totalAmount = totalAmount;
this.size = size;
}
}

View File

@ -0,0 +1,106 @@
package org.qortal.data.crosschain;
import javax.xml.bind.annotation.XmlAccessType;
import javax.xml.bind.annotation.XmlAccessorType;
@XmlAccessorType(XmlAccessType.FIELD)
public class TransactionSummary {
private String atAddress;
private String p2shValue;
private String p2shAddress;
private String lockingHash;
private Integer lockingTimestamp;
private long lockingTotalAmount;
private long lockingFee;
private int lockingSize;
private String unlockingHash;
private Integer unlockingTimestamp;
private long unlockingTotalAmount;
private long unlockingFee;
private int unlockingSize;
public TransactionSummary(){}
public TransactionSummary(
String atAddress,
String p2shValue,
String p2shAddress,
String lockingHash,
Integer lockingTimestamp,
long lockingTotalAmount,
long lockingFee,
int lockingSize,
String unlockingHash,
Integer unlockingTimestamp,
long unlockingTotalAmount,
long unlockingFee,
int unlockingSize) {
this.atAddress = atAddress;
this.p2shValue = p2shValue;
this.p2shAddress = p2shAddress;
this.lockingHash = lockingHash;
this.lockingTimestamp = lockingTimestamp;
this.lockingTotalAmount = lockingTotalAmount;
this.lockingFee = lockingFee;
this.lockingSize = lockingSize;
this.unlockingHash = unlockingHash;
this.unlockingTimestamp = unlockingTimestamp;
this.unlockingTotalAmount = unlockingTotalAmount;
this.unlockingFee = unlockingFee;
this.unlockingSize = unlockingSize;
}
public String getAtAddress() {
return atAddress;
}
public String getP2shValue() {
return p2shValue;
}
public String getP2shAddress() {
return p2shAddress;
}
public String getLockingHash() {
return lockingHash;
}
public Integer getLockingTimestamp() {
return lockingTimestamp;
}
public long getLockingTotalAmount() {
return lockingTotalAmount;
}
public long getLockingFee() {
return lockingFee;
}
public int getLockingSize() {
return lockingSize;
}
public String getUnlockingHash() {
return unlockingHash;
}
public Integer getUnlockingTimestamp() {
return unlockingTimestamp;
}
public long getUnlockingTotalAmount() {
return unlockingTotalAmount;
}
public long getUnlockingFee() {
return unlockingFee;
}
public int getUnlockingSize() {
return unlockingSize;
}
}

View File

@ -5,7 +5,6 @@ import org.apache.logging.log4j.Logger;
import org.qortal.controller.Controller;
import org.qortal.globalization.Translator;
import org.qortal.settings.Settings;
import org.qortal.utils.RandomizeList;
import org.qortal.utils.URLViewer;
import javax.swing.*;
@ -140,14 +139,6 @@ public class SysTray {
}
});
/* JMenuItem openUi = new JMenuItem(Translator.INSTANCE.translate("SysTray", "OPEN_UI"));
openUi.addActionListener(actionEvent -> {
destroyHiddenDialog();
new OpenUiWorker().execute();
});
menu.add(openUi); */
JMenuItem openTimeCheck = new JMenuItem(Translator.INSTANCE.translate("SysTray", "CHECK_TIME_ACCURACY"));
openTimeCheck.addActionListener(actionEvent -> {
destroyHiddenDialog();
@ -190,48 +181,6 @@ public class SysTray {
return menu;
}
static class OpenUiWorker extends SwingWorker<Void, Void> {
@Override
protected Void doInBackground() {
List<String> uiServers = new ArrayList<>();
String[] remoteUiServers = Settings.getInstance().getRemoteUiServers();
uiServers.addAll(Arrays.asList(remoteUiServers));
// Randomize remote servers
uiServers = RandomizeList.randomize(uiServers);
// Prepend local servers
String[] localUiServers = Settings.getInstance().getLocalUiServers();
uiServers.addAll(0, Arrays.asList(localUiServers));
// Check each server in turn before opening browser tab
int uiPort = Settings.getInstance().getUiServerPort();
for (String uiServer : uiServers) {
InetSocketAddress socketAddress = new InetSocketAddress(uiServer, uiPort);
// If we couldn't resolve try next
if (socketAddress.isUnresolved())
continue;
try (SocketChannel socketChannel = SocketChannel.open()) {
socketChannel.socket().connect(socketAddress, 100);
// If we reach here, then socket connected to UI server!
URLViewer.openWebpage(new URL(String.format("http://%s:%d", uiServer, uiPort)));
return null;
} catch (IOException e) {
// try next server
} catch (Exception e) {
LOGGER.error("Unable to open UI website in browser");
return null;
}
}
return null;
}
}
static class SynchronizeClockWorker extends SwingWorker<Void, Void> {
@Override
protected Void doInBackground() {

View File

@ -0,0 +1,425 @@
package org.qortal.network;
import java.io.IOException;
//import java.nio.channels.SelectionKey;
//import java.io.Paths;
import java.nio.file.Files;
import java.nio.file.Path;
import java.nio.file.StandardCopyOption;
import java.io.File;
import java.util.*;
//import java.util.function.BiConsumer;
//import java.util.function.Consumer;
//import java.util.function.Function;
//import java.util.concurrent.*;
//import java.util.concurrent.atomic.AtomicLong;
//import org.qortal.data.network.PeerData;
import org.qortal.repository.DataException;
//import org.qortal.settings.Settings;
import org.qortal.settings.Settings;
//import org.qortal.utils.NTP;
//import com.fasterxml.jackson.annotation.JsonGetter;
import org.apache.commons.codec.binary.Hex;
//import org.slf4j.Logger;
//import org.slf4j.LoggerFactory;
import io.reticulum.Reticulum;
import io.reticulum.Transport;
import io.reticulum.destination.Destination;
import io.reticulum.destination.DestinationType;
import io.reticulum.destination.Direction;
import io.reticulum.identity.Identity;
import io.reticulum.interfaces.ConnectionInterface;
import io.reticulum.destination.ProofStrategy;
import io.reticulum.transport.AnnounceHandler;
import static io.reticulum.constant.ReticulumConstant.CONFIG_FILE_NAME;
//import static io.reticulum.identity.IdentityKnownDestination.recall;
//import static io.reticulum.identity.IdentityKnownDestination.recallAppData;
//import static io.reticulum.destination.Direction.OUT;
import lombok.extern.slf4j.Slf4j;
import lombok.Synchronized;
import io.reticulum.link.Link;
import io.reticulum.link.LinkStatus;
//import io.reticulum.packet.PacketReceipt;
import io.reticulum.packet.Packet;
//import static io.reticulum.link.LinkStatus.ACTIVE;
import static io.reticulum.link.LinkStatus.CLOSED;
import static io.reticulum.link.LinkStatus.PENDING;
import static io.reticulum.link.LinkStatus.STALE;
import static java.nio.charset.StandardCharsets.UTF_8;
//import static java.util.Objects.isNull;
import static java.util.Objects.nonNull;
//import org.qortal.network.Network.NetworkProcessor;
//import org.qortal.utils.ExecuteProduceConsume;
//import org.qortal.utils.NamedThreadFactory;
//import java.time.Instant;
//import org.qortal.network.RNSPeer;
@Slf4j
public class RNSNetwork {
static final String APP_NAME = "qortal";
private Reticulum reticulum;
private Identity server_identity;
private Destination baseDestination; // service base (initially: anything node2node)
//private Destination dataDestination; // qdn services (eg. files like music, videos etc)
//private Destination liveDestination; // live/dynamic peer list (eg. video conferencing)
// the following should be retrieved from settings
private static Integer MAX_PEERS = 3;
private static Integer MIN_DESIRED_PEERS = 3;
//private final Integer MAX_PEERS = Settings.getInstance().getMaxReticulumPeers();
//private final Integer MIN_DESIRED_PEERS = Settings.getInstance().getMinDesiredReticulumPeers();
static final String defaultConfigPath = new String(".reticulum"); // if empty will look in Reticulums default paths
//private final String defaultConfigPath = Settings.getInstance().getDefaultConfigPathForReticulum();
//private static final Logger logger = LoggerFactory.getLogger(RNSNetwork.class);
//private final List<Link> linkedPeers = Collections.synchronizedList(new ArrayList<>());
//private List<Link> immutableLinkedPeers = Collections.emptyList();
private final List<RNSPeer> linkedPeers = Collections.synchronizedList(new ArrayList<>());
//private final ExecuteProduceConsume rnsNetworkEPC;
private static final long NETWORK_EPC_KEEPALIVE = 1000L; // 1 second
private volatile boolean isShuttingDown = false;
private int totalThreadCount = 0;
// TODO: settings - MaxReticulumPeers, MaxRNSNetworkThreadPoolSize (if needed)
// Constructor
private RNSNetwork () {
try {
initConfig(defaultConfigPath);
reticulum = new Reticulum(defaultConfigPath);
log.info("reticulum instance created: {}", reticulum.toString());
} catch (IOException e) {
log.error("unable to create Reticulum network", e);
}
// Settings.getInstance().getMaxRNSNetworkThreadPoolSize(), // statically set to 5 below
//ExecutorService RNSNetworkExecutor = new ThreadPoolExecutor(1,
// 5,
// NETWORK_EPC_KEEPALIVE, TimeUnit.SECONDS,
// new SynchronousQueue<Runnable>(),
// new NamedThreadFactory("RNSNetwork-EPC"));
//rnsNetworkEPC = new RNSNetworkProcessor(RNSNetworkExecutor);
}
// Note: potentially create persistent server_identity (utility rnid) and load it from file
public void start() throws IOException, DataException {
// create identity either from file or new (creating new keys)
var serverIdentityPath = reticulum.getStoragePath().resolve(APP_NAME);
if (Files.isReadable(serverIdentityPath)) {
server_identity = Identity.fromFile(serverIdentityPath);
log.info("server identity loaded from file {}", serverIdentityPath.toString());
} else {
server_identity = new Identity();
log.info("new server identity created dynamically.");
}
log.debug("Server Identity: {}", server_identity.toString());
// show the ifac_size of the configured interfaces (debug code)
for (ConnectionInterface i: Transport.getInstance().getInterfaces() ) {
log.info("interface {}, length: {}", i.getInterfaceName(), i.getIfacSize());
}
baseDestination = new Destination(
server_identity,
Direction.IN,
DestinationType.SINGLE,
APP_NAME,
"core"
);
//// ideas for other entry points
//dataDestination = new Destination(
// server_identity,
// Direction.IN,
// DestinationType.SINGLE,
// APP_NAME,
// "core",
// "qdn"
//);
//liveDestination = new Destination(
// server_identity,
// Direction.IN,
// DestinationType.SINGLE,
// APP_NAME,
// "core",
// "live"
//);
log.info("Destination "+Hex.encodeHexString(baseDestination.getHash())+" "+baseDestination.getName()+" running.");
//log.info("Destination "+Hex.encodeHexString(dataDestination.getHash())+" "+dataDestination.getName()+" running.");
baseDestination.setProofStrategy(ProofStrategy.PROVE_ALL);
//dataDestination.setProofStrategy(ProofStrategy.PROVE_ALL);
baseDestination.setAcceptLinkRequests(true);
//dataDestination.setAcceptLinkRequests(true);
//baseDestination.setLinkEstablishedCallback(this::linkExtabishedCallback);
baseDestination.setPacketCallback(this::packetCallback);
//baseDestination.setPacketCallback((message, packet) -> {
// log.info("xyz - Message raw {}", message);
// log.info("xyz - Packet {}", packet.toString());
//});
Transport.getInstance().registerAnnounceHandler(new QAnnounceHandler());
log.info("announceHandlers: {}", Transport.getInstance().getAnnounceHandlers());
baseDestination.announce();
//dataDestination.announce();
log.info("Sent initial announce from {} ({})", Hex.encodeHexString(baseDestination.getHash()), baseDestination.getName());
// Start up first networking thread (the "server loop")
//rnsNetworkEPC.start();
}
public void shutdown() {
isShuttingDown = true;
log.info("shutting down Reticulum");
// Stop processing threads (the "server loop")
//try {
// if (!this.rnsNetworkEPC.shutdown(5000)) {
// logger.warn("Network threads failed to terminate");
// }
//} catch (InterruptedException e) {
// logger.warn("Interrupted while waiting for networking threads to terminate");
//}
// Disconnect peers and terminate Reticulum
for (RNSPeer p : linkedPeers) {
if (nonNull(p.getLink())) {
p.getLink().teardown();
}
}
reticulum.exitHandler();
}
private void initConfig(String configDir) throws IOException {
File configDir1 = new File(defaultConfigPath);
if (!configDir1.exists()) {
configDir1.mkdir();
}
var configPath = Path.of(configDir1.getAbsolutePath());
Path configFile = configPath.resolve(CONFIG_FILE_NAME);
if (Files.notExists(configFile)) {
var defaultConfig = this.getClass().getClassLoader().getResourceAsStream("reticulum_default_config.yml");
Files.copy(defaultConfig, configFile, StandardCopyOption.REPLACE_EXISTING);
}
}
private void packetCallback(byte[] message, Packet packet) {
log.info("xyz - Message raw {}", message);
log.info("xyz - Packet {}", packet.toString());
}
//public void announceBaseDestination () {
// getBaseDestination().announce();
//}
//public Consumer<Link> clientConnected(Link link) {
// log.info("Client connected");
// link.setLinkClosedCallback(clientDisconnected(link));
// link.setPacketCallback(null);
//}
//public void clientDisconnected(Link link) {
// log.info("Client disconnected");
// linkedPeers.remove(link);
//}
// client part
//@Slf4j
private static class QAnnounceHandler implements AnnounceHandler {
@Override
public String getAspectFilter() {
// handle all announces
return null;
}
@Override
@Synchronized
public void receivedAnnounce(byte[] destinationHash, Identity announcedIdentity, byte[] appData) {
var peerExists = false;
log.info("Received an announce from {}", Hex.encodeHexString(destinationHash));
//log.info("aspect: {}", getAspectFilter());
//log.info("destinationhash: {}, announcedIdentity: {}, appData: {}", destinationHash, announcedIdentity, appData);
if (nonNull(appData)) {
log.debug("The announce contained the following app data: {}", new String(appData, UTF_8));
}
// add to peer list if we can use more peers
//synchronized (this) {
List<RNSPeer> lps = RNSNetwork.getInstance().getLinkedPeers();
if (lps.size() < MAX_PEERS) {
for (RNSPeer p : lps) {
//log.info("peer exists: hash: {}, destinationHash: {}", p.getDestinationLink().getDestination().getHash(), destinationHash);
if (Arrays.equals(p.getDestinationLink().getDestination().getHash(), destinationHash)) {
peerExists = true;
log.debug("peer exists: hash: {}, destinationHash: {}", p.getDestinationLink().getDestination().getHash(), destinationHash);
break;
}
}
if (!peerExists) {
//log.info("announce handler - cerate new peer: **announcedIdentity**: {}, **recall**: {}", announcedIdentity, recall(destinationHash));
RNSPeer newPeer = new RNSPeer(destinationHash);
lps.add(newPeer);
log.info("added new RNSPeer, Destination - {}, Link: {}", newPeer.getDestinationHash(), newPeer.getDestinationLink());
}
}
//}
}
}
// Main thread
//class RNSNetworkProcessor extends ExecuteProduceConsume {
//
// //private final Logger logger = LoggerFactory.getLogger(RNSNetworkProcessor.class);
//
// private final AtomicLong nextConnectTaskTimestamp = new AtomicLong(0L); // ms - try first connect once NTP syncs
// private final AtomicLong nextBroadcastTimestamp = new AtomicLong(0L); // ms - try first broadcast once NTP syncs
//
// private Iterator<SelectionKey> channelIterator = null;
//
// RNSNetworkProcessor(ExecutorService executor) {
// super(executor);
// }
//
// @Override
// protected void onSpawnFailure() {
// // For debugging:
// // ExecutorDumper.dump(this.executor, 3, ExecuteProduceConsume.class);
// }
//
// @Override
// protected Task produceTask(boolean canBlock) throws InterruptedException {
// Task task;
//
// //task = maybeProducePeerMessageTask();
// //if (task != null) {
// // return task;
// //}
// //
// //final Long now = NTP.getTime();
// //
// //task = maybeProducePeerPingTask(now);
// //if (task != null) {
// // return task;
// //}
// //
// //task = maybeProduceConnectPeerTask(now);
// //if (task != null) {
// // return task;
// //}
// //
// //task = maybeProduceBroadcastTask(now);
// //if (task != null) {
// // return task;
// //}
// //
// // Only this method can block to reduce CPU spin
// //return maybeProduceChannelTask(canBlock);
//
// // TODO: flesh out the tasks handled by Reticulum
// return null;
// }
// //...TODO: implement abstract methods...
//}
// getter / setter
private static class SingletonContainer {
private static final RNSNetwork INSTANCE = new RNSNetwork();
}
public static RNSNetwork getInstance() {
return SingletonContainer.INSTANCE;
}
public List<RNSPeer> getLinkedPeers() {
synchronized(this.linkedPeers) {
//return new ArrayList<>(this.linkedPeers);
return this.linkedPeers;
}
}
public Integer getTotalPeers() {
synchronized (this) {
return linkedPeers.size();
}
}
public Destination getBaseDestination() {
return baseDestination;
}
// maintenance
//private static class AnnounceTimer {
// //public void main(String[] args) throws InterruptedException
// public void main(String[] args) throws InterruptedException
// {
// Timer timer = new Timer();
// // run timer every 10s (10000ms)
// timer.schedule(new TimerTask() {
// @Override
// public void run() {
// System.out.println("AnnounceTimer: " + new java.util.Date());
// }
// }, 0, 10000);
// }
//}
@Synchronized
public void prunePeers() throws DataException {
// run periodically (by the Controller)
//log.info("Peer list (linkedPeers): {}",this.linkedPeers.toString());
//synchronized(this) {
//List<Link> linkList = getLinkedPeers();
List<RNSPeer> peerList = this.linkedPeers;
log.info("List of RNSPeers: {}", this.linkedPeers);
//log.info("number of links (linkedPeers) before prunig: {}", this.linkedPeers.size());
Link pLink;
LinkStatus lStatus;
for (RNSPeer p: peerList) {
pLink = p.getLink();
lStatus = pLink.getStatus();
//log.debug("link status: "+lStatus.toString());
// lStatus in: PENDING, HANDSHAKE, ACTIVE, STALE, CLOSED
if (lStatus == CLOSED) {
p.resetPeer();
peerList.remove(p);
} else if (lStatus == STALE) {
pLink.teardown();
p.resetPeer();
peerList.remove(p);
} else if (lStatus == PENDING) {
log.info("prunePeers - link state still {}", lStatus);
// TODO: can we help the Link along somehow?
}
}
log.info("number of links (linkedPeers) after prunig: {}", this.linkedPeers.size());
//}
maybeAnnounce(getBaseDestination());
}
public void maybeAnnounce(Destination d) {
if (getLinkedPeers().size() < MIN_DESIRED_PEERS) {
d.announce();
}
}
}

View File

@ -0,0 +1,110 @@
package org.qortal.network;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import static java.util.Objects.isNull;
import org.qortal.network.RNSNetwork;
import io.reticulum.link.Link;
import io.reticulum.packet.Packet;
import io.reticulum.identity.Identity;
import io.reticulum.channel.Channel;
import io.reticulum.destination.Destination;
import io.reticulum.destination.DestinationType;
import io.reticulum.destination.Direction;
import static io.reticulum.identity.IdentityKnownDestination.recall;
//import static io.reticulum.identity.IdentityKnownDestination.recallAppData;
import lombok.extern.slf4j.Slf4j;
import lombok.Setter;
import lombok.Data;
import lombok.AccessLevel;
@Data
@Slf4j
public class RNSPeer {
private byte[] destinationHash;
private Link destinationLink;
private Identity destinationIdentity;
@Setter(AccessLevel.PACKAGE) private long creationTimestamp;
private Long lastAccessTimestamp;
// constructors
public RNSPeer (byte[] dhash) {
this.destinationHash = dhash;
this.destinationIdentity = recall(dhash);
Link newLink = new Link(
new Destination(
this.destinationIdentity,
Direction.OUT,
DestinationType.SINGLE,
RNSNetwork.APP_NAME,
"core"
)
);
this.destinationLink = newLink;
destinationLink.setPacketCallback(this::packetCallback);
}
public RNSPeer (Link newLink) {
this.destinationHash = newLink.getDestination().getHash();
this.destinationLink = newLink;
this.destinationIdentity = newLink.getRemoteIdentity();
setCreationTimestamp(System.currentTimeMillis());
this.lastAccessTimestamp = null;
destinationLink.setPacketCallback(this::packetCallback);
}
public RNSPeer () {
this.destinationHash = null;
this.destinationLink = null;
this.destinationIdentity = null;
setCreationTimestamp(System.currentTimeMillis());
this.lastAccessTimestamp = null;
}
// utilities (change Link type, call tasks, ...)
//...
private void packetCallback(byte[] message, Packet packet) {
log.debug("Message raw {}", message);
log.debug("Packet {}", packet.toString());
// ...
}
public Link getLink() {
if (isNull(getDestinationLink())) {
Link newLink = new Link(
new Destination(
this.destinationIdentity,
Direction.OUT,
DestinationType.SINGLE,
RNSNetwork.APP_NAME,
"core"
)
);
this.destinationLink = newLink;
return newLink;
}
return getDestinationLink();
}
public Channel getChannel() {
if (isNull(getDestinationLink())) {
log.warn("link is null.");
return null;
}
setLastAccessTimestamp(System.currentTimeMillis());
return getDestinationLink().getChannel();
}
public void resetPeer () {
this.destinationHash = null;
this.destinationLink = null;
this.destinationIdentity = null;
this.lastAccessTimestamp = null;
}
}

View File

@ -205,6 +205,15 @@ public interface TransactionRepository {
*/
public List<String> getConfirmedRewardShareCreatorsExcludingSelfShares() throws DataException;
/**
* Returns list of transfer asset transaction creators.
* This uses confirmed transactions only.
*
* @return
* @throws DataException
*/
public List<String> getConfirmedTransferAssetCreators() throws DataException;
/**
* Returns list of transactions pending approval, with optional txGgroupId filtering.
* <p>

View File

@ -1047,6 +1047,11 @@ public class HSQLDBDatabaseUpdates {
stmt.execute("CREATE INDEX ArbitraryIdentifierIndex ON ArbitraryTransactions (identifier)");
break;
case 49:
// Update blocks minted penalty
stmt.execute("UPDATE Accounts SET blocks_minted_penalty = -5000000 WHERE blocks_minted_penalty < 0");
break;
default:
// nothing to do
return false;

View File

@ -1043,6 +1043,33 @@ public class HSQLDBTransactionRepository implements TransactionRepository {
}
}
public List<String> getConfirmedTransferAssetCreators() throws DataException {
List<String> transferAssetCreators = new ArrayList<>();
String sql = "SELECT account "
+ "FROM TransferAssetTransactions "
+ "JOIN Accounts ON Accounts.public_key = TransferAssetTransactions.sender "
+ "JOIN Transactions ON Transactions.signature = TransferAssetTransactions.signature "
+ "WHERE block_height IS NOT NULL AND TransferAssetTransactions.recipient != Accounts.account "
+ "GROUP BY account "
+ "ORDER BY account";
try (ResultSet resultSet = this.repository.checkedExecute(sql)) {
if (resultSet == null)
return transferAssetCreators;
do {
String address = resultSet.getString(1);
transferAssetCreators.add(address);
} while (resultSet.next());
return transferAssetCreators;
} catch (SQLException e) {
throw new DataException("Unable to fetch transfer asset from repository", e);
}
}
@Override
public List<TransactionData> getApprovalPendingTransactions(Integer txGroupId, Integer limit, Integer offset, Boolean reverse) throws DataException {
StringBuilder sql = new StringBuilder(512);

View File

@ -62,16 +62,6 @@ public class Settings {
private String bindAddress = "::"; // Use IPv6 wildcard to listen on all local addresses
private String bindAddressFallback = "0.0.0.0"; // Some systems are unable to bind using IPv6
// UI servers
private int uiPort = 12388;
private String[] uiLocalServers = new String[] {
"localhost", "127.0.0.1"
};
private String[] uiRemoteServers = new String[] {
"node1.qortal.org", "node2.qortal.org", "node3.qortal.org", "node4.qortal.org", "node5.qortal.org",
"node6.qortal.org", "node7.qortal.org", "node8.qortal.org", "node9.qortal.org", "node10.qortal.org"
};
// API-related
private boolean apiEnabled = true;
private Integer apiPort;
@ -138,11 +128,11 @@ public class Settings {
private long repositoryCheckpointInterval = 60 * 60 * 1000L; // 1 hour (ms) default
/** Whether to show a notification when we perform repository 'checkpoint'. */
private boolean showCheckpointNotification = false;
/* How many blocks to cache locally. Defaulted to 10, which covers a typical Synchronizer request + a few spare */
private int blockCacheSize = 10;
/* How many blocks to cache locally. Defaulted to 10, which covers a typical Synchronizer request + a few spare - increased to 100 */
private int blockCacheSize = 100;
/** Maximum number of transactions for the block minter to include in a block */
private int maxTransactionsPerBlock = 50;
private int maxTransactionsPerBlock = 100;
/** How long to keep old, full, AT state data (ms). */
private long atStatesMaxLifetime = 5 * 24 * 60 * 60 * 1000L; // milliseconds
@ -164,7 +154,7 @@ public class Settings {
private boolean lite = false;
/** Whether we should prune old data to reduce database size
* This prevents the node from being able to serve older blocks */
* This prevents the node from being able to serve older blocks - No longer used */
private boolean topOnly = false;
/** The amount of recent blocks we should keep when pruning */
private int pruneBlockLimit = 6000;
@ -205,13 +195,13 @@ public class Settings {
/** Minimum number of peers to allow block minting / synchronization. */
private int minBlockchainPeers = 3;
/** Target number of outbound connections to peers we should make. */
private int minOutboundPeers = 16;
private int minOutboundPeers = 32;
/** Maximum number of peer connections we allow. */
private int maxPeers = 40;
private int maxPeers = 60;
/** Number of slots to reserve for short-lived QDN data transfers */
private int maxDataPeers = 4;
private int maxDataPeers = 5;
/** Maximum number of threads for network engine. */
private int maxNetworkThreadPoolSize = 120;
private int maxNetworkThreadPoolSize = 620;
/** Maximum number of threads for network proof-of-work compute, used during handshaking. */
private int networkPoWComputePoolSize = 2;
/** Maximum number of retry attempts if a peer fails to respond with the requested data */
@ -221,7 +211,7 @@ public class Settings {
public long recoveryModeTimeout = 9999999999999L;
/** Minimum peer version number required in order to sync with them */
private String minPeerVersion = "4.3.2";
private String minPeerVersion = "4.5.0";
/** 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 */
@ -269,7 +259,7 @@ public class Settings {
/** Repository storage path. */
private String repositoryPath = "db";
/** Repository connection pool size. Needs to be a bit bigger than maxNetworkThreadPoolSize */
private int repositoryConnectionPoolSize = 240;
private int repositoryConnectionPoolSize = 1920;
private List<String> fixedNetwork;
// Export/import
@ -282,8 +272,7 @@ public class Settings {
private String[] bootstrapHosts = new String[] {
"http://bootstrap.qortal.org",
"http://bootstrap2.qortal.org",
"http://bootstrap3.qortal.org",
"http://bootstrap.qortal.online"
"http://bootstrap3.qortal.org"
};
// Auto-update sources
@ -370,7 +359,7 @@ public class Settings {
/** Whether to allow public (decryptable) data to be stored */
private boolean publicDataEnabled = true;
/** Whether to allow private (non-decryptable) data to be stored */
private boolean privateDataEnabled = false;
private boolean privateDataEnabled = true;
/** Maximum total size of hosted data, in bytes. Unlimited if null */
private Long maxStorageCapacity = null;
@ -620,18 +609,6 @@ public class Settings {
return this.localeLang;
}
public int getUiServerPort() {
return this.uiPort;
}
public String[] getLocalUiServers() {
return this.uiLocalServers;
}
public String[] getRemoteUiServers() {
return this.uiRemoteServers;
}
public boolean isApiEnabled() {
return this.apiEnabled;
}

View File

@ -54,6 +54,10 @@ public class RegisterNameTransaction extends Transaction {
Account registrant = getRegistrant();
String name = this.registerNameTransactionData.getName();
int blockchainHeight = this.repository.getBlockRepository().getBlockchainHeight();
final int start = BlockChain.getInstance().getSelfSponsorshipAlgoV2Height() - 1180;
final int end = BlockChain.getInstance().getSelfSponsorshipAlgoV3Height();
// Check name size bounds
int nameLength = Utf8.encodedLength(name);
if (nameLength < Name.MIN_NAME_SIZE || nameLength > Name.MAX_NAME_SIZE)
@ -76,6 +80,10 @@ public class RegisterNameTransaction extends Transaction {
if (registrant.getConfirmedBalance(Asset.QORT) < this.registerNameTransactionData.getFee())
return ValidationResult.NO_BALANCE;
// Check if we are on algo runs
if (blockchainHeight >= start && blockchainHeight <= end)
return ValidationResult.TEMPORARY_DISABLED;
return ValidationResult.OK;
}

View File

@ -3,6 +3,7 @@ package org.qortal.transaction;
import org.qortal.account.Account;
import org.qortal.account.PublicKeyAccount;
import org.qortal.asset.Asset;
import org.qortal.block.Block;
import org.qortal.block.BlockChain;
import org.qortal.crypto.Crypto;
import org.qortal.data.account.RewardShareData;
@ -180,6 +181,33 @@ public class RewardShareTransaction extends Transaction {
// Nothing to do
}
@Override
public boolean isConfirmableAtHeight(int height) {
final int startV2 = BlockChain.getInstance().getSelfSponsorshipAlgoV2Height() - 15;
final int startV3 = BlockChain.getInstance().getSelfSponsorshipAlgoV3Height() - 15;
final int endV2 = BlockChain.getInstance().getSelfSponsorshipAlgoV2Height() + 10;
final int endV3 = BlockChain.getInstance().getSelfSponsorshipAlgoV3Height() + 10;
if (height >= BlockChain.getInstance().getUnconfirmableRewardSharesHeight()) {
// Not confirmable in online accounts or distribution blocks
if (Block.isOnlineAccountsBlock(height) || Block.isBatchRewardDistributionBlock(height)) {
return false;
}
}
if (height >= startV2 && height <= endV2) {
// Not confirmable on algo V2 run
return false;
}
if (height >= startV3 && height <= endV3) {
// Not confirmable on algo V3 run
return false;
}
return true;
}
@Override
public void process() throws DataException {
PublicKeyAccount mintingAccount = getMintingAccount();

View File

@ -173,6 +173,7 @@ public abstract class Transaction {
INVALID_OPTION_LENGTH(20),
DUPLICATE_OPTION(21),
POLL_ALREADY_EXISTS(22),
POLL_ALREADY_HAS_VOTES(23),
POLL_DOES_NOT_EXIST(24),
POLL_OPTION_DOES_NOT_EXIST(25),
ALREADY_VOTED_FOR_THAT_OPTION(26),
@ -246,6 +247,8 @@ public abstract class Transaction {
NAME_BLOCKED(97),
GROUP_APPROVAL_REQUIRED(98),
ACCOUNT_NOT_TRANSFERABLE(99),
TRANSFER_PRIVS_DISABLED(100),
TEMPORARY_DISABLED(101),
INVALID_BUT_OK(999),
NOT_YET_RELEASED(1000),
NOT_SUPPORTED(1001);
@ -904,6 +907,15 @@ public abstract class Transaction {
return true;
}
/**
* Returns whether transaction is confirmable in a block at a given height.
* @return
*/
public boolean isConfirmableAtHeight(int height) {
/* To be optionally overridden */
return true;
}
/**
* Returns whether transaction can be added to the blockchain.
* <p>

View File

@ -1,6 +1,7 @@
package org.qortal.transaction;
import org.qortal.account.Account;
import org.qortal.block.BlockChain;
import org.qortal.data.PaymentData;
import org.qortal.data.transaction.TransactionData;
import org.qortal.data.transaction.TransferAssetTransactionData;
@ -51,6 +52,14 @@ public class TransferAssetTransaction extends Transaction {
@Override
public ValidationResult isValid() throws DataException {
int blockchainHeight = this.repository.getBlockRepository().getBlockchainHeight();
final int start = BlockChain.getInstance().getSelfSponsorshipAlgoV2Height();
final int end = BlockChain.getInstance().getSelfSponsorshipAlgoV3Height();
// Check if we are on algo runs
if (blockchainHeight >= start && blockchainHeight <= end)
return ValidationResult.ASSET_NOT_SPENDABLE;
// Wrap asset transfer as a payment and delegate final payment checks to Payment class
return new Payment(this.repository).isValid(this.transferAssetTransactionData.getSenderPublicKey(), getPaymentData(), this.transferAssetTransactionData.getFee());
}

View File

@ -4,6 +4,7 @@ import org.apache.logging.log4j.LogManager;
import org.apache.logging.log4j.Logger;
import org.qortal.account.Account;
import org.qortal.asset.Asset;
import org.qortal.block.Block;
import org.qortal.block.BlockChain;
import org.qortal.crypto.Crypto;
import org.qortal.data.account.AccountData;
@ -72,6 +73,13 @@ public class TransferPrivsTransaction extends Transaction {
if (senderAccountData == null || senderAccountData.getBlocksMintedPenalty() != 0)
return ValidationResult.ACCOUNT_NOT_TRANSFERABLE;
// Disable Transfer Privs (start - end) from feature trigger
long transactionTimestamp = this.transferPrivsTransactionData.getTimestamp();
final long startTimestamp = BlockChain.getInstance().getDisableTransferPrivsTimestamp();
final long endTimestamp = BlockChain.getInstance().getEnableTransferPrivsTimestamp();
if (transactionTimestamp > startTimestamp && transactionTimestamp < endTimestamp)
return ValidationResult.TRANSFER_PRIVS_DISABLED;
return ValidationResult.OK;
}
@ -80,6 +88,17 @@ public class TransferPrivsTransaction extends Transaction {
// Nothing to do
}
@Override
public boolean isConfirmableAtHeight(int height) {
if (height >= BlockChain.getInstance().getUnconfirmableRewardSharesHeight()) {
// Not confirmable in online accounts or distribution blocks
if (Block.isOnlineAccountsBlock(height) || Block.isBatchRewardDistributionBlock(height)) {
return false;
}
}
return true;
}
@Override
public void process() throws DataException {
Account sender = this.getSender();

View File

@ -30,6 +30,9 @@
"onlineAccountSignaturesMaxLifetime": 86400000,
"onlineAccountsModulusV2Timestamp": 1659801600000,
"selfSponsorshipAlgoV1SnapshotTimestamp": 1670230000000,
"selfSponsorshipAlgoV2SnapshotTimestamp": 1708360200000,
"selfSponsorshipAlgoV3SnapshotTimestamp": 1708432200000,
"referenceTimestampBlock": 1670684455220,
"mempowTransactionUpdatesTimestamp": 1693558800000,
"blockRewardBatchStartHeight": 1508000,
"blockRewardBatchSize": 1000,
@ -93,9 +96,14 @@
"increaseOnlineAccountsDifficultyTimestamp": 9999999999999,
"onlineAccountMinterLevelValidationHeight": 1092000,
"selfSponsorshipAlgoV1Height": 1092400,
"selfSponsorshipAlgoV2Height": 1611200,
"selfSponsorshipAlgoV3Height": 1612200,
"feeValidationFixTimestamp": 1671918000000,
"chatReferenceTimestamp": 1674316800000,
"arbitraryOptionalFeeTimestamp": 1680278400000
"arbitraryOptionalFeeTimestamp": 1680278400000,
"unconfirmableRewardSharesHeight": 1575500,
"disableTransferPrivsTimestamp": 1706745000000,
"enableTransferPrivsTimestamp": 1709251200000
},
"checkpoints": [
{ "height": 1136300, "signature": "3BbwawEF2uN8Ni5ofpJXkukoU8ctAPxYoFB7whq9pKfBnjfZcpfEJT4R95NvBDoTP8WDyWvsUvbfHbcr9qSZuYpSKZjUQTvdFf6eqznHGEwhZApWfvXu6zjGCxYCp65F4jsVYYJjkzbjmkCg5WAwN5voudngA23kMK6PpTNygapCzXt" }

View File

@ -0,0 +1,83 @@
#Generated by ResourceBundle Editor (http://essiembre.github.io/eclipse-rbe/)
# Keys are from api.ApiError enum
# "localeLang": "he",
### 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 = מזהה נכס לא חוקי
INVALID_ORDER_ID = מזהה הזמנת נכס לא חוקי
ORDER_UNKNOWN = מזהה הזמנת נכס לא ידוע
### 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 = עמית לא השיב בזמן המותר

View File

@ -35,8 +35,6 @@ MINTING_DISABLED = Münzprägung inaktiv
MINTING_ENABLED = \u2714 Münzprägung aktiv
OPEN_UI = Öffne Benutzeroberfläche
PERFORMING_DB_CHECKPOINT = Speichere unerfasste Datenbankänderungen...
PERFORMING_DB_MAINTENANCE = Planmäßige Wartung wird durchgeführt...
@ -46,3 +44,11 @@ SYNCHRONIZE_CLOCK = Synchronisiere Uhrzeit
SYNCHRONIZING_BLOCKCHAIN = Synchronisiere
SYNCHRONIZING_CLOCK = Uhrzeit wird synchronisiert
RESTARTING_NODE = Knoten wird neu gestartet
APPLYING_RESTARTING_NODE = Neustart des knotens wird angewendet. Bitte haben Sie Geduld...
BOOTSTRAP_NODE = Bootstrapping-Knoten
APPLYING_BOOTSTRAP_AND_RESTARTING = Bootstrap anwenden und Knoten neu starten. Bitte haben Sie Geduld...

View File

@ -35,8 +35,6 @@ MINTING_DISABLED = NOT minting
MINTING_ENABLED = \u2714 Minting
OPEN_UI = Open UI
PERFORMING_DB_CHECKPOINT = Saving uncommitted database changes...
PERFORMING_DB_MAINTENANCE = Performing scheduled maintenance...
@ -46,3 +44,11 @@ SYNCHRONIZE_CLOCK = Synchronize clock
SYNCHRONIZING_BLOCKCHAIN = Synchronizing
SYNCHRONIZING_CLOCK = Synchronizing clock
RESTARTING_NODE = Restarting Node
APPLYING_RESTARTING_NODE = Applying restarting node. Please be patient...
BOOTSTRAP_NODE = Bootstrapping Node
APPLYING_BOOTSTRAP_AND_RESTARTING = Applying bootstrap and restarting node. Please be patient...

View File

@ -33,8 +33,6 @@ MINTING_DISABLED = Acuñación NO habilitada
MINTING_ENABLED = \u2714 Acuñación habilitada
OPEN_UI = IU abierta
PERFORMING_DB_CHECKPOINT = Guardando cambios de base de datos no confirmados...
PERFORMING_DB_MAINTENANCE = Realizando mantenimiento programado...
@ -44,3 +42,11 @@ SYNCHRONIZE_CLOCK = Sincronizar reloj
SYNCHRONIZING_BLOCKCHAIN = Sincronizando
SYNCHRONIZING_CLOCK = Sincronizando reloj
RESTARTING_NODE = Reiniciando el nodo
APPLYING_RESTARTING_NODE = Aplicando el nodo de reinicio. Por favor sea paciente...
BOOTSTRAP_NODE = Nodo de arranque
APPLYING_BOOTSTRAP_AND_RESTARTING = Aplicando bootstrap y reiniciando el nodo. Por favor sea paciente...

View File

@ -35,8 +35,6 @@ MINTING_DISABLED = EI lyö rahaa
MINTING_ENABLED = \u2714 Lyö rahaa
OPEN_UI = Avaa UI
PERFORMING_DB_CHECKPOINT = Tallentaa kommittoidut tietokantamuutokset...
PERFORMING_DB_MAINTENANCE = Suoritetaan määräaikaishuoltoa...
@ -46,3 +44,11 @@ SYNCHRONIZE_CLOCK = Synkronisoi kello
SYNCHRONIZING_BLOCKCHAIN = Synkronisoi
SYNCHRONIZING_CLOCK = Synkronisoi kelloa
RESTARTING_NODE = Käynnistetään uudelleen solmu
APPLYING_RESTARTING_NODE = Käytetään uudelleenkäynnistyssolmua. Olkaa kärsivällisiä...
BOOTSTRAP_NODE = Käynnistyssolmu
APPLYING_BOOTSTRAP_AND_RESTARTING = Käynnistetään ja käynnistetään solmu uudelleen. Olkaa kärsivällisiä...

View File

@ -35,8 +35,6 @@ MINTING_DISABLED = NE mint PAS
MINTING_ENABLED = \u2714 Minting
OPEN_UI = Ouvrir l'interface
PERFORMING_DB_CHECKPOINT = Enregistrement des modifications de base de données non validées...
PERFORMING_DB_MAINTENANCE = Entrain d'effectuer la maintenance programmée...
@ -46,3 +44,11 @@ SYNCHRONIZE_CLOCK = Mettre l'heure à jour
SYNCHRONIZING_BLOCKCHAIN = Synchronisation
SYNCHRONIZING_CLOCK = Synchronisation de l'heure
RESTARTING_NODE = Redémarrage du nœud
APPLYING_RESTARTING_NODE = Application du redémarrage du nœud. S'il vous plaît, soyez patient...
BOOTSTRAP_NODE = Nœud d'amorçage
APPLYING_BOOTSTRAP_AND_RESTARTING = Application du bootstrap et redémarrage du nœud. S'il vous plaît, soyez patient...

View File

@ -0,0 +1,54 @@
#Generated by ResourceBundle Editor (http://essiembre.github.io/eclipse-rbe/)
# SysTray pop-up menu
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 = Lite Node
MINTING_DISABLED = כרייה מבוטלת
MINTING_ENABLED = \u2714 הטבעה
PERFORMING_DB_CHECKPOINT = שומר שינויים לא מחויבים במסד הנתונים...
PERFORMING_DB_MAINTENANCE = מבצע תחזוקה מתוזמנת...
SYNCHRONIZE_CLOCK = סנכרן שעון
SYNCHRONIZING_BLOCKCHAIN = מסנכרן
SYNCHRONIZING_CLOCK = מסנכרן שעון
RESTARTING_NODE = הפעלה מחדש של צומת
APPLYING_RESTARTING_NODE = החלת צומת הפעלה מחדש. אנא התאזר בסבלנות...
BOOTSTRAP_NODE = צומת אתחול
APPLYING_BOOTSTRAP_AND_RESTARTING = החלת אתחול והפעלת צומת

View File

@ -35,8 +35,6 @@ MINTING_DISABLED = QORT-érmeverés jelenleg nincs folyamatban
MINTING_ENABLED = \u2714 QORT-érmeverés folyamatban
OPEN_UI = Felhasználói felület megnyitása
PERFORMING_DB_CHECKPOINT = Mentetlen adatbázis-módosítások mentése...
PERFORMING_DB_MAINTENANCE = Ütemezett karbantartás...
@ -46,3 +44,11 @@ SYNCHRONIZE_CLOCK = Óra-szinkronizálás megkezdése
SYNCHRONIZING_BLOCKCHAIN = Szinkronizálás
SYNCHRONIZING_CLOCK = Óraszinkronizálás folyamatban
RESTARTING_NODE = Csomópont újraindítása
APPLYING_RESTARTING_NODE = Újraindító csomópont alkalmazása. Kérjük várjon...
BOOTSTRAP_NODE = Rendszerindítási csomópont
APPLYING_BOOTSTRAP_AND_RESTARTING = Bootstrap alkalmazása és csomópont újraindítása. Kérjük várjon...

View File

@ -35,8 +35,6 @@ MINTING_DISABLED = Conio disabilitato
MINTING_ENABLED = \u2714 Conio abilitato
OPEN_UI = Apri UI
PERFORMING_DB_CHECKPOINT = Salvataggio delle modifiche del database non salvate...
PERFORMING_DB_MAINTENANCE = Manutenzione programmata dell'efficienza...
@ -46,3 +44,11 @@ SYNCHRONIZE_CLOCK = Sincronizzare l'orologio
SYNCHRONIZING_BLOCKCHAIN = Sincronizzazione della blockchain
SYNCHRONIZING_CLOCK = Sincronizzazione orologio
RESTARTING_NODE = Riavvio del nodo
APPLYING_RESTARTING_NODE = Applicazione del nodo di riavvio. Per favore sii paziente...
BOOTSTRAP_NODE = Nodo di bootstrap
APPLYING_BOOTSTRAP_AND_RESTARTING = Applicazione del bootstrap e riavvio del nodo. Per favore sii paziente...

View File

@ -35,8 +35,6 @@ MINTING_DISABLED = ミント一時中止中
MINTING_ENABLED = \u2714 ミント
OPEN_UI = UIを開く
PERFORMING_DB_CHECKPOINT = コミットされていないデータベースの変更を保存中...
PERFORMING_DB_MAINTENANCE = 定期メンテナンスを実行中...
@ -46,3 +44,11 @@ SYNCHRONIZE_CLOCK = 時刻を同期
SYNCHRONIZING_BLOCKCHAIN = ブロックチェーンを同期中
SYNCHRONIZING_CLOCK = 時刻を同期中
RESTARTING_NODE = ノードを再起動しています
APPLYING_RESTARTING_NODE = 再起動ノードを適用しています。 しばらくお待ちください...
BOOTSTRAP_NODE = ブートストラップ ノード
APPLYING_BOOTSTRAP_AND_RESTARTING = ブートストラップを適用し、ノードを再起動します。 しばらくお待ちください...

View File

@ -35,8 +35,6 @@ MINTING_DISABLED = 민팅중이 아님
MINTING_ENABLED = \u2714 민팅
OPEN_UI = UI 열기
PERFORMING_DB_CHECKPOINT = 커밋되지 않은 데이터베이스 변경 내용을 저장하는 중...
PERFORMING_DB_MAINTENANCE = 예약된 유지 관리 수행 중...
@ -46,3 +44,11 @@ SYNCHRONIZE_CLOCK = 시간 동기화
SYNCHRONIZING_BLOCKCHAIN = 동기화중
SYNCHRONIZING_CLOCK = 시간 동기화
RESTARTING_NODE = 노드 다시 시작 중
APPLYING_RESTARTING_NODE = 노드 재시작을 적용합니다. 기다려주십시오...
BOOTSTRAP_NODE = 부트스트래핑 노드
APPLYING_BOOTSTRAP_AND_RESTARTING = 부트스트랩을 적용하고 노드를 다시 시작합니다. 기다려주십시오...

View File

@ -35,8 +35,6 @@ MINTING_DISABLED = Minten is uitgeschakeld
MINTING_ENABLED = \u2714 Minten is actief
OPEN_UI = Open UI
PERFORMING_DB_CHECKPOINT = De database wordt bijgewerkt...
PERFORMING_DB_MAINTENANCE = Bezig met gepland onderhoud...
@ -46,3 +44,11 @@ SYNCHRONIZE_CLOCK = Synchronizeer klok
SYNCHRONIZING_BLOCKCHAIN = Bezig met synchronizeren
SYNCHRONIZING_CLOCK = Klok wordt gesynchronizeerd
RESTARTING_NODE = Knooppunt opnieuw starten
APPLYING_RESTARTING_NODE = Herstartknooppunt toepassen. Wees alstublieft geduldig...
BOOTSTRAP_NODE = Opstartknooppunt
APPLYING_BOOTSTRAP_AND_RESTARTING = Bootstrap toepassen en knooppunt opnieuw starten. Wees alstublieft geduldig...

View File

@ -33,8 +33,6 @@ MINTING_DISABLED = Mennica zamknięta
MINTING_ENABLED = \u2714 Mennica aktywna
OPEN_UI = Otwórz interfejs użytkownika
PERFORMING_DB_CHECKPOINT = Zapisywanie niezaksięgowanych zmian w bazie danych...
PERFORMING_DB_MAINTENANCE = Performing scheduled maintenance...
@ -44,3 +42,11 @@ SYNCHRONIZE_CLOCK = Synchronizuj zegar
SYNCHRONIZING_BLOCKCHAIN = Synchronizacja
SYNCHRONIZING_CLOCK = Synchronizacja zegara
RESTARTING_NODE = Ponowne uruchamianie węzła
APPLYING_RESTARTING_NODE = Stosuję ponowne uruchomienie węzła. Proszę być cierpliwym...
BOOTSTRAP_NODE = Węzeł ładowania początkowego
APPLYING_BOOTSTRAP_AND_RESTARTING = Stosowanie ładowania początkowego i ponowne uruchamianie węzła. Proszę być cierpliwym...

View File

@ -35,14 +35,20 @@ MINTING_DISABLED = nu produce moneda
MINTING_ENABLED = \u2714 Minting
OPEN_UI = Deschidere interfata utilizator IU
PERFORMING_DB_CHECKPOINT = Salvarea modificarilor nerealizate ale bazei de date...
PERFORMING_DB_MAINTENANCE = Efectuarea intretinerii programate
PERFORMING_DB_MAINTENANCE = Efectuarea intretinerii programate<EFBFBD>
SYNCHRONIZE_CLOCK = Sincronizare ceas
SYNCHRONIZING_BLOCKCHAIN = Sincronizare
SYNCHRONIZING_CLOCK = Se sincronizeaza ceasul
RESTARTING_NODE = Repornirea nodului
APPLYING_RESTARTING_NODE = Se aplica nodul de repornire. Te rog fii rabdator...
BOOTSTRAP_NODE = Nod de bootstrap
APPLYING_BOOTSTRAP_AND_RESTARTING = Se aplica bootstrap si se reporneste nodul. Te rog fii rabdator...

View File

@ -35,8 +35,6 @@ MINTING_DISABLED = Чеканка отключена
MINTING_ENABLED = \u2714 Чеканка активна
OPEN_UI = Открыть пользовательский интерфейс
PERFORMING_DB_CHECKPOINT = Сохранение незафиксированных изменений базы данных...
PERFORMING_DB_MAINTENANCE = Выполнение планового технического обслуживания...
@ -46,3 +44,11 @@ SYNCHRONIZE_CLOCK = Синхронизировать время
SYNCHRONIZING_BLOCKCHAIN = Синхронизация цепи
SYNCHRONIZING_CLOCK = Проверка времени
RESTARTING_NODE = Перезапуск узла
APPLYING_RESTARTING_NODE = Применение перезапуска узла. Пожалуйста, будьте терпеливы...
BOOTSTRAP_NODE = Узел начальной загрузки
APPLYING_BOOTSTRAP_AND_RESTARTING = Применение начальной загрузки и перезапуск узла. Пожалуйста, будьте терпеливы...

View File

@ -33,8 +33,6 @@ MINTING_DISABLED = Präglar INTE
MINTING_ENABLED = \u2714 Präglar
OPEN_UI = Öppna UI
PERFORMING_DB_CHECKPOINT = Sparar oengagerade databasändringar...
PERFORMING_DB_MAINTENANCE = Utför schemalagt underhåll...
@ -44,3 +42,11 @@ SYNCHRONIZE_CLOCK = Synkronisera klockan
SYNCHRONIZING_BLOCKCHAIN = Synkroniserar
SYNCHRONIZING_CLOCK = Synkroniserar klockan
RESTARTING_NODE = Mimitian deui Node
APPLYING_RESTARTING_NODE = Nerapkeun titik ngamimitian deui. Mangga sing sabar...
BOOTSTRAP_NODE = Bootstrapping Node
APPLYING_BOOTSTRAP_AND_RESTARTING = Nerapkeun bootstrap sareng hurungkeun deui titik. Mangga sing sabar...

View File

@ -35,8 +35,6 @@ MINTING_DISABLED = 没有铸币
MINTING_ENABLED = \u2714 铸币
OPEN_UI = 开启Qortal界面
PERFORMING_DB_CHECKPOINT = 正在保存未提交的数据库修订...
PERFORMING_DB_MAINTENANCE = 正在执行定期数据库维护...
@ -46,3 +44,11 @@ SYNCHRONIZE_CLOCK = 同步时钟
SYNCHRONIZING_BLOCKCHAIN = 正在同步区块链
SYNCHRONIZING_CLOCK = 正在同步时钟
RESTARTING_NODE = 重新启动节点
APPLYING_RESTARTING_NODE = 应用重新启动节点。 请耐心等待...
BOOTSTRAP_NODE = 引导节点
APPLYING_BOOTSTRAP_AND_RESTARTING = 应用引导程序并重新启动节点。 请耐心等待...

View File

@ -35,8 +35,6 @@ MINTING_DISABLED = 沒有鑄幣
MINTING_ENABLED = \u2714 鑄幣
OPEN_UI = 開啓Qortal界面
PERFORMING_DB_CHECKPOINT = 正在保存未提交的數據庫修訂...
PERFORMING_DB_MAINTENANCE = 正在執行數據庫定期維護...
@ -46,3 +44,11 @@ SYNCHRONIZE_CLOCK = 同步時鐘
SYNCHRONIZING_BLOCKCHAIN = 正在同步區塊鏈
SYNCHRONIZING_CLOCK = 正在同步時鐘
RESTARTING_NODE = 重新啟動節點
APPLYING_RESTARTING_NODE = 應用重新啟動節點。 請耐心等待...
BOOTSTRAP_NODE = 引導節點
APPLYING_BOOTSTRAP_AND_RESTARTING = 應用引導程式並重新啟動節點。 請耐心等待...

View File

@ -193,3 +193,7 @@ TRANSACTION_ALREADY_EXISTS = Transaktion existiert bereits
TRANSACTION_UNKNOWN = Transaktion unbekannt
TX_GROUP_ID_MISMATCH = die Gruppen-ID der Transaktion stimmt nicht überein
TRANSFER_PRIVS_DISABLED = Übertragungsberechtigungen deaktiviert
TEMPORARY_DISABLED = Namensregistrierung vorübergehend deaktiviert

View File

@ -193,3 +193,7 @@ TRANSACTION_ALREADY_EXISTS = transaction already exists
TRANSACTION_UNKNOWN = transaction unknown
TX_GROUP_ID_MISMATCH = transaction's group ID does not match
TRANSFER_PRIVS_DISABLED = transfer privileges disabled
TEMPORARY_DISABLED = Name registration temporary disabled

View File

@ -193,3 +193,7 @@ TRANSACTION_ALREADY_EXISTS = la transacción ya existe
TRANSACTION_UNKNOWN = transacción desconocida
TX_GROUP_ID_MISMATCH = el ID de grupo de la transacción no coincide
TRANSFER_PRIVS_DISABLED = privilegios de transferencia deshabilitados
TEMPORARY_DISABLED = Registro de nombre temporalmente deshabilitado

View File

@ -193,3 +193,7 @@ TRANSACTION_ALREADY_EXISTS = transaktio on jo olemassa
TRANSACTION_UNKNOWN = tuntematon transaktio
TX_GROUP_ID_MISMATCH = transaktion ryhmä-ID:n vastaavuusvirhe
TRANSFER_PRIVS_DISABLED = siirtooikeudet poistettu käytöstä
TEMPORARY_DISABLED = Nimen rekisteröinti tilapäisesti poistettu käytöstä

View File

@ -193,3 +193,7 @@ TRANSACTION_ALREADY_EXISTS = la transaction existe déjà
TRANSACTION_UNKNOWN = transaction inconnue
TX_GROUP_ID_MISMATCH = l'identifiant du groupe de transaction ne correspond pas
TRANSFER_PRIVS_DISABLED = privilèges de transfert désactivés
TEMPORARY_DISABLED = Enregistrement du nom temporairement désactivé

View File

@ -0,0 +1,199 @@
#
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 = אי התאמה של מזהה הקבוצה
GROUP_OWNER_CANNOT_LEAVE = בעל הקבוצה לא יכול לעזוב את הקבוצה
HAVE_EQUALS_WANT = have-asset זהה ל-want-asset
INCORRECT_NONCE = לא תקין של PoW
INSUFFICIENT_FEE = עמלה לא מספקת
INVALID_ADDRESS = כתובת לא חוקית
INVALID_AMOUNT = סכום לא חוקי
INVALID_ASSET_OWNER = בעל נכס לא חוקי
INVALID_AT_TRANSACTION = עסקת AT לא חוקית
INVALID_AT_TYPE_LENGTH = אורך AT 'סוג' לא חוקי
INVALID_BUT_OK = לא חוקי אבל בסדר
INVALID_CREATION_BYTES = בתים לא חוקיים של יצירה
INVALID_DATA_LENGTH = אורך נתונים לא חוקי
INVALID_DESCRIPTION_LENGTH = אורך תיאור לא חוקי
INVALID_GROUP_APPROVAL_THRESHOLD = סף לא חוקי לאישור קבוצה
INVALID_GROUP_BLOCK_DELAY = עיכוב חסימת אישור קבוצה לא חוקי
INVALID_GROUP_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 = מזהה קבוצת עסקאות לא חוקי
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 = בסדר
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 = מזהה הקבוצה של העסקה אינו תואם
TRANSFER_PRIVS_DISABLED = הרשאות העברה מושבתות
TEMPORARY_DISABLED = רישום שמות מושבת זמנית

View File

@ -193,3 +193,7 @@ TRANSACTION_ALREADY_EXISTS = ez a tranzakció már létezik
TRANSACTION_UNKNOWN = ismeretlen tranzakció
TX_GROUP_ID_MISMATCH = a tranzakció csoportazonosítója nem egyezik
TRANSFER_PRIVS_DISABLED = átviteli jogosultságok letiltva
TEMPORARY_DISABLED = A névregisztráció ideiglenesen le van tiltva

View File

@ -193,3 +193,7 @@ TRANSACTION_ALREADY_EXISTS = la transazione già esiste
TRANSACTION_UNKNOWN = transazione sconosciuta
TX_GROUP_ID_MISMATCH = identificazione di gruppo della transazione non corrisponde
TRANSFER_PRIVS_DISABLED = privilegi di trasferimento disabilitati
TEMPORARY_DISABLED = Registrazione del nome temporaneamente disabilitata

View File

@ -193,3 +193,7 @@ TRANSACTION_ALREADY_EXISTS = 既にトランザクションは存在します
TRANSACTION_UNKNOWN = 不明なトランザクション
TX_GROUP_ID_MISMATCH = トランザクションのグループIDが一致しません
TRANSFER_PRIVS_DISABLED = 転送権限が無効になっています
TEMPORARY_DISABLED = 名前の登録が一時的に無効になっています

View File

@ -193,3 +193,7 @@ TRANSACTION_ALREADY_EXISTS = 거래가 이미 존재합니다
TRANSACTION_UNKNOWN = 알 수 없는 거래
TX_GROUP_ID_MISMATCH = 트랜잭션의 그룹 ID가 일치하지 않습니다
TRANSFER_PRIVS_DISABLED = 권한 이전이 비활성화되었습니다.
TEMPORARY_DISABLED = 이름 등록이 일시적으로 비활성화되었습니다.

View File

@ -193,3 +193,7 @@ TRANSACTION_ALREADY_EXISTS = transactie bestaat reeds
TRANSACTION_UNKNOWN = transactie onbekend
TX_GROUP_ID_MISMATCH = groep-ID komt niet overeen
TRANSFER_PRIVS_DISABLED = overdrachtsrechten uitgeschakeld
TEMPORARY_DISABLED = Naamregistratie tijdelijk uitgeschakeld

View File

@ -194,3 +194,6 @@ TRANSACTION_UNKNOWN = transakcja nieznana
TX_GROUP_ID_MISMATCH = niezgodność ID grupy transakcji
TRANSFER_PRIVS_DISABLED = uprawnienia do przenoszenia wyłączone
TEMPORARY_DISABLED = Rejestracja nazwy tymczasowo wyłączona

View File

@ -193,3 +193,7 @@ TRANSACTION_ALREADY_EXISTS = tranzactia exista deja
TRANSACTION_UNKNOWN = tranzactie necunoscuta
TX_GROUP_ID_MISMATCH = ID-ul de grup al tranzactiei nu se potriveste
TRANSFER_PRIVS_DISABLED = privilegii de transfer dezactivate
TEMPORARY_DISABLED = Înregistrarea numelui a fost temporar dezactivată

View File

@ -193,3 +193,7 @@ TRANSACTION_ALREADY_EXISTS = транзакция существует
TRANSACTION_UNKNOWN = неизвестная транзакция
TX_GROUP_ID_MISMATCH = не соответствие идентификатора группы в хэш транзации
TRANSFER_PRIVS_DISABLED = права на передачу отключены
TEMPORARY_DISABLED = Регистрация имени временно отключена

View File

@ -193,3 +193,7 @@ TRANSACTION_ALREADY_EXISTS = transaktionen finns redan
TRANSACTION_UNKNOWN = okänd transaktion
TX_GROUP_ID_MISMATCH = transaktionens grupp-ID matchar inte
TRANSFER_PRIVS_DISABLED = överföringsprivilegier inaktiverade
TEMPORARY_DISABLED = Namnregistrering tillfälligt inaktiverad

Some files were not shown because too many files have changed in this diff Show More