forked from Qortal/qortal
Merge pull request #186 from jschulthess/reticulum
Initial (almost) phase-1 Reticulum implementation
This commit is contained in:
commit
4df05364f5
@ -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
|
||||
|
@ -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>
|
Binary file not shown.
@ -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>
|
@ -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.
@ -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>
|
||||
|
@ -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
145
pom.xml
@ -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>
|
||||
|
227
src/main/java/org/qortal/ApplyBootstrap.java
Normal file
227
src/main/java/org/qortal/ApplyBootstrap.java
Normal 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()));
|
||||
}
|
||||
}
|
||||
}
|
196
src/main/java/org/qortal/ApplyRestart.java
Normal file
196
src/main/java/org/qortal/ApplyRestart.java
Normal 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()));
|
||||
}
|
||||
}
|
||||
}
|
@ -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);
|
||||
}
|
||||
|
||||
|
@ -363,5 +363,4 @@ public class SelfSponsorshipAlgoV1 {
|
||||
|
||||
return transactionDataList;
|
||||
}
|
||||
|
||||
}
|
||||
}
|
249
src/main/java/org/qortal/account/SelfSponsorshipAlgoV2.java
Normal file
249
src/main/java/org/qortal/account/SelfSponsorshipAlgoV2.java
Normal 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;
|
||||
}
|
||||
}
|
370
src/main/java/org/qortal/account/SelfSponsorshipAlgoV3.java
Normal file
370
src/main/java/org/qortal/account/SelfSponsorshipAlgoV3.java
Normal 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;
|
||||
}
|
||||
}
|
@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -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);
|
||||
}
|
||||
}
|
||||
}
|
@ -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);
|
||||
}
|
||||
}
|
||||
}
|
@ -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);
|
||||
}
|
||||
}
|
||||
}
|
@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -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)
|
||||
|
420
src/main/java/org/qortal/api/resource/CrossChainUtils.java
Normal file
420
src/main/java/org/qortal/api/resource/CrossChainUtils.java
Normal 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;
|
||||
}
|
||||
}
|
@ -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;
|
||||
|
@ -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();
|
||||
|
||||
|
@ -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();
|
||||
|
@ -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()));
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
@ -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 {
|
||||
|
@ -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)));
|
||||
}
|
||||
|
||||
}
|
||||
}
|
143
src/main/java/org/qortal/block/SelfSponsorshipAlgoV2Block.java
Normal file
143
src/main/java/org/qortal/block/SelfSponsorshipAlgoV2Block.java
Normal 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)));
|
||||
}
|
||||
|
||||
}
|
136
src/main/java/org/qortal/block/SelfSponsorshipAlgoV3Block.java
Normal file
136
src/main/java/org/qortal/block/SelfSponsorshipAlgoV3Block.java
Normal 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)));
|
||||
}
|
||||
|
||||
}
|
@ -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
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
@ -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()
|
||||
|
103
src/main/java/org/qortal/controller/BootstrapNode.java
Normal file
103
src/main/java/org/qortal/controller/BootstrapNode.java
Normal 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
|
||||
}
|
||||
}
|
||||
}
|
@ -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 {
|
||||
|
102
src/main/java/org/qortal/controller/RestartNode.java
Normal file
102
src/main/java/org/qortal/controller/RestartNode.java
Normal 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
|
||||
}
|
||||
}
|
||||
}
|
@ -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.
|
||||
*
|
||||
|
@ -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();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -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();
|
||||
}
|
||||
|
@ -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;
|
||||
}
|
||||
}
|
15
src/main/java/org/qortal/crosschain/ChainableServer.java
Normal file
15
src/main/java/org/qortal/crosschain/ChainableServer.java
Normal 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}
|
||||
}
|
@ -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 );
|
||||
}
|
||||
}
|
||||
|
@ -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 );
|
||||
}
|
||||
}
|
||||
|
@ -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;
|
||||
}
|
||||
}
|
||||
|
@ -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 );
|
||||
}
|
||||
}
|
||||
|
@ -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>
|
||||
|
@ -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;
|
||||
|
@ -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 );
|
||||
}
|
||||
}
|
||||
|
@ -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 +
|
||||
'}';
|
||||
}
|
||||
}
|
74
src/main/java/org/qortal/crosschain/ServerInfo.java
Normal file
74
src/main/java/org/qortal/crosschain/ServerInfo.java
Normal 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 +
|
||||
'}';
|
||||
}
|
||||
}
|
@ -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();
|
||||
}
|
||||
|
||||
}
|
||||
|
@ -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;
|
||||
}
|
||||
}
|
106
src/main/java/org/qortal/data/crosschain/TransactionSummary.java
Normal file
106
src/main/java/org/qortal/data/crosschain/TransactionSummary.java
Normal 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;
|
||||
}
|
||||
}
|
@ -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() {
|
||||
|
425
src/main/java/org/qortal/network/RNSNetwork.java
Normal file
425
src/main/java/org/qortal/network/RNSNetwork.java
Normal 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();
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
110
src/main/java/org/qortal/network/RNSPeer.java
Normal file
110
src/main/java/org/qortal/network/RNSPeer.java
Normal 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;
|
||||
}
|
||||
|
||||
}
|
@ -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>
|
||||
|
@ -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;
|
||||
|
@ -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);
|
||||
|
@ -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;
|
||||
}
|
||||
|
@ -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;
|
||||
}
|
||||
|
||||
|
@ -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();
|
||||
|
@ -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>
|
||||
|
@ -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());
|
||||
}
|
||||
|
@ -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();
|
||||
|
@ -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" }
|
||||
|
83
src/main/resources/i18n/ApiError_he.properties
Normal file
83
src/main/resources/i18n/ApiError_he.properties
Normal 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 = עמית לא השיב בזמן המותר
|
@ -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...
|
||||
|
@ -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...
|
||||
|
@ -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...
|
||||
|
@ -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ä...
|
||||
|
@ -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...
|
||||
|
54
src/main/resources/i18n/SysTray_he.properties
Normal file
54
src/main/resources/i18n/SysTray_he.properties
Normal 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 = החלת אתחול והפעלת צומת
|
@ -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...
|
||||
|
@ -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...
|
||||
|
@ -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 = ブートストラップを適用し、ノードを再起動します。 しばらくお待ちください...
|
||||
|
@ -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 = 부트스트랩을 적용하고 노드를 다시 시작합니다. 기다려주십시오...
|
||||
|
@ -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...
|
||||
|
@ -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...
|
||||
|
@ -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...
|
||||
|
@ -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 = Применение начальной загрузки и перезапуск узла. Пожалуйста, будьте терпеливы...
|
||||
|
@ -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...
|
||||
|
@ -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 = 应用引导程序并重新启动节点。 请耐心等待...
|
||||
|
@ -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 = 應用引導程式並重新啟動節點。 請耐心等待...
|
||||
|
@ -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
|
||||
|
@ -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
|
||||
|
@ -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
|
||||
|
@ -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ä
|
||||
|
@ -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é
|
||||
|
199
src/main/resources/i18n/TransactionValidity_he.properties
Normal file
199
src/main/resources/i18n/TransactionValidity_he.properties
Normal 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 = רישום שמות מושבת זמנית
|
@ -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
|
||||
|
@ -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
|
||||
|
@ -193,3 +193,7 @@ TRANSACTION_ALREADY_EXISTS = 既にトランザクションは存在します
|
||||
TRANSACTION_UNKNOWN = 不明なトランザクション
|
||||
|
||||
TX_GROUP_ID_MISMATCH = トランザクションのグループIDが一致しません
|
||||
|
||||
TRANSFER_PRIVS_DISABLED = 転送権限が無効になっています
|
||||
|
||||
TEMPORARY_DISABLED = 名前の登録が一時的に無効になっています
|
||||
|
@ -193,3 +193,7 @@ TRANSACTION_ALREADY_EXISTS = 거래가 이미 존재합니다
|
||||
TRANSACTION_UNKNOWN = 알 수 없는 거래
|
||||
|
||||
TX_GROUP_ID_MISMATCH = 트랜잭션의 그룹 ID가 일치하지 않습니다
|
||||
|
||||
TRANSFER_PRIVS_DISABLED = 권한 이전이 비활성화되었습니다.
|
||||
|
||||
TEMPORARY_DISABLED = 이름 등록이 일시적으로 비활성화되었습니다.
|
||||
|
@ -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
|
||||
|
@ -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
|
||||
|
@ -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ă
|
||||
|
@ -193,3 +193,7 @@ TRANSACTION_ALREADY_EXISTS = транзакция существует
|
||||
TRANSACTION_UNKNOWN = неизвестная транзакция
|
||||
|
||||
TX_GROUP_ID_MISMATCH = не соответствие идентификатора группы в хэш транзации
|
||||
|
||||
TRANSFER_PRIVS_DISABLED = права на передачу отключены
|
||||
|
||||
TEMPORARY_DISABLED = Регистрация имени временно отключена
|
||||
|
@ -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
Loading…
Reference in New Issue
Block a user