mirror of
https://github.com/Qortal/qortal.git
synced 2025-07-15 01:01:20 +00:00
Compare commits
No commits in common. "master" and "v4.6.6" have entirely different histories.
6
.github/workflows/pr-testing.yml
vendored
6
.github/workflows/pr-testing.yml
vendored
@ -1,7 +1,7 @@
|
||||
name: PR testing
|
||||
|
||||
on:
|
||||
push:
|
||||
pull_request:
|
||||
branches: [ master ]
|
||||
|
||||
jobs:
|
||||
@ -22,10 +22,6 @@ jobs:
|
||||
java-version: '11'
|
||||
distribution: 'adopt'
|
||||
|
||||
- name: Load custom deps
|
||||
run: |
|
||||
mvn install -DskipTests=true --file pom.xml
|
||||
|
||||
- name: Run all tests
|
||||
run: |
|
||||
mvn -B clean test -DskipTests=false --file pom.xml
|
||||
|
15
README.md
15
README.md
@ -15,31 +15,20 @@ Building the future one block at a time. Welcome to Qortal.
|
||||
|
||||
# Building the Qortal Core from Source
|
||||
|
||||
## Build / Run
|
||||
## Build / run
|
||||
|
||||
- Requires Java 11. OpenJDK 11 recommended over Java SE.
|
||||
- Install Maven
|
||||
- Use Maven to fetch dependencies and build: `mvn clean package`
|
||||
- Update Maven dependencies: `mvn install`
|
||||
- Built JAR should be something like `target/qortal-1.0.jar`
|
||||
- Create basic *settings.json* file: `echo '{}' > settings.json`
|
||||
- Run JAR in same working directory as *settings.json*: `java -jar target/qortal-1.0.jar`
|
||||
- Wrap in shell script, add JVM flags, redirection, backgrounding, etc. as necessary.
|
||||
- Or use supplied example shell script: *start.sh*
|
||||
|
||||
## IntelliJ IDEA Configuration
|
||||
|
||||
- Run -> Edit Configurations
|
||||
- Add New Application
|
||||
- Name: qortal
|
||||
- SDK: java 11
|
||||
- Main Class: org.qortal.controller.Controller
|
||||
- Program arguments: settings.json -Dlog4j.configurationFile=log4j2.properties -ea
|
||||
- Environment variables: Djava.net.preferIPv4Stack=false
|
||||
|
||||
# Using a pre-built Qortal 'jar' binary
|
||||
|
||||
If you prefer to utilize a released version of Qortal, you may do so by downloading one of the available releases from the releases page, that are also linked on https://qortal.org and https://qortal.dev.
|
||||
If you would prefer to utilize a released version of Qortal, you may do so by downloading one of the available releases from the releases page, that are also linked on https://qortal.org and https://qortal.dev.
|
||||
|
||||
# Learning Q-App Development
|
||||
|
||||
|
BIN
lib/org/ciyam/AT/1.3.7/AT-1.3.7.jar
Normal file
BIN
lib/org/ciyam/AT/1.3.7/AT-1.3.7.jar
Normal file
Binary file not shown.
9
lib/org/ciyam/AT/1.3.7/AT-1.3.7.pom
Normal file
9
lib/org/ciyam/AT/1.3.7/AT-1.3.7.pom
Normal file
@ -0,0 +1,9 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<project xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://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>org.ciyam</groupId>
|
||||
<artifactId>AT</artifactId>
|
||||
<version>1.3.7</version>
|
||||
<description>POM was created from install:install-file</description>
|
||||
</project>
|
BIN
lib/org/ciyam/AT/1.3.8/AT-1.3.8.jar
Normal file
BIN
lib/org/ciyam/AT/1.3.8/AT-1.3.8.jar
Normal file
Binary file not shown.
9
lib/org/ciyam/AT/1.3.8/AT-1.3.8.pom
Normal file
9
lib/org/ciyam/AT/1.3.8/AT-1.3.8.pom
Normal file
@ -0,0 +1,9 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<project xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://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>org.ciyam</groupId>
|
||||
<artifactId>AT</artifactId>
|
||||
<version>1.3.8</version>
|
||||
<description>POM was created from install:install-file</description>
|
||||
</project>
|
BIN
lib/org/ciyam/AT/1.4.0/AT-1.4.0.jar
Normal file
BIN
lib/org/ciyam/AT/1.4.0/AT-1.4.0.jar
Normal file
Binary file not shown.
9
lib/org/ciyam/AT/1.4.0/AT-1.4.0.pom
Normal file
9
lib/org/ciyam/AT/1.4.0/AT-1.4.0.pom
Normal file
@ -0,0 +1,9 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<project xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 https://maven.apache.org/xsd/maven-4.0.0.xsd" xmlns="http://maven.apache.org/POM/4.0.0"
|
||||
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance">
|
||||
<modelVersion>4.0.0</modelVersion>
|
||||
<groupId>org.ciyam</groupId>
|
||||
<artifactId>AT</artifactId>
|
||||
<version>1.4.0</version>
|
||||
<description>POM was created from install:install-file</description>
|
||||
</project>
|
BIN
lib/org/ciyam/AT/1.4.1/AT-1.4.1.jar
Normal file
BIN
lib/org/ciyam/AT/1.4.1/AT-1.4.1.jar
Normal file
Binary file not shown.
123
lib/org/ciyam/AT/1.4.1/AT-1.4.1.pom
Normal file
123
lib/org/ciyam/AT/1.4.1/AT-1.4.1.pom
Normal file
@ -0,0 +1,123 @@
|
||||
<project xmlns="http://maven.apache.org/POM/4.0.0"
|
||||
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
|
||||
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
|
||||
<modelVersion>4.0.0</modelVersion>
|
||||
<groupId>org.ciyam</groupId>
|
||||
<artifactId>AT</artifactId>
|
||||
<version>1.4.1</version>
|
||||
<packaging>jar</packaging>
|
||||
|
||||
<properties>
|
||||
<project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
|
||||
<skipTests>false</skipTests>
|
||||
<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>
|
||||
<sourceDirectory>src/main/java</sourceDirectory>
|
||||
<testSourceDirectory>src/test/java</testSourceDirectory>
|
||||
<plugins>
|
||||
<plugin>
|
||||
<groupId>org.apache.maven.plugins</groupId>
|
||||
<artifactId>maven-compiler-plugin</artifactId>
|
||||
<version>${maven-compiler-plugin.version}</version>
|
||||
<configuration>
|
||||
<source>11</source>
|
||||
<target>11</target>
|
||||
</configuration>
|
||||
</plugin>
|
||||
<plugin>
|
||||
<groupId>org.apache.maven.plugins</groupId>
|
||||
<artifactId>maven-surefire-plugin</artifactId>
|
||||
<version>${maven-surefire-plugin.version}</version>
|
||||
<configuration>
|
||||
<skipTests>${skipTests}</skipTests>
|
||||
</configuration>
|
||||
</plugin>
|
||||
<plugin>
|
||||
<groupId>org.apache.maven.plugins</groupId>
|
||||
<artifactId>maven-source-plugin</artifactId>
|
||||
<version>${maven-source-plugin.version}</version>
|
||||
<executions>
|
||||
<execution>
|
||||
<id>attach-sources</id>
|
||||
<goals>
|
||||
<goal>jar</goal>
|
||||
</goals>
|
||||
</execution>
|
||||
</executions>
|
||||
</plugin>
|
||||
<plugin>
|
||||
<groupId>org.apache.maven.plugins</groupId>
|
||||
<artifactId>maven-javadoc-plugin</artifactId>
|
||||
<version>${maven-javadoc-plugin.version}</version>
|
||||
<executions>
|
||||
<execution>
|
||||
<id>attach-javadoc</id>
|
||||
<goals>
|
||||
<goal>jar</goal>
|
||||
</goals>
|
||||
</execution>
|
||||
</executions>
|
||||
</plugin>
|
||||
<plugin>
|
||||
<groupId>org.apache.maven.plugins</groupId>
|
||||
<artifactId>maven-jar-plugin</artifactId>
|
||||
<version>${maven-jar-plugin.version}</version>
|
||||
<executions>
|
||||
<execution>
|
||||
<goals>
|
||||
<goal>test-jar</goal>
|
||||
</goals>
|
||||
</execution>
|
||||
</executions>
|
||||
</plugin>
|
||||
</plugins>
|
||||
</build>
|
||||
|
||||
<dependencies>
|
||||
<dependency>
|
||||
<groupId>org.apache.maven.plugins</groupId>
|
||||
<artifactId>maven-compiler-plugin</artifactId>
|
||||
<version>${maven-compiler-plugin.version}</version>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>org.apache.maven.plugins</groupId>
|
||||
<artifactId>maven-surefire-plugin</artifactId>
|
||||
<version>${maven-surefire-plugin.version}</version>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>org.apache.maven.plugins</groupId>
|
||||
<artifactId>maven-source-plugin</artifactId>
|
||||
<version>${maven-source-plugin.version}</version>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>org.apache.maven.plugins</groupId>
|
||||
<artifactId>maven-javadoc-plugin</artifactId>
|
||||
<version>${maven-javadoc-plugin.version}</version>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>org.apache.maven.plugins</groupId>
|
||||
<artifactId>maven-jar-plugin</artifactId>
|
||||
<version>${maven-jar-plugin.version}</version>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>org.bouncycastle</groupId>
|
||||
<artifactId>bcprov-jdk15on</artifactId>
|
||||
<version>${bouncycastle.version}</version>
|
||||
<scope>test</scope>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>junit</groupId>
|
||||
<artifactId>junit</artifactId>
|
||||
<version>${junit.version}</version>
|
||||
<scope>test</scope>
|
||||
</dependency>
|
||||
</dependencies>
|
||||
</project>
|
BIN
lib/org/ciyam/AT/1.4.2/AT-1.4.2.jar
Normal file
BIN
lib/org/ciyam/AT/1.4.2/AT-1.4.2.jar
Normal file
Binary file not shown.
123
lib/org/ciyam/AT/1.4.2/AT-1.4.2.pom
Normal file
123
lib/org/ciyam/AT/1.4.2/AT-1.4.2.pom
Normal file
@ -0,0 +1,123 @@
|
||||
<project xmlns="http://maven.apache.org/POM/4.0.0"
|
||||
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
|
||||
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
|
||||
<modelVersion>4.0.0</modelVersion>
|
||||
<groupId>org.ciyam</groupId>
|
||||
<artifactId>AT</artifactId>
|
||||
<version>1.4.2</version>
|
||||
<packaging>jar</packaging>
|
||||
|
||||
<properties>
|
||||
<project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
|
||||
<skipTests>false</skipTests>
|
||||
<bouncycastle.version>1.70</bouncycastle.version>
|
||||
<junit.version>4.13.2</junit.version>
|
||||
<maven-compiler-plugin.version>3.13.0</maven-compiler-plugin.version>
|
||||
<maven-source-plugin.version>3.3.0</maven-source-plugin.version>
|
||||
<maven-javadoc-plugin.version>3.6.3</maven-javadoc-plugin.version>
|
||||
<maven-surefire-plugin.version>3.2.5</maven-surefire-plugin.version>
|
||||
<maven-jar-plugin.version>3.4.1</maven-jar-plugin.version>
|
||||
</properties>
|
||||
|
||||
<build>
|
||||
<sourceDirectory>src/main/java</sourceDirectory>
|
||||
<testSourceDirectory>src/test/java</testSourceDirectory>
|
||||
<plugins>
|
||||
<plugin>
|
||||
<groupId>org.apache.maven.plugins</groupId>
|
||||
<artifactId>maven-compiler-plugin</artifactId>
|
||||
<version>${maven-compiler-plugin.version}</version>
|
||||
<configuration>
|
||||
<source>11</source>
|
||||
<target>11</target>
|
||||
</configuration>
|
||||
</plugin>
|
||||
<plugin>
|
||||
<groupId>org.apache.maven.plugins</groupId>
|
||||
<artifactId>maven-surefire-plugin</artifactId>
|
||||
<version>${maven-surefire-plugin.version}</version>
|
||||
<configuration>
|
||||
<skipTests>${skipTests}</skipTests>
|
||||
</configuration>
|
||||
</plugin>
|
||||
<plugin>
|
||||
<groupId>org.apache.maven.plugins</groupId>
|
||||
<artifactId>maven-source-plugin</artifactId>
|
||||
<version>${maven-source-plugin.version}</version>
|
||||
<executions>
|
||||
<execution>
|
||||
<id>attach-sources</id>
|
||||
<goals>
|
||||
<goal>jar</goal>
|
||||
</goals>
|
||||
</execution>
|
||||
</executions>
|
||||
</plugin>
|
||||
<plugin>
|
||||
<groupId>org.apache.maven.plugins</groupId>
|
||||
<artifactId>maven-javadoc-plugin</artifactId>
|
||||
<version>${maven-javadoc-plugin.version}</version>
|
||||
<executions>
|
||||
<execution>
|
||||
<id>attach-javadoc</id>
|
||||
<goals>
|
||||
<goal>jar</goal>
|
||||
</goals>
|
||||
</execution>
|
||||
</executions>
|
||||
</plugin>
|
||||
<plugin>
|
||||
<groupId>org.apache.maven.plugins</groupId>
|
||||
<artifactId>maven-jar-plugin</artifactId>
|
||||
<version>${maven-jar-plugin.version}</version>
|
||||
<executions>
|
||||
<execution>
|
||||
<goals>
|
||||
<goal>test-jar</goal>
|
||||
</goals>
|
||||
</execution>
|
||||
</executions>
|
||||
</plugin>
|
||||
</plugins>
|
||||
</build>
|
||||
|
||||
<dependencies>
|
||||
<dependency>
|
||||
<groupId>org.apache.maven.plugins</groupId>
|
||||
<artifactId>maven-compiler-plugin</artifactId>
|
||||
<version>${maven-compiler-plugin.version}</version>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>org.apache.maven.plugins</groupId>
|
||||
<artifactId>maven-surefire-plugin</artifactId>
|
||||
<version>${maven-surefire-plugin.version}</version>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>org.apache.maven.plugins</groupId>
|
||||
<artifactId>maven-source-plugin</artifactId>
|
||||
<version>${maven-source-plugin.version}</version>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>org.apache.maven.plugins</groupId>
|
||||
<artifactId>maven-javadoc-plugin</artifactId>
|
||||
<version>${maven-javadoc-plugin.version}</version>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>org.apache.maven.plugins</groupId>
|
||||
<artifactId>maven-jar-plugin</artifactId>
|
||||
<version>${maven-jar-plugin.version}</version>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>org.bouncycastle</groupId>
|
||||
<artifactId>bcprov-jdk15on</artifactId>
|
||||
<version>${bouncycastle.version}</version>
|
||||
<scope>test</scope>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>junit</groupId>
|
||||
<artifactId>junit</artifactId>
|
||||
<version>${junit.version}</version>
|
||||
<scope>test</scope>
|
||||
</dependency>
|
||||
</dependencies>
|
||||
</project>
|
16
lib/org/ciyam/AT/maven-metadata-local.xml
Normal file
16
lib/org/ciyam/AT/maven-metadata-local.xml
Normal file
@ -0,0 +1,16 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<metadata>
|
||||
<groupId>org.ciyam</groupId>
|
||||
<artifactId>AT</artifactId>
|
||||
<versioning>
|
||||
<release>1.4.2</release>
|
||||
<versions>
|
||||
<version>1.3.7</version>
|
||||
<version>1.3.8</version>
|
||||
<version>1.4.0</version>
|
||||
<version>1.4.1</version>
|
||||
<version>1.4.2</version>
|
||||
</versions>
|
||||
<lastUpdated>20240426084210</lastUpdated>
|
||||
</versioning>
|
||||
</metadata>
|
@ -1,5 +0,0 @@
|
||||
This is the production hsqldb-2.7.4 with the manifest file updated
|
||||
|
||||
Sealed: false
|
||||
|
||||
Allows the addition of the custom Qortal HSQLDBPool and Monitoring Classes
|
Binary file not shown.
95
pom.xml
95
pom.xml
@ -3,20 +3,18 @@
|
||||
<modelVersion>4.0.0</modelVersion>
|
||||
<groupId>org.qortal</groupId>
|
||||
<artifactId>qortal</artifactId>
|
||||
<version>5.0.2</version>
|
||||
<version>4.6.6</version>
|
||||
<packaging>jar</packaging>
|
||||
<properties>
|
||||
<project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
|
||||
<skipJUnitTests>true</skipJUnitTests>
|
||||
<skipTests>true</skipTests>
|
||||
|
||||
<!-- <altcoinj.version>7dc8c6f</altcoinj.version> qortal github BC v15.6-->
|
||||
<altcoinj.version>d7cf6ac</altcoinj.version> <!-- BC v16 / Updated Abstract Classes / alertSigningKey -->
|
||||
<bitcoinj.version>0.16.3</bitcoinj.version>
|
||||
<bouncycastle.version>1.73</bouncycastle.version>
|
||||
<altcoinj.version>7dc8c6f</altcoinj.version>
|
||||
<bitcoinj.version>0.15.10</bitcoinj.version>
|
||||
<bouncycastle.version>1.70</bouncycastle.version>
|
||||
<build.timestamp>${maven.build.timestamp}</build.timestamp>
|
||||
<ciyam-at.version>1b731d1</ciyam-at.version> <!-- This is the hash for v1.4.3 -->
|
||||
<!-- <ciyam-at.version>1.4.3</ciyam-at.version> -->
|
||||
<commons-net.version>3.9.0</commons-net.version>
|
||||
<ciyam-at.version>1.4.2</ciyam-at.version>
|
||||
<commons-net.version>3.8.0</commons-net.version>
|
||||
<commons-text.version>1.12.0</commons-text.version>
|
||||
<commons-io.version>2.18.0</commons-io.version>
|
||||
<commons-compress.version>1.27.1</commons-compress.version>
|
||||
@ -24,7 +22,7 @@
|
||||
<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.68.3</grpc.version>
|
||||
<grpc.version>1.68.1</grpc.version>
|
||||
<guava.version>33.3.1-jre</guava.version>
|
||||
<hamcrest-library.version>2.2</hamcrest-library.version>
|
||||
<homoglyph.version>1.2.1</homoglyph.version>
|
||||
@ -34,7 +32,7 @@
|
||||
<javax.servlet-api.version>4.0.1</javax.servlet-api.version>
|
||||
<jaxb-runtime.version>2.3.9</jaxb-runtime.version>
|
||||
<jersey.version>2.42</jersey.version>
|
||||
<jetty.version>9.4.57.v20241219</jetty.version>
|
||||
<jetty.version>9.4.56.v20240826</jetty.version>
|
||||
<json-simple.version>1.1.1</json-simple.version>
|
||||
<json.version>20240303</json.version>
|
||||
<jsoup.version>1.18.1</jsoup.version>
|
||||
@ -51,14 +49,11 @@
|
||||
<maven-reproducible-build-plugin.version>0.17</maven-reproducible-build-plugin.version>
|
||||
<maven-resources-plugin.version>3.3.1</maven-resources-plugin.version>
|
||||
<maven-shade-plugin.version>3.6.0</maven-shade-plugin.version>
|
||||
<maven-install-plugin.version>3.1.3</maven-install-plugin.version>
|
||||
<maven-surefire-plugin.version>3.5.3</maven-surefire-plugin.version>
|
||||
<protobuf.version>3.25.7</protobuf.version>
|
||||
<maven-surefire-plugin.version>3.5.2</maven-surefire-plugin.version>
|
||||
<protobuf.version>3.25.3</protobuf.version>
|
||||
<replacer.version>1.5.3</replacer.version>
|
||||
<simplemagic.version>1.17</simplemagic.version>
|
||||
<slf4j.version>1.7.36</slf4j.version>
|
||||
<!-- <swagger-api.version>2.2.30</swagger-api.version> need code upgrade -->
|
||||
<!-- <swagger-api.version>2.1.13</swagger-api.version> need code upgrade -->
|
||||
<swagger-api.version>2.0.10</swagger-api.version>
|
||||
<swagger-ui.version>5.18.2</swagger-ui.version>
|
||||
<upnp.version>1.2</upnp.version>
|
||||
@ -294,48 +289,20 @@
|
||||
<groupId>org.apache.maven.plugins</groupId>
|
||||
<artifactId>maven-jar-plugin</artifactId>
|
||||
<version>${maven-jar-plugin.version}</version>
|
||||
<executions>
|
||||
<execution>
|
||||
<configuration>
|
||||
<archive>
|
||||
<manifest>
|
||||
<addDefaultEntries>false</addDefaultEntries>
|
||||
<addDefaultImplementationEntries>true</addDefaultImplementationEntries>
|
||||
</manifest>
|
||||
<manifestEntries>
|
||||
<Last-Commit-Id>${git.commit.id.full}</Last-Commit-Id>
|
||||
<Last-Commit-Time>${git.commit.time}</Last-Commit-Time>
|
||||
<Reproducible-Build>true</Reproducible-Build>
|
||||
</manifestEntries>
|
||||
</archive>
|
||||
</configuration>
|
||||
</execution>
|
||||
</executions>
|
||||
</plugin>
|
||||
<!-- Copy modified hsqldb.jar to install / modified MANIFEST.MF-->
|
||||
<plugin>
|
||||
<groupId>org.apache.maven.plugins</groupId>
|
||||
<artifactId>maven-install-plugin</artifactId>
|
||||
<version>${maven-install-plugin.version}</version>
|
||||
<configuration>
|
||||
<groupId>org.hsqldb</groupId>
|
||||
<artifactId>hsqldb</artifactId>
|
||||
<version>${hsqldb.version}</version>
|
||||
<packaging>jar</packaging>
|
||||
<archive>
|
||||
<manifest>
|
||||
<addDefaultEntries>false</addDefaultEntries>
|
||||
<addDefaultImplementationEntries>true</addDefaultImplementationEntries>
|
||||
</manifest>
|
||||
<manifestEntries>
|
||||
<Last-Commit-Id>${git.commit.id.full}</Last-Commit-Id>
|
||||
<Last-Commit-Time>${git.commit.time}</Last-Commit-Time>
|
||||
<Reproducible-Build>true</Reproducible-Build>
|
||||
</manifestEntries>
|
||||
</archive>
|
||||
</configuration>
|
||||
<executions>
|
||||
<execution>
|
||||
<phase>install</phase>
|
||||
<goals>
|
||||
<goal>install-file</goal>
|
||||
</goals>
|
||||
<configuration>
|
||||
<file>${project.basedir}/lib/org/hsqldb/hsqldb/${hsqldb.version}/hsqldb.jar</file>
|
||||
</configuration>
|
||||
</execution>
|
||||
</executions>
|
||||
</plugin>
|
||||
|
||||
<plugin>
|
||||
<groupId>org.apache.maven.plugins</groupId>
|
||||
<artifactId>maven-shade-plugin</artifactId>
|
||||
@ -385,7 +352,6 @@
|
||||
</execution>
|
||||
</executions>
|
||||
</plugin>
|
||||
<!-- Removed, now use Maven reproducible by default v4.0, IntelliJ v2025.1 and later -->
|
||||
<plugin>
|
||||
<groupId>io.github.zlika</groupId>
|
||||
<artifactId>reproducible-build-maven-plugin</artifactId>
|
||||
@ -408,7 +374,7 @@
|
||||
<artifactId>maven-surefire-plugin</artifactId>
|
||||
<version>${maven-surefire-plugin.version}</version>
|
||||
<configuration>
|
||||
<skipTests>${skipJUnitTests}</skipTests>
|
||||
<skipTests>${skipTests}</skipTests>
|
||||
</configuration>
|
||||
</plugin>
|
||||
</plugins>
|
||||
@ -484,7 +450,7 @@
|
||||
<scope>provided</scope>
|
||||
<!-- needed for build, not for runtime -->
|
||||
</dependency>
|
||||
<!-- HSQLDB for repository should use local version with Sealed: false -->
|
||||
<!-- HSQLDB for repository -->
|
||||
<dependency>
|
||||
<groupId>org.hsqldb</groupId>
|
||||
<artifactId>hsqldb</artifactId>
|
||||
@ -492,7 +458,7 @@
|
||||
</dependency>
|
||||
<!-- CIYAM AT (automated transactions) -->
|
||||
<dependency>
|
||||
<groupId>com.github.iceburst</groupId>
|
||||
<groupId>org.ciyam</groupId>
|
||||
<artifactId>AT</artifactId>
|
||||
<version>${ciyam-at.version}</version>
|
||||
</dependency>
|
||||
@ -510,7 +476,7 @@
|
||||
</dependency>
|
||||
<!-- For Litecoin, etc. support, requires bitcoinj -->
|
||||
<dependency>
|
||||
<groupId>com.github.iceburst</groupId>
|
||||
<groupId>com.github.qortal</groupId>
|
||||
<artifactId>altcoinj</artifactId>
|
||||
<version>${altcoinj.version}</version>
|
||||
</dependency>
|
||||
@ -755,12 +721,12 @@
|
||||
<!-- BouncyCastle for crypto, including TLS secure networking -->
|
||||
<dependency>
|
||||
<groupId>org.bouncycastle</groupId>
|
||||
<artifactId>bcprov-jdk15to18</artifactId>
|
||||
<artifactId>bcprov-jdk15on</artifactId>
|
||||
<version>${bouncycastle.version}</version>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>org.bouncycastle</groupId>
|
||||
<artifactId>bctls-jdk15to18</artifactId>
|
||||
<artifactId>bctls-jdk15on</artifactId>
|
||||
<version>${bouncycastle.version}</version>
|
||||
</dependency>
|
||||
<dependency>
|
||||
@ -804,10 +770,5 @@
|
||||
<artifactId>jaxb-runtime</artifactId>
|
||||
<version>${jaxb-runtime.version}</version>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>org.apache.tika</groupId>
|
||||
<artifactId>tika-core</artifactId>
|
||||
<version>3.1.0</version>
|
||||
</dependency>
|
||||
</dependencies>
|
||||
</project>
|
||||
|
@ -1,173 +0,0 @@
|
||||
package org.hsqldb.jdbc;
|
||||
|
||||
import org.apache.logging.log4j.LogManager;
|
||||
import org.apache.logging.log4j.Logger;
|
||||
import org.hsqldb.jdbc.pool.JDBCPooledConnection;
|
||||
import org.qortal.data.system.DbConnectionInfo;
|
||||
import org.qortal.repository.hsqldb.HSQLDBRepositoryFactory;
|
||||
|
||||
import javax.sql.ConnectionEvent;
|
||||
import javax.sql.PooledConnection;
|
||||
import java.sql.Connection;
|
||||
import java.sql.SQLException;
|
||||
import java.util.Comparator;
|
||||
import java.util.List;
|
||||
import java.util.concurrent.ConcurrentHashMap;
|
||||
import java.util.stream.Collectors;
|
||||
|
||||
/**
|
||||
* Class HSQLDBPoolMonitored
|
||||
*
|
||||
* This class uses the same logic as HSQLDBPool. The only difference is it monitors the state of every connection
|
||||
* to the database. This is used for debugging purposes only.
|
||||
*/
|
||||
public class HSQLDBPoolMonitored extends HSQLDBPool {
|
||||
|
||||
private static final Logger LOGGER = LogManager.getLogger(HSQLDBRepositoryFactory.class);
|
||||
|
||||
private static final String EMPTY = "Empty";
|
||||
private static final String AVAILABLE = "Available";
|
||||
private static final String ALLOCATED = "Allocated";
|
||||
|
||||
private ConcurrentHashMap<Integer, DbConnectionInfo> infoByIndex;
|
||||
|
||||
public HSQLDBPoolMonitored(int poolSize) {
|
||||
super(poolSize);
|
||||
|
||||
this.infoByIndex = new ConcurrentHashMap<>(poolSize);
|
||||
}
|
||||
|
||||
/**
|
||||
* Tries to retrieve a new connection using the properties that have already been
|
||||
* set.
|
||||
*
|
||||
* @return a connection to the data source, or null if no spare connections in pool
|
||||
* @exception SQLException if a database access error occurs
|
||||
*/
|
||||
public Connection tryConnection() throws SQLException {
|
||||
for (int i = 0; i < states.length(); i++) {
|
||||
if (states.compareAndSet(i, RefState.available, RefState.allocated)) {
|
||||
JDBCPooledConnection pooledConnection = connections[i];
|
||||
|
||||
if (pooledConnection == null)
|
||||
// Probably shutdown situation
|
||||
return null;
|
||||
|
||||
infoByIndex.put(i, new DbConnectionInfo(System.currentTimeMillis(), Thread.currentThread().getName(), ALLOCATED));
|
||||
|
||||
return pooledConnection.getConnection();
|
||||
}
|
||||
|
||||
if (states.compareAndSet(i, RefState.empty, RefState.allocated)) {
|
||||
try {
|
||||
JDBCPooledConnection pooledConnection = (JDBCPooledConnection) source.getPooledConnection();
|
||||
|
||||
if (pooledConnection == null)
|
||||
// Probably shutdown situation
|
||||
return null;
|
||||
|
||||
pooledConnection.addConnectionEventListener(this);
|
||||
pooledConnection.addStatementEventListener(this);
|
||||
connections[i] = pooledConnection;
|
||||
|
||||
infoByIndex.put(i, new DbConnectionInfo(System.currentTimeMillis(), Thread.currentThread().getName(), ALLOCATED));
|
||||
|
||||
return pooledConnection.getConnection();
|
||||
} catch (SQLException e) {
|
||||
states.set(i, RefState.empty);
|
||||
infoByIndex.put(i, new DbConnectionInfo(System.currentTimeMillis(), Thread.currentThread().getName(), EMPTY));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
public Connection getConnection() throws SQLException {
|
||||
int var1 = 300;
|
||||
if (this.source.loginTimeout != 0) {
|
||||
var1 = this.source.loginTimeout * 10;
|
||||
}
|
||||
|
||||
if (this.closed) {
|
||||
throw new SQLException("connection pool is closed");
|
||||
} else {
|
||||
for(int var2 = 0; var2 < var1; ++var2) {
|
||||
for(int var3 = 0; var3 < this.states.length(); ++var3) {
|
||||
if (this.states.compareAndSet(var3, 1, 2)) {
|
||||
infoByIndex.put(var3, new DbConnectionInfo(System.currentTimeMillis(), Thread.currentThread().getName(), ALLOCATED));
|
||||
return this.connections[var3].getConnection();
|
||||
}
|
||||
|
||||
if (this.states.compareAndSet(var3, 0, 2)) {
|
||||
try {
|
||||
JDBCPooledConnection var4 = (JDBCPooledConnection)this.source.getPooledConnection();
|
||||
var4.addConnectionEventListener(this);
|
||||
var4.addStatementEventListener(this);
|
||||
this.connections[var3] = var4;
|
||||
|
||||
infoByIndex.put(var3, new DbConnectionInfo(System.currentTimeMillis(), Thread.currentThread().getName(), ALLOCATED));
|
||||
|
||||
return this.connections[var3].getConnection();
|
||||
} catch (SQLException var6) {
|
||||
this.states.set(var3, 0);
|
||||
infoByIndex.put(var3, new DbConnectionInfo(System.currentTimeMillis(), Thread.currentThread().getName(), EMPTY));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
try {
|
||||
Thread.sleep(100L);
|
||||
} catch (InterruptedException var5) {
|
||||
}
|
||||
}
|
||||
|
||||
throw JDBCUtil.invalidArgument();
|
||||
}
|
||||
}
|
||||
|
||||
public void connectionClosed(ConnectionEvent event) {
|
||||
PooledConnection connection = (PooledConnection) event.getSource();
|
||||
|
||||
for (int i = 0; i < connections.length; i++) {
|
||||
if (connections[i] == connection) {
|
||||
states.set(i, RefState.available);
|
||||
infoByIndex.put(i, new DbConnectionInfo(System.currentTimeMillis(), Thread.currentThread().getName(), AVAILABLE));
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public void connectionErrorOccurred(ConnectionEvent event) {
|
||||
PooledConnection connection = (PooledConnection) event.getSource();
|
||||
|
||||
for (int i = 0; i < connections.length; i++) {
|
||||
if (connections[i] == connection) {
|
||||
states.set(i, RefState.allocated);
|
||||
connections[i] = null;
|
||||
states.set(i, RefState.empty);
|
||||
infoByIndex.put(i, new DbConnectionInfo(System.currentTimeMillis(), Thread.currentThread().getName(), EMPTY));
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public List<DbConnectionInfo> getDbConnectionsStates() {
|
||||
|
||||
return infoByIndex.values().stream()
|
||||
.sorted(Comparator.comparingLong(DbConnectionInfo::getUpdated))
|
||||
.collect(Collectors.toList());
|
||||
}
|
||||
|
||||
private int findConnectionIndex(ConnectionEvent connectionEvent) {
|
||||
PooledConnection pooledConnection = (PooledConnection) connectionEvent.getSource();
|
||||
|
||||
for(int i = 0; i < this.connections.length; ++i) {
|
||||
if (this.connections[i] == pooledConnection) {
|
||||
return i;
|
||||
}
|
||||
}
|
||||
|
||||
return -1;
|
||||
}
|
||||
}
|
@ -2,30 +2,23 @@ package org.qortal.account;
|
||||
|
||||
import org.apache.logging.log4j.LogManager;
|
||||
import org.apache.logging.log4j.Logger;
|
||||
import org.qortal.api.resource.TransactionsResource;
|
||||
import org.qortal.block.BlockChain;
|
||||
import org.qortal.controller.LiteNode;
|
||||
import org.qortal.data.account.AccountBalanceData;
|
||||
import org.qortal.data.account.AccountData;
|
||||
import org.qortal.data.account.RewardShareData;
|
||||
import org.qortal.data.naming.NameData;
|
||||
import org.qortal.data.transaction.TransactionData;
|
||||
import org.qortal.repository.DataException;
|
||||
import org.qortal.repository.GroupRepository;
|
||||
import org.qortal.repository.NameRepository;
|
||||
import org.qortal.repository.Repository;
|
||||
import org.qortal.settings.Settings;
|
||||
import org.qortal.utils.Base58;
|
||||
import org.qortal.utils.Groups;
|
||||
|
||||
import javax.xml.bind.annotation.XmlAccessType;
|
||||
import javax.xml.bind.annotation.XmlAccessorType;
|
||||
|
||||
import java.util.Comparator;
|
||||
import java.util.HashMap;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.Optional;
|
||||
|
||||
import static org.qortal.utils.Amounts.prettyAmount;
|
||||
|
||||
@ -223,18 +216,9 @@ public class Account {
|
||||
String myAddress = accountData.getAddress();
|
||||
|
||||
int blockchainHeight = this.repository.getBlockRepository().getBlockchainHeight();
|
||||
|
||||
int levelToMint;
|
||||
|
||||
if( blockchainHeight >= BlockChain.getInstance().getIgnoreLevelForRewardShareHeight() ) {
|
||||
levelToMint = 0;
|
||||
}
|
||||
else {
|
||||
levelToMint = BlockChain.getInstance().getMinAccountLevelToMint();
|
||||
}
|
||||
|
||||
int levelToMint = BlockChain.getInstance().getMinAccountLevelToMint();
|
||||
int level = accountData.getLevel();
|
||||
List<Integer> groupIdsToMint = Groups.getGroupIdsToMint( BlockChain.getInstance(), blockchainHeight );
|
||||
int groupIdToMint = BlockChain.getInstance().getMintingGroupId();
|
||||
int nameCheckHeight = BlockChain.getInstance().getOnlyMintWithNameHeight();
|
||||
int groupCheckHeight = BlockChain.getInstance().getGroupMemberCheckHeight();
|
||||
int removeNameCheckHeight = BlockChain.getInstance().getRemoveOnlyMintWithNameHeight();
|
||||
@ -268,9 +252,9 @@ public class Account {
|
||||
if (blockchainHeight >= groupCheckHeight && blockchainHeight < removeNameCheckHeight) {
|
||||
List<NameData> myName = nameRepository.getNamesByOwner(myAddress);
|
||||
if (Account.isFounder(accountData.getFlags())) {
|
||||
return accountData.getBlocksMintedPenalty() == 0 && !myName.isEmpty() && (isGroupValidated || Groups.memberExistsInAnyGroup(groupRepository, groupIdsToMint, myAddress));
|
||||
return accountData.getBlocksMintedPenalty() == 0 && !myName.isEmpty() && (isGroupValidated || groupRepository.memberExists(groupIdToMint, myAddress));
|
||||
} else {
|
||||
return level >= levelToMint && !myName.isEmpty() && (isGroupValidated || Groups.memberExistsInAnyGroup(groupRepository, groupIdsToMint, myAddress));
|
||||
return level >= levelToMint && !myName.isEmpty() && (isGroupValidated || groupRepository.memberExists(groupIdToMint, myAddress));
|
||||
}
|
||||
}
|
||||
|
||||
@ -279,9 +263,9 @@ public class Account {
|
||||
// Account's address is a member of the minter group
|
||||
if (blockchainHeight >= removeNameCheckHeight) {
|
||||
if (Account.isFounder(accountData.getFlags())) {
|
||||
return accountData.getBlocksMintedPenalty() == 0 && (isGroupValidated || Groups.memberExistsInAnyGroup(groupRepository, groupIdsToMint, myAddress));
|
||||
return accountData.getBlocksMintedPenalty() == 0 && (isGroupValidated || groupRepository.memberExists(groupIdToMint, myAddress));
|
||||
} else {
|
||||
return level >= levelToMint && (isGroupValidated || Groups.memberExistsInAnyGroup(groupRepository, groupIdsToMint, myAddress));
|
||||
return level >= levelToMint && (isGroupValidated || groupRepository.memberExists(groupIdToMint, myAddress));
|
||||
}
|
||||
}
|
||||
|
||||
@ -322,9 +306,6 @@ public class Account {
|
||||
if (Account.isFounder(accountData.getFlags()) && accountData.getBlocksMintedPenalty() == 0)
|
||||
return true;
|
||||
|
||||
if( this.repository.getBlockRepository().getBlockchainHeight() >= BlockChain.getInstance().getIgnoreLevelForRewardShareHeight() )
|
||||
return true;
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
@ -367,142 +348,6 @@ public class Account {
|
||||
return accountData.getLevel();
|
||||
}
|
||||
|
||||
/**
|
||||
* Get Primary Name
|
||||
*
|
||||
* @return the primary name for this address if present, otherwise empty
|
||||
*
|
||||
* @throws DataException
|
||||
*/
|
||||
public Optional<String> getPrimaryName() throws DataException {
|
||||
|
||||
return this.repository.getNameRepository().getPrimaryName(this.address);
|
||||
}
|
||||
|
||||
/**
|
||||
* Remove Primary Name
|
||||
*
|
||||
* @throws DataException
|
||||
*/
|
||||
public void removePrimaryName() throws DataException {
|
||||
this.repository.getNameRepository().removePrimaryName(this.address);
|
||||
}
|
||||
|
||||
/**
|
||||
* Reset Primary Name
|
||||
*
|
||||
* Set primary name based on the names (and their history) this account owns.
|
||||
*
|
||||
* @param confirmationStatus the status of the transactions for the determining the primary name
|
||||
*
|
||||
* @return the primary name, empty if their isn't one
|
||||
*
|
||||
* @throws DataException
|
||||
*/
|
||||
public Optional<String> resetPrimaryName(TransactionsResource.ConfirmationStatus confirmationStatus) throws DataException {
|
||||
Optional<String> primaryName = determinePrimaryName(confirmationStatus);
|
||||
|
||||
if(primaryName.isPresent()) {
|
||||
return setPrimaryName(primaryName.get());
|
||||
}
|
||||
else {
|
||||
return primaryName;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Determine Primary Name
|
||||
*
|
||||
* Determine primary name based on a list of registered names.
|
||||
*
|
||||
* @param confirmationStatus the status of the transactions for this determination
|
||||
*
|
||||
* @return the primary name, empty if there is no primary name
|
||||
*
|
||||
* @throws DataException
|
||||
*/
|
||||
public Optional<String> determinePrimaryName(TransactionsResource.ConfirmationStatus confirmationStatus) throws DataException {
|
||||
|
||||
// all registered names for the owner
|
||||
List<NameData> names = this.repository.getNameRepository().getNamesByOwner(this.address);
|
||||
|
||||
Optional<String> primaryName;
|
||||
|
||||
// if no registered names, the no primary name possible
|
||||
if (names.isEmpty()) {
|
||||
primaryName = Optional.empty();
|
||||
}
|
||||
// if names
|
||||
else {
|
||||
// if one name, then that is the primary name
|
||||
if (names.size() == 1) {
|
||||
primaryName = Optional.of( names.get(0).getName() );
|
||||
}
|
||||
// if more than one name, then seek the earliest name acquisition that was never released
|
||||
else {
|
||||
Map<String, TransactionData> txByName = new HashMap<>(names.size());
|
||||
|
||||
// for each name, get the latest transaction
|
||||
for (NameData nameData : names) {
|
||||
|
||||
// since the name is currently registered to the owner,
|
||||
// we assume the latest transaction involving this name was the transaction that the acquired
|
||||
// name through registration, purchase or update
|
||||
Optional<TransactionData> latestTransaction
|
||||
= this.repository
|
||||
.getTransactionRepository()
|
||||
.getTransactionsInvolvingName(
|
||||
nameData.getName(),
|
||||
confirmationStatus
|
||||
)
|
||||
.stream()
|
||||
.sorted(Comparator.comparing(
|
||||
TransactionData::getTimestamp).reversed()
|
||||
)
|
||||
.findFirst(); // first is the last, since it was reversed
|
||||
|
||||
// if there is a latest transaction, expected for all registered names
|
||||
if (latestTransaction.isPresent()) {
|
||||
txByName.put(nameData.getName(), latestTransaction.get());
|
||||
}
|
||||
// if there is no latest transaction, then
|
||||
else {
|
||||
LOGGER.warn("No matching transaction for name: " + nameData.getName());
|
||||
}
|
||||
}
|
||||
|
||||
// get the first name aqcuistion for this address
|
||||
Optional<Map.Entry<String, TransactionData>> firstNameEntry
|
||||
= txByName.entrySet().stream().sorted(Comparator.comparing(entry -> entry.getValue().getTimestamp())).findFirst();
|
||||
|
||||
// if their is a name acquisition, then the first one is the primary name
|
||||
if (firstNameEntry.isPresent()) {
|
||||
primaryName = Optional.of( firstNameEntry.get().getKey() );
|
||||
}
|
||||
// if there is no nameacquistion, then there is no primary name
|
||||
else {
|
||||
primaryName = Optional.empty();
|
||||
}
|
||||
}
|
||||
}
|
||||
return primaryName;
|
||||
}
|
||||
|
||||
/**
|
||||
* Set Primary Name
|
||||
*
|
||||
* @param primaryName the primary to set to this address
|
||||
*
|
||||
* @return the primary name if successful, empty if unsuccessful
|
||||
*
|
||||
* @throws DataException
|
||||
*/
|
||||
public Optional<String> setPrimaryName( String primaryName ) throws DataException {
|
||||
int changed = this.repository.getNameRepository().setPrimaryName(this.address, primaryName);
|
||||
|
||||
return changed > 0 ? Optional.of(primaryName) : Optional.empty();
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns reward-share minting address, or unknown if reward-share does not exist.
|
||||
*
|
||||
|
@ -1,41 +1,17 @@
|
||||
package org.qortal.account;
|
||||
|
||||
import org.bouncycastle.crypto.generators.Ed25519KeyPairGenerator;
|
||||
import org.bouncycastle.crypto.params.Ed25519KeyGenerationParameters;
|
||||
import org.bouncycastle.crypto.params.Ed25519PublicKeyParameters;
|
||||
import org.qortal.crypto.Crypto;
|
||||
import org.qortal.data.account.AccountData;
|
||||
import org.qortal.repository.Repository;
|
||||
|
||||
import java.security.SecureRandom;
|
||||
|
||||
public class PublicKeyAccount extends Account {
|
||||
|
||||
protected final byte[] publicKey;
|
||||
protected final Ed25519PublicKeyParameters edPublicKeyParams;
|
||||
|
||||
/** <p>Constructor for generating a PublicKeyAccount</p>
|
||||
*
|
||||
* @param repository Block Chain
|
||||
* @param publicKey 32 byte Public Key
|
||||
* @since v4.7.3
|
||||
*/
|
||||
public PublicKeyAccount(Repository repository, byte[] publicKey) {
|
||||
super(repository, Crypto.toAddress(publicKey));
|
||||
|
||||
Ed25519PublicKeyParameters t = null;
|
||||
try {
|
||||
t = new Ed25519PublicKeyParameters(publicKey, 0);
|
||||
} catch (Exception e) {
|
||||
var gen = new Ed25519KeyPairGenerator();
|
||||
gen.init(new Ed25519KeyGenerationParameters(new SecureRandom()));
|
||||
var keyPair = gen.generateKeyPair();
|
||||
t = (Ed25519PublicKeyParameters) keyPair.getPublic();
|
||||
} finally {
|
||||
this.edPublicKeyParams = t;
|
||||
}
|
||||
|
||||
this.publicKey = publicKey;
|
||||
this(repository, new Ed25519PublicKeyParameters(publicKey, 0));
|
||||
}
|
||||
|
||||
protected PublicKeyAccount(Repository repository, Ed25519PublicKeyParameters edPublicKeyParams) {
|
||||
|
@ -46,7 +46,6 @@ public class ApiService {
|
||||
private ApiService() {
|
||||
this.config = new ResourceConfig();
|
||||
this.config.packages("org.qortal.api.resource", "org.qortal.api.restricted.resource");
|
||||
this.config.register(org.glassfish.jersey.media.multipart.MultiPartFeature.class);
|
||||
this.config.register(OpenApiResource.class);
|
||||
this.config.register(ApiDefinition.class);
|
||||
this.config.register(AnnotationPostProcessor.class);
|
||||
@ -195,10 +194,8 @@ public class ApiService {
|
||||
|
||||
context.addServlet(AdminStatusWebSocket.class, "/websockets/admin/status");
|
||||
context.addServlet(BlocksWebSocket.class, "/websockets/blocks");
|
||||
context.addServlet(DataMonitorSocket.class, "/websockets/datamonitor");
|
||||
context.addServlet(ActiveChatsWebSocket.class, "/websockets/chat/active/*");
|
||||
context.addServlet(ChatMessagesWebSocket.class, "/websockets/chat/messages");
|
||||
context.addServlet(UnsignedFeesSocket.class, "/websockets/crosschain/unsignedfees");
|
||||
context.addServlet(TradeOffersWebSocket.class, "/websockets/crosschain/tradeoffers");
|
||||
context.addServlet(TradeBotWebSocket.class, "/websockets/crosschain/tradebot");
|
||||
context.addServlet(TradePresenceWebSocket.class, "/websockets/crosschain/tradepresence");
|
||||
|
@ -40,7 +40,6 @@ public class DevProxyService {
|
||||
private DevProxyService() {
|
||||
this.config = new ResourceConfig();
|
||||
this.config.packages("org.qortal.api.proxy.resource", "org.qortal.api.resource");
|
||||
this.config.register(org.glassfish.jersey.media.multipart.MultiPartFeature.class);
|
||||
this.config.register(OpenApiResource.class);
|
||||
this.config.register(ApiDefinition.class);
|
||||
this.config.register(AnnotationPostProcessor.class);
|
||||
|
@ -39,7 +39,6 @@ public class DomainMapService {
|
||||
private DomainMapService() {
|
||||
this.config = new ResourceConfig();
|
||||
this.config.packages("org.qortal.api.resource", "org.qortal.api.domainmap.resource");
|
||||
this.config.register(org.glassfish.jersey.media.multipart.MultiPartFeature.class);
|
||||
this.config.register(OpenApiResource.class);
|
||||
this.config.register(ApiDefinition.class);
|
||||
this.config.register(AnnotationPostProcessor.class);
|
||||
|
@ -39,7 +39,6 @@ public class GatewayService {
|
||||
private GatewayService() {
|
||||
this.config = new ResourceConfig();
|
||||
this.config.packages("org.qortal.api.resource", "org.qortal.api.gateway.resource");
|
||||
this.config.register(org.glassfish.jersey.media.multipart.MultiPartFeature.class);
|
||||
this.config.register(OpenApiResource.class);
|
||||
this.config.register(ApiDefinition.class);
|
||||
this.config.register(AnnotationPostProcessor.class);
|
||||
|
@ -1,13 +1,14 @@
|
||||
package org.qortal.api;
|
||||
|
||||
|
||||
import org.apache.logging.log4j.LogManager;
|
||||
import org.apache.logging.log4j.Logger;
|
||||
import org.jsoup.Jsoup;
|
||||
import org.jsoup.nodes.Document;
|
||||
import org.jsoup.select.Elements;
|
||||
import org.qortal.arbitrary.misc.Service;
|
||||
|
||||
import java.util.Objects;
|
||||
|
||||
public class HTMLParser {
|
||||
|
||||
private static final Logger LOGGER = LogManager.getLogger(HTMLParser.class);
|
||||
@ -21,11 +22,10 @@ public class HTMLParser {
|
||||
private String identifier;
|
||||
private String path;
|
||||
private String theme;
|
||||
private String lang;
|
||||
private boolean usingCustomRouting;
|
||||
|
||||
public HTMLParser(String resourceId, String inPath, String prefix, boolean includeResourceIdInPrefix, byte[] data,
|
||||
String qdnContext, Service service, String identifier, String theme, boolean usingCustomRouting, String lang) {
|
||||
String qdnContext, Service service, String identifier, String theme, boolean usingCustomRouting) {
|
||||
String inPathWithoutFilename = inPath.contains("/") ? inPath.substring(0, inPath.lastIndexOf('/')) : String.format("/%s",inPath);
|
||||
this.qdnBase = includeResourceIdInPrefix ? String.format("%s/%s", prefix, resourceId) : prefix;
|
||||
this.qdnBaseWithPath = includeResourceIdInPrefix ? String.format("%s/%s%s", prefix, resourceId, inPathWithoutFilename) : String.format("%s%s", prefix, inPathWithoutFilename);
|
||||
@ -36,7 +36,6 @@ public class HTMLParser {
|
||||
this.identifier = identifier;
|
||||
this.path = inPath;
|
||||
this.theme = theme;
|
||||
this.lang = lang;
|
||||
this.usingCustomRouting = usingCustomRouting;
|
||||
}
|
||||
|
||||
@ -62,13 +61,9 @@ public class HTMLParser {
|
||||
String identifier = this.identifier != null ? this.identifier.replace("\\", "").replace("\"","\\\"") : "";
|
||||
String path = this.path != null ? this.path.replace("\\", "").replace("\"","\\\"") : "";
|
||||
String theme = this.theme != null ? this.theme.replace("\\", "").replace("\"","\\\"") : "";
|
||||
String lang = this.lang != null ? this.lang.replace("\\", "").replace("\"", "\\\"") : "";
|
||||
String qdnBase = this.qdnBase != null ? this.qdnBase.replace("\\", "").replace("\"","\\\"") : "";
|
||||
String qdnBaseWithPath = this.qdnBaseWithPath != null ? this.qdnBaseWithPath.replace("\\", "").replace("\"","\\\"") : "";
|
||||
String qdnContextVar = String.format(
|
||||
"<script>var _qdnContext=\"%s\"; var _qdnTheme=\"%s\"; var _qdnLang=\"%s\"; var _qdnService=\"%s\"; var _qdnName=\"%s\"; var _qdnIdentifier=\"%s\"; var _qdnPath=\"%s\"; var _qdnBase=\"%s\"; var _qdnBaseWithPath=\"%s\";</script>",
|
||||
qdnContext, theme, lang, service, name, identifier, path, qdnBase, qdnBaseWithPath
|
||||
);
|
||||
String qdnContextVar = String.format("<script>var _qdnContext=\"%s\"; var _qdnTheme=\"%s\"; var _qdnService=\"%s\"; var _qdnName=\"%s\"; var _qdnIdentifier=\"%s\"; var _qdnPath=\"%s\"; var _qdnBase=\"%s\"; var _qdnBaseWithPath=\"%s\";</script>", qdnContext, theme, service, name, identifier, path, qdnBase, qdnBaseWithPath);
|
||||
head.get(0).prepend(qdnContextVar);
|
||||
|
||||
// Add base href tag
|
||||
|
@ -1,72 +0,0 @@
|
||||
package org.qortal.api.model;
|
||||
|
||||
import io.swagger.v3.oas.annotations.media.Schema;
|
||||
import org.qortal.data.crosschain.CrossChainTradeData;
|
||||
|
||||
import javax.xml.bind.annotation.XmlAccessType;
|
||||
import javax.xml.bind.annotation.XmlAccessorType;
|
||||
import javax.xml.bind.annotation.adapters.XmlJavaTypeAdapter;
|
||||
|
||||
// All properties to be converted to JSON via JAXB
|
||||
@XmlAccessorType(XmlAccessType.FIELD)
|
||||
public class CrossChainTradeLedgerEntry {
|
||||
|
||||
private String market;
|
||||
|
||||
private String currency;
|
||||
|
||||
@XmlJavaTypeAdapter(value = org.qortal.api.AmountTypeAdapter.class)
|
||||
private long quantity;
|
||||
|
||||
@XmlJavaTypeAdapter(value = org.qortal.api.AmountTypeAdapter.class)
|
||||
private long feeAmount;
|
||||
|
||||
private String feeCurrency;
|
||||
|
||||
@XmlJavaTypeAdapter(value = org.qortal.api.AmountTypeAdapter.class)
|
||||
private long totalPrice;
|
||||
|
||||
private long tradeTimestamp;
|
||||
|
||||
protected CrossChainTradeLedgerEntry() {
|
||||
/* For JAXB */
|
||||
}
|
||||
|
||||
public CrossChainTradeLedgerEntry(String market, String currency, long quantity, long feeAmount, String feeCurrency, long totalPrice, long tradeTimestamp) {
|
||||
this.market = market;
|
||||
this.currency = currency;
|
||||
this.quantity = quantity;
|
||||
this.feeAmount = feeAmount;
|
||||
this.feeCurrency = feeCurrency;
|
||||
this.totalPrice = totalPrice;
|
||||
this.tradeTimestamp = tradeTimestamp;
|
||||
}
|
||||
|
||||
public String getMarket() {
|
||||
return market;
|
||||
}
|
||||
|
||||
public String getCurrency() {
|
||||
return currency;
|
||||
}
|
||||
|
||||
public long getQuantity() {
|
||||
return quantity;
|
||||
}
|
||||
|
||||
public long getFeeAmount() {
|
||||
return feeAmount;
|
||||
}
|
||||
|
||||
public String getFeeCurrency() {
|
||||
return feeCurrency;
|
||||
}
|
||||
|
||||
public long getTotalPrice() {
|
||||
return totalPrice;
|
||||
}
|
||||
|
||||
public long getTradeTimestamp() {
|
||||
return tradeTimestamp;
|
||||
}
|
||||
}
|
@ -1,50 +0,0 @@
|
||||
package org.qortal.api.model;
|
||||
|
||||
import javax.xml.bind.annotation.XmlAccessType;
|
||||
import javax.xml.bind.annotation.XmlAccessorType;
|
||||
import java.util.Objects;
|
||||
|
||||
// All properties to be converted to JSON via JAXB
|
||||
@XmlAccessorType(XmlAccessType.FIELD)
|
||||
public class DatasetStatus {
|
||||
|
||||
private String name;
|
||||
|
||||
private long count;
|
||||
|
||||
public DatasetStatus() {}
|
||||
|
||||
public DatasetStatus(String name, long count) {
|
||||
this.name = name;
|
||||
this.count = count;
|
||||
}
|
||||
|
||||
public String getName() {
|
||||
return name;
|
||||
}
|
||||
|
||||
public long getCount() {
|
||||
return count;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean equals(Object o) {
|
||||
if (this == o) return true;
|
||||
if (o == null || getClass() != o.getClass()) return false;
|
||||
DatasetStatus that = (DatasetStatus) o;
|
||||
return count == that.count && Objects.equals(name, that.name);
|
||||
}
|
||||
|
||||
@Override
|
||||
public int hashCode() {
|
||||
return Objects.hash(name, count);
|
||||
}
|
||||
|
||||
@Override
|
||||
public String toString() {
|
||||
return "DatasetStatus{" +
|
||||
"name='" + name + '\'' +
|
||||
", count=" + count +
|
||||
'}';
|
||||
}
|
||||
}
|
@ -304,11 +304,11 @@ public class BitcoinyTBDRequest {
|
||||
private String networkName;
|
||||
|
||||
/**
|
||||
* Fee Required
|
||||
* Fee Ceiling
|
||||
*
|
||||
* web search, LTC fee required = 1000L
|
||||
* web search, LTC fee ceiling = 1000L
|
||||
*/
|
||||
private long feeRequired;
|
||||
private long feeCeiling;
|
||||
|
||||
/**
|
||||
* Extended Public Key
|
||||
@ -570,8 +570,8 @@ public class BitcoinyTBDRequest {
|
||||
return this.networkName;
|
||||
}
|
||||
|
||||
public long getFeeRequired() {
|
||||
return this.feeRequired;
|
||||
public long getFeeCeiling() {
|
||||
return this.feeCeiling;
|
||||
}
|
||||
|
||||
public String getExtendedPublicKey() {
|
||||
@ -671,7 +671,7 @@ public class BitcoinyTBDRequest {
|
||||
", minimumOrderAmount=" + minimumOrderAmount +
|
||||
", feePerKb=" + feePerKb +
|
||||
", networkName='" + networkName + '\'' +
|
||||
", feeRequired=" + feeRequired +
|
||||
", feeCeiling=" + feeCeiling +
|
||||
", extendedPublicKey='" + extendedPublicKey + '\'' +
|
||||
", sendAmount=" + sendAmount +
|
||||
", sendingFeePerByte=" + sendingFeePerByte +
|
||||
|
@ -142,20 +142,10 @@ public class DevProxyServerResource {
|
||||
}
|
||||
}
|
||||
|
||||
String lang = request.getParameter("lang");
|
||||
if (lang == null || lang.isBlank()) {
|
||||
lang = "en"; // fallback
|
||||
}
|
||||
|
||||
String theme = request.getParameter("theme");
|
||||
if (theme == null || theme.isBlank()) {
|
||||
theme = "light";
|
||||
}
|
||||
|
||||
// Parse and modify output if needed
|
||||
if (HTMLParser.isHtmlFile(filename)) {
|
||||
// HTML file - needs to be parsed
|
||||
HTMLParser htmlParser = new HTMLParser("", inPath, "", false, data, "proxy", Service.APP, null, theme , true, lang);
|
||||
HTMLParser htmlParser = new HTMLParser("", inPath, "", false, data, "proxy", Service.APP, null, "light", true);
|
||||
htmlParser.addAdditionalHeaderTags();
|
||||
response.addHeader("Content-Security-Policy", "default-src 'self' 'unsafe-inline' 'unsafe-eval'; media-src 'self' data: blob:; img-src 'self' data: blob:; connect-src 'self' ws:; font-src 'self' data:;");
|
||||
response.setContentType(con.getContentType());
|
||||
|
@ -3,7 +3,6 @@ package org.qortal.api.resource;
|
||||
import com.google.common.primitives.Bytes;
|
||||
import com.j256.simplemagic.ContentInfo;
|
||||
import com.j256.simplemagic.ContentInfoUtil;
|
||||
|
||||
import io.swagger.v3.oas.annotations.Operation;
|
||||
import io.swagger.v3.oas.annotations.Parameter;
|
||||
import io.swagger.v3.oas.annotations.media.ArraySchema;
|
||||
@ -13,7 +12,6 @@ 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.apache.commons.io.FileUtils;
|
||||
import org.apache.commons.lang3.ArrayUtils;
|
||||
import org.apache.logging.log4j.LogManager;
|
||||
@ -35,13 +33,9 @@ import org.qortal.controller.arbitrary.ArbitraryDataStorageManager;
|
||||
import org.qortal.controller.arbitrary.ArbitraryMetadataManager;
|
||||
import org.qortal.data.account.AccountData;
|
||||
import org.qortal.data.arbitrary.ArbitraryCategoryInfo;
|
||||
import org.qortal.data.arbitrary.ArbitraryDataIndexDetail;
|
||||
import org.qortal.data.arbitrary.ArbitraryDataIndexScoreKey;
|
||||
import org.qortal.data.arbitrary.ArbitraryDataIndexScorecard;
|
||||
import org.qortal.data.arbitrary.ArbitraryResourceData;
|
||||
import org.qortal.data.arbitrary.ArbitraryResourceMetadata;
|
||||
import org.qortal.data.arbitrary.ArbitraryResourceStatus;
|
||||
import org.qortal.data.arbitrary.IndexCache;
|
||||
import org.qortal.data.naming.NameData;
|
||||
import org.qortal.data.transaction.ArbitraryTransactionData;
|
||||
import org.qortal.data.transaction.TransactionData;
|
||||
@ -65,36 +59,18 @@ import javax.servlet.http.HttpServletResponse;
|
||||
import javax.ws.rs.*;
|
||||
import javax.ws.rs.core.Context;
|
||||
import javax.ws.rs.core.MediaType;
|
||||
|
||||
import java.io.BufferedWriter;
|
||||
import java.io.File;
|
||||
import java.io.FileWriter;
|
||||
import java.io.IOException;
|
||||
import java.io.InputStream;
|
||||
import java.io.OutputStream;
|
||||
import java.net.FileNameMap;
|
||||
import java.net.URLConnection;
|
||||
import java.nio.file.Files;
|
||||
import java.nio.file.Paths;
|
||||
import java.nio.file.StandardCopyOption;
|
||||
import java.nio.file.StandardOpenOption;
|
||||
import java.util.ArrayList;
|
||||
import java.util.Arrays;
|
||||
import java.util.Comparator;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.Objects;
|
||||
import java.util.stream.Collectors;
|
||||
import java.util.zip.GZIPOutputStream;
|
||||
|
||||
import org.apache.tika.Tika;
|
||||
import org.apache.tika.mime.MimeTypeException;
|
||||
import org.apache.tika.mime.MimeTypes;
|
||||
|
||||
import javax.ws.rs.core.Response;
|
||||
|
||||
import org.glassfish.jersey.media.multipart.FormDataParam;
|
||||
import static org.qortal.api.ApiError.REPOSITORY_ISSUE;
|
||||
|
||||
@Path("/arbitrary")
|
||||
@Tag(name = "Arbitrary")
|
||||
@ -196,7 +172,6 @@ public class ArbitraryResource {
|
||||
@Parameter(description = "Name (searches name field only)") @QueryParam("name") List<String> names,
|
||||
@Parameter(description = "Title (searches title metadata field only)") @QueryParam("title") String title,
|
||||
@Parameter(description = "Description (searches description metadata field only)") @QueryParam("description") String description,
|
||||
@Parameter(description = "Keyword (searches description metadata field by keywords)") @QueryParam("keywords") List<String> keywords,
|
||||
@Parameter(description = "Prefix only (if true, only the beginning of fields are matched)") @QueryParam("prefix") Boolean prefixOnly,
|
||||
@Parameter(description = "Exact match names only (if true, partial name matches are excluded)") @QueryParam("exactmatchnames") Boolean exactMatchNamesOnly,
|
||||
@Parameter(description = "Default resources (without identifiers) only") @QueryParam("default") Boolean defaultResource,
|
||||
@ -237,7 +212,7 @@ public class ArbitraryResource {
|
||||
}
|
||||
|
||||
List<ArbitraryResourceData> resources = repository.getArbitraryRepository()
|
||||
.searchArbitraryResources(service, query, identifier, names, title, description, keywords, usePrefixOnly,
|
||||
.searchArbitraryResources(service, query, identifier, names, title, description, usePrefixOnly,
|
||||
exactMatchNames, defaultRes, mode, minLevel, followedOnly, excludeBlocked, includeMetadata, includeStatus,
|
||||
before, after, limit, offset, reverse);
|
||||
|
||||
@ -703,20 +678,20 @@ public class ArbitraryResource {
|
||||
)
|
||||
}
|
||||
)
|
||||
public void get(@PathParam("service") Service service,
|
||||
public HttpServletResponse get(@PathParam("service") Service service,
|
||||
@PathParam("name") String name,
|
||||
@QueryParam("filepath") String filepath,
|
||||
@QueryParam("encoding") String encoding,
|
||||
@QueryParam("rebuild") boolean rebuild,
|
||||
@QueryParam("async") boolean async,
|
||||
@QueryParam("attempts") Integer attempts, @QueryParam("attachment") boolean attachment, @QueryParam("attachmentFilename") String attachmentFilename) {
|
||||
@QueryParam("attempts") Integer attempts) {
|
||||
|
||||
// Authentication can be bypassed in the settings, for those running public QDN nodes
|
||||
if (!Settings.getInstance().isQDNAuthBypassEnabled()) {
|
||||
Security.checkApiCallAllowed(request);
|
||||
}
|
||||
|
||||
this.download(service, name, null, filepath, encoding, rebuild, async, attempts, attachment, attachmentFilename);
|
||||
return this.download(service, name, null, filepath, encoding, rebuild, async, attempts);
|
||||
}
|
||||
|
||||
@GET
|
||||
@ -736,21 +711,21 @@ public class ArbitraryResource {
|
||||
)
|
||||
}
|
||||
)
|
||||
public void get(@PathParam("service") Service service,
|
||||
public HttpServletResponse get(@PathParam("service") Service service,
|
||||
@PathParam("name") String name,
|
||||
@PathParam("identifier") String identifier,
|
||||
@QueryParam("filepath") String filepath,
|
||||
@QueryParam("encoding") String encoding,
|
||||
@QueryParam("rebuild") boolean rebuild,
|
||||
@QueryParam("async") boolean async,
|
||||
@QueryParam("attempts") Integer attempts, @QueryParam("attachment") boolean attachment, @QueryParam("attachmentFilename") String attachmentFilename) {
|
||||
@QueryParam("attempts") Integer attempts) {
|
||||
|
||||
// Authentication can be bypassed in the settings, for those running public QDN nodes
|
||||
if (!Settings.getInstance().isQDNAuthBypassEnabled()) {
|
||||
Security.checkApiCallAllowed(request, null);
|
||||
}
|
||||
|
||||
this.download(service, name, identifier, filepath, encoding, rebuild, async, attempts, attachment, attachmentFilename);
|
||||
return this.download(service, name, identifier, filepath, encoding, rebuild, async, attempts);
|
||||
}
|
||||
|
||||
|
||||
@ -895,464 +870,6 @@ public class ArbitraryResource {
|
||||
}
|
||||
|
||||
|
||||
@GET
|
||||
@Path("/check/tmp")
|
||||
@Produces(MediaType.TEXT_PLAIN)
|
||||
@Operation(
|
||||
summary = "Check if the disk has enough disk space for an upcoming upload",
|
||||
responses = {
|
||||
@ApiResponse(description = "OK if sufficient space", responseCode = "200"),
|
||||
@ApiResponse(description = "Insufficient space", responseCode = "507") // 507 = Insufficient Storage
|
||||
}
|
||||
)
|
||||
@SecurityRequirement(name = "apiKey")
|
||||
public Response checkUploadSpace(@HeaderParam(Security.API_KEY_HEADER) String apiKey,
|
||||
@QueryParam("totalSize") Long totalSize) {
|
||||
Security.checkApiCallAllowed(request);
|
||||
|
||||
if (totalSize == null || totalSize <= 0) {
|
||||
return Response.status(Response.Status.BAD_REQUEST)
|
||||
.entity("Missing or invalid totalSize parameter").build();
|
||||
}
|
||||
|
||||
File uploadDir = new File("uploads-temp");
|
||||
if (!uploadDir.exists()) {
|
||||
uploadDir.mkdirs(); // ensure the folder exists
|
||||
}
|
||||
|
||||
long usableSpace = uploadDir.getUsableSpace();
|
||||
long requiredSpace = (long)(((double)totalSize) * 2.2); // estimate for chunks + merge
|
||||
|
||||
if (usableSpace < requiredSpace) {
|
||||
return Response.status(507).entity("Insufficient disk space").build();
|
||||
}
|
||||
|
||||
return Response.ok("Sufficient disk space").build();
|
||||
}
|
||||
|
||||
@POST
|
||||
@Path("/{service}/{name}/chunk")
|
||||
@Consumes(MediaType.MULTIPART_FORM_DATA)
|
||||
@Operation(
|
||||
summary = "Upload a single file chunk to be later assembled into a complete arbitrary resource (no identifier)",
|
||||
requestBody = @RequestBody(
|
||||
required = true,
|
||||
content = @Content(
|
||||
mediaType = MediaType.MULTIPART_FORM_DATA,
|
||||
schema = @Schema(
|
||||
implementation = Object.class
|
||||
)
|
||||
)
|
||||
),
|
||||
responses = {
|
||||
@ApiResponse(
|
||||
description = "Chunk uploaded successfully",
|
||||
responseCode = "200"
|
||||
),
|
||||
@ApiResponse(
|
||||
description = "Error writing chunk",
|
||||
responseCode = "500"
|
||||
)
|
||||
}
|
||||
)
|
||||
@SecurityRequirement(name = "apiKey")
|
||||
public Response uploadChunkNoIdentifier(@HeaderParam(Security.API_KEY_HEADER) String apiKey,
|
||||
@PathParam("service") String serviceString,
|
||||
@PathParam("name") String name,
|
||||
@FormDataParam("chunk") InputStream chunkStream,
|
||||
@FormDataParam("index") int index) {
|
||||
Security.checkApiCallAllowed(request);
|
||||
|
||||
try {
|
||||
String safeService = Paths.get(serviceString).getFileName().toString();
|
||||
String safeName = Paths.get(name).getFileName().toString();
|
||||
|
||||
java.nio.file.Path tempDir = Paths.get("uploads-temp", safeService, safeName);
|
||||
Files.createDirectories(tempDir);
|
||||
|
||||
|
||||
|
||||
java.nio.file.Path chunkFile = tempDir.resolve("chunk_" + index);
|
||||
Files.copy(chunkStream, chunkFile, StandardCopyOption.REPLACE_EXISTING);
|
||||
|
||||
return Response.ok("Chunk " + index + " received").build();
|
||||
} catch (IOException e) {
|
||||
LOGGER.error("Failed to write chunk {} for service '{}' and name '{}'", index, serviceString, name, e);
|
||||
return Response.serverError().entity("Failed to write chunk: " + e.getMessage()).build();
|
||||
}
|
||||
}
|
||||
|
||||
@POST
|
||||
@Path("/{service}/{name}/finalize")
|
||||
@Produces(MediaType.TEXT_PLAIN)
|
||||
@Operation(
|
||||
summary = "Finalize a chunked upload (no identifier) and build a raw, unsigned, ARBITRARY transaction",
|
||||
responses = {
|
||||
@ApiResponse(
|
||||
description = "raw, unsigned, ARBITRARY transaction encoded in Base58",
|
||||
content = @Content(mediaType = MediaType.TEXT_PLAIN)
|
||||
)
|
||||
}
|
||||
)
|
||||
@SecurityRequirement(name = "apiKey")
|
||||
public String finalizeUploadNoIdentifier(
|
||||
@HeaderParam(Security.API_KEY_HEADER) String apiKey,
|
||||
@PathParam("service") String serviceString,
|
||||
@PathParam("name") String name,
|
||||
@QueryParam("title") String title,
|
||||
@QueryParam("description") String description,
|
||||
@QueryParam("tags") List<String> tags,
|
||||
@QueryParam("category") Category category,
|
||||
@QueryParam("filename") String filename,
|
||||
@QueryParam("fee") Long fee,
|
||||
@QueryParam("preview") Boolean preview,
|
||||
@QueryParam("isZip") Boolean isZip
|
||||
) {
|
||||
Security.checkApiCallAllowed(request);
|
||||
java.nio.file.Path tempFile = null;
|
||||
java.nio.file.Path tempDir = null;
|
||||
java.nio.file.Path chunkDir = null;
|
||||
String safeService = Paths.get(serviceString).getFileName().toString();
|
||||
String safeName = Paths.get(name).getFileName().toString();
|
||||
|
||||
|
||||
|
||||
try {
|
||||
chunkDir = Paths.get("uploads-temp", safeService, safeName);
|
||||
|
||||
if (!Files.exists(chunkDir) || !Files.isDirectory(chunkDir)) {
|
||||
throw ApiExceptionFactory.INSTANCE.createCustomException(request, ApiError.INVALID_CRITERIA, "No chunks found for upload");
|
||||
}
|
||||
|
||||
String safeFilename = (filename == null || filename.isBlank()) ? "qortal-" + NTP.getTime() : filename;
|
||||
tempDir = Files.createTempDirectory("qortal-");
|
||||
String sanitizedFilename = Paths.get(safeFilename).getFileName().toString();
|
||||
tempFile = tempDir.resolve(sanitizedFilename);
|
||||
|
||||
try (OutputStream out = Files.newOutputStream(tempFile, StandardOpenOption.CREATE, StandardOpenOption.TRUNCATE_EXISTING)) {
|
||||
byte[] buffer = new byte[65536];
|
||||
for (java.nio.file.Path chunk : Files.list(chunkDir)
|
||||
.filter(path -> path.getFileName().toString().startsWith("chunk_"))
|
||||
.sorted(Comparator.comparingInt(path -> {
|
||||
String name2 = path.getFileName().toString();
|
||||
String numberPart = name2.substring("chunk_".length());
|
||||
return Integer.parseInt(numberPart);
|
||||
})).collect(Collectors.toList())) {
|
||||
try (InputStream in = Files.newInputStream(chunk)) {
|
||||
int bytesRead;
|
||||
while ((bytesRead = in.read(buffer)) != -1) {
|
||||
out.write(buffer, 0, bytesRead);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
String detectedExtension = "";
|
||||
String uploadFilename = null;
|
||||
boolean extensionIsValid = false;
|
||||
|
||||
if (filename != null && !filename.isBlank()) {
|
||||
int lastDot = filename.lastIndexOf('.');
|
||||
if (lastDot > 0 && lastDot < filename.length() - 1) {
|
||||
extensionIsValid = true;
|
||||
uploadFilename = filename;
|
||||
}
|
||||
}
|
||||
|
||||
if (!extensionIsValid) {
|
||||
Tika tika = new Tika();
|
||||
String mimeType = tika.detect(tempFile.toFile());
|
||||
try {
|
||||
MimeTypes allTypes = MimeTypes.getDefaultMimeTypes();
|
||||
org.apache.tika.mime.MimeType mime = allTypes.forName(mimeType);
|
||||
detectedExtension = mime.getExtension();
|
||||
} catch (MimeTypeException e) {
|
||||
LOGGER.warn("Could not determine file extension for MIME type: {}", mimeType, e);
|
||||
}
|
||||
|
||||
if (filename != null && !filename.isBlank()) {
|
||||
int lastDot = filename.lastIndexOf('.');
|
||||
String baseName = (lastDot > 0) ? filename.substring(0, lastDot) : filename;
|
||||
uploadFilename = baseName + (detectedExtension != null ? detectedExtension : "");
|
||||
} else {
|
||||
uploadFilename = "qortal-" + NTP.getTime() + (detectedExtension != null ? detectedExtension : "");
|
||||
}
|
||||
}
|
||||
|
||||
Boolean isZipBoolean = false;
|
||||
|
||||
if (isZip != null && isZip) {
|
||||
isZipBoolean = true;
|
||||
}
|
||||
|
||||
|
||||
// ✅ Call upload with `null` as identifier
|
||||
return this.upload(
|
||||
Service.valueOf(serviceString),
|
||||
name,
|
||||
null, // no identifier
|
||||
tempFile.toString(),
|
||||
null,
|
||||
null,
|
||||
isZipBoolean,
|
||||
fee,
|
||||
uploadFilename,
|
||||
title,
|
||||
description,
|
||||
tags,
|
||||
category,
|
||||
preview
|
||||
);
|
||||
|
||||
} catch (IOException e) {
|
||||
LOGGER.error("Failed to merge chunks for service='{}', name='{}'", serviceString, name, e);
|
||||
|
||||
throw ApiExceptionFactory.INSTANCE.createCustomException(request, ApiError.REPOSITORY_ISSUE, "Failed to merge chunks: " + e.getMessage());
|
||||
} finally {
|
||||
if (tempDir != null) {
|
||||
try {
|
||||
Files.walk(tempDir)
|
||||
.sorted(Comparator.reverseOrder())
|
||||
.map(java.nio.file.Path::toFile)
|
||||
.forEach(File::delete);
|
||||
} catch (IOException e) {
|
||||
LOGGER.warn("Failed to delete temp directory: {}", tempDir, e);
|
||||
}
|
||||
}
|
||||
|
||||
try {
|
||||
Files.walk(chunkDir)
|
||||
.sorted(Comparator.reverseOrder())
|
||||
.map(java.nio.file.Path::toFile)
|
||||
.forEach(File::delete);
|
||||
} catch (IOException e) {
|
||||
LOGGER.warn("Failed to delete chunk directory: {}", chunkDir, e);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
|
||||
@POST
|
||||
@Path("/{service}/{name}/{identifier}/chunk")
|
||||
@Consumes(MediaType.MULTIPART_FORM_DATA)
|
||||
@Operation(
|
||||
summary = "Upload a single file chunk to be later assembled into a complete arbitrary resource",
|
||||
requestBody = @RequestBody(
|
||||
required = true,
|
||||
content = @Content(
|
||||
mediaType = MediaType.MULTIPART_FORM_DATA,
|
||||
schema = @Schema(
|
||||
implementation = Object.class
|
||||
)
|
||||
)
|
||||
),
|
||||
responses = {
|
||||
@ApiResponse(
|
||||
description = "Chunk uploaded successfully",
|
||||
responseCode = "200"
|
||||
),
|
||||
@ApiResponse(
|
||||
description = "Error writing chunk",
|
||||
responseCode = "500"
|
||||
)
|
||||
}
|
||||
)
|
||||
@SecurityRequirement(name = "apiKey")
|
||||
public Response uploadChunk(@HeaderParam(Security.API_KEY_HEADER) String apiKey,
|
||||
@PathParam("service") String serviceString,
|
||||
@PathParam("name") String name,
|
||||
@PathParam("identifier") String identifier,
|
||||
@FormDataParam("chunk") InputStream chunkStream,
|
||||
@FormDataParam("index") int index) {
|
||||
Security.checkApiCallAllowed(request);
|
||||
|
||||
try {
|
||||
String safeService = Paths.get(serviceString).getFileName().toString();
|
||||
String safeName = Paths.get(name).getFileName().toString();
|
||||
String safeIdentifier = Paths.get(identifier).getFileName().toString();
|
||||
|
||||
java.nio.file.Path tempDir = Paths.get("uploads-temp", safeService, safeName, safeIdentifier);
|
||||
|
||||
Files.createDirectories(tempDir);
|
||||
|
||||
java.nio.file.Path chunkFile = tempDir.resolve("chunk_" + index);
|
||||
Files.copy(chunkStream, chunkFile, StandardCopyOption.REPLACE_EXISTING);
|
||||
|
||||
return Response.ok("Chunk " + index + " received").build();
|
||||
} catch (IOException e) {
|
||||
LOGGER.error("Failed to write chunk {} for service='{}', name='{}', identifier='{}'", index, serviceString, name, identifier, e);
|
||||
return Response.serverError().entity("Failed to write chunk: " + e.getMessage()).build();
|
||||
}
|
||||
}
|
||||
|
||||
@POST
|
||||
@Path("/{service}/{name}/{identifier}/finalize")
|
||||
@Produces(MediaType.TEXT_PLAIN)
|
||||
@Operation(
|
||||
summary = "Finalize a chunked upload and build a raw, unsigned, ARBITRARY transaction",
|
||||
responses = {
|
||||
@ApiResponse(
|
||||
description = "raw, unsigned, ARBITRARY transaction encoded in Base58",
|
||||
content = @Content(mediaType = MediaType.TEXT_PLAIN)
|
||||
)
|
||||
}
|
||||
)
|
||||
@SecurityRequirement(name = "apiKey")
|
||||
public String finalizeUpload(
|
||||
@HeaderParam(Security.API_KEY_HEADER) String apiKey,
|
||||
@PathParam("service") String serviceString,
|
||||
@PathParam("name") String name,
|
||||
@PathParam("identifier") String identifier,
|
||||
@QueryParam("title") String title,
|
||||
@QueryParam("description") String description,
|
||||
@QueryParam("tags") List<String> tags,
|
||||
@QueryParam("category") Category category,
|
||||
@QueryParam("filename") String filename,
|
||||
@QueryParam("fee") Long fee,
|
||||
@QueryParam("preview") Boolean preview,
|
||||
@QueryParam("isZip") Boolean isZip
|
||||
) {
|
||||
Security.checkApiCallAllowed(request);
|
||||
java.nio.file.Path tempFile = null;
|
||||
java.nio.file.Path tempDir = null;
|
||||
java.nio.file.Path chunkDir = null;
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
try {
|
||||
String safeService = Paths.get(serviceString).getFileName().toString();
|
||||
String safeName = Paths.get(name).getFileName().toString();
|
||||
String safeIdentifier = Paths.get(identifier).getFileName().toString();
|
||||
java.nio.file.Path baseUploadsDir = Paths.get("uploads-temp"); // relative to Qortal working dir
|
||||
chunkDir = baseUploadsDir.resolve(safeService).resolve(safeName).resolve(safeIdentifier);
|
||||
|
||||
if (!Files.exists(chunkDir) || !Files.isDirectory(chunkDir)) {
|
||||
throw ApiExceptionFactory.INSTANCE.createCustomException(request, ApiError.INVALID_CRITERIA, "No chunks found for upload");
|
||||
}
|
||||
|
||||
// Step 1: Determine a safe filename for disk temp file (regardless of extension correctness)
|
||||
String safeFilename = filename;
|
||||
if (filename == null || filename.isBlank()) {
|
||||
safeFilename = "qortal-" + NTP.getTime();
|
||||
}
|
||||
|
||||
tempDir = Files.createTempDirectory("qortal-");
|
||||
String sanitizedFilename = Paths.get(safeFilename).getFileName().toString();
|
||||
tempFile = tempDir.resolve(sanitizedFilename);
|
||||
|
||||
|
||||
// Step 2: Merge chunks
|
||||
|
||||
try (OutputStream out = Files.newOutputStream(tempFile, StandardOpenOption.CREATE, StandardOpenOption.TRUNCATE_EXISTING)) {
|
||||
byte[] buffer = new byte[65536];
|
||||
for (java.nio.file.Path chunk : Files.list(chunkDir)
|
||||
.filter(path -> path.getFileName().toString().startsWith("chunk_"))
|
||||
.sorted(Comparator.comparingInt(path -> {
|
||||
String name2 = path.getFileName().toString();
|
||||
String numberPart = name2.substring("chunk_".length());
|
||||
return Integer.parseInt(numberPart);
|
||||
})).collect(Collectors.toList())) {
|
||||
try (InputStream in = Files.newInputStream(chunk)) {
|
||||
int bytesRead;
|
||||
while ((bytesRead = in.read(buffer)) != -1) {
|
||||
out.write(buffer, 0, bytesRead);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
// Step 3: Determine correct extension
|
||||
String detectedExtension = "";
|
||||
String uploadFilename = null;
|
||||
boolean extensionIsValid = false;
|
||||
|
||||
if (filename != null && !filename.isBlank()) {
|
||||
int lastDot = filename.lastIndexOf('.');
|
||||
if (lastDot > 0 && lastDot < filename.length() - 1) {
|
||||
extensionIsValid = true;
|
||||
uploadFilename = filename;
|
||||
}
|
||||
}
|
||||
|
||||
if (!extensionIsValid) {
|
||||
Tika tika = new Tika();
|
||||
String mimeType = tika.detect(tempFile.toFile());
|
||||
try {
|
||||
MimeTypes allTypes = MimeTypes.getDefaultMimeTypes();
|
||||
org.apache.tika.mime.MimeType mime = allTypes.forName(mimeType);
|
||||
detectedExtension = mime.getExtension();
|
||||
} catch (MimeTypeException e) {
|
||||
LOGGER.warn("Could not determine file extension for MIME type: {}", mimeType, e);
|
||||
}
|
||||
|
||||
if (filename != null && !filename.isBlank()) {
|
||||
int lastDot = filename.lastIndexOf('.');
|
||||
String baseName = (lastDot > 0) ? filename.substring(0, lastDot) : filename;
|
||||
uploadFilename = baseName + (detectedExtension != null ? detectedExtension : "");
|
||||
} else {
|
||||
uploadFilename = "qortal-" + NTP.getTime() + (detectedExtension != null ? detectedExtension : "");
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
Boolean isZipBoolean = false;
|
||||
|
||||
if (isZip != null && isZip) {
|
||||
isZipBoolean = true;
|
||||
}
|
||||
|
||||
|
||||
return this.upload(
|
||||
Service.valueOf(serviceString),
|
||||
name,
|
||||
identifier,
|
||||
tempFile.toString(),
|
||||
null,
|
||||
null,
|
||||
isZipBoolean,
|
||||
fee,
|
||||
uploadFilename,
|
||||
title,
|
||||
description,
|
||||
tags,
|
||||
category,
|
||||
preview
|
||||
);
|
||||
|
||||
} catch (IOException e) {
|
||||
LOGGER.error("Unexpected error in finalizeUpload for service='{}', name='{}', name='{}'", serviceString, name, identifier, e);
|
||||
|
||||
throw ApiExceptionFactory.INSTANCE.createCustomException(request, ApiError.REPOSITORY_ISSUE, "Failed to merge chunks: " + e.getMessage());
|
||||
} finally {
|
||||
if (tempDir != null) {
|
||||
try {
|
||||
Files.walk(tempDir)
|
||||
.sorted(Comparator.reverseOrder())
|
||||
.map(java.nio.file.Path::toFile)
|
||||
.forEach(File::delete);
|
||||
} catch (IOException e) {
|
||||
LOGGER.warn("Failed to delete temp directory: {}", tempDir, e);
|
||||
}
|
||||
}
|
||||
|
||||
try {
|
||||
Files.walk(chunkDir)
|
||||
.sorted(Comparator.reverseOrder())
|
||||
.map(java.nio.file.Path::toFile)
|
||||
.forEach(File::delete);
|
||||
} catch (IOException e) {
|
||||
LOGGER.warn("Failed to delete chunk directory: {}", chunkDir, e);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
// Upload base64-encoded data
|
||||
|
||||
@ -1668,90 +1185,6 @@ public String finalizeUpload(
|
||||
}
|
||||
}
|
||||
|
||||
@GET
|
||||
@Path("/indices")
|
||||
@Operation(
|
||||
summary = "Find matching arbitrary resource indices",
|
||||
description = "",
|
||||
responses = {
|
||||
@ApiResponse(
|
||||
description = "indices",
|
||||
content = @Content(
|
||||
array = @ArraySchema(
|
||||
schema = @Schema(
|
||||
implementation = ArbitraryDataIndexScorecard.class
|
||||
)
|
||||
)
|
||||
)
|
||||
)
|
||||
}
|
||||
)
|
||||
public List<ArbitraryDataIndexScorecard> searchIndices(@QueryParam("terms") String[] terms) {
|
||||
|
||||
List<ArbitraryDataIndexDetail> indices = new ArrayList<>();
|
||||
|
||||
// get index details for each term
|
||||
for( String term : terms ) {
|
||||
List<ArbitraryDataIndexDetail> details = IndexCache.getInstance().getIndicesByTerm().get(term);
|
||||
|
||||
if( details != null ) {
|
||||
indices.addAll(details);
|
||||
}
|
||||
}
|
||||
|
||||
// sum up the scores for each index with identical attributes
|
||||
Map<ArbitraryDataIndexScoreKey, Double> scoreForKey
|
||||
= indices.stream()
|
||||
.collect(
|
||||
Collectors.groupingBy(
|
||||
index -> new ArbitraryDataIndexScoreKey(index.name, index.category, index.link),
|
||||
Collectors.summingDouble(detail -> 1.0 / detail.rank)
|
||||
)
|
||||
);
|
||||
|
||||
// create scorecards for each index group and put them in descending order by score
|
||||
List<ArbitraryDataIndexScorecard> scorecards
|
||||
= scoreForKey.entrySet().stream().map(
|
||||
entry
|
||||
->
|
||||
new ArbitraryDataIndexScorecard(
|
||||
entry.getValue(),
|
||||
entry.getKey().name,
|
||||
entry.getKey().category,
|
||||
entry.getKey().link)
|
||||
)
|
||||
.sorted(Comparator.comparingDouble(ArbitraryDataIndexScorecard::getScore).reversed())
|
||||
.collect(Collectors.toList());
|
||||
|
||||
return scorecards;
|
||||
}
|
||||
|
||||
@GET
|
||||
@Path("/indices/{name}/{idPrefix}")
|
||||
@Operation(
|
||||
summary = "Find matching arbitrary resource indices for a registered name and identifier prefix",
|
||||
description = "",
|
||||
responses = {
|
||||
@ApiResponse(
|
||||
description = "indices",
|
||||
content = @Content(
|
||||
array = @ArraySchema(
|
||||
schema = @Schema(
|
||||
implementation = ArbitraryDataIndexDetail.class
|
||||
)
|
||||
)
|
||||
)
|
||||
)
|
||||
}
|
||||
)
|
||||
public List<ArbitraryDataIndexDetail> searchIndicesByName(@PathParam("name") String name, @PathParam("idPrefix") String idPrefix) {
|
||||
|
||||
return
|
||||
IndexCache.getInstance().getIndicesByIssuer()
|
||||
.getOrDefault(name, new ArrayList<>(0)).stream()
|
||||
.filter( indexDetail -> indexDetail.indexIdentifer.startsWith(idPrefix))
|
||||
.collect(Collectors.toList());
|
||||
}
|
||||
|
||||
// Shared methods
|
||||
|
||||
@ -1818,7 +1251,7 @@ public String finalizeUpload(
|
||||
if (path == null) {
|
||||
// See if we have a string instead
|
||||
if (string != null) {
|
||||
if (filename == null || filename.isBlank()) {
|
||||
if (filename == null) {
|
||||
// Use current time as filename
|
||||
filename = String.format("qortal-%d", NTP.getTime());
|
||||
}
|
||||
@ -1833,7 +1266,7 @@ public String finalizeUpload(
|
||||
}
|
||||
// ... or base64 encoded raw data
|
||||
else if (base64 != null) {
|
||||
if (filename == null || filename.isBlank()) {
|
||||
if (filename == null) {
|
||||
// Use current time as filename
|
||||
filename = String.format("qortal-%d", NTP.getTime());
|
||||
}
|
||||
@ -1884,7 +1317,6 @@ public String finalizeUpload(
|
||||
);
|
||||
|
||||
transactionBuilder.build();
|
||||
|
||||
// Don't compute nonce - this is done by the client (or via POST /arbitrary/compute)
|
||||
ArbitraryTransactionData transactionData = transactionBuilder.getArbitraryTransactionData();
|
||||
return Base58.encode(ArbitraryTransactionTransformer.toBytes(transactionData));
|
||||
@ -1900,20 +1332,22 @@ public String finalizeUpload(
|
||||
}
|
||||
}
|
||||
|
||||
private void download(Service service, String name, String identifier, String filepath, String encoding, boolean rebuild, boolean async, Integer maxAttempts, boolean attachment, String attachmentFilename) {
|
||||
private HttpServletResponse download(Service service, String name, String identifier, String filepath, String encoding, boolean rebuild, boolean async, Integer maxAttempts) {
|
||||
|
||||
try {
|
||||
ArbitraryDataReader arbitraryDataReader = new ArbitraryDataReader(name, ArbitraryDataFile.ResourceIdType.NAME, service, identifier);
|
||||
|
||||
|
||||
int attempts = 0;
|
||||
if (maxAttempts == null) {
|
||||
maxAttempts = 5;
|
||||
}
|
||||
|
||||
|
||||
// Loop until we have data
|
||||
if (async) {
|
||||
// Asynchronous
|
||||
arbitraryDataReader.loadAsynchronously(false, 1);
|
||||
} else {
|
||||
}
|
||||
else {
|
||||
// Synchronous
|
||||
while (!Controller.isStopping()) {
|
||||
attempts++;
|
||||
@ -1923,189 +1357,88 @@ public String finalizeUpload(
|
||||
break;
|
||||
} catch (MissingDataException e) {
|
||||
if (attempts > maxAttempts) {
|
||||
// Give up after 5 attempts
|
||||
throw ApiExceptionFactory.INSTANCE.createCustomException(request, ApiError.INVALID_CRITERIA, "Data unavailable. Please try again later.");
|
||||
}
|
||||
}
|
||||
}
|
||||
Thread.sleep(3000L);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
java.nio.file.Path outputPath = arbitraryDataReader.getFilePath();
|
||||
if (outputPath == null) {
|
||||
// Assume the resource doesn't exist
|
||||
throw ApiExceptionFactory.INSTANCE.createCustomException(request, ApiError.FILE_NOT_FOUND, "File not found");
|
||||
}
|
||||
|
||||
|
||||
if (filepath == null || filepath.isEmpty()) {
|
||||
// No file path supplied - so check if this is a single file resource
|
||||
String[] files = ArrayUtils.removeElement(outputPath.toFile().list(), ".qortal");
|
||||
if (files != null && files.length == 1) {
|
||||
// This is a single file resource
|
||||
filepath = files[0];
|
||||
} else {
|
||||
throw ApiExceptionFactory.INSTANCE.createCustomException(request, ApiError.INVALID_CRITERIA, "filepath is required for resources containing more than one file");
|
||||
}
|
||||
else {
|
||||
throw ApiExceptionFactory.INSTANCE.createCustomException(request, ApiError.INVALID_CRITERIA,
|
||||
"filepath is required for resources containing more than one file");
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
java.nio.file.Path path = Paths.get(outputPath.toString(), filepath);
|
||||
if (!Files.exists(path)) {
|
||||
throw ApiExceptionFactory.INSTANCE.createCustomException(request, ApiError.INVALID_CRITERIA, "No file exists at filepath: " + filepath);
|
||||
String message = String.format("No file exists at filepath: %s", filepath);
|
||||
throw ApiExceptionFactory.INSTANCE.createCustomException(request, ApiError.INVALID_CRITERIA, message);
|
||||
}
|
||||
|
||||
if (attachment) {
|
||||
String rawFilename;
|
||||
|
||||
if (attachmentFilename != null && !attachmentFilename.isEmpty()) {
|
||||
// 1. Sanitize first
|
||||
String safeAttachmentFilename = attachmentFilename.replaceAll("[\\\\/:*?\"<>|]", "_");
|
||||
|
||||
// 2. Check for a valid extension (3–5 alphanumeric chars)
|
||||
if (!safeAttachmentFilename.matches(".*\\.[a-zA-Z0-9]{2,5}$")) {
|
||||
safeAttachmentFilename += ".bin";
|
||||
}
|
||||
|
||||
rawFilename = safeAttachmentFilename;
|
||||
} else {
|
||||
// Fallback if no filename is provided
|
||||
String baseFilename = (identifier != null && !identifier.isEmpty())
|
||||
? name + "-" + identifier
|
||||
: name;
|
||||
rawFilename = baseFilename.replaceAll("[\\\\/:*?\"<>|]", "_") + ".bin";
|
||||
}
|
||||
|
||||
// Optional: trim length
|
||||
rawFilename = rawFilename.length() > 100 ? rawFilename.substring(0, 100) : rawFilename;
|
||||
|
||||
// 3. Set Content-Disposition header
|
||||
response.setHeader("Content-Disposition", "attachment; filename=\"" + rawFilename + "\"");
|
||||
}
|
||||
|
||||
// Determine the total size of the requested file
|
||||
long fileSize = Files.size(path);
|
||||
String mimeType = context.getMimeType(path.toString());
|
||||
|
||||
// Attempt to read the "Range" header from the request to support partial content delivery (e.g., for video streaming or resumable downloads)
|
||||
|
||||
byte[] data;
|
||||
int fileSize = (int)path.toFile().length();
|
||||
int length = fileSize;
|
||||
|
||||
// Parse "Range" header
|
||||
Integer rangeStart = null;
|
||||
Integer rangeEnd = null;
|
||||
String range = request.getHeader("Range");
|
||||
|
||||
long rangeStart = 0;
|
||||
long rangeEnd = fileSize - 1;
|
||||
boolean isPartial = false;
|
||||
|
||||
// If a Range header is present and no base64 encoding is requested, parse the range values
|
||||
if (range != null && encoding == null) {
|
||||
range = range.replace("bytes=", ""); // Remove the "bytes=" prefix
|
||||
String[] parts = range.split("-"); // Split the range into start and end
|
||||
|
||||
// Parse range start
|
||||
if (parts.length > 0 && !parts[0].isEmpty()) {
|
||||
rangeStart = Long.parseLong(parts[0]);
|
||||
}
|
||||
|
||||
// Parse range end, if present
|
||||
if (parts.length > 1 && !parts[1].isEmpty()) {
|
||||
rangeEnd = Long.parseLong(parts[1]);
|
||||
}
|
||||
|
||||
isPartial = true; // Indicate that this is a partial content request
|
||||
if (range != null) {
|
||||
range = range.replace("bytes=", "");
|
||||
String[] parts = range.split("-");
|
||||
rangeStart = (parts != null && parts.length > 0) ? Integer.parseInt(parts[0]) : null;
|
||||
rangeEnd = (parts != null && parts.length > 1) ? Integer.parseInt(parts[1]) : fileSize;
|
||||
}
|
||||
|
||||
// Calculate how many bytes should be sent in the response
|
||||
long contentLength = rangeEnd - rangeStart + 1;
|
||||
|
||||
// Inform the client that byte ranges are supported
|
||||
response.setHeader("Accept-Ranges", "bytes");
|
||||
|
||||
if (isPartial) {
|
||||
// If partial content was requested, return 206 Partial Content with appropriate headers
|
||||
response.setStatus(HttpServletResponse.SC_PARTIAL_CONTENT);
|
||||
response.setHeader("Content-Range", String.format("bytes %d-%d/%d", rangeStart, rangeEnd, fileSize));
|
||||
} else {
|
||||
// Otherwise, return the entire file with status 200 OK
|
||||
response.setStatus(HttpServletResponse.SC_OK);
|
||||
|
||||
if (rangeStart != null && rangeEnd != null) {
|
||||
// We have a range, so update the requested length
|
||||
length = rangeEnd - rangeStart;
|
||||
}
|
||||
|
||||
// Initialize output streams for writing the file to the response
|
||||
OutputStream rawOut = null;
|
||||
OutputStream base64Out = null;
|
||||
OutputStream gzipOut = null;
|
||||
|
||||
try {
|
||||
rawOut = response.getOutputStream();
|
||||
|
||||
if (encoding != null && "base64".equalsIgnoreCase(encoding)) {
|
||||
// If base64 encoding is requested, override content type
|
||||
response.setContentType("text/plain");
|
||||
|
||||
// Check if the client accepts gzip encoding
|
||||
String acceptEncoding = request.getHeader("Accept-Encoding");
|
||||
boolean wantsGzip = acceptEncoding != null && acceptEncoding.contains("gzip");
|
||||
|
||||
if (wantsGzip) {
|
||||
// Wrap output in GZIP and Base64 streams if gzip is accepted
|
||||
response.setHeader("Content-Encoding", "gzip");
|
||||
gzipOut = new GZIPOutputStream(rawOut);
|
||||
base64Out = java.util.Base64.getEncoder().wrap(gzipOut);
|
||||
} else {
|
||||
// Wrap output in Base64 only
|
||||
base64Out = java.util.Base64.getEncoder().wrap(rawOut);
|
||||
}
|
||||
|
||||
rawOut = base64Out; // Use the wrapped stream for writing
|
||||
} else {
|
||||
// For raw binary output, set the content type and length
|
||||
response.setContentType(mimeType != null ? mimeType : "application/octet-stream");
|
||||
response.setContentLength((int) contentLength);
|
||||
}
|
||||
|
||||
// Stream file content
|
||||
try (InputStream inputStream = Files.newInputStream(path)) {
|
||||
if (rangeStart > 0) {
|
||||
inputStream.skip(rangeStart);
|
||||
}
|
||||
|
||||
byte[] buffer = new byte[65536];
|
||||
long bytesRemaining = contentLength;
|
||||
int bytesRead;
|
||||
|
||||
while (bytesRemaining > 0 && (bytesRead = inputStream.read(buffer, 0, (int) Math.min(buffer.length, bytesRemaining))) != -1) {
|
||||
rawOut.write(buffer, 0, bytesRead);
|
||||
bytesRemaining -= bytesRead;
|
||||
}
|
||||
}
|
||||
|
||||
// Stream finished
|
||||
if (base64Out != null) {
|
||||
base64Out.close(); // Also flushes and closes the wrapped gzipOut
|
||||
} else if (gzipOut != null) {
|
||||
gzipOut.close(); // Only close gzipOut if it wasn't wrapped by base64Out
|
||||
} else {
|
||||
rawOut.flush(); // Flush only the base output stream if nothing was wrapped
|
||||
}
|
||||
|
||||
if (!response.isCommitted()) {
|
||||
response.setStatus(HttpServletResponse.SC_OK);
|
||||
response.getWriter().write(" ");
|
||||
}
|
||||
|
||||
} catch (IOException e) {
|
||||
// Streaming errors should not rethrow — just log
|
||||
LOGGER.warn(String.format("Streaming error for %s %s: %s", service, name, e.getMessage()));
|
||||
|
||||
if (length < fileSize && encoding == null) {
|
||||
// Partial content requested, and not encoding the data
|
||||
response.setStatus(206);
|
||||
response.addHeader("Content-Range", String.format("bytes %d-%d/%d", rangeStart, rangeEnd-1, fileSize));
|
||||
data = FilesystemUtils.readFromFile(path.toString(), rangeStart, length);
|
||||
}
|
||||
|
||||
} catch (IOException | ApiException | DataException e) {
|
||||
LOGGER.warn(String.format("Unable to load %s %s: %s", service, name, e.getMessage()));
|
||||
if (!response.isCommitted()) {
|
||||
throw ApiExceptionFactory.INSTANCE.createCustomException(request, ApiError.FILE_NOT_FOUND, e.getMessage());
|
||||
else {
|
||||
// Full content requested (or encoded data)
|
||||
response.setStatus(200);
|
||||
data = Files.readAllBytes(path); // TODO: limit file size that can be read into memory
|
||||
}
|
||||
} catch (NumberFormatException e) {
|
||||
LOGGER.warn(String.format("Invalid range for %s %s: %s", service, name, e.getMessage()));
|
||||
if (!response.isCommitted()) {
|
||||
throw ApiExceptionFactory.INSTANCE.createCustomException(request, ApiError.INVALID_DATA, e.getMessage());
|
||||
|
||||
// Encode the data if requested
|
||||
if (encoding != null && Objects.equals(encoding.toLowerCase(), "base64")) {
|
||||
data = Base64.encode(data);
|
||||
}
|
||||
|
||||
response.addHeader("Accept-Ranges", "bytes");
|
||||
response.setContentType(context.getMimeType(path.toString()));
|
||||
response.setContentLength(data.length);
|
||||
response.getOutputStream().write(data);
|
||||
|
||||
return response;
|
||||
} catch (Exception e) {
|
||||
LOGGER.debug(String.format("Unable to load %s %s: %s", service, name, e.getMessage()));
|
||||
throw ApiExceptionFactory.INSTANCE.createCustomException(request, ApiError.FILE_NOT_FOUND, e.getMessage());
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
|
||||
private FileProperties getFileProperties(Service service, String name, String identifier) {
|
||||
try {
|
||||
|
@ -16,13 +16,9 @@ import org.qortal.api.model.AggregatedOrder;
|
||||
import org.qortal.api.model.TradeWithOrderInfo;
|
||||
import org.qortal.api.resource.TransactionsResource.ConfirmationStatus;
|
||||
import org.qortal.asset.Asset;
|
||||
import org.qortal.controller.hsqldb.HSQLDBBalanceRecorder;
|
||||
import org.qortal.crypto.Crypto;
|
||||
import org.qortal.data.account.AccountBalanceData;
|
||||
import org.qortal.data.account.AccountData;
|
||||
import org.qortal.data.account.AddressAmountData;
|
||||
import org.qortal.data.account.BlockHeightRange;
|
||||
import org.qortal.data.account.BlockHeightRangeAddressAmounts;
|
||||
import org.qortal.data.asset.AssetData;
|
||||
import org.qortal.data.asset.OrderData;
|
||||
import org.qortal.data.asset.RecentTradeData;
|
||||
@ -37,7 +33,6 @@ import org.qortal.transaction.Transaction;
|
||||
import org.qortal.transaction.Transaction.ValidationResult;
|
||||
import org.qortal.transform.TransformationException;
|
||||
import org.qortal.transform.transaction.*;
|
||||
import org.qortal.utils.BalanceRecorderUtils;
|
||||
import org.qortal.utils.Base58;
|
||||
|
||||
import javax.servlet.http.HttpServletRequest;
|
||||
@ -47,7 +42,6 @@ import javax.ws.rs.core.MediaType;
|
||||
import java.util.ArrayList;
|
||||
import java.util.Collections;
|
||||
import java.util.List;
|
||||
import java.util.Optional;
|
||||
import java.util.stream.Collectors;
|
||||
|
||||
@Path("/assets")
|
||||
@ -185,122 +179,6 @@ public class AssetsResource {
|
||||
}
|
||||
}
|
||||
|
||||
@GET
|
||||
@Path("/balancedynamicranges")
|
||||
@Operation(
|
||||
summary = "Get balance dynamic ranges listed.",
|
||||
description = ".",
|
||||
responses = {
|
||||
@ApiResponse(
|
||||
content = @Content(
|
||||
array = @ArraySchema(
|
||||
schema = @Schema(
|
||||
implementation = BlockHeightRange.class
|
||||
)
|
||||
)
|
||||
)
|
||||
)
|
||||
}
|
||||
)
|
||||
public List<BlockHeightRange> getBalanceDynamicRanges(
|
||||
@Parameter(ref = "offset") @QueryParam("offset") Integer offset,
|
||||
@Parameter(ref = "limit") @QueryParam("limit") Integer limit,
|
||||
@Parameter(ref = "reverse") @QueryParam("reverse") Boolean reverse) {
|
||||
|
||||
Optional<HSQLDBBalanceRecorder> recorder = HSQLDBBalanceRecorder.getInstance();
|
||||
|
||||
if( recorder.isPresent()) {
|
||||
return recorder.get().getRanges(offset, limit, reverse);
|
||||
}
|
||||
else {
|
||||
return new ArrayList<>(0);
|
||||
}
|
||||
}
|
||||
|
||||
@GET
|
||||
@Path("/balancedynamicrange/{height}")
|
||||
@Operation(
|
||||
summary = "Get balance dynamic range for a given height.",
|
||||
description = ".",
|
||||
responses = {
|
||||
@ApiResponse(
|
||||
content = @Content(
|
||||
schema = @Schema(
|
||||
implementation = BlockHeightRange.class
|
||||
)
|
||||
)
|
||||
)
|
||||
}
|
||||
)
|
||||
@ApiErrors({
|
||||
ApiError.INVALID_CRITERIA, ApiError.INVALID_DATA
|
||||
})
|
||||
public BlockHeightRange getBalanceDynamicRange(@PathParam("height") int height) {
|
||||
|
||||
Optional<HSQLDBBalanceRecorder> recorder = HSQLDBBalanceRecorder.getInstance();
|
||||
|
||||
if( recorder.isPresent()) {
|
||||
Optional<BlockHeightRange> range = recorder.get().getRange(height);
|
||||
|
||||
if( range.isPresent() ) {
|
||||
return range.get();
|
||||
}
|
||||
else {
|
||||
throw ApiExceptionFactory.INSTANCE.createException(request, ApiError.INVALID_CRITERIA);
|
||||
}
|
||||
}
|
||||
else {
|
||||
throw ApiExceptionFactory.INSTANCE.createException(request, ApiError.INVALID_DATA);
|
||||
}
|
||||
}
|
||||
|
||||
@GET
|
||||
@Path("/balancedynamicamounts/{begin}/{end}")
|
||||
@Operation(
|
||||
summary = "Get balance dynamic ranges address amounts listed.",
|
||||
description = ".",
|
||||
responses = {
|
||||
@ApiResponse(
|
||||
content = @Content(
|
||||
array = @ArraySchema(
|
||||
schema = @Schema(
|
||||
implementation = AddressAmountData.class
|
||||
)
|
||||
)
|
||||
)
|
||||
)
|
||||
}
|
||||
)
|
||||
@ApiErrors({
|
||||
ApiError.INVALID_CRITERIA, ApiError.INVALID_DATA
|
||||
})
|
||||
public List<AddressAmountData> getBalanceDynamicAddressAmounts(
|
||||
@PathParam("begin") int begin,
|
||||
@PathParam("end") int end,
|
||||
@Parameter(ref = "offset") @QueryParam("offset") Integer offset,
|
||||
@Parameter(ref = "limit") @QueryParam("limit") Integer limit) {
|
||||
|
||||
Optional<HSQLDBBalanceRecorder> recorder = HSQLDBBalanceRecorder.getInstance();
|
||||
|
||||
if( recorder.isPresent()) {
|
||||
Optional<BlockHeightRangeAddressAmounts> addressAmounts = recorder.get().getAddressAmounts(new BlockHeightRange(begin, end, false));
|
||||
|
||||
if( addressAmounts.isPresent() ) {
|
||||
return addressAmounts.get().getAmounts().stream()
|
||||
.sorted(BalanceRecorderUtils.ADDRESS_AMOUNT_DATA_COMPARATOR.reversed())
|
||||
.skip(offset)
|
||||
.limit(limit)
|
||||
.collect(Collectors.toList());
|
||||
}
|
||||
else {
|
||||
throw ApiExceptionFactory.INSTANCE.createException(request, ApiError.INVALID_CRITERIA);
|
||||
}
|
||||
}
|
||||
else {
|
||||
throw ApiExceptionFactory.INSTANCE.createException(request, ApiError.INVALID_DATA);
|
||||
}
|
||||
}
|
||||
|
||||
@GET
|
||||
@Path("/openorders/{assetid}/{otherassetid}")
|
||||
@Operation(
|
||||
|
@ -19,8 +19,6 @@ import org.qortal.crypto.Crypto;
|
||||
import org.qortal.data.account.AccountData;
|
||||
import org.qortal.data.block.BlockData;
|
||||
import org.qortal.data.block.BlockSummaryData;
|
||||
import org.qortal.data.block.DecodedOnlineAccountData;
|
||||
import org.qortal.data.network.OnlineAccountData;
|
||||
import org.qortal.data.transaction.TransactionData;
|
||||
import org.qortal.repository.BlockArchiveReader;
|
||||
import org.qortal.repository.DataException;
|
||||
@ -29,7 +27,6 @@ import org.qortal.repository.RepositoryManager;
|
||||
import org.qortal.transform.TransformationException;
|
||||
import org.qortal.transform.block.BlockTransformer;
|
||||
import org.qortal.utils.Base58;
|
||||
import org.qortal.utils.Blocks;
|
||||
import org.qortal.utils.Triple;
|
||||
|
||||
import javax.servlet.http.HttpServletRequest;
|
||||
@ -48,7 +45,6 @@ import java.util.ArrayList;
|
||||
import java.util.Arrays;
|
||||
import java.util.Comparator;
|
||||
import java.util.List;
|
||||
import java.util.Set;
|
||||
|
||||
@Path("/blocks")
|
||||
@Tag(name = "Blocks")
|
||||
@ -893,50 +889,4 @@ public class BlocksResource {
|
||||
throw ApiExceptionFactory.INSTANCE.createException(request, ApiError.REPOSITORY_ISSUE, e);
|
||||
}
|
||||
}
|
||||
|
||||
@GET
|
||||
@Path("/onlineaccounts/{height}")
|
||||
@Operation(
|
||||
summary = "Get online accounts for block",
|
||||
description = "Returns the online accounts who submitted signatures for this block",
|
||||
responses = {
|
||||
@ApiResponse(
|
||||
description = "online accounts",
|
||||
content = @Content(
|
||||
array = @ArraySchema(
|
||||
schema = @Schema(
|
||||
implementation = DecodedOnlineAccountData.class
|
||||
)
|
||||
)
|
||||
)
|
||||
)
|
||||
}
|
||||
)
|
||||
@ApiErrors({
|
||||
ApiError.BLOCK_UNKNOWN, ApiError.REPOSITORY_ISSUE
|
||||
})
|
||||
public Set<DecodedOnlineAccountData> getOnlineAccounts(@PathParam("height") int height) {
|
||||
|
||||
try (final Repository repository = RepositoryManager.getRepository()) {
|
||||
|
||||
// get block from database
|
||||
BlockData blockData = repository.getBlockRepository().fromHeight(height);
|
||||
|
||||
// if block data is not in the database, then try the archive
|
||||
if (blockData == null) {
|
||||
blockData = repository.getBlockArchiveRepository().fromHeight(height);
|
||||
|
||||
// if the block is not in the database or the archive, then the block is unknown
|
||||
if( blockData == null ) {
|
||||
throw ApiExceptionFactory.INSTANCE.createException(request, ApiError.BLOCK_UNKNOWN);
|
||||
}
|
||||
}
|
||||
|
||||
Set<DecodedOnlineAccountData> onlineAccounts = Blocks.getDecodedOnlineAccountsForBlock(repository, blockData);
|
||||
|
||||
return onlineAccounts;
|
||||
} catch (DataException e) {
|
||||
throw ApiExceptionFactory.INSTANCE.createException(request, ApiError.REPOSITORY_ISSUE);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -502,10 +502,10 @@ public class CrossChainBitcoinResource {
|
||||
}
|
||||
|
||||
@GET
|
||||
@Path("/feerequired")
|
||||
@Path("/feeceiling")
|
||||
@Operation(
|
||||
summary = "The total fee required for unlocking BTC to the trade offer creator.",
|
||||
description = "This is in sats for a transaction that is approximately 300 kB in size.",
|
||||
summary = "Returns Bitcoin fee per Kb.",
|
||||
description = "Returns Bitcoin fee per Kb.",
|
||||
responses = {
|
||||
@ApiResponse(
|
||||
content = @Content(
|
||||
@ -516,17 +516,17 @@ public class CrossChainBitcoinResource {
|
||||
)
|
||||
}
|
||||
)
|
||||
public String getBitcoinFeeRequired() {
|
||||
public String getBitcoinFeeCeiling() {
|
||||
Bitcoin bitcoin = Bitcoin.getInstance();
|
||||
|
||||
return String.valueOf(bitcoin.getFeeRequired());
|
||||
return String.valueOf(bitcoin.getFeeCeiling());
|
||||
}
|
||||
|
||||
@POST
|
||||
@Path("/updatefeerequired")
|
||||
@Path("/updatefeeceiling")
|
||||
@Operation(
|
||||
summary = "The total fee required for unlocking BTC to the trade offer creator.",
|
||||
description = "This is in sats for a transaction that is approximately 300 kB in size.",
|
||||
summary = "Sets Bitcoin fee ceiling.",
|
||||
description = "Sets Bitcoin fee ceiling.",
|
||||
requestBody = @RequestBody(
|
||||
required = true,
|
||||
content = @Content(
|
||||
@ -545,13 +545,13 @@ public class CrossChainBitcoinResource {
|
||||
}
|
||||
)
|
||||
@ApiErrors({ApiError.INVALID_PRIVATE_KEY, ApiError.INVALID_CRITERIA})
|
||||
public String setBitcoinFeeRequired(@HeaderParam(Security.API_KEY_HEADER) String apiKey, String fee) {
|
||||
public String setBitcoinFeeCeiling(@HeaderParam(Security.API_KEY_HEADER) String apiKey, String fee) {
|
||||
Security.checkApiCallAllowed(request);
|
||||
|
||||
Bitcoin bitcoin = Bitcoin.getInstance();
|
||||
|
||||
try {
|
||||
return CrossChainUtils.setFeeRequired(bitcoin, fee);
|
||||
return CrossChainUtils.setFeeCeiling(bitcoin, fee);
|
||||
}
|
||||
catch (IllegalArgumentException e) {
|
||||
throw ApiExceptionFactory.INSTANCE.createException(request, ApiError.INVALID_CRITERIA);
|
||||
|
@ -502,10 +502,10 @@ public class CrossChainDigibyteResource {
|
||||
}
|
||||
|
||||
@GET
|
||||
@Path("/feerequired")
|
||||
@Path("/feeceiling")
|
||||
@Operation(
|
||||
summary = "The total fee required for unlocking DGB to the trade offer creator.",
|
||||
description = "This is in sats for a transaction that is approximately 300 kB in size.",
|
||||
summary = "Returns Digibyte fee per Kb.",
|
||||
description = "Returns Digibyte fee per Kb.",
|
||||
responses = {
|
||||
@ApiResponse(
|
||||
content = @Content(
|
||||
@ -516,17 +516,17 @@ public class CrossChainDigibyteResource {
|
||||
)
|
||||
}
|
||||
)
|
||||
public String getDigibyteFeeRequired() {
|
||||
public String getDigibyteFeeCeiling() {
|
||||
Digibyte digibyte = Digibyte.getInstance();
|
||||
|
||||
return String.valueOf(digibyte.getFeeRequired());
|
||||
return String.valueOf(digibyte.getFeeCeiling());
|
||||
}
|
||||
|
||||
@POST
|
||||
@Path("/updatefeerequired")
|
||||
@Path("/updatefeeceiling")
|
||||
@Operation(
|
||||
summary = "The total fee required for unlocking DGB to the trade offer creator.",
|
||||
description = "This is in sats for a transaction that is approximately 300 kB in size.",
|
||||
summary = "Sets Digibyte fee ceiling.",
|
||||
description = "Sets Digibyte fee ceiling.",
|
||||
requestBody = @RequestBody(
|
||||
required = true,
|
||||
content = @Content(
|
||||
@ -545,13 +545,13 @@ public class CrossChainDigibyteResource {
|
||||
}
|
||||
)
|
||||
@ApiErrors({ApiError.INVALID_PRIVATE_KEY, ApiError.INVALID_CRITERIA})
|
||||
public String setDigibyteFeeRequired(@HeaderParam(Security.API_KEY_HEADER) String apiKey, String fee) {
|
||||
public String setDigibyteFeeCeiling(@HeaderParam(Security.API_KEY_HEADER) String apiKey, String fee) {
|
||||
Security.checkApiCallAllowed(request);
|
||||
|
||||
Digibyte digibyte = Digibyte.getInstance();
|
||||
|
||||
try {
|
||||
return CrossChainUtils.setFeeRequired(digibyte, fee);
|
||||
return CrossChainUtils.setFeeCeiling(digibyte, fee);
|
||||
}
|
||||
catch (IllegalArgumentException e) {
|
||||
throw ApiExceptionFactory.INSTANCE.createException(request, ApiError.INVALID_CRITERIA);
|
||||
|
@ -502,10 +502,10 @@ public class CrossChainDogecoinResource {
|
||||
}
|
||||
|
||||
@GET
|
||||
@Path("/feerequired")
|
||||
@Path("/feeceiling")
|
||||
@Operation(
|
||||
summary = "The total fee required for unlocking DOGE to the trade offer creator.",
|
||||
description = "This is in sats for a transaction that is approximately 300 kB in size.",
|
||||
summary = "Returns Dogecoin fee per Kb.",
|
||||
description = "Returns Dogecoin fee per Kb.",
|
||||
responses = {
|
||||
@ApiResponse(
|
||||
content = @Content(
|
||||
@ -516,17 +516,17 @@ public class CrossChainDogecoinResource {
|
||||
)
|
||||
}
|
||||
)
|
||||
public String getDogecoinFeeRequired() {
|
||||
public String getDogecoinFeeCeiling() {
|
||||
Dogecoin dogecoin = Dogecoin.getInstance();
|
||||
|
||||
return String.valueOf(dogecoin.getFeeRequired());
|
||||
return String.valueOf(dogecoin.getFeeCeiling());
|
||||
}
|
||||
|
||||
@POST
|
||||
@Path("/updatefeerequired")
|
||||
@Path("/updatefeeceiling")
|
||||
@Operation(
|
||||
summary = "The total fee required for unlocking DOGE to the trade offer creator.",
|
||||
description = "This is in sats for a transaction that is approximately 300 kB in size.",
|
||||
summary = "Sets Dogecoin fee ceiling.",
|
||||
description = "Sets Dogecoin fee ceiling.",
|
||||
requestBody = @RequestBody(
|
||||
required = true,
|
||||
content = @Content(
|
||||
@ -545,13 +545,13 @@ public class CrossChainDogecoinResource {
|
||||
}
|
||||
)
|
||||
@ApiErrors({ApiError.INVALID_PRIVATE_KEY, ApiError.INVALID_CRITERIA})
|
||||
public String setDogecoinFeeRequired(@HeaderParam(Security.API_KEY_HEADER) String apiKey, String fee) {
|
||||
public String setDogecoinFeeCeiling(@HeaderParam(Security.API_KEY_HEADER) String apiKey, String fee) {
|
||||
Security.checkApiCallAllowed(request);
|
||||
|
||||
Dogecoin dogecoin = Dogecoin.getInstance();
|
||||
|
||||
try {
|
||||
return CrossChainUtils.setFeeRequired(dogecoin, fee);
|
||||
return CrossChainUtils.setFeeCeiling(dogecoin, fee);
|
||||
}
|
||||
catch (IllegalArgumentException e) {
|
||||
throw ApiExceptionFactory.INSTANCE.createException(request, ApiError.INVALID_CRITERIA);
|
||||
|
@ -540,10 +540,10 @@ public class CrossChainLitecoinResource {
|
||||
}
|
||||
|
||||
@GET
|
||||
@Path("/feerequired")
|
||||
@Path("/feeceiling")
|
||||
@Operation(
|
||||
summary = "The total fee required for unlocking LTC to the trade offer creator.",
|
||||
description = "This is in sats for a transaction that is approximately 300 kB in size.",
|
||||
summary = "Returns Litecoin fee per Kb.",
|
||||
description = "Returns Litecoin fee per Kb.",
|
||||
responses = {
|
||||
@ApiResponse(
|
||||
content = @Content(
|
||||
@ -554,17 +554,17 @@ public class CrossChainLitecoinResource {
|
||||
)
|
||||
}
|
||||
)
|
||||
public String getLitecoinFeeRequired() {
|
||||
public String getLitecoinFeeCeiling() {
|
||||
Litecoin litecoin = Litecoin.getInstance();
|
||||
|
||||
return String.valueOf(litecoin.getFeeRequired());
|
||||
return String.valueOf(litecoin.getFeeCeiling());
|
||||
}
|
||||
|
||||
@POST
|
||||
@Path("/updatefeerequired")
|
||||
@Path("/updatefeeceiling")
|
||||
@Operation(
|
||||
summary = "The total fee required for unlocking LTC to the trade offer creator.",
|
||||
description = "This is in sats for a transaction that is approximately 300 kB in size.",
|
||||
summary = "Sets Litecoin fee ceiling.",
|
||||
description = "Sets Litecoin fee ceiling.",
|
||||
requestBody = @RequestBody(
|
||||
required = true,
|
||||
content = @Content(
|
||||
@ -583,13 +583,13 @@ public class CrossChainLitecoinResource {
|
||||
}
|
||||
)
|
||||
@ApiErrors({ApiError.INVALID_PRIVATE_KEY, ApiError.INVALID_CRITERIA})
|
||||
public String setLitecoinFeeRequired(@HeaderParam(Security.API_KEY_HEADER) String apiKey, String fee) {
|
||||
public String setLitecoinFeeCeiling(@HeaderParam(Security.API_KEY_HEADER) String apiKey, String fee) {
|
||||
Security.checkApiCallAllowed(request);
|
||||
|
||||
Litecoin litecoin = Litecoin.getInstance();
|
||||
|
||||
try {
|
||||
return CrossChainUtils.setFeeRequired(litecoin, fee);
|
||||
return CrossChainUtils.setFeeCeiling(litecoin, fee);
|
||||
}
|
||||
catch (IllegalArgumentException e) {
|
||||
throw ApiExceptionFactory.INSTANCE.createException(request, ApiError.INVALID_CRITERIA);
|
||||
|
@ -587,10 +587,10 @@ public class CrossChainPirateChainResource {
|
||||
}
|
||||
|
||||
@GET
|
||||
@Path("/feerequired")
|
||||
@Path("/feeceiling")
|
||||
@Operation(
|
||||
summary = "The total fee required for unlocking ARRR to the trade offer creator.",
|
||||
description = "The total fee required for unlocking ARRR to the trade offer creator.",
|
||||
summary = "Returns PirateChain fee per Kb.",
|
||||
description = "Returns PirateChain fee per Kb.",
|
||||
responses = {
|
||||
@ApiResponse(
|
||||
content = @Content(
|
||||
@ -601,17 +601,17 @@ public class CrossChainPirateChainResource {
|
||||
)
|
||||
}
|
||||
)
|
||||
public String getPirateChainFeeRequired() {
|
||||
public String getPirateChainFeeCeiling() {
|
||||
PirateChain pirateChain = PirateChain.getInstance();
|
||||
|
||||
return String.valueOf(pirateChain.getFeeRequired());
|
||||
return String.valueOf(pirateChain.getFeeCeiling());
|
||||
}
|
||||
|
||||
@POST
|
||||
@Path("/updatefeerequired")
|
||||
@Path("/updatefeeceiling")
|
||||
@Operation(
|
||||
summary = "The total fee required for unlocking ARRR to the trade offer creator.",
|
||||
description = "This is in sats for a transaction that is approximately 300 kB in size.",
|
||||
summary = "Sets PirateChain fee ceiling.",
|
||||
description = "Sets PirateChain fee ceiling.",
|
||||
requestBody = @RequestBody(
|
||||
required = true,
|
||||
content = @Content(
|
||||
@ -630,13 +630,13 @@ public class CrossChainPirateChainResource {
|
||||
}
|
||||
)
|
||||
@ApiErrors({ApiError.INVALID_PRIVATE_KEY, ApiError.INVALID_CRITERIA})
|
||||
public String setPirateChainFeeRequired(@HeaderParam(Security.API_KEY_HEADER) String apiKey, String fee) {
|
||||
public String setPirateChainFeeCeiling(@HeaderParam(Security.API_KEY_HEADER) String apiKey, String fee) {
|
||||
Security.checkApiCallAllowed(request);
|
||||
|
||||
PirateChain pirateChain = PirateChain.getInstance();
|
||||
|
||||
try {
|
||||
return CrossChainUtils.setFeeRequired(pirateChain, fee);
|
||||
return CrossChainUtils.setFeeCeiling(pirateChain, fee);
|
||||
}
|
||||
catch (IllegalArgumentException e) {
|
||||
throw ApiExceptionFactory.INSTANCE.createException(request, ApiError.INVALID_CRITERIA);
|
||||
|
@ -502,10 +502,10 @@ public class CrossChainRavencoinResource {
|
||||
}
|
||||
|
||||
@GET
|
||||
@Path("/feerequired")
|
||||
@Path("/feeceiling")
|
||||
@Operation(
|
||||
summary = "The total fee required for unlocking RVN to the trade offer creator.",
|
||||
description = "This is in sats for a transaction that is approximately 300 kB in size.",
|
||||
summary = "Returns Ravencoin fee per Kb.",
|
||||
description = "Returns Ravencoin fee per Kb.",
|
||||
responses = {
|
||||
@ApiResponse(
|
||||
content = @Content(
|
||||
@ -516,17 +516,17 @@ public class CrossChainRavencoinResource {
|
||||
)
|
||||
}
|
||||
)
|
||||
public String getRavencoinFeeRequired() {
|
||||
public String getRavencoinFeeCeiling() {
|
||||
Ravencoin ravencoin = Ravencoin.getInstance();
|
||||
|
||||
return String.valueOf(ravencoin.getFeeRequired());
|
||||
return String.valueOf(ravencoin.getFeeCeiling());
|
||||
}
|
||||
|
||||
@POST
|
||||
@Path("/updatefeerequired")
|
||||
@Path("/updatefeeceiling")
|
||||
@Operation(
|
||||
summary = "The total fee required for unlocking RVN to the trade offer creator.",
|
||||
description = "This is in sats for a transaction that is approximately 300 kB in size.",
|
||||
summary = "Sets Ravencoin fee ceiling.",
|
||||
description = "Sets Ravencoin fee ceiling.",
|
||||
requestBody = @RequestBody(
|
||||
required = true,
|
||||
content = @Content(
|
||||
@ -545,13 +545,13 @@ public class CrossChainRavencoinResource {
|
||||
}
|
||||
)
|
||||
@ApiErrors({ApiError.INVALID_PRIVATE_KEY, ApiError.INVALID_CRITERIA})
|
||||
public String setRavencoinFeeRequired(@HeaderParam(Security.API_KEY_HEADER) String apiKey, String fee) {
|
||||
public String setRavencoinFeeCeiling(@HeaderParam(Security.API_KEY_HEADER) String apiKey, String fee) {
|
||||
Security.checkApiCallAllowed(request);
|
||||
|
||||
Ravencoin ravencoin = Ravencoin.getInstance();
|
||||
|
||||
try {
|
||||
return CrossChainUtils.setFeeRequired(ravencoin, fee);
|
||||
return CrossChainUtils.setFeeCeiling(ravencoin, fee);
|
||||
}
|
||||
catch (IllegalArgumentException e) {
|
||||
throw ApiExceptionFactory.INSTANCE.createException(request, ApiError.INVALID_CRITERIA);
|
||||
|
@ -10,17 +10,12 @@ 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.apache.logging.log4j.LogManager;
|
||||
import org.apache.logging.log4j.Logger;
|
||||
import org.glassfish.jersey.media.multipart.ContentDisposition;
|
||||
import org.qortal.api.ApiError;
|
||||
import org.qortal.api.ApiErrors;
|
||||
import org.qortal.api.ApiExceptionFactory;
|
||||
import org.qortal.api.Security;
|
||||
import org.qortal.api.model.CrossChainCancelRequest;
|
||||
import org.qortal.api.model.CrossChainTradeLedgerEntry;
|
||||
import org.qortal.api.model.CrossChainTradeSummary;
|
||||
import org.qortal.controller.ForeignFeesManager;
|
||||
import org.qortal.controller.tradebot.TradeBot;
|
||||
import org.qortal.crosschain.ACCT;
|
||||
import org.qortal.crosschain.AcctMode;
|
||||
@ -32,8 +27,6 @@ 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.crosschain.ForeignFeeDecodedData;
|
||||
import org.qortal.data.crosschain.ForeignFeeEncodedData;
|
||||
import org.qortal.data.transaction.BaseTransactionData;
|
||||
import org.qortal.data.transaction.MessageTransactionData;
|
||||
import org.qortal.data.transaction.TransactionData;
|
||||
@ -51,36 +44,21 @@ import org.qortal.utils.Base58;
|
||||
import org.qortal.utils.ByteArray;
|
||||
import org.qortal.utils.NTP;
|
||||
|
||||
import javax.servlet.ServletContext;
|
||||
import javax.servlet.http.HttpServletRequest;
|
||||
import javax.servlet.http.HttpServletResponse;
|
||||
import javax.ws.rs.*;
|
||||
import javax.ws.rs.core.Context;
|
||||
import javax.ws.rs.core.HttpHeaders;
|
||||
import javax.ws.rs.core.MediaType;
|
||||
import java.io.IOException;
|
||||
import java.util.*;
|
||||
import java.util.function.Supplier;
|
||||
import java.util.stream.Collectors;
|
||||
|
||||
|
||||
|
||||
@Path("/crosschain")
|
||||
@Tag(name = "Cross-Chain")
|
||||
public class CrossChainResource {
|
||||
|
||||
private static final Logger LOGGER = LogManager.getLogger(CrossChainResource.class);
|
||||
|
||||
@Context
|
||||
HttpServletRequest request;
|
||||
|
||||
@Context
|
||||
HttpServletResponse response;
|
||||
|
||||
@Context
|
||||
ServletContext context;
|
||||
|
||||
|
||||
@GET
|
||||
@Path("/tradeoffers")
|
||||
@Operation(
|
||||
@ -277,12 +255,6 @@ public class CrossChainResource {
|
||||
description = "Only return trades that completed on/after this timestamp (milliseconds since epoch)",
|
||||
example = "1597310000000"
|
||||
) @QueryParam("minimumTimestamp") Long minimumTimestamp,
|
||||
@Parameter(
|
||||
description = "Optionally filter by buyer Qortal public key"
|
||||
) @QueryParam("buyerPublicKey") String buyerPublicKey58,
|
||||
@Parameter(
|
||||
description = "Optionally filter by seller Qortal public key"
|
||||
) @QueryParam("sellerPublicKey") String sellerPublicKey58,
|
||||
@Parameter( ref = "limit") @QueryParam("limit") Integer limit,
|
||||
@Parameter( ref = "offset" ) @QueryParam("offset") Integer offset,
|
||||
@Parameter( ref = "reverse" ) @QueryParam("reverse") Boolean reverse) {
|
||||
@ -294,10 +266,6 @@ public class CrossChainResource {
|
||||
if (minimumTimestamp != null && minimumTimestamp <= 0)
|
||||
throw ApiExceptionFactory.INSTANCE.createException(request, ApiError.INVALID_CRITERIA);
|
||||
|
||||
// Decode public keys
|
||||
byte[] buyerPublicKey = decodePublicKey(buyerPublicKey58);
|
||||
byte[] sellerPublicKey = decodePublicKey(sellerPublicKey58);
|
||||
|
||||
final Boolean isFinished = Boolean.TRUE;
|
||||
|
||||
try (final Repository repository = RepositoryManager.getRepository()) {
|
||||
@ -328,7 +296,7 @@ public class CrossChainResource {
|
||||
byte[] codeHash = acctInfo.getKey().value;
|
||||
ACCT acct = acctInfo.getValue().get();
|
||||
|
||||
List<ATStateData> atStates = repository.getATRepository().getMatchingFinalATStates(codeHash, buyerPublicKey, sellerPublicKey,
|
||||
List<ATStateData> atStates = repository.getATRepository().getMatchingFinalATStates(codeHash,
|
||||
isFinished, acct.getModeByteOffset(), (long) AcctMode.REDEEMED.value, minimumFinalHeight,
|
||||
limit, offset, reverse);
|
||||
|
||||
@ -367,215 +335,6 @@ public class CrossChainResource {
|
||||
}
|
||||
}
|
||||
|
||||
@POST
|
||||
@Path("/signedfees")
|
||||
@Operation(
|
||||
summary = "",
|
||||
description = "",
|
||||
requestBody = @RequestBody(
|
||||
required = true,
|
||||
content = @Content(
|
||||
mediaType = MediaType.APPLICATION_JSON,
|
||||
array = @ArraySchema(
|
||||
schema = @Schema(
|
||||
implementation = ForeignFeeEncodedData.class
|
||||
)
|
||||
)
|
||||
)
|
||||
),
|
||||
responses = {
|
||||
@ApiResponse(
|
||||
description = "true on success",
|
||||
content = @Content(
|
||||
mediaType = MediaType.TEXT_PLAIN,
|
||||
schema = @Schema(
|
||||
type = "boolean"
|
||||
)
|
||||
)
|
||||
)
|
||||
}
|
||||
)
|
||||
public String postSignedForeignFees(List<ForeignFeeEncodedData> signedFees) {
|
||||
|
||||
LOGGER.info("signedFees = " + signedFees);
|
||||
|
||||
try {
|
||||
ForeignFeesManager.getInstance().addSignedFees(signedFees);
|
||||
|
||||
return "true";
|
||||
}
|
||||
catch( Exception e ) {
|
||||
|
||||
LOGGER.error(e.getMessage(), e);
|
||||
|
||||
return "false";
|
||||
}
|
||||
}
|
||||
|
||||
@GET
|
||||
@Path("/unsignedfees/{address}")
|
||||
@Operation(
|
||||
summary = "",
|
||||
description = "",
|
||||
responses = {
|
||||
@ApiResponse(
|
||||
content = @Content(
|
||||
array = @ArraySchema(
|
||||
schema = @Schema(
|
||||
implementation = ForeignFeeEncodedData.class
|
||||
)
|
||||
)
|
||||
)
|
||||
)
|
||||
}
|
||||
)
|
||||
@ApiErrors({ApiError.INVALID_CRITERIA, ApiError.REPOSITORY_ISSUE})
|
||||
public List<ForeignFeeEncodedData> getUnsignedFees(@PathParam("address") String address) {
|
||||
|
||||
List<ForeignFeeEncodedData> unsignedFeesForAddress = ForeignFeesManager.getInstance().getUnsignedFeesForAddress(address);
|
||||
|
||||
LOGGER.info("address = " + address);
|
||||
LOGGER.info("returning unsigned = " + unsignedFeesForAddress);
|
||||
return unsignedFeesForAddress;
|
||||
}
|
||||
|
||||
@GET
|
||||
@Path("/signedfees")
|
||||
@Operation(
|
||||
summary = "",
|
||||
description = "",
|
||||
responses = {
|
||||
@ApiResponse(
|
||||
content = @Content(
|
||||
array = @ArraySchema(
|
||||
schema = @Schema(
|
||||
implementation = ForeignFeeDecodedData.class
|
||||
)
|
||||
)
|
||||
)
|
||||
)
|
||||
}
|
||||
)
|
||||
@ApiErrors({ApiError.INVALID_CRITERIA, ApiError.REPOSITORY_ISSUE})
|
||||
public List<ForeignFeeDecodedData> getSignedFees() {
|
||||
|
||||
return ForeignFeesManager.getInstance().getSignedFees();
|
||||
}
|
||||
|
||||
/**
|
||||
* Decode Public Key
|
||||
*
|
||||
* @param publicKey58 the public key in a string
|
||||
*
|
||||
* @return the public key in bytes
|
||||
*/
|
||||
private byte[] decodePublicKey(String publicKey58) {
|
||||
|
||||
if( publicKey58 == null ) return null;
|
||||
if( publicKey58.isEmpty() ) return new byte[0];
|
||||
|
||||
byte[] publicKey;
|
||||
try {
|
||||
publicKey = Base58.decode(publicKey58);
|
||||
} catch (NumberFormatException e) {
|
||||
throw ApiExceptionFactory.INSTANCE.createException(request, ApiError.INVALID_PUBLIC_KEY, e);
|
||||
}
|
||||
|
||||
// Correct size for public key?
|
||||
if (publicKey.length != Transformer.PUBLIC_KEY_LENGTH)
|
||||
throw ApiExceptionFactory.INSTANCE.createException(request, ApiError.INVALID_PUBLIC_KEY);
|
||||
|
||||
return publicKey;
|
||||
}
|
||||
|
||||
@GET
|
||||
@Path("/ledger/{publicKey}")
|
||||
@Operation(
|
||||
summary = "Accounting entries for all trades.",
|
||||
description = "Returns accounting entries for all completed cross-chain trades",
|
||||
responses = {
|
||||
@ApiResponse(
|
||||
content = @Content(
|
||||
schema = @Schema(
|
||||
type = "string",
|
||||
format = "byte"
|
||||
)
|
||||
)
|
||||
)
|
||||
}
|
||||
)
|
||||
@ApiErrors({ApiError.INVALID_CRITERIA, ApiError.REPOSITORY_ISSUE})
|
||||
public HttpServletResponse getLedgerEntries(
|
||||
@PathParam("publicKey") String publicKey58,
|
||||
@Parameter(
|
||||
description = "Only return trades that completed on/after this timestamp (milliseconds since epoch)",
|
||||
example = "1597310000000"
|
||||
) @QueryParam("minimumTimestamp") Long minimumTimestamp) {
|
||||
|
||||
byte[] publicKey = decodePublicKey(publicKey58);
|
||||
|
||||
// minimumTimestamp (if given) needs to be positive
|
||||
if (minimumTimestamp != null && minimumTimestamp <= 0)
|
||||
throw ApiExceptionFactory.INSTANCE.createException(request, ApiError.INVALID_CRITERIA);
|
||||
|
||||
try (final Repository repository = RepositoryManager.getRepository()) {
|
||||
Integer minimumFinalHeight = null;
|
||||
|
||||
if (minimumTimestamp != null) {
|
||||
minimumFinalHeight = repository.getBlockRepository().getHeightFromTimestamp(minimumTimestamp);
|
||||
// If not found in the block repository it will return either 0 or 1
|
||||
if (minimumFinalHeight == 0 || minimumFinalHeight == 1) {
|
||||
// Try the archive
|
||||
minimumFinalHeight = repository.getBlockArchiveRepository().getHeightFromTimestamp(minimumTimestamp);
|
||||
}
|
||||
|
||||
if (minimumFinalHeight == 0)
|
||||
// We don't have any blocks since minimumTimestamp, let alone trades, so nothing to return
|
||||
return response;
|
||||
|
||||
// height returned from repository is for block BEFORE timestamp
|
||||
// but we want trades AFTER timestamp so bump height accordingly
|
||||
minimumFinalHeight++;
|
||||
}
|
||||
|
||||
List<CrossChainTradeLedgerEntry> crossChainTradeLedgerEntries = new ArrayList<>();
|
||||
|
||||
Map<ByteArray, Supplier<ACCT>> acctsByCodeHash = SupportedBlockchain.getAcctMap();
|
||||
|
||||
// collect ledger entries for each ACCT
|
||||
for (Map.Entry<ByteArray, Supplier<ACCT>> acctInfo : acctsByCodeHash.entrySet()) {
|
||||
byte[] codeHash = acctInfo.getKey().value;
|
||||
ACCT acct = acctInfo.getValue().get();
|
||||
|
||||
// collect buys and sells
|
||||
CrossChainUtils.collectLedgerEntries(publicKey, repository, minimumFinalHeight, crossChainTradeLedgerEntries, codeHash, acct, true);
|
||||
CrossChainUtils.collectLedgerEntries(publicKey, repository, minimumFinalHeight, crossChainTradeLedgerEntries, codeHash, acct, false);
|
||||
}
|
||||
|
||||
crossChainTradeLedgerEntries.sort((a, b) -> Longs.compare(a.getTradeTimestamp(), b.getTradeTimestamp()));
|
||||
|
||||
response.setStatus(HttpServletResponse.SC_OK);
|
||||
response.setContentType("text/csv");
|
||||
response.setHeader(
|
||||
HttpHeaders.CONTENT_DISPOSITION,
|
||||
ContentDisposition
|
||||
.type("attachment")
|
||||
.fileName(CrossChainUtils.createLedgerFileName(Crypto.toAddress(publicKey)))
|
||||
.build()
|
||||
.toString()
|
||||
);
|
||||
|
||||
CrossChainUtils.writeToLedger( response.getWriter(), crossChainTradeLedgerEntries);
|
||||
|
||||
return response;
|
||||
} catch (DataException e) {
|
||||
throw ApiExceptionFactory.INSTANCE.createException(request, ApiError.REPOSITORY_ISSUE, e);
|
||||
} catch (IOException e) {
|
||||
response.setStatus(HttpServletResponse.SC_INTERNAL_SERVER_ERROR);
|
||||
return response;
|
||||
}
|
||||
}
|
||||
|
||||
@GET
|
||||
@Path("/price/{blockchain}")
|
||||
@Operation(
|
||||
|
@ -10,37 +10,21 @@ import org.bitcoinj.script.ScriptBuilder;
|
||||
|
||||
import org.bouncycastle.util.Strings;
|
||||
import org.json.simple.JSONObject;
|
||||
import org.qortal.api.model.CrossChainTradeLedgerEntry;
|
||||
import org.qortal.api.model.crosschain.BitcoinyTBDRequest;
|
||||
import org.qortal.asset.Asset;
|
||||
import org.qortal.crosschain.*;
|
||||
import org.qortal.data.account.AccountBalanceData;
|
||||
import org.qortal.data.at.ATData;
|
||||
import org.qortal.data.at.ATStateData;
|
||||
import org.qortal.data.crosschain.*;
|
||||
import org.qortal.event.EventBus;
|
||||
import org.qortal.event.LockingFeeUpdateEvent;
|
||||
import org.qortal.event.RequiredFeeUpdateEvent;
|
||||
import org.qortal.repository.DataException;
|
||||
import org.qortal.repository.Repository;
|
||||
import org.qortal.utils.Amounts;
|
||||
import org.qortal.utils.BitTwiddling;
|
||||
|
||||
import java.io.BufferedWriter;
|
||||
import java.io.IOException;
|
||||
import java.io.Writer;
|
||||
import java.text.DateFormat;
|
||||
import java.text.SimpleDateFormat;
|
||||
import java.util.*;
|
||||
import java.util.function.Function;
|
||||
import java.util.stream.Collectors;
|
||||
|
||||
|
||||
public class CrossChainUtils {
|
||||
public static final String QORT_CURRENCY_CODE = "QORT";
|
||||
private static final Logger LOGGER = LogManager.getLogger(CrossChainUtils.class);
|
||||
public static final String CORE_API_CALL = "Core API Call";
|
||||
public static final String QORTAL_EXCHANGE_LABEL = "Qortal";
|
||||
|
||||
public static ServerConfigurationInfo buildServerConfigurationInfo(Bitcoiny blockchain) {
|
||||
|
||||
@ -104,13 +88,11 @@ public class CrossChainUtils {
|
||||
|
||||
bitcoiny.setFeePerKb(Coin.valueOf(satoshis) );
|
||||
|
||||
EventBus.INSTANCE.notify(new LockingFeeUpdateEvent());
|
||||
|
||||
return String.valueOf(bitcoiny.getFeePerKb().value);
|
||||
}
|
||||
|
||||
/**
|
||||
* Set Fee Required
|
||||
* Set Fee Ceiling
|
||||
*
|
||||
* @param bitcoiny the blockchain support
|
||||
* @param fee the fee in satoshis
|
||||
@ -119,16 +101,14 @@ public class CrossChainUtils {
|
||||
*
|
||||
* @throws IllegalArgumentException if invalid
|
||||
*/
|
||||
public static String setFeeRequired(Bitcoiny bitcoiny, String fee) throws IllegalArgumentException{
|
||||
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.setFeeRequired( Long.parseLong(fee));
|
||||
bitcoiny.setFeeCeiling( Long.parseLong(fee));
|
||||
|
||||
EventBus.INSTANCE.notify(new RequiredFeeUpdateEvent(bitcoiny));
|
||||
|
||||
return String.valueOf(bitcoiny.getFeeRequired());
|
||||
return String.valueOf(bitcoiny.getFeeCeiling());
|
||||
}
|
||||
|
||||
/**
|
||||
@ -237,9 +217,6 @@ public class CrossChainUtils {
|
||||
return bitcoiny.getBlockchainProvider().removeServer(server);
|
||||
}
|
||||
|
||||
public static ChainableServer getCurrentServer( Bitcoiny bitcoiny ) {
|
||||
return bitcoiny.getBlockchainProvider().getCurrentServer();
|
||||
}
|
||||
/**
|
||||
* Set Current Server
|
||||
*
|
||||
@ -655,170 +632,4 @@ public class CrossChainUtils {
|
||||
byte[] lockTimeABytes = BitTwiddling.toBEByteArray((long) lockTimeA);
|
||||
return Bytes.concat(partnerBitcoinPKH, hashOfSecretA, lockTimeABytes);
|
||||
}
|
||||
|
||||
/**
|
||||
* Write To Ledger
|
||||
*
|
||||
* @param writer the writer to the ledger
|
||||
* @param entries the entries to write to the ledger
|
||||
*
|
||||
* @throws IOException
|
||||
*/
|
||||
public static void writeToLedger(Writer writer, List<CrossChainTradeLedgerEntry> entries) throws IOException {
|
||||
|
||||
BufferedWriter bufferedWriter = new BufferedWriter(writer);
|
||||
|
||||
StringJoiner header = new StringJoiner(",");
|
||||
header.add("Market");
|
||||
header.add("Currency");
|
||||
header.add("Quantity");
|
||||
header.add("Commission Paid");
|
||||
header.add("Commission Currency");
|
||||
header.add("Total Price");
|
||||
header.add("Date Time");
|
||||
header.add("Exchange");
|
||||
|
||||
bufferedWriter.append(header.toString());
|
||||
|
||||
DateFormat dateFormatter = new SimpleDateFormat("yyyyMMdd HH:mm");
|
||||
dateFormatter.setTimeZone(TimeZone.getTimeZone("UTC"));
|
||||
|
||||
for( CrossChainTradeLedgerEntry entry : entries ) {
|
||||
StringJoiner joiner = new StringJoiner(",");
|
||||
|
||||
joiner.add(entry.getMarket());
|
||||
joiner.add(entry.getCurrency());
|
||||
joiner.add(String.valueOf(Amounts.prettyAmount(entry.getQuantity())));
|
||||
joiner.add(String.valueOf(Amounts.prettyAmount(entry.getFeeAmount())));
|
||||
joiner.add(entry.getFeeCurrency());
|
||||
joiner.add(String.valueOf(Amounts.prettyAmount(entry.getTotalPrice())));
|
||||
joiner.add(dateFormatter.format(new Date(entry.getTradeTimestamp())));
|
||||
joiner.add(QORTAL_EXCHANGE_LABEL);
|
||||
|
||||
bufferedWriter.newLine();
|
||||
bufferedWriter.append(joiner.toString());
|
||||
}
|
||||
|
||||
bufferedWriter.newLine();
|
||||
bufferedWriter.flush();
|
||||
}
|
||||
|
||||
/**
|
||||
* Create Ledger File Name
|
||||
*
|
||||
* Create a file name the includes timestamp and address.
|
||||
*
|
||||
* @param address the address
|
||||
*
|
||||
* @return the file name created
|
||||
*/
|
||||
public static String createLedgerFileName(String address) {
|
||||
DateFormat dateFormatter = new SimpleDateFormat("yyyyMMddHHmmss");
|
||||
String fileName = "ledger-" + address + "-" + dateFormatter.format(new Date());
|
||||
return fileName;
|
||||
}
|
||||
|
||||
/**
|
||||
* Collect Ledger Entries
|
||||
*
|
||||
* @param publicKey the public key for the ledger entries, buy and sell
|
||||
* @param repository the data repository
|
||||
* @param minimumFinalHeight the minimum block height for entries to be collected
|
||||
* @param entries the ledger entries to add to
|
||||
* @param codeHash code hash for the entry blockchain
|
||||
* @param acct the ACCT for the entry blockchain
|
||||
* @param isBuy true collecting entries for a buy, otherwise false
|
||||
*
|
||||
* @throws DataException
|
||||
*/
|
||||
public static void collectLedgerEntries(
|
||||
byte[] publicKey,
|
||||
Repository repository,
|
||||
Integer minimumFinalHeight,
|
||||
List<CrossChainTradeLedgerEntry> entries,
|
||||
byte[] codeHash,
|
||||
ACCT acct,
|
||||
boolean isBuy) throws DataException {
|
||||
|
||||
// get all the final AT states for the code hash (foreign coin)
|
||||
List<ATStateData> atStates
|
||||
= repository.getATRepository().getMatchingFinalATStates(
|
||||
codeHash,
|
||||
isBuy ? publicKey : null,
|
||||
!isBuy ? publicKey : null,
|
||||
Boolean.TRUE, acct.getModeByteOffset(),
|
||||
(long) AcctMode.REDEEMED.value,
|
||||
minimumFinalHeight,
|
||||
null, null, false
|
||||
);
|
||||
|
||||
String foreignBlockchainCurrencyCode = acct.getBlockchain().getCurrencyCode();
|
||||
|
||||
// for each trade, build ledger entry, collect ledger entry
|
||||
for (ATStateData atState : atStates) {
|
||||
CrossChainTradeData crossChainTradeData = acct.populateTradeData(repository, atState);
|
||||
|
||||
// We also need block timestamp for use as trade timestamp
|
||||
long localTimestamp = repository.getBlockRepository().getTimestampFromHeight(atState.getHeight());
|
||||
|
||||
if (localTimestamp == 0) {
|
||||
// Try the archive
|
||||
localTimestamp = repository.getBlockArchiveRepository().getTimestampFromHeight(atState.getHeight());
|
||||
}
|
||||
|
||||
CrossChainTradeLedgerEntry ledgerEntry
|
||||
= new CrossChainTradeLedgerEntry(
|
||||
isBuy ? QORT_CURRENCY_CODE : foreignBlockchainCurrencyCode,
|
||||
isBuy ? foreignBlockchainCurrencyCode : QORT_CURRENCY_CODE,
|
||||
isBuy ? crossChainTradeData.qortAmount : crossChainTradeData.expectedForeignAmount,
|
||||
0,
|
||||
foreignBlockchainCurrencyCode,
|
||||
isBuy ? crossChainTradeData.expectedForeignAmount : crossChainTradeData.qortAmount,
|
||||
localTimestamp);
|
||||
|
||||
entries.add(ledgerEntry);
|
||||
}
|
||||
}
|
||||
|
||||
public static List<CrossChainTradeData> populateTradeDataList(Repository repository, ACCT acct, List<ATData> atDataList) throws DataException {
|
||||
|
||||
if(atDataList.isEmpty()) return new ArrayList<>(0);
|
||||
|
||||
List<ATStateData> latestATStates
|
||||
= repository.getATRepository()
|
||||
.getLatestATStates(
|
||||
atDataList.stream()
|
||||
.map(ATData::getATAddress)
|
||||
.collect(Collectors.toList())
|
||||
);
|
||||
|
||||
Map<String, ATStateData> atStateDataByAtAddress
|
||||
= latestATStates.stream().collect(Collectors.toMap(ATStateData::getATAddress, Function.identity()));
|
||||
|
||||
Map<String, ATData> atDataByAtAddress
|
||||
= atDataList.stream().collect(Collectors.toMap(ATData::getATAddress, Function.identity()));
|
||||
|
||||
Map<String, Long> balanceByAtAddress
|
||||
= repository
|
||||
.getAccountRepository()
|
||||
.getBalances(new ArrayList<>(atDataByAtAddress.keySet()), Asset.QORT)
|
||||
.stream().collect(Collectors.toMap(AccountBalanceData::getAddress, AccountBalanceData::getBalance));
|
||||
|
||||
List<CrossChainTradeData> crossChainTradeDataList = new ArrayList<>(latestATStates.size());
|
||||
|
||||
for( ATStateData atStateData : latestATStates ) {
|
||||
ATData atData = atDataByAtAddress.get(atStateData.getATAddress());
|
||||
crossChainTradeDataList.add(
|
||||
acct.populateTradeData(
|
||||
repository,
|
||||
atData.getCreatorPublicKey(),
|
||||
atData.getCreation(),
|
||||
atStateData,
|
||||
OptionalLong.of(balanceByAtAddress.get(atStateData.getATAddress()))
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
return crossChainTradeDataList;
|
||||
}
|
||||
}
|
@ -33,7 +33,6 @@ import javax.ws.rs.*;
|
||||
import javax.ws.rs.core.Context;
|
||||
import javax.ws.rs.core.MediaType;
|
||||
import java.util.List;
|
||||
import java.util.Optional;
|
||||
import java.util.stream.Collectors;
|
||||
|
||||
@Path("/names")
|
||||
@ -105,45 +104,6 @@ public class NamesResource {
|
||||
}
|
||||
}
|
||||
|
||||
@GET
|
||||
@Path("/primary/{address}")
|
||||
@Operation(
|
||||
summary = "primary name owned by address",
|
||||
responses = {
|
||||
@ApiResponse(
|
||||
description = "registered primary name info",
|
||||
content = @Content(
|
||||
mediaType = MediaType.APPLICATION_JSON,
|
||||
schema = @Schema(implementation = NameSummary.class)
|
||||
)
|
||||
)
|
||||
}
|
||||
)
|
||||
@ApiErrors({ApiError.INVALID_ADDRESS, ApiError.REPOSITORY_ISSUE, ApiError.UNAUTHORIZED})
|
||||
public NameSummary getPrimaryNameByAddress(@PathParam("address") String address) {
|
||||
if (!Crypto.isValidAddress(address))
|
||||
throw ApiExceptionFactory.INSTANCE.createException(request, ApiError.INVALID_ADDRESS);
|
||||
|
||||
try (final Repository repository = RepositoryManager.getRepository()) {
|
||||
|
||||
if (Settings.getInstance().isLite()) {
|
||||
throw ApiExceptionFactory.INSTANCE.createException(request, ApiError.UNAUTHORIZED);
|
||||
}
|
||||
else {
|
||||
Optional<String> primaryName = repository.getNameRepository().getPrimaryName(address);
|
||||
|
||||
if(primaryName.isPresent()) {
|
||||
return new NameSummary(new NameData(primaryName.get(), address));
|
||||
}
|
||||
else {
|
||||
return new NameSummary((new NameData(null, address)));
|
||||
}
|
||||
}
|
||||
} catch (DataException e) {
|
||||
throw ApiExceptionFactory.INSTANCE.createException(request, ApiError.REPOSITORY_ISSUE, e);
|
||||
}
|
||||
}
|
||||
|
||||
@GET
|
||||
@Path("/{name}")
|
||||
@Operation(
|
||||
|
@ -32,7 +32,6 @@ import org.qortal.controller.Synchronizer.SynchronizationResult;
|
||||
import org.qortal.controller.repository.BlockArchiveRebuilder;
|
||||
import org.qortal.data.account.MintingAccountData;
|
||||
import org.qortal.data.account.RewardShareData;
|
||||
import org.qortal.data.system.DbConnectionInfo;
|
||||
import org.qortal.network.Network;
|
||||
import org.qortal.network.Peer;
|
||||
import org.qortal.network.PeerAddress;
|
||||
@ -41,7 +40,6 @@ import org.qortal.repository.DataException;
|
||||
import org.qortal.repository.Repository;
|
||||
import org.qortal.repository.RepositoryManager;
|
||||
import org.qortal.settings.Settings;
|
||||
import org.qortal.data.system.SystemInfo;
|
||||
import org.qortal.utils.Base58;
|
||||
import org.qortal.utils.NTP;
|
||||
|
||||
@ -54,7 +52,6 @@ import java.net.InetSocketAddress;
|
||||
import java.net.UnknownHostException;
|
||||
import java.nio.file.Files;
|
||||
import java.nio.file.Paths;
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
import java.util.concurrent.TimeUnit;
|
||||
import java.util.concurrent.TimeoutException;
|
||||
@ -1067,29 +1064,4 @@ public class AdminResource {
|
||||
return "true";
|
||||
}
|
||||
|
||||
@GET
|
||||
@Path("/systeminfo")
|
||||
@Operation(
|
||||
summary = "System Information",
|
||||
description = "System memory usage and available processors.",
|
||||
responses = {
|
||||
@ApiResponse(
|
||||
description = "memory usage and available processors",
|
||||
content = @Content(mediaType = MediaType.APPLICATION_JSON, schema = @Schema(implementation = SystemInfo.class))
|
||||
)
|
||||
}
|
||||
)
|
||||
@ApiErrors({ApiError.REPOSITORY_ISSUE})
|
||||
public SystemInfo getSystemInformation() {
|
||||
|
||||
SystemInfo info
|
||||
= new SystemInfo(
|
||||
Runtime.getRuntime().freeMemory(),
|
||||
Runtime.getRuntime().totalMemory() - Runtime.getRuntime().freeMemory(),
|
||||
Runtime.getRuntime().totalMemory(),
|
||||
Runtime.getRuntime().maxMemory(),
|
||||
Runtime.getRuntime().availableProcessors());
|
||||
|
||||
return info;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -71,33 +71,33 @@ public class RenderResource {
|
||||
@Path("/signature/{signature}")
|
||||
@SecurityRequirement(name = "apiKey")
|
||||
public HttpServletResponse getIndexBySignature(@PathParam("signature") String signature,
|
||||
@QueryParam("theme") String theme, @QueryParam("lang") String lang) {
|
||||
@QueryParam("theme") String theme) {
|
||||
if (!Settings.getInstance().isQDNAuthBypassEnabled())
|
||||
Security.requirePriorAuthorization(request, signature, Service.WEBSITE, null);
|
||||
|
||||
return this.get(signature, ResourceIdType.SIGNATURE, null, null, "/", null, "/render/signature", true, true, theme, lang);
|
||||
return this.get(signature, ResourceIdType.SIGNATURE, null, null, "/", null, "/render/signature", true, true, theme);
|
||||
}
|
||||
|
||||
@GET
|
||||
@Path("/signature/{signature}/{path:.*}")
|
||||
@SecurityRequirement(name = "apiKey")
|
||||
public HttpServletResponse getPathBySignature(@PathParam("signature") String signature, @PathParam("path") String inPath,
|
||||
@QueryParam("theme") String theme, @QueryParam("lang") String lang) {
|
||||
@QueryParam("theme") String theme) {
|
||||
if (!Settings.getInstance().isQDNAuthBypassEnabled())
|
||||
Security.requirePriorAuthorization(request, signature, Service.WEBSITE, null);
|
||||
|
||||
return this.get(signature, ResourceIdType.SIGNATURE, null, null, inPath,null, "/render/signature", true, true, theme, lang);
|
||||
return this.get(signature, ResourceIdType.SIGNATURE, null, null, inPath,null, "/render/signature", true, true, theme);
|
||||
}
|
||||
|
||||
@GET
|
||||
@Path("/hash/{hash}")
|
||||
@SecurityRequirement(name = "apiKey")
|
||||
public HttpServletResponse getIndexByHash(@PathParam("hash") String hash58, @QueryParam("secret") String secret58,
|
||||
@QueryParam("theme") String theme, @QueryParam("lang") String lang) {
|
||||
@QueryParam("theme") String theme) {
|
||||
if (!Settings.getInstance().isQDNAuthBypassEnabled())
|
||||
Security.requirePriorAuthorization(request, hash58, Service.WEBSITE, null);
|
||||
|
||||
return this.get(hash58, ResourceIdType.FILE_HASH, Service.ARBITRARY_DATA, null, "/", secret58, "/render/hash", true, false, theme, lang);
|
||||
return this.get(hash58, ResourceIdType.FILE_HASH, Service.ARBITRARY_DATA, null, "/", secret58, "/render/hash", true, false, theme);
|
||||
}
|
||||
|
||||
@GET
|
||||
@ -105,11 +105,11 @@ public class RenderResource {
|
||||
@SecurityRequirement(name = "apiKey")
|
||||
public HttpServletResponse getPathByHash(@PathParam("hash") String hash58, @PathParam("path") String inPath,
|
||||
@QueryParam("secret") String secret58,
|
||||
@QueryParam("theme") String theme, @QueryParam("lang") String lang) {
|
||||
@QueryParam("theme") String theme) {
|
||||
if (!Settings.getInstance().isQDNAuthBypassEnabled())
|
||||
Security.requirePriorAuthorization(request, hash58, Service.WEBSITE, null);
|
||||
|
||||
return this.get(hash58, ResourceIdType.FILE_HASH, Service.ARBITRARY_DATA, null, inPath, secret58, "/render/hash", true, false, theme, lang);
|
||||
return this.get(hash58, ResourceIdType.FILE_HASH, Service.ARBITRARY_DATA, null, inPath, secret58, "/render/hash", true, false, theme);
|
||||
}
|
||||
|
||||
@GET
|
||||
@ -119,12 +119,12 @@ public class RenderResource {
|
||||
@PathParam("name") String name,
|
||||
@PathParam("path") String inPath,
|
||||
@QueryParam("identifier") String identifier,
|
||||
@QueryParam("theme") String theme, @QueryParam("lang") String lang) {
|
||||
@QueryParam("theme") String theme) {
|
||||
if (!Settings.getInstance().isQDNAuthBypassEnabled())
|
||||
Security.requirePriorAuthorization(request, name, service, null);
|
||||
|
||||
String prefix = String.format("/render/%s", service);
|
||||
return this.get(name, ResourceIdType.NAME, service, identifier, inPath, null, prefix, true, true, theme, lang);
|
||||
return this.get(name, ResourceIdType.NAME, service, identifier, inPath, null, prefix, true, true, theme);
|
||||
}
|
||||
|
||||
@GET
|
||||
@ -133,18 +133,18 @@ public class RenderResource {
|
||||
public HttpServletResponse getIndexByName(@PathParam("service") Service service,
|
||||
@PathParam("name") String name,
|
||||
@QueryParam("identifier") String identifier,
|
||||
@QueryParam("theme") String theme, @QueryParam("lang") String lang) {
|
||||
@QueryParam("theme") String theme) {
|
||||
if (!Settings.getInstance().isQDNAuthBypassEnabled())
|
||||
Security.requirePriorAuthorization(request, name, service, null);
|
||||
|
||||
String prefix = String.format("/render/%s", service);
|
||||
return this.get(name, ResourceIdType.NAME, service, identifier, "/", null, prefix, true, true, theme, lang);
|
||||
return this.get(name, ResourceIdType.NAME, service, identifier, "/", null, prefix, true, true, theme);
|
||||
}
|
||||
|
||||
|
||||
|
||||
private HttpServletResponse get(String resourceId, ResourceIdType resourceIdType, Service service, String identifier,
|
||||
String inPath, String secret58, String prefix, boolean includeResourceIdInPrefix, boolean async, String theme, String lang) {
|
||||
String inPath, String secret58, String prefix, boolean includeResourceIdInPrefix, boolean async, String theme) {
|
||||
|
||||
ArbitraryDataRenderer renderer = new ArbitraryDataRenderer(resourceId, resourceIdType, service, identifier, inPath,
|
||||
secret58, prefix, includeResourceIdInPrefix, async, "render", request, response, context);
|
||||
@ -152,9 +152,6 @@ public class RenderResource {
|
||||
if (theme != null) {
|
||||
renderer.setTheme(theme);
|
||||
}
|
||||
if (lang != null) {
|
||||
renderer.setLang(lang);
|
||||
}
|
||||
return renderer.render();
|
||||
}
|
||||
|
||||
|
@ -1,102 +0,0 @@
|
||||
package org.qortal.api.websocket;
|
||||
|
||||
import org.apache.logging.log4j.LogManager;
|
||||
import org.apache.logging.log4j.Logger;
|
||||
import org.eclipse.jetty.websocket.api.Session;
|
||||
import org.eclipse.jetty.websocket.api.WebSocketException;
|
||||
import org.eclipse.jetty.websocket.api.annotations.OnWebSocketClose;
|
||||
import org.eclipse.jetty.websocket.api.annotations.OnWebSocketConnect;
|
||||
import org.eclipse.jetty.websocket.api.annotations.OnWebSocketError;
|
||||
import org.eclipse.jetty.websocket.api.annotations.OnWebSocketMessage;
|
||||
import org.eclipse.jetty.websocket.api.annotations.WebSocket;
|
||||
import org.eclipse.jetty.websocket.servlet.WebSocketServletFactory;
|
||||
import org.qortal.api.ApiError;
|
||||
import org.qortal.controller.Controller;
|
||||
import org.qortal.data.arbitrary.DataMonitorInfo;
|
||||
import org.qortal.event.DataMonitorEvent;
|
||||
import org.qortal.event.Event;
|
||||
import org.qortal.event.EventBus;
|
||||
import org.qortal.event.Listener;
|
||||
import org.qortal.repository.DataException;
|
||||
import org.qortal.repository.Repository;
|
||||
import org.qortal.repository.RepositoryManager;
|
||||
import org.qortal.utils.Base58;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.io.StringWriter;
|
||||
import java.util.List;
|
||||
|
||||
@WebSocket
|
||||
@SuppressWarnings("serial")
|
||||
public class DataMonitorSocket extends ApiWebSocket implements Listener {
|
||||
|
||||
private static final Logger LOGGER = LogManager.getLogger(DataMonitorSocket.class);
|
||||
|
||||
@Override
|
||||
public void configure(WebSocketServletFactory factory) {
|
||||
LOGGER.info("configure");
|
||||
|
||||
factory.register(DataMonitorSocket.class);
|
||||
|
||||
EventBus.INSTANCE.addListener(this);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void listen(Event event) {
|
||||
if (!(event instanceof DataMonitorEvent))
|
||||
return;
|
||||
|
||||
DataMonitorEvent dataMonitorEvent = (DataMonitorEvent) event;
|
||||
|
||||
for (Session session : getSessions())
|
||||
sendDataEventSummary(session, buildInfo(dataMonitorEvent));
|
||||
}
|
||||
|
||||
private DataMonitorInfo buildInfo(DataMonitorEvent dataMonitorEvent) {
|
||||
|
||||
return new DataMonitorInfo(
|
||||
dataMonitorEvent.getTimestamp(),
|
||||
dataMonitorEvent.getIdentifier(),
|
||||
dataMonitorEvent.getName(),
|
||||
dataMonitorEvent.getService(),
|
||||
dataMonitorEvent.getDescription(),
|
||||
dataMonitorEvent.getTransactionTimestamp(),
|
||||
dataMonitorEvent.getLatestPutTimestamp()
|
||||
);
|
||||
}
|
||||
|
||||
@OnWebSocketConnect
|
||||
@Override
|
||||
public void onWebSocketConnect(Session session) {
|
||||
super.onWebSocketConnect(session);
|
||||
}
|
||||
|
||||
@OnWebSocketClose
|
||||
@Override
|
||||
public void onWebSocketClose(Session session, int statusCode, String reason) {
|
||||
super.onWebSocketClose(session, statusCode, reason);
|
||||
}
|
||||
|
||||
@OnWebSocketError
|
||||
public void onWebSocketError(Session session, Throwable throwable) {
|
||||
/* We ignore errors for now, but method here to silence log spam */
|
||||
}
|
||||
|
||||
@OnWebSocketMessage
|
||||
public void onWebSocketMessage(Session session, String message) {
|
||||
LOGGER.info("onWebSocketMessage: message = " + message);
|
||||
}
|
||||
|
||||
private void sendDataEventSummary(Session session, DataMonitorInfo dataMonitorInfo) {
|
||||
StringWriter stringWriter = new StringWriter();
|
||||
|
||||
try {
|
||||
marshall(stringWriter, dataMonitorInfo);
|
||||
|
||||
session.getRemote().sendStringByFuture(stringWriter.toString());
|
||||
} catch (IOException | WebSocketException e) {
|
||||
// No output this time
|
||||
}
|
||||
}
|
||||
|
||||
}
|
@ -98,7 +98,7 @@ public class TradeOffersWebSocket extends ApiWebSocket implements Listener {
|
||||
byte[] codeHash = acctInfo.getKey().value;
|
||||
ACCT acct = acctInfo.getValue().get();
|
||||
|
||||
List<ATStateData> atStates = repository.getATRepository().getMatchingFinalATStates(codeHash, null, null,
|
||||
List<ATStateData> atStates = repository.getATRepository().getMatchingFinalATStates(codeHash,
|
||||
isFinished, dataByteOffset, expectedValue, minimumFinalHeight,
|
||||
null, null, null);
|
||||
|
||||
@ -259,7 +259,7 @@ public class TradeOffersWebSocket extends ApiWebSocket implements Listener {
|
||||
ACCT acct = acctInfo.getValue().get();
|
||||
|
||||
Integer dataByteOffset = acct.getModeByteOffset();
|
||||
List<ATStateData> initialAtStates = repository.getATRepository().getMatchingFinalATStates(codeHash, null, null,
|
||||
List<ATStateData> initialAtStates = repository.getATRepository().getMatchingFinalATStates(codeHash,
|
||||
isFinished, dataByteOffset, expectedValue, minimumFinalHeight,
|
||||
null, null, null);
|
||||
|
||||
@ -298,7 +298,7 @@ public class TradeOffersWebSocket extends ApiWebSocket implements Listener {
|
||||
byte[] codeHash = acctInfo.getKey().value;
|
||||
ACCT acct = acctInfo.getValue().get();
|
||||
|
||||
List<ATStateData> historicAtStates = repository.getATRepository().getMatchingFinalATStates(codeHash, null, null,
|
||||
List<ATStateData> historicAtStates = repository.getATRepository().getMatchingFinalATStates(codeHash,
|
||||
isFinished, dataByteOffset, expectedValue, minimumFinalHeight,
|
||||
null, null, null);
|
||||
|
||||
|
@ -1,83 +0,0 @@
|
||||
package org.qortal.api.websocket;
|
||||
|
||||
import org.apache.logging.log4j.LogManager;
|
||||
import org.apache.logging.log4j.Logger;
|
||||
import org.eclipse.jetty.websocket.api.Session;
|
||||
import org.eclipse.jetty.websocket.api.WebSocketException;
|
||||
import org.eclipse.jetty.websocket.api.annotations.OnWebSocketClose;
|
||||
import org.eclipse.jetty.websocket.api.annotations.OnWebSocketConnect;
|
||||
import org.eclipse.jetty.websocket.api.annotations.OnWebSocketError;
|
||||
import org.eclipse.jetty.websocket.api.annotations.OnWebSocketMessage;
|
||||
import org.eclipse.jetty.websocket.api.annotations.WebSocket;
|
||||
import org.eclipse.jetty.websocket.servlet.WebSocketServletFactory;
|
||||
import org.qortal.data.crosschain.UnsignedFeeEvent;
|
||||
import org.qortal.event.Event;
|
||||
import org.qortal.event.EventBus;
|
||||
import org.qortal.event.FeeWaitingEvent;
|
||||
import org.qortal.event.Listener;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.io.StringWriter;
|
||||
|
||||
@WebSocket
|
||||
@SuppressWarnings("serial")
|
||||
public class UnsignedFeesSocket extends ApiWebSocket implements Listener {
|
||||
|
||||
private static final Logger LOGGER = LogManager.getLogger(UnsignedFeesSocket.class);
|
||||
|
||||
@Override
|
||||
public void configure(WebSocketServletFactory factory) {
|
||||
LOGGER.info("configure");
|
||||
|
||||
factory.register(UnsignedFeesSocket.class);
|
||||
|
||||
EventBus.INSTANCE.addListener(this);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void listen(Event event) {
|
||||
if (!(event instanceof FeeWaitingEvent))
|
||||
return;
|
||||
|
||||
for (Session session : getSessions()) {
|
||||
FeeWaitingEvent feeWaitingEvent = (FeeWaitingEvent) event;
|
||||
sendUnsignedFeeEvent(session, new UnsignedFeeEvent(feeWaitingEvent.isPositive(), feeWaitingEvent.getAddress()));
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@OnWebSocketConnect
|
||||
@Override
|
||||
public void onWebSocketConnect(Session session) {
|
||||
super.onWebSocketConnect(session);
|
||||
}
|
||||
|
||||
@OnWebSocketClose
|
||||
@Override
|
||||
public void onWebSocketClose(Session session, int statusCode, String reason) {
|
||||
super.onWebSocketClose(session, statusCode, reason);
|
||||
}
|
||||
|
||||
@OnWebSocketError
|
||||
public void onWebSocketError(Session session, Throwable throwable) {
|
||||
/* We ignore errors for now, but method here to silence log spam */
|
||||
}
|
||||
|
||||
@OnWebSocketMessage
|
||||
public void onWebSocketMessage(Session session, String message) {
|
||||
LOGGER.info("onWebSocketMessage: message = " + message);
|
||||
}
|
||||
|
||||
private void sendUnsignedFeeEvent(Session session, UnsignedFeeEvent unsignedFeeEvent) {
|
||||
StringWriter stringWriter = new StringWriter();
|
||||
|
||||
try {
|
||||
marshall(stringWriter, unsignedFeeEvent);
|
||||
|
||||
session.getRemote().sendStringByFuture(stringWriter.toString());
|
||||
} catch (IOException | WebSocketException e) {
|
||||
// No output this time
|
||||
}
|
||||
}
|
||||
|
||||
}
|
@ -4,12 +4,9 @@ import org.qortal.repository.DataException;
|
||||
import org.qortal.utils.Base58;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.io.InputStream;
|
||||
import java.nio.charset.StandardCharsets;
|
||||
import java.nio.file.FileSystem;
|
||||
import java.nio.file.Files;
|
||||
import java.nio.file.Path;
|
||||
import java.nio.file.Paths;
|
||||
import java.security.MessageDigest;
|
||||
import java.security.NoSuchAlgorithmException;
|
||||
import java.util.Arrays;
|
||||
@ -26,53 +23,37 @@ public class ArbitraryDataDigest {
|
||||
}
|
||||
|
||||
public void compute() throws IOException, DataException {
|
||||
List<Path> allPaths = Files.walk(path)
|
||||
.filter(Files::isRegularFile)
|
||||
.sorted()
|
||||
.collect(Collectors.toList());
|
||||
|
||||
List<Path> allPaths = Files.walk(path).filter(Files::isRegularFile).sorted().collect(Collectors.toList());
|
||||
Path basePathAbsolute = this.path.toAbsolutePath();
|
||||
|
||||
|
||||
MessageDigest sha256;
|
||||
try {
|
||||
sha256 = MessageDigest.getInstance("SHA-256");
|
||||
} catch (NoSuchAlgorithmException e) {
|
||||
throw new DataException("SHA-256 hashing algorithm unavailable");
|
||||
}
|
||||
|
||||
|
||||
for (Path path : allPaths) {
|
||||
// We need to work with paths relative to the base path, to ensure the same hash
|
||||
// is generated on different systems
|
||||
Path relativePath = basePathAbsolute.relativize(path.toAbsolutePath());
|
||||
|
||||
|
||||
// Exclude Qortal folder since it can be different each time
|
||||
// We only care about hashing the actual user data
|
||||
if (relativePath.startsWith(".qortal/")) {
|
||||
continue;
|
||||
}
|
||||
|
||||
// Account for \ VS / : Linux VS Windows
|
||||
String pathString = relativePath.toString();
|
||||
if (relativePath.getFileSystem().toString().contains("Windows")) {
|
||||
pathString = pathString.replace("\\", "/");
|
||||
}
|
||||
|
||||
|
||||
// Hash path
|
||||
byte[] filePathBytes = pathString.getBytes(StandardCharsets.UTF_8);
|
||||
byte[] filePathBytes = relativePath.toString().getBytes(StandardCharsets.UTF_8);
|
||||
sha256.update(filePathBytes);
|
||||
|
||||
try (InputStream in = Files.newInputStream(path)) {
|
||||
byte[] buffer = new byte[65536]; // 64 KB
|
||||
int bytesRead;
|
||||
while ((bytesRead = in.read(buffer)) != -1) {
|
||||
sha256.update(buffer, 0, bytesRead);
|
||||
}
|
||||
}
|
||||
|
||||
// Hash contents
|
||||
byte[] fileContent = Files.readAllBytes(path);
|
||||
sha256.update(fileContent);
|
||||
}
|
||||
|
||||
this.hash = sha256.digest();
|
||||
}
|
||||
|
||||
|
||||
public boolean isHashValid(byte[] hash) {
|
||||
return Arrays.equals(hash, this.hash);
|
||||
|
@ -52,7 +52,7 @@ public class ArbitraryDataFile {
|
||||
|
||||
private static final Logger LOGGER = LogManager.getLogger(ArbitraryDataFile.class);
|
||||
|
||||
public static final long MAX_FILE_SIZE = 2L * 1024 * 1024 * 1024; // 2 GiB
|
||||
public static final long MAX_FILE_SIZE = 500 * 1024 * 1024; // 500MiB
|
||||
protected static final int MAX_CHUNK_SIZE = 1 * 1024 * 1024; // 1MiB
|
||||
public static final int CHUNK_SIZE = 512 * 1024; // 0.5MiB
|
||||
public static int SHORT_DIGEST_LENGTH = 8;
|
||||
|
@ -439,15 +439,7 @@ public class ArbitraryDataReader {
|
||||
// Ensure the complete hash matches the joined chunks
|
||||
if (!Arrays.equals(arbitraryDataFile.digest(), transactionData.getData())) {
|
||||
// Delete the invalid file
|
||||
LOGGER.info("Deleting invalid file: path = " + arbitraryDataFile.getFilePath());
|
||||
|
||||
if( arbitraryDataFile.delete() ) {
|
||||
LOGGER.info("Deleted invalid file successfully: path = " + arbitraryDataFile.getFilePath());
|
||||
}
|
||||
else {
|
||||
LOGGER.warn("Could not delete invalid file: path = " + arbitraryDataFile.getFilePath());
|
||||
}
|
||||
|
||||
arbitraryDataFile.delete();
|
||||
throw new DataException("Unable to validate complete file hash");
|
||||
}
|
||||
}
|
||||
|
@ -1,7 +1,6 @@
|
||||
package org.qortal.arbitrary;
|
||||
|
||||
import com.google.common.io.Resources;
|
||||
|
||||
import org.apache.commons.io.FileUtils;
|
||||
import org.apache.commons.lang3.ArrayUtils;
|
||||
import org.apache.logging.log4j.LogManager;
|
||||
@ -16,13 +15,11 @@ import org.qortal.settings.Settings;
|
||||
import javax.servlet.ServletContext;
|
||||
import javax.servlet.http.HttpServletRequest;
|
||||
import javax.servlet.http.HttpServletResponse;
|
||||
|
||||
import java.io.File;
|
||||
import java.io.FileInputStream;
|
||||
import java.io.FileNotFoundException;
|
||||
import java.io.IOException;
|
||||
import java.net.URL;
|
||||
|
||||
import java.nio.charset.StandardCharsets;
|
||||
import java.nio.file.Files;
|
||||
import java.nio.file.NoSuchFileException;
|
||||
@ -40,7 +37,6 @@ public class ArbitraryDataRenderer {
|
||||
private final Service service;
|
||||
private final String identifier;
|
||||
private String theme = "light";
|
||||
private String lang = "en";
|
||||
private String inPath;
|
||||
private final String secret58;
|
||||
private final String prefix;
|
||||
@ -170,16 +166,9 @@ public class ArbitraryDataRenderer {
|
||||
if (HTMLParser.isHtmlFile(filename)) {
|
||||
// HTML file - needs to be parsed
|
||||
byte[] data = Files.readAllBytes(filePath); // TODO: limit file size that can be read into memory
|
||||
String encodedResourceId;
|
||||
|
||||
if (resourceIdType == ResourceIdType.NAME) {
|
||||
encodedResourceId = resourceId.replace(" ", "%20");
|
||||
} else {
|
||||
encodedResourceId = resourceId;
|
||||
}
|
||||
HTMLParser htmlParser = new HTMLParser(encodedResourceId, inPath, prefix, includeResourceIdInPrefix, data, qdnContext, service, identifier, theme, usingCustomRouting, lang);
|
||||
HTMLParser htmlParser = new HTMLParser(resourceId, inPath, prefix, includeResourceIdInPrefix, data, qdnContext, service, identifier, theme, usingCustomRouting);
|
||||
htmlParser.addAdditionalHeaderTags();
|
||||
response.addHeader("Content-Security-Policy", "default-src 'self' 'unsafe-inline' 'unsafe-eval'; font-src 'self' data:; media-src 'self' data: blob:; img-src 'self' data: blob:; connect-src 'self' wss: blob:;");
|
||||
response.addHeader("Content-Security-Policy", "default-src 'self' 'unsafe-inline' 'unsafe-eval'; font-src 'self' data:; media-src 'self' data: blob:; img-src 'self' data: blob:; connect-src 'self' wss:;");
|
||||
response.setContentType(context.getMimeType(filename));
|
||||
response.setContentLength(htmlParser.getData().length);
|
||||
response.getOutputStream().write(htmlParser.getData());
|
||||
@ -267,8 +256,5 @@ public class ArbitraryDataRenderer {
|
||||
public void setTheme(String theme) {
|
||||
this.theme = theme;
|
||||
}
|
||||
public void setLang(String lang) {
|
||||
this.lang = lang;
|
||||
}
|
||||
|
||||
}
|
||||
|
@ -29,7 +29,6 @@ import org.qortal.utils.FilesystemUtils;
|
||||
import org.qortal.utils.NTP;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.nio.file.Files;
|
||||
import java.nio.file.Path;
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
@ -198,7 +197,7 @@ public class ArbitraryDataTransactionBuilder {
|
||||
|
||||
// We can't use PATCH for on-chain data because this requires the .qortal directory, which can't be put on chain
|
||||
final boolean isSingleFileResource = FilesystemUtils.isSingleFileResource(this.path, false);
|
||||
final boolean shouldUseOnChainData = (isSingleFileResource && AES.getEncryptedFileSize(Files.size(path)) <= ArbitraryTransaction.MAX_DATA_SIZE);
|
||||
final boolean shouldUseOnChainData = (isSingleFileResource && AES.getEncryptedFileSize(FilesystemUtils.getSingleFileContents(path).length) <= ArbitraryTransaction.MAX_DATA_SIZE);
|
||||
if (shouldUseOnChainData) {
|
||||
LOGGER.info("Data size is small enough to go on chain - using PUT");
|
||||
return Method.PUT;
|
||||
@ -246,7 +245,7 @@ public class ArbitraryDataTransactionBuilder {
|
||||
|
||||
// Single file resources are handled differently, especially for very small data payloads, as these go on chain
|
||||
final boolean isSingleFileResource = FilesystemUtils.isSingleFileResource(path, false);
|
||||
final boolean shouldUseOnChainData = (isSingleFileResource && AES.getEncryptedFileSize(Files.size(path)) <= ArbitraryTransaction.MAX_DATA_SIZE);
|
||||
final boolean shouldUseOnChainData = (isSingleFileResource && AES.getEncryptedFileSize(FilesystemUtils.getSingleFileContents(path).length) <= ArbitraryTransaction.MAX_DATA_SIZE);
|
||||
|
||||
// Use zip compression if data isn't going on chain
|
||||
Compression compression = shouldUseOnChainData ? Compression.NONE : Compression.ZIP;
|
||||
|
@ -37,7 +37,7 @@ public enum Service {
|
||||
if (files != null && files[0] != null) {
|
||||
final String extension = FilenameUtils.getExtension(files[0].getName()).toLowerCase();
|
||||
// We must allow blank file extensions because these are used by data published from a plaintext or base64-encoded string
|
||||
final List<String> allowedExtensions = Arrays.asList("qortal", "zip", "pdf", "txt", "odt", "ods", "doc", "docx", "xls", "xlsx", "ppt", "pptx", "");
|
||||
final List<String> allowedExtensions = Arrays.asList("zip", "pdf", "txt", "odt", "ods", "doc", "docx", "xls", "xlsx", "ppt", "pptx", "");
|
||||
if (extension == null || !allowedExtensions.contains(extension)) {
|
||||
return ValidationResult.INVALID_FILE_EXTENSION;
|
||||
}
|
||||
@ -62,17 +62,7 @@ public enum Service {
|
||||
|
||||
// Custom validation function to require an index HTML file in the root directory
|
||||
List<String> fileNames = ArbitraryDataRenderer.indexFiles();
|
||||
List<String> files;
|
||||
|
||||
// single files are paackaged differently
|
||||
if( path.toFile().isFile() ) {
|
||||
files = new ArrayList<>(1);
|
||||
files.add(path.getFileName().toString());
|
||||
}
|
||||
else {
|
||||
files = new ArrayList<>(Arrays.asList(path.toFile().list()));
|
||||
}
|
||||
|
||||
String[] files = path.toFile().list();
|
||||
if (files != null) {
|
||||
for (String file : files) {
|
||||
Path fileName = Paths.get(file).getFileName();
|
||||
|
@ -23,10 +23,8 @@ import org.qortal.data.at.ATStateData;
|
||||
import org.qortal.data.block.BlockData;
|
||||
import org.qortal.data.block.BlockSummaryData;
|
||||
import org.qortal.data.block.BlockTransactionData;
|
||||
import org.qortal.data.group.GroupAdminData;
|
||||
import org.qortal.data.network.OnlineAccountData;
|
||||
import org.qortal.data.transaction.TransactionData;
|
||||
import org.qortal.group.Group;
|
||||
import org.qortal.repository.*;
|
||||
import org.qortal.settings.Settings;
|
||||
import org.qortal.transaction.AtTransaction;
|
||||
@ -39,7 +37,6 @@ import org.qortal.transform.block.BlockTransformer;
|
||||
import org.qortal.transform.transaction.TransactionTransformer;
|
||||
import org.qortal.utils.Amounts;
|
||||
import org.qortal.utils.Base58;
|
||||
import org.qortal.utils.Groups;
|
||||
import org.qortal.utils.NTP;
|
||||
|
||||
import java.io.ByteArrayOutputStream;
|
||||
@ -151,7 +148,7 @@ public class Block {
|
||||
|
||||
final BlockChain blockChain = BlockChain.getInstance();
|
||||
|
||||
ExpandedAccount(Repository repository, RewardShareData rewardShareData, int blockHeight) throws DataException {
|
||||
ExpandedAccount(Repository repository, RewardShareData rewardShareData) throws DataException {
|
||||
this.rewardShareData = rewardShareData;
|
||||
this.sharePercent = this.rewardShareData.getSharePercent();
|
||||
|
||||
@ -160,12 +157,7 @@ public class Block {
|
||||
this.isMinterFounder = Account.isFounder(mintingAccountData.getFlags());
|
||||
|
||||
this.isRecipientAlsoMinter = this.rewardShareData.getRecipient().equals(this.mintingAccount.getAddress());
|
||||
this.isMinterMember
|
||||
= Groups.memberExistsInAnyGroup(
|
||||
repository.getGroupRepository(),
|
||||
Groups.getGroupIdsToMint(BlockChain.getInstance(), blockHeight),
|
||||
this.mintingAccount.getAddress()
|
||||
);
|
||||
this.isMinterMember = repository.getGroupRepository().memberExists(BlockChain.getInstance().getMintingGroupId(), this.mintingAccount.getAddress());
|
||||
|
||||
if (this.isRecipientAlsoMinter) {
|
||||
// Self-share: minter is also recipient
|
||||
@ -178,19 +170,6 @@ public class Block {
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Get Effective Minting Level
|
||||
*
|
||||
* @return the effective minting level, if a data exception is thrown, it catches the exception and returns a zero
|
||||
*/
|
||||
public int getEffectiveMintingLevel() {
|
||||
try {
|
||||
return this.mintingAccount.getEffectiveMintingLevel();
|
||||
} catch (DataException e) {
|
||||
return 0;
|
||||
}
|
||||
}
|
||||
|
||||
public Account getMintingAccount() {
|
||||
return this.mintingAccount;
|
||||
}
|
||||
@ -207,7 +186,7 @@ public class Block {
|
||||
* @return account-level share "bin" from blockchain config, or null if founder / none found
|
||||
*/
|
||||
public AccountLevelShareBin getShareBin(int blockHeight) {
|
||||
if (this.isMinterFounder && blockHeight < BlockChain.getInstance().getAdminsReplaceFoundersHeight())
|
||||
if (this.isMinterFounder)
|
||||
return null;
|
||||
|
||||
final int accountLevel = this.mintingAccountData.getLevel();
|
||||
@ -424,9 +403,7 @@ public class Block {
|
||||
onlineAccounts.removeIf(a -> a.getNonce() == null || a.getNonce() < 0);
|
||||
|
||||
// After feature trigger, remove any online accounts that are level 0
|
||||
// but only if they are before the ignore level feature trigger
|
||||
if (height < BlockChain.getInstance().getIgnoreLevelForRewardShareHeight() &&
|
||||
height >= BlockChain.getInstance().getOnlineAccountMinterLevelValidationHeight()) {
|
||||
if (height >= BlockChain.getInstance().getOnlineAccountMinterLevelValidationHeight()) {
|
||||
onlineAccounts.removeIf(a -> {
|
||||
try {
|
||||
return Account.getRewardShareEffectiveMintingLevel(repository, a.getPublicKey()) == 0;
|
||||
@ -441,9 +418,9 @@ public class Block {
|
||||
if (height >= BlockChain.getInstance().getGroupMemberCheckHeight()) {
|
||||
onlineAccounts.removeIf(a -> {
|
||||
try {
|
||||
List<Integer> groupIdsToMint = Groups.getGroupIdsToMint(BlockChain.getInstance(), height);
|
||||
int groupId = BlockChain.getInstance().getMintingGroupId();
|
||||
String address = Account.getRewardShareMintingAddress(repository, a.getPublicKey());
|
||||
boolean isMinterGroupMember = Groups.memberExistsInAnyGroup(repository.getGroupRepository(), groupIdsToMint, address);
|
||||
boolean isMinterGroupMember = repository.getGroupRepository().memberExists(groupId, address);
|
||||
return !isMinterGroupMember;
|
||||
} catch (DataException e) {
|
||||
// Something went wrong, so remove the account
|
||||
@ -759,7 +736,15 @@ public class Block {
|
||||
List<ExpandedAccount> expandedAccounts = new ArrayList<>();
|
||||
|
||||
for (RewardShareData rewardShare : this.cachedOnlineRewardShares) {
|
||||
expandedAccounts.add(new ExpandedAccount(repository, rewardShare, this.blockData.getHeight()));
|
||||
int groupId = BlockChain.getInstance().getMintingGroupId();
|
||||
String address = rewardShare.getMinter();
|
||||
boolean isMinterGroupMember = repository.getGroupRepository().memberExists(groupId, address);
|
||||
|
||||
if (this.getBlockData().getHeight() < BlockChain.getInstance().getFixBatchRewardHeight())
|
||||
expandedAccounts.add(new ExpandedAccount(repository, rewardShare));
|
||||
|
||||
if (this.getBlockData().getHeight() >= BlockChain.getInstance().getFixBatchRewardHeight() && isMinterGroupMember)
|
||||
expandedAccounts.add(new ExpandedAccount(repository, rewardShare));
|
||||
}
|
||||
|
||||
this.cachedExpandedAccounts = expandedAccounts;
|
||||
@ -1169,32 +1154,23 @@ public class Block {
|
||||
if (onlineRewardShares == null)
|
||||
return ValidationResult.ONLINE_ACCOUNT_UNKNOWN;
|
||||
|
||||
// After feature trigger, require all online account minters to be greater than level 0,
|
||||
// but only if it is before the feature trigger where we ignore level again
|
||||
if (this.blockData.getHeight() < BlockChain.getInstance().getIgnoreLevelForRewardShareHeight() &&
|
||||
this.getBlockData().getHeight() >= BlockChain.getInstance().getOnlineAccountMinterLevelValidationHeight()) {
|
||||
List<ExpandedAccount> expandedAccounts
|
||||
= this.getExpandedAccounts().stream()
|
||||
.filter(expandedAccount -> expandedAccount.isMinterMember)
|
||||
.collect(Collectors.toList());
|
||||
|
||||
// After feature trigger, require all online account minters to be greater than level 0
|
||||
if (this.getBlockData().getHeight() >= BlockChain.getInstance().getOnlineAccountMinterLevelValidationHeight()) {
|
||||
List<ExpandedAccount> expandedAccounts = this.getExpandedAccounts();
|
||||
for (ExpandedAccount account : expandedAccounts) {
|
||||
int groupId = BlockChain.getInstance().getMintingGroupId();
|
||||
String address = account.getMintingAccount().getAddress();
|
||||
boolean isMinterGroupMember = repository.getGroupRepository().memberExists(groupId, address);
|
||||
|
||||
if (account.getMintingAccount().getEffectiveMintingLevel() == 0)
|
||||
return ValidationResult.ONLINE_ACCOUNTS_INVALID;
|
||||
|
||||
if (this.getBlockData().getHeight() >= BlockChain.getInstance().getFixBatchRewardHeight()) {
|
||||
if (!account.isMinterMember)
|
||||
if (!isMinterGroupMember)
|
||||
return ValidationResult.ONLINE_ACCOUNTS_INVALID;
|
||||
}
|
||||
}
|
||||
}
|
||||
else if (this.blockData.getHeight() >= BlockChain.getInstance().getIgnoreLevelForRewardShareHeight()){
|
||||
Optional<ExpandedAccount> anyInvalidAccount
|
||||
= this.getExpandedAccounts().stream()
|
||||
.filter(account -> !account.isMinterMember)
|
||||
.findAny();
|
||||
if( anyInvalidAccount.isPresent() ) return ValidationResult.ONLINE_ACCOUNTS_INVALID;
|
||||
}
|
||||
|
||||
// If block is past a certain age then we simply assume the signatures were correct
|
||||
long signatureRequirementThreshold = NTP.getTime() - BlockChain.getInstance().getOnlineAccountSignaturesMinLifetime();
|
||||
@ -1640,8 +1616,6 @@ public class Block {
|
||||
SelfSponsorshipAlgoV2Block.processAccountPenalties(this);
|
||||
} else if (this.blockData.getHeight() == BlockChain.getInstance().getSelfSponsorshipAlgoV3Height()) {
|
||||
SelfSponsorshipAlgoV3Block.processAccountPenalties(this);
|
||||
} else if (this.blockData.getHeight() == BlockChain.getInstance().getMultipleNamesPerAccountHeight()) {
|
||||
PrimaryNamesBlock.processNames(this.repository);
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -1685,17 +1659,7 @@ public class Block {
|
||||
final List<Integer> cumulativeBlocksByLevel = BlockChain.getInstance().getCumulativeBlocksByLevel();
|
||||
final int maximumLevel = cumulativeBlocksByLevel.size() - 1;
|
||||
|
||||
final List<ExpandedAccount> expandedAccounts;
|
||||
|
||||
if (this.getBlockData().getHeight() < BlockChain.getInstance().getFixBatchRewardHeight()) {
|
||||
expandedAccounts = this.getExpandedAccounts().stream().collect(Collectors.toList());
|
||||
}
|
||||
else {
|
||||
expandedAccounts
|
||||
= this.getExpandedAccounts().stream()
|
||||
.filter(expandedAccount -> expandedAccount.isMinterMember)
|
||||
.collect(Collectors.toList());
|
||||
}
|
||||
final List<ExpandedAccount> expandedAccounts = this.getExpandedAccounts();
|
||||
|
||||
Set<AccountData> allUniqueExpandedAccounts = new HashSet<>();
|
||||
for (ExpandedAccount expandedAccount : expandedAccounts) {
|
||||
@ -1723,19 +1687,11 @@ public class Block {
|
||||
accountData.setBlocksMinted(accountData.getBlocksMinted() + 1);
|
||||
LOGGER.trace(() -> String.format("Block minter %s up to %d minted block%s", accountData.getAddress(), accountData.getBlocksMinted(), (accountData.getBlocksMinted() != 1 ? "s" : "")));
|
||||
|
||||
int blocksMintedAdjustment
|
||||
=
|
||||
(this.blockData.getHeight() > BlockChain.getInstance().getMintedBlocksAdjustmentRemovalHeight())
|
||||
?
|
||||
0
|
||||
:
|
||||
accountData.getBlocksMintedAdjustment();
|
||||
|
||||
final int effectiveBlocksMinted = accountData.getBlocksMinted() + blocksMintedAdjustment + accountData.getBlocksMintedPenalty();
|
||||
final int effectiveBlocksMinted = accountData.getBlocksMinted() + accountData.getBlocksMintedAdjustment() + accountData.getBlocksMintedPenalty();
|
||||
|
||||
for (int newLevel = maximumLevel; newLevel >= 0; --newLevel)
|
||||
if (effectiveBlocksMinted >= cumulativeBlocksByLevel.get(newLevel)) {
|
||||
if (newLevel != accountData.getLevel()) {
|
||||
if (newLevel > accountData.getLevel()) {
|
||||
// Account has increased in level!
|
||||
accountData.setLevel(newLevel);
|
||||
bumpedAccounts.put(accountData.getAddress(), newLevel);
|
||||
@ -1962,8 +1918,6 @@ public class Block {
|
||||
SelfSponsorshipAlgoV2Block.orphanAccountPenalties(this);
|
||||
} else if (this.blockData.getHeight() == BlockChain.getInstance().getSelfSponsorshipAlgoV3Height()) {
|
||||
SelfSponsorshipAlgoV3Block.orphanAccountPenalties(this);
|
||||
} else if (this.blockData.getHeight() == BlockChain.getInstance().getMultipleNamesPerAccountHeight()) {
|
||||
PrimaryNamesBlock.orphanNames( this.repository );
|
||||
}
|
||||
}
|
||||
|
||||
@ -2105,17 +2059,7 @@ public class Block {
|
||||
final List<Integer> cumulativeBlocksByLevel = BlockChain.getInstance().getCumulativeBlocksByLevel();
|
||||
final int maximumLevel = cumulativeBlocksByLevel.size() - 1;
|
||||
|
||||
final List<ExpandedAccount> expandedAccounts;
|
||||
|
||||
if (this.getBlockData().getHeight() < BlockChain.getInstance().getFixBatchRewardHeight()) {
|
||||
expandedAccounts = this.getExpandedAccounts().stream().collect(Collectors.toList());
|
||||
}
|
||||
else {
|
||||
expandedAccounts
|
||||
= this.getExpandedAccounts().stream()
|
||||
.filter(expandedAccount -> expandedAccount.isMinterMember)
|
||||
.collect(Collectors.toList());
|
||||
}
|
||||
final List<ExpandedAccount> expandedAccounts = this.getExpandedAccounts();
|
||||
|
||||
Set<AccountData> allUniqueExpandedAccounts = new HashSet<>();
|
||||
for (ExpandedAccount expandedAccount : expandedAccounts) {
|
||||
@ -2139,19 +2083,11 @@ public class Block {
|
||||
accountData.setBlocksMinted(accountData.getBlocksMinted() - 1);
|
||||
LOGGER.trace(() -> String.format("Block minter %s down to %d minted block%s", accountData.getAddress(), accountData.getBlocksMinted(), (accountData.getBlocksMinted() != 1 ? "s" : "")));
|
||||
|
||||
int blocksMintedAdjustment
|
||||
=
|
||||
(this.blockData.getHeight() -1 > BlockChain.getInstance().getMintedBlocksAdjustmentRemovalHeight())
|
||||
?
|
||||
0
|
||||
:
|
||||
accountData.getBlocksMintedAdjustment();
|
||||
|
||||
final int effectiveBlocksMinted = accountData.getBlocksMinted() + blocksMintedAdjustment + accountData.getBlocksMintedPenalty();
|
||||
final int effectiveBlocksMinted = accountData.getBlocksMinted() + accountData.getBlocksMintedAdjustment() + accountData.getBlocksMintedPenalty();
|
||||
|
||||
for (int newLevel = maximumLevel; newLevel >= 0; --newLevel)
|
||||
if (effectiveBlocksMinted >= cumulativeBlocksByLevel.get(newLevel)) {
|
||||
if (newLevel != accountData.getLevel()) {
|
||||
if (newLevel < accountData.getLevel()) {
|
||||
// Account has decreased in level!
|
||||
accountData.setLevel(newLevel);
|
||||
repository.getAccountRepository().setLevel(accountData);
|
||||
@ -2327,17 +2263,7 @@ public class Block {
|
||||
List<BlockRewardCandidate> rewardCandidates = new ArrayList<>();
|
||||
|
||||
// All online accounts
|
||||
final List<ExpandedAccount> expandedAccounts;
|
||||
|
||||
if (this.getBlockData().getHeight() < BlockChain.getInstance().getFixBatchRewardHeight()) {
|
||||
expandedAccounts = this.getExpandedAccounts().stream().collect(Collectors.toList());
|
||||
}
|
||||
else {
|
||||
expandedAccounts
|
||||
= this.getExpandedAccounts().stream()
|
||||
.filter(expandedAccount -> expandedAccount.isMinterMember)
|
||||
.collect(Collectors.toList());
|
||||
}
|
||||
final List<ExpandedAccount> expandedAccounts = this.getExpandedAccounts();
|
||||
|
||||
/*
|
||||
* Distribution rules:
|
||||
@ -2462,7 +2388,7 @@ public class Block {
|
||||
final long qoraHoldersShare = BlockChain.getInstance().getQoraHoldersShareAtHeight(this.blockData.getHeight());
|
||||
|
||||
// Perform account-level-based reward scaling if appropriate
|
||||
if (!haveFounders && this.blockData.getHeight() < BlockChain.getInstance().getAdminsReplaceFoundersHeight() ) {
|
||||
if (!haveFounders) {
|
||||
// Recalculate distribution ratios based on candidates
|
||||
|
||||
// Nothing shared? This shouldn't happen
|
||||
@ -2498,103 +2424,18 @@ public class Block {
|
||||
}
|
||||
|
||||
// Add founders as reward candidate if appropriate
|
||||
if (haveFounders && this.blockData.getHeight() < BlockChain.getInstance().getAdminsReplaceFoundersHeight()) {
|
||||
if (haveFounders) {
|
||||
// Yes: add to reward candidates list
|
||||
BlockRewardDistributor founderDistributor = (distributionAmount, balanceChanges) -> distributeBlockRewardShare(distributionAmount, onlineFounderAccounts, balanceChanges);
|
||||
|
||||
final long foundersShare = 1_00000000 - totalShares;
|
||||
BlockRewardCandidate rewardCandidate = new BlockRewardCandidate("Founders", foundersShare, founderDistributor);
|
||||
rewardCandidates.add(rewardCandidate);
|
||||
LOGGER.info("logging foundersShare prior to reward modifications {}",foundersShare);
|
||||
}
|
||||
else if (this.blockData.getHeight() >= BlockChain.getInstance().getAdminsReplaceFoundersHeight()) {
|
||||
try (final Repository repository = RepositoryManager.getRepository()) {
|
||||
GroupRepository groupRepository = repository.getGroupRepository();
|
||||
|
||||
List<Integer> mintingGroupIds = Groups.getGroupIdsToMint(BlockChain.getInstance(), this.blockData.getHeight());
|
||||
|
||||
// all minter admins
|
||||
List<String> minterAdmins = Groups.getAllAdmins(groupRepository, mintingGroupIds);
|
||||
|
||||
// all minter admins that are online
|
||||
List<ExpandedAccount> onlineMinterAdminAccounts
|
||||
= expandedAccounts.stream()
|
||||
.filter(expandedAccount -> minterAdmins.contains(expandedAccount.getMintingAccount().getAddress()))
|
||||
.collect(Collectors.toList());
|
||||
|
||||
long minterAdminShare;
|
||||
|
||||
if( onlineMinterAdminAccounts.isEmpty() ) {
|
||||
minterAdminShare = 0;
|
||||
}
|
||||
else {
|
||||
BlockRewardDistributor minterAdminDistributor
|
||||
= (distributionAmount, balanceChanges)
|
||||
->
|
||||
distributeBlockRewardShare(distributionAmount, onlineMinterAdminAccounts, balanceChanges);
|
||||
|
||||
long adminShare = 1_00000000 - totalShares;
|
||||
LOGGER.info("initial total Shares: {}", totalShares);
|
||||
LOGGER.info("logging adminShare after hardfork, this is the primary reward that will be split {}", adminShare);
|
||||
|
||||
minterAdminShare = adminShare / 2;
|
||||
BlockRewardCandidate minterAdminRewardCandidate
|
||||
= new BlockRewardCandidate("Minter Admins", minterAdminShare, minterAdminDistributor);
|
||||
rewardCandidates.add(minterAdminRewardCandidate);
|
||||
|
||||
totalShares += minterAdminShare;
|
||||
}
|
||||
|
||||
LOGGER.info("MINTER ADMIN SHARE: {}",minterAdminShare);
|
||||
|
||||
// all dev admins
|
||||
List<String> devAdminAddresses
|
||||
= groupRepository.getGroupAdmins(1).stream()
|
||||
.map(GroupAdminData::getAdmin)
|
||||
.collect(Collectors.toList());
|
||||
|
||||
LOGGER.info("Removing NULL Account Address, Dev Admin Count = {}", devAdminAddresses.size());
|
||||
devAdminAddresses.removeIf( address -> Group.NULL_OWNER_ADDRESS.equals(address) );
|
||||
LOGGER.info("Removed NULL Account Address, Dev Admin Count = {}", devAdminAddresses.size());
|
||||
|
||||
BlockRewardDistributor devAdminDistributor
|
||||
= (distributionAmount, balanceChanges) -> distributeToAccounts(distributionAmount, devAdminAddresses, balanceChanges);
|
||||
|
||||
long devAdminShare = 1_00000000 - totalShares;
|
||||
LOGGER.info("DEV ADMIN SHARE: {}",devAdminShare);
|
||||
BlockRewardCandidate devAdminRewardCandidate
|
||||
= new BlockRewardCandidate("Dev Admins", devAdminShare,devAdminDistributor);
|
||||
rewardCandidates.add(devAdminRewardCandidate);
|
||||
}
|
||||
}
|
||||
|
||||
return rewardCandidates;
|
||||
}
|
||||
|
||||
/**
|
||||
* Distribute To Accounts
|
||||
*
|
||||
* Merges distribute shares to a map of distribution shares.
|
||||
*
|
||||
* @param distributionAmount the amount to distribute
|
||||
* @param accountAddressess the addresses to distribute to
|
||||
* @param balanceChanges the map of distribution shares, this gets appended to
|
||||
*
|
||||
* @return the total amount mapped to addresses for distribution
|
||||
*/
|
||||
public static long distributeToAccounts(long distributionAmount, List<String> accountAddressess, Map<String, Long> balanceChanges) {
|
||||
|
||||
if( accountAddressess.isEmpty() ) return 0;
|
||||
|
||||
long distibutionShare = distributionAmount / accountAddressess.size();
|
||||
|
||||
for(String accountAddress : accountAddressess ) {
|
||||
balanceChanges.merge(accountAddress, distibutionShare, Long::sum);
|
||||
}
|
||||
|
||||
return distibutionShare * accountAddressess.size();
|
||||
}
|
||||
|
||||
private static long distributeBlockRewardShare(long distributionAmount, List<ExpandedAccount> accounts, Map<String, Long> balanceChanges) {
|
||||
// Collate all expanded accounts by minting account
|
||||
Map<String, List<ExpandedAccount>> accountsByMinter = new HashMap<>();
|
||||
|
@ -88,13 +88,7 @@ public class BlockChain {
|
||||
onlyMintWithNameHeight,
|
||||
removeOnlyMintWithNameHeight,
|
||||
groupMemberCheckHeight,
|
||||
fixBatchRewardHeight,
|
||||
adminsReplaceFoundersHeight,
|
||||
nullGroupMembershipHeight,
|
||||
ignoreLevelForRewardShareHeight,
|
||||
adminQueryFixHeight,
|
||||
multipleNamesPerAccountHeight,
|
||||
mintedBlocksAdjustmentRemovalHeight
|
||||
fixBatchRewardHeight
|
||||
}
|
||||
|
||||
// Custom transaction fees
|
||||
@ -114,8 +108,7 @@ public class BlockChain {
|
||||
/** Whether to use legacy, broken RIPEMD160 implementation when converting public keys to addresses. */
|
||||
private boolean useBrokenMD160ForAddresses = false;
|
||||
|
||||
/** This should get ignored and overwritten in the oneNamePerAccount(int blockchainHeight) method,
|
||||
* because it is based on block height, not based on the genesis block.*/
|
||||
/** Whether only one registered name is allowed per account. */
|
||||
private boolean oneNamePerAccount = false;
|
||||
|
||||
/** Checkpoints */
|
||||
@ -215,13 +208,7 @@ public class BlockChain {
|
||||
private int minAccountLevelToRewardShare;
|
||||
private int maxRewardSharesPerFounderMintingAccount;
|
||||
private int founderEffectiveMintingLevel;
|
||||
|
||||
public static class IdsForHeight {
|
||||
public int height;
|
||||
public List<Integer> ids;
|
||||
}
|
||||
|
||||
private List<IdsForHeight> mintingGroupIds;
|
||||
private int mintingGroupId;
|
||||
|
||||
/** Minimum time to retain online account signatures (ms) for block validity checks. */
|
||||
private long onlineAccountSignaturesMinLifetime;
|
||||
@ -477,9 +464,8 @@ public class BlockChain {
|
||||
return this.useBrokenMD160ForAddresses;
|
||||
}
|
||||
|
||||
public boolean oneNamePerAccount(int blockchainHeight) {
|
||||
// this is not set on a simple blockchain setting, it is based on a feature trigger height
|
||||
return blockchainHeight < this.getMultipleNamesPerAccountHeight();
|
||||
public boolean oneNamePerAccount() {
|
||||
return this.oneNamePerAccount;
|
||||
}
|
||||
|
||||
public List<Checkpoint> getCheckpoints() {
|
||||
@ -554,8 +540,8 @@ public class BlockChain {
|
||||
return this.onlineAccountSignaturesMaxLifetime;
|
||||
}
|
||||
|
||||
public List<IdsForHeight> getMintingGroupIds() {
|
||||
return mintingGroupIds;
|
||||
public int getMintingGroupId() {
|
||||
return this.mintingGroupId;
|
||||
}
|
||||
|
||||
public CiyamAtSettings getCiyamAtSettings() {
|
||||
@ -676,30 +662,6 @@ public class BlockChain {
|
||||
return this.featureTriggers.get(FeatureTrigger.fixBatchRewardHeight.name()).intValue();
|
||||
}
|
||||
|
||||
public int getAdminsReplaceFoundersHeight() {
|
||||
return this.featureTriggers.get(FeatureTrigger.adminsReplaceFoundersHeight.name()).intValue();
|
||||
}
|
||||
|
||||
public int getNullGroupMembershipHeight() {
|
||||
return this.featureTriggers.get(FeatureTrigger.nullGroupMembershipHeight.name()).intValue();
|
||||
}
|
||||
|
||||
public int getIgnoreLevelForRewardShareHeight() {
|
||||
return this.featureTriggers.get(FeatureTrigger.ignoreLevelForRewardShareHeight.name()).intValue();
|
||||
}
|
||||
|
||||
public int getAdminQueryFixHeight() {
|
||||
return this.featureTriggers.get(FeatureTrigger.adminQueryFixHeight.name()).intValue();
|
||||
}
|
||||
|
||||
public int getMultipleNamesPerAccountHeight() {
|
||||
return this.featureTriggers.get(FeatureTrigger.multipleNamesPerAccountHeight.name()).intValue();
|
||||
}
|
||||
|
||||
public int getMintedBlocksAdjustmentRemovalHeight() {
|
||||
return this.featureTriggers.get(FeatureTrigger.mintedBlocksAdjustmentRemovalHeight.name()).intValue();
|
||||
}
|
||||
|
||||
// More complex getters for aspects that change by height or timestamp
|
||||
|
||||
public long getRewardAtHeight(int ourHeight) {
|
||||
|
@ -1,47 +0,0 @@
|
||||
package org.qortal.block;
|
||||
|
||||
import org.qortal.account.Account;
|
||||
import org.qortal.api.resource.TransactionsResource;
|
||||
import org.qortal.data.naming.NameData;
|
||||
import org.qortal.repository.DataException;
|
||||
import org.qortal.repository.Repository;
|
||||
|
||||
import java.util.Set;
|
||||
import java.util.stream.Collectors;
|
||||
|
||||
/**
|
||||
* Class PrimaryNamesBlock
|
||||
*/
|
||||
public class PrimaryNamesBlock {
|
||||
|
||||
/**
|
||||
* Process Primary Names
|
||||
*
|
||||
* @param repository
|
||||
* @throws DataException
|
||||
*/
|
||||
public static void processNames(Repository repository) throws DataException {
|
||||
|
||||
Set<String> addressesWithNames
|
||||
= repository.getNameRepository().getAllNames().stream()
|
||||
.map(NameData::getOwner).collect(Collectors.toSet());
|
||||
|
||||
// for each address with a name, set primary name to the address
|
||||
for( String address : addressesWithNames ) {
|
||||
|
||||
Account account = new Account(repository, address);
|
||||
account.resetPrimaryName(TransactionsResource.ConfirmationStatus.CONFIRMED);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Orphan the Primary Names Block
|
||||
*
|
||||
* @param repository
|
||||
* @throws DataException
|
||||
*/
|
||||
public static void orphanNames(Repository repository) throws DataException {
|
||||
|
||||
repository.getNameRepository().clearPrimaryNames();
|
||||
}
|
||||
}
|
@ -46,7 +46,6 @@ import org.qortal.utils.*;
|
||||
|
||||
import javax.xml.bind.annotation.XmlAccessType;
|
||||
import javax.xml.bind.annotation.XmlAccessorType;
|
||||
|
||||
import java.awt.TrayIcon.MessageType;
|
||||
import java.io.File;
|
||||
import java.io.FileNotFoundException;
|
||||
@ -54,7 +53,6 @@ import java.io.IOException;
|
||||
import java.io.InputStream;
|
||||
import java.net.InetSocketAddress;
|
||||
import java.net.UnknownHostException;
|
||||
import java.nio.file.Files;
|
||||
import java.nio.file.Path;
|
||||
import java.nio.file.Paths;
|
||||
import java.security.SecureRandom;
|
||||
@ -72,10 +70,11 @@ import java.util.concurrent.locks.ReentrantLock;
|
||||
import java.util.function.Function;
|
||||
import java.util.function.Predicate;
|
||||
import java.util.stream.Collectors;
|
||||
import java.util.stream.Stream;
|
||||
|
||||
public class Controller extends Thread {
|
||||
|
||||
public static HSQLDBRepositoryFactory REPOSITORY_FACTORY;
|
||||
|
||||
static {
|
||||
// This must go before any calls to LogManager/Logger
|
||||
System.setProperty("log4j2.formatMsgNoLookups", "true");
|
||||
@ -397,9 +396,6 @@ public class Controller extends Thread {
|
||||
|
||||
Controller.newInstance(args);
|
||||
|
||||
|
||||
cleanChunkUploadTempDir(); // cleanup leftover chunks from streaming to disk
|
||||
|
||||
LOGGER.info("Starting NTP");
|
||||
Long ntpOffset = Settings.getInstance().getTestNtpOffset();
|
||||
if (ntpOffset != null)
|
||||
@ -409,8 +405,8 @@ public class Controller extends Thread {
|
||||
|
||||
LOGGER.info("Starting repository");
|
||||
try {
|
||||
HSQLDBRepositoryFactory repositoryFactory = new HSQLDBRepositoryFactory(getRepositoryUrl());
|
||||
RepositoryManager.setRepositoryFactory(repositoryFactory);
|
||||
REPOSITORY_FACTORY = new HSQLDBRepositoryFactory(getRepositoryUrl());
|
||||
RepositoryManager.setRepositoryFactory(REPOSITORY_FACTORY);
|
||||
RepositoryManager.setRequestedCheckpoint(Boolean.TRUE);
|
||||
|
||||
try (final Repository repository = RepositoryManager.getRepository()) {
|
||||
@ -427,12 +423,6 @@ public class Controller extends Thread {
|
||||
LOGGER.info("Db Cache Disabled");
|
||||
}
|
||||
|
||||
LOGGER.info("Arbitrary Indexing Starting ...");
|
||||
ArbitraryIndexUtils.startCaching(
|
||||
Settings.getInstance().getArbitraryIndexingPriority(),
|
||||
Settings.getInstance().getArbitraryIndexingFrequency()
|
||||
);
|
||||
|
||||
if( Settings.getInstance().isBalanceRecorderEnabled() ) {
|
||||
Optional<HSQLDBBalanceRecorder> recorder = HSQLDBBalanceRecorder.getInstance();
|
||||
|
||||
@ -551,25 +541,9 @@ public class Controller extends Thread {
|
||||
ArbitraryDataStorageManager.getInstance().start();
|
||||
ArbitraryDataRenderManager.getInstance().start();
|
||||
|
||||
// start rebuild arbitrary resource cache timer task
|
||||
if( Settings.getInstance().isRebuildArbitraryResourceCacheTaskEnabled() ) {
|
||||
new Timer().schedule(
|
||||
new RebuildArbitraryResourceCacheTask(),
|
||||
Settings.getInstance().getRebuildArbitraryResourceCacheTaskDelay() * RebuildArbitraryResourceCacheTask.MILLIS_IN_MINUTE,
|
||||
Settings.getInstance().getRebuildArbitraryResourceCacheTaskPeriod() * RebuildArbitraryResourceCacheTask.MILLIS_IN_HOUR
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
LOGGER.info("Starting online accounts manager");
|
||||
OnlineAccountsManager.getInstance().start();
|
||||
|
||||
LOGGER.info("Starting foreign fees manager");
|
||||
ForeignFeesManager.getInstance().start();
|
||||
|
||||
LOGGER.info("Starting follower");
|
||||
Follower.getInstance().start();
|
||||
|
||||
LOGGER.info("Starting transaction importer");
|
||||
TransactionImporter.getInstance().start();
|
||||
|
||||
@ -1140,9 +1114,6 @@ public class Controller extends Thread {
|
||||
LOGGER.info("Shutting down online accounts manager");
|
||||
OnlineAccountsManager.getInstance().shutdown();
|
||||
|
||||
LOGGER.info("Shutting down foreign fees manager");
|
||||
ForeignFeesManager.getInstance().shutdown();
|
||||
|
||||
LOGGER.info("Shutting down transaction importer");
|
||||
TransactionImporter.getInstance().shutdown();
|
||||
|
||||
@ -1487,14 +1458,6 @@ public class Controller extends Thread {
|
||||
OnlineAccountsManager.getInstance().onNetworkOnlineAccountsV3Message(peer, message);
|
||||
break;
|
||||
|
||||
case GET_FOREIGN_FEES:
|
||||
ForeignFeesManager.getInstance().onNetworkGetForeignFeesMessage(peer, message);
|
||||
break;
|
||||
|
||||
case FOREIGN_FEES:
|
||||
ForeignFeesManager.getInstance().onNetworkForeignFeesMessage(peer, message);
|
||||
break;
|
||||
|
||||
case GET_ARBITRARY_DATA:
|
||||
// Not currently supported
|
||||
break;
|
||||
@ -2181,24 +2144,6 @@ public class Controller extends Thread {
|
||||
return now - offset;
|
||||
}
|
||||
|
||||
private static void cleanChunkUploadTempDir() {
|
||||
Path uploadsTemp = Paths.get("uploads-temp");
|
||||
if (!Files.exists(uploadsTemp)) {
|
||||
return;
|
||||
}
|
||||
|
||||
try (Stream<Path> paths = Files.walk(uploadsTemp)) {
|
||||
paths.sorted(Comparator.reverseOrder())
|
||||
.map(Path::toFile)
|
||||
.forEach(File::delete);
|
||||
|
||||
LOGGER.info("Cleaned up all temporary uploads in {}", uploadsTemp);
|
||||
} catch (IOException e) {
|
||||
LOGGER.warn("Failed to clean up uploads-temp directory", e);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
public StatsSnapshot getStatsSnapshot() {
|
||||
return this.stats;
|
||||
}
|
||||
|
File diff suppressed because it is too large
Load Diff
@ -25,7 +25,6 @@ import org.qortal.repository.Repository;
|
||||
import org.qortal.repository.RepositoryManager;
|
||||
import org.qortal.settings.Settings;
|
||||
import org.qortal.utils.Base58;
|
||||
import org.qortal.utils.Groups;
|
||||
import org.qortal.utils.NTP;
|
||||
import org.qortal.utils.NamedThreadFactory;
|
||||
|
||||
@ -226,14 +225,11 @@ public class OnlineAccountsManager {
|
||||
Set<OnlineAccountData> onlineAccountsToAdd = new HashSet<>();
|
||||
Set<OnlineAccountData> onlineAccountsToRemove = new HashSet<>();
|
||||
try (final Repository repository = RepositoryManager.getRepository()) {
|
||||
|
||||
int blockHeight = repository.getBlockRepository().getBlockchainHeight();
|
||||
|
||||
List<String> mintingGroupMemberAddresses
|
||||
= Groups.getAllMembers(
|
||||
repository.getGroupRepository(),
|
||||
Groups.getGroupIdsToMint(BlockChain.getInstance(), blockHeight)
|
||||
);
|
||||
= repository.getGroupRepository()
|
||||
.getGroupMembers(BlockChain.getInstance().getMintingGroupId()).stream()
|
||||
.map(GroupMemberData::getMember)
|
||||
.collect(Collectors.toList());
|
||||
|
||||
for (OnlineAccountData onlineAccountData : this.onlineAccountsImportQueue) {
|
||||
if (isStopping)
|
||||
|
@ -2,7 +2,6 @@ package org.qortal.controller;
|
||||
|
||||
import org.apache.logging.log4j.LogManager;
|
||||
import org.apache.logging.log4j.Logger;
|
||||
import org.qortal.controller.arbitrary.PeerMessage;
|
||||
import org.qortal.data.block.BlockData;
|
||||
import org.qortal.data.transaction.TransactionData;
|
||||
import org.qortal.network.Network;
|
||||
@ -21,11 +20,7 @@ import org.qortal.utils.Base58;
|
||||
import org.qortal.utils.NTP;
|
||||
|
||||
import java.util.*;
|
||||
import java.util.concurrent.Executors;
|
||||
import java.util.concurrent.ScheduledExecutorService;
|
||||
import java.util.concurrent.TimeUnit;
|
||||
import java.util.concurrent.locks.ReentrantLock;
|
||||
import java.util.function.Function;
|
||||
import java.util.stream.Collectors;
|
||||
|
||||
public class TransactionImporter extends Thread {
|
||||
@ -55,10 +50,6 @@ public class TransactionImporter extends Thread {
|
||||
/** Cached list of unconfirmed transactions, used when counting per creator. This is replaced regularly */
|
||||
public static List<TransactionData> unconfirmedTransactionsCache = null;
|
||||
|
||||
public TransactionImporter() {
|
||||
signatureMessageScheduler.scheduleAtFixedRate(this::processNetworkTransactionSignaturesMessage, 60, 1, TimeUnit.SECONDS);
|
||||
getTransactionMessageScheduler.scheduleAtFixedRate(this::processNetworkGetTransactionMessages, 60, 1, TimeUnit.SECONDS);
|
||||
}
|
||||
|
||||
public static synchronized TransactionImporter getInstance() {
|
||||
if (instance == null) {
|
||||
@ -380,104 +371,36 @@ public class TransactionImporter extends Thread {
|
||||
}
|
||||
}
|
||||
|
||||
// List to collect messages
|
||||
private final List<PeerMessage> getTransactionMessageList = new ArrayList<>();
|
||||
// Lock to synchronize access to the list
|
||||
private final Object getTransactionMessageLock = new Object();
|
||||
|
||||
// Scheduled executor service to process messages every second
|
||||
private final ScheduledExecutorService getTransactionMessageScheduler = Executors.newScheduledThreadPool(1);
|
||||
|
||||
public void onNetworkGetTransactionMessage(Peer peer, Message message) {
|
||||
GetTransactionMessage getTransactionMessage = (GetTransactionMessage) message;
|
||||
byte[] signature = getTransactionMessage.getSignature();
|
||||
|
||||
synchronized (getTransactionMessageLock) {
|
||||
getTransactionMessageList.add(new PeerMessage(peer, message));
|
||||
}
|
||||
}
|
||||
|
||||
private void processNetworkGetTransactionMessages() {
|
||||
|
||||
try {
|
||||
List<PeerMessage> messagesToProcess;
|
||||
synchronized (getTransactionMessageLock) {
|
||||
messagesToProcess = new ArrayList<>(getTransactionMessageList);
|
||||
getTransactionMessageList.clear();
|
||||
}
|
||||
|
||||
if( messagesToProcess.isEmpty() ) return;
|
||||
|
||||
Map<String, PeerMessage> peerMessageBySignature58 = new HashMap<>(messagesToProcess.size());
|
||||
|
||||
for( PeerMessage peerMessage : messagesToProcess ) {
|
||||
GetTransactionMessage getTransactionMessage = (GetTransactionMessage) peerMessage.getMessage();
|
||||
byte[] signature = getTransactionMessage.getSignature();
|
||||
|
||||
peerMessageBySignature58.put(Base58.encode(signature), peerMessage);
|
||||
}
|
||||
|
||||
try (final Repository repository = RepositoryManager.getRepository()) {
|
||||
// Firstly check the sig-valid transactions that are currently queued for import
|
||||
Map<String, TransactionData> transactionsCachedBySignature58
|
||||
= this.getCachedSigValidTransactions().stream()
|
||||
.collect(Collectors.toMap(t -> Base58.encode(t.getSignature()), Function.identity()));
|
||||
TransactionData transactionData = this.getCachedSigValidTransactions().stream()
|
||||
.filter(t -> Arrays.equals(signature, t.getSignature()))
|
||||
.findFirst().orElse(null);
|
||||
|
||||
Map<Boolean, List<Map.Entry<String, PeerMessage>>> transactionsCachedBySignature58Partition
|
||||
= peerMessageBySignature58.entrySet().stream()
|
||||
.collect(Collectors.partitioningBy(entry -> transactionsCachedBySignature58.containsKey(entry.getKey())));
|
||||
|
||||
List<byte[]> signaturesNeeded
|
||||
= transactionsCachedBySignature58Partition.get(false).stream()
|
||||
.map(Map.Entry::getValue)
|
||||
.map(PeerMessage::getMessage)
|
||||
.map(message -> (GetTransactionMessage) message)
|
||||
.map(GetTransactionMessage::getSignature)
|
||||
.collect(Collectors.toList());
|
||||
|
||||
// transaction found in the import queue
|
||||
Map<String, TransactionData> transactionsToSendBySignature58 = new HashMap<>(messagesToProcess.size());
|
||||
for( Map.Entry<String, PeerMessage> entry : transactionsCachedBySignature58Partition.get(true)) {
|
||||
transactionsToSendBySignature58.put(entry.getKey(), transactionsCachedBySignature58.get(entry.getKey()));
|
||||
}
|
||||
|
||||
if( !signaturesNeeded.isEmpty() ) {
|
||||
if (transactionData == null) {
|
||||
// Not found in import queue, so try the database
|
||||
try (final Repository repository = RepositoryManager.getRepository()) {
|
||||
transactionsToSendBySignature58.putAll(
|
||||
repository.getTransactionRepository().fromSignatures(signaturesNeeded).stream()
|
||||
.collect(Collectors.toMap(transactionData -> Base58.encode(transactionData.getSignature()), Function.identity()))
|
||||
);
|
||||
} catch (DataException e) {
|
||||
LOGGER.error(e.getMessage(), e);
|
||||
}
|
||||
transactionData = repository.getTransactionRepository().fromSignature(signature);
|
||||
}
|
||||
|
||||
for( final Map.Entry<String, TransactionData> entry : transactionsToSendBySignature58.entrySet() ) {
|
||||
|
||||
PeerMessage peerMessage = peerMessageBySignature58.get(entry.getKey());
|
||||
final Message message = peerMessage.getMessage();
|
||||
final Peer peer = peerMessage.getPeer();
|
||||
|
||||
Runnable sendTransactionMessageRunner = () -> sendTransactionMessage(entry.getKey(), entry.getValue(), message, peer);
|
||||
Thread sendTransactionMessageThread = new Thread(sendTransactionMessageRunner);
|
||||
sendTransactionMessageThread.start();
|
||||
if (transactionData == null) {
|
||||
// Still not found - so we don't have this transaction
|
||||
LOGGER.debug(() -> String.format("Ignoring GET_TRANSACTION request from peer %s for unknown transaction %s", peer, Base58.encode(signature)));
|
||||
// Send no response at all???
|
||||
return;
|
||||
}
|
||||
} catch (Exception e) {
|
||||
LOGGER.error(e.getMessage(),e);
|
||||
}
|
||||
}
|
||||
|
||||
private static void sendTransactionMessage(String signature58, TransactionData data, Message message, Peer peer) {
|
||||
try {
|
||||
Message transactionMessage = new TransactionMessage(data);
|
||||
Message transactionMessage = new TransactionMessage(transactionData);
|
||||
transactionMessage.setId(message.getId());
|
||||
|
||||
if (!peer.sendMessage(transactionMessage))
|
||||
peer.disconnect("failed to send transaction");
|
||||
}
|
||||
catch (TransformationException e) {
|
||||
LOGGER.error(String.format("Serialization issue while sending transaction %s to peer %s", signature58, peer), e);
|
||||
}
|
||||
catch (Exception e) {
|
||||
LOGGER.error(e.getMessage(), e);
|
||||
} catch (DataException e) {
|
||||
LOGGER.error(String.format("Repository issue while sending transaction %s to peer %s", Base58.encode(signature), peer), e);
|
||||
} catch (TransformationException e) {
|
||||
LOGGER.error(String.format("Serialization issue while sending transaction %s to peer %s", Base58.encode(signature), peer), e);
|
||||
}
|
||||
}
|
||||
|
||||
@ -498,86 +421,44 @@ public class TransactionImporter extends Thread {
|
||||
}
|
||||
}
|
||||
|
||||
// List to collect messages
|
||||
private final List<PeerMessage> signatureMessageList = new ArrayList<>();
|
||||
// Lock to synchronize access to the list
|
||||
private final Object signatureMessageLock = new Object();
|
||||
|
||||
// Scheduled executor service to process messages every second
|
||||
private final ScheduledExecutorService signatureMessageScheduler = Executors.newScheduledThreadPool(1);
|
||||
|
||||
public void onNetworkTransactionSignaturesMessage(Peer peer, Message message) {
|
||||
synchronized (signatureMessageLock) {
|
||||
signatureMessageList.add(new PeerMessage(peer, message));
|
||||
}
|
||||
}
|
||||
TransactionSignaturesMessage transactionSignaturesMessage = (TransactionSignaturesMessage) message;
|
||||
List<byte[]> signatures = transactionSignaturesMessage.getSignatures();
|
||||
|
||||
public void processNetworkTransactionSignaturesMessage() {
|
||||
|
||||
try {
|
||||
List<PeerMessage> messagesToProcess;
|
||||
synchronized (signatureMessageLock) {
|
||||
messagesToProcess = new ArrayList<>(signatureMessageList);
|
||||
signatureMessageList.clear();
|
||||
}
|
||||
|
||||
Map<String, byte[]> signatureBySignature58 = new HashMap<>(messagesToProcess.size() * 10);
|
||||
Map<String, Peer> peerBySignature58 = new HashMap<>( messagesToProcess.size() * 10 );
|
||||
|
||||
for( PeerMessage peerMessage : messagesToProcess ) {
|
||||
|
||||
TransactionSignaturesMessage transactionSignaturesMessage = (TransactionSignaturesMessage) peerMessage.getMessage();
|
||||
List<byte[]> signatures = transactionSignaturesMessage.getSignatures();
|
||||
|
||||
for (byte[] signature : signatures) {
|
||||
String signature58 = Base58.encode(signature);
|
||||
if (invalidUnconfirmedTransactions.containsKey(signature58)) {
|
||||
// Previously invalid transaction - don't keep requesting it
|
||||
// It will be periodically removed from invalidUnconfirmedTransactions to allow for rechecks
|
||||
continue;
|
||||
}
|
||||
|
||||
// Ignore if this transaction is in the queue
|
||||
if (incomingTransactionQueueContains(signature)) {
|
||||
LOGGER.trace(() -> String.format("Ignoring existing queued transaction %s from peer %s", Base58.encode(signature), peerMessage.getPeer()));
|
||||
continue;
|
||||
}
|
||||
|
||||
signatureBySignature58.put(signature58, signature);
|
||||
peerBySignature58.put(signature58, peerMessage.getPeer());
|
||||
try (final Repository repository = RepositoryManager.getRepository()) {
|
||||
for (byte[] signature : signatures) {
|
||||
String signature58 = Base58.encode(signature);
|
||||
if (invalidUnconfirmedTransactions.containsKey(signature58)) {
|
||||
// Previously invalid transaction - don't keep requesting it
|
||||
// It will be periodically removed from invalidUnconfirmedTransactions to allow for rechecks
|
||||
continue;
|
||||
}
|
||||
}
|
||||
|
||||
if( !signatureBySignature58.isEmpty() ) {
|
||||
try (final Repository repository = RepositoryManager.getRepository()) {
|
||||
|
||||
// remove signatures in db already
|
||||
repository.getTransactionRepository()
|
||||
.fromSignatures(new ArrayList<>(signatureBySignature58.values())).stream()
|
||||
.map(TransactionData::getSignature)
|
||||
.map(signature -> Base58.encode(signature))
|
||||
.forEach(signature58 -> signatureBySignature58.remove(signature58));
|
||||
} catch (DataException e) {
|
||||
LOGGER.error(String.format("Repository issue while processing unconfirmed transactions from peer"), e);
|
||||
// Ignore if this transaction is in the queue
|
||||
if (incomingTransactionQueueContains(signature)) {
|
||||
LOGGER.trace(() -> String.format("Ignoring existing queued transaction %s from peer %s", Base58.encode(signature), peer));
|
||||
continue;
|
||||
}
|
||||
}
|
||||
|
||||
// Check isInterrupted() here and exit fast
|
||||
if (Thread.currentThread().isInterrupted())
|
||||
return;
|
||||
// Do we have it already? (Before requesting transaction data itself)
|
||||
if (repository.getTransactionRepository().exists(signature)) {
|
||||
LOGGER.trace(() -> String.format("Ignoring existing transaction %s from peer %s", Base58.encode(signature), peer));
|
||||
continue;
|
||||
}
|
||||
|
||||
for (Map.Entry<String, byte[]> entry : signatureBySignature58.entrySet()) {
|
||||
|
||||
Peer peer = peerBySignature58.get(entry.getKey());
|
||||
// Check isInterrupted() here and exit fast
|
||||
if (Thread.currentThread().isInterrupted())
|
||||
return;
|
||||
|
||||
// Fetch actual transaction data from peer
|
||||
Message getTransactionMessage = new GetTransactionMessage(entry.getValue());
|
||||
if (peer != null && !peer.sendMessage(getTransactionMessage)) {
|
||||
Message getTransactionMessage = new GetTransactionMessage(signature);
|
||||
if (!peer.sendMessage(getTransactionMessage)) {
|
||||
peer.disconnect("failed to request transaction");
|
||||
return;
|
||||
}
|
||||
}
|
||||
} catch (Exception e) {
|
||||
LOGGER.error(e.getMessage(), e);
|
||||
} catch (DataException e) {
|
||||
LOGGER.error(String.format("Repository issue while processing unconfirmed transactions from peer %s", peer), e);
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -2,30 +2,22 @@ package org.qortal.controller.arbitrary;
|
||||
|
||||
import org.apache.logging.log4j.LogManager;
|
||||
import org.apache.logging.log4j.Logger;
|
||||
import org.qortal.api.resource.TransactionsResource;
|
||||
import org.qortal.controller.Controller;
|
||||
import org.qortal.data.arbitrary.ArbitraryResourceData;
|
||||
import org.qortal.data.transaction.ArbitraryTransactionData;
|
||||
import org.qortal.event.DataMonitorEvent;
|
||||
import org.qortal.event.EventBus;
|
||||
import org.qortal.gui.SplashFrame;
|
||||
import org.qortal.repository.DataException;
|
||||
import org.qortal.repository.Repository;
|
||||
import org.qortal.repository.RepositoryManager;
|
||||
import org.qortal.settings.Settings;
|
||||
import org.qortal.transaction.ArbitraryTransaction;
|
||||
import org.qortal.transaction.Transaction;
|
||||
import org.qortal.utils.Base58;
|
||||
|
||||
import java.text.NumberFormat;
|
||||
import java.util.ArrayList;
|
||||
import java.util.Collections;
|
||||
import java.util.HashMap;
|
||||
import java.util.HashSet;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.Optional;
|
||||
import java.util.Set;
|
||||
import java.util.function.Function;
|
||||
import java.util.stream.Collectors;
|
||||
|
||||
public class ArbitraryDataCacheManager extends Thread {
|
||||
|
||||
@ -37,11 +29,6 @@ public class ArbitraryDataCacheManager extends Thread {
|
||||
/** Queue of arbitrary transactions that require cache updates */
|
||||
private final List<ArbitraryTransactionData> updateQueue = Collections.synchronizedList(new ArrayList<>());
|
||||
|
||||
private static final NumberFormat FORMATTER = NumberFormat.getNumberInstance();
|
||||
|
||||
static {
|
||||
FORMATTER.setGroupingUsed(true);
|
||||
}
|
||||
|
||||
public static synchronized ArbitraryDataCacheManager getInstance() {
|
||||
if (instance == null) {
|
||||
@ -58,22 +45,17 @@ public class ArbitraryDataCacheManager extends Thread {
|
||||
|
||||
try {
|
||||
while (!Controller.isStopping()) {
|
||||
try {
|
||||
Thread.sleep(500L);
|
||||
Thread.sleep(500L);
|
||||
|
||||
// Process queue
|
||||
processResourceQueue();
|
||||
} catch (Exception e) {
|
||||
LOGGER.error(e.getMessage(), e);
|
||||
Thread.sleep(600_000L); // wait 10 minutes to continue
|
||||
}
|
||||
// Process queue
|
||||
processResourceQueue();
|
||||
}
|
||||
|
||||
// Clear queue before terminating thread
|
||||
processResourceQueue();
|
||||
} catch (Exception e) {
|
||||
LOGGER.error(e.getMessage(), e);
|
||||
} catch (InterruptedException e) {
|
||||
// Fall through to exit thread
|
||||
}
|
||||
|
||||
// Clear queue before terminating thread
|
||||
processResourceQueue();
|
||||
}
|
||||
|
||||
public void shutdown() {
|
||||
@ -103,25 +85,14 @@ public class ArbitraryDataCacheManager extends Thread {
|
||||
// Update arbitrary resource caches
|
||||
try {
|
||||
ArbitraryTransaction arbitraryTransaction = new ArbitraryTransaction(repository, transactionData);
|
||||
arbitraryTransaction.updateArbitraryResourceCacheIncludingMetadata(repository, new HashSet<>(0), new HashMap<>(0));
|
||||
arbitraryTransaction.updateArbitraryResourceCache(repository);
|
||||
arbitraryTransaction.updateArbitraryMetadataCache(repository);
|
||||
repository.saveChanges();
|
||||
|
||||
// Update status as separate commit, as this is more prone to failure
|
||||
arbitraryTransaction.updateArbitraryResourceStatus(repository);
|
||||
repository.saveChanges();
|
||||
|
||||
EventBus.INSTANCE.notify(
|
||||
new DataMonitorEvent(
|
||||
System.currentTimeMillis(),
|
||||
transactionData.getIdentifier(),
|
||||
transactionData.getName(),
|
||||
transactionData.getService().name(),
|
||||
"updated resource cache and status, queue",
|
||||
transactionData.getTimestamp(),
|
||||
transactionData.getTimestamp()
|
||||
)
|
||||
);
|
||||
|
||||
LOGGER.debug(() -> String.format("Finished processing transaction %.8s in arbitrary resource queue...", Base58.encode(transactionData.getSignature())));
|
||||
|
||||
} catch (DataException e) {
|
||||
@ -132,9 +103,6 @@ public class ArbitraryDataCacheManager extends Thread {
|
||||
} catch (DataException e) {
|
||||
LOGGER.error("Repository issue while processing arbitrary resource cache updates", e);
|
||||
}
|
||||
catch (Exception e) {
|
||||
LOGGER.error(e.getMessage(), e);
|
||||
}
|
||||
}
|
||||
|
||||
public void addToUpdateQueue(ArbitraryTransactionData transactionData) {
|
||||
@ -180,66 +148,34 @@ public class ArbitraryDataCacheManager extends Thread {
|
||||
LOGGER.info("Building arbitrary resources cache...");
|
||||
SplashFrame.getInstance().updateStatus("Building QDN cache - please wait...");
|
||||
|
||||
final int batchSize = Settings.getInstance().getBuildArbitraryResourcesBatchSize();
|
||||
final int batchSize = 100;
|
||||
int offset = 0;
|
||||
|
||||
List<ArbitraryTransactionData> allArbitraryTransactionsInDescendingOrder
|
||||
= repository.getArbitraryRepository().getLatestArbitraryTransactions();
|
||||
|
||||
LOGGER.info("arbitrary transactions: count = " + allArbitraryTransactionsInDescendingOrder.size());
|
||||
|
||||
List<ArbitraryResourceData> resources = repository.getArbitraryRepository().getArbitraryResources(null, null, true);
|
||||
|
||||
Map<ArbitraryTransactionDataHashWrapper, ArbitraryResourceData> resourceByWrapper = new HashMap<>(resources.size());
|
||||
for( ArbitraryResourceData resource : resources ) {
|
||||
resourceByWrapper.put(
|
||||
new ArbitraryTransactionDataHashWrapper(resource.service.value, resource.name, resource.identifier),
|
||||
resource
|
||||
);
|
||||
}
|
||||
|
||||
LOGGER.info("arbitrary resources: count = " + resourceByWrapper.size());
|
||||
|
||||
Set<ArbitraryTransactionDataHashWrapper> latestTransactionsWrapped = new HashSet<>(allArbitraryTransactionsInDescendingOrder.size());
|
||||
|
||||
// Loop through all ARBITRARY transactions, and determine latest state
|
||||
while (!Controller.isStopping()) {
|
||||
LOGGER.info(
|
||||
"Fetching arbitrary transactions {} - {} / {} Total",
|
||||
FORMATTER.format(offset),
|
||||
FORMATTER.format(offset+batchSize-1),
|
||||
FORMATTER.format(allArbitraryTransactionsInDescendingOrder.size())
|
||||
);
|
||||
LOGGER.info("Fetching arbitrary transactions {} - {}", offset, offset+batchSize-1);
|
||||
|
||||
List<ArbitraryTransactionData> transactionsToProcess
|
||||
= allArbitraryTransactionsInDescendingOrder.stream()
|
||||
.skip(offset)
|
||||
.limit(batchSize)
|
||||
.collect(Collectors.toList());
|
||||
|
||||
if (transactionsToProcess.isEmpty()) {
|
||||
List<byte[]> signatures = repository.getTransactionRepository().getSignaturesMatchingCriteria(null, null, null, List.of(Transaction.TransactionType.ARBITRARY), null, null, null, TransactionsResource.ConfirmationStatus.BOTH, batchSize, offset, false);
|
||||
if (signatures.isEmpty()) {
|
||||
// Complete
|
||||
break;
|
||||
}
|
||||
|
||||
try {
|
||||
for( ArbitraryTransactionData transactionData : transactionsToProcess) {
|
||||
if (transactionData.getService() == null) {
|
||||
// Unsupported service - ignore this resource
|
||||
continue;
|
||||
}
|
||||
// Expand signatures to transactions
|
||||
for (byte[] signature : signatures) {
|
||||
ArbitraryTransactionData transactionData = (ArbitraryTransactionData) repository
|
||||
.getTransactionRepository().fromSignature(signature);
|
||||
|
||||
latestTransactionsWrapped.add(new ArbitraryTransactionDataHashWrapper(transactionData));
|
||||
|
||||
// Update arbitrary resource caches
|
||||
ArbitraryTransaction arbitraryTransaction = new ArbitraryTransaction(repository, transactionData);
|
||||
arbitraryTransaction.updateArbitraryResourceCacheIncludingMetadata(repository, latestTransactionsWrapped, resourceByWrapper);
|
||||
if (transactionData.getService() == null) {
|
||||
// Unsupported service - ignore this resource
|
||||
continue;
|
||||
}
|
||||
repository.saveChanges();
|
||||
} catch (DataException e) {
|
||||
repository.discardChanges();
|
||||
|
||||
LOGGER.error(e.getMessage(), e);
|
||||
// Update arbitrary resource caches
|
||||
ArbitraryTransaction arbitraryTransaction = new ArbitraryTransaction(repository, transactionData);
|
||||
arbitraryTransaction.updateArbitraryResourceCache(repository);
|
||||
arbitraryTransaction.updateArbitraryMetadataCache(repository);
|
||||
repository.saveChanges();
|
||||
}
|
||||
offset += batchSize;
|
||||
}
|
||||
@ -257,11 +193,6 @@ public class ArbitraryDataCacheManager extends Thread {
|
||||
repository.discardChanges();
|
||||
throw new DataException("Build of arbitrary resources cache failed.");
|
||||
}
|
||||
catch (Exception e) {
|
||||
LOGGER.error(e.getMessage(), e);
|
||||
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
private boolean refreshArbitraryStatuses(Repository repository) throws DataException {
|
||||
@ -269,48 +200,27 @@ public class ArbitraryDataCacheManager extends Thread {
|
||||
LOGGER.info("Refreshing arbitrary resource statuses for locally hosted transactions...");
|
||||
SplashFrame.getInstance().updateStatus("Refreshing statuses - please wait...");
|
||||
|
||||
final int batchSize = Settings.getInstance().getBuildArbitraryResourcesBatchSize();
|
||||
final int batchSize = 100;
|
||||
int offset = 0;
|
||||
|
||||
List<ArbitraryTransactionData> allHostedTransactions
|
||||
= ArbitraryDataStorageManager.getInstance()
|
||||
.listAllHostedTransactions(repository, null, null);
|
||||
|
||||
// Loop through all ARBITRARY transactions, and determine latest state
|
||||
while (!Controller.isStopping()) {
|
||||
LOGGER.info(
|
||||
"Fetching hosted transactions {} - {} / {} Total",
|
||||
FORMATTER.format(offset),
|
||||
FORMATTER.format(offset+batchSize-1),
|
||||
FORMATTER.format(allHostedTransactions.size())
|
||||
);
|
||||
|
||||
List<ArbitraryTransactionData> hostedTransactions
|
||||
= allHostedTransactions.stream()
|
||||
.skip(offset)
|
||||
.limit(batchSize)
|
||||
.collect(Collectors.toList());
|
||||
LOGGER.info("Fetching hosted transactions {} - {}", offset, offset+batchSize-1);
|
||||
|
||||
List<ArbitraryTransactionData> hostedTransactions = ArbitraryDataStorageManager.getInstance().listAllHostedTransactions(repository, batchSize, offset);
|
||||
if (hostedTransactions.isEmpty()) {
|
||||
// Complete
|
||||
break;
|
||||
}
|
||||
|
||||
try {
|
||||
// Loop through hosted transactions
|
||||
for (ArbitraryTransactionData transactionData : hostedTransactions) {
|
||||
// Loop through hosted transactions
|
||||
for (ArbitraryTransactionData transactionData : hostedTransactions) {
|
||||
|
||||
// Determine status and update cache
|
||||
ArbitraryTransaction arbitraryTransaction = new ArbitraryTransaction(repository, transactionData);
|
||||
arbitraryTransaction.updateArbitraryResourceStatus(repository);
|
||||
}
|
||||
// Determine status and update cache
|
||||
ArbitraryTransaction arbitraryTransaction = new ArbitraryTransaction(repository, transactionData);
|
||||
arbitraryTransaction.updateArbitraryResourceStatus(repository);
|
||||
repository.saveChanges();
|
||||
} catch (DataException e) {
|
||||
repository.discardChanges();
|
||||
|
||||
LOGGER.error(e.getMessage(), e);
|
||||
}
|
||||
|
||||
offset += batchSize;
|
||||
}
|
||||
|
||||
@ -324,11 +234,6 @@ public class ArbitraryDataCacheManager extends Thread {
|
||||
repository.discardChanges();
|
||||
throw new DataException("Refresh of arbitrary resource statuses failed.");
|
||||
}
|
||||
catch (Exception e) {
|
||||
LOGGER.error(e.getMessage(), e);
|
||||
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
@ -2,10 +2,9 @@ package org.qortal.controller.arbitrary;
|
||||
|
||||
import org.apache.logging.log4j.LogManager;
|
||||
import org.apache.logging.log4j.Logger;
|
||||
import org.qortal.api.resource.TransactionsResource.ConfirmationStatus;
|
||||
import org.qortal.data.transaction.ArbitraryTransactionData;
|
||||
import org.qortal.data.transaction.TransactionData;
|
||||
import org.qortal.event.DataMonitorEvent;
|
||||
import org.qortal.event.EventBus;
|
||||
import org.qortal.repository.DataException;
|
||||
import org.qortal.repository.Repository;
|
||||
import org.qortal.repository.RepositoryManager;
|
||||
@ -22,12 +21,8 @@ import java.nio.file.Paths;
|
||||
import java.security.SecureRandom;
|
||||
import java.util.ArrayList;
|
||||
import java.util.Arrays;
|
||||
import java.util.HashSet;
|
||||
import java.util.List;
|
||||
import java.util.Objects;
|
||||
import java.util.Optional;
|
||||
import java.util.Set;
|
||||
import java.util.stream.Collectors;
|
||||
|
||||
import static org.qortal.controller.arbitrary.ArbitraryDataStorageManager.DELETION_THRESHOLD;
|
||||
|
||||
@ -82,19 +77,6 @@ public class ArbitraryDataCleanupManager extends Thread {
|
||||
final int limit = 100;
|
||||
int offset = 0;
|
||||
|
||||
List<ArbitraryTransactionData> allArbitraryTransactionsInDescendingOrder;
|
||||
|
||||
try (final Repository repository = RepositoryManager.getRepository()) {
|
||||
allArbitraryTransactionsInDescendingOrder
|
||||
= repository.getArbitraryRepository()
|
||||
.getLatestArbitraryTransactions();
|
||||
} catch( Exception e) {
|
||||
LOGGER.error(e.getMessage(), e);
|
||||
allArbitraryTransactionsInDescendingOrder = new ArrayList<>(0);
|
||||
}
|
||||
|
||||
Set<ArbitraryTransactionData> processedTransactions = new HashSet<>();
|
||||
|
||||
try {
|
||||
while (!isStopping) {
|
||||
Thread.sleep(30000);
|
||||
@ -125,31 +107,27 @@ public class ArbitraryDataCleanupManager extends Thread {
|
||||
|
||||
// Any arbitrary transactions we want to fetch data for?
|
||||
try (final Repository repository = RepositoryManager.getRepository()) {
|
||||
List<ArbitraryTransactionData> transactions = allArbitraryTransactionsInDescendingOrder.stream().skip(offset).limit(limit).collect(Collectors.toList());
|
||||
List<byte[]> signatures = repository.getTransactionRepository().getSignaturesMatchingCriteria(null, null, null, ARBITRARY_TX_TYPE, null, null, null, ConfirmationStatus.BOTH, limit, offset, true);
|
||||
// LOGGER.info("Found {} arbitrary transactions at offset: {}, limit: {}", signatures.size(), offset, limit);
|
||||
if (isStopping) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (transactions == null || transactions.isEmpty()) {
|
||||
if (signatures == null || signatures.isEmpty()) {
|
||||
offset = 0;
|
||||
allArbitraryTransactionsInDescendingOrder
|
||||
= repository.getArbitraryRepository()
|
||||
.getLatestArbitraryTransactions();
|
||||
transactions = allArbitraryTransactionsInDescendingOrder.stream().limit(limit).collect(Collectors.toList());
|
||||
processedTransactions.clear();
|
||||
continue;
|
||||
}
|
||||
|
||||
offset += limit;
|
||||
now = NTP.getTime();
|
||||
|
||||
// Loop through the signatures in this batch
|
||||
for (int i=0; i<transactions.size(); i++) {
|
||||
for (int i=0; i<signatures.size(); i++) {
|
||||
if (isStopping) {
|
||||
return;
|
||||
}
|
||||
|
||||
ArbitraryTransactionData arbitraryTransactionData = transactions.get(i);
|
||||
if (arbitraryTransactionData == null) {
|
||||
byte[] signature = signatures.get(i);
|
||||
if (signature == null) {
|
||||
continue;
|
||||
}
|
||||
|
||||
@ -158,7 +136,9 @@ public class ArbitraryDataCleanupManager extends Thread {
|
||||
Thread.sleep(5000);
|
||||
}
|
||||
|
||||
if (arbitraryTransactionData.getService() == null) {
|
||||
// Fetch the transaction data
|
||||
ArbitraryTransactionData arbitraryTransactionData = ArbitraryTransactionUtils.fetchTransactionData(repository, signature);
|
||||
if (arbitraryTransactionData == null || arbitraryTransactionData.getService() == null) {
|
||||
continue;
|
||||
}
|
||||
|
||||
@ -167,8 +147,6 @@ public class ArbitraryDataCleanupManager extends Thread {
|
||||
continue;
|
||||
}
|
||||
|
||||
boolean mostRecentTransaction = processedTransactions.add(arbitraryTransactionData);
|
||||
|
||||
// Check if we have the complete file
|
||||
boolean completeFileExists = ArbitraryTransactionUtils.completeFileExists(arbitraryTransactionData);
|
||||
|
||||
@ -189,54 +167,20 @@ public class ArbitraryDataCleanupManager extends Thread {
|
||||
LOGGER.info("Deleting transaction {} because we can't host its data",
|
||||
Base58.encode(arbitraryTransactionData.getSignature()));
|
||||
ArbitraryTransactionUtils.deleteCompleteFileAndChunks(arbitraryTransactionData);
|
||||
|
||||
EventBus.INSTANCE.notify(
|
||||
new DataMonitorEvent(
|
||||
System.currentTimeMillis(),
|
||||
arbitraryTransactionData.getIdentifier(),
|
||||
arbitraryTransactionData.getName(),
|
||||
arbitraryTransactionData.getService().name(),
|
||||
"can't store data, deleting",
|
||||
arbitraryTransactionData.getTimestamp(),
|
||||
arbitraryTransactionData.getTimestamp()
|
||||
)
|
||||
);
|
||||
continue;
|
||||
}
|
||||
|
||||
// Check to see if we have had a more recent PUT
|
||||
if (!mostRecentTransaction) {
|
||||
boolean hasMoreRecentPutTransaction = ArbitraryTransactionUtils.hasMoreRecentPutTransaction(repository, arbitraryTransactionData);
|
||||
if (hasMoreRecentPutTransaction) {
|
||||
// There is a more recent PUT transaction than the one we are currently processing.
|
||||
// When a PUT is issued, it replaces any layers that would have been there before.
|
||||
// Therefore any data relating to this older transaction is no longer needed.
|
||||
LOGGER.info(String.format("Newer PUT found for %s %s since transaction %s. " +
|
||||
"Deleting all files associated with the earlier transaction.", arbitraryTransactionData.getService(),
|
||||
arbitraryTransactionData.getName(), Base58.encode(arbitraryTransactionData.getSignature())));
|
||||
arbitraryTransactionData.getName(), Base58.encode(signature)));
|
||||
|
||||
ArbitraryTransactionUtils.deleteCompleteFileAndChunks(arbitraryTransactionData);
|
||||
|
||||
Optional<ArbitraryTransactionData> moreRecentPutTransaction
|
||||
= processedTransactions.stream()
|
||||
.filter(data -> data.equals(arbitraryTransactionData))
|
||||
.findAny();
|
||||
|
||||
if( moreRecentPutTransaction.isPresent() ) {
|
||||
EventBus.INSTANCE.notify(
|
||||
new DataMonitorEvent(
|
||||
System.currentTimeMillis(),
|
||||
arbitraryTransactionData.getIdentifier(),
|
||||
arbitraryTransactionData.getName(),
|
||||
arbitraryTransactionData.getService().name(),
|
||||
"deleting data due to replacement",
|
||||
arbitraryTransactionData.getTimestamp(),
|
||||
moreRecentPutTransaction.get().getTimestamp()
|
||||
)
|
||||
);
|
||||
}
|
||||
else {
|
||||
LOGGER.warn("Something went wrong with the most recent put transaction determination!");
|
||||
}
|
||||
|
||||
continue;
|
||||
}
|
||||
|
||||
@ -255,21 +199,7 @@ public class ArbitraryDataCleanupManager extends Thread {
|
||||
LOGGER.debug(String.format("Transaction %s has complete file and all chunks",
|
||||
Base58.encode(arbitraryTransactionData.getSignature())));
|
||||
|
||||
boolean wasDeleted = ArbitraryTransactionUtils.deleteCompleteFile(arbitraryTransactionData, now, STALE_FILE_TIMEOUT);
|
||||
|
||||
if( wasDeleted ) {
|
||||
EventBus.INSTANCE.notify(
|
||||
new DataMonitorEvent(
|
||||
System.currentTimeMillis(),
|
||||
arbitraryTransactionData.getIdentifier(),
|
||||
arbitraryTransactionData.getName(),
|
||||
arbitraryTransactionData.getService().name(),
|
||||
"deleting file, retaining chunks",
|
||||
arbitraryTransactionData.getTimestamp(),
|
||||
arbitraryTransactionData.getTimestamp()
|
||||
)
|
||||
);
|
||||
}
|
||||
ArbitraryTransactionUtils.deleteCompleteFile(arbitraryTransactionData, now, STALE_FILE_TIMEOUT);
|
||||
continue;
|
||||
}
|
||||
|
||||
@ -307,6 +237,17 @@ public class ArbitraryDataCleanupManager extends Thread {
|
||||
this.storageLimitReached(repository);
|
||||
}
|
||||
|
||||
// Delete random data associated with name if we're over our storage limit for this name
|
||||
// Use the DELETION_THRESHOLD, for the same reasons as above
|
||||
for (String followedName : ListUtils.followedNames()) {
|
||||
if (isStopping) {
|
||||
return;
|
||||
}
|
||||
if (!storageManager.isStorageSpaceAvailableForName(repository, followedName, DELETION_THRESHOLD)) {
|
||||
this.storageLimitReachedForName(repository, followedName);
|
||||
}
|
||||
}
|
||||
|
||||
} catch (DataException e) {
|
||||
LOGGER.error("Repository issue when cleaning up arbitrary transaction data", e);
|
||||
}
|
||||
@ -385,6 +326,25 @@ public class ArbitraryDataCleanupManager extends Thread {
|
||||
// FUTURE: consider reducing the expiry time of the reader cache
|
||||
}
|
||||
|
||||
public void storageLimitReachedForName(Repository repository, String name) throws InterruptedException {
|
||||
// We think that the storage limit has been reached for supplied name - but we should double check
|
||||
if (ArbitraryDataStorageManager.getInstance().isStorageSpaceAvailableForName(repository, name, DELETION_THRESHOLD)) {
|
||||
// We have space available for this name, so don't delete anything
|
||||
return;
|
||||
}
|
||||
|
||||
// Delete a batch of random chunks associated with this name
|
||||
// This reduces the chance of too many nodes deleting the same chunk
|
||||
// when they reach their storage limit
|
||||
Path dataPath = Paths.get(Settings.getInstance().getDataPath());
|
||||
for (int i=0; i<CHUNK_DELETION_BATCH_SIZE; i++) {
|
||||
if (isStopping) {
|
||||
return;
|
||||
}
|
||||
this.deleteRandomFile(repository, dataPath.toFile(), name);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Iteratively walk through given directory and delete a single random file
|
||||
*
|
||||
@ -463,7 +423,6 @@ public class ArbitraryDataCleanupManager extends Thread {
|
||||
}
|
||||
|
||||
LOGGER.info("Deleting random file {} because we have reached max storage capacity...", randomItem.toString());
|
||||
fireRandomItemDeletionNotification(randomItem, repository, "Deleting random file, because we have reached max storage capacity");
|
||||
boolean success = randomItem.delete();
|
||||
if (success) {
|
||||
try {
|
||||
@ -478,35 +437,6 @@ public class ArbitraryDataCleanupManager extends Thread {
|
||||
return false;
|
||||
}
|
||||
|
||||
private void fireRandomItemDeletionNotification(File randomItem, Repository repository, String reason) {
|
||||
try {
|
||||
Path parentFileNamePath = randomItem.toPath().toAbsolutePath().getParent().getFileName();
|
||||
if (parentFileNamePath != null) {
|
||||
String signature58 = parentFileNamePath.toString();
|
||||
byte[] signature = Base58.decode(signature58);
|
||||
TransactionData transactionData = repository.getTransactionRepository().fromSignature(signature);
|
||||
if (transactionData != null && transactionData.getType() == Transaction.TransactionType.ARBITRARY) {
|
||||
ArbitraryTransactionData arbitraryTransactionData = (ArbitraryTransactionData) transactionData;
|
||||
|
||||
EventBus.INSTANCE.notify(
|
||||
new DataMonitorEvent(
|
||||
System.currentTimeMillis(),
|
||||
arbitraryTransactionData.getIdentifier(),
|
||||
arbitraryTransactionData.getName(),
|
||||
arbitraryTransactionData.getService().name(),
|
||||
reason,
|
||||
arbitraryTransactionData.getTimestamp(),
|
||||
arbitraryTransactionData.getTimestamp()
|
||||
)
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
} catch (Exception e) {
|
||||
LOGGER.error(e.getMessage(), e);
|
||||
}
|
||||
}
|
||||
|
||||
private void cleanupTempDirectory(String folder, long now, long minAge) {
|
||||
String baseDir = Settings.getInstance().getTempDataPath();
|
||||
Path tempDir = Paths.get(baseDir, folder);
|
||||
|
@ -1,21 +0,0 @@
|
||||
package org.qortal.controller.arbitrary;
|
||||
|
||||
public class ArbitraryDataExamination {
|
||||
|
||||
private boolean pass;
|
||||
|
||||
private String notes;
|
||||
|
||||
public ArbitraryDataExamination(boolean pass, String notes) {
|
||||
this.pass = pass;
|
||||
this.notes = notes;
|
||||
}
|
||||
|
||||
public boolean isPass() {
|
||||
return pass;
|
||||
}
|
||||
|
||||
public String getNotes() {
|
||||
return notes;
|
||||
}
|
||||
}
|
@ -25,10 +25,6 @@ import org.qortal.utils.NTP;
|
||||
import org.qortal.utils.Triple;
|
||||
|
||||
import java.util.*;
|
||||
import java.util.concurrent.Executors;
|
||||
import java.util.concurrent.ScheduledExecutorService;
|
||||
import java.util.concurrent.TimeUnit;
|
||||
import java.util.stream.Collectors;
|
||||
|
||||
import static org.qortal.controller.arbitrary.ArbitraryDataFileManager.MAX_FILE_HASH_RESPONSES;
|
||||
|
||||
@ -77,8 +73,6 @@ public class ArbitraryDataFileListManager {
|
||||
|
||||
|
||||
private ArbitraryDataFileListManager() {
|
||||
getArbitraryDataFileListMessageScheduler.scheduleAtFixedRate(this::processNetworkGetArbitraryDataFileListMessage, 60, 1, TimeUnit.SECONDS);
|
||||
arbitraryDataFileListMessageScheduler.scheduleAtFixedRate(this::processNetworkArbitraryDataFileListMessage, 60, 1, TimeUnit.SECONDS);
|
||||
}
|
||||
|
||||
public static ArbitraryDataFileListManager getInstance() {
|
||||
@ -124,8 +118,8 @@ public class ArbitraryDataFileListManager {
|
||||
if (timeSinceLastAttempt > 15 * 1000L) {
|
||||
// We haven't tried for at least 15 seconds
|
||||
|
||||
if (networkBroadcastCount < 12) {
|
||||
// We've made less than 12 total attempts
|
||||
if (networkBroadcastCount < 3) {
|
||||
// We've made less than 3 total attempts
|
||||
return true;
|
||||
}
|
||||
}
|
||||
@ -134,8 +128,8 @@ public class ArbitraryDataFileListManager {
|
||||
if (timeSinceLastAttempt > 60 * 1000L) {
|
||||
// We haven't tried for at least 1 minute
|
||||
|
||||
if (networkBroadcastCount < 40) {
|
||||
// We've made less than 40 total attempts
|
||||
if (networkBroadcastCount < 8) {
|
||||
// We've made less than 8 total attempts
|
||||
return true;
|
||||
}
|
||||
}
|
||||
@ -402,11 +396,11 @@ public class ArbitraryDataFileListManager {
|
||||
return true;
|
||||
}
|
||||
|
||||
public void deleteFileListRequestsForSignature(String signature58) {
|
||||
|
||||
public void deleteFileListRequestsForSignature(byte[] signature) {
|
||||
String signature58 = Base58.encode(signature);
|
||||
for (Iterator<Map.Entry<Integer, Triple<String, Peer, Long>>> it = arbitraryDataFileListRequests.entrySet().iterator(); it.hasNext();) {
|
||||
Map.Entry<Integer, Triple<String, Peer, Long>> entry = it.next();
|
||||
if (entry == null || entry.getKey() == null || entry.getValue() == null) {
|
||||
if (entry == null || entry.getKey() == null || entry.getValue() != null) {
|
||||
continue;
|
||||
}
|
||||
if (Objects.equals(entry.getValue().getA(), signature58)) {
|
||||
@ -419,116 +413,70 @@ public class ArbitraryDataFileListManager {
|
||||
|
||||
// Network handlers
|
||||
|
||||
// List to collect messages
|
||||
private final List<PeerMessage> arbitraryDataFileListMessageList = new ArrayList<>();
|
||||
// Lock to synchronize access to the list
|
||||
private final Object arbitraryDataFileListMessageLock = new Object();
|
||||
|
||||
// Scheduled executor service to process messages every second
|
||||
private final ScheduledExecutorService arbitraryDataFileListMessageScheduler = Executors.newScheduledThreadPool(1);
|
||||
|
||||
public void onNetworkArbitraryDataFileListMessage(Peer peer, Message message) {
|
||||
// Don't process if QDN is disabled
|
||||
if (!Settings.getInstance().isQdnEnabled()) {
|
||||
return;
|
||||
}
|
||||
|
||||
synchronized (arbitraryDataFileListMessageLock) {
|
||||
arbitraryDataFileListMessageList.add(new PeerMessage(peer, message));
|
||||
ArbitraryDataFileListMessage arbitraryDataFileListMessage = (ArbitraryDataFileListMessage) message;
|
||||
LOGGER.debug("Received hash list from peer {} with {} hashes", peer, arbitraryDataFileListMessage.getHashes().size());
|
||||
|
||||
if (LOGGER.isDebugEnabled() && arbitraryDataFileListMessage.getRequestTime() != null) {
|
||||
long totalRequestTime = NTP.getTime() - arbitraryDataFileListMessage.getRequestTime();
|
||||
LOGGER.debug("totalRequestTime: {}, requestHops: {}, peerAddress: {}, isRelayPossible: {}",
|
||||
totalRequestTime, arbitraryDataFileListMessage.getRequestHops(),
|
||||
arbitraryDataFileListMessage.getPeerAddress(), arbitraryDataFileListMessage.isRelayPossible());
|
||||
}
|
||||
}
|
||||
|
||||
private void processNetworkArbitraryDataFileListMessage() {
|
||||
// Do we have a pending request for this data?
|
||||
Triple<String, Peer, Long> request = arbitraryDataFileListRequests.get(message.getId());
|
||||
if (request == null || request.getA() == null) {
|
||||
return;
|
||||
}
|
||||
boolean isRelayRequest = (request.getB() != null);
|
||||
|
||||
try {
|
||||
List<PeerMessage> messagesToProcess;
|
||||
synchronized (arbitraryDataFileListMessageLock) {
|
||||
messagesToProcess = new ArrayList<>(arbitraryDataFileListMessageList);
|
||||
arbitraryDataFileListMessageList.clear();
|
||||
}
|
||||
// Does this message's signature match what we're expecting?
|
||||
byte[] signature = arbitraryDataFileListMessage.getSignature();
|
||||
String signature58 = Base58.encode(signature);
|
||||
if (!request.getA().equals(signature58)) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (messagesToProcess.isEmpty()) return;
|
||||
List<byte[]> hashes = arbitraryDataFileListMessage.getHashes();
|
||||
if (hashes == null || hashes.isEmpty()) {
|
||||
return;
|
||||
}
|
||||
|
||||
Map<String, PeerMessage> peerMessageBySignature58 = new HashMap<>(messagesToProcess.size());
|
||||
Map<String, byte[]> signatureBySignature58 = new HashMap<>(messagesToProcess.size());
|
||||
Map<String, Boolean> isRelayRequestBySignature58 = new HashMap<>(messagesToProcess.size());
|
||||
Map<String, List<byte[]>> hashesBySignature58 = new HashMap<>(messagesToProcess.size());
|
||||
Map<String, Triple<String, Peer, Long>> requestBySignature58 = new HashMap<>(messagesToProcess.size());
|
||||
ArbitraryTransactionData arbitraryTransactionData = null;
|
||||
|
||||
for (PeerMessage peerMessage : messagesToProcess) {
|
||||
Peer peer = peerMessage.getPeer();
|
||||
Message message = peerMessage.getMessage();
|
||||
|
||||
ArbitraryDataFileListMessage arbitraryDataFileListMessage = (ArbitraryDataFileListMessage) message;
|
||||
LOGGER.debug("Received hash list from peer {} with {} hashes", peer, arbitraryDataFileListMessage.getHashes().size());
|
||||
|
||||
if (LOGGER.isDebugEnabled() && arbitraryDataFileListMessage.getRequestTime() != null) {
|
||||
long totalRequestTime = NTP.getTime() - arbitraryDataFileListMessage.getRequestTime();
|
||||
LOGGER.debug("totalRequestTime: {}, requestHops: {}, peerAddress: {}, isRelayPossible: {}",
|
||||
totalRequestTime, arbitraryDataFileListMessage.getRequestHops(),
|
||||
arbitraryDataFileListMessage.getPeerAddress(), arbitraryDataFileListMessage.isRelayPossible());
|
||||
}
|
||||
|
||||
// Do we have a pending request for this data?
|
||||
Triple<String, Peer, Long> request = arbitraryDataFileListRequests.get(message.getId());
|
||||
if (request == null || request.getA() == null) {
|
||||
continue;
|
||||
}
|
||||
boolean isRelayRequest = (request.getB() != null);
|
||||
|
||||
// Does this message's signature match what we're expecting?
|
||||
byte[] signature = arbitraryDataFileListMessage.getSignature();
|
||||
String signature58 = Base58.encode(signature);
|
||||
if (!request.getA().equals(signature58)) {
|
||||
continue;
|
||||
}
|
||||
|
||||
List<byte[]> hashes = arbitraryDataFileListMessage.getHashes();
|
||||
if (hashes == null || hashes.isEmpty()) {
|
||||
continue;
|
||||
}
|
||||
|
||||
peerMessageBySignature58.put(signature58, peerMessage);
|
||||
signatureBySignature58.put(signature58, signature);
|
||||
isRelayRequestBySignature58.put(signature58, isRelayRequest);
|
||||
hashesBySignature58.put(signature58, hashes);
|
||||
requestBySignature58.put(signature58, request);
|
||||
}
|
||||
|
||||
if (signatureBySignature58.isEmpty()) return;
|
||||
|
||||
List<ArbitraryTransactionData> arbitraryTransactionDataList;
|
||||
|
||||
// Check transaction exists and hashes are correct
|
||||
try (final Repository repository = RepositoryManager.getRepository()) {
|
||||
arbitraryTransactionDataList
|
||||
= repository.getTransactionRepository()
|
||||
.fromSignatures(new ArrayList<>(signatureBySignature58.values())).stream()
|
||||
.filter(data -> data instanceof ArbitraryTransactionData)
|
||||
.map(data -> (ArbitraryTransactionData) data)
|
||||
.collect(Collectors.toList());
|
||||
} catch (DataException e) {
|
||||
LOGGER.error(String.format("Repository issue while finding arbitrary transaction data list"), e);
|
||||
// Check transaction exists and hashes are correct
|
||||
try (final Repository repository = RepositoryManager.getRepository()) {
|
||||
TransactionData transactionData = repository.getTransactionRepository().fromSignature(signature);
|
||||
if (!(transactionData instanceof ArbitraryTransactionData))
|
||||
return;
|
||||
}
|
||||
|
||||
for (ArbitraryTransactionData arbitraryTransactionData : arbitraryTransactionDataList) {
|
||||
arbitraryTransactionData = (ArbitraryTransactionData) transactionData;
|
||||
|
||||
byte[] signature = arbitraryTransactionData.getSignature();
|
||||
String signature58 = Base58.encode(signature);
|
||||
// // Load data file(s)
|
||||
// ArbitraryDataFile arbitraryDataFile = ArbitraryDataFile.fromTransactionData(arbitraryTransactionData);
|
||||
//
|
||||
// // Check all hashes exist
|
||||
// for (byte[] hash : hashes) {
|
||||
// //LOGGER.debug("Received hash {}", Base58.encode(hash));
|
||||
// if (!arbitraryDataFile.containsChunk(hash)) {
|
||||
// // Check the hash against the complete file
|
||||
// if (!Arrays.equals(arbitraryDataFile.getHash(), hash)) {
|
||||
// LOGGER.info("Received non-matching chunk hash {} for signature {}. This could happen if we haven't obtained the metadata file yet.", Base58.encode(hash), signature58);
|
||||
// return;
|
||||
// }
|
||||
// }
|
||||
// }
|
||||
|
||||
List<byte[]> hashes = hashesBySignature58.get(signature58);
|
||||
|
||||
PeerMessage peerMessage = peerMessageBySignature58.get(signature58);
|
||||
Peer peer = peerMessage.getPeer();
|
||||
Message message = peerMessage.getMessage();
|
||||
|
||||
ArbitraryDataFileListMessage arbitraryDataFileListMessage = (ArbitraryDataFileListMessage) message;
|
||||
|
||||
Boolean isRelayRequest = isRelayRequestBySignature58.get(signature58);
|
||||
if (!isRelayRequest || !Settings.getInstance().isRelayModeEnabled()) {
|
||||
Long now = NTP.getTime();
|
||||
if (!isRelayRequest || !Settings.getInstance().isRelayModeEnabled()) {
|
||||
Long now = NTP.getTime();
|
||||
|
||||
if (ArbitraryDataFileManager.getInstance().arbitraryDataFileHashResponses.size() < MAX_FILE_HASH_RESPONSES) {
|
||||
// Keep track of the hashes this peer reports to have access to
|
||||
for (byte[] hash : hashes) {
|
||||
String hash58 = Base58.encode(hash);
|
||||
@ -539,300 +487,233 @@ public class ArbitraryDataFileListManager {
|
||||
ArbitraryFileListResponseInfo responseInfo = new ArbitraryFileListResponseInfo(hash58, signature58,
|
||||
peer, now, arbitraryDataFileListMessage.getRequestTime(), requestHops);
|
||||
|
||||
ArbitraryDataFileManager.getInstance().addResponse(responseInfo);
|
||||
}
|
||||
|
||||
// Keep track of the source peer, for direct connections
|
||||
if (arbitraryDataFileListMessage.getPeerAddress() != null) {
|
||||
ArbitraryDataFileManager.getInstance().addDirectConnectionInfoIfUnique(
|
||||
new ArbitraryDirectConnectionInfo(signature, arbitraryDataFileListMessage.getPeerAddress(), hashes, now));
|
||||
ArbitraryDataFileManager.getInstance().arbitraryDataFileHashResponses.add(responseInfo);
|
||||
}
|
||||
}
|
||||
|
||||
// Forwarding
|
||||
if (isRelayRequest && Settings.getInstance().isRelayModeEnabled()) {
|
||||
// Keep track of the source peer, for direct connections
|
||||
if (arbitraryDataFileListMessage.getPeerAddress() != null) {
|
||||
ArbitraryDataFileManager.getInstance().addDirectConnectionInfoIfUnique(
|
||||
new ArbitraryDirectConnectionInfo(signature, arbitraryDataFileListMessage.getPeerAddress(), hashes, now));
|
||||
}
|
||||
}
|
||||
|
||||
boolean isBlocked = (arbitraryTransactionData == null || ListUtils.isNameBlocked(arbitraryTransactionData.getName()));
|
||||
if (!isBlocked) {
|
||||
Triple<String, Peer, Long> request = requestBySignature58.get(signature58);
|
||||
Peer requestingPeer = request.getB();
|
||||
if (requestingPeer != null) {
|
||||
Long requestTime = arbitraryDataFileListMessage.getRequestTime();
|
||||
Integer requestHops = arbitraryDataFileListMessage.getRequestHops();
|
||||
} catch (DataException e) {
|
||||
LOGGER.error(String.format("Repository issue while finding arbitrary transaction data list for peer %s", peer), e);
|
||||
}
|
||||
|
||||
// Add each hash to our local mapping so we know who to ask later
|
||||
Long now = NTP.getTime();
|
||||
for (byte[] hash : hashes) {
|
||||
String hash58 = Base58.encode(hash);
|
||||
ArbitraryRelayInfo relayInfo = new ArbitraryRelayInfo(hash58, signature58, peer, now, requestTime, requestHops);
|
||||
ArbitraryDataFileManager.getInstance().addToRelayMap(relayInfo);
|
||||
}
|
||||
// Forwarding
|
||||
if (isRelayRequest && Settings.getInstance().isRelayModeEnabled()) {
|
||||
boolean isBlocked = (arbitraryTransactionData == null || ListUtils.isNameBlocked(arbitraryTransactionData.getName()));
|
||||
if (!isBlocked) {
|
||||
Peer requestingPeer = request.getB();
|
||||
if (requestingPeer != null) {
|
||||
Long requestTime = arbitraryDataFileListMessage.getRequestTime();
|
||||
Integer requestHops = arbitraryDataFileListMessage.getRequestHops();
|
||||
|
||||
// Bump requestHops if it exists
|
||||
if (requestHops != null) {
|
||||
requestHops++;
|
||||
}
|
||||
// Add each hash to our local mapping so we know who to ask later
|
||||
Long now = NTP.getTime();
|
||||
for (byte[] hash : hashes) {
|
||||
String hash58 = Base58.encode(hash);
|
||||
ArbitraryRelayInfo relayInfo = new ArbitraryRelayInfo(hash58, signature58, peer, now, requestTime, requestHops);
|
||||
ArbitraryDataFileManager.getInstance().addToRelayMap(relayInfo);
|
||||
}
|
||||
|
||||
ArbitraryDataFileListMessage forwardArbitraryDataFileListMessage;
|
||||
// Bump requestHops if it exists
|
||||
if (requestHops != null) {
|
||||
requestHops++;
|
||||
}
|
||||
|
||||
// Remove optional parameters if the requesting peer doesn't support it yet
|
||||
// A message with less statistical data is better than no message at all
|
||||
if (!requestingPeer.isAtLeastVersion(MIN_PEER_VERSION_FOR_FILE_LIST_STATS)) {
|
||||
forwardArbitraryDataFileListMessage = new ArbitraryDataFileListMessage(signature, hashes);
|
||||
} else {
|
||||
forwardArbitraryDataFileListMessage = new ArbitraryDataFileListMessage(signature, hashes, requestTime, requestHops,
|
||||
arbitraryDataFileListMessage.getPeerAddress(), arbitraryDataFileListMessage.isRelayPossible());
|
||||
}
|
||||
forwardArbitraryDataFileListMessage.setId(message.getId());
|
||||
ArbitraryDataFileListMessage forwardArbitraryDataFileListMessage;
|
||||
|
||||
// Forward to requesting peer
|
||||
LOGGER.debug("Forwarding file list with {} hashes to requesting peer: {}", hashes.size(), requestingPeer);
|
||||
requestingPeer.sendMessage(forwardArbitraryDataFileListMessage);
|
||||
}
|
||||
// Remove optional parameters if the requesting peer doesn't support it yet
|
||||
// A message with less statistical data is better than no message at all
|
||||
if (!requestingPeer.isAtLeastVersion(MIN_PEER_VERSION_FOR_FILE_LIST_STATS)) {
|
||||
forwardArbitraryDataFileListMessage = new ArbitraryDataFileListMessage(signature, hashes);
|
||||
} else {
|
||||
forwardArbitraryDataFileListMessage = new ArbitraryDataFileListMessage(signature, hashes, requestTime, requestHops,
|
||||
arbitraryDataFileListMessage.getPeerAddress(), arbitraryDataFileListMessage.isRelayPossible());
|
||||
}
|
||||
forwardArbitraryDataFileListMessage.setId(message.getId());
|
||||
|
||||
// Forward to requesting peer
|
||||
LOGGER.debug("Forwarding file list with {} hashes to requesting peer: {}", hashes.size(), requestingPeer);
|
||||
if (!requestingPeer.sendMessage(forwardArbitraryDataFileListMessage)) {
|
||||
requestingPeer.disconnect("failed to forward arbitrary data file list");
|
||||
}
|
||||
}
|
||||
}
|
||||
} catch (Exception e) {
|
||||
LOGGER.error(e.getMessage(), e);
|
||||
}
|
||||
}
|
||||
|
||||
// List to collect messages
|
||||
private final List<PeerMessage> getArbitraryDataFileListMessageList = new ArrayList<>();
|
||||
// Lock to synchronize access to the list
|
||||
private final Object getArbitraryDataFileListMessageLock = new Object();
|
||||
|
||||
// Scheduled executor service to process messages every second
|
||||
private final ScheduledExecutorService getArbitraryDataFileListMessageScheduler = Executors.newScheduledThreadPool(1);
|
||||
|
||||
public void onNetworkGetArbitraryDataFileListMessage(Peer peer, Message message) {
|
||||
// Don't respond if QDN is disabled
|
||||
if (!Settings.getInstance().isQdnEnabled()) {
|
||||
return;
|
||||
}
|
||||
|
||||
synchronized (getArbitraryDataFileListMessageLock) {
|
||||
getArbitraryDataFileListMessageList.add(new PeerMessage(peer, message));
|
||||
Controller.getInstance().stats.getArbitraryDataFileListMessageStats.requests.incrementAndGet();
|
||||
|
||||
GetArbitraryDataFileListMessage getArbitraryDataFileListMessage = (GetArbitraryDataFileListMessage) message;
|
||||
byte[] signature = getArbitraryDataFileListMessage.getSignature();
|
||||
String signature58 = Base58.encode(signature);
|
||||
Long now = NTP.getTime();
|
||||
Triple<String, Peer, Long> newEntry = new Triple<>(signature58, peer, now);
|
||||
|
||||
// If we've seen this request recently, then ignore
|
||||
if (arbitraryDataFileListRequests.putIfAbsent(message.getId(), newEntry) != null) {
|
||||
LOGGER.trace("Ignoring hash list request from peer {} for signature {}", peer, signature58);
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
private void processNetworkGetArbitraryDataFileListMessage() {
|
||||
List<byte[]> requestedHashes = getArbitraryDataFileListMessage.getHashes();
|
||||
int hashCount = requestedHashes != null ? requestedHashes.size() : 0;
|
||||
String requestingPeer = getArbitraryDataFileListMessage.getRequestingPeer();
|
||||
|
||||
try {
|
||||
List<PeerMessage> messagesToProcess;
|
||||
synchronized (getArbitraryDataFileListMessageLock) {
|
||||
messagesToProcess = new ArrayList<>(getArbitraryDataFileListMessageList);
|
||||
getArbitraryDataFileListMessageList.clear();
|
||||
}
|
||||
if (requestingPeer != null) {
|
||||
LOGGER.debug("Received hash list request with {} hashes from peer {} (requesting peer {}) for signature {}", hashCount, peer, requestingPeer, signature58);
|
||||
}
|
||||
else {
|
||||
LOGGER.debug("Received hash list request with {} hashes from peer {} for signature {}", hashCount, peer, signature58);
|
||||
}
|
||||
|
||||
if (messagesToProcess.isEmpty()) return;
|
||||
List<byte[]> hashes = new ArrayList<>();
|
||||
ArbitraryTransactionData transactionData = null;
|
||||
boolean allChunksExist = false;
|
||||
boolean hasMetadata = false;
|
||||
|
||||
Map<String, byte[]> signatureBySignature58 = new HashMap<>(messagesToProcess.size());
|
||||
Map<String, List<byte[]>> requestedHashesBySignature58 = new HashMap<>(messagesToProcess.size());
|
||||
Map<String, String> requestingPeerBySignature58 = new HashMap<>(messagesToProcess.size());
|
||||
Map<String, Long> nowBySignature58 = new HashMap<>((messagesToProcess.size()));
|
||||
Map<String, PeerMessage> peerMessageBySignature58 = new HashMap<>(messagesToProcess.size());
|
||||
try (final Repository repository = RepositoryManager.getRepository()) {
|
||||
|
||||
for (PeerMessage messagePeer : messagesToProcess) {
|
||||
Controller.getInstance().stats.getArbitraryDataFileListMessageStats.requests.incrementAndGet();
|
||||
|
||||
Message message = messagePeer.message;
|
||||
Peer peer = messagePeer.peer;
|
||||
|
||||
GetArbitraryDataFileListMessage getArbitraryDataFileListMessage = (GetArbitraryDataFileListMessage) message;
|
||||
byte[] signature = getArbitraryDataFileListMessage.getSignature();
|
||||
String signature58 = Base58.encode(signature);
|
||||
Long now = NTP.getTime();
|
||||
Triple<String, Peer, Long> newEntry = new Triple<>(signature58, peer, now);
|
||||
|
||||
// If we've seen this request recently, then ignore
|
||||
if (arbitraryDataFileListRequests.putIfAbsent(message.getId(), newEntry) != null) {
|
||||
LOGGER.trace("Ignoring hash list request from peer {} for signature {}", peer, signature58);
|
||||
continue;
|
||||
}
|
||||
|
||||
List<byte[]> requestedHashes = getArbitraryDataFileListMessage.getHashes();
|
||||
int hashCount = requestedHashes != null ? requestedHashes.size() : 0;
|
||||
String requestingPeer = getArbitraryDataFileListMessage.getRequestingPeer();
|
||||
|
||||
if (requestingPeer != null) {
|
||||
LOGGER.debug("Received hash list request with {} hashes from peer {} (requesting peer {}) for signature {}", hashCount, peer, requestingPeer, signature58);
|
||||
} else {
|
||||
LOGGER.debug("Received hash list request with {} hashes from peer {} for signature {}", hashCount, peer, signature58);
|
||||
}
|
||||
|
||||
signatureBySignature58.put(signature58, signature);
|
||||
requestedHashesBySignature58.put(signature58, requestedHashes);
|
||||
requestingPeerBySignature58.put(signature58, requestingPeer);
|
||||
nowBySignature58.put(signature58, now);
|
||||
peerMessageBySignature58.put(signature58, messagePeer);
|
||||
}
|
||||
|
||||
if (signatureBySignature58.isEmpty()) {
|
||||
return;
|
||||
}
|
||||
|
||||
List<byte[]> hashes = new ArrayList<>();
|
||||
boolean allChunksExist = false;
|
||||
boolean hasMetadata = false;
|
||||
|
||||
List<ArbitraryTransactionData> transactionDataList;
|
||||
try (final Repository repository = RepositoryManager.getRepository()) {
|
||||
|
||||
// Firstly we need to lookup this file on chain to get a list of its hashes
|
||||
transactionDataList
|
||||
= repository.getTransactionRepository()
|
||||
.fromSignatures(new ArrayList<>(signatureBySignature58.values())).stream()
|
||||
.filter(data -> data instanceof ArbitraryTransactionData)
|
||||
.map(data -> (ArbitraryTransactionData) data)
|
||||
.collect(Collectors.toList());
|
||||
|
||||
} catch (DataException e) {
|
||||
LOGGER.error(String.format("Repository issue while fetching arbitrary file list for peer"), e);
|
||||
return;
|
||||
}
|
||||
|
||||
for (ArbitraryTransactionData transactionData : transactionDataList) {
|
||||
byte[] signature = transactionData.getSignature();
|
||||
String signature58 = Base58.encode(signature);
|
||||
List<byte[]> requestedHashes = requestedHashesBySignature58.get(signature58);
|
||||
// Firstly we need to lookup this file on chain to get a list of its hashes
|
||||
transactionData = (ArbitraryTransactionData)repository.getTransactionRepository().fromSignature(signature);
|
||||
if (transactionData instanceof ArbitraryTransactionData) {
|
||||
|
||||
// Check if we're even allowed to serve data for this transaction
|
||||
if (ArbitraryDataStorageManager.getInstance().canStoreData(transactionData)) {
|
||||
|
||||
try {
|
||||
// Load file(s) and add any that exist to the list of hashes
|
||||
ArbitraryDataFile arbitraryDataFile = ArbitraryDataFile.fromTransactionData(transactionData);
|
||||
// Load file(s) and add any that exist to the list of hashes
|
||||
ArbitraryDataFile arbitraryDataFile = ArbitraryDataFile.fromTransactionData(transactionData);
|
||||
|
||||
// If the peer didn't supply a hash list, we need to return all hashes for this transaction
|
||||
if (requestedHashes == null || requestedHashes.isEmpty()) {
|
||||
requestedHashes = new ArrayList<>();
|
||||
// If the peer didn't supply a hash list, we need to return all hashes for this transaction
|
||||
if (requestedHashes == null || requestedHashes.isEmpty()) {
|
||||
requestedHashes = new ArrayList<>();
|
||||
|
||||
// Add the metadata file
|
||||
if (arbitraryDataFile.getMetadataHash() != null) {
|
||||
requestedHashes.add(arbitraryDataFile.getMetadataHash());
|
||||
hasMetadata = true;
|
||||
}
|
||||
|
||||
// Add the chunk hashes
|
||||
if (!arbitraryDataFile.getChunkHashes().isEmpty()) {
|
||||
requestedHashes.addAll(arbitraryDataFile.getChunkHashes());
|
||||
}
|
||||
// Add complete file if there are no hashes
|
||||
else {
|
||||
requestedHashes.add(arbitraryDataFile.getHash());
|
||||
}
|
||||
// Add the metadata file
|
||||
if (arbitraryDataFile.getMetadataHash() != null) {
|
||||
requestedHashes.add(arbitraryDataFile.getMetadataHash());
|
||||
hasMetadata = true;
|
||||
}
|
||||
|
||||
|
||||
// Assume all chunks exists, unless one can't be found below
|
||||
allChunksExist = true;
|
||||
|
||||
for (byte[] requestedHash : requestedHashes) {
|
||||
ArbitraryDataFileChunk chunk = ArbitraryDataFileChunk.fromHash(requestedHash, signature);
|
||||
if (chunk.exists()) {
|
||||
hashes.add(chunk.getHash());
|
||||
//LOGGER.trace("Added hash {}", chunk.getHash58());
|
||||
} else {
|
||||
LOGGER.trace("Couldn't add hash {} because it doesn't exist", chunk.getHash58());
|
||||
allChunksExist = false;
|
||||
}
|
||||
// Add the chunk hashes
|
||||
if (!arbitraryDataFile.getChunkHashes().isEmpty()) {
|
||||
requestedHashes.addAll(arbitraryDataFile.getChunkHashes());
|
||||
}
|
||||
// Add complete file if there are no hashes
|
||||
else {
|
||||
requestedHashes.add(arbitraryDataFile.getHash());
|
||||
}
|
||||
} catch (DataException e) {
|
||||
LOGGER.error(e.getMessage(), e);
|
||||
}
|
||||
}
|
||||
|
||||
// If the only file we have is the metadata then we shouldn't respond. Most nodes will already have that,
|
||||
// or can use the separate metadata protocol to fetch it. This should greatly reduce network spam.
|
||||
if (hasMetadata && hashes.size() == 1) {
|
||||
hashes.clear();
|
||||
}
|
||||
|
||||
PeerMessage peerMessage = peerMessageBySignature58.get(signature58);
|
||||
Peer peer = peerMessage.getPeer();
|
||||
Message message = peerMessage.getMessage();
|
||||
|
||||
Long now = nowBySignature58.get(signature58);
|
||||
|
||||
// We should only respond if we have at least one hash
|
||||
String requestingPeer = requestingPeerBySignature58.get(signature58);
|
||||
if (!hashes.isEmpty()) {
|
||||
|
||||
// Firstly we should keep track of the requesting peer, to allow for potential direct connections later
|
||||
ArbitraryDataFileManager.getInstance().addRecentDataRequest(requestingPeer);
|
||||
|
||||
// We have all the chunks, so update requests map to reflect that we've sent it
|
||||
// There is no need to keep track of the request, as we can serve all the chunks
|
||||
if (allChunksExist) {
|
||||
Triple<String, Peer, Long> newEntry = new Triple<>(null, null, now);
|
||||
arbitraryDataFileListRequests.put(message.getId(), newEntry);
|
||||
}
|
||||
|
||||
String ourAddress = Network.getInstance().getOurExternalIpAddressAndPort();
|
||||
ArbitraryDataFileListMessage arbitraryDataFileListMessage;
|
||||
|
||||
// Remove optional parameters if the requesting peer doesn't support it yet
|
||||
// A message with less statistical data is better than no message at all
|
||||
if (!peer.isAtLeastVersion(MIN_PEER_VERSION_FOR_FILE_LIST_STATS)) {
|
||||
arbitraryDataFileListMessage = new ArbitraryDataFileListMessage(signature, hashes);
|
||||
} else {
|
||||
arbitraryDataFileListMessage = new ArbitraryDataFileListMessage(signature,
|
||||
hashes, NTP.getTime(), 0, ourAddress, true);
|
||||
}
|
||||
|
||||
arbitraryDataFileListMessage.setId(message.getId());
|
||||
|
||||
if (!peer.sendMessage(arbitraryDataFileListMessage)) {
|
||||
LOGGER.debug("Couldn't send list of hashes");
|
||||
continue;
|
||||
}
|
||||
|
||||
if (allChunksExist) {
|
||||
// Nothing left to do, so return to prevent any unnecessary forwarding from occurring
|
||||
LOGGER.debug("No need for any forwarding because file list request is fully served");
|
||||
continue;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
// We may need to forward this request on
|
||||
boolean isBlocked = (transactionData == null || ListUtils.isNameBlocked(transactionData.getName()));
|
||||
if (Settings.getInstance().isRelayModeEnabled() && !isBlocked) {
|
||||
// In relay mode - so ask our other peers if they have it
|
||||
|
||||
|
||||
GetArbitraryDataFileListMessage getArbitraryDataFileListMessage = (GetArbitraryDataFileListMessage) message;
|
||||
|
||||
long requestTime = getArbitraryDataFileListMessage.getRequestTime();
|
||||
int requestHops = getArbitraryDataFileListMessage.getRequestHops() + 1;
|
||||
long totalRequestTime = now - requestTime;
|
||||
|
||||
if (totalRequestTime < RELAY_REQUEST_MAX_DURATION) {
|
||||
// Relay request hasn't timed out yet, so can potentially be rebroadcast
|
||||
if (requestHops < RELAY_REQUEST_MAX_HOPS) {
|
||||
// Relay request hasn't reached the maximum number of hops yet, so can be rebroadcast
|
||||
|
||||
Message relayGetArbitraryDataFileListMessage = new GetArbitraryDataFileListMessage(signature, hashes, requestTime, requestHops, requestingPeer);
|
||||
relayGetArbitraryDataFileListMessage.setId(message.getId());
|
||||
|
||||
LOGGER.debug("Rebroadcasting hash list request from peer {} for signature {} to our other peers... totalRequestTime: {}, requestHops: {}", peer, Base58.encode(signature), totalRequestTime, requestHops);
|
||||
Network.getInstance().broadcast(
|
||||
broadcastPeer ->
|
||||
!broadcastPeer.isAtLeastVersion(RELAY_MIN_PEER_VERSION) ? null :
|
||||
broadcastPeer == peer || Objects.equals(broadcastPeer.getPeerData().getAddress().getHost(), peer.getPeerData().getAddress().getHost()) ? null : relayGetArbitraryDataFileListMessage
|
||||
);
|
||||
// Assume all chunks exists, unless one can't be found below
|
||||
allChunksExist = true;
|
||||
|
||||
for (byte[] requestedHash : requestedHashes) {
|
||||
ArbitraryDataFileChunk chunk = ArbitraryDataFileChunk.fromHash(requestedHash, signature);
|
||||
if (chunk.exists()) {
|
||||
hashes.add(chunk.getHash());
|
||||
//LOGGER.trace("Added hash {}", chunk.getHash58());
|
||||
} else {
|
||||
// This relay request has reached the maximum number of allowed hops
|
||||
LOGGER.trace("Couldn't add hash {} because it doesn't exist", chunk.getHash58());
|
||||
allChunksExist = false;
|
||||
}
|
||||
} else {
|
||||
// This relay request has timed out
|
||||
}
|
||||
}
|
||||
}
|
||||
} catch (Exception e) {
|
||||
LOGGER.error(e.getMessage(), e);
|
||||
|
||||
} catch (DataException e) {
|
||||
LOGGER.error(String.format("Repository issue while fetching arbitrary file list for peer %s", peer), e);
|
||||
}
|
||||
|
||||
// If the only file we have is the metadata then we shouldn't respond. Most nodes will already have that,
|
||||
// or can use the separate metadata protocol to fetch it. This should greatly reduce network spam.
|
||||
if (hasMetadata && hashes.size() == 1) {
|
||||
hashes.clear();
|
||||
}
|
||||
|
||||
// We should only respond if we have at least one hash
|
||||
if (!hashes.isEmpty()) {
|
||||
|
||||
// Firstly we should keep track of the requesting peer, to allow for potential direct connections later
|
||||
ArbitraryDataFileManager.getInstance().addRecentDataRequest(requestingPeer);
|
||||
|
||||
// We have all the chunks, so update requests map to reflect that we've sent it
|
||||
// There is no need to keep track of the request, as we can serve all the chunks
|
||||
if (allChunksExist) {
|
||||
newEntry = new Triple<>(null, null, now);
|
||||
arbitraryDataFileListRequests.put(message.getId(), newEntry);
|
||||
}
|
||||
|
||||
String ourAddress = Network.getInstance().getOurExternalIpAddressAndPort();
|
||||
ArbitraryDataFileListMessage arbitraryDataFileListMessage;
|
||||
|
||||
// Remove optional parameters if the requesting peer doesn't support it yet
|
||||
// A message with less statistical data is better than no message at all
|
||||
if (!peer.isAtLeastVersion(MIN_PEER_VERSION_FOR_FILE_LIST_STATS)) {
|
||||
arbitraryDataFileListMessage = new ArbitraryDataFileListMessage(signature, hashes);
|
||||
} else {
|
||||
arbitraryDataFileListMessage = new ArbitraryDataFileListMessage(signature,
|
||||
hashes, NTP.getTime(), 0, ourAddress, true);
|
||||
}
|
||||
|
||||
arbitraryDataFileListMessage.setId(message.getId());
|
||||
|
||||
if (!peer.sendMessage(arbitraryDataFileListMessage)) {
|
||||
LOGGER.debug("Couldn't send list of hashes");
|
||||
peer.disconnect("failed to send list of hashes");
|
||||
return;
|
||||
}
|
||||
LOGGER.debug("Sent list of hashes (count: {})", hashes.size());
|
||||
|
||||
if (allChunksExist) {
|
||||
// Nothing left to do, so return to prevent any unnecessary forwarding from occurring
|
||||
LOGGER.debug("No need for any forwarding because file list request is fully served");
|
||||
return;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
// We may need to forward this request on
|
||||
boolean isBlocked = (transactionData == null || ListUtils.isNameBlocked(transactionData.getName()));
|
||||
if (Settings.getInstance().isRelayModeEnabled() && !isBlocked) {
|
||||
// In relay mode - so ask our other peers if they have it
|
||||
|
||||
long requestTime = getArbitraryDataFileListMessage.getRequestTime();
|
||||
int requestHops = getArbitraryDataFileListMessage.getRequestHops() + 1;
|
||||
long totalRequestTime = now - requestTime;
|
||||
|
||||
if (totalRequestTime < RELAY_REQUEST_MAX_DURATION) {
|
||||
// Relay request hasn't timed out yet, so can potentially be rebroadcast
|
||||
if (requestHops < RELAY_REQUEST_MAX_HOPS) {
|
||||
// Relay request hasn't reached the maximum number of hops yet, so can be rebroadcast
|
||||
|
||||
Message relayGetArbitraryDataFileListMessage = new GetArbitraryDataFileListMessage(signature, hashes, requestTime, requestHops, requestingPeer);
|
||||
relayGetArbitraryDataFileListMessage.setId(message.getId());
|
||||
|
||||
LOGGER.debug("Rebroadcasting hash list request from peer {} for signature {} to our other peers... totalRequestTime: {}, requestHops: {}", peer, Base58.encode(signature), totalRequestTime, requestHops);
|
||||
Network.getInstance().broadcast(
|
||||
broadcastPeer ->
|
||||
!broadcastPeer.isAtLeastVersion(RELAY_MIN_PEER_VERSION) ? null :
|
||||
broadcastPeer == peer || Objects.equals(broadcastPeer.getPeerData().getAddress().getHost(), peer.getPeerData().getAddress().getHost()) ? null : relayGetArbitraryDataFileListMessage
|
||||
);
|
||||
|
||||
}
|
||||
else {
|
||||
// This relay request has reached the maximum number of allowed hops
|
||||
}
|
||||
}
|
||||
else {
|
||||
// This relay request has timed out
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -1,7 +1,6 @@
|
||||
package org.qortal.controller.arbitrary;
|
||||
|
||||
import com.google.common.net.InetAddresses;
|
||||
|
||||
import org.apache.logging.log4j.LogManager;
|
||||
import org.apache.logging.log4j.Logger;
|
||||
import org.qortal.arbitrary.ArbitraryDataFile;
|
||||
@ -13,7 +12,6 @@ import org.qortal.data.network.PeerData;
|
||||
import org.qortal.data.transaction.ArbitraryTransactionData;
|
||||
import org.qortal.network.Network;
|
||||
import org.qortal.network.Peer;
|
||||
import org.qortal.network.PeerSendManagement;
|
||||
import org.qortal.network.message.*;
|
||||
import org.qortal.repository.DataException;
|
||||
import org.qortal.repository.Repository;
|
||||
@ -25,16 +23,12 @@ import org.qortal.utils.NTP;
|
||||
|
||||
import java.security.SecureRandom;
|
||||
import java.util.*;
|
||||
import java.util.concurrent.ConcurrentHashMap;
|
||||
import java.util.concurrent.ConcurrentMap;
|
||||
import java.util.concurrent.ExecutorService;
|
||||
import java.util.concurrent.Executors;
|
||||
import java.util.concurrent.ScheduledExecutorService;
|
||||
import java.util.concurrent.TimeUnit;
|
||||
import java.util.stream.Collectors;
|
||||
|
||||
public class ArbitraryDataFileManager extends Thread {
|
||||
|
||||
public static final int SEND_TIMEOUT_MS = 500;
|
||||
private static final Logger LOGGER = LogManager.getLogger(ArbitraryDataFileManager.class);
|
||||
|
||||
private static ArbitraryDataFileManager instance;
|
||||
@ -54,7 +48,7 @@ public class ArbitraryDataFileManager extends Thread {
|
||||
/**
|
||||
* List to keep track of any arbitrary data file hash responses
|
||||
*/
|
||||
private final List<ArbitraryFileListResponseInfo> arbitraryDataFileHashResponses = Collections.synchronizedList(new ArrayList<>());
|
||||
public final List<ArbitraryFileListResponseInfo> arbitraryDataFileHashResponses = Collections.synchronizedList(new ArrayList<>());
|
||||
|
||||
/**
|
||||
* List to keep track of peers potentially available for direct connections, based on recent requests
|
||||
@ -71,9 +65,8 @@ public class ArbitraryDataFileManager extends Thread {
|
||||
|
||||
public static int MAX_FILE_HASH_RESPONSES = 1000;
|
||||
|
||||
|
||||
private ArbitraryDataFileManager() {
|
||||
this.arbitraryDataFileHashResponseScheduler.scheduleAtFixedRate( this::processResponses, 60, 1, TimeUnit.SECONDS);
|
||||
this.arbitraryDataFileHashResponseScheduler.scheduleAtFixedRate(this::handleFileListRequestProcess, 60, 1, TimeUnit.SECONDS);
|
||||
}
|
||||
|
||||
public static ArbitraryDataFileManager getInstance() {
|
||||
@ -83,13 +76,18 @@ public class ArbitraryDataFileManager extends Thread {
|
||||
return instance;
|
||||
}
|
||||
|
||||
|
||||
|
||||
@Override
|
||||
public void run() {
|
||||
Thread.currentThread().setName("Arbitrary Data File Manager");
|
||||
|
||||
try {
|
||||
// Use a fixed thread pool to execute the arbitrary data file requests
|
||||
int threadCount = 5;
|
||||
ExecutorService arbitraryDataFileRequestExecutor = Executors.newFixedThreadPool(threadCount);
|
||||
for (int i = 0; i < threadCount; i++) {
|
||||
arbitraryDataFileRequestExecutor.execute(new ArbitraryDataFileRequestThread());
|
||||
}
|
||||
|
||||
while (!isStopping) {
|
||||
// Nothing to do yet
|
||||
Thread.sleep(1000);
|
||||
@ -114,6 +112,7 @@ public class ArbitraryDataFileManager extends Thread {
|
||||
|
||||
final long relayMinimumTimestamp = now - ArbitraryDataManager.getInstance().ARBITRARY_RELAY_TIMEOUT;
|
||||
arbitraryRelayMap.removeIf(entry -> entry == null || entry.getTimestamp() == null || entry.getTimestamp() < relayMinimumTimestamp);
|
||||
arbitraryDataFileHashResponses.removeIf(entry -> entry.getTimestamp() < relayMinimumTimestamp);
|
||||
|
||||
final long directConnectionInfoMinimumTimestamp = now - ArbitraryDataManager.getInstance().ARBITRARY_DIRECT_CONNECTION_INFO_TIMEOUT;
|
||||
directConnectionInfo.removeIf(entry -> entry.getTimestamp() < directConnectionInfoMinimumTimestamp);
|
||||
@ -126,7 +125,8 @@ public class ArbitraryDataFileManager extends Thread {
|
||||
|
||||
// Fetch data files by hash
|
||||
|
||||
public boolean fetchArbitraryDataFiles(Peer peer,
|
||||
public boolean fetchArbitraryDataFiles(Repository repository,
|
||||
Peer peer,
|
||||
byte[] signature,
|
||||
ArbitraryTransactionData arbitraryTransactionData,
|
||||
List<byte[]> hashes) throws DataException {
|
||||
@ -146,15 +146,21 @@ public class ArbitraryDataFileManager extends Thread {
|
||||
if (!arbitraryDataFileRequests.containsKey(Base58.encode(hash))) {
|
||||
LOGGER.debug("Requesting data file {} from peer {}", hash58, peer);
|
||||
Long startTime = NTP.getTime();
|
||||
ArbitraryDataFile receivedArbitraryDataFile = fetchArbitraryDataFile(peer, arbitraryTransactionData, signature, hash);
|
||||
ArbitraryDataFile receivedArbitraryDataFile = fetchArbitraryDataFile(peer, null, arbitraryTransactionData, signature, hash, null);
|
||||
Long endTime = NTP.getTime();
|
||||
if (receivedArbitraryDataFile != null) {
|
||||
LOGGER.debug("Received data file {} from peer {}. Time taken: {} ms", receivedArbitraryDataFile.getHash58(), peer, (endTime-startTime));
|
||||
receivedAtLeastOneFile = true;
|
||||
|
||||
// Remove this hash from arbitraryDataFileHashResponses now that we have received it
|
||||
arbitraryDataFileHashResponses.remove(hash58);
|
||||
}
|
||||
else {
|
||||
LOGGER.debug("Peer {} didn't respond with data file {} for signature {}. Time taken: {} ms", peer, Base58.encode(hash), Base58.encode(signature), (endTime-startTime));
|
||||
|
||||
// Remove this hash from arbitraryDataFileHashResponses now that we have failed to receive it
|
||||
arbitraryDataFileHashResponses.remove(hash58);
|
||||
|
||||
// Stop asking for files from this peer
|
||||
break;
|
||||
}
|
||||
@ -163,6 +169,10 @@ public class ArbitraryDataFileManager extends Thread {
|
||||
LOGGER.trace("Already requesting data file {} for signature {} from peer {}", arbitraryDataFile, Base58.encode(signature), peer);
|
||||
}
|
||||
}
|
||||
else {
|
||||
// Remove this hash from arbitraryDataFileHashResponses because we have a local copy
|
||||
arbitraryDataFileHashResponses.remove(hash58);
|
||||
}
|
||||
}
|
||||
|
||||
if (receivedAtLeastOneFile) {
|
||||
@ -181,103 +191,14 @@ public class ArbitraryDataFileManager extends Thread {
|
||||
return receivedAtLeastOneFile;
|
||||
}
|
||||
|
||||
// Lock to synchronize access to the list
|
||||
private final Object arbitraryDataFileHashResponseLock = new Object();
|
||||
|
||||
// Scheduled executor service to process messages every second
|
||||
private final ScheduledExecutorService arbitraryDataFileHashResponseScheduler = Executors.newScheduledThreadPool(1);
|
||||
|
||||
|
||||
public void addResponse( ArbitraryFileListResponseInfo responseInfo ) {
|
||||
|
||||
synchronized (arbitraryDataFileHashResponseLock) {
|
||||
this.arbitraryDataFileHashResponses.add(responseInfo);
|
||||
}
|
||||
}
|
||||
|
||||
private void processResponses() {
|
||||
try {
|
||||
List<ArbitraryFileListResponseInfo> responsesToProcess;
|
||||
synchronized (arbitraryDataFileHashResponseLock) {
|
||||
responsesToProcess = new ArrayList<>(arbitraryDataFileHashResponses);
|
||||
arbitraryDataFileHashResponses.clear();
|
||||
}
|
||||
|
||||
if (responsesToProcess.isEmpty()) return;
|
||||
|
||||
Long now = NTP.getTime();
|
||||
|
||||
ArbitraryDataFileRequestThread.getInstance().processFileHashes(now, responsesToProcess, this);
|
||||
} catch (Exception e) {
|
||||
LOGGER.error(e.getMessage(), e);
|
||||
}
|
||||
}
|
||||
|
||||
private ArbitraryDataFile fetchArbitraryDataFile(Peer peer, ArbitraryTransactionData arbitraryTransactionData, byte[] signature, byte[] hash) throws DataException {
|
||||
private ArbitraryDataFile fetchArbitraryDataFile(Peer peer, Peer requestingPeer, ArbitraryTransactionData arbitraryTransactionData, byte[] signature, byte[] hash, Message originalMessage) throws DataException {
|
||||
ArbitraryDataFile existingFile = ArbitraryDataFile.fromHash(hash, signature);
|
||||
boolean fileAlreadyExists = existingFile.exists();
|
||||
String hash58 = Base58.encode(hash);
|
||||
ArbitraryDataFile arbitraryDataFile;
|
||||
|
||||
try {
|
||||
ArbitraryDataFile existingFile = ArbitraryDataFile.fromHash(hash, signature);
|
||||
boolean fileAlreadyExists = existingFile.exists();
|
||||
String hash58 = Base58.encode(hash);
|
||||
|
||||
// Fetch the file if it doesn't exist locally
|
||||
if (!fileAlreadyExists) {
|
||||
LOGGER.debug(String.format("Fetching data file %.8s from peer %s", hash58, peer));
|
||||
arbitraryDataFileRequests.put(hash58, NTP.getTime());
|
||||
Message getArbitraryDataFileMessage = new GetArbitraryDataFileMessage(signature, hash);
|
||||
|
||||
Message response = null;
|
||||
try {
|
||||
response = peer.getResponseWithTimeout(getArbitraryDataFileMessage, (int) ArbitraryDataManager.ARBITRARY_REQUEST_TIMEOUT);
|
||||
} catch (InterruptedException e) {
|
||||
// Will return below due to null response
|
||||
}
|
||||
arbitraryDataFileRequests.remove(hash58);
|
||||
LOGGER.trace(String.format("Removed hash %.8s from arbitraryDataFileRequests", hash58));
|
||||
|
||||
if (response == null) {
|
||||
LOGGER.debug("Received null response from peer {}", peer);
|
||||
return null;
|
||||
}
|
||||
if (response.getType() != MessageType.ARBITRARY_DATA_FILE) {
|
||||
LOGGER.debug("Received response with invalid type: {} from peer {}", response.getType(), peer);
|
||||
return null;
|
||||
}
|
||||
|
||||
ArbitraryDataFileMessage peersArbitraryDataFileMessage = (ArbitraryDataFileMessage) response;
|
||||
arbitraryDataFile = peersArbitraryDataFileMessage.getArbitraryDataFile();
|
||||
} else {
|
||||
LOGGER.debug(String.format("File hash %s already exists, so skipping the request", hash58));
|
||||
arbitraryDataFile = existingFile;
|
||||
}
|
||||
|
||||
if (arbitraryDataFile != null) {
|
||||
|
||||
arbitraryDataFile.save();
|
||||
|
||||
// If this is a metadata file then we need to update the cache
|
||||
if (arbitraryTransactionData != null && arbitraryTransactionData.getMetadataHash() != null) {
|
||||
if (Arrays.equals(arbitraryTransactionData.getMetadataHash(), hash)) {
|
||||
ArbitraryDataCacheManager.getInstance().addToUpdateQueue(arbitraryTransactionData);
|
||||
}
|
||||
}
|
||||
|
||||
// We may need to remove the file list request, if we have all the files for this transaction
|
||||
this.handleFileListRequests(signature);
|
||||
}
|
||||
} catch (DataException e) {
|
||||
LOGGER.error(e.getMessage(), e);
|
||||
arbitraryDataFile = null;
|
||||
}
|
||||
|
||||
return arbitraryDataFile;
|
||||
}
|
||||
|
||||
private void fetchFileForRelay(Peer peer, Peer requestingPeer, byte[] signature, byte[] hash, Message originalMessage) throws DataException {
|
||||
try {
|
||||
String hash58 = Base58.encode(hash);
|
||||
|
||||
// Fetch the file if it doesn't exist locally
|
||||
if (!fileAlreadyExists) {
|
||||
LOGGER.debug(String.format("Fetching data file %.8s from peer %s", hash58, peer));
|
||||
arbitraryDataFileRequests.put(hash58, NTP.getTime());
|
||||
Message getArbitraryDataFileMessage = new GetArbitraryDataFileMessage(signature, hash);
|
||||
@ -291,73 +212,73 @@ public class ArbitraryDataFileManager extends Thread {
|
||||
arbitraryDataFileRequests.remove(hash58);
|
||||
LOGGER.trace(String.format("Removed hash %.8s from arbitraryDataFileRequests", hash58));
|
||||
|
||||
// We may need to remove the file list request, if we have all the files for this transaction
|
||||
this.handleFileListRequests(signature);
|
||||
|
||||
if (response == null) {
|
||||
LOGGER.debug("Received null response from peer {}", peer);
|
||||
return;
|
||||
return null;
|
||||
}
|
||||
if (response.getType() != MessageType.ARBITRARY_DATA_FILE) {
|
||||
LOGGER.debug("Received response with invalid type: {} from peer {}", response.getType(), peer);
|
||||
return;
|
||||
return null;
|
||||
}
|
||||
|
||||
ArbitraryDataFileMessage peersArbitraryDataFileMessage = (ArbitraryDataFileMessage) response;
|
||||
ArbitraryDataFile arbitraryDataFile = peersArbitraryDataFileMessage.getArbitraryDataFile();
|
||||
|
||||
if (arbitraryDataFile != null) {
|
||||
|
||||
// We might want to forward the request to the peer that originally requested it
|
||||
this.handleArbitraryDataFileForwarding(requestingPeer, new ArbitraryDataFileMessage(signature, arbitraryDataFile), originalMessage);
|
||||
}
|
||||
} catch (Exception e) {
|
||||
LOGGER.error(e.getMessage(), e);
|
||||
arbitraryDataFile = peersArbitraryDataFileMessage.getArbitraryDataFile();
|
||||
} else {
|
||||
LOGGER.debug(String.format("File hash %s already exists, so skipping the request", hash58));
|
||||
arbitraryDataFile = existingFile;
|
||||
}
|
||||
|
||||
if (arbitraryDataFile == null) {
|
||||
// We don't have a file, so give up here
|
||||
return null;
|
||||
}
|
||||
|
||||
// We might want to forward the request to the peer that originally requested it
|
||||
this.handleArbitraryDataFileForwarding(requestingPeer, new ArbitraryDataFileMessage(signature, arbitraryDataFile), originalMessage);
|
||||
|
||||
boolean isRelayRequest = (requestingPeer != null);
|
||||
if (isRelayRequest) {
|
||||
if (!fileAlreadyExists) {
|
||||
// File didn't exist locally before the request, and it's a forwarding request, so delete it if it exists.
|
||||
// It shouldn't exist on the filesystem yet, but leaving this here just in case.
|
||||
arbitraryDataFile.delete(10);
|
||||
}
|
||||
}
|
||||
else {
|
||||
arbitraryDataFile.save();
|
||||
}
|
||||
|
||||
// If this is a metadata file then we need to update the cache
|
||||
if (arbitraryTransactionData != null && arbitraryTransactionData.getMetadataHash() != null) {
|
||||
if (Arrays.equals(arbitraryTransactionData.getMetadataHash(), hash)) {
|
||||
ArbitraryDataCacheManager.getInstance().addToUpdateQueue(arbitraryTransactionData);
|
||||
}
|
||||
}
|
||||
|
||||
return arbitraryDataFile;
|
||||
}
|
||||
|
||||
Map<String, byte[]> signatureBySignature58 = new HashMap<>();
|
||||
|
||||
// Lock to synchronize access to the list
|
||||
private final Object handleFileListRequestsLock = new Object();
|
||||
|
||||
// Scheduled executor service to process messages every second
|
||||
private final ScheduledExecutorService handleFileListRequestsScheduler = Executors.newScheduledThreadPool(1);
|
||||
|
||||
private void handleFileListRequests(byte[] signature) {
|
||||
|
||||
synchronized (handleFileListRequestsLock) {
|
||||
signatureBySignature58.put(Base58.encode(signature), signature);
|
||||
}
|
||||
}
|
||||
|
||||
private void handleFileListRequestProcess() {
|
||||
|
||||
Map<String, byte[]> signaturesToProcess;
|
||||
|
||||
synchronized (handleFileListRequestsLock) {
|
||||
signaturesToProcess = new HashMap<>(signatureBySignature58);
|
||||
signatureBySignature58.clear();
|
||||
}
|
||||
|
||||
if( signaturesToProcess.isEmpty() ) return;
|
||||
|
||||
try (final Repository repository = RepositoryManager.getRepository()) {
|
||||
|
||||
// Fetch the transaction data
|
||||
List<ArbitraryTransactionData> arbitraryTransactionDataList
|
||||
= ArbitraryTransactionUtils.fetchTransactionDataList(repository, new ArrayList<>(signaturesToProcess.values()));
|
||||
|
||||
for( ArbitraryTransactionData arbitraryTransactionData : arbitraryTransactionDataList ) {
|
||||
boolean completeFileExists = ArbitraryTransactionUtils.completeFileExists(arbitraryTransactionData);
|
||||
|
||||
if (completeFileExists) {
|
||||
String signature58 = Base58.encode(arbitraryTransactionData.getSignature());
|
||||
LOGGER.debug("All chunks or complete file exist for transaction {}", signature58);
|
||||
|
||||
ArbitraryDataFileListManager.getInstance().deleteFileListRequestsForSignature(signature58);
|
||||
}
|
||||
ArbitraryTransactionData arbitraryTransactionData = ArbitraryTransactionUtils.fetchTransactionData(repository, signature);
|
||||
if (arbitraryTransactionData == null) {
|
||||
return;
|
||||
}
|
||||
|
||||
} catch (Exception e) {
|
||||
LOGGER.error(e.getMessage(), e);
|
||||
boolean allChunksExist = ArbitraryTransactionUtils.allChunksExist(arbitraryTransactionData);
|
||||
|
||||
if (allChunksExist) {
|
||||
// Update requests map to reflect that we've received all chunks
|
||||
ArbitraryDataFileListManager.getInstance().deleteFileListRequestsForSignature(signature);
|
||||
}
|
||||
|
||||
} catch (DataException e) {
|
||||
LOGGER.debug("Unable to handle file list requests: {}", e.getMessage());
|
||||
}
|
||||
}
|
||||
|
||||
@ -374,14 +295,15 @@ public class ArbitraryDataFileManager extends Thread {
|
||||
|
||||
LOGGER.debug("Received arbitrary data file - forwarding is needed");
|
||||
|
||||
try {
|
||||
// The ID needs to match that of the original request
|
||||
message.setId(originalMessage.getId());
|
||||
// The ID needs to match that of the original request
|
||||
message.setId(originalMessage.getId());
|
||||
|
||||
PeerSendManagement.getInstance().getOrCreateSendManager(requestingPeer).queueMessage(message, SEND_TIMEOUT_MS);
|
||||
|
||||
} catch (Exception e) {
|
||||
LOGGER.error(e.getMessage(), e);
|
||||
if (!requestingPeer.sendMessageWithTimeout(message, (int) ArbitraryDataManager.ARBITRARY_REQUEST_TIMEOUT)) {
|
||||
LOGGER.debug("Failed to forward arbitrary data file to peer {}", requestingPeer);
|
||||
requestingPeer.disconnect("failed to forward arbitrary data file");
|
||||
}
|
||||
else {
|
||||
LOGGER.debug("Forwarded arbitrary data file to peer {}", requestingPeer);
|
||||
}
|
||||
}
|
||||
|
||||
@ -655,9 +577,13 @@ public class ArbitraryDataFileManager extends Thread {
|
||||
LOGGER.debug("Sending file {}...", arbitraryDataFile);
|
||||
ArbitraryDataFileMessage arbitraryDataFileMessage = new ArbitraryDataFileMessage(signature, arbitraryDataFile);
|
||||
arbitraryDataFileMessage.setId(message.getId());
|
||||
|
||||
PeerSendManagement.getInstance().getOrCreateSendManager(peer).queueMessage(arbitraryDataFileMessage, SEND_TIMEOUT_MS);
|
||||
|
||||
if (!peer.sendMessageWithTimeout(arbitraryDataFileMessage, (int) ArbitraryDataManager.ARBITRARY_REQUEST_TIMEOUT)) {
|
||||
LOGGER.debug("Couldn't send file {}", arbitraryDataFile);
|
||||
peer.disconnect("failed to send file");
|
||||
}
|
||||
else {
|
||||
LOGGER.debug("Sent file {}", arbitraryDataFile);
|
||||
}
|
||||
}
|
||||
else if (relayInfo != null) {
|
||||
LOGGER.debug("We have relay info for hash {}", Base58.encode(hash));
|
||||
@ -669,7 +595,7 @@ public class ArbitraryDataFileManager extends Thread {
|
||||
LOGGER.debug("Asking peer {} for hash {}", peerToAsk, hash58);
|
||||
// No need to pass arbitraryTransactionData below because this is only used for metadata caching,
|
||||
// and metadata isn't retained when relaying.
|
||||
this.fetchFileForRelay(peerToAsk, peer, signature, hash, message);
|
||||
this.fetchArbitraryDataFile(peerToAsk, peer, null, signature, hash, message);
|
||||
}
|
||||
else {
|
||||
LOGGER.debug("Peer {} not found in relay info", peer);
|
||||
@ -691,6 +617,7 @@ public class ArbitraryDataFileManager extends Thread {
|
||||
fileUnknownMessage.setId(message.getId());
|
||||
if (!peer.sendMessage(fileUnknownMessage)) {
|
||||
LOGGER.debug("Couldn't sent file-unknown response");
|
||||
peer.disconnect("failed to send file-unknown response");
|
||||
}
|
||||
else {
|
||||
LOGGER.debug("Sent file-unknown response for file {}", arbitraryDataFile);
|
||||
|
@ -4,186 +4,125 @@ import org.apache.logging.log4j.LogManager;
|
||||
import org.apache.logging.log4j.Logger;
|
||||
import org.qortal.controller.Controller;
|
||||
import org.qortal.data.arbitrary.ArbitraryFileListResponseInfo;
|
||||
import org.qortal.data.arbitrary.ArbitraryResourceData;
|
||||
import org.qortal.data.transaction.ArbitraryTransactionData;
|
||||
import org.qortal.network.Peer;
|
||||
import org.qortal.network.message.MessageType;
|
||||
import org.qortal.repository.DataException;
|
||||
import org.qortal.repository.Repository;
|
||||
import org.qortal.repository.RepositoryManager;
|
||||
import org.qortal.settings.Settings;
|
||||
import org.qortal.utils.ArbitraryTransactionUtils;
|
||||
import org.qortal.utils.Base58;
|
||||
import org.qortal.utils.NTP;
|
||||
import org.qortal.utils.NamedThreadFactory;
|
||||
|
||||
import java.net.http.HttpResponse;
|
||||
import java.util.ArrayList;
|
||||
import java.util.Arrays;
|
||||
import java.util.Comparator;
|
||||
import java.util.HashMap;
|
||||
import java.util.Iterator;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.concurrent.ConcurrentHashMap;
|
||||
import java.util.concurrent.Executor;
|
||||
import java.util.concurrent.ExecutorService;
|
||||
import java.util.concurrent.Executors;
|
||||
import java.util.concurrent.ScheduledExecutorService;
|
||||
import java.util.concurrent.ThreadPoolExecutor;
|
||||
import java.util.concurrent.TimeUnit;
|
||||
|
||||
import static java.lang.Thread.NORM_PRIORITY;
|
||||
|
||||
public class ArbitraryDataFileRequestThread {
|
||||
public class ArbitraryDataFileRequestThread implements Runnable {
|
||||
|
||||
private static final Logger LOGGER = LogManager.getLogger(ArbitraryDataFileRequestThread.class);
|
||||
|
||||
private static final Integer FETCHER_LIMIT_PER_PEER = Settings.getInstance().getMaxThreadsForMessageType(MessageType.GET_ARBITRARY_DATA_FILE);
|
||||
private static final String FETCHER_THREAD_PREFIX = "Arbitrary Data Fetcher ";
|
||||
public ArbitraryDataFileRequestThread() {
|
||||
|
||||
private ConcurrentHashMap<String, ExecutorService> executorByPeer = new ConcurrentHashMap<>();
|
||||
|
||||
private ArbitraryDataFileRequestThread() {
|
||||
cleanupExecutorByPeerScheduler.scheduleAtFixedRate(this::cleanupExecutorsByPeer, 1, 1, TimeUnit.MINUTES);
|
||||
}
|
||||
|
||||
private static ArbitraryDataFileRequestThread instance = null;
|
||||
|
||||
public static ArbitraryDataFileRequestThread getInstance() {
|
||||
|
||||
if( instance == null ) {
|
||||
instance = new ArbitraryDataFileRequestThread();
|
||||
}
|
||||
|
||||
return instance;
|
||||
}
|
||||
|
||||
private final ScheduledExecutorService cleanupExecutorByPeerScheduler = Executors.newScheduledThreadPool(1);
|
||||
|
||||
private void cleanupExecutorsByPeer() {
|
||||
@Override
|
||||
public void run() {
|
||||
Thread.currentThread().setName("Arbitrary Data File Request Thread");
|
||||
Thread.currentThread().setPriority(NORM_PRIORITY);
|
||||
|
||||
try {
|
||||
this.executorByPeer.forEach((key, value) -> {
|
||||
if (value instanceof ThreadPoolExecutor) {
|
||||
ThreadPoolExecutor threadPoolExecutor = (ThreadPoolExecutor) value;
|
||||
if (threadPoolExecutor.getActiveCount() == 0) {
|
||||
threadPoolExecutor.shutdown();
|
||||
if (this.executorByPeer.computeIfPresent(key, (k, v) -> null) == null) {
|
||||
LOGGER.trace("removed executor: peer = " + key);
|
||||
}
|
||||
}
|
||||
} else {
|
||||
LOGGER.warn("casting issue in cleanup");
|
||||
}
|
||||
});
|
||||
} catch (Exception e) {
|
||||
LOGGER.error(e.getMessage(), e);
|
||||
while (!Controller.isStopping()) {
|
||||
Long now = NTP.getTime();
|
||||
this.processFileHashes(now);
|
||||
}
|
||||
} catch (InterruptedException e) {
|
||||
// Fall-through to exit thread...
|
||||
}
|
||||
}
|
||||
|
||||
public void processFileHashes(Long now, List<ArbitraryFileListResponseInfo> responseInfos, ArbitraryDataFileManager arbitraryDataFileManager) {
|
||||
private void processFileHashes(Long now) throws InterruptedException {
|
||||
if (Controller.isStopping()) {
|
||||
return;
|
||||
}
|
||||
|
||||
Map<String, byte[]> signatureBySignature58 = new HashMap<>(responseInfos.size());
|
||||
Map<String, List<ArbitraryFileListResponseInfo>> responseInfoBySignature58 = new HashMap<>();
|
||||
ArbitraryDataFileManager arbitraryDataFileManager = ArbitraryDataFileManager.getInstance();
|
||||
String signature58 = null;
|
||||
String hash58 = null;
|
||||
Peer peer = null;
|
||||
boolean shouldProcess = false;
|
||||
|
||||
for( ArbitraryFileListResponseInfo responseInfo : responseInfos) {
|
||||
synchronized (arbitraryDataFileManager.arbitraryDataFileHashResponses) {
|
||||
if (!arbitraryDataFileManager.arbitraryDataFileHashResponses.isEmpty()) {
|
||||
|
||||
if( responseInfo == null ) continue;
|
||||
// Sort by lowest number of node hops first
|
||||
Comparator<ArbitraryFileListResponseInfo> lowestHopsFirstComparator =
|
||||
Comparator.comparingInt(ArbitraryFileListResponseInfo::getRequestHops);
|
||||
arbitraryDataFileManager.arbitraryDataFileHashResponses.sort(lowestHopsFirstComparator);
|
||||
|
||||
if (Controller.isStopping()) {
|
||||
return;
|
||||
Iterator iterator = arbitraryDataFileManager.arbitraryDataFileHashResponses.iterator();
|
||||
while (iterator.hasNext()) {
|
||||
if (Controller.isStopping()) {
|
||||
return;
|
||||
}
|
||||
|
||||
ArbitraryFileListResponseInfo responseInfo = (ArbitraryFileListResponseInfo) iterator.next();
|
||||
if (responseInfo == null) {
|
||||
iterator.remove();
|
||||
continue;
|
||||
}
|
||||
|
||||
hash58 = responseInfo.getHash58();
|
||||
peer = responseInfo.getPeer();
|
||||
signature58 = responseInfo.getSignature58();
|
||||
Long timestamp = responseInfo.getTimestamp();
|
||||
|
||||
if (now - timestamp >= ArbitraryDataManager.ARBITRARY_RELAY_TIMEOUT || signature58 == null || peer == null) {
|
||||
// Ignore - to be deleted
|
||||
iterator.remove();
|
||||
continue;
|
||||
}
|
||||
|
||||
// Skip if already requesting, but don't remove, as we might want to retry later
|
||||
if (arbitraryDataFileManager.arbitraryDataFileRequests.containsKey(hash58)) {
|
||||
// Already requesting - leave this attempt for later
|
||||
continue;
|
||||
}
|
||||
|
||||
// We want to process this file
|
||||
shouldProcess = true;
|
||||
iterator.remove();
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
Peer peer = responseInfo.getPeer();
|
||||
|
||||
// if relay timeout, then move on
|
||||
if (now - responseInfo.getTimestamp() >= ArbitraryDataManager.ARBITRARY_RELAY_TIMEOUT || responseInfo.getSignature58() == null || peer == null) {
|
||||
continue;
|
||||
}
|
||||
|
||||
// Skip if already requesting, but don't remove, as we might want to retry later
|
||||
if (arbitraryDataFileManager.arbitraryDataFileRequests.containsKey(responseInfo.getHash58())) {
|
||||
// Already requesting - leave this attempt for later
|
||||
arbitraryDataFileManager.addResponse(responseInfo); // don't remove -> adding back, beacause it was removed already above
|
||||
continue;
|
||||
}
|
||||
|
||||
|
||||
byte[] hash = Base58.decode(responseInfo.getHash58());
|
||||
byte[] signature = Base58.decode(responseInfo.getSignature58());
|
||||
|
||||
// check for null
|
||||
if (signature == null || hash == null || peer == null) {
|
||||
continue;
|
||||
}
|
||||
|
||||
// We want to process this file, store and map data to process later
|
||||
signatureBySignature58.put(responseInfo.getSignature58(), signature);
|
||||
responseInfoBySignature58
|
||||
.computeIfAbsent(responseInfo.getSignature58(), signature58 -> new ArrayList<>())
|
||||
.add(responseInfo);
|
||||
}
|
||||
|
||||
// if there are no signatures, then there is nothing to process and nothing query the database
|
||||
if( signatureBySignature58.isEmpty() ) return;
|
||||
if (!shouldProcess) {
|
||||
// Nothing to do
|
||||
Thread.sleep(1000L);
|
||||
return;
|
||||
}
|
||||
|
||||
List<ArbitraryTransactionData> arbitraryTransactionDataList = new ArrayList<>();
|
||||
byte[] hash = Base58.decode(hash58);
|
||||
byte[] signature = Base58.decode(signature58);
|
||||
|
||||
// Fetch the transaction data
|
||||
try (final Repository repository = RepositoryManager.getRepository()) {
|
||||
arbitraryTransactionDataList.addAll(
|
||||
ArbitraryTransactionUtils.fetchTransactionDataList(repository, new ArrayList<>(signatureBySignature58.values())));
|
||||
} catch (DataException e) {
|
||||
LOGGER.warn("Unable to fetch transaction data: {}", e.getMessage());
|
||||
}
|
||||
|
||||
if( !arbitraryTransactionDataList.isEmpty() ) {
|
||||
long start = System.currentTimeMillis();
|
||||
|
||||
for(ArbitraryTransactionData data : arbitraryTransactionDataList ) {
|
||||
String signature58 = Base58.encode(data.getSignature());
|
||||
for( ArbitraryFileListResponseInfo responseInfo : responseInfoBySignature58.get(signature58)) {
|
||||
Runnable fetcher = () -> arbitraryDataFileFetcher(arbitraryDataFileManager, responseInfo, data);
|
||||
this.executorByPeer
|
||||
.computeIfAbsent(
|
||||
responseInfo.getPeer().toString(),
|
||||
peer -> Executors.newFixedThreadPool(
|
||||
FETCHER_LIMIT_PER_PEER,
|
||||
new NamedThreadFactory(FETCHER_THREAD_PREFIX + responseInfo.getPeer().toString(), NORM_PRIORITY)
|
||||
)
|
||||
)
|
||||
.execute(fetcher);
|
||||
}
|
||||
}
|
||||
long timeLapse = System.currentTimeMillis() - start;
|
||||
}
|
||||
}
|
||||
|
||||
private void arbitraryDataFileFetcher(ArbitraryDataFileManager arbitraryDataFileManager, ArbitraryFileListResponseInfo responseInfo, ArbitraryTransactionData arbitraryTransactionData) {
|
||||
try {
|
||||
Long now = NTP.getTime();
|
||||
|
||||
if (now - responseInfo.getTimestamp() >= ArbitraryDataManager.ARBITRARY_RELAY_TIMEOUT ) {
|
||||
|
||||
Peer peer = responseInfo.getPeer();
|
||||
String hash58 = responseInfo.getHash58();
|
||||
String signature58 = responseInfo.getSignature58();
|
||||
LOGGER.debug("Peer {} version {} didn't fetch data file {} for signature {} due to relay timeout.", peer, peer.getPeersVersionString(), hash58, signature58);
|
||||
ArbitraryTransactionData arbitraryTransactionData = ArbitraryTransactionUtils.fetchTransactionData(repository, signature);
|
||||
if (arbitraryTransactionData == null) {
|
||||
return;
|
||||
}
|
||||
|
||||
arbitraryDataFileManager.fetchArbitraryDataFiles(
|
||||
responseInfo.getPeer(),
|
||||
arbitraryTransactionData.getSignature(),
|
||||
arbitraryTransactionData,
|
||||
Arrays.asList(Base58.decode(responseInfo.getHash58()))
|
||||
);
|
||||
if (signature == null || hash == null || peer == null || arbitraryTransactionData == null) {
|
||||
return;
|
||||
}
|
||||
|
||||
LOGGER.trace("Fetching file {} from peer {} via request thread...", hash58, peer);
|
||||
arbitraryDataFileManager.fetchArbitraryDataFiles(repository, peer, signature, arbitraryTransactionData, Arrays.asList(hash));
|
||||
|
||||
} catch (DataException e) {
|
||||
LOGGER.warn("Unable to process file hashes: {}", e.getMessage());
|
||||
LOGGER.debug("Unable to process file hashes: {}", e.getMessage());
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -10,8 +10,6 @@ import org.qortal.arbitrary.misc.Service;
|
||||
import org.qortal.controller.Controller;
|
||||
import org.qortal.data.transaction.ArbitraryTransactionData;
|
||||
import org.qortal.data.transaction.TransactionData;
|
||||
import org.qortal.event.DataMonitorEvent;
|
||||
import org.qortal.event.EventBus;
|
||||
import org.qortal.network.Network;
|
||||
import org.qortal.network.Peer;
|
||||
import org.qortal.repository.DataException;
|
||||
@ -30,7 +28,6 @@ import java.nio.file.Files;
|
||||
import java.nio.file.Path;
|
||||
import java.nio.file.Paths;
|
||||
import java.util.*;
|
||||
import java.util.stream.Collectors;
|
||||
|
||||
public class ArbitraryDataManager extends Thread {
|
||||
|
||||
@ -42,10 +39,10 @@ public class ArbitraryDataManager extends Thread {
|
||||
private int powDifficulty = 14; // Must not be final, as unit tests need to reduce this value
|
||||
|
||||
/** Request timeout when transferring arbitrary data */
|
||||
public static final long ARBITRARY_REQUEST_TIMEOUT = 24 * 1000L; // ms
|
||||
public static final long ARBITRARY_REQUEST_TIMEOUT = 12 * 1000L; // ms
|
||||
|
||||
/** Maximum time to hold information about an in-progress relay */
|
||||
public static final long ARBITRARY_RELAY_TIMEOUT = 120 * 1000L; // ms
|
||||
public static final long ARBITRARY_RELAY_TIMEOUT = 60 * 1000L; // ms
|
||||
|
||||
/** Maximum time to hold direct peer connection information */
|
||||
public static final long ARBITRARY_DIRECT_CONNECTION_INFO_TIMEOUT = 2 * 60 * 1000L; // ms
|
||||
@ -198,35 +195,13 @@ public class ArbitraryDataManager extends Thread {
|
||||
final int limit = 100;
|
||||
int offset = 0;
|
||||
|
||||
List<ArbitraryTransactionData> allArbitraryTransactionsInDescendingOrder;
|
||||
|
||||
try (final Repository repository = RepositoryManager.getRepository()) {
|
||||
|
||||
if( name == null ) {
|
||||
allArbitraryTransactionsInDescendingOrder
|
||||
= repository.getArbitraryRepository()
|
||||
.getLatestArbitraryTransactions();
|
||||
}
|
||||
else {
|
||||
allArbitraryTransactionsInDescendingOrder
|
||||
= repository.getArbitraryRepository()
|
||||
.getLatestArbitraryTransactionsByName(name);
|
||||
}
|
||||
} catch( Exception e) {
|
||||
LOGGER.error(e.getMessage(), e);
|
||||
allArbitraryTransactionsInDescendingOrder = new ArrayList<>(0);
|
||||
}
|
||||
|
||||
// collect processed transactions in a set to ensure outdated data transactions do not get fetched
|
||||
Set<ArbitraryTransactionDataHashWrapper> processedTransactions = new HashSet<>();
|
||||
|
||||
while (!isStopping) {
|
||||
Thread.sleep(1000L);
|
||||
|
||||
// Any arbitrary transactions we want to fetch data for?
|
||||
try (final Repository repository = RepositoryManager.getRepository()) {
|
||||
List<byte[]> signatures = processTransactionsForSignatures(limit, offset, allArbitraryTransactionsInDescendingOrder, processedTransactions);
|
||||
|
||||
List<byte[]> signatures = repository.getTransactionRepository().getSignaturesMatchingCriteria(null, null, null, ARBITRARY_TX_TYPE, null, name, null, ConfirmationStatus.BOTH, limit, offset, true);
|
||||
// LOGGER.trace("Found {} arbitrary transactions at offset: {}, limit: {}", signatures.size(), offset, limit);
|
||||
if (signatures == null || signatures.isEmpty()) {
|
||||
offset = 0;
|
||||
break;
|
||||
@ -248,38 +223,14 @@ public class ArbitraryDataManager extends Thread {
|
||||
ArbitraryTransactionData arbitraryTransactionData = (ArbitraryTransactionData) arbitraryTransaction.getTransactionData();
|
||||
|
||||
// Skip transactions that we don't need to proactively store data for
|
||||
ArbitraryDataExamination arbitraryDataExamination = storageManager.shouldPreFetchData(repository, arbitraryTransactionData);
|
||||
if (!arbitraryDataExamination.isPass()) {
|
||||
if (!storageManager.shouldPreFetchData(repository, arbitraryTransactionData)) {
|
||||
iterator.remove();
|
||||
|
||||
EventBus.INSTANCE.notify(
|
||||
new DataMonitorEvent(
|
||||
System.currentTimeMillis(),
|
||||
arbitraryTransactionData.getIdentifier(),
|
||||
arbitraryTransactionData.getName(),
|
||||
arbitraryTransactionData.getService().name(),
|
||||
arbitraryDataExamination.getNotes(),
|
||||
arbitraryTransactionData.getTimestamp(),
|
||||
arbitraryTransactionData.getTimestamp()
|
||||
)
|
||||
);
|
||||
continue;
|
||||
}
|
||||
|
||||
// Remove transactions that we already have local data for
|
||||
if (hasLocalData(arbitraryTransaction)) {
|
||||
iterator.remove();
|
||||
EventBus.INSTANCE.notify(
|
||||
new DataMonitorEvent(
|
||||
System.currentTimeMillis(),
|
||||
arbitraryTransactionData.getIdentifier(),
|
||||
arbitraryTransactionData.getName(),
|
||||
arbitraryTransactionData.getService().name(),
|
||||
"already have local data, skipping",
|
||||
arbitraryTransactionData.getTimestamp(),
|
||||
arbitraryTransactionData.getTimestamp()
|
||||
)
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@ -297,21 +248,8 @@ public class ArbitraryDataManager extends Thread {
|
||||
|
||||
// Check to see if we have had a more recent PUT
|
||||
ArbitraryTransactionData arbitraryTransactionData = ArbitraryTransactionUtils.fetchTransactionData(repository, signature);
|
||||
|
||||
Optional<ArbitraryTransactionData> moreRecentPutTransaction = ArbitraryTransactionUtils.hasMoreRecentPutTransaction(repository, arbitraryTransactionData);
|
||||
|
||||
if (moreRecentPutTransaction.isPresent()) {
|
||||
EventBus.INSTANCE.notify(
|
||||
new DataMonitorEvent(
|
||||
System.currentTimeMillis(),
|
||||
arbitraryTransactionData.getIdentifier(),
|
||||
arbitraryTransactionData.getName(),
|
||||
arbitraryTransactionData.getService().name(),
|
||||
"not fetching old data",
|
||||
arbitraryTransactionData.getTimestamp(),
|
||||
moreRecentPutTransaction.get().getTimestamp()
|
||||
)
|
||||
);
|
||||
boolean hasMoreRecentPutTransaction = ArbitraryTransactionUtils.hasMoreRecentPutTransaction(repository, arbitraryTransactionData);
|
||||
if (hasMoreRecentPutTransaction) {
|
||||
// There is a more recent PUT transaction than the one we are currently processing.
|
||||
// When a PUT is issued, it replaces any layers that would have been there before.
|
||||
// Therefore any data relating to this older transaction is no longer needed and we
|
||||
@ -319,34 +257,10 @@ public class ArbitraryDataManager extends Thread {
|
||||
continue;
|
||||
}
|
||||
|
||||
EventBus.INSTANCE.notify(
|
||||
new DataMonitorEvent(
|
||||
System.currentTimeMillis(),
|
||||
arbitraryTransactionData.getIdentifier(),
|
||||
arbitraryTransactionData.getName(),
|
||||
arbitraryTransactionData.getService().name(),
|
||||
"fetching data",
|
||||
arbitraryTransactionData.getTimestamp(),
|
||||
arbitraryTransactionData.getTimestamp()
|
||||
)
|
||||
);
|
||||
|
||||
// Ask our connected peers if they have files for this signature
|
||||
// This process automatically then fetches the files themselves if a peer is found
|
||||
fetchData(arbitraryTransactionData);
|
||||
|
||||
EventBus.INSTANCE.notify(
|
||||
new DataMonitorEvent(
|
||||
System.currentTimeMillis(),
|
||||
arbitraryTransactionData.getIdentifier(),
|
||||
arbitraryTransactionData.getName(),
|
||||
arbitraryTransactionData.getService().name(),
|
||||
"fetched data",
|
||||
arbitraryTransactionData.getTimestamp(),
|
||||
arbitraryTransactionData.getTimestamp()
|
||||
)
|
||||
);
|
||||
|
||||
} catch (DataException e) {
|
||||
LOGGER.error("Repository issue when fetching arbitrary transaction data", e);
|
||||
}
|
||||
@ -360,20 +274,6 @@ public class ArbitraryDataManager extends Thread {
|
||||
final int limit = 100;
|
||||
int offset = 0;
|
||||
|
||||
List<ArbitraryTransactionData> allArbitraryTransactionsInDescendingOrder;
|
||||
|
||||
try (final Repository repository = RepositoryManager.getRepository()) {
|
||||
allArbitraryTransactionsInDescendingOrder
|
||||
= repository.getArbitraryRepository()
|
||||
.getLatestArbitraryTransactions();
|
||||
} catch( Exception e) {
|
||||
LOGGER.error(e.getMessage(), e);
|
||||
allArbitraryTransactionsInDescendingOrder = new ArrayList<>(0);
|
||||
}
|
||||
|
||||
// collect processed transactions in a set to ensure outdated data transactions do not get fetched
|
||||
Set<ArbitraryTransactionDataHashWrapper> processedTransactions = new HashSet<>();
|
||||
|
||||
while (!isStopping) {
|
||||
final int minSeconds = 3;
|
||||
final int maxSeconds = 10;
|
||||
@ -382,8 +282,8 @@ public class ArbitraryDataManager extends Thread {
|
||||
|
||||
// Any arbitrary transactions we want to fetch data for?
|
||||
try (final Repository repository = RepositoryManager.getRepository()) {
|
||||
List<byte[]> signatures = processTransactionsForSignatures(limit, offset, allArbitraryTransactionsInDescendingOrder, processedTransactions);
|
||||
|
||||
List<byte[]> signatures = repository.getTransactionRepository().getSignaturesMatchingCriteria(null, null, null, ARBITRARY_TX_TYPE, null, null, null, ConfirmationStatus.BOTH, limit, offset, true);
|
||||
// LOGGER.trace("Found {} arbitrary transactions at offset: {}, limit: {}", signatures.size(), offset, limit);
|
||||
if (signatures == null || signatures.isEmpty()) {
|
||||
offset = 0;
|
||||
break;
|
||||
@ -428,74 +328,26 @@ public class ArbitraryDataManager extends Thread {
|
||||
continue;
|
||||
}
|
||||
|
||||
// No longer need to see if we have had a more recent PUT since we compared the transactions to process
|
||||
// to the transactions previously processed, so we can fetch the transactiondata, notify the event bus,
|
||||
// fetch the metadata and notify the event bus again
|
||||
// Check to see if we have had a more recent PUT
|
||||
ArbitraryTransactionData arbitraryTransactionData = ArbitraryTransactionUtils.fetchTransactionData(repository, signature);
|
||||
boolean hasMoreRecentPutTransaction = ArbitraryTransactionUtils.hasMoreRecentPutTransaction(repository, arbitraryTransactionData);
|
||||
if (hasMoreRecentPutTransaction) {
|
||||
// There is a more recent PUT transaction than the one we are currently processing.
|
||||
// When a PUT is issued, it replaces any layers that would have been there before.
|
||||
// Therefore any data relating to this older transaction is no longer needed and we
|
||||
// shouldn't fetch it from the network.
|
||||
continue;
|
||||
}
|
||||
|
||||
// Ask our connected peers if they have metadata for this signature
|
||||
fetchMetadata(arbitraryTransactionData);
|
||||
|
||||
EventBus.INSTANCE.notify(
|
||||
new DataMonitorEvent(
|
||||
System.currentTimeMillis(),
|
||||
arbitraryTransactionData.getIdentifier(),
|
||||
arbitraryTransactionData.getName(),
|
||||
arbitraryTransactionData.getService().name(),
|
||||
"fetched metadata",
|
||||
arbitraryTransactionData.getTimestamp(),
|
||||
arbitraryTransactionData.getTimestamp()
|
||||
)
|
||||
);
|
||||
} catch (DataException e) {
|
||||
LOGGER.error("Repository issue when fetching arbitrary transaction data", e);
|
||||
} catch (Exception e) {
|
||||
LOGGER.error(e.getMessage(), e);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private static List<byte[]> processTransactionsForSignatures(
|
||||
int limit,
|
||||
int offset,
|
||||
List<ArbitraryTransactionData> transactionsInDescendingOrder,
|
||||
Set<ArbitraryTransactionDataHashWrapper> processedTransactions) {
|
||||
// these transactions are in descending order, latest transactions come first
|
||||
List<ArbitraryTransactionData> transactions
|
||||
= transactionsInDescendingOrder.stream()
|
||||
.skip(offset)
|
||||
.limit(limit)
|
||||
.collect(Collectors.toList());
|
||||
|
||||
// wrap the transactions, so they can be used for hashing and comparing
|
||||
// Class ArbitraryTransactionDataHashWrapper supports hashCode() and equals(...) for this purpose
|
||||
List<ArbitraryTransactionDataHashWrapper> wrappedTransactions
|
||||
= transactions.stream()
|
||||
.map(transaction -> new ArbitraryTransactionDataHashWrapper(transaction))
|
||||
.collect(Collectors.toList());
|
||||
|
||||
// create a set of wrappers and populate it first to last, so that all outdated transactions get rejected
|
||||
Set<ArbitraryTransactionDataHashWrapper> transactionsToProcess = new HashSet<>(wrappedTransactions.size());
|
||||
for(ArbitraryTransactionDataHashWrapper wrappedTransaction : wrappedTransactions) {
|
||||
transactionsToProcess.add(wrappedTransaction);
|
||||
}
|
||||
|
||||
// remove the matches for previously processed transactions,
|
||||
// because these transactions have had updates that have already been processed
|
||||
transactionsToProcess.removeAll(processedTransactions);
|
||||
|
||||
// add to processed transactions to compare and remove matches from future processing iterations
|
||||
processedTransactions.addAll(transactionsToProcess);
|
||||
|
||||
List<byte[]> signatures
|
||||
= transactionsToProcess.stream()
|
||||
.map(transactionToProcess -> transactionToProcess.getData()
|
||||
.getSignature())
|
||||
.collect(Collectors.toList());
|
||||
|
||||
return signatures;
|
||||
}
|
||||
|
||||
private ArbitraryTransaction fetchTransaction(final Repository repository, byte[] signature) {
|
||||
try {
|
||||
TransactionData transactionData = repository.getTransactionRepository().fromSignature(signature);
|
||||
|
@ -47,15 +47,15 @@ public class ArbitraryDataStorageManager extends Thread {
|
||||
|
||||
private static final long DIRECTORY_SIZE_CHECK_INTERVAL = 10 * 60 * 1000L; // 10 minutes
|
||||
|
||||
/** Treat storage as full at 80% usage, to reduce risk of going over the limit.
|
||||
/** Treat storage as full at 90% usage, to reduce risk of going over the limit.
|
||||
* This is necessary because we don't calculate total storage values before every write.
|
||||
* It also helps avoid a fetch/delete loop, as we will stop fetching before the hard limit.
|
||||
* This must be lower than DELETION_THRESHOLD. */
|
||||
private static final double STORAGE_FULL_THRESHOLD = 0.8f; // 80%
|
||||
private static final double STORAGE_FULL_THRESHOLD = 0.90f; // 90%
|
||||
|
||||
/** Start deleting files once we reach 90% usage.
|
||||
/** Start deleting files once we reach 98% usage.
|
||||
* This must be higher than STORAGE_FULL_THRESHOLD in order to avoid a fetch/delete loop. */
|
||||
public static final double DELETION_THRESHOLD = 0.9f; // 90%
|
||||
public static final double DELETION_THRESHOLD = 0.98f; // 98%
|
||||
|
||||
private static final long PER_NAME_STORAGE_MULTIPLIER = 4L;
|
||||
|
||||
@ -155,24 +155,31 @@ public class ArbitraryDataStorageManager extends Thread {
|
||||
* @param arbitraryTransactionData - the transaction
|
||||
* @return boolean - whether to prefetch or not
|
||||
*/
|
||||
public ArbitraryDataExamination shouldPreFetchData(Repository repository, ArbitraryTransactionData arbitraryTransactionData) {
|
||||
public boolean shouldPreFetchData(Repository repository, ArbitraryTransactionData arbitraryTransactionData) {
|
||||
String name = arbitraryTransactionData.getName();
|
||||
|
||||
// Only fetch data associated with hashes, as we already have RAW_DATA
|
||||
if (arbitraryTransactionData.getDataType() != ArbitraryTransactionData.DataType.DATA_HASH) {
|
||||
return new ArbitraryDataExamination(false, "Only fetch data associated with hashes");
|
||||
return false;
|
||||
}
|
||||
|
||||
// Don't fetch anything more if we're (nearly) out of space
|
||||
// Make sure to keep STORAGE_FULL_THRESHOLD considerably less than 1, to
|
||||
// avoid a fetch/delete loop
|
||||
if (!this.isStorageSpaceAvailable(STORAGE_FULL_THRESHOLD)) {
|
||||
return new ArbitraryDataExamination(false,"Don't fetch anything more if we're (nearly) out of space");
|
||||
return false;
|
||||
}
|
||||
|
||||
// Don't fetch anything if we're (nearly) out of space for this name
|
||||
// Again, make sure to keep STORAGE_FULL_THRESHOLD considerably less than 1, to
|
||||
// avoid a fetch/delete loop
|
||||
if (!this.isStorageSpaceAvailableForName(repository, arbitraryTransactionData.getName(), STORAGE_FULL_THRESHOLD)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
// Don't store data unless it's an allowed type (public/private)
|
||||
if (!this.isDataTypeAllowed(arbitraryTransactionData)) {
|
||||
return new ArbitraryDataExamination(false, "Don't store data unless it's an allowed type (public/private)");
|
||||
return false;
|
||||
}
|
||||
|
||||
// Handle transactions without names differently
|
||||
@ -182,21 +189,21 @@ public class ArbitraryDataStorageManager extends Thread {
|
||||
|
||||
// Never fetch data from blocked names, even if they are followed
|
||||
if (ListUtils.isNameBlocked(name)) {
|
||||
return new ArbitraryDataExamination(false, "blocked name");
|
||||
return false;
|
||||
}
|
||||
|
||||
switch (Settings.getInstance().getStoragePolicy()) {
|
||||
case FOLLOWED:
|
||||
case FOLLOWED_OR_VIEWED:
|
||||
return new ArbitraryDataExamination(ListUtils.isFollowingName(name), Settings.getInstance().getStoragePolicy().name());
|
||||
return ListUtils.isFollowingName(name);
|
||||
|
||||
case ALL:
|
||||
return new ArbitraryDataExamination(true, Settings.getInstance().getStoragePolicy().name());
|
||||
return true;
|
||||
|
||||
case NONE:
|
||||
case VIEWED:
|
||||
default:
|
||||
return new ArbitraryDataExamination(false, Settings.getInstance().getStoragePolicy().name());
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
@ -207,17 +214,17 @@ public class ArbitraryDataStorageManager extends Thread {
|
||||
*
|
||||
* @return boolean - whether the storage policy allows for unnamed data
|
||||
*/
|
||||
private ArbitraryDataExamination shouldPreFetchDataWithoutName() {
|
||||
private boolean shouldPreFetchDataWithoutName() {
|
||||
switch (Settings.getInstance().getStoragePolicy()) {
|
||||
case ALL:
|
||||
return new ArbitraryDataExamination(true, "Fetching all data");
|
||||
return true;
|
||||
|
||||
case NONE:
|
||||
case VIEWED:
|
||||
case FOLLOWED:
|
||||
case FOLLOWED_OR_VIEWED:
|
||||
default:
|
||||
return new ArbitraryDataExamination(false, Settings.getInstance().getStoragePolicy().name());
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
@ -477,6 +484,51 @@ public class ArbitraryDataStorageManager extends Thread {
|
||||
return true;
|
||||
}
|
||||
|
||||
public boolean isStorageSpaceAvailableForName(Repository repository, String name, double threshold) {
|
||||
if (!this.isStorageSpaceAvailable(threshold)) {
|
||||
// No storage space available at all, so no need to check this name
|
||||
return false;
|
||||
}
|
||||
|
||||
if (Settings.getInstance().getStoragePolicy() == StoragePolicy.ALL) {
|
||||
// Using storage policy ALL, so don't limit anything per name
|
||||
return true;
|
||||
}
|
||||
|
||||
if (name == null) {
|
||||
// This transaction doesn't have a name, so fall back to total space limitations
|
||||
return true;
|
||||
}
|
||||
|
||||
int followedNamesCount = ListUtils.followedNamesCount();
|
||||
if (followedNamesCount == 0) {
|
||||
// Not following any names, so we have space
|
||||
return true;
|
||||
}
|
||||
|
||||
long totalSizeForName = 0;
|
||||
long maxStoragePerName = this.storageCapacityPerName(threshold);
|
||||
|
||||
// Fetch all hosted transactions
|
||||
List<ArbitraryTransactionData> hostedTransactions = this.listAllHostedTransactions(repository, null, null);
|
||||
for (ArbitraryTransactionData transactionData : hostedTransactions) {
|
||||
String transactionName = transactionData.getName();
|
||||
if (!Objects.equals(name, transactionName)) {
|
||||
// Transaction relates to a different name
|
||||
continue;
|
||||
}
|
||||
|
||||
totalSizeForName += transactionData.getSize();
|
||||
}
|
||||
|
||||
// Have we reached the limit for this name?
|
||||
if (totalSizeForName > maxStoragePerName) {
|
||||
return false;
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
public long storageCapacityPerName(double threshold) {
|
||||
int followedNamesCount = ListUtils.followedNamesCount();
|
||||
if (followedNamesCount == 0) {
|
||||
|
@ -24,11 +24,6 @@ import org.qortal.utils.Triple;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.util.*;
|
||||
import java.util.concurrent.Executors;
|
||||
import java.util.concurrent.ScheduledExecutorService;
|
||||
import java.util.concurrent.TimeUnit;
|
||||
import java.util.function.Function;
|
||||
import java.util.stream.Collectors;
|
||||
|
||||
import static org.qortal.controller.arbitrary.ArbitraryDataFileListManager.*;
|
||||
|
||||
@ -66,7 +61,6 @@ public class ArbitraryMetadataManager {
|
||||
|
||||
|
||||
private ArbitraryMetadataManager() {
|
||||
scheduler.scheduleAtFixedRate(this::processNetworkGetArbitraryMetadataMessage, 60, 1, TimeUnit.SECONDS);
|
||||
}
|
||||
|
||||
public static ArbitraryMetadataManager getInstance() {
|
||||
@ -360,8 +354,9 @@ public class ArbitraryMetadataManager {
|
||||
|
||||
// Forward to requesting peer
|
||||
LOGGER.debug("Forwarding metadata to requesting peer: {}", requestingPeer);
|
||||
requestingPeer.sendMessage(forwardArbitraryMetadataMessage);
|
||||
|
||||
if (!requestingPeer.sendMessage(forwardArbitraryMetadataMessage)) {
|
||||
requestingPeer.disconnect("failed to forward arbitrary metadata");
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -376,159 +371,107 @@ public class ArbitraryMetadataManager {
|
||||
}
|
||||
}
|
||||
|
||||
// List to collect messages
|
||||
private final List<PeerMessage> messageList = new ArrayList<>();
|
||||
// Lock to synchronize access to the list
|
||||
private final Object lock = new Object();
|
||||
|
||||
// Scheduled executor service to process messages every second
|
||||
private final ScheduledExecutorService scheduler = Executors.newScheduledThreadPool(1);
|
||||
|
||||
public void onNetworkGetArbitraryMetadataMessage(Peer peer, Message message) {
|
||||
|
||||
// Don't respond if QDN is disabled
|
||||
if (!Settings.getInstance().isQdnEnabled()) {
|
||||
return;
|
||||
}
|
||||
|
||||
synchronized (lock) {
|
||||
messageList.add(new PeerMessage(peer, message));
|
||||
Controller.getInstance().stats.getArbitraryMetadataMessageStats.requests.incrementAndGet();
|
||||
|
||||
GetArbitraryMetadataMessage getArbitraryMetadataMessage = (GetArbitraryMetadataMessage) message;
|
||||
byte[] signature = getArbitraryMetadataMessage.getSignature();
|
||||
String signature58 = Base58.encode(signature);
|
||||
Long now = NTP.getTime();
|
||||
Triple<String, Peer, Long> newEntry = new Triple<>(signature58, peer, now);
|
||||
|
||||
// If we've seen this request recently, then ignore
|
||||
if (arbitraryMetadataRequests.putIfAbsent(message.getId(), newEntry) != null) {
|
||||
LOGGER.debug("Ignoring metadata request from peer {} for signature {}", peer, signature58);
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
private void processNetworkGetArbitraryMetadataMessage() {
|
||||
LOGGER.debug("Received metadata request from peer {} for signature {}", peer, signature58);
|
||||
|
||||
try {
|
||||
List<PeerMessage> messagesToProcess;
|
||||
synchronized (lock) {
|
||||
messagesToProcess = new ArrayList<>(messageList);
|
||||
messageList.clear();
|
||||
}
|
||||
ArbitraryTransactionData transactionData = null;
|
||||
ArbitraryDataFile metadataFile = null;
|
||||
|
||||
Map<String, byte[]> signatureBySignature58 = new HashMap<>((messagesToProcess.size()));
|
||||
Map<String, Long> nowBySignature58 = new HashMap<>(messagesToProcess.size());
|
||||
Map<String,PeerMessage> peerMessageBySignature58 = new HashMap<>(messagesToProcess.size());
|
||||
try (final Repository repository = RepositoryManager.getRepository()) {
|
||||
|
||||
for( PeerMessage peerMessage : messagesToProcess) {
|
||||
Controller.getInstance().stats.getArbitraryMetadataMessageStats.requests.incrementAndGet();
|
||||
// Firstly we need to lookup this file on chain to get its metadata hash
|
||||
transactionData = (ArbitraryTransactionData)repository.getTransactionRepository().fromSignature(signature);
|
||||
if (transactionData instanceof ArbitraryTransactionData) {
|
||||
|
||||
GetArbitraryMetadataMessage getArbitraryMetadataMessage = (GetArbitraryMetadataMessage) peerMessage.message;
|
||||
byte[] signature = getArbitraryMetadataMessage.getSignature();
|
||||
String signature58 = Base58.encode(signature);
|
||||
Long now = NTP.getTime();
|
||||
Triple<String, Peer, Long> newEntry = new Triple<>(signature58, peerMessage.peer, now);
|
||||
// Check if we're even allowed to serve metadata for this transaction
|
||||
if (ArbitraryDataStorageManager.getInstance().canStoreData(transactionData)) {
|
||||
|
||||
// If we've seen this request recently, then ignore
|
||||
if (arbitraryMetadataRequests.putIfAbsent(peerMessage.message.getId(), newEntry) != null) {
|
||||
LOGGER.debug("Ignoring metadata request from peer {} for signature {}", peerMessage.peer, signature58);
|
||||
continue;
|
||||
byte[] metadataHash = transactionData.getMetadataHash();
|
||||
if (metadataHash != null) {
|
||||
|
||||
// Load metadata file
|
||||
metadataFile = ArbitraryDataFile.fromHash(metadataHash, signature);
|
||||
}
|
||||
}
|
||||
|
||||
LOGGER.debug("Received metadata request from peer {} for signature {}", peerMessage.peer, signature58);
|
||||
|
||||
signatureBySignature58.put(signature58, signature);
|
||||
nowBySignature58.put(signature58, now);
|
||||
peerMessageBySignature58.put(signature58, peerMessage);
|
||||
}
|
||||
|
||||
if( signatureBySignature58.isEmpty() ) return;
|
||||
} catch (DataException e) {
|
||||
LOGGER.error(String.format("Repository issue while fetching arbitrary metadata for peer %s", peer), e);
|
||||
}
|
||||
|
||||
List<TransactionData> transactionDataList;
|
||||
try (final Repository repository = RepositoryManager.getRepository()) {
|
||||
// We should only respond if we have the metadata file
|
||||
if (metadataFile != null && metadataFile.exists()) {
|
||||
|
||||
// Firstly we need to lookup this file on chain to get its metadata hash
|
||||
transactionDataList = repository.getTransactionRepository().fromSignatures(new ArrayList(signatureBySignature58.values()));
|
||||
} catch (DataException e) {
|
||||
LOGGER.error(String.format("Repository issue while fetching arbitrary transactions"), e);
|
||||
// We have the metadata file, so update requests map to reflect that we've sent it
|
||||
newEntry = new Triple<>(null, null, now);
|
||||
arbitraryMetadataRequests.put(message.getId(), newEntry);
|
||||
|
||||
ArbitraryMetadataMessage arbitraryMetadataMessage = new ArbitraryMetadataMessage(signature, metadataFile);
|
||||
arbitraryMetadataMessage.setId(message.getId());
|
||||
if (!peer.sendMessage(arbitraryMetadataMessage)) {
|
||||
LOGGER.debug("Couldn't send metadata");
|
||||
peer.disconnect("failed to send metadata");
|
||||
return;
|
||||
}
|
||||
LOGGER.debug("Sent metadata");
|
||||
|
||||
Map<String, ArbitraryTransactionData> dataBySignature58
|
||||
= transactionDataList.stream()
|
||||
.filter(data -> data instanceof ArbitraryTransactionData)
|
||||
.map(ArbitraryTransactionData.class::cast)
|
||||
.collect(Collectors.toMap(data -> Base58.encode(data.getSignature()), Function.identity()));
|
||||
// Nothing left to do, so return to prevent any unnecessary forwarding from occurring
|
||||
LOGGER.debug("No need for any forwarding because metadata request is fully served");
|
||||
return;
|
||||
|
||||
for(Map.Entry<String, ArbitraryTransactionData> entry : dataBySignature58.entrySet()) {
|
||||
String signature58 = entry.getKey();
|
||||
ArbitraryTransactionData transactionData = entry.getValue();
|
||||
}
|
||||
|
||||
try {
|
||||
// We may need to forward this request on
|
||||
boolean isBlocked = (transactionData == null || ListUtils.isNameBlocked(transactionData.getName()));
|
||||
if (Settings.getInstance().isRelayModeEnabled() && !isBlocked) {
|
||||
// In relay mode - so ask our other peers if they have it
|
||||
|
||||
// Check if we're even allowed to serve metadata for this transaction
|
||||
if (ArbitraryDataStorageManager.getInstance().canStoreData(transactionData)) {
|
||||
long requestTime = getArbitraryMetadataMessage.getRequestTime();
|
||||
int requestHops = getArbitraryMetadataMessage.getRequestHops() + 1;
|
||||
long totalRequestTime = now - requestTime;
|
||||
|
||||
byte[] metadataHash = transactionData.getMetadataHash();
|
||||
if (metadataHash != null) {
|
||||
if (totalRequestTime < RELAY_REQUEST_MAX_DURATION) {
|
||||
// Relay request hasn't timed out yet, so can potentially be rebroadcast
|
||||
if (requestHops < RELAY_REQUEST_MAX_HOPS) {
|
||||
// Relay request hasn't reached the maximum number of hops yet, so can be rebroadcast
|
||||
|
||||
// Load metadata file
|
||||
ArbitraryDataFile metadataFile = ArbitraryDataFile.fromHash(metadataHash, transactionData.getSignature());
|
||||
// We should only respond if we have the metadata file
|
||||
if (metadataFile != null && metadataFile.exists()) {
|
||||
Message relayGetArbitraryMetadataMessage = new GetArbitraryMetadataMessage(signature, requestTime, requestHops);
|
||||
relayGetArbitraryMetadataMessage.setId(message.getId());
|
||||
|
||||
PeerMessage peerMessage = peerMessageBySignature58.get(signature58);
|
||||
Message message = peerMessage.message;
|
||||
Peer peer = peerMessage.peer;
|
||||
LOGGER.debug("Rebroadcasting metadata request from peer {} for signature {} to our other peers... totalRequestTime: {}, requestHops: {}", peer, Base58.encode(signature), totalRequestTime, requestHops);
|
||||
Network.getInstance().broadcast(
|
||||
broadcastPeer ->
|
||||
!broadcastPeer.isAtLeastVersion(RELAY_MIN_PEER_VERSION) ? null :
|
||||
broadcastPeer == peer || Objects.equals(broadcastPeer.getPeerData().getAddress().getHost(), peer.getPeerData().getAddress().getHost()) ? null : relayGetArbitraryMetadataMessage);
|
||||
|
||||
// We have the metadata file, so update requests map to reflect that we've sent it
|
||||
Triple newEntry = new Triple<>(null, null, nowBySignature58.get(signature58));
|
||||
arbitraryMetadataRequests.put(message.getId(), newEntry);
|
||||
|
||||
ArbitraryMetadataMessage arbitraryMetadataMessage = new ArbitraryMetadataMessage(entry.getValue().getSignature(), metadataFile);
|
||||
arbitraryMetadataMessage.setId(message.getId());
|
||||
if (!peer.sendMessage(arbitraryMetadataMessage)) {
|
||||
LOGGER.debug("Couldn't send metadata");
|
||||
continue;
|
||||
}
|
||||
LOGGER.debug("Sent metadata");
|
||||
|
||||
// Nothing left to do, so return to prevent any unnecessary forwarding from occurring
|
||||
LOGGER.debug("No need for any forwarding because metadata request is fully served");
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
} catch (DataException e) {
|
||||
LOGGER.error(String.format("Repository issue while fetching arbitrary metadata"), e);
|
||||
}
|
||||
|
||||
// We may need to forward this request on
|
||||
boolean isBlocked = (transactionDataList == null || ListUtils.isNameBlocked(transactionData.getName()));
|
||||
if (Settings.getInstance().isRelayModeEnabled() && !isBlocked) {
|
||||
// In relay mode - so ask our other peers if they have it
|
||||
|
||||
PeerMessage peerMessage = peerMessageBySignature58.get(signature58);
|
||||
GetArbitraryMetadataMessage getArbitraryMetadataMessage = (GetArbitraryMetadataMessage) peerMessage.message;
|
||||
long requestTime = getArbitraryMetadataMessage.getRequestTime();
|
||||
int requestHops = getArbitraryMetadataMessage.getRequestHops() + 1;
|
||||
long totalRequestTime = nowBySignature58.get(signature58) - requestTime;
|
||||
|
||||
if (totalRequestTime < RELAY_REQUEST_MAX_DURATION) {
|
||||
// Relay request hasn't timed out yet, so can potentially be rebroadcast
|
||||
if (requestHops < RELAY_REQUEST_MAX_HOPS) {
|
||||
// Relay request hasn't reached the maximum number of hops yet, so can be rebroadcast
|
||||
|
||||
byte[] signature = signatureBySignature58.get(signature58);
|
||||
Message relayGetArbitraryMetadataMessage = new GetArbitraryMetadataMessage(signature, requestTime, requestHops);
|
||||
relayGetArbitraryMetadataMessage.setId(getArbitraryMetadataMessage.getId());
|
||||
|
||||
Peer peer = peerMessage.peer;
|
||||
LOGGER.debug("Rebroadcasting metadata request from peer {} for signature {} to our other peers... totalRequestTime: {}, requestHops: {}", peer, Base58.encode(signature), totalRequestTime, requestHops);
|
||||
Network.getInstance().broadcast(
|
||||
broadcastPeer ->
|
||||
!broadcastPeer.isAtLeastVersion(RELAY_MIN_PEER_VERSION) ? null :
|
||||
broadcastPeer == peer || Objects.equals(broadcastPeer.getPeerData().getAddress().getHost(), peer.getPeerData().getAddress().getHost()) ? null : relayGetArbitraryMetadataMessage);
|
||||
|
||||
} else {
|
||||
// This relay request has reached the maximum number of allowed hops
|
||||
}
|
||||
} else {
|
||||
// This relay request has timed out
|
||||
}
|
||||
else {
|
||||
// This relay request has reached the maximum number of allowed hops
|
||||
}
|
||||
}
|
||||
} catch (Exception e) {
|
||||
LOGGER.error(e.getMessage(), e);
|
||||
else {
|
||||
// This relay request has timed out
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
@ -1,48 +0,0 @@
|
||||
package org.qortal.controller.arbitrary;
|
||||
|
||||
import org.qortal.arbitrary.misc.Service;
|
||||
import org.qortal.data.transaction.ArbitraryTransactionData;
|
||||
|
||||
import java.util.Objects;
|
||||
|
||||
public class ArbitraryTransactionDataHashWrapper {
|
||||
|
||||
private ArbitraryTransactionData data;
|
||||
|
||||
private int service;
|
||||
|
||||
private String name;
|
||||
|
||||
private String identifier;
|
||||
|
||||
public ArbitraryTransactionDataHashWrapper(ArbitraryTransactionData data) {
|
||||
this.data = data;
|
||||
|
||||
this.service = data.getService().value;
|
||||
this.name = data.getName();
|
||||
this.identifier = data.getIdentifier();
|
||||
}
|
||||
|
||||
public ArbitraryTransactionDataHashWrapper(int service, String name, String identifier) {
|
||||
this.service = service;
|
||||
this.name = name;
|
||||
this.identifier = identifier;
|
||||
}
|
||||
|
||||
public ArbitraryTransactionData getData() {
|
||||
return data;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean equals(Object o) {
|
||||
if (this == o) return true;
|
||||
if (o == null || getClass() != o.getClass()) return false;
|
||||
ArbitraryTransactionDataHashWrapper that = (ArbitraryTransactionDataHashWrapper) o;
|
||||
return service == that.service && name.equals(that.name) && Objects.equals(identifier, that.identifier);
|
||||
}
|
||||
|
||||
@Override
|
||||
public int hashCode() {
|
||||
return Objects.hash(service, name, identifier);
|
||||
}
|
||||
}
|
@ -1,130 +0,0 @@
|
||||
package org.qortal.controller.arbitrary;
|
||||
|
||||
import org.apache.logging.log4j.LogManager;
|
||||
import org.apache.logging.log4j.Logger;
|
||||
import org.qortal.data.transaction.ArbitraryTransactionData;
|
||||
import org.qortal.repository.Repository;
|
||||
import org.qortal.repository.RepositoryManager;
|
||||
import org.qortal.settings.Settings;
|
||||
import org.qortal.utils.ListUtils;
|
||||
import org.qortal.utils.NamedThreadFactory;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.HashSet;
|
||||
import java.util.List;
|
||||
import java.util.OptionalInt;
|
||||
import java.util.Set;
|
||||
import java.util.concurrent.Executors;
|
||||
import java.util.concurrent.ScheduledExecutorService;
|
||||
import java.util.concurrent.TimeUnit;
|
||||
import java.util.stream.Collectors;
|
||||
|
||||
public class Follower {
|
||||
|
||||
private static final Logger LOGGER = LogManager.getLogger(Follower.class);
|
||||
|
||||
private ScheduledExecutorService service
|
||||
= Executors.newScheduledThreadPool(2, new NamedThreadFactory("Follower", Thread.NORM_PRIORITY));
|
||||
|
||||
private Follower() {
|
||||
|
||||
}
|
||||
|
||||
private static Follower instance;
|
||||
|
||||
public static Follower getInstance() {
|
||||
|
||||
if( instance == null ) {
|
||||
instance = new Follower();
|
||||
}
|
||||
|
||||
return instance;
|
||||
}
|
||||
|
||||
public void start() {
|
||||
|
||||
// fetch arbitrary transactions from followed names from the last 100 blocks every 2 minutes
|
||||
service.scheduleWithFixedDelay(() -> fetch(OptionalInt.of(100)), 10, 2, TimeUnit.MINUTES);
|
||||
|
||||
// fetch arbitrary transaction from followed names from any block every 24 hours
|
||||
service.scheduleWithFixedDelay(() -> fetch(OptionalInt.empty()), 4, 24, TimeUnit.HOURS);
|
||||
}
|
||||
|
||||
private void fetch(OptionalInt limit) {
|
||||
|
||||
try {
|
||||
// for each followed name, get arbitraty transactions, then examine those transactions before fetching
|
||||
for (String name : ListUtils.followedNames()) {
|
||||
|
||||
List<ArbitraryTransactionData> transactionsInReverseOrder;
|
||||
|
||||
// open database to get the transactions in reverse order for the followed name
|
||||
try (final Repository repository = RepositoryManager.getRepository()) {
|
||||
|
||||
List<ArbitraryTransactionData> latestArbitraryTransactionsByName
|
||||
= repository.getArbitraryRepository().getLatestArbitraryTransactionsByName(name);
|
||||
|
||||
if (limit.isPresent()) {
|
||||
final int blockHeightThreshold = repository.getBlockRepository().getBlockchainHeight() - limit.getAsInt();
|
||||
|
||||
transactionsInReverseOrder
|
||||
= latestArbitraryTransactionsByName.stream().filter(tx -> tx.getBlockHeight() > blockHeightThreshold)
|
||||
.collect(Collectors.toList());
|
||||
} else {
|
||||
transactionsInReverseOrder = latestArbitraryTransactionsByName;
|
||||
}
|
||||
|
||||
} catch (Exception e) {
|
||||
LOGGER.error(e.getMessage(), e);
|
||||
transactionsInReverseOrder = new ArrayList<>(0);
|
||||
}
|
||||
|
||||
// collect process transaction hashes, so we don't fetch outdated transactions
|
||||
Set<ArbitraryTransactionDataHashWrapper> processedTransactions = new HashSet<>();
|
||||
|
||||
ArbitraryDataStorageManager storageManager = ArbitraryDataStorageManager.getInstance();
|
||||
|
||||
// for each arbitrary transaction for the followed name process, evaluate, fetch
|
||||
for (ArbitraryTransactionData arbitraryTransaction : transactionsInReverseOrder) {
|
||||
|
||||
boolean examined = false;
|
||||
|
||||
try (final Repository repository = RepositoryManager.getRepository()) {
|
||||
|
||||
// if not processed
|
||||
if (!processedTransactions.contains(new ArbitraryTransactionDataHashWrapper(arbitraryTransaction))) {
|
||||
boolean isLocal = repository.getArbitraryRepository().isDataLocal(arbitraryTransaction.getSignature());
|
||||
|
||||
// if not local, then continue to evaluate
|
||||
if (!isLocal) {
|
||||
|
||||
// evaluate fetching status for this transaction on this node
|
||||
ArbitraryDataExamination examination = storageManager.shouldPreFetchData(repository, arbitraryTransaction);
|
||||
|
||||
// if the evaluation passed, then fetch
|
||||
examined = examination.isPass();
|
||||
}
|
||||
// if locally stored, then nothing needs to be done
|
||||
|
||||
// add to processed transactions
|
||||
processedTransactions.add(new ArbitraryTransactionDataHashWrapper(arbitraryTransaction));
|
||||
}
|
||||
}
|
||||
|
||||
// if passed examination for fetching, then fetch
|
||||
if (examined) {
|
||||
LOGGER.info("for {} on {}, fetching {}", name, arbitraryTransaction.getService(), arbitraryTransaction.getIdentifier());
|
||||
boolean fetched = ArbitraryDataFileListManager.getInstance().fetchArbitraryDataFileList(arbitraryTransaction);
|
||||
|
||||
LOGGER.info("fetched = " + fetched);
|
||||
}
|
||||
|
||||
// pause a second before moving on to another transaction
|
||||
Thread.sleep(1000);
|
||||
}
|
||||
}
|
||||
} catch (Exception e) {
|
||||
LOGGER.error(e.getMessage(), e);
|
||||
}
|
||||
}
|
||||
}
|
@ -1,22 +0,0 @@
|
||||
package org.qortal.controller.arbitrary;
|
||||
|
||||
import org.qortal.network.Peer;
|
||||
import org.qortal.network.message.Message;
|
||||
|
||||
public class PeerMessage {
|
||||
Peer peer;
|
||||
Message message;
|
||||
|
||||
public PeerMessage(Peer peer, Message message) {
|
||||
this.peer = peer;
|
||||
this.message = message;
|
||||
}
|
||||
|
||||
public Peer getPeer() {
|
||||
return peer;
|
||||
}
|
||||
|
||||
public Message getMessage() {
|
||||
return message;
|
||||
}
|
||||
}
|
@ -1,33 +0,0 @@
|
||||
package org.qortal.controller.arbitrary;
|
||||
|
||||
import org.apache.logging.log4j.LogManager;
|
||||
import org.apache.logging.log4j.Logger;
|
||||
import org.qortal.repository.DataException;
|
||||
import org.qortal.repository.Repository;
|
||||
import org.qortal.repository.RepositoryManager;
|
||||
|
||||
import java.util.TimerTask;
|
||||
|
||||
public class RebuildArbitraryResourceCacheTask extends TimerTask {
|
||||
|
||||
private static final Logger LOGGER = LogManager.getLogger(RebuildArbitraryResourceCacheTask.class);
|
||||
|
||||
public static final long MILLIS_IN_HOUR = 60 * 60 * 1000;
|
||||
|
||||
public static final long MILLIS_IN_MINUTE = 60 * 1000;
|
||||
|
||||
private static final String REBUILD_ARBITRARY_RESOURCE_CACHE_TASK = "Rebuild Arbitrary Resource Cache Task";
|
||||
|
||||
@Override
|
||||
public void run() {
|
||||
|
||||
Thread.currentThread().setName(REBUILD_ARBITRARY_RESOURCE_CACHE_TASK);
|
||||
|
||||
try (final Repository repository = RepositoryManager.getRepository()) {
|
||||
ArbitraryDataCacheManager.getInstance().buildArbitraryResourcesCache(repository, true);
|
||||
}
|
||||
catch( DataException e ) {
|
||||
LOGGER.error(e.getMessage(), e);
|
||||
}
|
||||
}
|
||||
}
|
@ -2,19 +2,15 @@ package org.qortal.controller.hsqldb;
|
||||
|
||||
import org.apache.logging.log4j.LogManager;
|
||||
import org.apache.logging.log4j.Logger;
|
||||
import org.apache.logging.log4j.util.PropertySource;
|
||||
import org.qortal.data.account.AccountBalanceData;
|
||||
import org.qortal.data.account.BlockHeightRange;
|
||||
import org.qortal.data.account.BlockHeightRangeAddressAmounts;
|
||||
import org.qortal.repository.hsqldb.HSQLDBCacheUtils;
|
||||
import org.qortal.settings.Settings;
|
||||
import org.qortal.utils.BalanceRecorderUtils;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.Comparator;
|
||||
import java.util.List;
|
||||
import java.util.Optional;
|
||||
import java.util.concurrent.ConcurrentHashMap;
|
||||
import java.util.concurrent.CopyOnWriteArrayList;
|
||||
import java.util.stream.Collectors;
|
||||
|
||||
public class HSQLDBBalanceRecorder extends Thread{
|
||||
@ -27,8 +23,6 @@ public class HSQLDBBalanceRecorder extends Thread{
|
||||
|
||||
private ConcurrentHashMap<String, List<AccountBalanceData>> balancesByAddress = new ConcurrentHashMap<>();
|
||||
|
||||
private CopyOnWriteArrayList<BlockHeightRangeAddressAmounts> balanceDynamics = new CopyOnWriteArrayList<>();
|
||||
|
||||
private int priorityRequested;
|
||||
private int frequency;
|
||||
private int capacity;
|
||||
@ -67,52 +61,36 @@ public class HSQLDBBalanceRecorder extends Thread{
|
||||
|
||||
Thread.currentThread().setName("Balance Recorder");
|
||||
|
||||
HSQLDBCacheUtils.startRecordingBalances(this.balancesByHeight, this.balanceDynamics, this.priorityRequested, this.frequency, this.capacity);
|
||||
HSQLDBCacheUtils.startRecordingBalances(this.balancesByHeight, this.balancesByAddress, this.priorityRequested, this.frequency, this.capacity);
|
||||
}
|
||||
|
||||
public List<BlockHeightRangeAddressAmounts> getLatestDynamics(int limit, long offset) {
|
||||
public List<AccountBalanceData> getLatestRecordings(int limit, long offset) {
|
||||
ArrayList<AccountBalanceData> data;
|
||||
|
||||
List<BlockHeightRangeAddressAmounts> latest = this.balanceDynamics.stream()
|
||||
.sorted(BalanceRecorderUtils.BLOCK_HEIGHT_RANGE_ADDRESS_AMOUNTS_COMPARATOR.reversed())
|
||||
.skip(offset)
|
||||
.limit(limit)
|
||||
.collect(Collectors.toList());
|
||||
Optional<Integer> lastHeight = getLastHeight();
|
||||
|
||||
return latest;
|
||||
}
|
||||
if(lastHeight.isPresent() ) {
|
||||
List<AccountBalanceData> latest = this.balancesByHeight.get(lastHeight.get());
|
||||
|
||||
public List<BlockHeightRange> getRanges(Integer offset, Integer limit, Boolean reverse) {
|
||||
|
||||
if( reverse ) {
|
||||
return this.balanceDynamics.stream()
|
||||
.map(BlockHeightRangeAddressAmounts::getRange)
|
||||
.sorted(BalanceRecorderUtils.BLOCK_HEIGHT_RANGE_COMPARATOR.reversed())
|
||||
.skip(offset)
|
||||
.limit(limit)
|
||||
.collect(Collectors.toList());
|
||||
if( latest != null ) {
|
||||
data = new ArrayList<>(latest.size());
|
||||
data.addAll(
|
||||
latest.stream()
|
||||
.sorted(Comparator.comparingDouble(AccountBalanceData::getBalance).reversed())
|
||||
.skip(offset)
|
||||
.limit(limit)
|
||||
.collect(Collectors.toList())
|
||||
);
|
||||
}
|
||||
else {
|
||||
data = new ArrayList<>(0);
|
||||
}
|
||||
}
|
||||
else {
|
||||
return this.balanceDynamics.stream()
|
||||
.map(BlockHeightRangeAddressAmounts::getRange)
|
||||
.sorted(BalanceRecorderUtils.BLOCK_HEIGHT_RANGE_COMPARATOR)
|
||||
.skip(offset)
|
||||
.limit(limit)
|
||||
.collect(Collectors.toList());
|
||||
data = new ArrayList<>(0);
|
||||
}
|
||||
}
|
||||
|
||||
public Optional<BlockHeightRangeAddressAmounts> getAddressAmounts(BlockHeightRange range) {
|
||||
|
||||
return this.balanceDynamics.stream()
|
||||
.filter( dynamic -> dynamic.getRange().equals(range))
|
||||
.findAny();
|
||||
}
|
||||
|
||||
public Optional<BlockHeightRange> getRange( int height ) {
|
||||
return this.balanceDynamics.stream()
|
||||
.map(BlockHeightRangeAddressAmounts::getRange)
|
||||
.filter( range -> range.getBegin() < height && range.getEnd() >= height )
|
||||
.findAny();
|
||||
return data;
|
||||
}
|
||||
|
||||
private Optional<Integer> getLastHeight() {
|
||||
|
@ -8,7 +8,6 @@ import org.qortal.account.PrivateKeyAccount;
|
||||
import org.qortal.api.model.crosschain.TradeBotCreateRequest;
|
||||
import org.qortal.controller.Controller;
|
||||
import org.qortal.controller.Synchronizer;
|
||||
import org.qortal.controller.arbitrary.PeerMessage;
|
||||
import org.qortal.controller.tradebot.AcctTradeBot.ResponseResult;
|
||||
import org.qortal.crosschain.*;
|
||||
import org.qortal.crypto.Crypto;
|
||||
@ -38,12 +37,7 @@ import org.qortal.utils.NTP;
|
||||
import java.awt.TrayIcon.MessageType;
|
||||
import java.security.SecureRandom;
|
||||
import java.util.*;
|
||||
import java.util.concurrent.Executors;
|
||||
import java.util.concurrent.ScheduledExecutorService;
|
||||
import java.util.concurrent.TimeUnit;
|
||||
import java.util.function.Function;
|
||||
import java.util.function.Supplier;
|
||||
import java.util.stream.Collectors;
|
||||
|
||||
/**
|
||||
* Performing cross-chain trading steps on behalf of user.
|
||||
@ -124,9 +118,6 @@ public class TradeBot implements Listener {
|
||||
private Map<String, Long> validTrades = new HashMap<>();
|
||||
|
||||
private TradeBot() {
|
||||
|
||||
tradePresenceMessageScheduler.scheduleAtFixedRate( this::processTradePresencesMessages, 60, 1, TimeUnit.SECONDS);
|
||||
|
||||
EventBus.INSTANCE.addListener(event -> TradeBot.getInstance().listen(event));
|
||||
}
|
||||
|
||||
@ -560,139 +551,77 @@ public class TradeBot implements Listener {
|
||||
}
|
||||
}
|
||||
|
||||
// List to collect messages
|
||||
private final List<PeerMessage> tradePresenceMessageList = new ArrayList<>();
|
||||
// Lock to synchronize access to the list
|
||||
private final Object tradePresenceMessageLock = new Object();
|
||||
|
||||
// Scheduled executor service to process messages every second
|
||||
private final ScheduledExecutorService tradePresenceMessageScheduler = Executors.newScheduledThreadPool(1);
|
||||
|
||||
public void onTradePresencesMessage(Peer peer, Message message) {
|
||||
TradePresencesMessage tradePresencesMessage = (TradePresencesMessage) message;
|
||||
|
||||
synchronized (tradePresenceMessageLock) {
|
||||
tradePresenceMessageList.add(new PeerMessage(peer, message));
|
||||
}
|
||||
}
|
||||
List<TradePresenceData> peersTradePresences = tradePresencesMessage.getTradePresences();
|
||||
|
||||
public void processTradePresencesMessages() {
|
||||
long now = NTP.getTime();
|
||||
// Timestamps before this are too far into the past
|
||||
long pastThreshold = now;
|
||||
// Timestamps after this are too far into the future
|
||||
long futureThreshold = now + PRESENCE_LIFETIME;
|
||||
|
||||
try {
|
||||
List<PeerMessage> messagesToProcess;
|
||||
synchronized (tradePresenceMessageLock) {
|
||||
messagesToProcess = new ArrayList<>(tradePresenceMessageList);
|
||||
tradePresenceMessageList.clear();
|
||||
}
|
||||
Map<ByteArray, Supplier<ACCT>> acctSuppliersByCodeHash = SupportedBlockchain.getAcctMap();
|
||||
|
||||
if( messagesToProcess.isEmpty() ) return;
|
||||
int newCount = 0;
|
||||
|
||||
Map<Peer, List<TradePresenceData>> tradePresencesByPeer = new HashMap<>(messagesToProcess.size());
|
||||
try (final Repository repository = RepositoryManager.getRepository()) {
|
||||
for (TradePresenceData peersTradePresence : peersTradePresences) {
|
||||
long timestamp = peersTradePresence.getTimestamp();
|
||||
|
||||
// map all trade presences from the messages to their peer
|
||||
for( PeerMessage peerMessage : messagesToProcess ) {
|
||||
TradePresencesMessage tradePresencesMessage = (TradePresencesMessage) peerMessage.getMessage();
|
||||
|
||||
List<TradePresenceData> peersTradePresences = tradePresencesMessage.getTradePresences();
|
||||
|
||||
tradePresencesByPeer.put(peerMessage.getPeer(), peersTradePresences);
|
||||
}
|
||||
|
||||
long now = NTP.getTime();
|
||||
// Timestamps before this are too far into the past
|
||||
long pastThreshold = now;
|
||||
// Timestamps after this are too far into the future
|
||||
long futureThreshold = now + PRESENCE_LIFETIME;
|
||||
|
||||
Map<ByteArray, Supplier<ACCT>> acctSuppliersByCodeHash = SupportedBlockchain.getAcctMap();
|
||||
|
||||
int newCount = 0;
|
||||
|
||||
Map<String, List<Peer>> peersByAtAddress = new HashMap<>(tradePresencesByPeer.size());
|
||||
Map<String, TradePresenceData> tradePresenceByAtAddress = new HashMap<>(tradePresencesByPeer.size());
|
||||
|
||||
// for each batch of trade presence data from a peer, validate and populate the maps declared above
|
||||
for ( Map.Entry<Peer, List<TradePresenceData>> entry: tradePresencesByPeer.entrySet()) {
|
||||
|
||||
Peer peer = entry.getKey();
|
||||
|
||||
for( TradePresenceData peersTradePresence : entry.getValue() ) {
|
||||
// TradePresenceData peersTradePresence
|
||||
long timestamp = peersTradePresence.getTimestamp();
|
||||
|
||||
// Ignore if timestamp is out of bounds
|
||||
if (timestamp < pastThreshold || timestamp > futureThreshold) {
|
||||
if (timestamp < pastThreshold)
|
||||
LOGGER.trace("Ignoring trade presence {} from peer {} as timestamp {} is too old vs {}",
|
||||
peersTradePresence.getAtAddress(), peer, timestamp, pastThreshold
|
||||
);
|
||||
else
|
||||
LOGGER.trace("Ignoring trade presence {} from peer {} as timestamp {} is too new vs {}",
|
||||
peersTradePresence.getAtAddress(), peer, timestamp, pastThreshold
|
||||
);
|
||||
|
||||
continue;
|
||||
}
|
||||
|
||||
ByteArray pubkeyByteArray = ByteArray.wrap(peersTradePresence.getPublicKey());
|
||||
|
||||
// Ignore if we've previously verified this timestamp+publickey combo or sent timestamp is older
|
||||
TradePresenceData existingTradeData = this.safeAllTradePresencesByPubkey.get(pubkeyByteArray);
|
||||
if (existingTradeData != null && timestamp <= existingTradeData.getTimestamp()) {
|
||||
if (timestamp == existingTradeData.getTimestamp())
|
||||
LOGGER.trace("Ignoring trade presence {} from peer {} as we have verified timestamp {} before",
|
||||
peersTradePresence.getAtAddress(), peer, timestamp
|
||||
);
|
||||
else
|
||||
LOGGER.trace("Ignoring trade presence {} from peer {} as timestamp {} is older than latest {}",
|
||||
peersTradePresence.getAtAddress(), peer, timestamp, existingTradeData.getTimestamp()
|
||||
);
|
||||
|
||||
continue;
|
||||
}
|
||||
|
||||
// Check timestamp signature
|
||||
byte[] timestampSignature = peersTradePresence.getSignature();
|
||||
byte[] timestampBytes = Longs.toByteArray(timestamp);
|
||||
byte[] publicKey = peersTradePresence.getPublicKey();
|
||||
if (!Crypto.verify(publicKey, timestampSignature, timestampBytes)) {
|
||||
LOGGER.trace("Ignoring trade presence {} from peer {} as signature failed to verify",
|
||||
peersTradePresence.getAtAddress(), peer
|
||||
// Ignore if timestamp is out of bounds
|
||||
if (timestamp < pastThreshold || timestamp > futureThreshold) {
|
||||
if (timestamp < pastThreshold)
|
||||
LOGGER.trace("Ignoring trade presence {} from peer {} as timestamp {} is too old vs {}",
|
||||
peersTradePresence.getAtAddress(), peer, timestamp, pastThreshold
|
||||
);
|
||||
else
|
||||
LOGGER.trace("Ignoring trade presence {} from peer {} as timestamp {} is too new vs {}",
|
||||
peersTradePresence.getAtAddress(), peer, timestamp, pastThreshold
|
||||
);
|
||||
|
||||
continue;
|
||||
}
|
||||
|
||||
peersByAtAddress.computeIfAbsent(peersTradePresence.getAtAddress(), address -> new ArrayList<>()).add(peer);
|
||||
tradePresenceByAtAddress.put(peersTradePresence.getAtAddress(), peersTradePresence);
|
||||
continue;
|
||||
}
|
||||
}
|
||||
|
||||
if( tradePresenceByAtAddress.isEmpty() ) return;
|
||||
ByteArray pubkeyByteArray = ByteArray.wrap(peersTradePresence.getPublicKey());
|
||||
|
||||
List<ATData> atDataList;
|
||||
try (final Repository repository = RepositoryManager.getRepository()) {
|
||||
atDataList = repository.getATRepository().fromATAddresses( new ArrayList<>(tradePresenceByAtAddress.keySet()) );
|
||||
} catch (DataException e) {
|
||||
LOGGER.error("Couldn't process TRADE_PRESENCES message due to repository issue", e);
|
||||
return;
|
||||
}
|
||||
|
||||
Map<String, Supplier<ACCT>> supplierByAtAddress = new HashMap<>(atDataList.size());
|
||||
|
||||
List<ATData> validatedAtDataList = new ArrayList<>(atDataList.size());
|
||||
|
||||
// for each trade
|
||||
for( ATData atData : atDataList ) {
|
||||
|
||||
TradePresenceData peersTradePresence = tradePresenceByAtAddress.get(atData.getATAddress());
|
||||
if (atData == null || atData.getIsFrozen() || atData.getIsFinished()) {
|
||||
if (atData == null)
|
||||
LOGGER.trace("Ignoring trade presence {} from peer as AT doesn't exist",
|
||||
peersTradePresence.getAtAddress()
|
||||
// Ignore if we've previously verified this timestamp+publickey combo or sent timestamp is older
|
||||
TradePresenceData existingTradeData = this.safeAllTradePresencesByPubkey.get(pubkeyByteArray);
|
||||
if (existingTradeData != null && timestamp <= existingTradeData.getTimestamp()) {
|
||||
if (timestamp == existingTradeData.getTimestamp())
|
||||
LOGGER.trace("Ignoring trade presence {} from peer {} as we have verified timestamp {} before",
|
||||
peersTradePresence.getAtAddress(), peer, timestamp
|
||||
);
|
||||
else
|
||||
LOGGER.trace("Ignoring trade presence {} from peer as AT is frozen or finished",
|
||||
peersTradePresence.getAtAddress()
|
||||
LOGGER.trace("Ignoring trade presence {} from peer {} as timestamp {} is older than latest {}",
|
||||
peersTradePresence.getAtAddress(), peer, timestamp, existingTradeData.getTimestamp()
|
||||
);
|
||||
|
||||
continue;
|
||||
}
|
||||
|
||||
// Check timestamp signature
|
||||
byte[] timestampSignature = peersTradePresence.getSignature();
|
||||
byte[] timestampBytes = Longs.toByteArray(timestamp);
|
||||
byte[] publicKey = peersTradePresence.getPublicKey();
|
||||
if (!Crypto.verify(publicKey, timestampSignature, timestampBytes)) {
|
||||
LOGGER.trace("Ignoring trade presence {} from peer {} as signature failed to verify",
|
||||
peersTradePresence.getAtAddress(), peer
|
||||
);
|
||||
|
||||
continue;
|
||||
}
|
||||
|
||||
ATData atData = repository.getATRepository().fromATAddress(peersTradePresence.getAtAddress());
|
||||
if (atData == null || atData.getIsFrozen() || atData.getIsFinished()) {
|
||||
if (atData == null)
|
||||
LOGGER.trace("Ignoring trade presence {} from peer {} as AT doesn't exist",
|
||||
peersTradePresence.getAtAddress(), peer
|
||||
);
|
||||
else
|
||||
LOGGER.trace("Ignoring trade presence {} from peer {} as AT is frozen or finished",
|
||||
peersTradePresence.getAtAddress(), peer
|
||||
);
|
||||
|
||||
continue;
|
||||
@ -701,87 +630,51 @@ public class TradeBot implements Listener {
|
||||
ByteArray atCodeHash = ByteArray.wrap(atData.getCodeHash());
|
||||
Supplier<ACCT> acctSupplier = acctSuppliersByCodeHash.get(atCodeHash);
|
||||
if (acctSupplier == null) {
|
||||
LOGGER.trace("Ignoring trade presence {} from peer as AT isn't a known ACCT?",
|
||||
peersTradePresence.getAtAddress()
|
||||
LOGGER.trace("Ignoring trade presence {} from peer {} as AT isn't a known ACCT?",
|
||||
peersTradePresence.getAtAddress(), peer
|
||||
);
|
||||
|
||||
continue;
|
||||
}
|
||||
validatedAtDataList.add(atData);
|
||||
}
|
||||
|
||||
// populated data for each trade
|
||||
List<CrossChainTradeData> crossChainTradeDataList;
|
||||
|
||||
// validated trade data grouped by code (cross chain coin)
|
||||
Map<ByteArray, List<ATData>> atDataByCodeHash
|
||||
= validatedAtDataList.stream().collect(
|
||||
Collectors.groupingBy(data -> ByteArray.wrap(data.getCodeHash())));
|
||||
|
||||
try (final Repository repository = RepositoryManager.getRepository()) {
|
||||
|
||||
crossChainTradeDataList = new ArrayList<>();
|
||||
|
||||
// for each code (cross chain coin), get each trade, then populate trade data
|
||||
for( Map.Entry<ByteArray, List<ATData>> entry : atDataByCodeHash.entrySet() ) {
|
||||
|
||||
Supplier<ACCT> acctSupplier = acctSuppliersByCodeHash.get(entry.getKey());
|
||||
|
||||
crossChainTradeDataList.addAll(
|
||||
acctSupplier.get().populateTradeDataList(
|
||||
repository,
|
||||
entry.getValue()
|
||||
)
|
||||
.stream().filter( data -> data != null )
|
||||
.collect(Collectors.toList())
|
||||
);
|
||||
}
|
||||
} catch (DataException e) {
|
||||
LOGGER.error("Couldn't process TRADE_PRESENCES message due to repository issue", e);
|
||||
return;
|
||||
}
|
||||
|
||||
// for each populated trade data, validate and fire event
|
||||
for( CrossChainTradeData tradeData : crossChainTradeDataList ) {
|
||||
|
||||
List<Peer> peers = peersByAtAddress.get(tradeData.qortalAtAddress);
|
||||
|
||||
for( Peer peer : peers ) {
|
||||
|
||||
TradePresenceData peersTradePresence = tradePresenceByAtAddress.get(tradeData.qortalAtAddress);
|
||||
|
||||
// Convert signer's public key to address form
|
||||
String signerAddress = peersTradePresence.getTradeAddress();
|
||||
|
||||
// Signer's public key (in address form) must match Bob's / Alice's trade public key (in address form)
|
||||
if (!signerAddress.equals(tradeData.qortalCreatorTradeAddress) && !signerAddress.equals(tradeData.qortalPartnerAddress)) {
|
||||
LOGGER.trace("Ignoring trade presence {} from peer {} as signer isn't Alice or Bob?",
|
||||
peersTradePresence.getAtAddress(), peer
|
||||
);
|
||||
|
||||
continue;
|
||||
}
|
||||
|
||||
ByteArray pubkeyByteArray = ByteArray.wrap(peersTradePresence.getPublicKey());
|
||||
|
||||
// This is new to us
|
||||
this.allTradePresencesByPubkey.put(pubkeyByteArray, peersTradePresence);
|
||||
++newCount;
|
||||
|
||||
LOGGER.trace("Added trade presence {} from peer {} with timestamp {}",
|
||||
peersTradePresence.getAtAddress(), peer, tradeData.creationTimestamp
|
||||
CrossChainTradeData tradeData = acctSupplier.get().populateTradeData(repository, atData);
|
||||
if (tradeData == null) {
|
||||
LOGGER.trace("Ignoring trade presence {} from peer {} as trade data not found?",
|
||||
peersTradePresence.getAtAddress(), peer
|
||||
);
|
||||
|
||||
EventBus.INSTANCE.notify(new TradePresenceEvent(peersTradePresence));
|
||||
continue;
|
||||
}
|
||||
}
|
||||
|
||||
if (newCount > 0) {
|
||||
LOGGER.info("New trade presences: {}, all trade presences: {}", newCount, allTradePresencesByPubkey.size());
|
||||
rebuildSafeAllTradePresences();
|
||||
// Convert signer's public key to address form
|
||||
String signerAddress = peersTradePresence.getTradeAddress();
|
||||
|
||||
// Signer's public key (in address form) must match Bob's / Alice's trade public key (in address form)
|
||||
if (!signerAddress.equals(tradeData.qortalCreatorTradeAddress) && !signerAddress.equals(tradeData.qortalPartnerAddress)) {
|
||||
LOGGER.trace("Ignoring trade presence {} from peer {} as signer isn't Alice or Bob?",
|
||||
peersTradePresence.getAtAddress(), peer
|
||||
);
|
||||
|
||||
continue;
|
||||
}
|
||||
|
||||
// This is new to us
|
||||
this.allTradePresencesByPubkey.put(pubkeyByteArray, peersTradePresence);
|
||||
++newCount;
|
||||
|
||||
LOGGER.trace("Added trade presence {} from peer {} with timestamp {}",
|
||||
peersTradePresence.getAtAddress(), peer, timestamp
|
||||
);
|
||||
|
||||
EventBus.INSTANCE.notify(new TradePresenceEvent(peersTradePresence));
|
||||
}
|
||||
} catch (Exception e) {
|
||||
LOGGER.error(e.getMessage(), e);
|
||||
} catch (DataException e) {
|
||||
LOGGER.error("Couldn't process TRADE_PRESENCES message due to repository issue", e);
|
||||
}
|
||||
|
||||
if (newCount > 0) {
|
||||
LOGGER.debug("New trade presences: {}, all trade presences: {}", newCount, allTradePresencesByPubkey.size());
|
||||
rebuildSafeAllTradePresences();
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -6,9 +6,6 @@ import org.qortal.data.crosschain.CrossChainTradeData;
|
||||
import org.qortal.repository.DataException;
|
||||
import org.qortal.repository.Repository;
|
||||
|
||||
import java.util.List;
|
||||
import java.util.OptionalLong;
|
||||
|
||||
public interface ACCT {
|
||||
|
||||
public byte[] getCodeBytesHash();
|
||||
@ -19,12 +16,8 @@ public interface ACCT {
|
||||
|
||||
public CrossChainTradeData populateTradeData(Repository repository, ATData atData) throws DataException;
|
||||
|
||||
public List<CrossChainTradeData> populateTradeDataList(Repository respository, List<ATData> atDataList) throws DataException;
|
||||
|
||||
public CrossChainTradeData populateTradeData(Repository repository, ATStateData atStateData) throws DataException;
|
||||
|
||||
CrossChainTradeData populateTradeData(Repository repository, byte[] creatorPublicKey, long creationTimestamp, ATStateData atStateData, OptionalLong optionalBalance) throws DataException;
|
||||
|
||||
public byte[] buildCancelMessage(String creatorQortalAddress);
|
||||
|
||||
public byte[] findSecretA(Repository repository, CrossChainTradeData crossChainTradeData) throws DataException;
|
||||
|
@ -1,6 +1,5 @@
|
||||
package org.qortal.crosschain;
|
||||
|
||||
import org.bitcoinj.core.Coin;
|
||||
import org.bitcoinj.core.Context;
|
||||
import org.bitcoinj.core.NetworkParameters;
|
||||
import org.bitcoinj.core.Transaction;
|
||||
@ -15,21 +14,15 @@ import java.util.Arrays;
|
||||
import java.util.Collection;
|
||||
import java.util.EnumMap;
|
||||
import java.util.Map;
|
||||
import java.util.concurrent.atomic.AtomicLong;
|
||||
|
||||
public class Bitcoin extends Bitcoiny {
|
||||
|
||||
public static final String CURRENCY_CODE = "BTC";
|
||||
|
||||
// Locking fee to lock in a QORT for BTC. This is the default value that the user should reset to
|
||||
// a value inline with the BTC fee market. This is 5 sats per kB.
|
||||
private static final Coin DEFAULT_FEE_PER_KB = Coin.valueOf(5_000); // 0.00005 BTC per 1000 bytes
|
||||
private static final long MINIMUM_ORDER_AMOUNT = 100000; // 0.001 BTC minimum order, due to high fees
|
||||
|
||||
private static final long MINIMUM_ORDER_AMOUNT = 100_000; // 0.001 BTC minimum order, due to high fees
|
||||
|
||||
// Default value until user resets fee to compete with the current market. This is a total value for a
|
||||
// p2sh transaction, size 300 kB, 5 sats per kB
|
||||
private static final long NEW_FEE_AMOUNT = 1_500L;
|
||||
// Temporary values until a dynamic fee system is written.
|
||||
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
|
||||
|
||||
@ -118,7 +111,7 @@ public class Bitcoin extends Bitcoiny {
|
||||
|
||||
@Override
|
||||
public long getP2shFee(Long timestamp) {
|
||||
return this.getFeeRequired();
|
||||
return this.getFeeCeiling();
|
||||
}
|
||||
},
|
||||
TEST3 {
|
||||
@ -180,14 +173,14 @@ public class Bitcoin extends Bitcoiny {
|
||||
}
|
||||
};
|
||||
|
||||
private AtomicLong feeRequired = new AtomicLong(NEW_FEE_AMOUNT);
|
||||
private long feeCeiling = NEW_FEE_AMOUNT;
|
||||
|
||||
public long getFeeRequired() {
|
||||
return feeRequired.get();
|
||||
public long getFeeCeiling() {
|
||||
return feeCeiling;
|
||||
}
|
||||
|
||||
public void setFeeRequired(long feeRequired) {
|
||||
this.feeRequired.set(feeRequired);
|
||||
public void setFeeCeiling(long feeCeiling) {
|
||||
this.feeCeiling = feeCeiling;
|
||||
}
|
||||
|
||||
public abstract NetworkParameters getParams();
|
||||
@ -203,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, DEFAULT_FEE_PER_KB);
|
||||
super(blockchain, bitcoinjContext, currencyCode, bitcoinjContext.getFeePerKb());
|
||||
this.bitcoinNet = bitcoinNet;
|
||||
|
||||
LOGGER.info(() -> String.format("Starting Bitcoin support using %s", this.bitcoinNet.name()));
|
||||
@ -249,14 +242,14 @@ public class Bitcoin extends Bitcoiny {
|
||||
}
|
||||
|
||||
@Override
|
||||
public long getFeeRequired() {
|
||||
return this.bitcoinNet.getFeeRequired();
|
||||
public long getFeeCeiling() {
|
||||
return this.bitcoinNet.getFeeCeiling();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void setFeeRequired(long fee) {
|
||||
public void setFeeCeiling(long fee) {
|
||||
|
||||
this.bitcoinNet.setFeeRequired( fee );
|
||||
this.bitcoinNet.setFeeCeiling( fee );
|
||||
}
|
||||
/**
|
||||
* Returns bitcoinj transaction sending <tt>amount</tt> to <tt>recipient</tt> using 20 sat/byte fee.
|
||||
|
@ -4,7 +4,6 @@ import com.google.common.hash.HashCode;
|
||||
import com.google.common.primitives.Bytes;
|
||||
import org.ciyam.at.*;
|
||||
import org.qortal.account.Account;
|
||||
import org.qortal.api.resource.CrossChainUtils;
|
||||
import org.qortal.asset.Asset;
|
||||
import org.qortal.at.QortalFunctionCode;
|
||||
import org.qortal.crypto.Crypto;
|
||||
@ -20,7 +19,6 @@ import org.qortal.utils.BitTwiddling;
|
||||
import java.nio.ByteBuffer;
|
||||
import java.util.Arrays;
|
||||
import java.util.List;
|
||||
import java.util.OptionalLong;
|
||||
|
||||
import static org.ciyam.at.OpCode.calcOffset;
|
||||
|
||||
@ -610,14 +608,7 @@ public class BitcoinACCTv1 implements ACCT {
|
||||
@Override
|
||||
public CrossChainTradeData populateTradeData(Repository repository, ATData atData) throws DataException {
|
||||
ATStateData atStateData = repository.getATRepository().getLatestATState(atData.getATAddress());
|
||||
return populateTradeData(repository, atData.getCreatorPublicKey(), atData.getCreation(), atStateData, OptionalLong.empty());
|
||||
}
|
||||
|
||||
@Override
|
||||
public List<CrossChainTradeData> populateTradeDataList(Repository repository, List<ATData> atDataList) throws DataException {
|
||||
List<CrossChainTradeData> crossChainTradeDataList = CrossChainUtils.populateTradeDataList(repository, this, atDataList);
|
||||
|
||||
return crossChainTradeDataList;
|
||||
return populateTradeData(repository, atData.getCreatorPublicKey(), atData.getCreation(), atStateData);
|
||||
}
|
||||
|
||||
/**
|
||||
@ -626,14 +617,13 @@ public class BitcoinACCTv1 implements ACCT {
|
||||
@Override
|
||||
public CrossChainTradeData populateTradeData(Repository repository, ATStateData atStateData) throws DataException {
|
||||
ATData atData = repository.getATRepository().fromATAddress(atStateData.getATAddress());
|
||||
return populateTradeData(repository, atData.getCreatorPublicKey(), atData.getCreation(), atStateData, OptionalLong.empty());
|
||||
return populateTradeData(repository, atData.getCreatorPublicKey(), atData.getCreation(), atStateData);
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns CrossChainTradeData with useful info extracted from AT.
|
||||
*/
|
||||
@Override
|
||||
public CrossChainTradeData populateTradeData(Repository repository, byte[] creatorPublicKey, long creationTimestamp, ATStateData atStateData, OptionalLong optionalBalance) throws DataException {
|
||||
public CrossChainTradeData populateTradeData(Repository repository, byte[] creatorPublicKey, long creationTimestamp, ATStateData atStateData) throws DataException {
|
||||
byte[] addressBytes = new byte[25]; // for general use
|
||||
String atAddress = atStateData.getATAddress();
|
||||
|
||||
@ -646,13 +636,8 @@ public class BitcoinACCTv1 implements ACCT {
|
||||
tradeData.qortalCreator = Crypto.toAddress(creatorPublicKey);
|
||||
tradeData.creationTimestamp = creationTimestamp;
|
||||
|
||||
if(optionalBalance.isPresent()) {
|
||||
tradeData.qortBalance = optionalBalance.getAsLong();
|
||||
}
|
||||
else {
|
||||
Account atAccount = new Account(repository, atAddress);
|
||||
tradeData.qortBalance = atAccount.getConfirmedBalance(Asset.QORT);
|
||||
}
|
||||
Account atAccount = new Account(repository, atAddress);
|
||||
tradeData.qortBalance = atAccount.getConfirmedBalance(Asset.QORT);
|
||||
|
||||
byte[] stateData = atStateData.getStateData();
|
||||
ByteBuffer dataByteBuffer = ByteBuffer.wrap(stateData);
|
||||
|
@ -6,7 +6,6 @@ import org.apache.logging.log4j.LogManager;
|
||||
import org.apache.logging.log4j.Logger;
|
||||
import org.ciyam.at.*;
|
||||
import org.qortal.account.Account;
|
||||
import org.qortal.api.resource.CrossChainUtils;
|
||||
import org.qortal.asset.Asset;
|
||||
import org.qortal.at.QortalFunctionCode;
|
||||
import org.qortal.crypto.Crypto;
|
||||
@ -22,7 +21,6 @@ import org.qortal.utils.BitTwiddling;
|
||||
import java.nio.ByteBuffer;
|
||||
import java.util.Arrays;
|
||||
import java.util.List;
|
||||
import java.util.OptionalLong;
|
||||
|
||||
import static org.ciyam.at.OpCode.calcOffset;
|
||||
|
||||
@ -571,14 +569,7 @@ public class BitcoinACCTv3 implements ACCT {
|
||||
@Override
|
||||
public CrossChainTradeData populateTradeData(Repository repository, ATData atData) throws DataException {
|
||||
ATStateData atStateData = repository.getATRepository().getLatestATState(atData.getATAddress());
|
||||
return populateTradeData(repository, atData.getCreatorPublicKey(), atData.getCreation(), atStateData, OptionalLong.empty());
|
||||
}
|
||||
|
||||
@Override
|
||||
public List<CrossChainTradeData> populateTradeDataList(Repository repository, List<ATData> atDataList) throws DataException {
|
||||
List<CrossChainTradeData> crossChainTradeDataList = CrossChainUtils.populateTradeDataList(repository, this, atDataList);
|
||||
|
||||
return crossChainTradeDataList;
|
||||
return populateTradeData(repository, atData.getCreatorPublicKey(), atData.getCreation(), atStateData);
|
||||
}
|
||||
|
||||
/**
|
||||
@ -587,14 +578,13 @@ public class BitcoinACCTv3 implements ACCT {
|
||||
@Override
|
||||
public CrossChainTradeData populateTradeData(Repository repository, ATStateData atStateData) throws DataException {
|
||||
ATData atData = repository.getATRepository().fromATAddress(atStateData.getATAddress());
|
||||
return populateTradeData(repository, atData.getCreatorPublicKey(), atData.getCreation(), atStateData, OptionalLong.empty());
|
||||
return populateTradeData(repository, atData.getCreatorPublicKey(), atData.getCreation(), atStateData);
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns CrossChainTradeData with useful info extracted from AT.
|
||||
*/
|
||||
@Override
|
||||
public CrossChainTradeData populateTradeData(Repository repository, byte[] creatorPublicKey, long creationTimestamp, ATStateData atStateData, OptionalLong optionalBalance) throws DataException {
|
||||
public CrossChainTradeData populateTradeData(Repository repository, byte[] creatorPublicKey, long creationTimestamp, ATStateData atStateData) throws DataException {
|
||||
byte[] addressBytes = new byte[25]; // for general use
|
||||
String atAddress = atStateData.getATAddress();
|
||||
|
||||
@ -607,13 +597,8 @@ public class BitcoinACCTv3 implements ACCT {
|
||||
tradeData.qortalCreator = Crypto.toAddress(creatorPublicKey);
|
||||
tradeData.creationTimestamp = creationTimestamp;
|
||||
|
||||
if(optionalBalance.isPresent()) {
|
||||
tradeData.qortBalance = optionalBalance.getAsLong();
|
||||
}
|
||||
else {
|
||||
Account atAccount = new Account(repository, atAddress);
|
||||
tradeData.qortBalance = atAccount.getConfirmedBalance(Asset.QORT);
|
||||
}
|
||||
Account atAccount = new Account(repository, atAddress);
|
||||
tradeData.qortBalance = atAccount.getConfirmedBalance(Asset.QORT);
|
||||
|
||||
byte[] stateData = atStateData.getStateData();
|
||||
ByteBuffer dataByteBuffer = ByteBuffer.wrap(stateData);
|
||||
|
@ -8,8 +8,6 @@ import org.bitcoinj.core.*;
|
||||
import org.bitcoinj.crypto.ChildNumber;
|
||||
import org.bitcoinj.crypto.DeterministicHierarchy;
|
||||
import org.bitcoinj.crypto.DeterministicKey;
|
||||
import org.bitcoinj.crypto.HDPath;
|
||||
import org.bitcoinj.params.AbstractBitcoinNetParams;
|
||||
import org.bitcoinj.script.Script.ScriptType;
|
||||
import org.bitcoinj.script.ScriptBuilder;
|
||||
import org.bitcoinj.wallet.DeterministicKeyChain;
|
||||
@ -27,7 +25,7 @@ import java.util.*;
|
||||
import java.util.stream.Collectors;
|
||||
|
||||
/** Bitcoin-like (Bitcoin, Litecoin, etc.) support */
|
||||
public abstract class Bitcoiny extends AbstractBitcoinNetParams implements ForeignBlockchain {
|
||||
public abstract class Bitcoiny implements ForeignBlockchain {
|
||||
|
||||
protected static final Logger LOGGER = LogManager.getLogger(Bitcoiny.class);
|
||||
|
||||
@ -67,7 +65,6 @@ public abstract class Bitcoiny extends AbstractBitcoinNetParams implements Forei
|
||||
// Constructors and instance
|
||||
|
||||
protected Bitcoiny(BitcoinyBlockchainProvider blockchainProvider, Context bitcoinjContext, String currencyCode, Coin feePerKb) {
|
||||
this.genesisBlock = this.getGenesisBlock();
|
||||
this.blockchainProvider = blockchainProvider;
|
||||
this.bitcoinjContext = bitcoinjContext;
|
||||
this.currencyCode = currencyCode;
|
||||
@ -77,15 +74,6 @@ public abstract class Bitcoiny extends AbstractBitcoinNetParams implements Forei
|
||||
}
|
||||
|
||||
// Getters & setters
|
||||
@Override
|
||||
public String getPaymentProtocolId() {
|
||||
return this.id;
|
||||
}
|
||||
|
||||
@Override
|
||||
public Block getGenesisBlock() {
|
||||
return this.genesisBlock;
|
||||
}
|
||||
|
||||
public BitcoinyBlockchainProvider getBlockchainProvider() {
|
||||
return this.blockchainProvider;
|
||||
@ -95,7 +83,6 @@ public abstract class Bitcoiny extends AbstractBitcoinNetParams implements Forei
|
||||
return this.bitcoinjContext;
|
||||
}
|
||||
|
||||
@Override
|
||||
public String getCurrencyCode() {
|
||||
return this.currencyCode;
|
||||
}
|
||||
@ -602,27 +589,15 @@ public abstract class Bitcoiny extends AbstractBitcoinNetParams implements Forei
|
||||
|
||||
return new AddressInfo(
|
||||
address.toString(),
|
||||
toIntegerList( key.getPath() ),
|
||||
toIntegerList( key.getPath()),
|
||||
summingUnspentOutputs(address.toString()),
|
||||
key.getPathAsString(),
|
||||
transactionCount,
|
||||
candidates.contains(address.toString()));
|
||||
}
|
||||
|
||||
/**
|
||||
* <p>Convert BitcoinJ native type to List of Integers, BitcoinJ v16 compatible
|
||||
* </p>
|
||||
*
|
||||
* @param path path to deterministic key
|
||||
* @return Array of Ints representing the keys position in the tree
|
||||
* @since v4.7.2
|
||||
*/
|
||||
private static List<Integer> toIntegerList(HDPath path) {
|
||||
return path.stream().map(ChildNumber::num).collect(Collectors.toList());
|
||||
}
|
||||
|
||||
// BitcoinJ v15 compatible
|
||||
private static List<Integer> toIntegerList(ImmutableList<ChildNumber> path) {
|
||||
|
||||
return path.stream().map(ChildNumber::num).collect(Collectors.toList());
|
||||
}
|
||||
|
||||
@ -864,9 +839,9 @@ public abstract class Bitcoiny extends AbstractBitcoinNetParams implements Forei
|
||||
} while (true);
|
||||
}
|
||||
|
||||
public abstract long getFeeRequired();
|
||||
public abstract long getFeeCeiling();
|
||||
|
||||
public abstract void setFeeRequired(long fee);
|
||||
public abstract void setFeeCeiling(long fee);
|
||||
|
||||
// UTXOProvider support
|
||||
|
||||
|
@ -1,6 +1,5 @@
|
||||
package org.qortal.crosschain;
|
||||
|
||||
import org.bitcoinj.core.Block;
|
||||
import org.bitcoinj.core.Coin;
|
||||
import org.bitcoinj.core.Context;
|
||||
import org.bitcoinj.core.NetworkParameters;
|
||||
@ -90,7 +89,7 @@ public class BitcoinyTBD extends Bitcoiny {
|
||||
NetTBD netTBD
|
||||
= new NetTBD(
|
||||
bitcoinyTBDRequest.getNetworkName(),
|
||||
bitcoinyTBDRequest.getFeeRequired(),
|
||||
bitcoinyTBDRequest.getFeeCeiling(),
|
||||
networkParams,
|
||||
Collections.emptyList(),
|
||||
bitcoinyTBDRequest.getExpectedGenesisHash()
|
||||
@ -135,30 +134,18 @@ public class BitcoinyTBD extends Bitcoiny {
|
||||
@Override
|
||||
public long getP2shFee(Long timestamp) throws ForeignBlockchainException {
|
||||
|
||||
return this.netTBD.getFeeRequired();
|
||||
return this.netTBD.getFeeCeiling();
|
||||
}
|
||||
|
||||
@Override
|
||||
public long getFeeRequired() {
|
||||
public long getFeeCeiling() {
|
||||
|
||||
return this.netTBD.getFeeRequired();
|
||||
return this.netTBD.getFeeCeiling();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void setFeeRequired(long fee) {
|
||||
public void setFeeCeiling(long fee) {
|
||||
|
||||
this.netTBD.setFeeRequired( fee );
|
||||
}
|
||||
|
||||
@Override
|
||||
public String getPaymentProtocolId() {
|
||||
return params.getId();
|
||||
}
|
||||
|
||||
@Override
|
||||
public Block getGenesisBlock() {
|
||||
if(genesisBlock == null)
|
||||
genesisBlock = params.getGenesisBlock();
|
||||
return this.genesisBlock;
|
||||
this.netTBD.setFeeCeiling( fee );
|
||||
}
|
||||
}
|
@ -98,10 +98,9 @@ public class DeterminedNetworkParams extends NetworkParameters implements Altcoi
|
||||
|
||||
LOGGER.info( "Creating Genesis Block ...");
|
||||
|
||||
// BitcoinJ v16 has a new native method for this
|
||||
//this.genesisBlock = CoinParamsUtil.createGenesisBlockFromRequest(this, request);
|
||||
|
||||
// LOGGER.info("Created Genesis Block: genesisBlock = " + genesisBlock );
|
||||
LOGGER.info("Created Genesis Block: genesisBlock = " + genesisBlock );
|
||||
|
||||
// this is 100 for each coin from what I can tell
|
||||
this.spendableCoinbaseDepth = 100;
|
||||
@ -114,9 +113,8 @@ public class DeterminedNetworkParams extends NetworkParameters implements Altcoi
|
||||
//
|
||||
// LOGGER.info("request = " + request);
|
||||
//
|
||||
// checkState(genesisHash.equals(request.getExpectedGenesisHash()))
|
||||
// alertSigningKey is removed in v16
|
||||
// this.alertSigningKey = Hex.decode(request.getPubKey());
|
||||
// checkState(genesisHash.equals(request.getExpectedGenesisHash()));
|
||||
this.alertSigningKey = Hex.decode(request.getPubKey());
|
||||
|
||||
this.majorityEnforceBlockUpgrade = request.getMajorityEnforceBlockUpgrade();
|
||||
this.majorityRejectBlockOutdated = request.getMajorityRejectBlockOutdated();
|
||||
@ -223,12 +221,6 @@ public class DeterminedNetworkParams extends NetworkParameters implements Altcoi
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public Block getGenesisBlock() {
|
||||
//ToDo: Finish
|
||||
return null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the difficulty target expected for the next block. This includes all
|
||||
* the weird cases for Litecoin such as testnet blocks which can be maximum
|
||||
|
@ -14,7 +14,6 @@ import java.util.Arrays;
|
||||
import java.util.Collection;
|
||||
import java.util.EnumMap;
|
||||
import java.util.Map;
|
||||
import java.util.concurrent.atomic.AtomicLong;
|
||||
|
||||
public class Digibyte extends Bitcoiny {
|
||||
|
||||
@ -60,7 +59,7 @@ public class Digibyte extends Bitcoiny {
|
||||
|
||||
@Override
|
||||
public long getP2shFee(Long timestamp) {
|
||||
return this.getFeeRequired();
|
||||
return this.getFeeCeiling();
|
||||
}
|
||||
},
|
||||
TEST3 {
|
||||
@ -110,14 +109,14 @@ public class Digibyte extends Bitcoiny {
|
||||
}
|
||||
};
|
||||
|
||||
private AtomicLong feeRequired = new AtomicLong(MAINNET_FEE);
|
||||
private long feeCeiling = MAINNET_FEE;
|
||||
|
||||
public long getFeeRequired() {
|
||||
return feeRequired.get();
|
||||
public long getFeeCeiling() {
|
||||
return feeCeiling;
|
||||
}
|
||||
|
||||
public void setFeeRequired(long feeRequired) {
|
||||
this.feeRequired.set(feeRequired);
|
||||
public void setFeeCeiling(long feeCeiling) {
|
||||
this.feeCeiling = feeCeiling;
|
||||
}
|
||||
|
||||
public abstract NetworkParameters getParams();
|
||||
@ -179,13 +178,13 @@ public class Digibyte extends Bitcoiny {
|
||||
}
|
||||
|
||||
@Override
|
||||
public long getFeeRequired() {
|
||||
return this.digibyteNet.getFeeRequired();
|
||||
public long getFeeCeiling() {
|
||||
return this.digibyteNet.getFeeCeiling();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void setFeeRequired(long fee) {
|
||||
public void setFeeCeiling(long fee) {
|
||||
|
||||
this.digibyteNet.setFeeRequired( fee );
|
||||
this.digibyteNet.setFeeCeiling( fee );
|
||||
}
|
||||
}
|
||||
|
@ -6,7 +6,6 @@ import org.apache.logging.log4j.LogManager;
|
||||
import org.apache.logging.log4j.Logger;
|
||||
import org.ciyam.at.*;
|
||||
import org.qortal.account.Account;
|
||||
import org.qortal.api.resource.CrossChainUtils;
|
||||
import org.qortal.asset.Asset;
|
||||
import org.qortal.at.QortalFunctionCode;
|
||||
import org.qortal.crypto.Crypto;
|
||||
@ -22,7 +21,6 @@ import org.qortal.utils.BitTwiddling;
|
||||
import java.nio.ByteBuffer;
|
||||
import java.util.Arrays;
|
||||
import java.util.List;
|
||||
import java.util.OptionalLong;
|
||||
|
||||
import static org.ciyam.at.OpCode.calcOffset;
|
||||
|
||||
@ -571,14 +569,7 @@ public class DigibyteACCTv3 implements ACCT {
|
||||
@Override
|
||||
public CrossChainTradeData populateTradeData(Repository repository, ATData atData) throws DataException {
|
||||
ATStateData atStateData = repository.getATRepository().getLatestATState(atData.getATAddress());
|
||||
return populateTradeData(repository, atData.getCreatorPublicKey(), atData.getCreation(), atStateData, OptionalLong.empty());
|
||||
}
|
||||
|
||||
@Override
|
||||
public List<CrossChainTradeData> populateTradeDataList(Repository repository, List<ATData> atDataList) throws DataException {
|
||||
List<CrossChainTradeData> crossChainTradeDataList = CrossChainUtils.populateTradeDataList(repository, this, atDataList);
|
||||
|
||||
return crossChainTradeDataList;
|
||||
return populateTradeData(repository, atData.getCreatorPublicKey(), atData.getCreation(), atStateData);
|
||||
}
|
||||
|
||||
/**
|
||||
@ -587,14 +578,13 @@ public class DigibyteACCTv3 implements ACCT {
|
||||
@Override
|
||||
public CrossChainTradeData populateTradeData(Repository repository, ATStateData atStateData) throws DataException {
|
||||
ATData atData = repository.getATRepository().fromATAddress(atStateData.getATAddress());
|
||||
return populateTradeData(repository, atData.getCreatorPublicKey(), atData.getCreation(), atStateData, OptionalLong.empty());
|
||||
return populateTradeData(repository, atData.getCreatorPublicKey(), atData.getCreation(), atStateData);
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns CrossChainTradeData with useful info extracted from AT.
|
||||
*/
|
||||
@Override
|
||||
public CrossChainTradeData populateTradeData(Repository repository, byte[] creatorPublicKey, long creationTimestamp, ATStateData atStateData, OptionalLong optionalBalance) throws DataException {
|
||||
public CrossChainTradeData populateTradeData(Repository repository, byte[] creatorPublicKey, long creationTimestamp, ATStateData atStateData) throws DataException {
|
||||
byte[] addressBytes = new byte[25]; // for general use
|
||||
String atAddress = atStateData.getATAddress();
|
||||
|
||||
@ -607,13 +597,8 @@ public class DigibyteACCTv3 implements ACCT {
|
||||
tradeData.qortalCreator = Crypto.toAddress(creatorPublicKey);
|
||||
tradeData.creationTimestamp = creationTimestamp;
|
||||
|
||||
if(optionalBalance.isPresent()) {
|
||||
tradeData.qortBalance = optionalBalance.getAsLong();
|
||||
}
|
||||
else {
|
||||
Account atAccount = new Account(repository, atAddress);
|
||||
tradeData.qortBalance = atAccount.getConfirmedBalance(Asset.QORT);
|
||||
}
|
||||
Account atAccount = new Account(repository, atAddress);
|
||||
tradeData.qortBalance = atAccount.getConfirmedBalance(Asset.QORT);
|
||||
|
||||
byte[] stateData = atStateData.getStateData();
|
||||
ByteBuffer dataByteBuffer = ByteBuffer.wrap(stateData);
|
||||
|
@ -13,7 +13,6 @@ import java.util.Arrays;
|
||||
import java.util.Collection;
|
||||
import java.util.EnumMap;
|
||||
import java.util.Map;
|
||||
import java.util.concurrent.atomic.AtomicLong;
|
||||
|
||||
public class Dogecoin extends Bitcoiny {
|
||||
|
||||
@ -61,7 +60,7 @@ public class Dogecoin extends Bitcoiny {
|
||||
|
||||
@Override
|
||||
public long getP2shFee(Long timestamp) {
|
||||
return this.getFeeRequired();
|
||||
return this.getFeeCeiling();
|
||||
}
|
||||
},
|
||||
TEST3 {
|
||||
@ -111,14 +110,14 @@ public class Dogecoin extends Bitcoiny {
|
||||
}
|
||||
};
|
||||
|
||||
private AtomicLong feeRequired = new AtomicLong(MAINNET_FEE);
|
||||
private long feeCeiling = MAINNET_FEE;
|
||||
|
||||
public long getFeeRequired() {
|
||||
return feeRequired.get();
|
||||
public long getFeeCeiling() {
|
||||
return feeCeiling;
|
||||
}
|
||||
|
||||
public void setFeeRequired(long feeRequired) {
|
||||
this.feeRequired.set(feeRequired);
|
||||
public void setFeeCeiling(long feeCeiling) {
|
||||
this.feeCeiling = feeCeiling;
|
||||
}
|
||||
|
||||
public abstract NetworkParameters getParams();
|
||||
@ -180,13 +179,13 @@ public class Dogecoin extends Bitcoiny {
|
||||
}
|
||||
|
||||
@Override
|
||||
public long getFeeRequired() {
|
||||
return this.dogecoinNet.getFeeRequired();
|
||||
public long getFeeCeiling() {
|
||||
return this.dogecoinNet.getFeeCeiling();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void setFeeRequired(long fee) {
|
||||
public void setFeeCeiling(long fee) {
|
||||
|
||||
this.dogecoinNet.setFeeRequired( fee );
|
||||
this.dogecoinNet.setFeeCeiling( fee );
|
||||
}
|
||||
}
|
||||
|
@ -6,7 +6,6 @@ import org.apache.logging.log4j.LogManager;
|
||||
import org.apache.logging.log4j.Logger;
|
||||
import org.ciyam.at.*;
|
||||
import org.qortal.account.Account;
|
||||
import org.qortal.api.resource.CrossChainUtils;
|
||||
import org.qortal.asset.Asset;
|
||||
import org.qortal.at.QortalFunctionCode;
|
||||
import org.qortal.crypto.Crypto;
|
||||
@ -22,7 +21,6 @@ import org.qortal.utils.BitTwiddling;
|
||||
import java.nio.ByteBuffer;
|
||||
import java.util.Arrays;
|
||||
import java.util.List;
|
||||
import java.util.OptionalLong;
|
||||
|
||||
import static org.ciyam.at.OpCode.calcOffset;
|
||||
|
||||
@ -568,14 +566,7 @@ public class DogecoinACCTv1 implements ACCT {
|
||||
@Override
|
||||
public CrossChainTradeData populateTradeData(Repository repository, ATData atData) throws DataException {
|
||||
ATStateData atStateData = repository.getATRepository().getLatestATState(atData.getATAddress());
|
||||
return populateTradeData(repository, atData.getCreatorPublicKey(), atData.getCreation(), atStateData, OptionalLong.empty());
|
||||
}
|
||||
|
||||
@Override
|
||||
public List<CrossChainTradeData> populateTradeDataList(Repository repository, List<ATData> atDataList) throws DataException {
|
||||
List<CrossChainTradeData> crossChainTradeDataList = CrossChainUtils.populateTradeDataList(repository, this, atDataList);
|
||||
|
||||
return crossChainTradeDataList;
|
||||
return populateTradeData(repository, atData.getCreatorPublicKey(), atData.getCreation(), atStateData);
|
||||
}
|
||||
|
||||
/**
|
||||
@ -584,14 +575,13 @@ public class DogecoinACCTv1 implements ACCT {
|
||||
@Override
|
||||
public CrossChainTradeData populateTradeData(Repository repository, ATStateData atStateData) throws DataException {
|
||||
ATData atData = repository.getATRepository().fromATAddress(atStateData.getATAddress());
|
||||
return populateTradeData(repository, atData.getCreatorPublicKey(), atData.getCreation(), atStateData, OptionalLong.empty());
|
||||
return populateTradeData(repository, atData.getCreatorPublicKey(), atData.getCreation(), atStateData);
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns CrossChainTradeData with useful info extracted from AT.
|
||||
*/
|
||||
@Override
|
||||
public CrossChainTradeData populateTradeData(Repository repository, byte[] creatorPublicKey, long creationTimestamp, ATStateData atStateData, OptionalLong optionalBalance) throws DataException {
|
||||
public CrossChainTradeData populateTradeData(Repository repository, byte[] creatorPublicKey, long creationTimestamp, ATStateData atStateData) throws DataException {
|
||||
byte[] addressBytes = new byte[25]; // for general use
|
||||
String atAddress = atStateData.getATAddress();
|
||||
|
||||
@ -604,13 +594,8 @@ public class DogecoinACCTv1 implements ACCT {
|
||||
tradeData.qortalCreator = Crypto.toAddress(creatorPublicKey);
|
||||
tradeData.creationTimestamp = creationTimestamp;
|
||||
|
||||
if(optionalBalance.isPresent()) {
|
||||
tradeData.qortBalance = optionalBalance.getAsLong();
|
||||
}
|
||||
else {
|
||||
Account atAccount = new Account(repository, atAddress);
|
||||
tradeData.qortBalance = atAccount.getConfirmedBalance(Asset.QORT);
|
||||
}
|
||||
Account atAccount = new Account(repository, atAddress);
|
||||
tradeData.qortBalance = atAccount.getConfirmedBalance(Asset.QORT);
|
||||
|
||||
byte[] stateData = atStateData.getStateData();
|
||||
ByteBuffer dataByteBuffer = ByteBuffer.wrap(stateData);
|
||||
|
@ -6,7 +6,6 @@ import org.apache.logging.log4j.LogManager;
|
||||
import org.apache.logging.log4j.Logger;
|
||||
import org.ciyam.at.*;
|
||||
import org.qortal.account.Account;
|
||||
import org.qortal.api.resource.CrossChainUtils;
|
||||
import org.qortal.asset.Asset;
|
||||
import org.qortal.at.QortalFunctionCode;
|
||||
import org.qortal.crypto.Crypto;
|
||||
@ -22,7 +21,6 @@ import org.qortal.utils.BitTwiddling;
|
||||
import java.nio.ByteBuffer;
|
||||
import java.util.Arrays;
|
||||
import java.util.List;
|
||||
import java.util.OptionalLong;
|
||||
|
||||
import static org.ciyam.at.OpCode.calcOffset;
|
||||
|
||||
@ -571,14 +569,7 @@ public class DogecoinACCTv3 implements ACCT {
|
||||
@Override
|
||||
public CrossChainTradeData populateTradeData(Repository repository, ATData atData) throws DataException {
|
||||
ATStateData atStateData = repository.getATRepository().getLatestATState(atData.getATAddress());
|
||||
return populateTradeData(repository, atData.getCreatorPublicKey(), atData.getCreation(), atStateData, OptionalLong.empty());
|
||||
}
|
||||
|
||||
@Override
|
||||
public List<CrossChainTradeData> populateTradeDataList(Repository repository, List<ATData> atDataList) throws DataException {
|
||||
List<CrossChainTradeData> crossChainTradeDataList = CrossChainUtils.populateTradeDataList(repository, this, atDataList);
|
||||
|
||||
return crossChainTradeDataList;
|
||||
return populateTradeData(repository, atData.getCreatorPublicKey(), atData.getCreation(), atStateData);
|
||||
}
|
||||
|
||||
/**
|
||||
@ -587,14 +578,13 @@ public class DogecoinACCTv3 implements ACCT {
|
||||
@Override
|
||||
public CrossChainTradeData populateTradeData(Repository repository, ATStateData atStateData) throws DataException {
|
||||
ATData atData = repository.getATRepository().fromATAddress(atStateData.getATAddress());
|
||||
return populateTradeData(repository, atData.getCreatorPublicKey(), atData.getCreation(), atStateData, OptionalLong.empty());
|
||||
return populateTradeData(repository, atData.getCreatorPublicKey(), atData.getCreation(), atStateData);
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns CrossChainTradeData with useful info extracted from AT.
|
||||
*/
|
||||
@Override
|
||||
public CrossChainTradeData populateTradeData(Repository repository, byte[] creatorPublicKey, long creationTimestamp, ATStateData atStateData, OptionalLong optionalBalance) throws DataException {
|
||||
public CrossChainTradeData populateTradeData(Repository repository, byte[] creatorPublicKey, long creationTimestamp, ATStateData atStateData) throws DataException {
|
||||
byte[] addressBytes = new byte[25]; // for general use
|
||||
String atAddress = atStateData.getATAddress();
|
||||
|
||||
@ -607,13 +597,8 @@ public class DogecoinACCTv3 implements ACCT {
|
||||
tradeData.qortalCreator = Crypto.toAddress(creatorPublicKey);
|
||||
tradeData.creationTimestamp = creationTimestamp;
|
||||
|
||||
if(optionalBalance.isPresent()) {
|
||||
tradeData.qortBalance = optionalBalance.getAsLong();
|
||||
}
|
||||
else {
|
||||
Account atAccount = new Account(repository, atAddress);
|
||||
tradeData.qortBalance = atAccount.getConfirmedBalance(Asset.QORT);
|
||||
}
|
||||
Account atAccount = new Account(repository, atAddress);
|
||||
tradeData.qortBalance = atAccount.getConfirmedBalance(Asset.QORT);
|
||||
|
||||
byte[] stateData = atStateData.getStateData();
|
||||
ByteBuffer dataByteBuffer = ByteBuffer.wrap(stateData);
|
||||
|
@ -644,10 +644,8 @@ public class ElectrumX extends BitcoinyBlockchainProvider {
|
||||
}
|
||||
|
||||
/**
|
||||
* <p>Performs RPC call, with automatic reconnection to different server if needed.
|
||||
* </p>
|
||||
* @param method String representation of the RPC call value
|
||||
* @param params a list of Objects passed to the method of the Remote Server
|
||||
* Performs RPC call, with automatic reconnection to different server if needed.
|
||||
* <p>
|
||||
* @return "result" object from within JSON output
|
||||
* @throws ForeignBlockchainException if server returns error or something goes wrong
|
||||
*/
|
||||
|
@ -2,8 +2,6 @@ package org.qortal.crosschain;
|
||||
|
||||
public interface ForeignBlockchain {
|
||||
|
||||
public String getCurrencyCode();
|
||||
|
||||
public boolean isValidAddress(String address);
|
||||
|
||||
public boolean isValidWalletKey(String walletKey);
|
||||
|
@ -184,11 +184,6 @@ public class LegacyZcashAddress extends Address {
|
||||
return p2sh ? ScriptType.P2SH : ScriptType.P2PKH;
|
||||
}
|
||||
|
||||
@Override
|
||||
public int compareTo(Address address) {
|
||||
return this.toString().compareTo(address.toString());
|
||||
}
|
||||
|
||||
/**
|
||||
* Given an address, examines the version byte and attempts to find a matching NetworkParameters. If you aren't sure
|
||||
* which network the address is intended for (eg, it was provided by a user), you can use this to decide if it is
|
||||
|
@ -14,7 +14,6 @@ import java.util.Arrays;
|
||||
import java.util.Collection;
|
||||
import java.util.EnumMap;
|
||||
import java.util.Map;
|
||||
import java.util.concurrent.atomic.AtomicLong;
|
||||
|
||||
public class Litecoin extends Bitcoiny {
|
||||
|
||||
@ -64,7 +63,7 @@ public class Litecoin extends Bitcoiny {
|
||||
|
||||
@Override
|
||||
public long getP2shFee(Long timestamp) {
|
||||
return this.getFeeRequired();
|
||||
return this.getFeeCeiling();
|
||||
}
|
||||
},
|
||||
TEST3 {
|
||||
@ -117,14 +116,14 @@ public class Litecoin extends Bitcoiny {
|
||||
}
|
||||
};
|
||||
|
||||
private AtomicLong feeRequired = new AtomicLong(MAINNET_FEE);
|
||||
private long feeCeiling = MAINNET_FEE;
|
||||
|
||||
public long getFeeRequired() {
|
||||
return feeRequired.get();
|
||||
public long getFeeCeiling() {
|
||||
return feeCeiling;
|
||||
}
|
||||
|
||||
public void setFeeRequired(long feeRequired) {
|
||||
this.feeRequired.set(feeRequired);
|
||||
public void setFeeCeiling(long feeCeiling) {
|
||||
this.feeCeiling = feeCeiling;
|
||||
}
|
||||
|
||||
public abstract NetworkParameters getParams();
|
||||
@ -186,13 +185,13 @@ public class Litecoin extends Bitcoiny {
|
||||
}
|
||||
|
||||
@Override
|
||||
public long getFeeRequired() {
|
||||
return this.litecoinNet.getFeeRequired();
|
||||
public long getFeeCeiling() {
|
||||
return this.litecoinNet.getFeeCeiling();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void setFeeRequired(long fee) {
|
||||
public void setFeeCeiling(long fee) {
|
||||
|
||||
this.litecoinNet.setFeeRequired( fee );
|
||||
this.litecoinNet.setFeeCeiling( fee );
|
||||
}
|
||||
}
|
||||
|
@ -4,7 +4,6 @@ import com.google.common.hash.HashCode;
|
||||
import com.google.common.primitives.Bytes;
|
||||
import org.ciyam.at.*;
|
||||
import org.qortal.account.Account;
|
||||
import org.qortal.api.resource.CrossChainUtils;
|
||||
import org.qortal.asset.Asset;
|
||||
import org.qortal.at.QortalFunctionCode;
|
||||
import org.qortal.crypto.Crypto;
|
||||
@ -20,7 +19,6 @@ import org.qortal.utils.BitTwiddling;
|
||||
import java.nio.ByteBuffer;
|
||||
import java.util.Arrays;
|
||||
import java.util.List;
|
||||
import java.util.OptionalLong;
|
||||
|
||||
import static org.ciyam.at.OpCode.calcOffset;
|
||||
|
||||
@ -561,14 +559,7 @@ public class LitecoinACCTv1 implements ACCT {
|
||||
@Override
|
||||
public CrossChainTradeData populateTradeData(Repository repository, ATData atData) throws DataException {
|
||||
ATStateData atStateData = repository.getATRepository().getLatestATState(atData.getATAddress());
|
||||
return populateTradeData(repository, atData.getCreatorPublicKey(), atData.getCreation(), atStateData, OptionalLong.empty());
|
||||
}
|
||||
|
||||
@Override
|
||||
public List<CrossChainTradeData> populateTradeDataList(Repository repository, List<ATData> atDataList) throws DataException {
|
||||
List<CrossChainTradeData> crossChainTradeDataList = CrossChainUtils.populateTradeDataList(repository, this, atDataList);
|
||||
|
||||
return crossChainTradeDataList;
|
||||
return populateTradeData(repository, atData.getCreatorPublicKey(), atData.getCreation(), atStateData);
|
||||
}
|
||||
|
||||
/**
|
||||
@ -577,14 +568,13 @@ public class LitecoinACCTv1 implements ACCT {
|
||||
@Override
|
||||
public CrossChainTradeData populateTradeData(Repository repository, ATStateData atStateData) throws DataException {
|
||||
ATData atData = repository.getATRepository().fromATAddress(atStateData.getATAddress());
|
||||
return populateTradeData(repository, atData.getCreatorPublicKey(), atData.getCreation(), atStateData, OptionalLong.empty());
|
||||
return populateTradeData(repository, atData.getCreatorPublicKey(), atData.getCreation(), atStateData);
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns CrossChainTradeData with useful info extracted from AT.
|
||||
*/
|
||||
@Override
|
||||
public CrossChainTradeData populateTradeData(Repository repository, byte[] creatorPublicKey, long creationTimestamp, ATStateData atStateData, OptionalLong optionalBalance) throws DataException {
|
||||
public CrossChainTradeData populateTradeData(Repository repository, byte[] creatorPublicKey, long creationTimestamp, ATStateData atStateData) throws DataException {
|
||||
byte[] addressBytes = new byte[25]; // for general use
|
||||
String atAddress = atStateData.getATAddress();
|
||||
|
||||
@ -597,13 +587,8 @@ public class LitecoinACCTv1 implements ACCT {
|
||||
tradeData.qortalCreator = Crypto.toAddress(creatorPublicKey);
|
||||
tradeData.creationTimestamp = creationTimestamp;
|
||||
|
||||
if(optionalBalance.isPresent()) {
|
||||
tradeData.qortBalance = optionalBalance.getAsLong();
|
||||
}
|
||||
else {
|
||||
Account atAccount = new Account(repository, atAddress);
|
||||
tradeData.qortBalance = atAccount.getConfirmedBalance(Asset.QORT);
|
||||
}
|
||||
Account atAccount = new Account(repository, atAddress);
|
||||
tradeData.qortBalance = atAccount.getConfirmedBalance(Asset.QORT);
|
||||
|
||||
byte[] stateData = atStateData.getStateData();
|
||||
ByteBuffer dataByteBuffer = ByteBuffer.wrap(stateData);
|
||||
|
@ -4,7 +4,6 @@ import com.google.common.hash.HashCode;
|
||||
import com.google.common.primitives.Bytes;
|
||||
import org.ciyam.at.*;
|
||||
import org.qortal.account.Account;
|
||||
import org.qortal.api.resource.CrossChainUtils;
|
||||
import org.qortal.asset.Asset;
|
||||
import org.qortal.at.QortalFunctionCode;
|
||||
import org.qortal.crypto.Crypto;
|
||||
@ -20,7 +19,6 @@ import org.qortal.utils.BitTwiddling;
|
||||
import java.nio.ByteBuffer;
|
||||
import java.util.Arrays;
|
||||
import java.util.List;
|
||||
import java.util.OptionalLong;
|
||||
|
||||
import static org.ciyam.at.OpCode.calcOffset;
|
||||
|
||||
@ -564,14 +562,7 @@ public class LitecoinACCTv3 implements ACCT {
|
||||
@Override
|
||||
public CrossChainTradeData populateTradeData(Repository repository, ATData atData) throws DataException {
|
||||
ATStateData atStateData = repository.getATRepository().getLatestATState(atData.getATAddress());
|
||||
return populateTradeData(repository, atData.getCreatorPublicKey(), atData.getCreation(), atStateData, OptionalLong.empty());
|
||||
}
|
||||
|
||||
@Override
|
||||
public List<CrossChainTradeData> populateTradeDataList(Repository repository, List<ATData> atDataList) throws DataException {
|
||||
List<CrossChainTradeData> crossChainTradeDataList = CrossChainUtils.populateTradeDataList(repository, this, atDataList);
|
||||
|
||||
return crossChainTradeDataList;
|
||||
return populateTradeData(repository, atData.getCreatorPublicKey(), atData.getCreation(), atStateData);
|
||||
}
|
||||
|
||||
/**
|
||||
@ -580,14 +571,13 @@ public class LitecoinACCTv3 implements ACCT {
|
||||
@Override
|
||||
public CrossChainTradeData populateTradeData(Repository repository, ATStateData atStateData) throws DataException {
|
||||
ATData atData = repository.getATRepository().fromATAddress(atStateData.getATAddress());
|
||||
return populateTradeData(repository, atData.getCreatorPublicKey(), atData.getCreation(), atStateData, OptionalLong.empty());
|
||||
return populateTradeData(repository, atData.getCreatorPublicKey(), atData.getCreation(), atStateData);
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns CrossChainTradeData with useful info extracted from AT.
|
||||
*/
|
||||
@Override
|
||||
public CrossChainTradeData populateTradeData(Repository repository, byte[] creatorPublicKey, long creationTimestamp, ATStateData atStateData, OptionalLong optionalBalance) throws DataException {
|
||||
public CrossChainTradeData populateTradeData(Repository repository, byte[] creatorPublicKey, long creationTimestamp, ATStateData atStateData) throws DataException {
|
||||
byte[] addressBytes = new byte[25]; // for general use
|
||||
String atAddress = atStateData.getATAddress();
|
||||
|
||||
@ -600,13 +590,8 @@ public class LitecoinACCTv3 implements ACCT {
|
||||
tradeData.qortalCreator = Crypto.toAddress(creatorPublicKey);
|
||||
tradeData.creationTimestamp = creationTimestamp;
|
||||
|
||||
if(optionalBalance.isPresent()) {
|
||||
tradeData.qortBalance = optionalBalance.getAsLong();
|
||||
}
|
||||
else {
|
||||
Account atAccount = new Account(repository, atAddress);
|
||||
tradeData.qortBalance = atAccount.getConfirmedBalance(Asset.QORT);
|
||||
}
|
||||
Account atAccount = new Account(repository, atAddress);
|
||||
tradeData.qortBalance = atAccount.getConfirmedBalance(Asset.QORT);
|
||||
|
||||
byte[] stateData = atStateData.getStateData();
|
||||
ByteBuffer dataByteBuffer = ByteBuffer.wrap(stateData);
|
||||
|
@ -3,19 +3,18 @@ package org.qortal.crosschain;
|
||||
import org.bitcoinj.core.NetworkParameters;
|
||||
|
||||
import java.util.Collection;
|
||||
import java.util.concurrent.atomic.AtomicLong;
|
||||
|
||||
public class NetTBD {
|
||||
|
||||
private String name;
|
||||
private AtomicLong feeRequired;
|
||||
private long feeCeiling;
|
||||
private NetworkParameters params;
|
||||
private Collection<ElectrumX.Server> servers;
|
||||
private String genesisHash;
|
||||
|
||||
public NetTBD(String name, long feeRequired, NetworkParameters params, Collection<ElectrumX.Server> servers, String genesisHash) {
|
||||
public NetTBD(String name, long feeCeiling, NetworkParameters params, Collection<ElectrumX.Server> servers, String genesisHash) {
|
||||
this.name = name;
|
||||
this.feeRequired = new AtomicLong(feeRequired);
|
||||
this.feeCeiling = feeCeiling;
|
||||
this.params = params;
|
||||
this.servers = servers;
|
||||
this.genesisHash = genesisHash;
|
||||
@ -26,14 +25,14 @@ public class NetTBD {
|
||||
return this.name;
|
||||
}
|
||||
|
||||
public long getFeeRequired() {
|
||||
public long getFeeCeiling() {
|
||||
|
||||
return feeRequired.get();
|
||||
return feeCeiling;
|
||||
}
|
||||
|
||||
public void setFeeRequired(long feeRequired) {
|
||||
public void setFeeCeiling(long feeCeiling) {
|
||||
|
||||
this.feeRequired.set(feeRequired);
|
||||
this.feeCeiling = feeCeiling;
|
||||
}
|
||||
|
||||
public NetworkParameters getParams() {
|
||||
|
@ -21,7 +21,6 @@ import org.qortal.utils.BitTwiddling;
|
||||
|
||||
import java.nio.ByteBuffer;
|
||||
import java.util.*;
|
||||
import java.util.concurrent.atomic.AtomicLong;
|
||||
|
||||
public class PirateChain extends Bitcoiny {
|
||||
|
||||
@ -52,7 +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("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)
|
||||
);
|
||||
}
|
||||
|
||||
@ -63,7 +67,7 @@ public class PirateChain extends Bitcoiny {
|
||||
|
||||
@Override
|
||||
public long getP2shFee(Long timestamp) {
|
||||
return this.getFeeRequired();
|
||||
return this.getFeeCeiling();
|
||||
}
|
||||
},
|
||||
TEST3 {
|
||||
@ -113,14 +117,14 @@ public class PirateChain extends Bitcoiny {
|
||||
}
|
||||
};
|
||||
|
||||
private AtomicLong feeRequired = new AtomicLong(MAINNET_FEE);
|
||||
private long feeCeiling = MAINNET_FEE;
|
||||
|
||||
public long getFeeRequired() {
|
||||
return feeRequired.get();
|
||||
public long getFeeCeiling() {
|
||||
return feeCeiling;
|
||||
}
|
||||
|
||||
public void setFeeRequired(long feeRequired) {
|
||||
this.feeRequired.set(feeRequired);
|
||||
public void setFeeCeiling(long feeCeiling) {
|
||||
this.feeCeiling = feeCeiling;
|
||||
}
|
||||
|
||||
public abstract NetworkParameters getParams();
|
||||
@ -182,14 +186,14 @@ public class PirateChain extends Bitcoiny {
|
||||
}
|
||||
|
||||
@Override
|
||||
public long getFeeRequired() {
|
||||
return this.pirateChainNet.getFeeRequired();
|
||||
public long getFeeCeiling() {
|
||||
return this.pirateChainNet.getFeeCeiling();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void setFeeRequired(long fee) {
|
||||
public void setFeeCeiling(long fee) {
|
||||
|
||||
this.pirateChainNet.setFeeRequired( fee );
|
||||
this.pirateChainNet.setFeeCeiling( fee );
|
||||
}
|
||||
/**
|
||||
* Returns confirmed balance, based on passed payment script.
|
||||
|
@ -4,7 +4,6 @@ import com.google.common.hash.HashCode;
|
||||
import com.google.common.primitives.Bytes;
|
||||
import org.ciyam.at.*;
|
||||
import org.qortal.account.Account;
|
||||
import org.qortal.api.resource.CrossChainUtils;
|
||||
import org.qortal.asset.Asset;
|
||||
import org.qortal.at.QortalFunctionCode;
|
||||
import org.qortal.crypto.Crypto;
|
||||
@ -20,7 +19,6 @@ import org.qortal.utils.BitTwiddling;
|
||||
import java.nio.ByteBuffer;
|
||||
import java.util.Arrays;
|
||||
import java.util.List;
|
||||
import java.util.OptionalLong;
|
||||
|
||||
import static org.ciyam.at.OpCode.calcOffset;
|
||||
|
||||
@ -582,14 +580,7 @@ public class PirateChainACCTv3 implements ACCT {
|
||||
@Override
|
||||
public CrossChainTradeData populateTradeData(Repository repository, ATData atData) throws DataException {
|
||||
ATStateData atStateData = repository.getATRepository().getLatestATState(atData.getATAddress());
|
||||
return populateTradeData(repository, atData.getCreatorPublicKey(), atData.getCreation(), atStateData, OptionalLong.empty());
|
||||
}
|
||||
|
||||
@Override
|
||||
public List<CrossChainTradeData> populateTradeDataList(Repository repository, List<ATData> atDataList) throws DataException {
|
||||
List<CrossChainTradeData> crossChainTradeDataList = CrossChainUtils.populateTradeDataList(repository, this, atDataList);
|
||||
|
||||
return crossChainTradeDataList;
|
||||
return populateTradeData(repository, atData.getCreatorPublicKey(), atData.getCreation(), atStateData);
|
||||
}
|
||||
|
||||
/**
|
||||
@ -598,14 +589,13 @@ public class PirateChainACCTv3 implements ACCT {
|
||||
@Override
|
||||
public CrossChainTradeData populateTradeData(Repository repository, ATStateData atStateData) throws DataException {
|
||||
ATData atData = repository.getATRepository().fromATAddress(atStateData.getATAddress());
|
||||
return populateTradeData(repository, atData.getCreatorPublicKey(), atData.getCreation(), atStateData, OptionalLong.empty());
|
||||
return populateTradeData(repository, atData.getCreatorPublicKey(), atData.getCreation(), atStateData);
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns CrossChainTradeData with useful info extracted from AT.
|
||||
*/
|
||||
@Override
|
||||
public CrossChainTradeData populateTradeData(Repository repository, byte[] creatorPublicKey, long creationTimestamp, ATStateData atStateData, OptionalLong optionalBalance) throws DataException {
|
||||
public CrossChainTradeData populateTradeData(Repository repository, byte[] creatorPublicKey, long creationTimestamp, ATStateData atStateData) throws DataException {
|
||||
byte[] addressBytes = new byte[25]; // for general use
|
||||
String atAddress = atStateData.getATAddress();
|
||||
|
||||
@ -618,13 +608,8 @@ public class PirateChainACCTv3 implements ACCT {
|
||||
tradeData.qortalCreator = Crypto.toAddress(creatorPublicKey);
|
||||
tradeData.creationTimestamp = creationTimestamp;
|
||||
|
||||
if(optionalBalance.isPresent()) {
|
||||
tradeData.qortBalance = optionalBalance.getAsLong();
|
||||
}
|
||||
else {
|
||||
Account atAccount = new Account(repository, atAddress);
|
||||
tradeData.qortBalance = atAccount.getConfirmedBalance(Asset.QORT);
|
||||
}
|
||||
Account atAccount = new Account(repository, atAddress);
|
||||
tradeData.qortBalance = atAccount.getConfirmedBalance(Asset.QORT);
|
||||
|
||||
byte[] stateData = atStateData.getStateData();
|
||||
ByteBuffer dataByteBuffer = ByteBuffer.wrap(stateData);
|
||||
|
@ -8,7 +8,6 @@ import org.bouncycastle.util.encoders.DecoderException;
|
||||
import org.json.JSONArray;
|
||||
import org.json.JSONException;
|
||||
import org.json.JSONObject;
|
||||
import org.qortal.api.resource.CrossChainUtils;
|
||||
import org.qortal.controller.PirateChainWalletController;
|
||||
import org.qortal.crypto.Crypto;
|
||||
import org.qortal.settings.Settings;
|
||||
@ -68,8 +67,8 @@ public class PirateWallet {
|
||||
}
|
||||
|
||||
// Pick a random server
|
||||
ChainableServer server = PirateChain.getInstance().blockchainProvider.getCurrentServer();
|
||||
String serverUri = String.format("https://%s:%d/", server.getHostName(), server.getPort());
|
||||
PirateLightClient.Server server = this.getRandomServer();
|
||||
String serverUri = String.format("https://%s:%d/", server.hostname, server.port);
|
||||
|
||||
// Pirate library uses base64 encoding
|
||||
String entropy64 = Base64.toBase64String(this.entropyBytes);
|
||||
|
@ -14,7 +14,6 @@ import java.util.Arrays;
|
||||
import java.util.Collection;
|
||||
import java.util.EnumMap;
|
||||
import java.util.Map;
|
||||
import java.util.concurrent.atomic.AtomicLong;
|
||||
|
||||
public class Ravencoin extends Bitcoiny {
|
||||
|
||||
@ -62,7 +61,7 @@ public class Ravencoin extends Bitcoiny {
|
||||
|
||||
@Override
|
||||
public long getP2shFee(Long timestamp) {
|
||||
return this.getFeeRequired();
|
||||
return this.getFeeCeiling();
|
||||
}
|
||||
},
|
||||
TEST3 {
|
||||
@ -112,14 +111,14 @@ public class Ravencoin extends Bitcoiny {
|
||||
}
|
||||
};
|
||||
|
||||
private AtomicLong feeRequired = new AtomicLong( MAINNET_FEE );
|
||||
private long feeCeiling = MAINNET_FEE;
|
||||
|
||||
public long getFeeRequired() {
|
||||
return feeRequired.get();
|
||||
public long getFeeCeiling() {
|
||||
return feeCeiling;
|
||||
}
|
||||
|
||||
public void setFeeRequired(long feeRequired) {
|
||||
this.feeRequired.set(feeRequired);
|
||||
public void setFeeCeiling(long feeCeiling) {
|
||||
this.feeCeiling = feeCeiling;
|
||||
}
|
||||
|
||||
public abstract NetworkParameters getParams();
|
||||
@ -181,13 +180,13 @@ public class Ravencoin extends Bitcoiny {
|
||||
}
|
||||
|
||||
@Override
|
||||
public long getFeeRequired() {
|
||||
return this.ravencoinNet.getFeeRequired();
|
||||
public long getFeeCeiling() {
|
||||
return this.ravencoinNet.getFeeCeiling();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void setFeeRequired(long fee) {
|
||||
public void setFeeCeiling(long fee) {
|
||||
|
||||
this.ravencoinNet.setFeeRequired( fee );
|
||||
this.ravencoinNet.setFeeCeiling( fee );
|
||||
}
|
||||
}
|
||||
|
@ -6,7 +6,6 @@ import org.apache.logging.log4j.LogManager;
|
||||
import org.apache.logging.log4j.Logger;
|
||||
import org.ciyam.at.*;
|
||||
import org.qortal.account.Account;
|
||||
import org.qortal.api.resource.CrossChainUtils;
|
||||
import org.qortal.asset.Asset;
|
||||
import org.qortal.at.QortalFunctionCode;
|
||||
import org.qortal.crypto.Crypto;
|
||||
@ -22,7 +21,6 @@ import org.qortal.utils.BitTwiddling;
|
||||
import java.nio.ByteBuffer;
|
||||
import java.util.Arrays;
|
||||
import java.util.List;
|
||||
import java.util.OptionalLong;
|
||||
|
||||
import static org.ciyam.at.OpCode.calcOffset;
|
||||
|
||||
@ -571,14 +569,7 @@ public class RavencoinACCTv3 implements ACCT {
|
||||
@Override
|
||||
public CrossChainTradeData populateTradeData(Repository repository, ATData atData) throws DataException {
|
||||
ATStateData atStateData = repository.getATRepository().getLatestATState(atData.getATAddress());
|
||||
return populateTradeData(repository, atData.getCreatorPublicKey(), atData.getCreation(), atStateData, OptionalLong.empty());
|
||||
}
|
||||
|
||||
@Override
|
||||
public List<CrossChainTradeData> populateTradeDataList(Repository repository, List<ATData> atDataList) throws DataException {
|
||||
List<CrossChainTradeData> crossChainTradeDataList = CrossChainUtils.populateTradeDataList(repository, this, atDataList);
|
||||
|
||||
return crossChainTradeDataList;
|
||||
return populateTradeData(repository, atData.getCreatorPublicKey(), atData.getCreation(), atStateData);
|
||||
}
|
||||
|
||||
/**
|
||||
@ -587,14 +578,13 @@ public class RavencoinACCTv3 implements ACCT {
|
||||
@Override
|
||||
public CrossChainTradeData populateTradeData(Repository repository, ATStateData atStateData) throws DataException {
|
||||
ATData atData = repository.getATRepository().fromATAddress(atStateData.getATAddress());
|
||||
return populateTradeData(repository, atData.getCreatorPublicKey(), atData.getCreation(), atStateData, OptionalLong.empty());
|
||||
return populateTradeData(repository, atData.getCreatorPublicKey(), atData.getCreation(), atStateData);
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns CrossChainTradeData with useful info extracted from AT.
|
||||
*/
|
||||
@Override
|
||||
public CrossChainTradeData populateTradeData(Repository repository, byte[] creatorPublicKey, long creationTimestamp, ATStateData atStateData, OptionalLong optionalBalance) throws DataException {
|
||||
public CrossChainTradeData populateTradeData(Repository repository, byte[] creatorPublicKey, long creationTimestamp, ATStateData atStateData) throws DataException {
|
||||
byte[] addressBytes = new byte[25]; // for general use
|
||||
String atAddress = atStateData.getATAddress();
|
||||
|
||||
@ -607,13 +597,8 @@ public class RavencoinACCTv3 implements ACCT {
|
||||
tradeData.qortalCreator = Crypto.toAddress(creatorPublicKey);
|
||||
tradeData.creationTimestamp = creationTimestamp;
|
||||
|
||||
if(optionalBalance.isPresent()) {
|
||||
tradeData.qortBalance = optionalBalance.getAsLong();
|
||||
}
|
||||
else {
|
||||
Account atAccount = new Account(repository, atAddress);
|
||||
tradeData.qortBalance = atAccount.getConfirmedBalance(Asset.QORT);
|
||||
}
|
||||
Account atAccount = new Account(repository, atAddress);
|
||||
tradeData.qortBalance = atAccount.getConfirmedBalance(Asset.QORT);
|
||||
|
||||
byte[] stateData = atStateData.getStateData();
|
||||
ByteBuffer dataByteBuffer = ByteBuffer.wrap(stateData);
|
||||
|
@ -100,7 +100,7 @@ public class AES {
|
||||
// Prepend the output stream with the 16 byte initialization vector
|
||||
outputStream.write(iv.getIV());
|
||||
|
||||
byte[] buffer = new byte[65536];
|
||||
byte[] buffer = new byte[1024];
|
||||
int bytesRead;
|
||||
while ((bytesRead = inputStream.read(buffer)) != -1) {
|
||||
byte[] output = cipher.update(buffer, 0, bytesRead);
|
||||
@ -138,7 +138,7 @@ public class AES {
|
||||
Cipher cipher = Cipher.getInstance(algorithm);
|
||||
cipher.init(Cipher.DECRYPT_MODE, key, new IvParameterSpec(iv));
|
||||
|
||||
byte[] buffer = new byte[65536];
|
||||
byte[] buffer = new byte[64];
|
||||
int bytesRead;
|
||||
while ((bytesRead = inputStream.read(buffer)) != -1) {
|
||||
byte[] output = cipher.update(buffer, 0, bytesRead);
|
||||
|
@ -1,54 +0,0 @@
|
||||
package org.qortal.data.account;
|
||||
|
||||
import javax.xml.bind.annotation.XmlAccessType;
|
||||
import javax.xml.bind.annotation.XmlAccessorType;
|
||||
import javax.xml.bind.annotation.adapters.XmlJavaTypeAdapter;
|
||||
import java.util.Objects;
|
||||
|
||||
// All properties to be converted to JSON via JAXB
|
||||
@XmlAccessorType(XmlAccessType.FIELD)
|
||||
public class AddressAmountData {
|
||||
|
||||
private String address;
|
||||
|
||||
@XmlJavaTypeAdapter(value = org.qortal.api.AmountTypeAdapter.class)
|
||||
private long amount;
|
||||
|
||||
public AddressAmountData() {
|
||||
}
|
||||
|
||||
public AddressAmountData(String address, long amount) {
|
||||
|
||||
this.address = address;
|
||||
this.amount = amount;
|
||||
}
|
||||
|
||||
public String getAddress() {
|
||||
return address;
|
||||
}
|
||||
|
||||
public long getAmount() {
|
||||
return amount;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean equals(Object o) {
|
||||
if (this == o) return true;
|
||||
if (o == null || getClass() != o.getClass()) return false;
|
||||
AddressAmountData that = (AddressAmountData) o;
|
||||
return amount == that.amount && Objects.equals(address, that.address);
|
||||
}
|
||||
|
||||
@Override
|
||||
public int hashCode() {
|
||||
return Objects.hash(address, amount);
|
||||
}
|
||||
|
||||
@Override
|
||||
public String toString() {
|
||||
return "AddressAmountData{" +
|
||||
"address='" + address + '\'' +
|
||||
", amount=" + amount +
|
||||
'}';
|
||||
}
|
||||
}
|
@ -1,59 +0,0 @@
|
||||
package org.qortal.data.account;
|
||||
|
||||
import javax.xml.bind.annotation.XmlAccessType;
|
||||
import javax.xml.bind.annotation.XmlAccessorType;
|
||||
import java.util.Objects;
|
||||
|
||||
// All properties to be converted to JSON via JAXB
|
||||
@XmlAccessorType(XmlAccessType.FIELD)
|
||||
public class BlockHeightRange {
|
||||
|
||||
private int begin;
|
||||
|
||||
private int end;
|
||||
|
||||
private boolean isRewardDistribution;
|
||||
|
||||
public BlockHeightRange() {
|
||||
}
|
||||
|
||||
public BlockHeightRange(int begin, int end, boolean isRewardDistribution) {
|
||||
this.begin = begin;
|
||||
this.end = end;
|
||||
this.isRewardDistribution = isRewardDistribution;
|
||||
}
|
||||
|
||||
public int getBegin() {
|
||||
return begin;
|
||||
}
|
||||
|
||||
public int getEnd() {
|
||||
return end;
|
||||
}
|
||||
|
||||
public boolean isRewardDistribution() {
|
||||
return isRewardDistribution;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean equals(Object o) {
|
||||
if (this == o) return true;
|
||||
if (o == null || getClass() != o.getClass()) return false;
|
||||
BlockHeightRange that = (BlockHeightRange) o;
|
||||
return begin == that.begin && end == that.end;
|
||||
}
|
||||
|
||||
@Override
|
||||
public int hashCode() {
|
||||
return Objects.hash(begin, end);
|
||||
}
|
||||
|
||||
@Override
|
||||
public String toString() {
|
||||
return "BlockHeightRange{" +
|
||||
"begin=" + begin +
|
||||
", end=" + end +
|
||||
", isRewardDistribution=" + isRewardDistribution +
|
||||
'}';
|
||||
}
|
||||
}
|
Some files were not shown because too many files have changed in this diff Show More
Loading…
x
Reference in New Issue
Block a user