forked from Qortal/qortal
Compare commits
44 Commits
Author | SHA1 | Date | |
---|---|---|---|
|
a02d1cec75 | ||
|
2d9f1d6d81 | ||
|
c1da495dd1 | ||
|
533df9f2b8 | ||
|
44b4b08117 | ||
|
a140805c36 | ||
|
ea1d4dd962 | ||
|
a6cbdaafd8 | ||
|
5db808e300 | ||
|
cc95106019 | ||
|
64537ad705 | ||
|
806dc6d056 | ||
|
d58fbab1b5 | ||
|
61ede811cd | ||
|
95d42db773 | ||
|
ef07a444d6 | ||
|
5910ceff80 | ||
|
f916d3581b | ||
|
498f409346 | ||
|
d54f840265 | ||
|
cc740cceec | ||
|
df39819de0 | ||
|
e83b2263f0 | ||
|
5c90c4bc61 | ||
|
f7793443f3 | ||
|
f6b91df7b6 | ||
|
92d589a1ca | ||
|
c4a7fb3b92 | ||
|
2d27901f9f | ||
|
ce8fb002cc | ||
|
3f29116b47 | ||
|
d05359dfa9 | ||
|
587b063e6a | ||
|
9e001dfc16 | ||
|
55f941467f | ||
|
4f9a4a2091 | ||
|
d579606d2d | ||
|
dcedcf8685 | ||
|
f78764880c | ||
|
6051b85e52 | ||
|
9c62740f44 | ||
|
21c45535be | ||
|
15ae32efd9 | ||
|
03ba36729b |
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>
|
@@ -3,13 +3,14 @@
|
||||
<groupId>org.ciyam</groupId>
|
||||
<artifactId>AT</artifactId>
|
||||
<versioning>
|
||||
<release>1.4.1</release>
|
||||
<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>20231212092227</lastUpdated>
|
||||
<lastUpdated>20240426084210</lastUpdated>
|
||||
</versioning>
|
||||
</metadata>
|
||||
|
88
pom.xml
88
pom.xml
@@ -3,58 +3,60 @@
|
||||
<modelVersion>4.0.0</modelVersion>
|
||||
<groupId>org.qortal</groupId>
|
||||
<artifactId>qortal</artifactId>
|
||||
<version>4.5.0</version>
|
||||
<version>4.5.2</version>
|
||||
<packaging>jar</packaging>
|
||||
<properties>
|
||||
<project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
|
||||
<skipTests>true</skipTests>
|
||||
|
||||
<altcoinj.version>7dc8c6f</altcoinj.version>
|
||||
<bitcoinj.version>0.15.10</bitcoinj.version>
|
||||
<bouncycastle.version>1.69</bouncycastle.version>
|
||||
<build-helper-maven-plugin.version>3.5.0</build-helper-maven-plugin.version>
|
||||
<bouncycastle.version>1.70</bouncycastle.version>
|
||||
<build.timestamp>${maven.build.timestamp}</build.timestamp>
|
||||
<ciyam-at.version>1.4.1</ciyam-at.version>
|
||||
<ciyam-at.version>1.4.2</ciyam-at.version>
|
||||
<commons-net.version>3.8.0</commons-net.version>
|
||||
<commons-text.version>1.11.0</commons-text.version>
|
||||
<commons-io.version>2.11.0</commons-io.version>
|
||||
<commons-compress.version>1.25.0</commons-compress.version>
|
||||
<commons-text.version>1.12.0</commons-text.version>
|
||||
<commons-io.version>2.16.1</commons-io.version>
|
||||
<commons-compress.version>1.26.2</commons-compress.version>
|
||||
<commons-lang3.version>3.14.0</commons-lang3.version>
|
||||
<dagger.version>1.2.2</dagger.version>
|
||||
<extendedset.version>0.12.3</extendedset.version>
|
||||
<git-commit-id-plugin.version>4.9.10</git-commit-id-plugin.version>
|
||||
<grpc.version>1.61.0</grpc.version>
|
||||
<guava.version>33.0.0-jre</guava.version>
|
||||
<grpc.version>1.65.0</grpc.version>
|
||||
<guava.version>33.2.1-jre</guava.version>
|
||||
<hamcrest-library.version>2.2</hamcrest-library.version>
|
||||
<homoglyph.version>1.2.1</homoglyph.version>
|
||||
<hsqldb.version>2.5.1</hsqldb.version>
|
||||
<icu4j.version>74.2</icu4j.version>
|
||||
<icu4j.version>75.1</icu4j.version>
|
||||
<java-diff-utils.version>4.12</java-diff-utils.version>
|
||||
<javax.servlet-api.version>4.0.1</javax.servlet-api.version>
|
||||
<jaxb-runtime.version>2.3.9</jaxb-runtime.version>
|
||||
<jersey.version>2.41</jersey.version>
|
||||
<jetty.version>9.4.53.v20231009</jetty.version>
|
||||
<jersey.version>2.42</jersey.version>
|
||||
<jetty.version>9.4.54.v20240208</jetty.version>
|
||||
<json-simple.version>1.1.1</json-simple.version>
|
||||
<json.version>20231013</json.version>
|
||||
<json.version>20240303</json.version>
|
||||
<jsoup.version>1.17.2</jsoup.version>
|
||||
<junit-jupiter-engine.version>5.10.0</junit-jupiter-engine.version>
|
||||
<junit-jupiter-engine.version>5.11.0-M2</junit-jupiter-engine.version>
|
||||
<lifecycle-mapping.version>1.0.0</lifecycle-mapping.version>
|
||||
<log4j.version>2.22.1</log4j.version>
|
||||
<log4j.version>2.23.1</log4j.version>
|
||||
<mail.version>1.5.0-b01</mail.version>
|
||||
<maven-compiler-plugin.version>3.12.1</maven-compiler-plugin.version>
|
||||
<maven-jar-plugin.version>3.3.0</maven-jar-plugin.version>
|
||||
<maven-build-helper-plugin.version>3.6.0</maven-build-helper-plugin.version>
|
||||
<maven-compiler-plugin.version>3.13.0</maven-compiler-plugin.version>
|
||||
<maven-dependency-plugin.version>3.6.1</maven-dependency-plugin.version>
|
||||
<maven-jar-plugin.version>3.4.2</maven-jar-plugin.version>
|
||||
<maven-package-info-plugin.version>1.1.0</maven-package-info-plugin.version>
|
||||
<maven-plugin.version>2.16.2</maven-plugin.version>
|
||||
<maven-reproducible-build-plugin.version>0.16</maven-reproducible-build-plugin.version>
|
||||
<maven-resources-plugin.version>3.3.1</maven-resources-plugin.version>
|
||||
<maven-shade-plugin.version>3.5.1</maven-shade-plugin.version>
|
||||
<maven-surefire-plugin.version>3.2.5</maven-surefire-plugin.version>
|
||||
<package-info-maven-plugin.version>1.1.0</package-info-maven-plugin.version>
|
||||
<project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
|
||||
<protobuf.version>3.25.1</protobuf.version>
|
||||
<maven-shade-plugin.version>3.6.0</maven-shade-plugin.version>
|
||||
<maven-surefire-plugin.version>3.3.0</maven-surefire-plugin.version>
|
||||
<protobuf.version>3.25.3</protobuf.version>
|
||||
<replacer.version>1.5.3</replacer.version>
|
||||
<reproducible-build-maven-plugin.version>0.16</reproducible-build-maven-plugin.version>
|
||||
<simplemagic.version>1.17</simplemagic.version>
|
||||
<slf4j.version>1.7.36</slf4j.version>
|
||||
<swagger-api.version>2.0.10</swagger-api.version>
|
||||
<swagger-ui.version>5.10.3</swagger-ui.version>
|
||||
<swagger-ui.version>5.17.14</swagger-ui.version>
|
||||
<upnp.version>1.2</upnp.version>
|
||||
<versions-maven-plugin.version>2.16.2</versions-maven-plugin.version>
|
||||
<xz.version>1.9</xz.version>
|
||||
</properties>
|
||||
<build>
|
||||
@@ -70,7 +72,7 @@
|
||||
<plugin>
|
||||
<groupId>org.codehaus.mojo</groupId>
|
||||
<artifactId>versions-maven-plugin</artifactId>
|
||||
<version>${versions-maven-plugin.version}</version>
|
||||
<version>${maven-plugin.version}</version>
|
||||
<configuration>
|
||||
<generateBackupPoms>false</generateBackupPoms>
|
||||
</configuration>
|
||||
@@ -238,7 +240,7 @@
|
||||
<plugin>
|
||||
<groupId>com.github.bohnman</groupId>
|
||||
<artifactId>package-info-maven-plugin</artifactId>
|
||||
<version>${package-info-maven-plugin.version}</version>
|
||||
<version>${maven-package-info-plugin.version}</version>
|
||||
<configuration>
|
||||
<packages>
|
||||
<package>
|
||||
@@ -268,7 +270,7 @@
|
||||
<plugin>
|
||||
<groupId>org.codehaus.mojo</groupId>
|
||||
<artifactId>build-helper-maven-plugin</artifactId>
|
||||
<version>${build-helper-maven-plugin.version}</version>
|
||||
<version>${maven-build-helper-plugin.version}</version>
|
||||
<executions>
|
||||
<execution>
|
||||
<phase>generate-sources</phase>
|
||||
@@ -353,7 +355,7 @@
|
||||
<plugin>
|
||||
<groupId>io.github.zlika</groupId>
|
||||
<artifactId>reproducible-build-maven-plugin</artifactId>
|
||||
<version>${reproducible-build-maven-plugin.version}</version>
|
||||
<version>${maven-reproducible-build-plugin.version}</version>
|
||||
<executions>
|
||||
<execution>
|
||||
<phase>package</phase>
|
||||
@@ -388,15 +390,9 @@
|
||||
<pluginExecutions>
|
||||
<pluginExecution>
|
||||
<pluginExecutionFilter>
|
||||
<groupId>
|
||||
org.apache.maven.plugins
|
||||
</groupId>
|
||||
<artifactId>
|
||||
maven-dependency-plugin
|
||||
</artifactId>
|
||||
<versionRange>
|
||||
[3.6.0,)
|
||||
</versionRange>
|
||||
<groupId>org.apache.maven.plugins</groupId>
|
||||
<artifactId>maven-dependency-plugin</artifactId>
|
||||
<version>${maven-dependency-plugin.version}</version>
|
||||
<goals>
|
||||
<goal>unpack</goal>
|
||||
</goals>
|
||||
@@ -407,15 +403,9 @@
|
||||
</pluginExecution>
|
||||
<pluginExecution>
|
||||
<pluginExecutionFilter>
|
||||
<groupId>
|
||||
com.google.code.maven-replacer-plugin
|
||||
</groupId>
|
||||
<artifactId>
|
||||
replacer
|
||||
</artifactId>
|
||||
<versionRange>
|
||||
[1.5.3,)
|
||||
</versionRange>
|
||||
<groupId>com.google.code.maven-replacer-plugin</groupId>
|
||||
<artifactId>replacer</artifactId>
|
||||
<version>${replacer.version}</version>
|
||||
<goals>
|
||||
<goal>replace</goal>
|
||||
</goals>
|
||||
@@ -448,7 +438,7 @@
|
||||
<dependency>
|
||||
<groupId>org.codehaus.mojo</groupId>
|
||||
<artifactId>build-helper-maven-plugin</artifactId>
|
||||
<version>${build-helper-maven-plugin.version}</version>
|
||||
<version>${maven-build-helper-plugin.version}</version>
|
||||
<scope>provided</scope>
|
||||
<!-- needed for build, not for runtime -->
|
||||
</dependency>
|
||||
@@ -456,7 +446,7 @@
|
||||
<dependency>
|
||||
<groupId>com.github.bohnman</groupId>
|
||||
<artifactId>package-info-maven-plugin</artifactId>
|
||||
<version>${package-info-maven-plugin.version}</version>
|
||||
<version>${maven-package-info-plugin.version}</version>
|
||||
<scope>provided</scope>
|
||||
<!-- needed for build, not for runtime -->
|
||||
</dependency>
|
||||
|
@@ -5290,7 +5290,7 @@ public final class Service {
|
||||
if (!com.google.protobuf.GeneratedMessageV3.isStringEmpty(vendor_)) {
|
||||
com.google.protobuf.GeneratedMessageV3.writeString(output, 2, vendor_);
|
||||
}
|
||||
if (taddrSupport_ != false) {
|
||||
if (taddrSupport_) {
|
||||
output.writeBool(3, taddrSupport_);
|
||||
}
|
||||
if (!com.google.protobuf.GeneratedMessageV3.isStringEmpty(chainName_)) {
|
||||
@@ -5341,7 +5341,7 @@ public final class Service {
|
||||
if (!com.google.protobuf.GeneratedMessageV3.isStringEmpty(vendor_)) {
|
||||
size += com.google.protobuf.GeneratedMessageV3.computeStringSize(2, vendor_);
|
||||
}
|
||||
if (taddrSupport_ != false) {
|
||||
if (taddrSupport_) {
|
||||
size += com.google.protobuf.CodedOutputStream
|
||||
.computeBoolSize(3, taddrSupport_);
|
||||
}
|
||||
@@ -5729,7 +5729,7 @@ public final class Service {
|
||||
vendor_ = other.vendor_;
|
||||
onChanged();
|
||||
}
|
||||
if (other.getTaddrSupport() != false) {
|
||||
if (other.getTaddrSupport()) {
|
||||
setTaddrSupport(other.getTaddrSupport());
|
||||
}
|
||||
if (!other.getChainName().isEmpty()) {
|
||||
|
@@ -340,7 +340,7 @@ public class SelfSponsorshipAlgoV1 {
|
||||
return true;
|
||||
}
|
||||
transactionDataList.removeIf(t -> t.getTimestamp() >= this.snapshotTimestamp);
|
||||
return transactionDataList.size() == 0;
|
||||
return transactionDataList.isEmpty();
|
||||
}
|
||||
|
||||
private static List<TransactionData> fetchTransactions(Repository repository, List<TransactionType> txTypes, String address, boolean reverse) throws DataException {
|
||||
|
249
src/main/java/org/qortal/account/SelfSponsorshipAlgoV2.java
Normal file
249
src/main/java/org/qortal/account/SelfSponsorshipAlgoV2.java
Normal file
@@ -0,0 +1,249 @@
|
||||
package org.qortal.account;
|
||||
|
||||
import org.qortal.api.resource.TransactionsResource;
|
||||
import org.qortal.asset.Asset;
|
||||
import org.qortal.block.BlockChain;
|
||||
import org.qortal.data.account.AccountData;
|
||||
import org.qortal.data.transaction.*;
|
||||
import org.qortal.repository.DataException;
|
||||
import org.qortal.repository.Repository;
|
||||
import org.qortal.transaction.Transaction.TransactionType;
|
||||
|
||||
import java.util.*;
|
||||
|
||||
public class SelfSponsorshipAlgoV2 {
|
||||
|
||||
private final long snapshotTimestampV1 = BlockChain.getInstance().getSelfSponsorshipAlgoV1SnapshotTimestamp();
|
||||
private final long snapshotTimestampV2 = BlockChain.getInstance().getSelfSponsorshipAlgoV2SnapshotTimestamp();
|
||||
private final long referenceTimestamp = BlockChain.getInstance().getReferenceTimestampBlock();
|
||||
|
||||
private final boolean override;
|
||||
private final Repository repository;
|
||||
private final String address;
|
||||
|
||||
private int recentAssetSendCount = 0;
|
||||
private int recentSponsorshipCount = 0;
|
||||
|
||||
private final Set<String> assetAddresses = new LinkedHashSet<>();
|
||||
private final Set<String> penaltyAddresses = new LinkedHashSet<>();
|
||||
private final Set<String> sponsorAddresses = new LinkedHashSet<>();
|
||||
|
||||
private List<RewardShareTransactionData> sponsorshipRewardShares = new ArrayList<>();
|
||||
private List<TransferAssetTransactionData> transferAssetForAddress = new ArrayList<>();
|
||||
|
||||
public SelfSponsorshipAlgoV2(Repository repository, String address, boolean override) {
|
||||
this.repository = repository;
|
||||
this.address = address;
|
||||
this.override = override;
|
||||
}
|
||||
|
||||
public String getAddress() {
|
||||
return this.address;
|
||||
}
|
||||
|
||||
public Set<String> getPenaltyAddresses() {
|
||||
return this.penaltyAddresses;
|
||||
}
|
||||
|
||||
public void run() throws DataException {
|
||||
if (!override) {
|
||||
this.getAccountPrivs(this.address);
|
||||
}
|
||||
|
||||
if (override) {
|
||||
this.fetchTransferAssetForAddress(this.address);
|
||||
this.findRecentAssetSendCount();
|
||||
|
||||
if (this.recentAssetSendCount >= 6) {
|
||||
this.penaltyAddresses.add(this.address);
|
||||
this.penaltyAddresses.addAll(this.assetAddresses);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private void getAccountPrivs(String address) throws DataException {
|
||||
AccountData accountData = this.repository.getAccountRepository().getAccount(address);
|
||||
List<TransactionData> transferPrivsTransactions = fetchTransferPrivsForAddress(address);
|
||||
transferPrivsTransactions.removeIf(t -> t.getTimestamp() > this.referenceTimestamp || accountData.getAddress().equals(t.getRecipient()));
|
||||
|
||||
if (transferPrivsTransactions.isEmpty()) {
|
||||
// Nothing to do
|
||||
return;
|
||||
}
|
||||
|
||||
for (TransactionData transactionData : transferPrivsTransactions) {
|
||||
TransferPrivsTransactionData transferPrivsTransactionData = (TransferPrivsTransactionData) transactionData;
|
||||
this.penaltyAddresses.add(transferPrivsTransactionData.getRecipient());
|
||||
this.fetchSponsorshipRewardShares(transferPrivsTransactionData.getRecipient());
|
||||
this.findRecentSponsorshipCount();
|
||||
|
||||
if (this.recentSponsorshipCount >= 1) {
|
||||
this.penaltyAddresses.addAll(this.sponsorAddresses);
|
||||
}
|
||||
|
||||
String newAddress = this.getDestinationAccount(transferPrivsTransactionData.getRecipient());
|
||||
|
||||
while (newAddress != null) {
|
||||
// Found destination account
|
||||
this.penaltyAddresses.add(newAddress);
|
||||
this.fetchSponsorshipRewardShares(newAddress);
|
||||
this.findRecentSponsorshipCount();
|
||||
|
||||
if (this.recentSponsorshipCount >= 1) {
|
||||
this.penaltyAddresses.addAll(this.sponsorAddresses);
|
||||
}
|
||||
|
||||
newAddress = this.getDestinationAccount(newAddress);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private String getDestinationAccount(String address) throws DataException {
|
||||
AccountData accountData = this.repository.getAccountRepository().getAccount(address);
|
||||
List<TransactionData> transferPrivsTransactions = fetchTransferPrivsForAddress(address);
|
||||
transferPrivsTransactions.removeIf(t -> t.getTimestamp() > this.referenceTimestamp || accountData.getAddress().equals(t.getRecipient()));
|
||||
|
||||
if (transferPrivsTransactions.isEmpty()) {
|
||||
return null;
|
||||
}
|
||||
|
||||
if (accountData == null) {
|
||||
return null;
|
||||
}
|
||||
|
||||
for (TransactionData transactionData : transferPrivsTransactions) {
|
||||
TransferPrivsTransactionData transferPrivsTransactionData = (TransferPrivsTransactionData) transactionData;
|
||||
if (Arrays.equals(transferPrivsTransactionData.getSenderPublicKey(), accountData.getPublicKey())) {
|
||||
return transferPrivsTransactionData.getRecipient();
|
||||
}
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
private void fetchSponsorshipRewardShares(String address) throws DataException {
|
||||
AccountData accountDataRs = this.repository.getAccountRepository().getAccount(address);
|
||||
List<RewardShareTransactionData> sponsorshipRewardShares = new ArrayList<>();
|
||||
|
||||
// Define relevant transactions
|
||||
List<TransactionType> txTypes = List.of(TransactionType.REWARD_SHARE);
|
||||
List<TransactionData> transactionDataList = fetchTransactions(repository, txTypes, address, false);
|
||||
|
||||
for (TransactionData transactionData : transactionDataList) {
|
||||
if (transactionData.getType() != TransactionType.REWARD_SHARE) {
|
||||
continue;
|
||||
}
|
||||
|
||||
RewardShareTransactionData rewardShareTransactionData = (RewardShareTransactionData) transactionData;
|
||||
|
||||
// Skip removals
|
||||
if (rewardShareTransactionData.getSharePercent() < 0) {
|
||||
continue;
|
||||
}
|
||||
|
||||
// Skip if not sponsored by this account
|
||||
if (!Arrays.equals(rewardShareTransactionData.getCreatorPublicKey(), accountDataRs.getPublicKey())) {
|
||||
continue;
|
||||
}
|
||||
|
||||
// Skip self shares
|
||||
if (Objects.equals(rewardShareTransactionData.getRecipient(), address)) {
|
||||
continue;
|
||||
}
|
||||
|
||||
boolean duplicateFound = false;
|
||||
for (RewardShareTransactionData existingRewardShare : sponsorshipRewardShares) {
|
||||
if (Objects.equals(existingRewardShare.getRecipient(), rewardShareTransactionData.getRecipient())) {
|
||||
// Duplicate
|
||||
duplicateFound = true;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
if (!duplicateFound) {
|
||||
sponsorshipRewardShares.add(rewardShareTransactionData);
|
||||
this.sponsorAddresses.add(rewardShareTransactionData.getRecipient());
|
||||
}
|
||||
}
|
||||
|
||||
this.sponsorshipRewardShares = sponsorshipRewardShares;
|
||||
}
|
||||
|
||||
private void fetchTransferAssetForAddress(String address) throws DataException {
|
||||
List<TransferAssetTransactionData> transferAssetForAddress = new ArrayList<>();
|
||||
|
||||
// Define relevant transactions
|
||||
List<TransactionType> txTypes = List.of(TransactionType.TRANSFER_ASSET);
|
||||
List<TransactionData> transactionDataList = fetchTransactions(repository, txTypes, address, false);
|
||||
transactionDataList.removeIf(t -> t.getTimestamp() <= this.snapshotTimestampV1 || t.getTimestamp() >= this.snapshotTimestampV2);
|
||||
|
||||
for (TransactionData transactionData : transactionDataList) {
|
||||
if (transactionData.getType() != TransactionType.TRANSFER_ASSET) {
|
||||
continue;
|
||||
}
|
||||
|
||||
TransferAssetTransactionData transferAssetTransactionData = (TransferAssetTransactionData) transactionData;
|
||||
|
||||
if (transferAssetTransactionData.getAssetId() == Asset.QORT) {
|
||||
if (!Objects.equals(transferAssetTransactionData.getRecipient(), address)) {
|
||||
// Outgoing transfer asset for this account
|
||||
transferAssetForAddress.add(transferAssetTransactionData);
|
||||
this.assetAddresses.add(transferAssetTransactionData.getRecipient());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
this.transferAssetForAddress = transferAssetForAddress;
|
||||
}
|
||||
|
||||
private void findRecentSponsorshipCount() {
|
||||
int recentSponsorshipCount = 0;
|
||||
|
||||
for (RewardShareTransactionData rewardShare : sponsorshipRewardShares) {
|
||||
if (rewardShare.getTimestamp() >= this.snapshotTimestampV1) {
|
||||
recentSponsorshipCount++;
|
||||
}
|
||||
}
|
||||
|
||||
this.recentSponsorshipCount = recentSponsorshipCount;
|
||||
}
|
||||
|
||||
private void findRecentAssetSendCount() {
|
||||
int recentAssetSendCount = 0;
|
||||
|
||||
for (TransferAssetTransactionData assetSend : transferAssetForAddress) {
|
||||
if (assetSend.getTimestamp() >= this.snapshotTimestampV1) {
|
||||
recentAssetSendCount++;
|
||||
}
|
||||
}
|
||||
|
||||
this.recentAssetSendCount = recentAssetSendCount;
|
||||
}
|
||||
|
||||
private List<TransactionData> fetchTransferPrivsForAddress(String address) throws DataException {
|
||||
return fetchTransactions(repository,
|
||||
List.of(TransactionType.TRANSFER_PRIVS),
|
||||
address, true);
|
||||
}
|
||||
|
||||
private static List<TransactionData> fetchTransactions(Repository repository, List<TransactionType> txTypes, String address, boolean reverse) throws DataException {
|
||||
// Fetch all relevant transactions for this account
|
||||
List<byte[]> signatures = repository.getTransactionRepository()
|
||||
.getSignaturesMatchingCriteria(null, null, null, txTypes,
|
||||
null, null, address, TransactionsResource.ConfirmationStatus.CONFIRMED,
|
||||
null, null, reverse);
|
||||
|
||||
List<TransactionData> transactionDataList = new ArrayList<>();
|
||||
|
||||
for (byte[] signature : signatures) {
|
||||
// Fetch transaction data
|
||||
TransactionData transactionData = repository.getTransactionRepository().fromSignature(signature);
|
||||
if (transactionData == null) {
|
||||
continue;
|
||||
}
|
||||
transactionDataList.add(transactionData);
|
||||
}
|
||||
|
||||
return transactionDataList;
|
||||
}
|
||||
}
|
370
src/main/java/org/qortal/account/SelfSponsorshipAlgoV3.java
Normal file
370
src/main/java/org/qortal/account/SelfSponsorshipAlgoV3.java
Normal file
@@ -0,0 +1,370 @@
|
||||
package org.qortal.account;
|
||||
|
||||
import org.qortal.api.resource.TransactionsResource;
|
||||
import org.qortal.asset.Asset;
|
||||
import org.qortal.data.account.AccountData;
|
||||
import org.qortal.data.naming.NameData;
|
||||
import org.qortal.data.transaction.*;
|
||||
import org.qortal.repository.DataException;
|
||||
import org.qortal.repository.Repository;
|
||||
import org.qortal.transaction.Transaction.TransactionType;
|
||||
|
||||
import java.util.*;
|
||||
import java.util.stream.Collectors;
|
||||
|
||||
public class SelfSponsorshipAlgoV3 {
|
||||
|
||||
private final Repository repository;
|
||||
private final String address;
|
||||
private final AccountData accountData;
|
||||
private final long snapshotTimestampV1;
|
||||
private final long snapshotTimestampV3;
|
||||
private final boolean override;
|
||||
|
||||
private int registeredNameCount = 0;
|
||||
private int suspiciousCount = 0;
|
||||
private int suspiciousPercent = 0;
|
||||
private int consolidationCount = 0;
|
||||
private int bulkIssuanceCount = 0;
|
||||
private int recentSponsorshipCount = 0;
|
||||
|
||||
private List<RewardShareTransactionData> sponsorshipRewardShares = new ArrayList<>();
|
||||
private final Map<String, List<TransactionData>> paymentsByAddress = new HashMap<>();
|
||||
private final Set<String> sponsees = new LinkedHashSet<>();
|
||||
private Set<String> consolidatedAddresses = new LinkedHashSet<>();
|
||||
private final Set<String> zeroTransactionAddreses = new LinkedHashSet<>();
|
||||
private final Set<String> penaltyAddresses = new LinkedHashSet<>();
|
||||
|
||||
public SelfSponsorshipAlgoV3(Repository repository, String address, long snapshotTimestampV1, long snapshotTimestampV3, boolean override) throws DataException {
|
||||
this.repository = repository;
|
||||
this.address = address;
|
||||
this.accountData = this.repository.getAccountRepository().getAccount(this.address);
|
||||
this.snapshotTimestampV1 = snapshotTimestampV1;
|
||||
this.snapshotTimestampV3 = snapshotTimestampV3;
|
||||
this.override = override;
|
||||
}
|
||||
|
||||
public String getAddress() {
|
||||
return this.address;
|
||||
}
|
||||
|
||||
public Set<String> getPenaltyAddresses() {
|
||||
return this.penaltyAddresses;
|
||||
}
|
||||
|
||||
|
||||
public void run() throws DataException {
|
||||
if (this.accountData == null) {
|
||||
// Nothing to do
|
||||
return;
|
||||
}
|
||||
|
||||
this.fetchSponsorshipRewardShares();
|
||||
if (this.sponsorshipRewardShares.isEmpty()) {
|
||||
// Nothing to do
|
||||
return;
|
||||
}
|
||||
|
||||
this.findConsolidatedRewards();
|
||||
this.findBulkIssuance();
|
||||
this.findRegisteredNameCount();
|
||||
this.findRecentSponsorshipCount();
|
||||
|
||||
int score = this.calculateScore();
|
||||
if (score <= 0 && !override) {
|
||||
return;
|
||||
}
|
||||
|
||||
String newAddress = this.getDestinationAccount(this.address);
|
||||
while (newAddress != null) {
|
||||
// Found destination account
|
||||
this.penaltyAddresses.add(newAddress);
|
||||
|
||||
// Run algo for this address, but in "override" mode because it has already been flagged
|
||||
SelfSponsorshipAlgoV3 algoV3 = new SelfSponsorshipAlgoV3(this.repository, newAddress, this.snapshotTimestampV1, this.snapshotTimestampV3, true);
|
||||
algoV3.run();
|
||||
this.penaltyAddresses.addAll(algoV3.getPenaltyAddresses());
|
||||
|
||||
newAddress = this.getDestinationAccount(newAddress);
|
||||
}
|
||||
|
||||
this.penaltyAddresses.add(this.address);
|
||||
|
||||
if (this.override || this.recentSponsorshipCount < 20) {
|
||||
this.penaltyAddresses.addAll(this.consolidatedAddresses);
|
||||
this.penaltyAddresses.addAll(this.zeroTransactionAddreses);
|
||||
}
|
||||
else {
|
||||
this.penaltyAddresses.addAll(this.sponsees);
|
||||
}
|
||||
}
|
||||
|
||||
private String getDestinationAccount(String address) throws DataException {
|
||||
List<TransactionData> transferPrivsTransactions = fetchTransferPrivsForAddress(address);
|
||||
if (transferPrivsTransactions.isEmpty()) {
|
||||
// No TRANSFER_PRIVS transactions for this address
|
||||
return null;
|
||||
}
|
||||
|
||||
AccountData accountData = this.repository.getAccountRepository().getAccount(address);
|
||||
if (accountData == null) {
|
||||
return null;
|
||||
}
|
||||
|
||||
for (TransactionData transactionData : transferPrivsTransactions) {
|
||||
TransferPrivsTransactionData transferPrivsTransactionData = (TransferPrivsTransactionData) transactionData;
|
||||
if (Arrays.equals(transferPrivsTransactionData.getSenderPublicKey(), accountData.getPublicKey())) {
|
||||
return transferPrivsTransactionData.getRecipient();
|
||||
}
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
private void findConsolidatedRewards() throws DataException {
|
||||
List<String> sponseesThatSentRewards = new ArrayList<>();
|
||||
Map<String, Integer> paymentRecipients = new HashMap<>();
|
||||
|
||||
// Collect outgoing payments of each sponsee
|
||||
for (String sponseeAddress : this.sponsees) {
|
||||
|
||||
// Firstly fetch all payments for address, since the functions below depend on this data
|
||||
this.fetchPaymentsForAddress(sponseeAddress);
|
||||
|
||||
// Check if the address has zero relevant transactions
|
||||
if (this.hasZeroTransactions(sponseeAddress)) {
|
||||
this.zeroTransactionAddreses.add(sponseeAddress);
|
||||
}
|
||||
|
||||
// Get payment recipients
|
||||
List<String> allPaymentRecipients = this.fetchOutgoingPaymentRecipientsForAddress(sponseeAddress);
|
||||
if (allPaymentRecipients.isEmpty()) {
|
||||
continue;
|
||||
}
|
||||
sponseesThatSentRewards.add(sponseeAddress);
|
||||
|
||||
List<String> addressesPaidByThisSponsee = new ArrayList<>();
|
||||
for (String paymentRecipient : allPaymentRecipients) {
|
||||
if (addressesPaidByThisSponsee.contains(paymentRecipient)) {
|
||||
// We already tracked this association - don't allow multiple to stack up
|
||||
continue;
|
||||
}
|
||||
addressesPaidByThisSponsee.add(paymentRecipient);
|
||||
|
||||
// Increment count for this recipient, or initialize to 1 if not present
|
||||
if (paymentRecipients.computeIfPresent(paymentRecipient, (k, v) -> v + 1) == null) {
|
||||
paymentRecipients.put(paymentRecipient, 1);
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
// Exclude addresses with a low number of payments
|
||||
Map<String, Integer> filteredPaymentRecipients = paymentRecipients.entrySet().stream()
|
||||
.filter(p -> p.getValue() != null && p.getValue() >= 10)
|
||||
.collect(Collectors.toMap(Map.Entry::getKey, Map.Entry::getValue));
|
||||
|
||||
// Now check how many sponsees have sent to this subset of addresses
|
||||
Map<String, Integer> sponseesThatConsolidatedRewards = new HashMap<>();
|
||||
for (String sponseeAddress : sponseesThatSentRewards) {
|
||||
List<String> allPaymentRecipients = this.fetchOutgoingPaymentRecipientsForAddress(sponseeAddress);
|
||||
// Remove any that aren't to one of the flagged recipients (i.e. consolidation)
|
||||
allPaymentRecipients.removeIf(r -> !filteredPaymentRecipients.containsKey(r));
|
||||
|
||||
int count = allPaymentRecipients.size();
|
||||
if (count == 0) {
|
||||
continue;
|
||||
}
|
||||
if (sponseesThatConsolidatedRewards.computeIfPresent(sponseeAddress, (k, v) -> v + count) == null) {
|
||||
sponseesThatConsolidatedRewards.put(sponseeAddress, count);
|
||||
}
|
||||
}
|
||||
|
||||
// Remove sponsees that have only sent a low number of payments to the filtered addresses
|
||||
Map<String, Integer> filteredSponseesThatConsolidatedRewards = sponseesThatConsolidatedRewards.entrySet().stream()
|
||||
.filter(p -> p.getValue() != null && p.getValue() >= 2)
|
||||
.collect(Collectors.toMap(Map.Entry::getKey, Map.Entry::getValue));
|
||||
|
||||
this.consolidationCount = sponseesThatConsolidatedRewards.size();
|
||||
this.consolidatedAddresses = new LinkedHashSet<>(filteredSponseesThatConsolidatedRewards.keySet());
|
||||
this.suspiciousCount = this.consolidationCount + this.zeroTransactionAddreses.size();
|
||||
this.suspiciousPercent = (int)(this.suspiciousCount / (float) this.sponsees.size() * 100);
|
||||
}
|
||||
|
||||
private void findBulkIssuance() {
|
||||
Long lastTimestamp = null;
|
||||
for (RewardShareTransactionData rewardShareTransactionData : sponsorshipRewardShares) {
|
||||
long timestamp = rewardShareTransactionData.getTimestamp();
|
||||
if (timestamp >= this.snapshotTimestampV3) {
|
||||
continue;
|
||||
}
|
||||
|
||||
if (lastTimestamp != null) {
|
||||
if (timestamp - lastTimestamp < 3*60*1000L) {
|
||||
this.bulkIssuanceCount++;
|
||||
}
|
||||
}
|
||||
lastTimestamp = timestamp;
|
||||
}
|
||||
}
|
||||
|
||||
private void findRegisteredNameCount() throws DataException {
|
||||
int registeredNameCount = 0;
|
||||
for (String sponseeAddress : sponsees) {
|
||||
List<NameData> names = repository.getNameRepository().getNamesByOwner(sponseeAddress);
|
||||
for (NameData name : names) {
|
||||
if (name.getRegistered() < this.snapshotTimestampV3) {
|
||||
registeredNameCount++;
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
this.registeredNameCount = registeredNameCount;
|
||||
}
|
||||
|
||||
private void findRecentSponsorshipCount() {
|
||||
int recentSponsorshipCount = 0;
|
||||
for (RewardShareTransactionData rewardShare : sponsorshipRewardShares) {
|
||||
if (rewardShare.getTimestamp() >= this.snapshotTimestampV1) {
|
||||
recentSponsorshipCount++;
|
||||
}
|
||||
}
|
||||
this.recentSponsorshipCount = recentSponsorshipCount;
|
||||
}
|
||||
|
||||
private int calculateScore() {
|
||||
final int suspiciousMultiplier = (this.suspiciousCount >= 100) ? this.suspiciousPercent : 1;
|
||||
final int nameMultiplier = (this.sponsees.size() >= 25 && this.registeredNameCount <= 1) ? 21 :
|
||||
(this.sponsees.size() >= 15 && this.registeredNameCount <= 1) ? 11 :
|
||||
(this.sponsees.size() >= 5 && this.registeredNameCount <= 1) ? 5 : 1;
|
||||
final int consolidationMultiplier = Math.max(this.consolidationCount, 1);
|
||||
final int bulkIssuanceMultiplier = Math.max(this.bulkIssuanceCount / 2, 1);
|
||||
final int offset = 20;
|
||||
return suspiciousMultiplier * nameMultiplier * consolidationMultiplier * bulkIssuanceMultiplier - offset;
|
||||
}
|
||||
|
||||
private void fetchSponsorshipRewardShares() throws DataException {
|
||||
List<RewardShareTransactionData> sponsorshipRewardShares = new ArrayList<>();
|
||||
|
||||
// Define relevant transactions
|
||||
List<TransactionType> txTypes = List.of(TransactionType.REWARD_SHARE);
|
||||
List<TransactionData> transactionDataList = fetchTransactions(repository, txTypes, this.address, false);
|
||||
transactionDataList.removeIf(t -> t.getTimestamp() <= this.snapshotTimestampV1 || t.getTimestamp() >= this.snapshotTimestampV3);
|
||||
|
||||
for (TransactionData transactionData : transactionDataList) {
|
||||
if (transactionData.getType() != TransactionType.REWARD_SHARE) {
|
||||
continue;
|
||||
}
|
||||
|
||||
RewardShareTransactionData rewardShareTransactionData = (RewardShareTransactionData) transactionData;
|
||||
|
||||
// Skip removals
|
||||
if (rewardShareTransactionData.getSharePercent() < 0) {
|
||||
continue;
|
||||
}
|
||||
|
||||
// Skip if not sponsored by this account
|
||||
if (!Arrays.equals(rewardShareTransactionData.getCreatorPublicKey(), accountData.getPublicKey())) {
|
||||
continue;
|
||||
}
|
||||
|
||||
// Skip self shares
|
||||
if (Objects.equals(rewardShareTransactionData.getRecipient(), this.address)) {
|
||||
continue;
|
||||
}
|
||||
|
||||
boolean duplicateFound = false;
|
||||
for (RewardShareTransactionData existingRewardShare : sponsorshipRewardShares) {
|
||||
if (Objects.equals(existingRewardShare.getRecipient(), rewardShareTransactionData.getRecipient())) {
|
||||
// Duplicate
|
||||
duplicateFound = true;
|
||||
break;
|
||||
}
|
||||
}
|
||||
if (!duplicateFound) {
|
||||
sponsorshipRewardShares.add(rewardShareTransactionData);
|
||||
this.sponsees.add(rewardShareTransactionData.getRecipient());
|
||||
}
|
||||
}
|
||||
|
||||
this.sponsorshipRewardShares = sponsorshipRewardShares;
|
||||
}
|
||||
|
||||
private List<TransactionData> fetchTransferPrivsForAddress(String address) throws DataException {
|
||||
return fetchTransactions(repository,
|
||||
List.of(TransactionType.TRANSFER_PRIVS),
|
||||
address, true);
|
||||
}
|
||||
|
||||
private void fetchPaymentsForAddress(String address) throws DataException {
|
||||
List<TransactionData> payments = fetchTransactions(repository,
|
||||
Arrays.asList(TransactionType.PAYMENT, TransactionType.TRANSFER_ASSET),
|
||||
address, false);
|
||||
this.paymentsByAddress.put(address, payments);
|
||||
}
|
||||
|
||||
private List<String> fetchOutgoingPaymentRecipientsForAddress(String address) {
|
||||
List<String> outgoingPaymentRecipients = new ArrayList<>();
|
||||
|
||||
List<TransactionData> transactionDataList = this.paymentsByAddress.get(address);
|
||||
if (transactionDataList == null) transactionDataList = new ArrayList<>();
|
||||
transactionDataList.removeIf(t -> t.getTimestamp() <= this.snapshotTimestampV1 || t.getTimestamp() >= this.snapshotTimestampV3);
|
||||
for (TransactionData transactionData : transactionDataList) {
|
||||
switch (transactionData.getType()) {
|
||||
|
||||
case PAYMENT:
|
||||
PaymentTransactionData paymentTransactionData = (PaymentTransactionData) transactionData;
|
||||
if (!Objects.equals(paymentTransactionData.getRecipient(), address)) {
|
||||
// Outgoing payment from this account
|
||||
outgoingPaymentRecipients.add(paymentTransactionData.getRecipient());
|
||||
}
|
||||
break;
|
||||
|
||||
case TRANSFER_ASSET:
|
||||
TransferAssetTransactionData transferAssetTransactionData = (TransferAssetTransactionData) transactionData;
|
||||
if (transferAssetTransactionData.getAssetId() == Asset.QORT) {
|
||||
if (!Objects.equals(transferAssetTransactionData.getRecipient(), address)) {
|
||||
// Outgoing payment from this account
|
||||
outgoingPaymentRecipients.add(transferAssetTransactionData.getRecipient());
|
||||
}
|
||||
}
|
||||
break;
|
||||
|
||||
default:
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
return outgoingPaymentRecipients;
|
||||
}
|
||||
|
||||
private boolean hasZeroTransactions(String address) {
|
||||
List<TransactionData> transactionDataList = this.paymentsByAddress.get(address);
|
||||
if (transactionDataList == null) {
|
||||
return true;
|
||||
}
|
||||
transactionDataList.removeIf(t -> t.getTimestamp() <= this.snapshotTimestampV1 || t.getTimestamp() >= this.snapshotTimestampV3);
|
||||
return transactionDataList.isEmpty();
|
||||
}
|
||||
|
||||
private static List<TransactionData> fetchTransactions(Repository repository, List<TransactionType> txTypes, String address, boolean reverse) throws DataException {
|
||||
// Fetch all relevant transactions for this account
|
||||
List<byte[]> signatures = repository.getTransactionRepository()
|
||||
.getSignaturesMatchingCriteria(null, null, null, txTypes,
|
||||
null, null, address, TransactionsResource.ConfirmationStatus.CONFIRMED,
|
||||
null, null, reverse);
|
||||
|
||||
List<TransactionData> transactionDataList = new ArrayList<>();
|
||||
|
||||
for (byte[] signature : signatures) {
|
||||
// Fetch transaction data
|
||||
TransactionData transactionData = repository.getTransactionRepository().fromSignature(signature);
|
||||
if (transactionData == null) {
|
||||
continue;
|
||||
}
|
||||
transactionDataList.add(transactionData);
|
||||
}
|
||||
|
||||
return transactionDataList;
|
||||
}
|
||||
}
|
@@ -141,7 +141,7 @@ public class ApiRequest {
|
||||
}
|
||||
|
||||
String resultString = result.toString();
|
||||
return resultString.length() > 0 ? resultString.substring(0, resultString.length() - 1) : resultString;
|
||||
return !resultString.isEmpty() ? resultString.substring(0, resultString.length() - 1) : resultString;
|
||||
}
|
||||
|
||||
/**
|
||||
|
@@ -82,7 +82,7 @@ public class HTMLParser {
|
||||
}
|
||||
|
||||
public static boolean isHtmlFile(String path) {
|
||||
if (path.endsWith(".html") || path.endsWith(".htm") || path.equals("")) {
|
||||
if (path.endsWith(".html") || path.endsWith(".htm") || path.isEmpty()) {
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
|
@@ -2,5 +2,5 @@ package org.qortal.api;
|
||||
|
||||
public enum SearchMode {
|
||||
LATEST,
|
||||
ALL;
|
||||
ALL
|
||||
}
|
||||
|
@@ -41,7 +41,7 @@ public class GatewayResource {
|
||||
private ArbitraryResourceStatus getStatus(Service service, String name, String identifier, Boolean build) {
|
||||
|
||||
// If "build=true" has been specified in the query string, build the resource before returning its status
|
||||
if (build != null && build == true) {
|
||||
if (build != null && build) {
|
||||
try {
|
||||
ArbitraryDataReader reader = new ArbitraryDataReader(name, ArbitraryDataFile.ResourceIdType.NAME, service, null);
|
||||
if (!reader.isBuilding()) {
|
||||
@@ -80,7 +80,7 @@ public class GatewayResource {
|
||||
|
||||
private HttpServletResponse parsePath(String inPath, String qdnContext, String secret58, boolean includeResourceIdInPrefix, boolean async) {
|
||||
|
||||
if (inPath == null || inPath.equals("")) {
|
||||
if (inPath == null || inPath.isEmpty()) {
|
||||
// Assume not a real file
|
||||
return ArbitraryDataRenderer.getResponse(response, 404, "Error 404: File Not Found");
|
||||
}
|
||||
@@ -140,7 +140,7 @@ public class GatewayResource {
|
||||
}
|
||||
|
||||
String prefix = StringUtils.join(prefixParts, "/");
|
||||
if (prefix != null && prefix.length() > 0) {
|
||||
if (prefix != null && !prefix.isEmpty()) {
|
||||
prefix = "/" + prefix;
|
||||
}
|
||||
|
||||
|
@@ -17,7 +17,7 @@ public class ConnectedPeer {
|
||||
|
||||
public enum Direction {
|
||||
INBOUND,
|
||||
OUTBOUND;
|
||||
OUTBOUND
|
||||
}
|
||||
|
||||
public Direction direction;
|
||||
|
@@ -233,8 +233,7 @@ public class AddressesResource {
|
||||
}
|
||||
|
||||
} catch (DataException e) {
|
||||
continue;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Sort by level
|
||||
|
@@ -119,7 +119,7 @@ public class ArbitraryResource {
|
||||
|
||||
// Ensure that "default" and "identifier" parameters cannot coexist
|
||||
boolean defaultRes = Boolean.TRUE.equals(defaultResource);
|
||||
if (defaultRes == true && identifier != null) {
|
||||
if (defaultRes && identifier != null) {
|
||||
throw ApiExceptionFactory.INSTANCE.createCustomException(request, ApiError.INVALID_CRITERIA, "identifier cannot be specified when requesting a default resource");
|
||||
}
|
||||
|
||||
@@ -491,7 +491,7 @@ public class ArbitraryResource {
|
||||
|
||||
List<ArbitraryTransactionData> transactionDataList;
|
||||
|
||||
if (query == null || query.equals("")) {
|
||||
if (query == null || query.isEmpty()) {
|
||||
transactionDataList = ArbitraryDataStorageManager.getInstance().listAllHostedTransactions(repository, limit, offset);
|
||||
} else {
|
||||
transactionDataList = ArbitraryDataStorageManager.getInstance().searchHostedTransactions(repository,query, limit, offset);
|
||||
@@ -1258,7 +1258,7 @@ public class ArbitraryResource {
|
||||
}
|
||||
|
||||
// Finish here if user has requested a preview
|
||||
if (preview != null && preview == true) {
|
||||
if (preview != null && preview) {
|
||||
return this.preview(path, service);
|
||||
}
|
||||
|
||||
|
@@ -86,7 +86,7 @@ public class BlocksResource {
|
||||
// Check the database first
|
||||
BlockData blockData = repository.getBlockRepository().fromSignature(signature);
|
||||
if (blockData != null) {
|
||||
if (includeOnlineSignatures == null || includeOnlineSignatures == false) {
|
||||
if (includeOnlineSignatures == null || !includeOnlineSignatures) {
|
||||
blockData.setOnlineAccountsSignatures(null);
|
||||
}
|
||||
return blockData;
|
||||
@@ -95,7 +95,7 @@ public class BlocksResource {
|
||||
// Not found, so try the block archive
|
||||
blockData = repository.getBlockArchiveRepository().fromSignature(signature);
|
||||
if (blockData != null) {
|
||||
if (includeOnlineSignatures == null || includeOnlineSignatures == false) {
|
||||
if (includeOnlineSignatures == null || !includeOnlineSignatures) {
|
||||
blockData.setOnlineAccountsSignatures(null);
|
||||
}
|
||||
return blockData;
|
||||
@@ -304,7 +304,7 @@ public class BlocksResource {
|
||||
try (final Repository repository = RepositoryManager.getRepository()) {
|
||||
BlockData blockData = repository.getBlockRepository().getLastBlock();
|
||||
|
||||
if (includeOnlineSignatures == null || includeOnlineSignatures == false) {
|
||||
if (includeOnlineSignatures == null || !includeOnlineSignatures) {
|
||||
blockData.setOnlineAccountsSignatures(null);
|
||||
}
|
||||
|
||||
@@ -474,7 +474,7 @@ public class BlocksResource {
|
||||
// Firstly check the database
|
||||
BlockData blockData = repository.getBlockRepository().fromHeight(height);
|
||||
if (blockData != null) {
|
||||
if (includeOnlineSignatures == null || includeOnlineSignatures == false) {
|
||||
if (includeOnlineSignatures == null || !includeOnlineSignatures) {
|
||||
blockData.setOnlineAccountsSignatures(null);
|
||||
}
|
||||
return blockData;
|
||||
@@ -483,7 +483,7 @@ public class BlocksResource {
|
||||
// Not found, so try the archive
|
||||
blockData = repository.getBlockArchiveRepository().fromHeight(height);
|
||||
if (blockData != null) {
|
||||
if (includeOnlineSignatures == null || includeOnlineSignatures == false) {
|
||||
if (includeOnlineSignatures == null || !includeOnlineSignatures) {
|
||||
blockData.setOnlineAccountsSignatures(null);
|
||||
}
|
||||
return blockData;
|
||||
@@ -596,7 +596,7 @@ public class BlocksResource {
|
||||
if (height > 1) {
|
||||
// Found match in Blocks table
|
||||
blockData = repository.getBlockRepository().fromHeight(height);
|
||||
if (includeOnlineSignatures == null || includeOnlineSignatures == false) {
|
||||
if (includeOnlineSignatures == null || !includeOnlineSignatures) {
|
||||
blockData.setOnlineAccountsSignatures(null);
|
||||
}
|
||||
return blockData;
|
||||
@@ -614,7 +614,7 @@ public class BlocksResource {
|
||||
throw ApiExceptionFactory.INSTANCE.createException(request, ApiError.BLOCK_UNKNOWN);
|
||||
}
|
||||
|
||||
if (includeOnlineSignatures == null || includeOnlineSignatures == false) {
|
||||
if (includeOnlineSignatures == null || !includeOnlineSignatures) {
|
||||
blockData.setOnlineAccountsSignatures(null);
|
||||
}
|
||||
|
||||
@@ -651,7 +651,7 @@ public class BlocksResource {
|
||||
@QueryParam("includeOnlineSignatures") Boolean includeOnlineSignatures) {
|
||||
try (final Repository repository = RepositoryManager.getRepository()) {
|
||||
List<BlockData> blocks = new ArrayList<>();
|
||||
boolean shouldReverse = (reverse != null && reverse == true);
|
||||
boolean shouldReverse = (reverse != null && reverse);
|
||||
|
||||
int i = 0;
|
||||
while (i < count) {
|
||||
@@ -664,7 +664,7 @@ public class BlocksResource {
|
||||
break;
|
||||
}
|
||||
}
|
||||
if (includeOnlineSignatures == null || includeOnlineSignatures == false) {
|
||||
if (includeOnlineSignatures == null || !includeOnlineSignatures) {
|
||||
blockData.setOnlineAccountsSignatures(null);
|
||||
}
|
||||
|
||||
|
@@ -17,7 +17,11 @@ import org.qortal.api.model.crosschain.AddressRequest;
|
||||
import org.qortal.api.model.crosschain.BitcoinSendRequest;
|
||||
import org.qortal.crosschain.AddressInfo;
|
||||
import org.qortal.crosschain.Bitcoin;
|
||||
import org.qortal.crosschain.ChainableServer;
|
||||
import org.qortal.crosschain.ElectrumX;
|
||||
import org.qortal.crosschain.ForeignBlockchainException;
|
||||
import org.qortal.crosschain.ServerConnectionInfo;
|
||||
import org.qortal.crosschain.ServerInfo;
|
||||
import org.qortal.crosschain.SimpleTransaction;
|
||||
import org.qortal.crosschain.ServerConfigurationInfo;
|
||||
|
||||
@@ -265,4 +269,292 @@ public class CrossChainBitcoinResource {
|
||||
|
||||
return CrossChainUtils.buildServerConfigurationInfo(Bitcoin.getInstance());
|
||||
}
|
||||
|
||||
@GET
|
||||
@Path("/serverconnectionhistory")
|
||||
@Operation(
|
||||
summary = "Returns Bitcoin server connection history",
|
||||
description = "Returns Bitcoin server connection history",
|
||||
responses = {
|
||||
@ApiResponse(
|
||||
content = @Content(array = @ArraySchema( schema = @Schema( implementation = ServerConnectionInfo.class ) ) )
|
||||
)
|
||||
}
|
||||
)
|
||||
public List<ServerConnectionInfo> getServerConnectionHistory() {
|
||||
|
||||
return CrossChainUtils.buildServerConnectionHistory(Bitcoin.getInstance());
|
||||
}
|
||||
|
||||
@POST
|
||||
@Path("/addserver")
|
||||
@Operation(
|
||||
summary = "Add server to list of Bitcoin servers",
|
||||
description = "Add server to list of Bitcoin servers",
|
||||
requestBody = @RequestBody(
|
||||
required = true,
|
||||
content = @Content(
|
||||
mediaType = MediaType.APPLICATION_JSON,
|
||||
schema = @Schema(
|
||||
implementation = ServerInfo.class
|
||||
)
|
||||
)
|
||||
),
|
||||
responses = {
|
||||
@ApiResponse(
|
||||
description = "true if added, false if not added",
|
||||
content = @Content(
|
||||
schema = @Schema(
|
||||
type = "string"
|
||||
)
|
||||
)
|
||||
)
|
||||
}
|
||||
|
||||
)
|
||||
@ApiErrors({ApiError.INVALID_DATA})
|
||||
@SecurityRequirement(name = "apiKey")
|
||||
public String addServer(@HeaderParam(Security.API_KEY_HEADER) String apiKey, ServerInfo serverInfo) {
|
||||
Security.checkApiCallAllowed(request);
|
||||
|
||||
try {
|
||||
ElectrumX.Server server = new ElectrumX.Server(
|
||||
serverInfo.getHostName(),
|
||||
ChainableServer.ConnectionType.valueOf(serverInfo.getConnectionType()),
|
||||
serverInfo.getPort()
|
||||
);
|
||||
|
||||
if( CrossChainUtils.addServer( Bitcoin.getInstance(), server )) {
|
||||
return "true";
|
||||
}
|
||||
else {
|
||||
return "false";
|
||||
}
|
||||
}
|
||||
catch (IllegalArgumentException | NullPointerException e) {
|
||||
throw ApiExceptionFactory.INSTANCE.createException(request, ApiError.INVALID_DATA);
|
||||
}
|
||||
catch (Exception e) {
|
||||
return "false";
|
||||
}
|
||||
}
|
||||
|
||||
@POST
|
||||
@Path("/removeserver")
|
||||
@Operation(
|
||||
summary = "Remove server from list of Bitcoin servers",
|
||||
description = "Remove server from list of Bitcoin servers",
|
||||
requestBody = @RequestBody(
|
||||
required = true,
|
||||
content = @Content(
|
||||
mediaType = MediaType.APPLICATION_JSON,
|
||||
schema = @Schema(
|
||||
implementation = ServerInfo.class
|
||||
)
|
||||
)
|
||||
),
|
||||
responses = {
|
||||
@ApiResponse(
|
||||
description = "true if removed, otherwise",
|
||||
content = @Content(
|
||||
schema = @Schema(
|
||||
type = "string"
|
||||
)
|
||||
)
|
||||
)
|
||||
}
|
||||
|
||||
)
|
||||
@ApiErrors({ApiError.INVALID_DATA})
|
||||
@SecurityRequirement(name = "apiKey")
|
||||
public String removeServer(@HeaderParam(Security.API_KEY_HEADER) String apiKey, ServerInfo serverInfo) {
|
||||
Security.checkApiCallAllowed(request);
|
||||
|
||||
try {
|
||||
ElectrumX.Server server = new ElectrumX.Server(
|
||||
serverInfo.getHostName(),
|
||||
ChainableServer.ConnectionType.valueOf(serverInfo.getConnectionType()),
|
||||
serverInfo.getPort()
|
||||
);
|
||||
|
||||
if( CrossChainUtils.removeServer( Bitcoin.getInstance(), server ) ) {
|
||||
|
||||
return "true";
|
||||
}
|
||||
else {
|
||||
return "false";
|
||||
}
|
||||
}
|
||||
catch (IllegalArgumentException | NullPointerException e) {
|
||||
throw ApiExceptionFactory.INSTANCE.createException(request, ApiError.INVALID_DATA);
|
||||
}
|
||||
catch (Exception e) {
|
||||
return "false";
|
||||
}
|
||||
}
|
||||
|
||||
@POST
|
||||
@Path("/setcurrentserver")
|
||||
@Operation(
|
||||
summary = "Set current Bitcoin server",
|
||||
description = "Set current Bitcoin server",
|
||||
requestBody = @RequestBody(
|
||||
required = true,
|
||||
content = @Content(
|
||||
mediaType = MediaType.APPLICATION_JSON,
|
||||
schema = @Schema(
|
||||
implementation = ServerInfo.class
|
||||
)
|
||||
)
|
||||
),
|
||||
responses = {
|
||||
@ApiResponse(
|
||||
description = "connection info",
|
||||
content = @Content(
|
||||
mediaType = MediaType.APPLICATION_JSON,
|
||||
schema = @Schema(
|
||||
implementation = ServerConnectionInfo.class
|
||||
)
|
||||
)
|
||||
)
|
||||
}
|
||||
|
||||
)
|
||||
@ApiErrors({ApiError.INVALID_DATA})
|
||||
@SecurityRequirement(name = "apiKey")
|
||||
public ServerConnectionInfo setCurrentServer(@HeaderParam(Security.API_KEY_HEADER) String apiKey, ServerInfo serverInfo) {
|
||||
Security.checkApiCallAllowed(request);
|
||||
|
||||
if( serverInfo.getConnectionType() == null ||
|
||||
serverInfo.getHostName() == null) throw ApiExceptionFactory.INSTANCE.createException(request, ApiError.INVALID_DATA);
|
||||
try {
|
||||
return CrossChainUtils.setCurrentServer( Bitcoin.getInstance(), serverInfo );
|
||||
}
|
||||
catch (IllegalArgumentException e) {
|
||||
throw ApiExceptionFactory.INSTANCE.createException(request, ApiError.INVALID_DATA);
|
||||
}
|
||||
catch (Exception e) {
|
||||
return new ServerConnectionInfo(
|
||||
serverInfo,
|
||||
CrossChainUtils.CORE_API_CALL,
|
||||
true,
|
||||
false,
|
||||
System.currentTimeMillis(),
|
||||
CrossChainUtils.getNotes(e));
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@GET
|
||||
@Path("/feekb")
|
||||
@Operation(
|
||||
summary = "Returns Bitcoin fee per Kb.",
|
||||
description = "Returns Bitcoin fee per Kb.",
|
||||
responses = {
|
||||
@ApiResponse(
|
||||
content = @Content(
|
||||
schema = @Schema(
|
||||
type = "number"
|
||||
)
|
||||
)
|
||||
)
|
||||
}
|
||||
)
|
||||
public String getBitcoinFeePerKb() {
|
||||
Bitcoin bitcoin = Bitcoin.getInstance();
|
||||
|
||||
return String.valueOf(bitcoin.getFeePerKb().value);
|
||||
}
|
||||
|
||||
@POST
|
||||
@Path("/updatefeekb")
|
||||
@Operation(
|
||||
summary = "Sets Bitcoin fee per Kb.",
|
||||
description = "Sets Bitcoin fee per Kb.",
|
||||
requestBody = @RequestBody(
|
||||
required = true,
|
||||
content = @Content(
|
||||
mediaType = MediaType.TEXT_PLAIN,
|
||||
schema = @Schema(
|
||||
type = "number",
|
||||
description = "the fee per Kb",
|
||||
example = "100"
|
||||
)
|
||||
)
|
||||
),
|
||||
responses = {
|
||||
@ApiResponse(
|
||||
content = @Content(mediaType = MediaType.TEXT_PLAIN, schema = @Schema(type = "number", description = "fee"))
|
||||
)
|
||||
}
|
||||
)
|
||||
@ApiErrors({ApiError.INVALID_PRIVATE_KEY, ApiError.INVALID_CRITERIA})
|
||||
public String setBitcoinFeePerKb(@HeaderParam(Security.API_KEY_HEADER) String apiKey, String fee) {
|
||||
Security.checkApiCallAllowed(request);
|
||||
|
||||
Bitcoin bitcoin = Bitcoin.getInstance();
|
||||
|
||||
try {
|
||||
return CrossChainUtils.setFeePerKb(bitcoin, fee);
|
||||
} catch (IllegalArgumentException e) {
|
||||
throw ApiExceptionFactory.INSTANCE.createException(request, ApiError.INVALID_CRITERIA);
|
||||
}
|
||||
}
|
||||
|
||||
@GET
|
||||
@Path("/feeceiling")
|
||||
@Operation(
|
||||
summary = "Returns Bitcoin fee per Kb.",
|
||||
description = "Returns Bitcoin fee per Kb.",
|
||||
responses = {
|
||||
@ApiResponse(
|
||||
content = @Content(
|
||||
schema = @Schema(
|
||||
type = "number"
|
||||
)
|
||||
)
|
||||
)
|
||||
}
|
||||
)
|
||||
public String getBitcoinFeeCeiling() {
|
||||
Bitcoin bitcoin = Bitcoin.getInstance();
|
||||
|
||||
return String.valueOf(bitcoin.getFeeCeiling());
|
||||
}
|
||||
|
||||
@POST
|
||||
@Path("/updatefeeceiling")
|
||||
@Operation(
|
||||
summary = "Sets Bitcoin fee ceiling.",
|
||||
description = "Sets Bitcoin fee ceiling.",
|
||||
requestBody = @RequestBody(
|
||||
required = true,
|
||||
content = @Content(
|
||||
mediaType = MediaType.TEXT_PLAIN,
|
||||
schema = @Schema(
|
||||
type = "number",
|
||||
description = "the fee",
|
||||
example = "100"
|
||||
)
|
||||
)
|
||||
),
|
||||
responses = {
|
||||
@ApiResponse(
|
||||
content = @Content(mediaType = MediaType.TEXT_PLAIN, schema = @Schema(type = "number", description = "fee"))
|
||||
)
|
||||
}
|
||||
)
|
||||
@ApiErrors({ApiError.INVALID_PRIVATE_KEY, ApiError.INVALID_CRITERIA})
|
||||
public String setBitcoinFeeCeiling(@HeaderParam(Security.API_KEY_HEADER) String apiKey, String fee) {
|
||||
Security.checkApiCallAllowed(request);
|
||||
|
||||
Bitcoin bitcoin = Bitcoin.getInstance();
|
||||
|
||||
try {
|
||||
return CrossChainUtils.setFeeCeiling(bitcoin, fee);
|
||||
}
|
||||
catch (IllegalArgumentException e) {
|
||||
throw ApiExceptionFactory.INSTANCE.createException(request, ApiError.INVALID_CRITERIA);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@@ -16,8 +16,12 @@ import org.qortal.api.Security;
|
||||
import org.qortal.api.model.crosschain.AddressRequest;
|
||||
import org.qortal.api.model.crosschain.DigibyteSendRequest;
|
||||
import org.qortal.crosschain.AddressInfo;
|
||||
import org.qortal.crosschain.Digibyte;
|
||||
import org.qortal.crosschain.ChainableServer;
|
||||
import org.qortal.crosschain.ElectrumX;
|
||||
import org.qortal.crosschain.ForeignBlockchainException;
|
||||
import org.qortal.crosschain.Digibyte;
|
||||
import org.qortal.crosschain.ServerConnectionInfo;
|
||||
import org.qortal.crosschain.ServerInfo;
|
||||
import org.qortal.crosschain.SimpleTransaction;
|
||||
import org.qortal.crosschain.ServerConfigurationInfo;
|
||||
|
||||
@@ -265,4 +269,292 @@ public class CrossChainDigibyteResource {
|
||||
|
||||
return CrossChainUtils.buildServerConfigurationInfo(Digibyte.getInstance());
|
||||
}
|
||||
}
|
||||
|
||||
@GET
|
||||
@Path("/serverconnectionhistory")
|
||||
@Operation(
|
||||
summary = "Returns Digibyte server connection history",
|
||||
description = "Returns Digibyte server connection history",
|
||||
responses = {
|
||||
@ApiResponse(
|
||||
content = @Content(array = @ArraySchema( schema = @Schema( implementation = ServerConnectionInfo.class ) ) )
|
||||
)
|
||||
}
|
||||
)
|
||||
public List<ServerConnectionInfo> getServerConnectionHistory() {
|
||||
|
||||
return CrossChainUtils.buildServerConnectionHistory(Digibyte.getInstance());
|
||||
}
|
||||
|
||||
@POST
|
||||
@Path("/addserver")
|
||||
@Operation(
|
||||
summary = "Add server to list of Digibyte servers",
|
||||
description = "Add server to list of Digibyte servers",
|
||||
requestBody = @RequestBody(
|
||||
required = true,
|
||||
content = @Content(
|
||||
mediaType = MediaType.APPLICATION_JSON,
|
||||
schema = @Schema(
|
||||
implementation = ServerInfo.class
|
||||
)
|
||||
)
|
||||
),
|
||||
responses = {
|
||||
@ApiResponse(
|
||||
description = "true if added, false if not added",
|
||||
content = @Content(
|
||||
schema = @Schema(
|
||||
type = "string"
|
||||
)
|
||||
)
|
||||
)
|
||||
}
|
||||
|
||||
)
|
||||
@ApiErrors({ApiError.INVALID_DATA})
|
||||
@SecurityRequirement(name = "apiKey")
|
||||
public String addServer(@HeaderParam(Security.API_KEY_HEADER) String apiKey, ServerInfo serverInfo) {
|
||||
Security.checkApiCallAllowed(request);
|
||||
|
||||
try {
|
||||
ElectrumX.Server server = new ElectrumX.Server(
|
||||
serverInfo.getHostName(),
|
||||
ChainableServer.ConnectionType.valueOf(serverInfo.getConnectionType()),
|
||||
serverInfo.getPort()
|
||||
);
|
||||
|
||||
if( CrossChainUtils.addServer( Digibyte.getInstance(), server )) {
|
||||
return "true";
|
||||
}
|
||||
else {
|
||||
return "false";
|
||||
}
|
||||
}
|
||||
catch (IllegalArgumentException | NullPointerException e) {
|
||||
throw ApiExceptionFactory.INSTANCE.createException(request, ApiError.INVALID_DATA);
|
||||
}
|
||||
catch (Exception e) {
|
||||
return "false";
|
||||
}
|
||||
}
|
||||
|
||||
@POST
|
||||
@Path("/removeserver")
|
||||
@Operation(
|
||||
summary = "Remove server from list of Digibyte servers",
|
||||
description = "Remove server from list of Digibyte servers",
|
||||
requestBody = @RequestBody(
|
||||
required = true,
|
||||
content = @Content(
|
||||
mediaType = MediaType.APPLICATION_JSON,
|
||||
schema = @Schema(
|
||||
implementation = ServerInfo.class
|
||||
)
|
||||
)
|
||||
),
|
||||
responses = {
|
||||
@ApiResponse(
|
||||
description = "true if removed, otherwise",
|
||||
content = @Content(
|
||||
schema = @Schema(
|
||||
type = "string"
|
||||
)
|
||||
)
|
||||
)
|
||||
}
|
||||
|
||||
)
|
||||
@ApiErrors({ApiError.INVALID_DATA})
|
||||
@SecurityRequirement(name = "apiKey")
|
||||
public String removeServer(@HeaderParam(Security.API_KEY_HEADER) String apiKey, ServerInfo serverInfo) {
|
||||
Security.checkApiCallAllowed(request);
|
||||
|
||||
try {
|
||||
ElectrumX.Server server = new ElectrumX.Server(
|
||||
serverInfo.getHostName(),
|
||||
ChainableServer.ConnectionType.valueOf(serverInfo.getConnectionType()),
|
||||
serverInfo.getPort()
|
||||
);
|
||||
|
||||
if( CrossChainUtils.removeServer( Digibyte.getInstance(), server ) ) {
|
||||
|
||||
return "true";
|
||||
}
|
||||
else {
|
||||
return "false";
|
||||
}
|
||||
}
|
||||
catch (IllegalArgumentException | NullPointerException e) {
|
||||
throw ApiExceptionFactory.INSTANCE.createException(request, ApiError.INVALID_DATA);
|
||||
}
|
||||
catch (Exception e) {
|
||||
return "false";
|
||||
}
|
||||
}
|
||||
|
||||
@POST
|
||||
@Path("/setcurrentserver")
|
||||
@Operation(
|
||||
summary = "Set current Digibyte server",
|
||||
description = "Set current Digibyte server",
|
||||
requestBody = @RequestBody(
|
||||
required = true,
|
||||
content = @Content(
|
||||
mediaType = MediaType.APPLICATION_JSON,
|
||||
schema = @Schema(
|
||||
implementation = ServerInfo.class
|
||||
)
|
||||
)
|
||||
),
|
||||
responses = {
|
||||
@ApiResponse(
|
||||
description = "connection info",
|
||||
content = @Content(
|
||||
mediaType = MediaType.APPLICATION_JSON,
|
||||
schema = @Schema(
|
||||
implementation = ServerConnectionInfo.class
|
||||
)
|
||||
)
|
||||
)
|
||||
}
|
||||
|
||||
)
|
||||
@ApiErrors({ApiError.INVALID_DATA})
|
||||
@SecurityRequirement(name = "apiKey")
|
||||
public ServerConnectionInfo setCurrentServer(@HeaderParam(Security.API_KEY_HEADER) String apiKey, ServerInfo serverInfo) {
|
||||
Security.checkApiCallAllowed(request);
|
||||
|
||||
if( serverInfo.getConnectionType() == null ||
|
||||
serverInfo.getHostName() == null) throw ApiExceptionFactory.INSTANCE.createException(request, ApiError.INVALID_DATA);
|
||||
try {
|
||||
return CrossChainUtils.setCurrentServer( Digibyte.getInstance(), serverInfo );
|
||||
}
|
||||
catch (IllegalArgumentException e) {
|
||||
throw ApiExceptionFactory.INSTANCE.createException(request, ApiError.INVALID_DATA);
|
||||
}
|
||||
catch (Exception e) {
|
||||
return new ServerConnectionInfo(
|
||||
serverInfo,
|
||||
CrossChainUtils.CORE_API_CALL,
|
||||
true,
|
||||
false,
|
||||
System.currentTimeMillis(),
|
||||
CrossChainUtils.getNotes(e));
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@GET
|
||||
@Path("/feekb")
|
||||
@Operation(
|
||||
summary = "Returns Digibyte fee per Kb.",
|
||||
description = "Returns Digibyte fee per Kb.",
|
||||
responses = {
|
||||
@ApiResponse(
|
||||
content = @Content(
|
||||
schema = @Schema(
|
||||
type = "number"
|
||||
)
|
||||
)
|
||||
)
|
||||
}
|
||||
)
|
||||
public String getDigibyteFeePerKb() {
|
||||
Digibyte digibyte = Digibyte.getInstance();
|
||||
|
||||
return String.valueOf(digibyte.getFeePerKb().value);
|
||||
}
|
||||
|
||||
@POST
|
||||
@Path("/updatefeekb")
|
||||
@Operation(
|
||||
summary = "Sets Digibyte fee per Kb.",
|
||||
description = "Sets Digibyte fee per Kb.",
|
||||
requestBody = @RequestBody(
|
||||
required = true,
|
||||
content = @Content(
|
||||
mediaType = MediaType.TEXT_PLAIN,
|
||||
schema = @Schema(
|
||||
type = "number",
|
||||
description = "the fee per Kb",
|
||||
example = "100"
|
||||
)
|
||||
)
|
||||
),
|
||||
responses = {
|
||||
@ApiResponse(
|
||||
content = @Content(mediaType = MediaType.TEXT_PLAIN, schema = @Schema(type = "number", description = "fee"))
|
||||
)
|
||||
}
|
||||
)
|
||||
@ApiErrors({ApiError.INVALID_PRIVATE_KEY, ApiError.INVALID_CRITERIA})
|
||||
public String setDigibyteFeePerKb(@HeaderParam(Security.API_KEY_HEADER) String apiKey, String fee) {
|
||||
Security.checkApiCallAllowed(request);
|
||||
|
||||
Digibyte digibyte = Digibyte.getInstance();
|
||||
|
||||
try {
|
||||
return CrossChainUtils.setFeePerKb(digibyte, fee);
|
||||
} catch (IllegalArgumentException e) {
|
||||
throw ApiExceptionFactory.INSTANCE.createException(request, ApiError.INVALID_CRITERIA);
|
||||
}
|
||||
}
|
||||
|
||||
@GET
|
||||
@Path("/feeceiling")
|
||||
@Operation(
|
||||
summary = "Returns Digibyte fee per Kb.",
|
||||
description = "Returns Digibyte fee per Kb.",
|
||||
responses = {
|
||||
@ApiResponse(
|
||||
content = @Content(
|
||||
schema = @Schema(
|
||||
type = "number"
|
||||
)
|
||||
)
|
||||
)
|
||||
}
|
||||
)
|
||||
public String getDigibyteFeeCeiling() {
|
||||
Digibyte digibyte = Digibyte.getInstance();
|
||||
|
||||
return String.valueOf(digibyte.getFeeCeiling());
|
||||
}
|
||||
|
||||
@POST
|
||||
@Path("/updatefeeceiling")
|
||||
@Operation(
|
||||
summary = "Sets Digibyte fee ceiling.",
|
||||
description = "Sets Digibyte fee ceiling.",
|
||||
requestBody = @RequestBody(
|
||||
required = true,
|
||||
content = @Content(
|
||||
mediaType = MediaType.TEXT_PLAIN,
|
||||
schema = @Schema(
|
||||
type = "number",
|
||||
description = "the fee",
|
||||
example = "100"
|
||||
)
|
||||
)
|
||||
),
|
||||
responses = {
|
||||
@ApiResponse(
|
||||
content = @Content(mediaType = MediaType.TEXT_PLAIN, schema = @Schema(type = "number", description = "fee"))
|
||||
)
|
||||
}
|
||||
)
|
||||
@ApiErrors({ApiError.INVALID_PRIVATE_KEY, ApiError.INVALID_CRITERIA})
|
||||
public String setDigibyteFeeCeiling(@HeaderParam(Security.API_KEY_HEADER) String apiKey, String fee) {
|
||||
Security.checkApiCallAllowed(request);
|
||||
|
||||
Digibyte digibyte = Digibyte.getInstance();
|
||||
|
||||
try {
|
||||
return CrossChainUtils.setFeeCeiling(digibyte, fee);
|
||||
}
|
||||
catch (IllegalArgumentException e) {
|
||||
throw ApiExceptionFactory.INSTANCE.createException(request, ApiError.INVALID_CRITERIA);
|
||||
}
|
||||
}
|
||||
}
|
@@ -16,8 +16,12 @@ import org.qortal.api.Security;
|
||||
import org.qortal.api.model.crosschain.AddressRequest;
|
||||
import org.qortal.api.model.crosschain.DogecoinSendRequest;
|
||||
import org.qortal.crosschain.AddressInfo;
|
||||
import org.qortal.crosschain.Dogecoin;
|
||||
import org.qortal.crosschain.ChainableServer;
|
||||
import org.qortal.crosschain.ElectrumX;
|
||||
import org.qortal.crosschain.ForeignBlockchainException;
|
||||
import org.qortal.crosschain.Dogecoin;
|
||||
import org.qortal.crosschain.ServerConnectionInfo;
|
||||
import org.qortal.crosschain.ServerInfo;
|
||||
import org.qortal.crosschain.SimpleTransaction;
|
||||
import org.qortal.crosschain.ServerConfigurationInfo;
|
||||
|
||||
@@ -265,4 +269,292 @@ public class CrossChainDogecoinResource {
|
||||
|
||||
return CrossChainUtils.buildServerConfigurationInfo(Dogecoin.getInstance());
|
||||
}
|
||||
}
|
||||
|
||||
@GET
|
||||
@Path("/serverconnectionhistory")
|
||||
@Operation(
|
||||
summary = "Returns Dogecoin server connection history",
|
||||
description = "Returns Dogecoin server connection history",
|
||||
responses = {
|
||||
@ApiResponse(
|
||||
content = @Content(array = @ArraySchema( schema = @Schema( implementation = ServerConnectionInfo.class ) ) )
|
||||
)
|
||||
}
|
||||
)
|
||||
public List<ServerConnectionInfo> getServerConnectionHistory() {
|
||||
|
||||
return CrossChainUtils.buildServerConnectionHistory(Dogecoin.getInstance());
|
||||
}
|
||||
|
||||
@POST
|
||||
@Path("/addserver")
|
||||
@Operation(
|
||||
summary = "Add server to list of Dogecoin servers",
|
||||
description = "Add server to list of Dogecoin servers",
|
||||
requestBody = @RequestBody(
|
||||
required = true,
|
||||
content = @Content(
|
||||
mediaType = MediaType.APPLICATION_JSON,
|
||||
schema = @Schema(
|
||||
implementation = ServerInfo.class
|
||||
)
|
||||
)
|
||||
),
|
||||
responses = {
|
||||
@ApiResponse(
|
||||
description = "true if added, false if not added",
|
||||
content = @Content(
|
||||
schema = @Schema(
|
||||
type = "string"
|
||||
)
|
||||
)
|
||||
)
|
||||
}
|
||||
|
||||
)
|
||||
@ApiErrors({ApiError.INVALID_DATA})
|
||||
@SecurityRequirement(name = "apiKey")
|
||||
public String addServer(@HeaderParam(Security.API_KEY_HEADER) String apiKey, ServerInfo serverInfo) {
|
||||
Security.checkApiCallAllowed(request);
|
||||
|
||||
try {
|
||||
ElectrumX.Server server = new ElectrumX.Server(
|
||||
serverInfo.getHostName(),
|
||||
ChainableServer.ConnectionType.valueOf(serverInfo.getConnectionType()),
|
||||
serverInfo.getPort()
|
||||
);
|
||||
|
||||
if( CrossChainUtils.addServer( Dogecoin.getInstance(), server )) {
|
||||
return "true";
|
||||
}
|
||||
else {
|
||||
return "false";
|
||||
}
|
||||
}
|
||||
catch (IllegalArgumentException | NullPointerException e) {
|
||||
throw ApiExceptionFactory.INSTANCE.createException(request, ApiError.INVALID_DATA);
|
||||
}
|
||||
catch (Exception e) {
|
||||
return "false";
|
||||
}
|
||||
}
|
||||
|
||||
@POST
|
||||
@Path("/removeserver")
|
||||
@Operation(
|
||||
summary = "Remove server from list of Dogecoin servers",
|
||||
description = "Remove server from list of Dogecoin servers",
|
||||
requestBody = @RequestBody(
|
||||
required = true,
|
||||
content = @Content(
|
||||
mediaType = MediaType.APPLICATION_JSON,
|
||||
schema = @Schema(
|
||||
implementation = ServerInfo.class
|
||||
)
|
||||
)
|
||||
),
|
||||
responses = {
|
||||
@ApiResponse(
|
||||
description = "true if removed, otherwise",
|
||||
content = @Content(
|
||||
schema = @Schema(
|
||||
type = "string"
|
||||
)
|
||||
)
|
||||
)
|
||||
}
|
||||
|
||||
)
|
||||
@ApiErrors({ApiError.INVALID_DATA})
|
||||
@SecurityRequirement(name = "apiKey")
|
||||
public String removeServer(@HeaderParam(Security.API_KEY_HEADER) String apiKey, ServerInfo serverInfo) {
|
||||
Security.checkApiCallAllowed(request);
|
||||
|
||||
try {
|
||||
ElectrumX.Server server = new ElectrumX.Server(
|
||||
serverInfo.getHostName(),
|
||||
ChainableServer.ConnectionType.valueOf(serverInfo.getConnectionType()),
|
||||
serverInfo.getPort()
|
||||
);
|
||||
|
||||
if( CrossChainUtils.removeServer( Dogecoin.getInstance(), server ) ) {
|
||||
|
||||
return "true";
|
||||
}
|
||||
else {
|
||||
return "false";
|
||||
}
|
||||
}
|
||||
catch (IllegalArgumentException | NullPointerException e) {
|
||||
throw ApiExceptionFactory.INSTANCE.createException(request, ApiError.INVALID_DATA);
|
||||
}
|
||||
catch (Exception e) {
|
||||
return "false";
|
||||
}
|
||||
}
|
||||
|
||||
@POST
|
||||
@Path("/setcurrentserver")
|
||||
@Operation(
|
||||
summary = "Set current Dogecoin server",
|
||||
description = "Set current Dogecoin server",
|
||||
requestBody = @RequestBody(
|
||||
required = true,
|
||||
content = @Content(
|
||||
mediaType = MediaType.APPLICATION_JSON,
|
||||
schema = @Schema(
|
||||
implementation = ServerInfo.class
|
||||
)
|
||||
)
|
||||
),
|
||||
responses = {
|
||||
@ApiResponse(
|
||||
description = "connection info",
|
||||
content = @Content(
|
||||
mediaType = MediaType.APPLICATION_JSON,
|
||||
schema = @Schema(
|
||||
implementation = ServerConnectionInfo.class
|
||||
)
|
||||
)
|
||||
)
|
||||
}
|
||||
|
||||
)
|
||||
@ApiErrors({ApiError.INVALID_DATA})
|
||||
@SecurityRequirement(name = "apiKey")
|
||||
public ServerConnectionInfo setCurrentServer(@HeaderParam(Security.API_KEY_HEADER) String apiKey, ServerInfo serverInfo) {
|
||||
Security.checkApiCallAllowed(request);
|
||||
|
||||
if( serverInfo.getConnectionType() == null ||
|
||||
serverInfo.getHostName() == null) throw ApiExceptionFactory.INSTANCE.createException(request, ApiError.INVALID_DATA);
|
||||
try {
|
||||
return CrossChainUtils.setCurrentServer( Dogecoin.getInstance(), serverInfo );
|
||||
}
|
||||
catch (IllegalArgumentException e) {
|
||||
throw ApiExceptionFactory.INSTANCE.createException(request, ApiError.INVALID_DATA);
|
||||
}
|
||||
catch (Exception e) {
|
||||
return new ServerConnectionInfo(
|
||||
serverInfo,
|
||||
CrossChainUtils.CORE_API_CALL,
|
||||
true,
|
||||
false,
|
||||
System.currentTimeMillis(),
|
||||
CrossChainUtils.getNotes(e));
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@GET
|
||||
@Path("/feekb")
|
||||
@Operation(
|
||||
summary = "Returns Dogecoin fee per Kb.",
|
||||
description = "Returns Dogecoin fee per Kb.",
|
||||
responses = {
|
||||
@ApiResponse(
|
||||
content = @Content(
|
||||
schema = @Schema(
|
||||
type = "number"
|
||||
)
|
||||
)
|
||||
)
|
||||
}
|
||||
)
|
||||
public String getDogecoinFeePerKb() {
|
||||
Dogecoin dogecoin = Dogecoin.getInstance();
|
||||
|
||||
return String.valueOf(dogecoin.getFeePerKb().value);
|
||||
}
|
||||
|
||||
@POST
|
||||
@Path("/updatefeekb")
|
||||
@Operation(
|
||||
summary = "Sets Dogecoin fee per Kb.",
|
||||
description = "Sets Dogecoin fee per Kb.",
|
||||
requestBody = @RequestBody(
|
||||
required = true,
|
||||
content = @Content(
|
||||
mediaType = MediaType.TEXT_PLAIN,
|
||||
schema = @Schema(
|
||||
type = "number",
|
||||
description = "the fee per Kb",
|
||||
example = "100"
|
||||
)
|
||||
)
|
||||
),
|
||||
responses = {
|
||||
@ApiResponse(
|
||||
content = @Content(mediaType = MediaType.TEXT_PLAIN, schema = @Schema(type = "number", description = "fee"))
|
||||
)
|
||||
}
|
||||
)
|
||||
@ApiErrors({ApiError.INVALID_PRIVATE_KEY, ApiError.INVALID_CRITERIA})
|
||||
public String setDogecoinFeePerKb(@HeaderParam(Security.API_KEY_HEADER) String apiKey, String fee) {
|
||||
Security.checkApiCallAllowed(request);
|
||||
|
||||
Dogecoin dogecoin = Dogecoin.getInstance();
|
||||
|
||||
try {
|
||||
return CrossChainUtils.setFeePerKb(dogecoin, fee);
|
||||
} catch (IllegalArgumentException e) {
|
||||
throw ApiExceptionFactory.INSTANCE.createException(request, ApiError.INVALID_CRITERIA);
|
||||
}
|
||||
}
|
||||
|
||||
@GET
|
||||
@Path("/feeceiling")
|
||||
@Operation(
|
||||
summary = "Returns Dogecoin fee per Kb.",
|
||||
description = "Returns Dogecoin fee per Kb.",
|
||||
responses = {
|
||||
@ApiResponse(
|
||||
content = @Content(
|
||||
schema = @Schema(
|
||||
type = "number"
|
||||
)
|
||||
)
|
||||
)
|
||||
}
|
||||
)
|
||||
public String getDogecoinFeeCeiling() {
|
||||
Dogecoin dogecoin = Dogecoin.getInstance();
|
||||
|
||||
return String.valueOf(dogecoin.getFeeCeiling());
|
||||
}
|
||||
|
||||
@POST
|
||||
@Path("/updatefeeceiling")
|
||||
@Operation(
|
||||
summary = "Sets Dogecoin fee ceiling.",
|
||||
description = "Sets Dogecoin fee ceiling.",
|
||||
requestBody = @RequestBody(
|
||||
required = true,
|
||||
content = @Content(
|
||||
mediaType = MediaType.TEXT_PLAIN,
|
||||
schema = @Schema(
|
||||
type = "number",
|
||||
description = "the fee",
|
||||
example = "100"
|
||||
)
|
||||
)
|
||||
),
|
||||
responses = {
|
||||
@ApiResponse(
|
||||
content = @Content(mediaType = MediaType.TEXT_PLAIN, schema = @Schema(type = "number", description = "fee"))
|
||||
)
|
||||
}
|
||||
)
|
||||
@ApiErrors({ApiError.INVALID_PRIVATE_KEY, ApiError.INVALID_CRITERIA})
|
||||
public String setDogecoinFeeCeiling(@HeaderParam(Security.API_KEY_HEADER) String apiKey, String fee) {
|
||||
Security.checkApiCallAllowed(request);
|
||||
|
||||
Dogecoin dogecoin = Dogecoin.getInstance();
|
||||
|
||||
try {
|
||||
return CrossChainUtils.setFeeCeiling(dogecoin, fee);
|
||||
}
|
||||
catch (IllegalArgumentException e) {
|
||||
throw ApiExceptionFactory.INSTANCE.createException(request, ApiError.INVALID_CRITERIA);
|
||||
}
|
||||
}
|
||||
}
|
@@ -16,8 +16,12 @@ import org.qortal.api.Security;
|
||||
import org.qortal.api.model.crosschain.AddressRequest;
|
||||
import org.qortal.api.model.crosschain.LitecoinSendRequest;
|
||||
import org.qortal.crosschain.AddressInfo;
|
||||
import org.qortal.crosschain.ChainableServer;
|
||||
import org.qortal.crosschain.ElectrumX;
|
||||
import org.qortal.crosschain.ForeignBlockchainException;
|
||||
import org.qortal.crosschain.Litecoin;
|
||||
import org.qortal.crosschain.ServerConnectionInfo;
|
||||
import org.qortal.crosschain.ServerInfo;
|
||||
import org.qortal.crosschain.SimpleTransaction;
|
||||
import org.qortal.crosschain.ServerConfigurationInfo;
|
||||
|
||||
@@ -266,6 +270,180 @@ public class CrossChainLitecoinResource {
|
||||
return CrossChainUtils.buildServerConfigurationInfo(Litecoin.getInstance());
|
||||
}
|
||||
|
||||
@GET
|
||||
@Path("/serverconnectionhistory")
|
||||
@Operation(
|
||||
summary = "Returns Litecoin server connection history",
|
||||
description = "Returns Litecoin server connection history",
|
||||
responses = {
|
||||
@ApiResponse(
|
||||
content = @Content(array = @ArraySchema( schema = @Schema( implementation = ServerConnectionInfo.class ) ) )
|
||||
)
|
||||
}
|
||||
)
|
||||
public List<ServerConnectionInfo> getServerConnectionHistory() {
|
||||
|
||||
return CrossChainUtils.buildServerConnectionHistory(Litecoin.getInstance());
|
||||
}
|
||||
|
||||
@POST
|
||||
@Path("/addserver")
|
||||
@Operation(
|
||||
summary = "Add server to list of Litecoin servers",
|
||||
description = "Add server to list of Litecoin servers",
|
||||
requestBody = @RequestBody(
|
||||
required = true,
|
||||
content = @Content(
|
||||
mediaType = MediaType.APPLICATION_JSON,
|
||||
schema = @Schema(
|
||||
implementation = ServerInfo.class
|
||||
)
|
||||
)
|
||||
),
|
||||
responses = {
|
||||
@ApiResponse(
|
||||
description = "true if added, false if not added",
|
||||
content = @Content(
|
||||
schema = @Schema(
|
||||
type = "string"
|
||||
)
|
||||
)
|
||||
)
|
||||
}
|
||||
|
||||
)
|
||||
@ApiErrors({ApiError.INVALID_DATA})
|
||||
@SecurityRequirement(name = "apiKey")
|
||||
public String addServer(@HeaderParam(Security.API_KEY_HEADER) String apiKey, ServerInfo serverInfo) {
|
||||
Security.checkApiCallAllowed(request);
|
||||
|
||||
try {
|
||||
ElectrumX.Server server = new ElectrumX.Server(
|
||||
serverInfo.getHostName(),
|
||||
ChainableServer.ConnectionType.valueOf(serverInfo.getConnectionType()),
|
||||
serverInfo.getPort()
|
||||
);
|
||||
|
||||
if( CrossChainUtils.addServer( Litecoin.getInstance(), server )) {
|
||||
return "true";
|
||||
}
|
||||
else {
|
||||
return "false";
|
||||
}
|
||||
}
|
||||
catch (IllegalArgumentException | NullPointerException e) {
|
||||
throw ApiExceptionFactory.INSTANCE.createException(request, ApiError.INVALID_DATA);
|
||||
}
|
||||
catch (Exception e) {
|
||||
return "false";
|
||||
}
|
||||
}
|
||||
|
||||
@POST
|
||||
@Path("/removeserver")
|
||||
@Operation(
|
||||
summary = "Remove server from list of Litecoin servers",
|
||||
description = "Remove server from list of Litecoin servers",
|
||||
requestBody = @RequestBody(
|
||||
required = true,
|
||||
content = @Content(
|
||||
mediaType = MediaType.APPLICATION_JSON,
|
||||
schema = @Schema(
|
||||
implementation = ServerInfo.class
|
||||
)
|
||||
)
|
||||
),
|
||||
responses = {
|
||||
@ApiResponse(
|
||||
description = "true if removed, otherwise",
|
||||
content = @Content(
|
||||
schema = @Schema(
|
||||
type = "string"
|
||||
)
|
||||
)
|
||||
)
|
||||
}
|
||||
|
||||
)
|
||||
@ApiErrors({ApiError.INVALID_DATA})
|
||||
@SecurityRequirement(name = "apiKey")
|
||||
public String removeServer(@HeaderParam(Security.API_KEY_HEADER) String apiKey, ServerInfo serverInfo) {
|
||||
Security.checkApiCallAllowed(request);
|
||||
|
||||
try {
|
||||
ElectrumX.Server server = new ElectrumX.Server(
|
||||
serverInfo.getHostName(),
|
||||
ChainableServer.ConnectionType.valueOf(serverInfo.getConnectionType()),
|
||||
serverInfo.getPort()
|
||||
);
|
||||
|
||||
if( CrossChainUtils.removeServer( Litecoin.getInstance(), server ) ) {
|
||||
|
||||
return "true";
|
||||
}
|
||||
else {
|
||||
return "false";
|
||||
}
|
||||
}
|
||||
catch (IllegalArgumentException | NullPointerException e) {
|
||||
throw ApiExceptionFactory.INSTANCE.createException(request, ApiError.INVALID_DATA);
|
||||
}
|
||||
catch (Exception e) {
|
||||
return "false";
|
||||
}
|
||||
}
|
||||
|
||||
@POST
|
||||
@Path("/setcurrentserver")
|
||||
@Operation(
|
||||
summary = "Set current Litecoin server",
|
||||
description = "Set current Litecoin server",
|
||||
requestBody = @RequestBody(
|
||||
required = true,
|
||||
content = @Content(
|
||||
mediaType = MediaType.APPLICATION_JSON,
|
||||
schema = @Schema(
|
||||
implementation = ServerInfo.class
|
||||
)
|
||||
)
|
||||
),
|
||||
responses = {
|
||||
@ApiResponse(
|
||||
description = "connection info",
|
||||
content = @Content(
|
||||
mediaType = MediaType.APPLICATION_JSON,
|
||||
schema = @Schema(
|
||||
implementation = ServerConnectionInfo.class
|
||||
)
|
||||
)
|
||||
)
|
||||
}
|
||||
|
||||
)
|
||||
@ApiErrors({ApiError.INVALID_DATA})
|
||||
@SecurityRequirement(name = "apiKey")
|
||||
public ServerConnectionInfo setCurrentServer(@HeaderParam(Security.API_KEY_HEADER) String apiKey, ServerInfo serverInfo) {
|
||||
Security.checkApiCallAllowed(request);
|
||||
|
||||
if( serverInfo.getConnectionType() == null ||
|
||||
serverInfo.getHostName() == null) throw ApiExceptionFactory.INSTANCE.createException(request, ApiError.INVALID_DATA);
|
||||
try {
|
||||
return CrossChainUtils.setCurrentServer( Litecoin.getInstance(), serverInfo );
|
||||
}
|
||||
catch (IllegalArgumentException e) {
|
||||
throw ApiExceptionFactory.INSTANCE.createException(request, ApiError.INVALID_DATA);
|
||||
}
|
||||
catch (Exception e) {
|
||||
return new ServerConnectionInfo(
|
||||
serverInfo,
|
||||
CrossChainUtils.CORE_API_CALL,
|
||||
true,
|
||||
false,
|
||||
System.currentTimeMillis(),
|
||||
CrossChainUtils.getNotes(e));
|
||||
}
|
||||
}
|
||||
|
||||
@POST
|
||||
@Path("/repair")
|
||||
@Operation(
|
||||
@@ -304,4 +482,117 @@ public class CrossChainLitecoinResource {
|
||||
throw ApiExceptionFactory.INSTANCE.createException(request, ApiError.FOREIGN_BLOCKCHAIN_NETWORK_ISSUE);
|
||||
}
|
||||
}
|
||||
|
||||
@GET
|
||||
@Path("/feekb")
|
||||
@Operation(
|
||||
summary = "Returns Litecoin fee per Kb.",
|
||||
description = "Returns Litecoin fee per Kb.",
|
||||
responses = {
|
||||
@ApiResponse(
|
||||
content = @Content(
|
||||
schema = @Schema(
|
||||
type = "number"
|
||||
)
|
||||
)
|
||||
)
|
||||
}
|
||||
)
|
||||
public String getLitecoinFeePerKb() {
|
||||
Litecoin litecoin = Litecoin.getInstance();
|
||||
|
||||
return String.valueOf(litecoin.getFeePerKb().value);
|
||||
}
|
||||
|
||||
@POST
|
||||
@Path("/updatefeekb")
|
||||
@Operation(
|
||||
summary = "Sets Litecoin fee per Kb.",
|
||||
description = "Sets Litecoin fee per Kb.",
|
||||
requestBody = @RequestBody(
|
||||
required = true,
|
||||
content = @Content(
|
||||
mediaType = MediaType.TEXT_PLAIN,
|
||||
schema = @Schema(
|
||||
type = "number",
|
||||
description = "the fee per Kb",
|
||||
example = "100"
|
||||
)
|
||||
)
|
||||
),
|
||||
responses = {
|
||||
@ApiResponse(
|
||||
content = @Content(mediaType = MediaType.TEXT_PLAIN, schema = @Schema(type = "number", description = "fee"))
|
||||
)
|
||||
}
|
||||
)
|
||||
@ApiErrors({ApiError.INVALID_PRIVATE_KEY, ApiError.INVALID_CRITERIA})
|
||||
public String setLitecoinFeePerKb(@HeaderParam(Security.API_KEY_HEADER) String apiKey, String fee) {
|
||||
Security.checkApiCallAllowed(request);
|
||||
|
||||
Litecoin litecoin = Litecoin.getInstance();
|
||||
|
||||
try {
|
||||
return CrossChainUtils.setFeePerKb(litecoin, fee);
|
||||
} catch (IllegalArgumentException e) {
|
||||
throw ApiExceptionFactory.INSTANCE.createException(request, ApiError.INVALID_CRITERIA);
|
||||
}
|
||||
}
|
||||
|
||||
@GET
|
||||
@Path("/feeceiling")
|
||||
@Operation(
|
||||
summary = "Returns Litecoin fee per Kb.",
|
||||
description = "Returns Litecoin fee per Kb.",
|
||||
responses = {
|
||||
@ApiResponse(
|
||||
content = @Content(
|
||||
schema = @Schema(
|
||||
type = "number"
|
||||
)
|
||||
)
|
||||
)
|
||||
}
|
||||
)
|
||||
public String getLitecoinFeeCeiling() {
|
||||
Litecoin litecoin = Litecoin.getInstance();
|
||||
|
||||
return String.valueOf(litecoin.getFeeCeiling());
|
||||
}
|
||||
|
||||
@POST
|
||||
@Path("/updatefeeceiling")
|
||||
@Operation(
|
||||
summary = "Sets Litecoin fee ceiling.",
|
||||
description = "Sets Litecoin fee ceiling.",
|
||||
requestBody = @RequestBody(
|
||||
required = true,
|
||||
content = @Content(
|
||||
mediaType = MediaType.TEXT_PLAIN,
|
||||
schema = @Schema(
|
||||
type = "number",
|
||||
description = "the fee",
|
||||
example = "100"
|
||||
)
|
||||
)
|
||||
),
|
||||
responses = {
|
||||
@ApiResponse(
|
||||
content = @Content(mediaType = MediaType.TEXT_PLAIN, schema = @Schema(type = "number", description = "fee"))
|
||||
)
|
||||
}
|
||||
)
|
||||
@ApiErrors({ApiError.INVALID_PRIVATE_KEY, ApiError.INVALID_CRITERIA})
|
||||
public String setLitecoinFeeCeiling(@HeaderParam(Security.API_KEY_HEADER) String apiKey, String fee) {
|
||||
Security.checkApiCallAllowed(request);
|
||||
|
||||
Litecoin litecoin = Litecoin.getInstance();
|
||||
|
||||
try {
|
||||
return CrossChainUtils.setFeeCeiling(litecoin, fee);
|
||||
}
|
||||
catch (IllegalArgumentException e) {
|
||||
throw ApiExceptionFactory.INSTANCE.createException(request, ApiError.INVALID_CRITERIA);
|
||||
}
|
||||
}
|
||||
}
|
@@ -13,8 +13,12 @@ import org.qortal.api.ApiErrors;
|
||||
import org.qortal.api.ApiExceptionFactory;
|
||||
import org.qortal.api.Security;
|
||||
import org.qortal.api.model.crosschain.PirateChainSendRequest;
|
||||
import org.qortal.crosschain.ChainableServer;
|
||||
import org.qortal.crosschain.ForeignBlockchainException;
|
||||
import org.qortal.crosschain.PirateChain;
|
||||
import org.qortal.crosschain.PirateLightClient;
|
||||
import org.qortal.crosschain.ServerConnectionInfo;
|
||||
import org.qortal.crosschain.ServerInfo;
|
||||
import org.qortal.crosschain.SimpleTransaction;
|
||||
import org.qortal.crosschain.ServerConfigurationInfo;
|
||||
|
||||
@@ -351,4 +355,291 @@ public class CrossChainPirateChainResource {
|
||||
|
||||
return CrossChainUtils.buildServerConfigurationInfo(PirateChain.getInstance());
|
||||
}
|
||||
|
||||
@GET
|
||||
@Path("/serverconnectionhistory")
|
||||
@Operation(
|
||||
summary = "Returns Pirate Chain server connection history",
|
||||
description = "Returns Pirate Chain server connection history",
|
||||
responses = {
|
||||
@ApiResponse(
|
||||
content = @Content(array = @ArraySchema( schema = @Schema( implementation = ServerConnectionInfo.class ) ) )
|
||||
)
|
||||
}
|
||||
)
|
||||
public List<ServerConnectionInfo> getServerConnectionHistory() {
|
||||
|
||||
return CrossChainUtils.buildServerConnectionHistory(PirateChain.getInstance());
|
||||
}
|
||||
|
||||
@POST
|
||||
@Path("/addserver")
|
||||
@Operation(
|
||||
summary = "Add server to list of Pirate Chain servers",
|
||||
description = "Add server to list of Pirate Chain servers",
|
||||
requestBody = @RequestBody(
|
||||
required = true,
|
||||
content = @Content(
|
||||
mediaType = MediaType.APPLICATION_JSON,
|
||||
schema = @Schema(
|
||||
implementation = ServerInfo.class
|
||||
)
|
||||
)
|
||||
),
|
||||
responses = {
|
||||
@ApiResponse(
|
||||
description = "true if added, false if not added",
|
||||
content = @Content(
|
||||
schema = @Schema(
|
||||
type = "string"
|
||||
)
|
||||
)
|
||||
)
|
||||
}
|
||||
|
||||
)
|
||||
@ApiErrors({ApiError.INVALID_DATA})
|
||||
@SecurityRequirement(name = "apiKey")
|
||||
public String addServerInfo(@HeaderParam(Security.API_KEY_HEADER) String apiKey, ServerInfo serverInfo) {
|
||||
Security.checkApiCallAllowed(request);
|
||||
|
||||
try {
|
||||
PirateLightClient.Server server = new PirateLightClient.Server(
|
||||
serverInfo.getHostName(),
|
||||
ChainableServer.ConnectionType.valueOf(serverInfo.getConnectionType()),
|
||||
serverInfo.getPort()
|
||||
);
|
||||
|
||||
if( CrossChainUtils.addServer( PirateChain.getInstance(), server )) {
|
||||
return "true";
|
||||
}
|
||||
else {
|
||||
return "false";
|
||||
}
|
||||
}
|
||||
catch (IllegalArgumentException | NullPointerException e) {
|
||||
throw ApiExceptionFactory.INSTANCE.createException(request, ApiError.INVALID_DATA);
|
||||
}
|
||||
catch (Exception e) {
|
||||
return "false";
|
||||
}
|
||||
}
|
||||
|
||||
@POST
|
||||
@Path("/removeserver")
|
||||
@Operation(
|
||||
summary = "Remove server from list of Pirate Chain servers",
|
||||
description = "Remove server from list of Pirate Chain servers",
|
||||
requestBody = @RequestBody(
|
||||
required = true,
|
||||
content = @Content(
|
||||
mediaType = MediaType.APPLICATION_JSON,
|
||||
schema = @Schema(
|
||||
implementation = ServerInfo.class
|
||||
)
|
||||
)
|
||||
),
|
||||
responses = {
|
||||
@ApiResponse(
|
||||
description = "true if removed, otherwise",
|
||||
content = @Content(
|
||||
schema = @Schema(
|
||||
type = "string"
|
||||
)
|
||||
)
|
||||
)
|
||||
}
|
||||
|
||||
)
|
||||
@ApiErrors({ApiError.INVALID_DATA})
|
||||
@SecurityRequirement(name = "apiKey")
|
||||
public String removeServerInfo(@HeaderParam(Security.API_KEY_HEADER) String apiKey, ServerInfo serverInfo) {
|
||||
Security.checkApiCallAllowed(request);
|
||||
|
||||
try {
|
||||
PirateLightClient.Server server = new PirateLightClient.Server(
|
||||
serverInfo.getHostName(),
|
||||
ChainableServer.ConnectionType.valueOf(serverInfo.getConnectionType()),
|
||||
serverInfo.getPort()
|
||||
);
|
||||
|
||||
if( CrossChainUtils.removeServer( PirateChain.getInstance(), server ) ) {
|
||||
|
||||
return "true";
|
||||
}
|
||||
else {
|
||||
return "false";
|
||||
}
|
||||
}
|
||||
catch (IllegalArgumentException | NullPointerException e) {
|
||||
throw ApiExceptionFactory.INSTANCE.createException(request, ApiError.INVALID_DATA);
|
||||
}
|
||||
catch (Exception e) {
|
||||
return "false";
|
||||
}
|
||||
}
|
||||
|
||||
@POST
|
||||
@Path("/setcurrentserver")
|
||||
@Operation(
|
||||
summary = "Set current Pirate Chain server",
|
||||
description = "Set current Pirate Chain server",
|
||||
requestBody = @RequestBody(
|
||||
required = true,
|
||||
content = @Content(
|
||||
mediaType = MediaType.APPLICATION_JSON,
|
||||
schema = @Schema(
|
||||
implementation = ServerInfo.class
|
||||
)
|
||||
)
|
||||
),
|
||||
responses = {
|
||||
@ApiResponse(
|
||||
description = "connection info",
|
||||
content = @Content(
|
||||
mediaType = MediaType.APPLICATION_JSON,
|
||||
schema = @Schema(
|
||||
implementation = ServerConnectionInfo.class
|
||||
)
|
||||
)
|
||||
)
|
||||
}
|
||||
|
||||
)
|
||||
@ApiErrors({ApiError.INVALID_DATA})
|
||||
@SecurityRequirement(name = "apiKey")
|
||||
public ServerConnectionInfo setCurrentServerInfo(@HeaderParam(Security.API_KEY_HEADER) String apiKey, ServerInfo serverInfo) {
|
||||
Security.checkApiCallAllowed(request);
|
||||
|
||||
if( serverInfo.getConnectionType() == null ||
|
||||
serverInfo.getHostName() == null) throw ApiExceptionFactory.INSTANCE.createException(request, ApiError.INVALID_DATA);
|
||||
try {
|
||||
return CrossChainUtils.setCurrentServer( PirateChain.getInstance(), serverInfo );
|
||||
}
|
||||
catch (IllegalArgumentException e) {
|
||||
throw ApiExceptionFactory.INSTANCE.createException(request, ApiError.INVALID_DATA);
|
||||
}
|
||||
catch (Exception e) {
|
||||
return new ServerConnectionInfo(
|
||||
serverInfo,
|
||||
CrossChainUtils.CORE_API_CALL,
|
||||
true,
|
||||
false,
|
||||
System.currentTimeMillis(),
|
||||
CrossChainUtils.getNotes(e));
|
||||
}
|
||||
}
|
||||
|
||||
@GET
|
||||
@Path("/feekb")
|
||||
@Operation(
|
||||
summary = "Returns PirateChain fee per Kb.",
|
||||
description = "Returns PirateChain fee per Kb.",
|
||||
responses = {
|
||||
@ApiResponse(
|
||||
content = @Content(
|
||||
schema = @Schema(
|
||||
type = "number"
|
||||
)
|
||||
)
|
||||
)
|
||||
}
|
||||
)
|
||||
public String getPirateChainFeePerKb() {
|
||||
PirateChain pirateChain = PirateChain.getInstance();
|
||||
|
||||
return String.valueOf(pirateChain.getFeePerKb().value);
|
||||
}
|
||||
|
||||
@POST
|
||||
@Path("/updatefeekb")
|
||||
@Operation(
|
||||
summary = "Sets PirateChain fee per Kb.",
|
||||
description = "Sets PirateChain fee per Kb.",
|
||||
requestBody = @RequestBody(
|
||||
required = true,
|
||||
content = @Content(
|
||||
mediaType = MediaType.TEXT_PLAIN,
|
||||
schema = @Schema(
|
||||
type = "number",
|
||||
description = "the fee per Kb",
|
||||
example = "100"
|
||||
)
|
||||
)
|
||||
),
|
||||
responses = {
|
||||
@ApiResponse(
|
||||
content = @Content(mediaType = MediaType.TEXT_PLAIN, schema = @Schema(type = "number", description = "fee"))
|
||||
)
|
||||
}
|
||||
)
|
||||
@ApiErrors({ApiError.INVALID_PRIVATE_KEY, ApiError.INVALID_CRITERIA})
|
||||
public String setPirateChainFeePerKb(@HeaderParam(Security.API_KEY_HEADER) String apiKey, String fee) {
|
||||
Security.checkApiCallAllowed(request);
|
||||
|
||||
PirateChain pirateChain = PirateChain.getInstance();
|
||||
|
||||
try {
|
||||
return CrossChainUtils.setFeePerKb(pirateChain, fee);
|
||||
} catch (IllegalArgumentException e) {
|
||||
throw ApiExceptionFactory.INSTANCE.createException(request, ApiError.INVALID_CRITERIA);
|
||||
}
|
||||
}
|
||||
|
||||
@GET
|
||||
@Path("/feeceiling")
|
||||
@Operation(
|
||||
summary = "Returns PirateChain fee per Kb.",
|
||||
description = "Returns PirateChain fee per Kb.",
|
||||
responses = {
|
||||
@ApiResponse(
|
||||
content = @Content(
|
||||
schema = @Schema(
|
||||
type = "number"
|
||||
)
|
||||
)
|
||||
)
|
||||
}
|
||||
)
|
||||
public String getPirateChainFeeCeiling() {
|
||||
PirateChain pirateChain = PirateChain.getInstance();
|
||||
|
||||
return String.valueOf(pirateChain.getFeeCeiling());
|
||||
}
|
||||
|
||||
@POST
|
||||
@Path("/updatefeeceiling")
|
||||
@Operation(
|
||||
summary = "Sets PirateChain fee ceiling.",
|
||||
description = "Sets PirateChain fee ceiling.",
|
||||
requestBody = @RequestBody(
|
||||
required = true,
|
||||
content = @Content(
|
||||
mediaType = MediaType.TEXT_PLAIN,
|
||||
schema = @Schema(
|
||||
type = "number",
|
||||
description = "the fee",
|
||||
example = "100"
|
||||
)
|
||||
)
|
||||
),
|
||||
responses = {
|
||||
@ApiResponse(
|
||||
content = @Content(mediaType = MediaType.TEXT_PLAIN, schema = @Schema(type = "number", description = "fee"))
|
||||
)
|
||||
}
|
||||
)
|
||||
@ApiErrors({ApiError.INVALID_PRIVATE_KEY, ApiError.INVALID_CRITERIA})
|
||||
public String setPirateChainFeeCeiling(@HeaderParam(Security.API_KEY_HEADER) String apiKey, String fee) {
|
||||
Security.checkApiCallAllowed(request);
|
||||
|
||||
PirateChain pirateChain = PirateChain.getInstance();
|
||||
|
||||
try {
|
||||
return CrossChainUtils.setFeeCeiling(pirateChain, fee);
|
||||
}
|
||||
catch (IllegalArgumentException e) {
|
||||
throw ApiExceptionFactory.INSTANCE.createException(request, ApiError.INVALID_CRITERIA);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@@ -16,8 +16,12 @@ import org.qortal.api.Security;
|
||||
import org.qortal.api.model.crosschain.AddressRequest;
|
||||
import org.qortal.api.model.crosschain.RavencoinSendRequest;
|
||||
import org.qortal.crosschain.AddressInfo;
|
||||
import org.qortal.crosschain.ChainableServer;
|
||||
import org.qortal.crosschain.ElectrumX;
|
||||
import org.qortal.crosschain.ForeignBlockchainException;
|
||||
import org.qortal.crosschain.Ravencoin;
|
||||
import org.qortal.crosschain.ServerConnectionInfo;
|
||||
import org.qortal.crosschain.ServerInfo;
|
||||
import org.qortal.crosschain.SimpleTransaction;
|
||||
import org.qortal.crosschain.ServerConfigurationInfo;
|
||||
|
||||
@@ -265,4 +269,292 @@ public class CrossChainRavencoinResource {
|
||||
|
||||
return CrossChainUtils.buildServerConfigurationInfo(Ravencoin.getInstance());
|
||||
}
|
||||
|
||||
@GET
|
||||
@Path("/serverconnectionhistory")
|
||||
@Operation(
|
||||
summary = "Returns Ravencoin server connection history",
|
||||
description = "Returns Ravencoin server connection history",
|
||||
responses = {
|
||||
@ApiResponse(
|
||||
content = @Content(array = @ArraySchema( schema = @Schema( implementation = ServerConnectionInfo.class ) ) )
|
||||
)
|
||||
}
|
||||
)
|
||||
public List<ServerConnectionInfo> getServerConnectionHistory() {
|
||||
|
||||
return CrossChainUtils.buildServerConnectionHistory(Ravencoin.getInstance());
|
||||
}
|
||||
|
||||
@POST
|
||||
@Path("/addserver")
|
||||
@Operation(
|
||||
summary = "Add server to list of Ravencoin servers",
|
||||
description = "Add server to list of Ravencoin servers",
|
||||
requestBody = @RequestBody(
|
||||
required = true,
|
||||
content = @Content(
|
||||
mediaType = MediaType.APPLICATION_JSON,
|
||||
schema = @Schema(
|
||||
implementation = ServerInfo.class
|
||||
)
|
||||
)
|
||||
),
|
||||
responses = {
|
||||
@ApiResponse(
|
||||
description = "true if added, false if not added",
|
||||
content = @Content(
|
||||
schema = @Schema(
|
||||
type = "string"
|
||||
)
|
||||
)
|
||||
)
|
||||
}
|
||||
|
||||
)
|
||||
@ApiErrors({ApiError.INVALID_DATA})
|
||||
@SecurityRequirement(name = "apiKey")
|
||||
public String addServer(@HeaderParam(Security.API_KEY_HEADER) String apiKey, ServerInfo serverInfo) {
|
||||
Security.checkApiCallAllowed(request);
|
||||
|
||||
try {
|
||||
ElectrumX.Server server = new ElectrumX.Server(
|
||||
serverInfo.getHostName(),
|
||||
ChainableServer.ConnectionType.valueOf(serverInfo.getConnectionType()),
|
||||
serverInfo.getPort()
|
||||
);
|
||||
|
||||
if( CrossChainUtils.addServer( Ravencoin.getInstance(), server )) {
|
||||
return "true";
|
||||
}
|
||||
else {
|
||||
return "false";
|
||||
}
|
||||
}
|
||||
catch (IllegalArgumentException | NullPointerException e) {
|
||||
throw ApiExceptionFactory.INSTANCE.createException(request, ApiError.INVALID_DATA);
|
||||
}
|
||||
catch (Exception e) {
|
||||
return "false";
|
||||
}
|
||||
}
|
||||
|
||||
@POST
|
||||
@Path("/removeserver")
|
||||
@Operation(
|
||||
summary = "Remove server from list of Ravencoin servers",
|
||||
description = "Remove server from list of Ravencoin servers",
|
||||
requestBody = @RequestBody(
|
||||
required = true,
|
||||
content = @Content(
|
||||
mediaType = MediaType.APPLICATION_JSON,
|
||||
schema = @Schema(
|
||||
implementation = ServerInfo.class
|
||||
)
|
||||
)
|
||||
),
|
||||
responses = {
|
||||
@ApiResponse(
|
||||
description = "true if removed, otherwise",
|
||||
content = @Content(
|
||||
schema = @Schema(
|
||||
type = "string"
|
||||
)
|
||||
)
|
||||
)
|
||||
}
|
||||
|
||||
)
|
||||
@ApiErrors({ApiError.INVALID_DATA})
|
||||
@SecurityRequirement(name = "apiKey")
|
||||
public String removeServer(@HeaderParam(Security.API_KEY_HEADER) String apiKey, ServerInfo serverInfo) {
|
||||
Security.checkApiCallAllowed(request);
|
||||
|
||||
try {
|
||||
ElectrumX.Server server = new ElectrumX.Server(
|
||||
serverInfo.getHostName(),
|
||||
ChainableServer.ConnectionType.valueOf(serverInfo.getConnectionType()),
|
||||
serverInfo.getPort()
|
||||
);
|
||||
|
||||
if( CrossChainUtils.removeServer( Ravencoin.getInstance(), server ) ) {
|
||||
|
||||
return "true";
|
||||
}
|
||||
else {
|
||||
return "false";
|
||||
}
|
||||
}
|
||||
catch (IllegalArgumentException | NullPointerException e) {
|
||||
throw ApiExceptionFactory.INSTANCE.createException(request, ApiError.INVALID_DATA);
|
||||
}
|
||||
catch (Exception e) {
|
||||
return "false";
|
||||
}
|
||||
}
|
||||
|
||||
@POST
|
||||
@Path("/setcurrentserver")
|
||||
@Operation(
|
||||
summary = "Set current Ravencoin server",
|
||||
description = "Set current Ravencoin server",
|
||||
requestBody = @RequestBody(
|
||||
required = true,
|
||||
content = @Content(
|
||||
mediaType = MediaType.APPLICATION_JSON,
|
||||
schema = @Schema(
|
||||
implementation = ServerInfo.class
|
||||
)
|
||||
)
|
||||
),
|
||||
responses = {
|
||||
@ApiResponse(
|
||||
description = "connection info",
|
||||
content = @Content(
|
||||
mediaType = MediaType.APPLICATION_JSON,
|
||||
schema = @Schema(
|
||||
implementation = ServerConnectionInfo.class
|
||||
)
|
||||
)
|
||||
)
|
||||
}
|
||||
|
||||
)
|
||||
@ApiErrors({ApiError.INVALID_DATA})
|
||||
@SecurityRequirement(name = "apiKey")
|
||||
public ServerConnectionInfo setCurrentServer(@HeaderParam(Security.API_KEY_HEADER) String apiKey, ServerInfo serverInfo) {
|
||||
Security.checkApiCallAllowed(request);
|
||||
|
||||
if( serverInfo.getConnectionType() == null ||
|
||||
serverInfo.getHostName() == null) throw ApiExceptionFactory.INSTANCE.createException(request, ApiError.INVALID_DATA);
|
||||
try {
|
||||
return CrossChainUtils.setCurrentServer( Ravencoin.getInstance(), serverInfo );
|
||||
}
|
||||
catch (IllegalArgumentException e) {
|
||||
throw ApiExceptionFactory.INSTANCE.createException(request, ApiError.INVALID_DATA);
|
||||
}
|
||||
catch (Exception e) {
|
||||
return new ServerConnectionInfo(
|
||||
serverInfo,
|
||||
CrossChainUtils.CORE_API_CALL,
|
||||
true,
|
||||
false,
|
||||
System.currentTimeMillis(),
|
||||
CrossChainUtils.getNotes(e));
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@GET
|
||||
@Path("/feekb")
|
||||
@Operation(
|
||||
summary = "Returns Ravencoin fee per Kb.",
|
||||
description = "Returns Ravencoin fee per Kb.",
|
||||
responses = {
|
||||
@ApiResponse(
|
||||
content = @Content(
|
||||
schema = @Schema(
|
||||
type = "number"
|
||||
)
|
||||
)
|
||||
)
|
||||
}
|
||||
)
|
||||
public String getRavencoinFeePerKb() {
|
||||
Ravencoin ravencoin = Ravencoin.getInstance();
|
||||
|
||||
return String.valueOf(ravencoin.getFeePerKb().value);
|
||||
}
|
||||
|
||||
@POST
|
||||
@Path("/updatefeekb")
|
||||
@Operation(
|
||||
summary = "Sets Ravencoin fee per Kb.",
|
||||
description = "Sets Ravencoin fee per Kb.",
|
||||
requestBody = @RequestBody(
|
||||
required = true,
|
||||
content = @Content(
|
||||
mediaType = MediaType.TEXT_PLAIN,
|
||||
schema = @Schema(
|
||||
type = "number",
|
||||
description = "the fee per Kb",
|
||||
example = "100"
|
||||
)
|
||||
)
|
||||
),
|
||||
responses = {
|
||||
@ApiResponse(
|
||||
content = @Content(mediaType = MediaType.TEXT_PLAIN, schema = @Schema(type = "number", description = "fee"))
|
||||
)
|
||||
}
|
||||
)
|
||||
@ApiErrors({ApiError.INVALID_PRIVATE_KEY, ApiError.INVALID_CRITERIA})
|
||||
public String setRavencoinFeePerKb(@HeaderParam(Security.API_KEY_HEADER) String apiKey, String fee) {
|
||||
Security.checkApiCallAllowed(request);
|
||||
|
||||
Ravencoin ravencoin = Ravencoin.getInstance();
|
||||
|
||||
try {
|
||||
return CrossChainUtils.setFeePerKb(ravencoin, fee);
|
||||
} catch (IllegalArgumentException e) {
|
||||
throw ApiExceptionFactory.INSTANCE.createException(request, ApiError.INVALID_CRITERIA);
|
||||
}
|
||||
}
|
||||
|
||||
@GET
|
||||
@Path("/feeceiling")
|
||||
@Operation(
|
||||
summary = "Returns Ravencoin fee per Kb.",
|
||||
description = "Returns Ravencoin fee per Kb.",
|
||||
responses = {
|
||||
@ApiResponse(
|
||||
content = @Content(
|
||||
schema = @Schema(
|
||||
type = "number"
|
||||
)
|
||||
)
|
||||
)
|
||||
}
|
||||
)
|
||||
public String getRavencoinFeeCeiling() {
|
||||
Ravencoin ravencoin = Ravencoin.getInstance();
|
||||
|
||||
return String.valueOf(ravencoin.getFeeCeiling());
|
||||
}
|
||||
|
||||
@POST
|
||||
@Path("/updatefeeceiling")
|
||||
@Operation(
|
||||
summary = "Sets Ravencoin fee ceiling.",
|
||||
description = "Sets Ravencoin fee ceiling.",
|
||||
requestBody = @RequestBody(
|
||||
required = true,
|
||||
content = @Content(
|
||||
mediaType = MediaType.TEXT_PLAIN,
|
||||
schema = @Schema(
|
||||
type = "number",
|
||||
description = "the fee",
|
||||
example = "100"
|
||||
)
|
||||
)
|
||||
),
|
||||
responses = {
|
||||
@ApiResponse(
|
||||
content = @Content(mediaType = MediaType.TEXT_PLAIN, schema = @Schema(type = "number", description = "fee"))
|
||||
)
|
||||
}
|
||||
)
|
||||
@ApiErrors({ApiError.INVALID_PRIVATE_KEY, ApiError.INVALID_CRITERIA})
|
||||
public String setRavencoinFeeCeiling(@HeaderParam(Security.API_KEY_HEADER) String apiKey, String fee) {
|
||||
Security.checkApiCallAllowed(request);
|
||||
|
||||
Ravencoin ravencoin = Ravencoin.getInstance();
|
||||
|
||||
try {
|
||||
return CrossChainUtils.setFeeCeiling(ravencoin, fee);
|
||||
}
|
||||
catch (IllegalArgumentException e) {
|
||||
throw ApiExceptionFactory.INSTANCE.createException(request, ApiError.INVALID_CRITERIA);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@@ -19,11 +19,14 @@ import org.qortal.api.model.CrossChainTradeSummary;
|
||||
import org.qortal.controller.tradebot.TradeBot;
|
||||
import org.qortal.crosschain.ACCT;
|
||||
import org.qortal.crosschain.AcctMode;
|
||||
import org.qortal.crosschain.Bitcoiny;
|
||||
import org.qortal.crosschain.ForeignBlockchainException;
|
||||
import org.qortal.crosschain.SupportedBlockchain;
|
||||
import org.qortal.crypto.Crypto;
|
||||
import org.qortal.data.at.ATData;
|
||||
import org.qortal.data.at.ATStateData;
|
||||
import org.qortal.data.crosschain.CrossChainTradeData;
|
||||
import org.qortal.data.crosschain.TransactionSummary;
|
||||
import org.qortal.data.transaction.BaseTransactionData;
|
||||
import org.qortal.data.transaction.MessageTransactionData;
|
||||
import org.qortal.data.transaction.TransactionData;
|
||||
@@ -47,6 +50,7 @@ import javax.ws.rs.core.Context;
|
||||
import javax.ws.rs.core.MediaType;
|
||||
import java.util.*;
|
||||
import java.util.function.Supplier;
|
||||
import java.util.stream.Collectors;
|
||||
|
||||
@Path("/crosschain")
|
||||
@Tag(name = "Cross-Chain")
|
||||
@@ -372,7 +376,7 @@ public class CrossChainResource {
|
||||
int maximumCount = maxtrades != null ? maxtrades : 10;
|
||||
long minimumPeriod = 4 * 60 * 60 * 1000L; // ms
|
||||
Boolean isFinished = Boolean.TRUE;
|
||||
boolean useInversePrice = (inverse != null && inverse == true);
|
||||
boolean useInversePrice = (inverse != null && inverse);
|
||||
|
||||
try (final Repository repository = RepositoryManager.getRepository()) {
|
||||
Map<ByteArray, Supplier<ACCT>> acctsByCodeHash = SupportedBlockchain.getFilteredAcctMap(foreignBlockchain);
|
||||
@@ -497,6 +501,111 @@ public class CrossChainResource {
|
||||
}
|
||||
}
|
||||
|
||||
@POST
|
||||
@Path("/p2sh")
|
||||
@Operation(
|
||||
summary = "Returns P2SH Address",
|
||||
description = "Get the P2SH address to lock foreign coin in a cross chain trade for QORT",
|
||||
requestBody = @RequestBody(
|
||||
required = true,
|
||||
content = @Content(
|
||||
mediaType = MediaType.TEXT_PLAIN,
|
||||
schema = @Schema(
|
||||
type = "string",
|
||||
description = "the AT address",
|
||||
example = "AKFnu9yBp7tUAc5HAphhfCxRZTYoeKXgUy"
|
||||
)
|
||||
)
|
||||
),
|
||||
responses = {
|
||||
@ApiResponse(
|
||||
content = @Content(mediaType = MediaType.TEXT_PLAIN, schema = @Schema(type = "string", description = "address"))
|
||||
)
|
||||
}
|
||||
)
|
||||
@ApiErrors({ApiError.ADDRESS_UNKNOWN, ApiError.INVALID_CRITERIA})
|
||||
@SecurityRequirement(name = "apiKey")
|
||||
public String getForeignP2SH(@HeaderParam(Security.API_KEY_HEADER) String apiKey, String atAddress) {
|
||||
Security.checkApiCallAllowed(request);
|
||||
|
||||
try (final Repository repository = RepositoryManager.getRepository()) {
|
||||
ATData atData = repository.getATRepository().fromATAddress(atAddress);
|
||||
if (atData == null)
|
||||
throw ApiExceptionFactory.INSTANCE.createException(request, ApiError.ADDRESS_UNKNOWN);
|
||||
|
||||
ACCT acct = SupportedBlockchain.getAcctByCodeHash(atData.getCodeHash());
|
||||
|
||||
if( acct == null || !(acct.getBlockchain() instanceof Bitcoiny) )
|
||||
throw ApiExceptionFactory.INSTANCE.createException(request, ApiError.INVALID_CRITERIA);
|
||||
|
||||
Bitcoiny bitcoiny = (Bitcoiny) acct.getBlockchain();
|
||||
|
||||
CrossChainTradeData crossChainTradeData = acct.populateTradeData(repository, atData);
|
||||
|
||||
Optional<String> p2sh
|
||||
= CrossChainUtils.getP2ShAddressForAT(atAddress, repository, bitcoiny, crossChainTradeData);
|
||||
|
||||
if(p2sh.isPresent()){
|
||||
return p2sh.get();
|
||||
}
|
||||
else{
|
||||
throw ApiExceptionFactory.INSTANCE.createException(request, ApiError.ADDRESS_UNKNOWN);
|
||||
}
|
||||
}
|
||||
catch (DataException e) {
|
||||
throw ApiExceptionFactory.INSTANCE.createCustomException(request, ApiError.REPOSITORY_ISSUE, e.getMessage());
|
||||
}
|
||||
}
|
||||
|
||||
@POST
|
||||
@Path("/txactivity")
|
||||
@Operation(
|
||||
summary = "Returns Foreign Transaction Activity",
|
||||
description = "Get the activity related to foreign coin trading",
|
||||
responses = {
|
||||
@ApiResponse(
|
||||
content = @Content(
|
||||
array = @ArraySchema(
|
||||
schema = @Schema(
|
||||
implementation = TransactionSummary.class
|
||||
)
|
||||
)
|
||||
)
|
||||
)
|
||||
}
|
||||
)
|
||||
@ApiErrors({ApiError.INVALID_CRITERIA, ApiError.REPOSITORY_ISSUE, ApiError.FOREIGN_BLOCKCHAIN_NETWORK_ISSUE})
|
||||
@SecurityRequirement(name = "apiKey")
|
||||
public List<TransactionSummary> getForeignTransactionActivity(@HeaderParam(Security.API_KEY_HEADER) String apiKey, @Parameter(
|
||||
description = "Limit to specific blockchain",
|
||||
example = "LITECOIN",
|
||||
schema = @Schema(implementation = SupportedBlockchain.class)
|
||||
) @QueryParam("foreignBlockchain") SupportedBlockchain foreignBlockchain) {
|
||||
Security.checkApiCallAllowed(request);
|
||||
|
||||
if (!(foreignBlockchain.getInstance() instanceof Bitcoiny))
|
||||
throw ApiExceptionFactory.INSTANCE.createException(request, ApiError.INVALID_CRITERIA);
|
||||
|
||||
Bitcoiny bitcoiny = (Bitcoiny) foreignBlockchain.getInstance() ;
|
||||
|
||||
org.bitcoinj.core.Context.propagate( bitcoiny.getBitcoinjContext() );
|
||||
|
||||
try (final Repository repository = RepositoryManager.getRepository()) {
|
||||
|
||||
// sort from last lock to first lock
|
||||
return CrossChainUtils
|
||||
.getForeignTradeSummaries(foreignBlockchain, repository, bitcoiny).stream()
|
||||
.sorted(Comparator.comparing(TransactionSummary::getLockingTimestamp).reversed())
|
||||
.collect(Collectors.toList());
|
||||
}
|
||||
catch (DataException e) {
|
||||
throw ApiExceptionFactory.INSTANCE.createCustomException(request, ApiError.REPOSITORY_ISSUE, e.getMessage());
|
||||
}
|
||||
catch (ForeignBlockchainException e) {
|
||||
throw ApiExceptionFactory.INSTANCE.createCustomException(request, ApiError.FOREIGN_BLOCKCHAIN_NETWORK_ISSUE, e.getMessage());
|
||||
}
|
||||
}
|
||||
|
||||
private ATData fetchAtDataWithChecking(Repository repository, String atAddress) throws DataException {
|
||||
ATData atData = repository.getATRepository().fromATAddress(atAddress);
|
||||
if (atData == null)
|
||||
|
@@ -2,22 +2,43 @@ package org.qortal.api.resource;
|
||||
|
||||
import org.apache.logging.log4j.LogManager;
|
||||
import org.apache.logging.log4j.Logger;
|
||||
import org.qortal.crosschain.*;
|
||||
import org.bitcoinj.core.Address;
|
||||
import org.bitcoinj.core.Coin;
|
||||
import org.bitcoinj.script.Script;
|
||||
import org.bitcoinj.script.ScriptBuilder;
|
||||
|
||||
import org.qortal.crosschain.*;
|
||||
import org.qortal.data.at.ATData;
|
||||
import org.qortal.data.crosschain.*;
|
||||
import org.qortal.repository.DataException;
|
||||
import org.qortal.repository.Repository;
|
||||
|
||||
import java.util.*;
|
||||
import java.util.stream.Collectors;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.Collection;
|
||||
import java.util.List;
|
||||
|
||||
public class CrossChainUtils {
|
||||
private static final Logger LOGGER = LogManager.getLogger(CrossChainUtils.class);
|
||||
public static final String CORE_API_CALL = "Core API Call";
|
||||
|
||||
public static ServerConfigurationInfo buildServerConfigurationInfo(Bitcoiny blockchain) {
|
||||
|
||||
BitcoinyBlockchainProvider blockchainProvider = blockchain.getBlockchainProvider();
|
||||
|
||||
// the only reason this is called is to ensure the current server is set on the blockchain provider,
|
||||
// if there is an exception, then ignore it
|
||||
try {
|
||||
blockchainProvider.getCurrentHeight();
|
||||
} catch (ForeignBlockchainException e) {
|
||||
LOGGER.warn("Problems getting block height before building server configuration infos");
|
||||
}
|
||||
|
||||
ChainableServer currentServer = blockchainProvider.getCurrentServer();
|
||||
|
||||
return new ServerConfigurationInfo(
|
||||
buildInfos(blockchainProvider.getServers(), currentServer),
|
||||
buildInfos(blockchainProvider.getServers(), currentServer).stream()
|
||||
.sorted(Comparator.comparing(ServerInfo::isCurrent).reversed())
|
||||
.collect(Collectors.toList()),
|
||||
buildInfos(blockchainProvider.getRemainingServers(), currentServer),
|
||||
buildInfos(blockchainProvider.getUselessServers(), currentServer)
|
||||
);
|
||||
@@ -44,4 +65,484 @@ public class CrossChainUtils {
|
||||
|
||||
return infos;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Set Fee Per Kb
|
||||
*
|
||||
* @param bitcoiny the blockchain support
|
||||
* @param fee the fee in satoshis
|
||||
*
|
||||
* @return the fee if valid
|
||||
*
|
||||
* @throws IllegalArgumentException if invalid
|
||||
*/
|
||||
public static String setFeePerKb(Bitcoiny bitcoiny, String fee) throws IllegalArgumentException {
|
||||
|
||||
long satoshis = Long.parseLong(fee);
|
||||
if( satoshis < 0 ) throw new IllegalArgumentException("can't set fee to negative number");
|
||||
|
||||
bitcoiny.setFeePerKb(Coin.valueOf(satoshis) );
|
||||
|
||||
return String.valueOf(bitcoiny.getFeePerKb().value);
|
||||
}
|
||||
|
||||
/**
|
||||
* Set Fee Ceiling
|
||||
*
|
||||
* @param bitcoiny the blockchain support
|
||||
* @param fee the fee in satoshis
|
||||
*
|
||||
* @return the fee if valid
|
||||
*
|
||||
* @throws IllegalArgumentException if invalid
|
||||
*/
|
||||
public static String setFeeCeiling(Bitcoiny bitcoiny, String fee) throws IllegalArgumentException{
|
||||
|
||||
long satoshis = Long.parseLong(fee);
|
||||
if( satoshis < 0 ) throw new IllegalArgumentException("can't set fee to negative number");
|
||||
|
||||
bitcoiny.setFeeCeiling( Long.parseLong(fee));
|
||||
|
||||
return String.valueOf(bitcoiny.getFeeCeiling());
|
||||
}
|
||||
|
||||
/**
|
||||
* Get P2Sh Address For AT
|
||||
*
|
||||
* @param atAddress the AT address
|
||||
* @param repository the repository
|
||||
* @param bitcoiny the blockchain data
|
||||
* @param crossChainTradeData the trade data
|
||||
*
|
||||
* @return the p2sh address for the trade, if there is one
|
||||
*
|
||||
* @throws DataException
|
||||
*/
|
||||
public static Optional<String> getP2ShAddressForAT(
|
||||
String atAddress,
|
||||
Repository repository,
|
||||
Bitcoiny bitcoiny,
|
||||
CrossChainTradeData crossChainTradeData) throws DataException {
|
||||
|
||||
// get the trade bot data for the AT address
|
||||
Optional<TradeBotData> tradeBotDataOptional
|
||||
= repository.getCrossChainRepository()
|
||||
.getAllTradeBotData().stream()
|
||||
.filter(data -> data.getAtAddress().equals(atAddress))
|
||||
.findFirst();
|
||||
|
||||
if( tradeBotDataOptional.isEmpty() )
|
||||
return Optional.empty();
|
||||
|
||||
TradeBotData tradeBotData = tradeBotDataOptional.get();
|
||||
|
||||
// return the p2sh address from the trade bot
|
||||
return getP2ShFromTradeBot(bitcoiny, crossChainTradeData, tradeBotData);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get Foreign Trade Summaries
|
||||
*
|
||||
* @param foreignBlockchain the blockchain traded on
|
||||
* @param repository the repository
|
||||
* @param bitcoiny data for the blockchain trade on
|
||||
* @return
|
||||
* @throws DataException
|
||||
* @throws ForeignBlockchainException
|
||||
*/
|
||||
public static List<TransactionSummary> getForeignTradeSummaries(
|
||||
SupportedBlockchain foreignBlockchain,
|
||||
Repository repository,
|
||||
Bitcoiny bitcoiny) throws DataException, ForeignBlockchainException {
|
||||
|
||||
// get all the AT address for the given blockchain
|
||||
List<String> atAddresses
|
||||
= repository.getCrossChainRepository().getAllTradeBotData().stream()
|
||||
.filter(data -> foreignBlockchain.name().toLowerCase().equals(data.getForeignBlockchain().toLowerCase()))
|
||||
//.filter( data -> data.getForeignKey().equals( xpriv )) // TODO
|
||||
.map(data -> data.getAtAddress())
|
||||
.collect(Collectors.toList());
|
||||
|
||||
List<TransactionSummary> summaries = new ArrayList<>( atAddresses.size() * 2 );
|
||||
|
||||
// for each AT address, gather the data and get foreign trade summary
|
||||
for( String atAddress: atAddresses) {
|
||||
|
||||
ATData atData = repository.getATRepository().fromATAddress(atAddress);
|
||||
|
||||
CrossChainTradeData crossChainTradeData = foreignBlockchain.getLatestAcct().populateTradeData(repository, atData);
|
||||
|
||||
Optional<String> address = getP2ShAddressForAT(atAddress,repository, bitcoiny, crossChainTradeData);
|
||||
|
||||
if( address.isPresent()){
|
||||
summaries.add( getForeignTradeSummary( bitcoiny, address.get(), atAddress ) );
|
||||
}
|
||||
}
|
||||
|
||||
return summaries;
|
||||
}
|
||||
|
||||
/**
|
||||
* Add Server
|
||||
*
|
||||
* Add foreign blockchain server to list of candidates.
|
||||
*
|
||||
* @param bitcoiny the foreign blockchain
|
||||
* @param server the server
|
||||
*
|
||||
* @return true if the add was successful, otherwise false
|
||||
*/
|
||||
public static boolean addServer(Bitcoiny bitcoiny, ChainableServer server) {
|
||||
|
||||
return bitcoiny.getBlockchainProvider().addServer(server);
|
||||
}
|
||||
|
||||
/**
|
||||
* Remove Server
|
||||
*
|
||||
* Remove foreign blockchain server from list of candidates.
|
||||
*
|
||||
* @param bitcoiny the foreign blockchain
|
||||
* @param server the server
|
||||
*
|
||||
* @return true if the removal was successful, otherwise false
|
||||
*/
|
||||
public static boolean removeServer(Bitcoiny bitcoiny, ChainableServer server){
|
||||
|
||||
return bitcoiny.getBlockchainProvider().removeServer(server);
|
||||
}
|
||||
|
||||
/**
|
||||
* Set Current Server
|
||||
*
|
||||
* Set the server to use the intended foreign blockchain.
|
||||
*
|
||||
* @param bitcoiny the foreign blockchain
|
||||
* @param serverInfo the server configuration information
|
||||
*
|
||||
* @return the server connection information
|
||||
*/
|
||||
public static ServerConnectionInfo setCurrentServer(Bitcoiny bitcoiny, ServerInfo serverInfo) throws ForeignBlockchainException {
|
||||
|
||||
final BitcoinyBlockchainProvider blockchainProvider = bitcoiny.getBlockchainProvider();
|
||||
|
||||
ChainableServer server = blockchainProvider.getServer(
|
||||
serverInfo.getHostName(),
|
||||
ChainableServer.ConnectionType.valueOf(serverInfo.getConnectionType()),
|
||||
serverInfo.getPort()
|
||||
);
|
||||
|
||||
ChainableServerConnection connection = blockchainProvider.setCurrentServer(server, CORE_API_CALL).get();
|
||||
|
||||
return new ServerConnectionInfo(
|
||||
new ServerInfo(
|
||||
0,
|
||||
serverInfo.getHostName(),
|
||||
serverInfo.getPort(),
|
||||
serverInfo.getConnectionType(),
|
||||
connection.isSuccess()
|
||||
),
|
||||
CORE_API_CALL,
|
||||
true,
|
||||
connection.isSuccess() ,
|
||||
System.currentTimeMillis(),
|
||||
connection.getNotes()
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get P2Sh From Trade Bot
|
||||
*
|
||||
* Get P2Sh address from the trade bot
|
||||
*
|
||||
* @param bitcoiny the blockchain for the trade
|
||||
* @param crossChainTradeData the cross cahin data for the trade
|
||||
* @param tradeBotData the data from the trade bot
|
||||
*
|
||||
* @return the address, original format
|
||||
*/
|
||||
private static Optional<String> getP2ShFromTradeBot(
|
||||
Bitcoiny bitcoiny,
|
||||
CrossChainTradeData crossChainTradeData,
|
||||
TradeBotData tradeBotData) {
|
||||
|
||||
// Pirate Chain does not support this
|
||||
if( SupportedBlockchain.PIRATECHAIN.name().equals(tradeBotData.getForeignBlockchain())) return Optional.empty();
|
||||
|
||||
// need to get the trade PKH from the trade bot
|
||||
if( tradeBotData.getTradeForeignPublicKeyHash() == null ) return Optional.empty();
|
||||
|
||||
// need to get the lock time from the trade bot
|
||||
if( tradeBotData.getLockTimeA() == null ) return Optional.empty();
|
||||
|
||||
// need to get the creator PKH from the trade bot
|
||||
if( crossChainTradeData.creatorForeignPKH == null ) return Optional.empty();
|
||||
|
||||
// need to get the secret from the trade bot
|
||||
if( tradeBotData.getHashOfSecret() == null ) return Optional.empty();
|
||||
|
||||
// if we have the necessary data from the trade bot,
|
||||
// then build the redeem script necessary to facilitate the trade
|
||||
byte[] redeemScriptBytes
|
||||
= BitcoinyHTLC.buildScript(
|
||||
tradeBotData.getTradeForeignPublicKeyHash(),
|
||||
tradeBotData.getLockTimeA(),
|
||||
crossChainTradeData.creatorForeignPKH,
|
||||
tradeBotData.getHashOfSecret()
|
||||
);
|
||||
|
||||
|
||||
String p2shAddress = bitcoiny.deriveP2shAddress(redeemScriptBytes);
|
||||
|
||||
return Optional.of(p2shAddress);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get Foreign Trade Summary
|
||||
*
|
||||
* @param bitcoiny the blockchain the trade occurred on
|
||||
* @param p2shAddress the p2sh address
|
||||
* @param atAddress the AT address the p2sh address is derived from
|
||||
*
|
||||
* @return the summary
|
||||
*
|
||||
* @throws ForeignBlockchainException
|
||||
*/
|
||||
public static TransactionSummary getForeignTradeSummary(Bitcoiny bitcoiny, String p2shAddress, String atAddress)
|
||||
throws ForeignBlockchainException {
|
||||
Script outputScript = ScriptBuilder.createOutputScript(
|
||||
Address.fromString(bitcoiny.getNetworkParameters(), p2shAddress));
|
||||
|
||||
List<TransactionHash> hashes
|
||||
= bitcoiny.getAddressTransactions( outputScript.getProgram(), true);
|
||||
|
||||
TransactionSummary summary;
|
||||
|
||||
if(hashes.isEmpty()){
|
||||
summary
|
||||
= new TransactionSummary(
|
||||
atAddress,
|
||||
p2shAddress,
|
||||
"N/A",
|
||||
"N/A",
|
||||
0,
|
||||
0,
|
||||
0,
|
||||
0,
|
||||
"N/A",
|
||||
0,
|
||||
0,
|
||||
0,
|
||||
0);
|
||||
}
|
||||
else if( hashes.size() == 1) {
|
||||
AtomicTransactionData data = buildTransactionData(bitcoiny, hashes.get(0));
|
||||
summary = new TransactionSummary(
|
||||
atAddress,
|
||||
p2shAddress,
|
||||
"N/A",
|
||||
data.hash.txHash,
|
||||
data.timestamp,
|
||||
data.totalAmount,
|
||||
getTotalInput(bitcoiny, data.inputs) - data.totalAmount,
|
||||
data.size,
|
||||
"N/A",
|
||||
0,
|
||||
0,
|
||||
0,
|
||||
0);
|
||||
}
|
||||
// otherwise assuming there is 2 and only 2 hashes
|
||||
else {
|
||||
List<AtomicTransactionData> atomicTransactionDataList = new ArrayList<>(2);
|
||||
|
||||
// hashes -> data
|
||||
for( TransactionHash hash : hashes){
|
||||
atomicTransactionDataList.add(buildTransactionData(bitcoiny,hash));
|
||||
}
|
||||
|
||||
// sort the transaction data by time
|
||||
List<AtomicTransactionData> sorted
|
||||
= atomicTransactionDataList.stream()
|
||||
.sorted((data1, data2) -> data1.timestamp.compareTo(data2.timestamp))
|
||||
.collect(Collectors.toList());
|
||||
|
||||
// build the summary using the first 2 transactions
|
||||
summary = buildForeignTradeSummary(atAddress, p2shAddress, sorted.get(0), sorted.get(1), bitcoiny);
|
||||
}
|
||||
return summary;
|
||||
}
|
||||
|
||||
/**
|
||||
* Build Foreign Trade Summary
|
||||
*
|
||||
* @param p2shValue the p2sh address, original format
|
||||
* @param lockingTransaction the transaction lock the foreighn coin
|
||||
* @param unlockingTransaction the transaction to unlock the foreign coin
|
||||
* @param bitcoiny the blockchain the trade occurred on
|
||||
*
|
||||
* @return
|
||||
*
|
||||
* @throws ForeignBlockchainException
|
||||
*/
|
||||
private static TransactionSummary buildForeignTradeSummary(
|
||||
String atAddress,
|
||||
String p2shValue,
|
||||
AtomicTransactionData lockingTransaction,
|
||||
AtomicTransactionData unlockingTransaction,
|
||||
Bitcoiny bitcoiny) throws ForeignBlockchainException {
|
||||
|
||||
// get sum of the relevant inputs for each transaction
|
||||
long lockingTotalInput = getTotalInput(bitcoiny, lockingTransaction.inputs);
|
||||
long unlockingTotalInput = getTotalInput(bitcoiny, unlockingTransaction.inputs);
|
||||
|
||||
// find the address that has output that matches the total input
|
||||
Optional<Map.Entry<List<String>, Long>> addressValue
|
||||
= lockingTransaction.valueByAddress.entrySet().stream()
|
||||
.filter(entry -> entry.getValue() == unlockingTotalInput).findFirst();
|
||||
|
||||
// set that matching address, if found
|
||||
String p2shAddress;
|
||||
if( addressValue.isPresent() && addressValue.get().getKey().size() == 1 ){
|
||||
p2shAddress = addressValue.get().getKey().get(0);
|
||||
}
|
||||
else {
|
||||
p2shAddress = "N/A";
|
||||
}
|
||||
|
||||
// build summaries with prepared values
|
||||
// the fees are the total amount subtracted by the total transaction input
|
||||
return new TransactionSummary(
|
||||
atAddress,
|
||||
p2shValue,
|
||||
p2shAddress,
|
||||
lockingTransaction.hash.txHash,
|
||||
lockingTransaction.timestamp,
|
||||
lockingTransaction.totalAmount,
|
||||
lockingTotalInput - lockingTransaction.totalAmount,
|
||||
lockingTransaction.size,
|
||||
unlockingTransaction.hash.txHash,
|
||||
unlockingTransaction.timestamp,
|
||||
unlockingTransaction.totalAmount,
|
||||
unlockingTotalInput - unlockingTransaction.totalAmount,
|
||||
unlockingTransaction.size
|
||||
);
|
||||
|
||||
}
|
||||
|
||||
/**
|
||||
* Build Transaction Data
|
||||
*
|
||||
* @param bitcoiny the coin for the transaction
|
||||
* @param hash the hash for the transaction
|
||||
*
|
||||
* @return the data for the transaction
|
||||
*
|
||||
* @throws ForeignBlockchainException
|
||||
*/
|
||||
private static AtomicTransactionData buildTransactionData( Bitcoiny bitcoiny, TransactionHash hash)
|
||||
throws ForeignBlockchainException {
|
||||
|
||||
BitcoinyTransaction transaction = bitcoiny.getTransaction(hash.txHash);
|
||||
|
||||
// destination address list -> value
|
||||
Map<List<String>, Long> valueByAddress = new HashMap<>();
|
||||
|
||||
// for each output in the transaction, index by address list
|
||||
for( BitcoinyTransaction.Output output : transaction.outputs) {
|
||||
valueByAddress.put(output.addresses, output.value);
|
||||
}
|
||||
|
||||
return new AtomicTransactionData(
|
||||
hash,
|
||||
transaction.timestamp,
|
||||
transaction.inputs,
|
||||
valueByAddress,
|
||||
transaction.totalAmount,
|
||||
transaction.size);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get Total Input
|
||||
*
|
||||
* Get the sum of all the inputs used in a list of inputs.
|
||||
*
|
||||
* @param bitcoiny the coin the inputs belong to
|
||||
* @param inputs the inputs
|
||||
*
|
||||
* @return the sum
|
||||
*
|
||||
* @throws ForeignBlockchainException
|
||||
*/
|
||||
private static long getTotalInput(Bitcoiny bitcoiny, List<BitcoinyTransaction.Input> inputs)
|
||||
throws ForeignBlockchainException {
|
||||
|
||||
long totalInputOut = 0;
|
||||
|
||||
// for each input, add to total input,
|
||||
// get the indexed transaction output value and add to total value
|
||||
for( BitcoinyTransaction.Input input : inputs){
|
||||
|
||||
BitcoinyTransaction inputOut = bitcoiny.getTransaction(input.outputTxHash);
|
||||
BitcoinyTransaction.Output output = inputOut.outputs.get(input.outputVout);
|
||||
totalInputOut += output.value;
|
||||
}
|
||||
return totalInputOut;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get Notes
|
||||
*
|
||||
* Build notes from an exception thrown.
|
||||
*
|
||||
* @param e the exception
|
||||
*
|
||||
* @return the exception message or the exception class name
|
||||
*/
|
||||
public static String getNotes(Exception e) {
|
||||
return e.getMessage() + " (" + e.getClass().getSimpleName() + ")";
|
||||
}
|
||||
|
||||
/**
|
||||
* Build Server Connection History
|
||||
*
|
||||
* @param bitcoiny the foreign blockchain
|
||||
*
|
||||
* @return the history of connections from latest to first
|
||||
*/
|
||||
public static List<ServerConnectionInfo> buildServerConnectionHistory(Bitcoiny bitcoiny) {
|
||||
|
||||
return bitcoiny.getBlockchainProvider().getServerConnections().stream()
|
||||
.sorted(Comparator.comparing(ChainableServerConnection::getCurrentTimeMillis).reversed())
|
||||
.map(
|
||||
connection -> new ServerConnectionInfo(
|
||||
serverToServerInfo( connection.getServer()),
|
||||
connection.getRequestedBy(),
|
||||
connection.isOpen(),
|
||||
connection.isSuccess(),
|
||||
connection.getCurrentTimeMillis(),
|
||||
connection.getNotes()
|
||||
)
|
||||
)
|
||||
.collect(Collectors.toList());
|
||||
}
|
||||
|
||||
/**
|
||||
* Server To Server Info
|
||||
*
|
||||
* Make a server info object from a server object.
|
||||
*
|
||||
* @param server the server
|
||||
*
|
||||
* @return the server info
|
||||
*/
|
||||
private static ServerInfo serverToServerInfo(ChainableServer server) {
|
||||
|
||||
return new ServerInfo(
|
||||
0,
|
||||
server.getHostName(),
|
||||
server.getPort(),
|
||||
server.getConnectionType().toString(),
|
||||
false);
|
||||
}
|
||||
}
|
@@ -142,7 +142,7 @@ public class PollsResource {
|
||||
for (VoteOnPollData vote : votes) {
|
||||
String voter = Crypto.toAddress(vote.getVoterPublicKey());
|
||||
AccountData voterData = repository.getAccountRepository().getAccount(voter);
|
||||
int voteWeight = voterData.getBlocksMinted() - voterData.getBlocksMintedPenalty();
|
||||
int voteWeight = voterData.getBlocksMinted() + voterData.getBlocksMintedPenalty();
|
||||
if (voteWeight < 0) voteWeight = 0;
|
||||
totalWeight += voteWeight;
|
||||
|
||||
|
@@ -330,8 +330,8 @@ public class TransactionsResource {
|
||||
public enum ConfirmationStatus {
|
||||
CONFIRMED,
|
||||
UNCONFIRMED,
|
||||
BOTH;
|
||||
}
|
||||
BOTH
|
||||
}
|
||||
|
||||
@GET
|
||||
@Path("/search")
|
||||
|
@@ -31,7 +31,7 @@ public class AdminStatusWebSocket extends ApiWebSocket implements Listener {
|
||||
return;
|
||||
}
|
||||
|
||||
EventBus.INSTANCE.addListener(this::listen);
|
||||
EventBus.INSTANCE.addListener(this);
|
||||
}
|
||||
|
||||
@Override
|
||||
|
@@ -28,7 +28,7 @@ public class BlocksWebSocket extends ApiWebSocket implements Listener {
|
||||
public void configure(WebSocketServletFactory factory) {
|
||||
factory.register(BlocksWebSocket.class);
|
||||
|
||||
EventBus.INSTANCE.addListener(this::listen);
|
||||
EventBus.INSTANCE.addListener(this);
|
||||
}
|
||||
|
||||
@Override
|
||||
|
@@ -86,7 +86,7 @@ public class PresenceWebSocket extends ApiWebSocket implements Listener {
|
||||
return;
|
||||
}
|
||||
|
||||
EventBus.INSTANCE.addListener(this::listen);
|
||||
EventBus.INSTANCE.addListener(this);
|
||||
}
|
||||
|
||||
@Override
|
||||
|
@@ -43,7 +43,7 @@ public class TradeBotWebSocket extends ApiWebSocket implements Listener {
|
||||
// No output this time
|
||||
}
|
||||
|
||||
EventBus.INSTANCE.addListener(this::listen);
|
||||
EventBus.INSTANCE.addListener(this);
|
||||
}
|
||||
|
||||
@Override
|
||||
|
@@ -67,7 +67,7 @@ public class TradeOffersWebSocket extends ApiWebSocket implements Listener {
|
||||
return;
|
||||
}
|
||||
|
||||
EventBus.INSTANCE.addListener(this::listen);
|
||||
EventBus.INSTANCE.addListener(this);
|
||||
}
|
||||
|
||||
@Override
|
||||
|
@@ -29,7 +29,7 @@ public class TradePresenceWebSocket extends ApiWebSocket implements Listener {
|
||||
|
||||
populateCurrentInfo();
|
||||
|
||||
EventBus.INSTANCE.addListener(this::listen);
|
||||
EventBus.INSTANCE.addListener(this);
|
||||
}
|
||||
|
||||
@Override
|
||||
|
@@ -104,7 +104,7 @@ public class ArbitraryDataBuilder {
|
||||
if (latestPut.getMethod() != Method.PUT) {
|
||||
throw new DataException("Expected PUT but received PATCH");
|
||||
}
|
||||
if (transactionDataList.size() == 0) {
|
||||
if (transactionDataList.isEmpty()) {
|
||||
throw new DataException(String.format("No transactions found for name %s, service %s, " +
|
||||
"identifier: %s, since %d", name, service, this.identifierString(), latestPut.getTimestamp()));
|
||||
}
|
||||
@@ -176,7 +176,7 @@ public class ArbitraryDataBuilder {
|
||||
}
|
||||
|
||||
private void findLatestSignature() throws DataException {
|
||||
if (this.transactions.size() == 0) {
|
||||
if (this.transactions.isEmpty()) {
|
||||
throw new DataException("Unable to find latest signature from empty transaction list");
|
||||
}
|
||||
|
||||
|
@@ -58,6 +58,9 @@ public class ArbitraryDataFile {
|
||||
public static int SHORT_DIGEST_LENGTH = 8;
|
||||
|
||||
protected Path filePath;
|
||||
protected byte[] fileContent;
|
||||
private boolean useTemporaryFile;
|
||||
|
||||
protected String hash58;
|
||||
protected byte[] signature;
|
||||
private ArrayList<ArbitraryDataFileChunk> chunks;
|
||||
@@ -90,8 +93,14 @@ public class ArbitraryDataFile {
|
||||
this.signature = signature;
|
||||
LOGGER.trace(String.format("File digest: %s, size: %d bytes", this.hash58, fileContent.length));
|
||||
|
||||
this.fileContent = fileContent;
|
||||
this.useTemporaryFile = useTemporaryFile;
|
||||
}
|
||||
|
||||
public void save() throws DataException {
|
||||
|
||||
Path outputFilePath;
|
||||
if (useTemporaryFile) {
|
||||
if (this.useTemporaryFile) {
|
||||
try {
|
||||
outputFilePath = Files.createTempFile("qortalRawData", null);
|
||||
outputFilePath.toFile().deleteOnExit();
|
||||
@@ -149,6 +158,7 @@ public class ArbitraryDataFile {
|
||||
|
||||
case RAW_DATA:
|
||||
arbitraryDataFile = ArbitraryDataFile.fromRawData(data, signature);
|
||||
arbitraryDataFile.save();
|
||||
break;
|
||||
}
|
||||
|
||||
@@ -324,6 +334,7 @@ public class ArbitraryDataFile {
|
||||
out.flush();
|
||||
|
||||
ArbitraryDataFileChunk chunk = new ArbitraryDataFileChunk(out.toByteArray(), this.signature);
|
||||
chunk.save();
|
||||
ValidationResult validationResult = chunk.isValid();
|
||||
if (validationResult == ValidationResult.OK) {
|
||||
this.chunks.add(chunk);
|
||||
@@ -343,7 +354,7 @@ public class ArbitraryDataFile {
|
||||
|
||||
public boolean join() {
|
||||
// Ensure we have chunks
|
||||
if (this.chunks != null && this.chunks.size() > 0) {
|
||||
if (this.chunks != null && !this.chunks.isEmpty()) {
|
||||
|
||||
// Create temporary path for joined file
|
||||
// Use the user-specified temp dir, as it is deterministic, and is more likely to be located on reusable storage hardware
|
||||
@@ -406,6 +417,10 @@ public class ArbitraryDataFile {
|
||||
}
|
||||
|
||||
public boolean delete(int attempts) {
|
||||
if (this.filePath == null) {
|
||||
return false;
|
||||
}
|
||||
|
||||
// Keep trying to delete the data until it is deleted, or we reach 10 attempts
|
||||
for (int i=0; i<attempts; i++) {
|
||||
if (this.delete()) {
|
||||
@@ -424,7 +439,7 @@ public class ArbitraryDataFile {
|
||||
boolean success = false;
|
||||
|
||||
// Delete the individual chunks
|
||||
if (this.chunks != null && this.chunks.size() > 0) {
|
||||
if (this.chunks != null && !this.chunks.isEmpty()) {
|
||||
Iterator iterator = this.chunks.iterator();
|
||||
while (iterator.hasNext()) {
|
||||
ArbitraryDataFileChunk chunk = (ArbitraryDataFileChunk) iterator.next();
|
||||
@@ -467,6 +482,10 @@ public class ArbitraryDataFile {
|
||||
}
|
||||
|
||||
public byte[] getBytes() {
|
||||
if (this.fileContent != null) {
|
||||
return this.fileContent;
|
||||
}
|
||||
|
||||
try {
|
||||
return Files.readAllBytes(this.filePath);
|
||||
} catch (IOException e) {
|
||||
@@ -690,7 +709,7 @@ public class ArbitraryDataFile {
|
||||
}
|
||||
|
||||
public byte[] chunkHashes() throws DataException {
|
||||
if (this.chunks != null && this.chunks.size() > 0) {
|
||||
if (this.chunks != null && !this.chunks.isEmpty()) {
|
||||
// Return null if we only have one chunk, with the same hash as the parent
|
||||
if (Arrays.equals(this.digest(), this.chunks.get(0).digest())) {
|
||||
return null;
|
||||
@@ -717,7 +736,7 @@ public class ArbitraryDataFile {
|
||||
public List<byte[]> chunkHashList() {
|
||||
List<byte[]> chunks = new ArrayList<>();
|
||||
|
||||
if (this.chunks != null && this.chunks.size() > 0) {
|
||||
if (this.chunks != null && !this.chunks.isEmpty()) {
|
||||
// Return null if we only have one chunk, with the same hash as the parent
|
||||
if (Arrays.equals(this.digest(), this.chunks.get(0).digest())) {
|
||||
return null;
|
||||
@@ -801,7 +820,7 @@ public class ArbitraryDataFile {
|
||||
String outputString = "";
|
||||
if (this.chunkCount() > 0) {
|
||||
for (ArbitraryDataFileChunk chunk : this.chunks) {
|
||||
if (outputString.length() > 0) {
|
||||
if (!outputString.isEmpty()) {
|
||||
outputString = outputString.concat(",");
|
||||
}
|
||||
outputString = outputString.concat(chunk.digest58());
|
||||
|
@@ -73,7 +73,7 @@ public class ArbitraryDataReader {
|
||||
}
|
||||
|
||||
// If identifier is a blank string, or reserved keyword "default", treat it as null
|
||||
if (identifier == null || identifier.equals("") || identifier.equals("default")) {
|
||||
if (identifier == null || identifier.isEmpty() || identifier.equals("default")) {
|
||||
identifier = null;
|
||||
}
|
||||
|
||||
|
@@ -199,7 +199,7 @@ public class ArbitraryDataRenderer {
|
||||
}
|
||||
|
||||
private String getFilename(String directory, String userPath) {
|
||||
if (userPath == null || userPath.endsWith("/") || userPath.equals("")) {
|
||||
if (userPath == null || userPath.endsWith("/") || userPath.isEmpty()) {
|
||||
// Locate index file
|
||||
List<String> indexFiles = ArbitraryDataRenderer.indexFiles();
|
||||
for (String indexFile : indexFiles) {
|
||||
|
@@ -52,7 +52,7 @@ public class ArbitraryDataResource {
|
||||
this.service = service;
|
||||
|
||||
// If identifier is a blank string, or reserved keyword "default", treat it as null
|
||||
if (identifier == null || identifier.equals("") || identifier.equals("default")) {
|
||||
if (identifier == null || identifier.isEmpty() || identifier.equals("default")) {
|
||||
identifier = null;
|
||||
}
|
||||
this.identifier = identifier;
|
||||
|
@@ -81,7 +81,7 @@ public class ArbitraryDataTransactionBuilder {
|
||||
this.service = service;
|
||||
|
||||
// If identifier is a blank string, or reserved keyword "default", treat it as null
|
||||
if (identifier == null || identifier.equals("") || identifier.equals("default")) {
|
||||
if (identifier == null || identifier.isEmpty() || identifier.equals("default")) {
|
||||
identifier = null;
|
||||
}
|
||||
this.identifier = identifier;
|
||||
|
@@ -78,7 +78,7 @@ public class ArbitraryDataWriter {
|
||||
this.compression = compression;
|
||||
|
||||
// If identifier is a blank string, or reserved keyword "default", treat it as null
|
||||
if (identifier == null || identifier.equals("") || identifier.equals("default")) {
|
||||
if (identifier == null || identifier.isEmpty() || identifier.equals("default")) {
|
||||
identifier = null;
|
||||
}
|
||||
this.identifier = identifier;
|
||||
|
@@ -132,7 +132,7 @@ public class AT {
|
||||
// Nothing happened?
|
||||
if (state.getSteps() == 0 && Arrays.equals(stateHash, latestAtStateData.getStateHash()))
|
||||
// We currently want to execute frozen ATs, to maintain backwards support.
|
||||
if (state.isFrozen() == false)
|
||||
if (!state.isFrozen())
|
||||
// this.atStateData will be null
|
||||
return Collections.emptyList();
|
||||
|
||||
|
@@ -1061,8 +1061,10 @@ public class Block {
|
||||
return ValidationResult.MINTER_NOT_ACCEPTED;
|
||||
|
||||
long expectedTimestamp = calcTimestamp(parentBlockData, this.blockData.getMinterPublicKey(), minterLevel);
|
||||
if (this.blockData.getTimestamp() != expectedTimestamp)
|
||||
if (this.blockData.getTimestamp() != expectedTimestamp) {
|
||||
LOGGER.debug(String.format("timestamp mismatch! block had %s but we expected %s", this.blockData.getTimestamp(), expectedTimestamp));
|
||||
return ValidationResult.TIMESTAMP_INCORRECT;
|
||||
}
|
||||
|
||||
return ValidationResult.OK;
|
||||
}
|
||||
@@ -1084,7 +1086,7 @@ public class Block {
|
||||
|
||||
// Online accounts should only be included in designated blocks; all others must be empty
|
||||
if (!this.isOnlineAccountsBlock()) {
|
||||
if (this.blockData.getOnlineAccountsCount() != 0 || accountIndexes.size() != 0) {
|
||||
if (this.blockData.getOnlineAccountsCount() != 0 || !accountIndexes.isEmpty()) {
|
||||
return ValidationResult.ONLINE_ACCOUNTS_INVALID;
|
||||
}
|
||||
// Not a designated online accounts block and account count is 0. Everything is correct so no need to validate further.
|
||||
@@ -1557,9 +1559,12 @@ public class Block {
|
||||
SelfSponsorshipAlgoV1Block.processAccountPenalties(this);
|
||||
}
|
||||
|
||||
if (this.blockData.getHeight() == BlockChain.getInstance().getPenaltyFixHeight()) {
|
||||
// Apply fix for penalties
|
||||
PenaltyFix.processPenaltiesFix(this);
|
||||
if (this.blockData.getHeight() == BlockChain.getInstance().getSelfSponsorshipAlgoV2Height()) {
|
||||
SelfSponsorshipAlgoV2Block.processAccountPenalties(this);
|
||||
}
|
||||
|
||||
if (this.blockData.getHeight() == BlockChain.getInstance().getSelfSponsorshipAlgoV3Height()) {
|
||||
SelfSponsorshipAlgoV3Block.processAccountPenalties(this);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1854,11 +1859,14 @@ public class Block {
|
||||
SelfSponsorshipAlgoV1Block.orphanAccountPenalties(this);
|
||||
}
|
||||
|
||||
if (this.blockData.getHeight() == BlockChain.getInstance().getPenaltyFixHeight()) {
|
||||
// Revert fix for penalties
|
||||
PenaltyFix.orphanPenaltiesFix(this);
|
||||
if (this.blockData.getHeight() == BlockChain.getInstance().getSelfSponsorshipAlgoV2Height()) {
|
||||
SelfSponsorshipAlgoV2Block.orphanAccountPenalties(this);
|
||||
}
|
||||
|
||||
if (this.blockData.getHeight() == BlockChain.getInstance().getSelfSponsorshipAlgoV3Height()) {
|
||||
SelfSponsorshipAlgoV3Block.orphanAccountPenalties(this);
|
||||
}
|
||||
|
||||
// Account levels and block rewards are only processed/orphaned on block reward distribution blocks
|
||||
if (this.isRewardDistributionBlock()) {
|
||||
// Block rewards, including transaction fees, removed after transactions undone
|
||||
@@ -2556,4 +2564,4 @@ public class Block {
|
||||
LOGGER.info(() -> String.format("Unable to log block debugging info: %s", e.getMessage()));
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@@ -74,13 +74,13 @@ public class BlockChain {
|
||||
onlineAccountMinterLevelValidationHeight,
|
||||
selfSponsorshipAlgoV1Height,
|
||||
selfSponsorshipAlgoV2Height,
|
||||
selfSponsorshipAlgoV3Height,
|
||||
feeValidationFixTimestamp,
|
||||
chatReferenceTimestamp,
|
||||
arbitraryOptionalFeeTimestamp,
|
||||
unconfirmableRewardSharesHeight,
|
||||
disableTransferPrivsTimestamp,
|
||||
enableTransferPrivsTimestamp,
|
||||
penaltyFixHeight
|
||||
enableTransferPrivsTimestamp
|
||||
}
|
||||
|
||||
// Custom transaction fees
|
||||
@@ -217,6 +217,12 @@ public class BlockChain {
|
||||
/** Snapshot timestamp for self sponsorship algo V2 */
|
||||
private long selfSponsorshipAlgoV2SnapshotTimestamp;
|
||||
|
||||
/** Snapshot timestamp for self sponsorship algo V3 */
|
||||
private long selfSponsorshipAlgoV3SnapshotTimestamp;
|
||||
|
||||
/** Reference timestamp for self sponsorship algo V1 block height */
|
||||
private long referenceTimestampBlock;
|
||||
|
||||
/** Feature-trigger timestamp to modify behaviour of various transactions that support mempow */
|
||||
private long mempowTransactionUpdatesTimestamp;
|
||||
|
||||
@@ -419,6 +425,15 @@ public class BlockChain {
|
||||
return this.selfSponsorshipAlgoV2SnapshotTimestamp;
|
||||
}
|
||||
|
||||
// Self sponsorship algo V3
|
||||
public long getSelfSponsorshipAlgoV3SnapshotTimestamp() {
|
||||
return this.selfSponsorshipAlgoV3SnapshotTimestamp;
|
||||
}
|
||||
|
||||
// Self sponsorship algo V3
|
||||
public long getReferenceTimestampBlock() {
|
||||
return this.referenceTimestampBlock;
|
||||
}
|
||||
// Feature-trigger timestamp to modify behaviour of various transactions that support mempow
|
||||
public long getMemPoWTransactionUpdatesTimestamp() {
|
||||
return this.mempowTransactionUpdatesTimestamp;
|
||||
@@ -563,6 +578,10 @@ public class BlockChain {
|
||||
return this.featureTriggers.get(FeatureTrigger.selfSponsorshipAlgoV2Height.name()).intValue();
|
||||
}
|
||||
|
||||
public int getSelfSponsorshipAlgoV3Height() {
|
||||
return this.featureTriggers.get(FeatureTrigger.selfSponsorshipAlgoV3Height.name()).intValue();
|
||||
}
|
||||
|
||||
public long getOnlineAccountMinterLevelValidationHeight() {
|
||||
return this.featureTriggers.get(FeatureTrigger.onlineAccountMinterLevelValidationHeight.name()).intValue();
|
||||
}
|
||||
@@ -591,10 +610,6 @@ public class BlockChain {
|
||||
return this.featureTriggers.get(FeatureTrigger.enableTransferPrivsTimestamp.name()).longValue();
|
||||
}
|
||||
|
||||
public int getPenaltyFixHeight() {
|
||||
return this.featureTriggers.get(FeatureTrigger.penaltyFixHeight.name()).intValue();
|
||||
}
|
||||
|
||||
// More complex getters for aspects that change by height or timestamp
|
||||
|
||||
public long getRewardAtHeight(int ourHeight) {
|
||||
|
@@ -1,187 +0,0 @@
|
||||
package org.qortal.block;
|
||||
|
||||
import org.apache.commons.lang3.StringUtils;
|
||||
import org.apache.logging.log4j.LogManager;
|
||||
import org.apache.logging.log4j.Logger;
|
||||
import org.eclipse.persistence.jaxb.JAXBContextFactory;
|
||||
import org.eclipse.persistence.jaxb.UnmarshallerProperties;
|
||||
import org.qortal.api.model.AccountPenaltyStats;
|
||||
import org.qortal.crypto.Crypto;
|
||||
import org.qortal.data.account.AccountData;
|
||||
import org.qortal.data.account.AccountPenaltyData;
|
||||
import org.qortal.repository.DataException;
|
||||
import org.qortal.repository.Repository;
|
||||
import org.qortal.utils.Base58;
|
||||
|
||||
import javax.xml.bind.JAXBContext;
|
||||
import javax.xml.bind.JAXBException;
|
||||
import javax.xml.bind.UnmarshalException;
|
||||
import javax.xml.bind.Unmarshaller;
|
||||
import javax.xml.transform.stream.StreamSource;
|
||||
import java.io.InputStream;
|
||||
import java.nio.charset.StandardCharsets;
|
||||
import java.util.*;
|
||||
import java.util.stream.Collectors;
|
||||
|
||||
public final class PenaltyFix {
|
||||
|
||||
private static final Logger LOGGER = LogManager.getLogger(PenaltyFix.class);
|
||||
|
||||
private static final String PENALTY_FIX_SOURCE = "penalty-fix.json";
|
||||
private static final String PENALTY_FIX_HASH = BlockChain.getInstance().getPenaltyFixHash();
|
||||
private static final List<AccountPenaltyData> penalties = accountsPenaltyFix();
|
||||
|
||||
private PenaltyFix() {
|
||||
/* Do not instantiate */
|
||||
}
|
||||
|
||||
@SuppressWarnings("unchecked")
|
||||
private static List<AccountPenaltyData> accountsPenaltyFix() {
|
||||
Unmarshaller unmarshaller;
|
||||
|
||||
try {
|
||||
// Create JAXB context aware of classes we need to unmarshal
|
||||
JAXBContext pf = JAXBContextFactory.createContext(new Class[] {
|
||||
AccountPenaltyData.class
|
||||
}, null);
|
||||
|
||||
// Create unmarshaller
|
||||
unmarshaller = pf.createUnmarshaller();
|
||||
|
||||
// Set the unmarshaller media type to JSON
|
||||
unmarshaller.setProperty(UnmarshallerProperties.MEDIA_TYPE, "application/json");
|
||||
|
||||
// Tell unmarshaller that there's no JSON root element in the JSON input
|
||||
unmarshaller.setProperty(UnmarshallerProperties.JSON_INCLUDE_ROOT, false);
|
||||
} catch (JAXBException e) {
|
||||
String message = "Failed to setup unmarshaller for penalty fix";
|
||||
LOGGER.error(message, e);
|
||||
throw new RuntimeException(message, e);
|
||||
}
|
||||
|
||||
ClassLoader classLoader = BlockChain.class.getClassLoader();
|
||||
InputStream pfIn = classLoader.getResourceAsStream(PENALTY_FIX_SOURCE);
|
||||
StreamSource pfSource = new StreamSource(pfIn);
|
||||
|
||||
try {
|
||||
// Attempt to unmarshal JSON stream to BlockChain config
|
||||
return (List<AccountPenaltyData>) unmarshaller.unmarshal(pfSource, AccountPenaltyData.class).getValue();
|
||||
} catch (UnmarshalException e) {
|
||||
String message = "Failed to parse penalty fix";
|
||||
LOGGER.error(message, e);
|
||||
throw new RuntimeException(message, e);
|
||||
} catch (JAXBException e) {
|
||||
String message = "Unexpected JAXB issue while processing penalty fix";
|
||||
LOGGER.error(message, e);
|
||||
throw new RuntimeException(message, e);
|
||||
}
|
||||
}
|
||||
|
||||
public static void processPenaltiesFix(Block block) throws DataException {
|
||||
|
||||
// Create reverse penalties
|
||||
List<AccountPenaltyData> reversePenalties = penalties.stream()
|
||||
.map(penalty -> new AccountPenaltyData(penalty.getAddress(), 0 +5000000))
|
||||
.collect(Collectors.toList());
|
||||
|
||||
Set<AccountPenaltyData> penaltiesFix = new HashSet<AccountPenaltyData>(reversePenalties);
|
||||
|
||||
final String PENALTY_FIX_HASH_VERIFY = getHash(penaltiesFix.stream().map(p -> p.getAddress()).collect(Collectors.toList()));
|
||||
|
||||
// Verify if we are on same penalty hash
|
||||
if (PENALTY_FIX_HASH.equals(PENALTY_FIX_HASH_VERIFY)) {
|
||||
LOGGER.info("Verify hash passed! Running process penalty fix - this will take a while...");
|
||||
logPenaltyStats(block.repository);
|
||||
long startTime = System.currentTimeMillis();
|
||||
block.repository.getAccountRepository().updateBlocksMintedPenalties(penaltiesFix);
|
||||
long totalTime = System.currentTimeMillis() - startTime;
|
||||
LOGGER.info("{} penalty addresses processed. Total time taken: {} seconds", reversePenalties.size(), (int)(totalTime / 1000.0f));
|
||||
logPenaltyStats(block.repository);
|
||||
|
||||
int updatedCount = updateAccountLevels(block.repository, penaltiesFix);
|
||||
LOGGER.info("Account levels updated for {} penalty addresses", updatedCount);
|
||||
} else {
|
||||
LOGGER.info("Verify hash failed! Stopping process penalty fix!");
|
||||
}
|
||||
}
|
||||
|
||||
public static void orphanPenaltiesFix(Block block) throws DataException {
|
||||
|
||||
// Create inverse penalties
|
||||
List<AccountPenaltyData> inversePenalties = penalties.stream()
|
||||
.map(penalty -> new AccountPenaltyData(penalty.getAddress(), 0 -5000000))
|
||||
.collect(Collectors.toList());
|
||||
|
||||
Set<AccountPenaltyData> penaltiesFix = new HashSet<AccountPenaltyData>(inversePenalties);
|
||||
|
||||
final String PENALTY_FIX_HASH_VERIFY = getHash(penaltiesFix.stream().map(p -> p.getAddress()).collect(Collectors.toList()));
|
||||
|
||||
// Verify if we are on same penalty hash
|
||||
if (PENALTY_FIX_HASH.equals(PENALTY_FIX_HASH_VERIFY)) {
|
||||
LOGGER.info("Verify hash passed! Running orphan penalty fix - this will take a while...");
|
||||
logPenaltyStats(block.repository);
|
||||
long startTime = System.currentTimeMillis();
|
||||
block.repository.getAccountRepository().updateBlocksMintedPenalties(penaltiesFix);
|
||||
long totalTime = System.currentTimeMillis() - startTime;
|
||||
LOGGER.info("{} penalty addresses processed. Total time taken: {} seconds", inversePenalties.size(), (int)(totalTime / 1000.0f));
|
||||
logPenaltyStats(block.repository);
|
||||
|
||||
int updatedCount = updateAccountLevels(block.repository, penaltiesFix);
|
||||
LOGGER.info("Account levels updated for {} penalty addresses", updatedCount);
|
||||
} else {
|
||||
LOGGER.info("Verify hash failed! Stopping orphan penalty fix!");
|
||||
}
|
||||
}
|
||||
|
||||
private static int updateAccountLevels(Repository repository, Set<AccountPenaltyData> accountPenalties) throws DataException {
|
||||
final List<Integer> cumulativeBlocksByLevel = BlockChain.getInstance().getCumulativeBlocksByLevel();
|
||||
final int maximumLevel = cumulativeBlocksByLevel.size() - 1;
|
||||
|
||||
int updatedCount = 0;
|
||||
|
||||
for (AccountPenaltyData penaltyData : accountPenalties) {
|
||||
AccountData accountData = repository.getAccountRepository().getAccount(penaltyData.getAddress());
|
||||
final int effectiveBlocksMinted = accountData.getBlocksMinted() + accountData.getBlocksMintedAdjustment() + accountData.getBlocksMintedPenalty();
|
||||
|
||||
// Shortcut for penalties
|
||||
if (effectiveBlocksMinted < 0) {
|
||||
accountData.setLevel(0);
|
||||
repository.getAccountRepository().setLevel(accountData);
|
||||
updatedCount++;
|
||||
LOGGER.trace(() -> String.format("Block minter %s dropped to level %d", accountData.getAddress(), accountData.getLevel()));
|
||||
continue;
|
||||
}
|
||||
|
||||
for (int newLevel = maximumLevel; newLevel >= 0; --newLevel) {
|
||||
if (effectiveBlocksMinted >= cumulativeBlocksByLevel.get(newLevel)) {
|
||||
accountData.setLevel(newLevel);
|
||||
repository.getAccountRepository().setLevel(accountData);
|
||||
updatedCount++;
|
||||
LOGGER.trace(() -> String.format("Block minter %s increased to level %d", accountData.getAddress(), accountData.getLevel()));
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
return updatedCount;
|
||||
}
|
||||
|
||||
private static void logPenaltyStats(Repository repository) {
|
||||
try {
|
||||
LOGGER.info(getPenaltyStats(repository));
|
||||
} catch (DataException e) {}
|
||||
}
|
||||
|
||||
private static AccountPenaltyStats getPenaltyStats(Repository repository) throws DataException {
|
||||
List<AccountData> accounts = repository.getAccountRepository().getPenaltyAccounts();
|
||||
return AccountPenaltyStats.fromAccounts(accounts);
|
||||
}
|
||||
|
||||
public static String getHash(List<String> penaltyAddresses) {
|
||||
if (penaltyAddresses == null || penaltyAddresses.isEmpty()) {
|
||||
return null;
|
||||
}
|
||||
|
||||
Collections.sort(penaltyAddresses);
|
||||
return Base58.encode(Crypto.digest(StringUtils.join(penaltyAddresses).getBytes(StandardCharsets.UTF_8)));
|
||||
}
|
||||
}
|
143
src/main/java/org/qortal/block/SelfSponsorshipAlgoV2Block.java
Normal file
143
src/main/java/org/qortal/block/SelfSponsorshipAlgoV2Block.java
Normal file
@@ -0,0 +1,143 @@
|
||||
package org.qortal.block;
|
||||
|
||||
import org.apache.commons.lang3.StringUtils;
|
||||
import org.apache.logging.log4j.LogManager;
|
||||
import org.apache.logging.log4j.Logger;
|
||||
import org.qortal.account.SelfSponsorshipAlgoV2;
|
||||
import org.qortal.api.model.AccountPenaltyStats;
|
||||
import org.qortal.crypto.Crypto;
|
||||
import org.qortal.data.account.AccountData;
|
||||
import org.qortal.data.account.AccountPenaltyData;
|
||||
import org.qortal.repository.DataException;
|
||||
import org.qortal.repository.Repository;
|
||||
import org.qortal.utils.Base58;
|
||||
|
||||
import java.nio.charset.StandardCharsets;
|
||||
import java.util.*;
|
||||
import java.util.stream.Collectors;
|
||||
|
||||
/**
|
||||
* Self Sponsorship AlgoV2 Block
|
||||
* <p>
|
||||
* Selected block for the initial run on the "self sponsorship detection algorithm"
|
||||
*/
|
||||
public final class SelfSponsorshipAlgoV2Block {
|
||||
|
||||
private static final Logger LOGGER = LogManager.getLogger(SelfSponsorshipAlgoV2Block.class);
|
||||
|
||||
private SelfSponsorshipAlgoV2Block() {
|
||||
/* Do not instantiate */
|
||||
}
|
||||
|
||||
public static void processAccountPenalties(Block block) throws DataException {
|
||||
LOGGER.info("Process Self Sponsorship Algo V2 - this will take a while...");
|
||||
logPenaltyStats(block.repository);
|
||||
long startTime = System.currentTimeMillis();
|
||||
Set<AccountPenaltyData> penalties = getAccountPenalties(block.repository, -5000000);
|
||||
block.repository.getAccountRepository().updateBlocksMintedPenalties(penalties);
|
||||
long totalTime = System.currentTimeMillis() - startTime;
|
||||
String hash = getHash(penalties.stream().map(p -> p.getAddress()).collect(Collectors.toList()));
|
||||
LOGGER.info("{} penalty addresses processed (hash: {}). Total time taken: {} seconds", penalties.size(), hash, (int)(totalTime / 1000.0f));
|
||||
logPenaltyStats(block.repository);
|
||||
|
||||
int updatedCount = updateAccountLevels(block.repository, penalties);
|
||||
LOGGER.info("Account levels updated for {} penalty addresses", updatedCount);
|
||||
}
|
||||
|
||||
public static void orphanAccountPenalties(Block block) throws DataException {
|
||||
LOGGER.info("Orphan Self Sponsorship Algo V2 - this will take a while...");
|
||||
logPenaltyStats(block.repository);
|
||||
long startTime = System.currentTimeMillis();
|
||||
Set<AccountPenaltyData> penalties = getAccountPenalties(block.repository, 5000000);
|
||||
block.repository.getAccountRepository().updateBlocksMintedPenalties(penalties);
|
||||
long totalTime = System.currentTimeMillis() - startTime;
|
||||
String hash = getHash(penalties.stream().map(p -> p.getAddress()).collect(Collectors.toList()));
|
||||
LOGGER.info("{} penalty addresses orphaned (hash: {}). Total time taken: {} seconds", penalties.size(), hash, (int)(totalTime / 1000.0f));
|
||||
logPenaltyStats(block.repository);
|
||||
|
||||
int updatedCount = updateAccountLevels(block.repository, penalties);
|
||||
LOGGER.info("Account levels updated for {} penalty addresses", updatedCount);
|
||||
}
|
||||
|
||||
private static Set<AccountPenaltyData> getAccountPenalties(Repository repository, int penalty) throws DataException {
|
||||
Set<AccountPenaltyData> penalties = new LinkedHashSet<>();
|
||||
List<AccountData> penalizedAddresses = repository.getAccountRepository().getPenaltyAccounts();
|
||||
List<String> assetAddresses = repository.getTransactionRepository().getConfirmedTransferAssetCreators();
|
||||
|
||||
for (AccountData penalizedAddress : penalizedAddresses) {
|
||||
//System.out.println(String.format("address: %s", address));
|
||||
SelfSponsorshipAlgoV2 selfSponsorshipAlgoV2 = new SelfSponsorshipAlgoV2(repository, penalizedAddress.getAddress(), false);
|
||||
selfSponsorshipAlgoV2.run();
|
||||
//System.out.println(String.format("Penalty addresses: %d", selfSponsorshipAlgoV2.getPenaltyAddresses().size()));
|
||||
for (String penaltyAddress : selfSponsorshipAlgoV2.getPenaltyAddresses()) {
|
||||
penalties.add(new AccountPenaltyData(penaltyAddress, penalty));
|
||||
}
|
||||
}
|
||||
|
||||
for (String assetAddress : assetAddresses) {
|
||||
//System.out.println(String.format("address: %s", address));
|
||||
SelfSponsorshipAlgoV2 selfSponsorshipAlgoV2 = new SelfSponsorshipAlgoV2(repository, assetAddress, true);
|
||||
selfSponsorshipAlgoV2.run();
|
||||
//System.out.println(String.format("Penalty addresses: %d", selfSponsorshipAlgoV2.getPenaltyAddresses().size()));
|
||||
for (String penaltyAddress : selfSponsorshipAlgoV2.getPenaltyAddresses()) {
|
||||
penalties.add(new AccountPenaltyData(penaltyAddress, penalty));
|
||||
}
|
||||
}
|
||||
|
||||
return penalties;
|
||||
}
|
||||
|
||||
private static int updateAccountLevels(Repository repository, Set<AccountPenaltyData> accountPenalties) throws DataException {
|
||||
final List<Integer> cumulativeBlocksByLevel = BlockChain.getInstance().getCumulativeBlocksByLevel();
|
||||
final int maximumLevel = cumulativeBlocksByLevel.size() - 1;
|
||||
|
||||
int updatedCount = 0;
|
||||
|
||||
for (AccountPenaltyData penaltyData : accountPenalties) {
|
||||
AccountData accountData = repository.getAccountRepository().getAccount(penaltyData.getAddress());
|
||||
final int effectiveBlocksMinted = accountData.getBlocksMinted() + accountData.getBlocksMintedAdjustment() + accountData.getBlocksMintedPenalty();
|
||||
|
||||
// Shortcut for penalties
|
||||
if (effectiveBlocksMinted < 0) {
|
||||
accountData.setLevel(0);
|
||||
repository.getAccountRepository().setLevel(accountData);
|
||||
updatedCount++;
|
||||
LOGGER.trace(() -> String.format("Block minter %s dropped to level %d", accountData.getAddress(), accountData.getLevel()));
|
||||
continue;
|
||||
}
|
||||
|
||||
for (int newLevel = maximumLevel; newLevel >= 0; --newLevel) {
|
||||
if (effectiveBlocksMinted >= cumulativeBlocksByLevel.get(newLevel)) {
|
||||
accountData.setLevel(newLevel);
|
||||
repository.getAccountRepository().setLevel(accountData);
|
||||
updatedCount++;
|
||||
LOGGER.trace(() -> String.format("Block minter %s increased to level %d", accountData.getAddress(), accountData.getLevel()));
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return updatedCount;
|
||||
}
|
||||
|
||||
private static void logPenaltyStats(Repository repository) {
|
||||
try {
|
||||
LOGGER.info(getPenaltyStats(repository));
|
||||
|
||||
} catch (DataException e) {}
|
||||
}
|
||||
|
||||
private static AccountPenaltyStats getPenaltyStats(Repository repository) throws DataException {
|
||||
List<AccountData> accounts = repository.getAccountRepository().getPenaltyAccounts();
|
||||
return AccountPenaltyStats.fromAccounts(accounts);
|
||||
}
|
||||
|
||||
public static String getHash(List<String> penaltyAddresses) {
|
||||
if (penaltyAddresses == null || penaltyAddresses.isEmpty()) {
|
||||
return null;
|
||||
}
|
||||
Collections.sort(penaltyAddresses);
|
||||
return Base58.encode(Crypto.digest(StringUtils.join(penaltyAddresses).getBytes(StandardCharsets.UTF_8)));
|
||||
}
|
||||
|
||||
}
|
136
src/main/java/org/qortal/block/SelfSponsorshipAlgoV3Block.java
Normal file
136
src/main/java/org/qortal/block/SelfSponsorshipAlgoV3Block.java
Normal file
@@ -0,0 +1,136 @@
|
||||
package org.qortal.block;
|
||||
|
||||
import org.apache.commons.lang3.StringUtils;
|
||||
import org.apache.logging.log4j.LogManager;
|
||||
import org.apache.logging.log4j.Logger;
|
||||
import org.qortal.account.SelfSponsorshipAlgoV3;
|
||||
import org.qortal.api.model.AccountPenaltyStats;
|
||||
import org.qortal.crypto.Crypto;
|
||||
import org.qortal.data.account.AccountData;
|
||||
import org.qortal.data.account.AccountPenaltyData;
|
||||
import org.qortal.repository.DataException;
|
||||
import org.qortal.repository.Repository;
|
||||
import org.qortal.utils.Base58;
|
||||
|
||||
import java.nio.charset.StandardCharsets;
|
||||
import java.util.Collections;
|
||||
import java.util.LinkedHashSet;
|
||||
import java.util.List;
|
||||
import java.util.Set;
|
||||
import java.util.stream.Collectors;
|
||||
|
||||
/**
|
||||
* Self Sponsorship AlgoV3 Block
|
||||
* <p>
|
||||
* Selected block for the initial run on the "self sponsorship detection algorithm"
|
||||
*/
|
||||
public final class SelfSponsorshipAlgoV3Block {
|
||||
|
||||
private static final Logger LOGGER = LogManager.getLogger(SelfSponsorshipAlgoV3Block.class);
|
||||
|
||||
private SelfSponsorshipAlgoV3Block() {
|
||||
/* Do not instantiate */
|
||||
}
|
||||
|
||||
public static void processAccountPenalties(Block block) throws DataException {
|
||||
LOGGER.info("Process Self Sponsorship Algo V3 - this will take a while...");
|
||||
logPenaltyStats(block.repository);
|
||||
long startTime = System.currentTimeMillis();
|
||||
Set<AccountPenaltyData> penalties = getAccountPenalties(block.repository, -5000000);
|
||||
block.repository.getAccountRepository().updateBlocksMintedPenalties(penalties);
|
||||
long totalTime = System.currentTimeMillis() - startTime;
|
||||
String hash = getHash(penalties.stream().map(p -> p.getAddress()).collect(Collectors.toList()));
|
||||
LOGGER.info("{} penalty addresses processed (hash: {}). Total time taken: {} seconds", penalties.size(), hash, (int)(totalTime / 1000.0f));
|
||||
logPenaltyStats(block.repository);
|
||||
|
||||
int updatedCount = updateAccountLevels(block.repository, penalties);
|
||||
LOGGER.info("Account levels updated for {} penalty addresses", updatedCount);
|
||||
}
|
||||
|
||||
public static void orphanAccountPenalties(Block block) throws DataException {
|
||||
LOGGER.info("Orphan Self Sponsorship Algo V3 - this will take a while...");
|
||||
logPenaltyStats(block.repository);
|
||||
long startTime = System.currentTimeMillis();
|
||||
Set<AccountPenaltyData> penalties = getAccountPenalties(block.repository, 5000000);
|
||||
block.repository.getAccountRepository().updateBlocksMintedPenalties(penalties);
|
||||
long totalTime = System.currentTimeMillis() - startTime;
|
||||
String hash = getHash(penalties.stream().map(p -> p.getAddress()).collect(Collectors.toList()));
|
||||
LOGGER.info("{} penalty addresses orphaned (hash: {}). Total time taken: {} seconds", penalties.size(), hash, (int)(totalTime / 1000.0f));
|
||||
logPenaltyStats(block.repository);
|
||||
|
||||
int updatedCount = updateAccountLevels(block.repository, penalties);
|
||||
LOGGER.info("Account levels updated for {} penalty addresses", updatedCount);
|
||||
}
|
||||
|
||||
public static Set<AccountPenaltyData> getAccountPenalties(Repository repository, int penalty) throws DataException {
|
||||
final long snapshotTimestampV1 = BlockChain.getInstance().getSelfSponsorshipAlgoV1SnapshotTimestamp();
|
||||
final long snapshotTimestampV3 = BlockChain.getInstance().getSelfSponsorshipAlgoV3SnapshotTimestamp();
|
||||
Set<AccountPenaltyData> penalties = new LinkedHashSet<>();
|
||||
List<String> addresses = repository.getTransactionRepository().getConfirmedRewardShareCreatorsExcludingSelfShares();
|
||||
for (String address : addresses) {
|
||||
//System.out.println(String.format("address: %s", address));
|
||||
SelfSponsorshipAlgoV3 selfSponsorshipAlgoV3 = new SelfSponsorshipAlgoV3(repository, address, snapshotTimestampV1, snapshotTimestampV3, false);
|
||||
selfSponsorshipAlgoV3.run();
|
||||
//System.out.println(String.format("Penalty addresses: %d", selfSponsorshipAlgoV3.getPenaltyAddresses().size()));
|
||||
|
||||
for (String penaltyAddress : selfSponsorshipAlgoV3.getPenaltyAddresses()) {
|
||||
penalties.add(new AccountPenaltyData(penaltyAddress, penalty));
|
||||
}
|
||||
}
|
||||
return penalties;
|
||||
}
|
||||
|
||||
private static int updateAccountLevels(Repository repository, Set<AccountPenaltyData> accountPenalties) throws DataException {
|
||||
final List<Integer> cumulativeBlocksByLevel = BlockChain.getInstance().getCumulativeBlocksByLevel();
|
||||
final int maximumLevel = cumulativeBlocksByLevel.size() - 1;
|
||||
|
||||
int updatedCount = 0;
|
||||
|
||||
for (AccountPenaltyData penaltyData : accountPenalties) {
|
||||
AccountData accountData = repository.getAccountRepository().getAccount(penaltyData.getAddress());
|
||||
final int effectiveBlocksMinted = accountData.getBlocksMinted() + accountData.getBlocksMintedAdjustment() + accountData.getBlocksMintedPenalty();
|
||||
|
||||
// Shortcut for penalties
|
||||
if (effectiveBlocksMinted < 0) {
|
||||
accountData.setLevel(0);
|
||||
repository.getAccountRepository().setLevel(accountData);
|
||||
updatedCount++;
|
||||
LOGGER.trace(() -> String.format("Block minter %s dropped to level %d", accountData.getAddress(), accountData.getLevel()));
|
||||
continue;
|
||||
}
|
||||
|
||||
for (int newLevel = maximumLevel; newLevel >= 0; --newLevel) {
|
||||
if (effectiveBlocksMinted >= cumulativeBlocksByLevel.get(newLevel)) {
|
||||
accountData.setLevel(newLevel);
|
||||
repository.getAccountRepository().setLevel(accountData);
|
||||
updatedCount++;
|
||||
LOGGER.trace(() -> String.format("Block minter %s increased to level %d", accountData.getAddress(), accountData.getLevel()));
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return updatedCount;
|
||||
}
|
||||
|
||||
private static void logPenaltyStats(Repository repository) {
|
||||
try {
|
||||
LOGGER.info(getPenaltyStats(repository));
|
||||
|
||||
} catch (DataException e) {}
|
||||
}
|
||||
|
||||
private static AccountPenaltyStats getPenaltyStats(Repository repository) throws DataException {
|
||||
List<AccountData> accounts = repository.getAccountRepository().getPenaltyAccounts();
|
||||
return AccountPenaltyStats.fromAccounts(accounts);
|
||||
}
|
||||
|
||||
public static String getHash(List<String> penaltyAddresses) {
|
||||
if (penaltyAddresses == null || penaltyAddresses.isEmpty()) {
|
||||
return null;
|
||||
}
|
||||
Collections.sort(penaltyAddresses);
|
||||
return Base58.encode(Crypto.digest(StringUtils.join(penaltyAddresses).getBytes(StandardCharsets.UTF_8)));
|
||||
}
|
||||
|
||||
}
|
@@ -159,8 +159,7 @@ public class BlockMinter extends Thread {
|
||||
int level = mintingAccount.getEffectiveMintingLevel();
|
||||
if (level < BlockChain.getInstance().getMinAccountLevelForBlockSubmissions()) {
|
||||
madi.remove();
|
||||
continue;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Needs a mutable copy of the unmodifiableList
|
||||
@@ -172,7 +171,7 @@ public class BlockMinter extends Thread {
|
||||
|
||||
// Disregard peers that don't have a recent block, but only if we're not in recovery mode.
|
||||
// In that mode, we want to allow minting on top of older blocks, to recover stalled networks.
|
||||
if (Synchronizer.getInstance().getRecoveryMode() == false)
|
||||
if (!Synchronizer.getInstance().getRecoveryMode())
|
||||
peers.removeIf(Controller.hasNoRecentBlock);
|
||||
|
||||
// Don't mint if we don't have enough up-to-date peers as where would the transactions/consensus come from?
|
||||
@@ -197,7 +196,7 @@ public class BlockMinter extends Thread {
|
||||
|
||||
// If our latest block isn't recent then we need to synchronize instead of minting, unless we're in recovery mode.
|
||||
if (!peers.isEmpty() && lastBlockData.getTimestamp() < minLatestBlockTimestamp)
|
||||
if (Synchronizer.getInstance().getRecoveryMode() == false && recoverInvalidBlock == false)
|
||||
if (!Synchronizer.getInstance().getRecoveryMode() && !recoverInvalidBlock)
|
||||
continue;
|
||||
|
||||
// There are enough peers with a recent block and our latest block is recent
|
||||
|
@@ -17,7 +17,7 @@ public class ChatNotifier {
|
||||
void notify(ChatTransactionData chatTransactionData);
|
||||
}
|
||||
|
||||
private Map<Session, Listener> listenersBySession = new HashMap<>();
|
||||
private final Map<Session, Listener> listenersBySession = new HashMap<>();
|
||||
|
||||
private ChatNotifier() {
|
||||
}
|
||||
|
@@ -564,6 +564,34 @@ public class Controller extends Thread {
|
||||
|
||||
// If GUI is enabled, we're no longer starting up but actually running now
|
||||
Gui.getInstance().notifyRunning();
|
||||
|
||||
// Check every 10 minutes to see if the block minter is running
|
||||
Timer timer = new Timer();
|
||||
|
||||
timer.schedule(new TimerTask() {
|
||||
@Override
|
||||
public void run() {
|
||||
if (blockMinter.isAlive()) {
|
||||
LOGGER.debug("Block minter is running? {}", blockMinter.isAlive());
|
||||
} else if (!blockMinter.isAlive()) {
|
||||
LOGGER.debug("Block minter is running? {}", blockMinter.isAlive());
|
||||
blockMinter.shutdown();
|
||||
|
||||
try {
|
||||
// Wait 10 seconds before restart
|
||||
TimeUnit.SECONDS.sleep(10);
|
||||
|
||||
// Start new block minter thread
|
||||
LOGGER.info("Restarting block minter");
|
||||
blockMinter.start();
|
||||
} catch (InterruptedException e) {
|
||||
// Couldn't start new block minter thread
|
||||
LOGGER.info("Starting block minter failed {}", e);
|
||||
throw new RuntimeException(e);
|
||||
}
|
||||
}
|
||||
}
|
||||
}, 10*60*1000, 10*60*1000);
|
||||
}
|
||||
|
||||
/** Called by AdvancedInstaller's launch EXE in single-instance mode, when an instance is already running. */
|
||||
@@ -571,7 +599,6 @@ public class Controller extends Thread {
|
||||
// Return as we don't want to run more than one instance
|
||||
}
|
||||
|
||||
|
||||
// Main thread
|
||||
|
||||
@Override
|
||||
@@ -775,7 +802,7 @@ public class Controller extends Thread {
|
||||
|
||||
public static final Predicate<Peer> hasOldVersion = peer -> {
|
||||
final String minPeerVersion = Settings.getInstance().getMinPeerVersion();
|
||||
return peer.isAtLeastVersion(minPeerVersion) == false;
|
||||
return !peer.isAtLeastVersion(minPeerVersion);
|
||||
};
|
||||
|
||||
public static final Predicate<Peer> hasInvalidSigner = peer -> {
|
||||
@@ -1921,8 +1948,7 @@ public class Controller extends Thread {
|
||||
// Disregard peers that don't have a recent block
|
||||
if (peerChainTipData.getTimestamp() == null || peerChainTipData.getTimestamp() < minLatestBlockTimestamp) {
|
||||
iterator.remove();
|
||||
continue;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return peers;
|
||||
@@ -2002,5 +2028,4 @@ public class Controller extends Thread {
|
||||
public StatsSnapshot getStatsSnapshot() {
|
||||
return this.stats;
|
||||
}
|
||||
|
||||
}
|
||||
|
@@ -538,7 +538,6 @@ public class OnlineAccountsManager {
|
||||
|
||||
if (++i > 1 + 1) {
|
||||
iterator.remove();
|
||||
continue;
|
||||
}
|
||||
}
|
||||
} catch (DataException e) {
|
||||
|
@@ -2,7 +2,7 @@ package org.qortal.controller;
|
||||
|
||||
import org.apache.logging.log4j.LogManager;
|
||||
import org.apache.logging.log4j.Logger;
|
||||
import org.qortal.ApplyRestart;;
|
||||
import org.qortal.ApplyRestart;
|
||||
import org.qortal.globalization.Translator;
|
||||
import org.qortal.gui.SysTray;
|
||||
import org.qortal.repository.RepositoryManager;
|
||||
|
@@ -90,8 +90,8 @@ public class Synchronizer extends Thread {
|
||||
private static Synchronizer instance;
|
||||
|
||||
public enum SynchronizationResult {
|
||||
OK, NOTHING_TO_DO, GENESIS_ONLY, NO_COMMON_BLOCK, TOO_DIVERGENT, NO_REPLY, INFERIOR_CHAIN, INVALID_DATA, NO_BLOCKCHAIN_LOCK, REPOSITORY_ISSUE, SHUTTING_DOWN, CHAIN_TIP_TOO_OLD;
|
||||
}
|
||||
OK, NOTHING_TO_DO, GENESIS_ONLY, NO_COMMON_BLOCK, TOO_DIVERGENT, NO_REPLY, INFERIOR_CHAIN, INVALID_DATA, NO_BLOCKCHAIN_LOCK, REPOSITORY_ISSUE, SHUTTING_DOWN, CHAIN_TIP_TOO_OLD
|
||||
}
|
||||
|
||||
public static class NewChainTipEvent implements Event {
|
||||
private final BlockData priorChainTip;
|
||||
@@ -258,7 +258,7 @@ public class Synchronizer extends Thread {
|
||||
peers.removeIf(Controller.hasNoRecentBlock);
|
||||
|
||||
final int peersRemoved = peersBeforeComparison - peers.size();
|
||||
if (peersRemoved > 0 && peers.size() > 0)
|
||||
if (peersRemoved > 0 && !peers.isEmpty())
|
||||
LOGGER.debug(String.format("Ignoring %d peers on inferior chains. Peers remaining: %d", peersRemoved, peers.size()));
|
||||
|
||||
if (peers.isEmpty())
|
||||
@@ -392,7 +392,7 @@ public class Synchronizer extends Thread {
|
||||
private boolean checkRecoveryModeForPeers(List<Peer> qualifiedPeers) {
|
||||
List<Peer> handshakedPeers = Network.getInstance().getImmutableHandshakedPeers();
|
||||
|
||||
if (handshakedPeers.size() > 0) {
|
||||
if (!handshakedPeers.isEmpty()) {
|
||||
// There is at least one handshaked peer
|
||||
if (qualifiedPeers.isEmpty()) {
|
||||
// There are no 'qualified' peers - i.e. peers that have a recent block we can sync to
|
||||
@@ -406,7 +406,7 @@ public class Synchronizer extends Thread {
|
||||
// If enough time has passed, enter recovery mode, which lifts some restrictions on who we can sync with and when we can mint
|
||||
long recoveryModeTimeout = Settings.getInstance().getRecoveryModeTimeout();
|
||||
if (NTP.getTime() - timePeersLastAvailable > recoveryModeTimeout) {
|
||||
if (recoveryMode == false) {
|
||||
if (!recoveryMode) {
|
||||
LOGGER.info(String.format("Peers have been unavailable for %d minutes. Entering recovery mode...", recoveryModeTimeout/60/1000));
|
||||
recoveryMode = true;
|
||||
}
|
||||
@@ -445,7 +445,7 @@ public class Synchronizer extends Thread {
|
||||
try (final Repository repository = RepositoryManager.getRepository()) {
|
||||
try {
|
||||
|
||||
if (peers.size() == 0)
|
||||
if (peers.isEmpty())
|
||||
return SynchronizationResult.NOTHING_TO_DO;
|
||||
|
||||
// If our latest block is very old, it's best that we don't try and determine the best peers to sync to.
|
||||
@@ -663,7 +663,7 @@ public class Synchronizer extends Thread {
|
||||
}
|
||||
}
|
||||
|
||||
if (useCachedSummaries == false) {
|
||||
if (!useCachedSummaries) {
|
||||
if (summariesRequired > 0) {
|
||||
LOGGER.trace(String.format("Requesting %d block summar%s from peer %s after common block %.8s. Peer height: %d", summariesRequired, (summariesRequired != 1 ? "ies" : "y"), peer, Base58.encode(commonBlockSummary.getSignature()), peerHeight));
|
||||
|
||||
@@ -701,7 +701,7 @@ public class Synchronizer extends Thread {
|
||||
|
||||
// Reduce minChainLength if needed. If we don't have any blocks, this peer will be excluded from chain weight comparisons later in the process, so we shouldn't update minChainLength
|
||||
List <BlockSummaryData> peerBlockSummaries = peer.getCommonBlockData().getBlockSummariesAfterCommonBlock();
|
||||
if (peerBlockSummaries != null && peerBlockSummaries.size() > 0)
|
||||
if (peerBlockSummaries != null && !peerBlockSummaries.isEmpty())
|
||||
if (peerBlockSummaries.size() < minChainLength)
|
||||
minChainLength = peerBlockSummaries.size();
|
||||
}
|
||||
@@ -728,7 +728,7 @@ public class Synchronizer extends Thread {
|
||||
|
||||
// Calculate our chain weight
|
||||
BigInteger ourChainWeight = BigInteger.valueOf(0);
|
||||
if (ourBlockSummaries.size() > 0)
|
||||
if (!ourBlockSummaries.isEmpty())
|
||||
ourChainWeight = Block.calcChainWeight(commonBlockSummary.getHeight(), commonBlockSummary.getSignature(), ourBlockSummaries, maxHeightForChainWeightComparisons);
|
||||
|
||||
LOGGER.debug(String.format("Our chain weight based on %d blocks is %s", (usingSameLengthChainWeight ? minChainLength : ourBlockSummaries.size()), accurateFormatter.format(ourChainWeight)));
|
||||
@@ -780,7 +780,7 @@ public class Synchronizer extends Thread {
|
||||
}
|
||||
|
||||
// Now that we have selected the best peers, compare them against each other and remove any with lower weights
|
||||
if (superiorPeersForComparison.size() > 0) {
|
||||
if (!superiorPeersForComparison.isEmpty()) {
|
||||
BigInteger bestChainWeight = null;
|
||||
for (Peer peer : superiorPeersForComparison) {
|
||||
// Increase bestChainWeight if needed
|
||||
@@ -1290,7 +1290,7 @@ public class Synchronizer extends Thread {
|
||||
cachedCommonBlockData.setBlockSummariesAfterCommonBlock(null);
|
||||
|
||||
// If we have already received newer blocks from this peer that what we have already, go ahead and apply them
|
||||
if (peerBlocks.size() > 0) {
|
||||
if (!peerBlocks.isEmpty()) {
|
||||
final BlockData ourLatestBlockData = repository.getBlockRepository().getLastBlock();
|
||||
final Block peerLatestBlock = peerBlocks.get(peerBlocks.size() - 1);
|
||||
final Long minLatestBlockTimestamp = Controller.getMinimumLatestBlockTimestamp();
|
||||
@@ -1352,7 +1352,7 @@ public class Synchronizer extends Thread {
|
||||
|
||||
if (retryCount >= maxRetries) {
|
||||
// If we have already received newer blocks from this peer that what we have already, go ahead and apply them
|
||||
if (peerBlocks.size() > 0) {
|
||||
if (!peerBlocks.isEmpty()) {
|
||||
final BlockData ourLatestBlockData = repository.getBlockRepository().getLastBlock();
|
||||
final Block peerLatestBlock = peerBlocks.get(peerBlocks.size() - 1);
|
||||
final Long minLatestBlockTimestamp = Controller.getMinimumLatestBlockTimestamp();
|
||||
|
@@ -208,8 +208,7 @@ public class ArbitraryDataCleanupManager extends Thread {
|
||||
Base58.encode(arbitraryTransactionData.getSignature())));
|
||||
|
||||
ArbitraryTransactionUtils.convertFileToChunks(arbitraryTransactionData, now, STALE_FILE_TIMEOUT);
|
||||
continue;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
} catch (DataException e) {
|
||||
@@ -284,8 +283,7 @@ public class ArbitraryDataCleanupManager extends Thread {
|
||||
}
|
||||
|
||||
} catch (DataException e) {
|
||||
continue;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return pathList;
|
||||
|
@@ -605,7 +605,7 @@ public class ArbitraryDataFileListManager {
|
||||
}
|
||||
|
||||
// Add the chunk hashes
|
||||
if (arbitraryDataFile.getChunkHashes().size() > 0) {
|
||||
if (!arbitraryDataFile.getChunkHashes().isEmpty()) {
|
||||
requestedHashes.addAll(arbitraryDataFile.getChunkHashes());
|
||||
}
|
||||
// Add complete file if there are no hashes
|
||||
@@ -641,7 +641,7 @@ public class ArbitraryDataFileListManager {
|
||||
}
|
||||
|
||||
// We should only respond if we have at least one hash
|
||||
if (hashes.size() > 0) {
|
||||
if (!hashes.isEmpty()) {
|
||||
|
||||
// Firstly we should keep track of the requesting peer, to allow for potential direct connections later
|
||||
ArbitraryDataFileManager.getInstance().addRecentDataRequest(requestingPeer);
|
||||
|
@@ -43,7 +43,7 @@ public class ArbitraryDataFileManager extends Thread {
|
||||
/**
|
||||
* Map to keep track of hashes that we might need to relay
|
||||
*/
|
||||
public List<ArbitraryRelayInfo> arbitraryRelayMap = Collections.synchronizedList(new ArrayList<>());
|
||||
public final List<ArbitraryRelayInfo> arbitraryRelayMap = Collections.synchronizedList(new ArrayList<>());
|
||||
|
||||
/**
|
||||
* List to keep track of any arbitrary data file hash responses
|
||||
@@ -53,7 +53,7 @@ public class ArbitraryDataFileManager extends Thread {
|
||||
/**
|
||||
* List to keep track of peers potentially available for direct connections, based on recent requests
|
||||
*/
|
||||
private List<ArbitraryDirectConnectionInfo> directConnectionInfo = Collections.synchronizedList(new ArrayList<>());
|
||||
private final List<ArbitraryDirectConnectionInfo> directConnectionInfo = Collections.synchronizedList(new ArrayList<>());
|
||||
|
||||
/**
|
||||
* Map to keep track of peers requesting QDN data that we hold.
|
||||
@@ -242,13 +242,14 @@ public class ArbitraryDataFileManager extends Thread {
|
||||
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
|
||||
LOGGER.debug("Deleting file {} because it was needed for forwarding only", Base58.encode(hash));
|
||||
|
||||
// Keep trying to delete the data until it is deleted, or we reach 10 attempts
|
||||
// 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) {
|
||||
|
@@ -230,8 +230,7 @@ public class ArbitraryDataManager extends Thread {
|
||||
// Remove transactions that we already have local data for
|
||||
if (hasLocalData(arbitraryTransaction)) {
|
||||
iterator.remove();
|
||||
continue;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (signatures.isEmpty()) {
|
||||
@@ -313,8 +312,7 @@ public class ArbitraryDataManager extends Thread {
|
||||
// Remove transactions that we already have local data for
|
||||
if (hasLocalMetadata(arbitraryTransaction)) {
|
||||
iterator.remove();
|
||||
continue;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (signatures.isEmpty()) {
|
||||
|
@@ -291,7 +291,6 @@ public class ArbitraryDataStorageManager extends Thread {
|
||||
arbitraryTransactionDataList.add(arbitraryTransactionData);
|
||||
|
||||
} catch (DataException e) {
|
||||
continue;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -345,7 +344,6 @@ public class ArbitraryDataStorageManager extends Thread {
|
||||
}
|
||||
|
||||
} catch (Exception e) {
|
||||
continue;
|
||||
}
|
||||
}
|
||||
|
||||
|
@@ -334,11 +334,17 @@ public class ArbitraryMetadataManager {
|
||||
}
|
||||
ArbitraryTransactionData arbitraryTransactionData = (ArbitraryTransactionData) transactionData;
|
||||
|
||||
// Check if the name is blocked
|
||||
boolean isBlocked = (arbitraryTransactionData == null || ListUtils.isNameBlocked(arbitraryTransactionData.getName()));
|
||||
|
||||
// Save if not blocked
|
||||
ArbitraryDataFile arbitraryMetadataFile = arbitraryMetadataMessage.getArbitraryMetadataFile();
|
||||
if (!isBlocked && arbitraryMetadataFile != null) {
|
||||
arbitraryMetadataFile.save();
|
||||
}
|
||||
|
||||
// Forwarding
|
||||
if (isRelayRequest && Settings.getInstance().isRelayModeEnabled()) {
|
||||
|
||||
// Check if the name is blocked
|
||||
boolean isBlocked = (arbitraryTransactionData == null || ListUtils.isNameBlocked(arbitraryTransactionData.getName()));
|
||||
if (!isBlocked) {
|
||||
Peer requestingPeer = request.getB();
|
||||
if (requestingPeer != null) {
|
||||
|
@@ -207,7 +207,7 @@ public class NamesDatabaseIntegrityCheck {
|
||||
// FUTURE: check database integrity for names that have been updated and then the original name re-registered
|
||||
else if (Objects.equals(updateNameTransactionData.getName(), registeredName)) {
|
||||
String newName = updateNameTransactionData.getNewName();
|
||||
if (newName == null || newName.length() == 0) {
|
||||
if (newName == null || newName.isEmpty()) {
|
||||
// If new name is blank (or maybe null, just to be safe), it means that it stayed the same
|
||||
newName = registeredName;
|
||||
}
|
||||
|
@@ -724,8 +724,7 @@ public class TradeBot implements Listener {
|
||||
|
||||
} catch (DataException e) {
|
||||
LOGGER.info("Unable to determine failed state of AT {}", crossChainTradeData.qortalAtAddress);
|
||||
continue;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return updatedCrossChainTrades;
|
||||
|
@@ -22,8 +22,6 @@ public class Bitcoin extends Bitcoiny {
|
||||
private static final long MINIMUM_ORDER_AMOUNT = 100000; // 0.001 BTC minimum order, due to high fees
|
||||
|
||||
// Temporary values until a dynamic fee system is written.
|
||||
private static final long OLD_FEE_AMOUNT = 4_000L; // Not 5000 so that existing P2SH-B can output 1000, avoiding dust issue, leaving 4000 for fees.
|
||||
private static final long NEW_FEE_TIMESTAMP = 1598280000000L; // milliseconds since epoch
|
||||
private static final long NEW_FEE_AMOUNT = 6_000L;
|
||||
|
||||
private static final long NON_MAINNET_FEE = 1000L; // enough for TESTNET3 and should be OK for REGTEST
|
||||
@@ -46,74 +44,62 @@ public class Bitcoin extends Bitcoiny {
|
||||
return Arrays.asList(
|
||||
// Servers chosen on NO BASIS WHATSOEVER from various sources!
|
||||
// Status verified at https://1209k.com/bitcoin-eye/ele.php?chain=btc
|
||||
new Server("104.248.139.211", Server.ConnectionType.SSL, 50002),
|
||||
new Server("104.198.149.61", Server.ConnectionType.SSL, 50002),
|
||||
new Server("128.0.190.26", Server.ConnectionType.SSL, 50002),
|
||||
new Server("142.93.6.38", Server.ConnectionType.SSL, 50002),
|
||||
new Server("157.245.172.236", Server.ConnectionType.SSL, 50002),
|
||||
new Server("167.172.226.175", Server.ConnectionType.SSL, 50002),
|
||||
new Server("167.172.42.31", Server.ConnectionType.SSL, 50002),
|
||||
new Server("178.62.80.20", Server.ConnectionType.SSL, 50002),
|
||||
new Server("185.64.116.15", Server.ConnectionType.SSL, 50002),
|
||||
new Server("188.165.206.215", Server.ConnectionType.SSL, 50002),
|
||||
new Server("188.165.211.112", Server.ConnectionType.SSL, 50002),
|
||||
new Server("2azzarita.hopto.org", Server.ConnectionType.SSL, 50002),
|
||||
new Server("2electrumx.hopto.me", Server.ConnectionType.SSL, 56022),
|
||||
new Server("2ex.digitaleveryware.com", Server.ConnectionType.SSL, 50002),
|
||||
new Server("65.39.140.37", Server.ConnectionType.SSL, 50002),
|
||||
new Server("68.183.188.105", Server.ConnectionType.SSL, 50002),
|
||||
new Server("71.73.14.254", Server.ConnectionType.SSL, 50002),
|
||||
new Server("94.23.247.135", Server.ConnectionType.SSL, 50002),
|
||||
new Server("assuredly.not.fyi", Server.ConnectionType.SSL, 50002),
|
||||
new Server("ax101.blockeng.ch", Server.ConnectionType.SSL, 50002),
|
||||
new Server("ax102.blockeng.ch", Server.ConnectionType.SSL, 50002),
|
||||
new Server("260.whyza.net", Server.ConnectionType.SSL, 50002),
|
||||
new Server("34.136.93.37", Server.ConnectionType.SSL, 50002),
|
||||
new Server("34.67.22.216", Server.ConnectionType.SSL, 50002),
|
||||
new Server("34.68.133.78", Server.ConnectionType.SSL, 50002),
|
||||
new Server("alviss.coinjoined.com", Server.ConnectionType.SSL, 50002),
|
||||
new Server("b.1209k.com", Server.ConnectionType.SSL, 50002),
|
||||
new Server("b6.1209k.com", Server.ConnectionType.SSL, 50002),
|
||||
new Server("bitcoin.dermichi.com", Server.ConnectionType.SSL, 50002),
|
||||
new Server("bitcoin.lu.ke", Server.ConnectionType.SSL, 50002),
|
||||
new Server("bitcoin.lukechilds.co", Server.ConnectionType.SSL, 50002),
|
||||
new Server("blkhub.net", Server.ConnectionType.SSL, 50002),
|
||||
new Server("btc.electroncash.dk", Server.ConnectionType.SSL, 60002),
|
||||
new Server("btc.aftrek.org", Server.ConnectionType.SSL, 50002),
|
||||
new Server("btc.hodler.ninja", Server.ConnectionType.SSL, 50002),
|
||||
new Server("btc.ocf.sh", Server.ConnectionType.SSL, 50002),
|
||||
new Server("btce.iiiiiii.biz", Server.ConnectionType.SSL, 50002),
|
||||
new Server("caleb.vegas", Server.ConnectionType.SSL, 50002),
|
||||
new Server("d762li0k0g.d.firewalla.org", Server.ConnectionType.SSL, 50002),
|
||||
new Server("de.poiuty.com", Server.ConnectionType.SSL, 50002),
|
||||
new Server("dijon.anties.org", Server.ConnectionType.SSL, 50002),
|
||||
new Server("eai.coincited.net", Server.ConnectionType.SSL, 50002),
|
||||
new Server("electrum.bhoovd.com", Server.ConnectionType.SSL, 50002),
|
||||
new Server("electrum.bitaroo.net", Server.ConnectionType.SSL, 50002),
|
||||
new Server("electrum.bitcoinlizard.net", Server.ConnectionType.SSL, 50002),
|
||||
new Server("electrum.blockstream.info", Server.ConnectionType.SSL, 50002),
|
||||
new Server("electrum.bitrefill.com", Server.ConnectionType.SSL, 50002),
|
||||
new Server("electrum.brainshome.de", Server.ConnectionType.SSL, 50002),
|
||||
new Server("electrum.emzy.de", Server.ConnectionType.SSL, 50002),
|
||||
new Server("electrum.exan.tech", Server.ConnectionType.SSL, 50002),
|
||||
new Server("electrum.kcicom.net", Server.ConnectionType.SSL, 50002),
|
||||
new Server("electrum.kendigisland.xyz", Server.ConnectionType.SSL, 50002),
|
||||
new Server("electrum.mmitech.info", Server.ConnectionType.SSL, 50002),
|
||||
new Server("electrum.petrkr.net", Server.ConnectionType.SSL, 50002),
|
||||
new Server("electrum.stippy.com", Server.ConnectionType.SSL, 50002),
|
||||
new Server("electrum.thomasfischbach.de", Server.ConnectionType.SSL, 50002),
|
||||
new Server("electrum-btc.leblancnet.us", Server.ConnectionType.SSL, 50002),
|
||||
new Server("electrum0.snel.it", Server.ConnectionType.SSL, 50002),
|
||||
new Server("electrum1.cipig.net", Server.ConnectionType.SSL, 50002),
|
||||
new Server("electrum2.cipig.net", Server.ConnectionType.SSL, 50002),
|
||||
new Server("electrum3.cipig.net", Server.ConnectionType.SSL, 50002),
|
||||
new Server("electrumx.alexridevski.net", Server.ConnectionType.SSL, 50002),
|
||||
new Server("electrum1.cipig.net", Server.ConnectionType.SSL, 20000),
|
||||
new Server("electrum2.cipig.net", Server.ConnectionType.SSL, 20000),
|
||||
new Server("electrum3.cipig.net", Server.ConnectionType.SSL, 20000),
|
||||
new Server("electrumx.blockfinance-eco.li", Server.ConnectionType.SSL, 50002),
|
||||
new Server("electrumx.indoor.app", Server.ConnectionType.SSL, 50002),
|
||||
new Server("electrumx.iodata.org", Server.ConnectionType.SSL, 50002),
|
||||
new Server("electrumx-core.1209k.com", Server.ConnectionType.SSL, 50002),
|
||||
new Server("elx.bitske.com", Server.ConnectionType.SSL, 50002),
|
||||
new Server("ex03.axalgo.com", Server.ConnectionType.SSL, 50002),
|
||||
new Server("ex05.axalgo.com", Server.ConnectionType.SSL, 50002),
|
||||
new Server("ex07.axalgo.com", Server.ConnectionType.SSL, 50002),
|
||||
new Server("exs.dyshek.org", Server.ConnectionType.SSL, 50002),
|
||||
new Server("fortress.qtornado.com", Server.ConnectionType.SSL, 50002),
|
||||
new Server("fulcrum.grey.pw", Server.ConnectionType.SSL, 50002),
|
||||
new Server("fulcrum.sethforprivacy.com", Server.ConnectionType.SSL, 51002),
|
||||
new Server("guichet.centure.cc", Server.ConnectionType.SSL, 50002),
|
||||
new Server("hodl.artyomk13.me", Server.ConnectionType.SSL, 50002),
|
||||
new Server("hodlers.beer", Server.ConnectionType.SSL, 50002),
|
||||
new Server("kareoke.qoppa.org", Server.ConnectionType.SSL, 50002),
|
||||
new Server("kirsche.emzy.de", Server.ConnectionType.SSL, 50002),
|
||||
new Server("kittyserver.ddnsfree.com", Server.ConnectionType.SSL, 50002),
|
||||
new Server("lille.anties.org", Server.ConnectionType.SSL, 50002),
|
||||
new Server("marseille.anties.org", Server.ConnectionType.SSL, 50002),
|
||||
new Server("node1.btccuracao.com", Server.ConnectionType.SSL, 50002),
|
||||
new Server("osr1ex1.compumundohipermegared.one", Server.ConnectionType.SSL, 50002),
|
||||
new Server("smmalis37.ddns.net", Server.ConnectionType.SSL, 50002),
|
||||
new Server("ulrichard.ch", Server.ConnectionType.SSL, 50002),
|
||||
new Server("vmd104012.contaboserver.net", Server.ConnectionType.SSL, 50002),
|
||||
new Server("vmd104014.contaboserver.net", Server.ConnectionType.SSL, 50002),
|
||||
new Server("paris.anties.org", Server.ConnectionType.SSL, 50002),
|
||||
new Server("ragtor.duckdns.org", Server.ConnectionType.SSL, 50002),
|
||||
new Server("stavver.dyshek.org", Server.ConnectionType.SSL, 50002),
|
||||
new Server("vmd63185.contaboserver.net", Server.ConnectionType.SSL, 50002),
|
||||
new Server("vmd71287.contaboserver.net", Server.ConnectionType.SSL, 50002),
|
||||
new Server("vmd84592.contaboserver.net", Server.ConnectionType.SSL, 50002),
|
||||
new Server("xtrum.com", Server.ConnectionType.SSL, 50002)
|
||||
);
|
||||
}
|
||||
@@ -125,11 +111,7 @@ public class Bitcoin extends Bitcoiny {
|
||||
|
||||
@Override
|
||||
public long getP2shFee(Long timestamp) {
|
||||
// TODO: This will need to be replaced with something better in the near future!
|
||||
if (timestamp != null && timestamp < NEW_FEE_TIMESTAMP)
|
||||
return OLD_FEE_AMOUNT;
|
||||
|
||||
return NEW_FEE_AMOUNT;
|
||||
return this.getFeeCeiling();
|
||||
}
|
||||
},
|
||||
TEST3 {
|
||||
@@ -141,12 +123,17 @@ public class Bitcoin extends Bitcoiny {
|
||||
@Override
|
||||
public Collection<ElectrumX.Server> getServers() {
|
||||
return Arrays.asList(
|
||||
new Server("tn.not.fyi", Server.ConnectionType.SSL, 55002),
|
||||
new Server("electrumx-test.1209k.com", Server.ConnectionType.SSL, 50002),
|
||||
new Server("testnet.qtornado.com", Server.ConnectionType.SSL, 51002),
|
||||
new Server("testnet.aranguren.org", Server.ConnectionType.TCP, 51001),
|
||||
new Server("bitcoin.devmole.eu", Server.ConnectionType.TCP, 5000),
|
||||
new Server("bitcoin.stagemole.eu", Server.ConnectionType.TCP, 5000),
|
||||
new Server("blockstream.info", Server.ConnectionType.SSL, 993),
|
||||
new Server("electrum.blockstream.info", Server.ConnectionType.SSL, 60002),
|
||||
new Server("electrum1.cipig.net", Server.ConnectionType.TCP, 10068),
|
||||
new Server("electrum2.cipig.net", Server.ConnectionType.TCP, 10068),
|
||||
new Server("electrum3.cipig.net", Server.ConnectionType.TCP, 10068),
|
||||
new Server("testnet.aranguren.org", Server.ConnectionType.SSL, 51002),
|
||||
new Server("testnet.hsmiths.com", Server.ConnectionType.SSL, 53012)
|
||||
new Server("testnet.hsmiths.com", Server.ConnectionType.SSL, 53012),
|
||||
new Server("testnet.qtornado.com", Server.ConnectionType.SSL, 51002),
|
||||
new Server("v22019051929289916.bestsrv.de", Server.ConnectionType.SSL, 50002)
|
||||
);
|
||||
}
|
||||
|
||||
@@ -186,6 +173,16 @@ public class Bitcoin extends Bitcoiny {
|
||||
}
|
||||
};
|
||||
|
||||
private long feeCeiling = NEW_FEE_AMOUNT;
|
||||
|
||||
public long getFeeCeiling() {
|
||||
return feeCeiling;
|
||||
}
|
||||
|
||||
public void setFeeCeiling(long feeCeiling) {
|
||||
this.feeCeiling = feeCeiling;
|
||||
}
|
||||
|
||||
public abstract NetworkParameters getParams();
|
||||
public abstract Collection<ElectrumX.Server> getServers();
|
||||
public abstract String getGenesisHash();
|
||||
@@ -199,7 +196,7 @@ public class Bitcoin extends Bitcoiny {
|
||||
// Constructors and instance
|
||||
|
||||
private Bitcoin(BitcoinNet bitcoinNet, BitcoinyBlockchainProvider blockchain, Context bitcoinjContext, String currencyCode) {
|
||||
super(blockchain, bitcoinjContext, currencyCode);
|
||||
super(blockchain, bitcoinjContext, currencyCode, bitcoinjContext.getFeePerKb());
|
||||
this.bitcoinNet = bitcoinNet;
|
||||
|
||||
LOGGER.info(() -> String.format("Starting Bitcoin support using %s", this.bitcoinNet.name()));
|
||||
@@ -244,6 +241,16 @@ public class Bitcoin extends Bitcoiny {
|
||||
return this.bitcoinNet.getP2shFee(timestamp);
|
||||
}
|
||||
|
||||
@Override
|
||||
public long getFeeCeiling() {
|
||||
return this.bitcoinNet.getFeeCeiling();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void setFeeCeiling(long fee) {
|
||||
|
||||
this.bitcoinNet.setFeeCeiling( fee );
|
||||
}
|
||||
/**
|
||||
* Returns bitcoinj transaction sending <tt>amount</tt> to <tt>recipient</tt> using 20 sat/byte fee.
|
||||
*
|
||||
|
@@ -53,12 +53,15 @@ public abstract class Bitcoiny implements ForeignBlockchain {
|
||||
/** Byte offset into raw block headers to block timestamp. */
|
||||
private static final int TIMESTAMP_OFFSET = 4 + 32 + 32;
|
||||
|
||||
protected Coin feePerKb;
|
||||
|
||||
// Constructors and instance
|
||||
|
||||
protected Bitcoiny(BitcoinyBlockchainProvider blockchainProvider, Context bitcoinjContext, String currencyCode) {
|
||||
protected Bitcoiny(BitcoinyBlockchainProvider blockchainProvider, Context bitcoinjContext, String currencyCode, Coin feePerKb) {
|
||||
this.blockchainProvider = blockchainProvider;
|
||||
this.bitcoinjContext = bitcoinjContext;
|
||||
this.currencyCode = currencyCode;
|
||||
this.feePerKb = feePerKb;
|
||||
|
||||
this.params = this.bitcoinjContext.getParams();
|
||||
}
|
||||
@@ -167,7 +170,11 @@ public abstract class Bitcoiny implements ForeignBlockchain {
|
||||
|
||||
/** Returns fee per transaction KB. To be overridden for testnet/regtest. */
|
||||
public Coin getFeePerKb() {
|
||||
return this.bitcoinjContext.getFeePerKb();
|
||||
return this.feePerKb;
|
||||
}
|
||||
|
||||
public void setFeePerKb(Coin feePerKb) {
|
||||
this.feePerKb = feePerKb;
|
||||
}
|
||||
|
||||
/** Returns minimum order size in sats. To be overridden for coins that need to restrict order size. */
|
||||
@@ -750,13 +757,17 @@ public abstract class Bitcoiny implements ForeignBlockchain {
|
||||
Address address = Address.fromKey(this.params, keyChain.getKey(KeyChain.KeyPurpose.RECEIVE_FUNDS), ScriptType.P2PKH);
|
||||
|
||||
// if zero transactions, return address
|
||||
if( 0 == getAddressTransactions(ScriptBuilder.createOutputScript(address).getProgram(), true).size() )
|
||||
if(getAddressTransactions(ScriptBuilder.createOutputScript(address).getProgram(), true).isEmpty())
|
||||
return address.toString();
|
||||
|
||||
// else try the next receive funds address
|
||||
} while (true);
|
||||
}
|
||||
|
||||
public abstract long getFeeCeiling();
|
||||
|
||||
public abstract void setFeeCeiling(long fee);
|
||||
|
||||
// UTXOProvider support
|
||||
|
||||
static class WalletAwareUTXOProvider implements UTXOProvider {
|
||||
|
@@ -3,12 +3,14 @@ package org.qortal.crosschain;
|
||||
import cash.z.wallet.sdk.rpc.CompactFormats.CompactBlock;
|
||||
|
||||
import java.util.List;
|
||||
import java.util.Optional;
|
||||
import java.util.Set;
|
||||
|
||||
public abstract class BitcoinyBlockchainProvider {
|
||||
|
||||
public static final boolean INCLUDE_UNCONFIRMED = true;
|
||||
public static final boolean EXCLUDE_UNCONFIRMED = false;
|
||||
public static final String EMPTY = "";
|
||||
|
||||
/** Sets the blockchain using this provider instance */
|
||||
public abstract void setBlockchain(Bitcoiny blockchain);
|
||||
@@ -67,4 +69,62 @@ public abstract class BitcoinyBlockchainProvider {
|
||||
public abstract Set<ChainableServer> getUselessServers();
|
||||
|
||||
public abstract ChainableServer getCurrentServer();
|
||||
|
||||
/**
|
||||
* Add Server
|
||||
*
|
||||
* Add server to list of candidate servers.
|
||||
*
|
||||
* @param server the server
|
||||
*
|
||||
* @return true if added, otherwise false
|
||||
*/
|
||||
public abstract boolean addServer( ChainableServer server );
|
||||
|
||||
/**
|
||||
* Remove Server
|
||||
*
|
||||
* Remove server from list of candidate servers.
|
||||
*
|
||||
* @param server the server
|
||||
*
|
||||
* @return true if removed, otherwise false
|
||||
*/
|
||||
public abstract boolean removeServer( ChainableServer server );
|
||||
|
||||
/**
|
||||
* Set Current Server
|
||||
*
|
||||
* Set server to be used for this foreign blockchain.
|
||||
*
|
||||
* @param server the server
|
||||
* @param requestedBy who requested this setting
|
||||
*
|
||||
* @return the connection that was made
|
||||
*
|
||||
* @throws ForeignBlockchainException
|
||||
*/
|
||||
public abstract Optional<ChainableServerConnection> setCurrentServer(ChainableServer server, String requestedBy) throws ForeignBlockchainException;
|
||||
|
||||
/**
|
||||
* Get Server Connections
|
||||
*
|
||||
* Get the server connections made to this foreign blockchain,
|
||||
*
|
||||
* @return the server connections
|
||||
*/
|
||||
public abstract List<ChainableServerConnection> getServerConnections();
|
||||
|
||||
/**
|
||||
* Get Server
|
||||
*
|
||||
* Get a server for this foreign blockchain.
|
||||
*
|
||||
* @param hostName the host URL
|
||||
* @param type the type of connection (TCP, SSL)
|
||||
* @param port the port
|
||||
*
|
||||
* @return the server
|
||||
*/
|
||||
public abstract ChainableServer getServer(String hostName, ChainableServer.ConnectionType type, int port);
|
||||
}
|
||||
|
@@ -0,0 +1,71 @@
|
||||
package org.qortal.crosschain;
|
||||
|
||||
import java.util.Objects;
|
||||
|
||||
public class ChainableServerConnection {
|
||||
|
||||
private ChainableServer server;
|
||||
private String requestedBy;
|
||||
private boolean open;
|
||||
private boolean success;
|
||||
private long currentTimeMillis;
|
||||
private String notes;
|
||||
|
||||
public ChainableServerConnection(ChainableServer server, String requestedBy, boolean open, boolean success, long currentTimeMillis, String notes) {
|
||||
this.server = server;
|
||||
this.requestedBy = requestedBy;
|
||||
this.open = open;
|
||||
this.success = success;
|
||||
this.currentTimeMillis = currentTimeMillis;
|
||||
this.notes = notes;
|
||||
}
|
||||
|
||||
public ChainableServer getServer() {
|
||||
return server;
|
||||
}
|
||||
|
||||
public String getRequestedBy() {
|
||||
return requestedBy;
|
||||
}
|
||||
|
||||
public boolean isOpen() {
|
||||
return open;
|
||||
}
|
||||
|
||||
public boolean isSuccess() {
|
||||
return success;
|
||||
}
|
||||
|
||||
public long getCurrentTimeMillis() {
|
||||
return currentTimeMillis;
|
||||
}
|
||||
|
||||
public String getNotes() {
|
||||
return notes;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean equals(Object o) {
|
||||
if (this == o) return true;
|
||||
if (o == null || getClass() != o.getClass()) return false;
|
||||
ChainableServerConnection that = (ChainableServerConnection) o;
|
||||
return currentTimeMillis == that.currentTimeMillis && Objects.equals(server, that.server);
|
||||
}
|
||||
|
||||
@Override
|
||||
public int hashCode() {
|
||||
return Objects.hash(server, currentTimeMillis);
|
||||
}
|
||||
|
||||
@Override
|
||||
public String toString() {
|
||||
return "ChainableServerConnection{" +
|
||||
"server=" + server +
|
||||
", requestedBy='" + requestedBy + '\'' +
|
||||
", open=" + open +
|
||||
", success=" + success +
|
||||
", currentTimeMillis=" + currentTimeMillis +
|
||||
", notes='" + notes + '\'' +
|
||||
'}';
|
||||
}
|
||||
}
|
@@ -0,0 +1,45 @@
|
||||
package org.qortal.crosschain;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.Comparator;
|
||||
import java.util.List;
|
||||
|
||||
public class ChainableServerConnectionRecorder {
|
||||
|
||||
private List<ChainableServerConnection> connections;
|
||||
private int limit;
|
||||
|
||||
public ChainableServerConnectionRecorder(int limit) {
|
||||
this.connections = new ArrayList<>(limit);
|
||||
this.limit = limit;
|
||||
}
|
||||
|
||||
public ChainableServerConnection recordConnection(
|
||||
ChainableServer server, String requestedBy, boolean open, boolean success, String notes) {
|
||||
|
||||
ChainableServerConnection connection
|
||||
= new ChainableServerConnection(server, requestedBy, open, success, System.currentTimeMillis(), notes);
|
||||
|
||||
connections.add(connection);
|
||||
|
||||
if( connections.size() > limit) {
|
||||
ChainableServerConnection firstConnection
|
||||
= connections.stream().sorted(Comparator.comparing(ChainableServerConnection::getCurrentTimeMillis))
|
||||
.findFirst().get();
|
||||
connections.remove(firstConnection);
|
||||
}
|
||||
return connection;
|
||||
}
|
||||
|
||||
public int getLimit() {
|
||||
return limit;
|
||||
}
|
||||
|
||||
public void setLimit(int limit) {
|
||||
this.limit = limit;
|
||||
}
|
||||
|
||||
public List<ChainableServerConnection> getConnections() {
|
||||
return this.connections;
|
||||
}
|
||||
}
|
@@ -59,8 +59,7 @@ public class Digibyte extends Bitcoiny {
|
||||
|
||||
@Override
|
||||
public long getP2shFee(Long timestamp) {
|
||||
// TODO: This will need to be replaced with something better in the near future!
|
||||
return MAINNET_FEE;
|
||||
return this.getFeeCeiling();
|
||||
}
|
||||
},
|
||||
TEST3 {
|
||||
@@ -110,6 +109,16 @@ public class Digibyte extends Bitcoiny {
|
||||
}
|
||||
};
|
||||
|
||||
private long feeCeiling = MAINNET_FEE;
|
||||
|
||||
public long getFeeCeiling() {
|
||||
return feeCeiling;
|
||||
}
|
||||
|
||||
public void setFeeCeiling(long feeCeiling) {
|
||||
this.feeCeiling = feeCeiling;
|
||||
}
|
||||
|
||||
public abstract NetworkParameters getParams();
|
||||
public abstract Collection<Server> getServers();
|
||||
public abstract String getGenesisHash();
|
||||
@@ -123,7 +132,7 @@ public class Digibyte extends Bitcoiny {
|
||||
// Constructors and instance
|
||||
|
||||
private Digibyte(DigibyteNet digibyteNet, BitcoinyBlockchainProvider blockchain, Context bitcoinjContext, String currencyCode) {
|
||||
super(blockchain, bitcoinjContext, currencyCode);
|
||||
super(blockchain, bitcoinjContext, currencyCode, DEFAULT_FEE_PER_KB);
|
||||
this.digibyteNet = digibyteNet;
|
||||
|
||||
LOGGER.info(() -> String.format("Starting Digibyte support using %s", this.digibyteNet.name()));
|
||||
@@ -152,11 +161,6 @@ public class Digibyte extends Bitcoiny {
|
||||
|
||||
// Actual useful methods for use by other classes
|
||||
|
||||
@Override
|
||||
public Coin getFeePerKb() {
|
||||
return DEFAULT_FEE_PER_KB;
|
||||
}
|
||||
|
||||
@Override
|
||||
public long getMinimumOrderAmount() {
|
||||
return MINIMUM_ORDER_AMOUNT;
|
||||
@@ -173,4 +177,14 @@ public class Digibyte extends Bitcoiny {
|
||||
return this.digibyteNet.getP2shFee(timestamp);
|
||||
}
|
||||
|
||||
@Override
|
||||
public long getFeeCeiling() {
|
||||
return this.digibyteNet.getFeeCeiling();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void setFeeCeiling(long fee) {
|
||||
|
||||
this.digibyteNet.setFeeCeiling( fee );
|
||||
}
|
||||
}
|
||||
|
@@ -45,6 +45,7 @@ public class Dogecoin extends Bitcoiny {
|
||||
return Arrays.asList(
|
||||
// Servers chosen on NO BASIS WHATSOEVER from various sources!
|
||||
// Status verified at https://1209k.com/bitcoin-eye/ele.php?chain=doge
|
||||
new Server("dogecoin.stackwallet.com", Server.ConnectionType.SSL, 50022),
|
||||
new Server("electrum.qortal.link", Server.ConnectionType.SSL, 54002),
|
||||
new Server("electrum1.cipig.net", Server.ConnectionType.SSL, 20060),
|
||||
new Server("electrum2.cipig.net", Server.ConnectionType.SSL, 20060),
|
||||
@@ -59,8 +60,7 @@ public class Dogecoin extends Bitcoiny {
|
||||
|
||||
@Override
|
||||
public long getP2shFee(Long timestamp) {
|
||||
// TODO: This will need to be replaced with something better in the near future!
|
||||
return MAINNET_FEE;
|
||||
return this.getFeeCeiling();
|
||||
}
|
||||
},
|
||||
TEST3 {
|
||||
@@ -110,6 +110,16 @@ public class Dogecoin extends Bitcoiny {
|
||||
}
|
||||
};
|
||||
|
||||
private long feeCeiling = MAINNET_FEE;
|
||||
|
||||
public long getFeeCeiling() {
|
||||
return feeCeiling;
|
||||
}
|
||||
|
||||
public void setFeeCeiling(long feeCeiling) {
|
||||
this.feeCeiling = feeCeiling;
|
||||
}
|
||||
|
||||
public abstract NetworkParameters getParams();
|
||||
public abstract Collection<Server> getServers();
|
||||
public abstract String getGenesisHash();
|
||||
@@ -123,7 +133,7 @@ public class Dogecoin extends Bitcoiny {
|
||||
// Constructors and instance
|
||||
|
||||
private Dogecoin(DogecoinNet dogecoinNet, BitcoinyBlockchainProvider blockchain, Context bitcoinjContext, String currencyCode) {
|
||||
super(blockchain, bitcoinjContext, currencyCode);
|
||||
super(blockchain, bitcoinjContext, currencyCode, DEFAULT_FEE_PER_KB);
|
||||
this.dogecoinNet = dogecoinNet;
|
||||
|
||||
LOGGER.info(() -> String.format("Starting Dogecoin support using %s", this.dogecoinNet.name()));
|
||||
@@ -152,11 +162,6 @@ public class Dogecoin extends Bitcoiny {
|
||||
|
||||
// Actual useful methods for use by other classes
|
||||
|
||||
@Override
|
||||
public Coin getFeePerKb() {
|
||||
return DEFAULT_FEE_PER_KB;
|
||||
}
|
||||
|
||||
@Override
|
||||
public long getMinimumOrderAmount() {
|
||||
return MINIMUM_ORDER_AMOUNT;
|
||||
@@ -173,4 +178,14 @@ public class Dogecoin extends Bitcoiny {
|
||||
return this.dogecoinNet.getP2shFee(timestamp);
|
||||
}
|
||||
|
||||
@Override
|
||||
public long getFeeCeiling() {
|
||||
return this.dogecoinNet.getFeeCeiling();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void setFeeCeiling(long fee) {
|
||||
|
||||
this.dogecoinNet.setFeeCeiling( fee );
|
||||
}
|
||||
}
|
||||
|
@@ -8,6 +8,7 @@ import org.apache.logging.log4j.Logger;
|
||||
import org.json.simple.JSONArray;
|
||||
import org.json.simple.JSONObject;
|
||||
import org.json.simple.JSONValue;
|
||||
import org.qortal.api.resource.CrossChainUtils;
|
||||
import org.qortal.crypto.Crypto;
|
||||
import org.qortal.crypto.TrustlessSSLSocketFactory;
|
||||
import org.qortal.utils.BitTwiddling;
|
||||
@@ -26,6 +27,7 @@ import java.util.regex.Pattern;
|
||||
/** ElectrumX network support for querying Bitcoiny-related info like block headers, transaction outputs, etc. */
|
||||
public class ElectrumX extends BitcoinyBlockchainProvider {
|
||||
|
||||
public static final String NULL_RESPONSE_FROM_ELECTRUM_X_SERVER = "Null response from ElectrumX server";
|
||||
private static final Logger LOGGER = LogManager.getLogger(ElectrumX.class);
|
||||
private static final Random RANDOM = new Random();
|
||||
|
||||
@@ -44,6 +46,10 @@ public class ElectrumX extends BitcoinyBlockchainProvider {
|
||||
|
||||
private static final int RESPONSE_TIME_READINGS = 5;
|
||||
private static final long MAX_AVG_RESPONSE_TIME = 2000L; // ms
|
||||
public static final String MINIMUM_VERSION_ERROR = "MINIMUM VERSION ERROR";
|
||||
public static final String EXPECTED_GENESIS_ERROR = "EXPECTED GENESIS ERROR";
|
||||
|
||||
private ChainableServerConnectionRecorder recorder = new ChainableServerConnectionRecorder(100);
|
||||
|
||||
public static class Server implements ChainableServer {
|
||||
String hostname;
|
||||
@@ -136,7 +142,7 @@ public class ElectrumX extends BitcoinyBlockchainProvider {
|
||||
private int nextId = 1;
|
||||
|
||||
private static final int TX_CACHE_SIZE = 1000;
|
||||
@SuppressWarnings("serial")
|
||||
|
||||
private final Map<String, BitcoinyTransaction> transactionCache = Collections.synchronizedMap(new LinkedHashMap<>(TX_CACHE_SIZE + 1, 0.75F, true) {
|
||||
// This method is called just after a new entry has been added
|
||||
@Override
|
||||
@@ -216,10 +222,10 @@ public class ElectrumX extends BitcoinyBlockchainProvider {
|
||||
if (!(countObj instanceof Long) || !(hexObj instanceof String))
|
||||
throw new ForeignBlockchainException.NetworkException("Missing/invalid 'count' or 'hex' entries in JSON from ElectrumX blockchain.block.headers RPC");
|
||||
|
||||
Long returnedCount = (Long) countObj;
|
||||
long returnedCount = (Long) countObj;
|
||||
String hex = (String) hexObj;
|
||||
|
||||
List<byte[]> rawBlockHeaders = new ArrayList<>(returnedCount.intValue());
|
||||
List<byte[]> rawBlockHeaders = new ArrayList<>((int) returnedCount);
|
||||
|
||||
byte[] raw = HashCode.fromString(hex).asBytes();
|
||||
|
||||
@@ -421,7 +427,7 @@ public class ElectrumX extends BitcoinyBlockchainProvider {
|
||||
Server uselessServer = (Server) e.getServer();
|
||||
LOGGER.trace(() -> String.format("Server %s doesn't support verbose transactions - barring use of that server", uselessServer));
|
||||
this.uselessServers.add(uselessServer);
|
||||
this.closeServer(uselessServer);
|
||||
this.closeServer(uselessServer, this.getClass().getSimpleName(), CrossChainUtils.getNotes(e));
|
||||
continue;
|
||||
}
|
||||
|
||||
@@ -495,12 +501,13 @@ public class ElectrumX extends BitcoinyBlockchainProvider {
|
||||
// Update: it turns out that they were just using a different key - "address" instead of "addresses"
|
||||
// The code below can remain in place, just in case a peer returns a missing address in the future
|
||||
if (addresses == null || addresses.isEmpty()) {
|
||||
final String message = String.format("No output addresses returned for transaction %s", txHash);
|
||||
if (this.currentServer != null) {
|
||||
this.uselessServers.add(this.currentServer);
|
||||
this.closeServer(this.currentServer);
|
||||
this.closeServer(this.currentServer, this.getClass().getSimpleName(), message);
|
||||
}
|
||||
LOGGER.info("No output addresses returned for transaction {}", txHash);
|
||||
throw new ForeignBlockchainException(String.format("No output addresses returned for transaction %s", txHash));
|
||||
throw new ForeignBlockchainException(message);
|
||||
}
|
||||
|
||||
outputs.add(new BitcoinyTransaction.Output(scriptPubKey, value, addresses));
|
||||
@@ -585,7 +592,7 @@ public class ElectrumX extends BitcoinyBlockchainProvider {
|
||||
|
||||
Object peers = this.connectedRpc("server.peers.subscribe");
|
||||
|
||||
for (Object rawPeer : (JSONArray) peers) {
|
||||
for (Object rawPeer : (JSONArray) Objects.requireNonNull(peers)) {
|
||||
JSONArray peer = (JSONArray) rawPeer;
|
||||
if (peer.size() < 3)
|
||||
// We're expecting at least 3 fields for each peer entry: IP, hostname, features
|
||||
@@ -654,8 +661,9 @@ public class ElectrumX extends BitcoinyBlockchainProvider {
|
||||
if (!this.remainingServers.isEmpty()) {
|
||||
long averageResponseTime = this.currentServer.averageResponseTime();
|
||||
if (averageResponseTime > MAX_AVG_RESPONSE_TIME) {
|
||||
LOGGER.info("Slow average response time {}ms from {} - trying another server...", averageResponseTime, this.currentServer.getHostName());
|
||||
this.closeServer();
|
||||
String message = String.format("Slow average response time %dms from %s - trying another server...", averageResponseTime, this.currentServer.getHostName());
|
||||
LOGGER.info(message);
|
||||
this.closeServer(this.getClass().getSimpleName(), message);
|
||||
break;
|
||||
}
|
||||
}
|
||||
@@ -663,8 +671,9 @@ public class ElectrumX extends BitcoinyBlockchainProvider {
|
||||
if (response != null)
|
||||
return response;
|
||||
|
||||
LOGGER.info(NULL_RESPONSE_FROM_ELECTRUM_X_SERVER);
|
||||
// Didn't work, try another server...
|
||||
this.closeServer();
|
||||
this.closeServer(this.getClass().getSimpleName(), NULL_RESPONSE_FROM_ELECTRUM_X_SERVER);
|
||||
}
|
||||
|
||||
// Failed to perform RPC - maybe lack of servers?
|
||||
@@ -680,56 +689,63 @@ public class ElectrumX extends BitcoinyBlockchainProvider {
|
||||
|
||||
while (!this.remainingServers.isEmpty()) {
|
||||
ChainableServer server = this.remainingServers.remove(RANDOM.nextInt(this.remainingServers.size()));
|
||||
LOGGER.trace(() -> String.format("Connecting to %s", server));
|
||||
|
||||
try {
|
||||
SocketAddress endpoint = new InetSocketAddress(server.getHostName(), server.getPort());
|
||||
int timeout = 5000; // ms
|
||||
|
||||
this.socket = new Socket();
|
||||
this.socket.connect(endpoint, timeout);
|
||||
this.socket.setTcpNoDelay(true);
|
||||
|
||||
if (server.getConnectionType() == Server.ConnectionType.SSL) {
|
||||
SSLSocketFactory factory = TrustlessSSLSocketFactory.getSocketFactory();
|
||||
this.socket = factory.createSocket(this.socket, server.getHostName(), server.getPort(), true);
|
||||
}
|
||||
|
||||
this.scanner = new Scanner(this.socket.getInputStream());
|
||||
this.scanner.useDelimiter("\n");
|
||||
|
||||
// All connections need to start with a version negotiation
|
||||
this.connectedRpc("server.version");
|
||||
|
||||
// Check connection is suitable by asking for server features, including genesis block hash
|
||||
JSONObject featuresJson = (JSONObject) this.connectedRpc("server.features");
|
||||
|
||||
if (featuresJson == null || Double.valueOf((String) featuresJson.get("protocol_min")) < MIN_PROTOCOL_VERSION)
|
||||
continue;
|
||||
|
||||
if (this.expectedGenesisHash != null && !((String) featuresJson.get("genesis_hash")).equals(this.expectedGenesisHash))
|
||||
continue;
|
||||
|
||||
// Ask for more servers
|
||||
Set<Server> moreServers = serverPeersSubscribe();
|
||||
// Discard duplicate servers we already know
|
||||
moreServers.removeAll(this.servers);
|
||||
// Add to both lists
|
||||
this.remainingServers.addAll(moreServers);
|
||||
this.servers.addAll(moreServers);
|
||||
|
||||
LOGGER.debug(() -> String.format("Connected to %s", server));
|
||||
this.currentServer = server;
|
||||
return true;
|
||||
} catch (IOException | ForeignBlockchainException | ClassCastException | NullPointerException e) {
|
||||
// Didn't work, try another server...
|
||||
closeServer();
|
||||
}
|
||||
Optional<ChainableServerConnection> chainableServerConnection = makeConnection(server, this.getClass().getSimpleName());
|
||||
if(chainableServerConnection.isPresent() && chainableServerConnection.get().isSuccess() ) return true;
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
private Optional<ChainableServerConnection> makeConnection(ChainableServer server, String requestedBy) {
|
||||
LOGGER.info(() -> String.format("Connecting to %s", server));
|
||||
|
||||
try {
|
||||
SocketAddress endpoint = new InetSocketAddress(server.getHostName(), server.getPort());
|
||||
int timeout = 5000; // ms
|
||||
|
||||
this.socket = new Socket();
|
||||
this.socket.connect(endpoint, timeout);
|
||||
this.socket.setTcpNoDelay(true);
|
||||
|
||||
if (server.getConnectionType() == Server.ConnectionType.SSL) {
|
||||
SSLSocketFactory factory = TrustlessSSLSocketFactory.getSocketFactory();
|
||||
this.socket = factory.createSocket(this.socket, server.getHostName(), server.getPort(), true);
|
||||
}
|
||||
|
||||
this.scanner = new Scanner(this.socket.getInputStream());
|
||||
this.scanner.useDelimiter("\n");
|
||||
|
||||
// All connections need to start with a version negotiation
|
||||
this.connectedRpc("server.version");
|
||||
|
||||
// Check connection is suitable by asking for server features, including genesis block hash
|
||||
JSONObject featuresJson = (JSONObject) this.connectedRpc("server.features");
|
||||
|
||||
if (featuresJson == null || Double.parseDouble((String) featuresJson.get("protocol_min")) < MIN_PROTOCOL_VERSION)
|
||||
return Optional.of( recorder.recordConnection(server, requestedBy, true, false, MINIMUM_VERSION_ERROR) );
|
||||
|
||||
if (this.expectedGenesisHash != null && !((String) featuresJson.get("genesis_hash")).equals(this.expectedGenesisHash))
|
||||
return Optional.of( recorder.recordConnection(server, requestedBy, true, false, EXPECTED_GENESIS_ERROR) );
|
||||
|
||||
// Ask for more servers
|
||||
Set<Server> moreServers = serverPeersSubscribe();
|
||||
|
||||
// Discard duplicate servers we already know
|
||||
moreServers.removeAll(this.servers);
|
||||
|
||||
// Add all servers to both lists
|
||||
this.remainingServers.addAll(moreServers);
|
||||
this.servers.addAll(moreServers);
|
||||
|
||||
LOGGER.info(() -> String.format("Connected to %s", server));
|
||||
this.currentServer = server;
|
||||
return Optional.of( this.recorder.recordConnection( server, requestedBy, true, true, EMPTY) );
|
||||
} catch (IOException | ForeignBlockchainException | ClassCastException | NullPointerException e) {
|
||||
// Didn't work, try another server...
|
||||
return Optional.of( this.recorder.recordConnection( server, requestedBy, true, false, CrossChainUtils.getNotes(e)));
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Perform RPC using currently connected server.
|
||||
* <p>
|
||||
@@ -846,12 +862,19 @@ public class ElectrumX extends BitcoinyBlockchainProvider {
|
||||
|
||||
/**
|
||||
* Closes connection to <tt>server</tt> if it is currently connected server.
|
||||
*
|
||||
* @param server
|
||||
* @param notes
|
||||
*/
|
||||
private void closeServer(ChainableServer server) {
|
||||
private Optional<ChainableServerConnection> closeServer(ChainableServer server, String requestedBy, String notes) {
|
||||
|
||||
ChainableServerConnection chainableServerConnection;
|
||||
|
||||
synchronized (this.serverLock) {
|
||||
if (this.currentServer == null || !this.currentServer.equals(server))
|
||||
return;
|
||||
return Optional.empty();
|
||||
|
||||
chainableServerConnection = this.recorder.recordConnection(server, requestedBy, false, true, notes);
|
||||
|
||||
if (this.socket != null && !this.socket.isClosed())
|
||||
try {
|
||||
@@ -864,12 +887,14 @@ public class ElectrumX extends BitcoinyBlockchainProvider {
|
||||
this.scanner = null;
|
||||
this.currentServer = null;
|
||||
}
|
||||
|
||||
return Optional.of( chainableServerConnection );
|
||||
}
|
||||
|
||||
/** Closes connection to currently connected server (if any). */
|
||||
private void closeServer() {
|
||||
private Optional<ChainableServerConnection> closeServer(String requestedBy, String notes) {
|
||||
synchronized (this.serverLock) {
|
||||
this.closeServer(this.currentServer);
|
||||
return this.closeServer(this.currentServer, requestedBy, notes);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -893,4 +918,32 @@ public class ElectrumX extends BitcoinyBlockchainProvider {
|
||||
public ChainableServer getCurrentServer() {
|
||||
return currentServer;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean addServer(ChainableServer server) {
|
||||
return this.servers.add(server);
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean removeServer(ChainableServer server) {
|
||||
boolean removedServer = this.servers.remove(server);
|
||||
boolean removedRemaining = this.remainingServers.remove(server);
|
||||
|
||||
return removedServer || removedRemaining;
|
||||
}
|
||||
|
||||
@Override
|
||||
public Optional<ChainableServerConnection> setCurrentServer(ChainableServer server, String requestedBy) {
|
||||
return this.makeConnection(server, requestedBy);
|
||||
}
|
||||
|
||||
@Override
|
||||
public List<ChainableServerConnection> getServerConnections() {
|
||||
return this.recorder.getConnections();
|
||||
}
|
||||
|
||||
@Override
|
||||
public ChainableServer getServer(String hostName, ChainableServer.ConnectionType type, int port) {
|
||||
return new ElectrumX.Server(hostName, type, port);
|
||||
}
|
||||
}
|
||||
|
@@ -45,9 +45,9 @@ public class Litecoin extends Bitcoiny {
|
||||
return Arrays.asList(
|
||||
// Servers chosen on NO BASIS WHATSOEVER from various sources!
|
||||
// Status verified at https://1209k.com/bitcoin-eye/ele.php?chain=ltc
|
||||
new Server("electrum.qortal.link", Server.ConnectionType.SSL, 50002),
|
||||
new Server("backup.electrum-ltc.org", Server.ConnectionType.SSL, 443),
|
||||
new Server("electrum.ltc.xurious.com", Server.ConnectionType.SSL, 50002),
|
||||
new Server("electrum.qortal.link", Server.ConnectionType.SSL, 50002),
|
||||
new Server("electrum-ltc.petrkr.net", Server.ConnectionType.SSL, 60002),
|
||||
new Server("electrum1.cipig.net", Server.ConnectionType.SSL, 20063),
|
||||
new Server("electrum2.cipig.net", Server.ConnectionType.SSL, 20063),
|
||||
@@ -63,8 +63,7 @@ public class Litecoin extends Bitcoiny {
|
||||
|
||||
@Override
|
||||
public long getP2shFee(Long timestamp) {
|
||||
// TODO: This will need to be replaced with something better in the near future!
|
||||
return MAINNET_FEE;
|
||||
return this.getFeeCeiling();
|
||||
}
|
||||
},
|
||||
TEST3 {
|
||||
@@ -76,9 +75,7 @@ public class Litecoin extends Bitcoiny {
|
||||
@Override
|
||||
public Collection<ElectrumX.Server> getServers() {
|
||||
return Arrays.asList(
|
||||
new Server("electrum-ltc.bysh.me", Server.ConnectionType.TCP, 51001),
|
||||
new Server("electrum-ltc.bysh.me", Server.ConnectionType.SSL, 51002),
|
||||
new Server("electrum.ltc.xurious.com", Server.ConnectionType.TCP, 51001),
|
||||
new Server("electrum.ltc.xurious.com", Server.ConnectionType.SSL, 51002)
|
||||
);
|
||||
}
|
||||
@@ -119,6 +116,16 @@ public class Litecoin extends Bitcoiny {
|
||||
}
|
||||
};
|
||||
|
||||
private long feeCeiling = MAINNET_FEE;
|
||||
|
||||
public long getFeeCeiling() {
|
||||
return feeCeiling;
|
||||
}
|
||||
|
||||
public void setFeeCeiling(long feeCeiling) {
|
||||
this.feeCeiling = feeCeiling;
|
||||
}
|
||||
|
||||
public abstract NetworkParameters getParams();
|
||||
public abstract Collection<ElectrumX.Server> getServers();
|
||||
public abstract String getGenesisHash();
|
||||
@@ -132,7 +139,7 @@ public class Litecoin extends Bitcoiny {
|
||||
// Constructors and instance
|
||||
|
||||
private Litecoin(LitecoinNet litecoinNet, BitcoinyBlockchainProvider blockchain, Context bitcoinjContext, String currencyCode) {
|
||||
super(blockchain, bitcoinjContext, currencyCode);
|
||||
super(blockchain, bitcoinjContext, currencyCode, DEFAULT_FEE_PER_KB);
|
||||
this.litecoinNet = litecoinNet;
|
||||
|
||||
LOGGER.info(() -> String.format("Starting Litecoin support using %s", this.litecoinNet.name()));
|
||||
@@ -161,12 +168,6 @@ public class Litecoin extends Bitcoiny {
|
||||
|
||||
// Actual useful methods for use by other classes
|
||||
|
||||
/** Default Litecoin fee is lower than Bitcoin: only 10sats/byte. */
|
||||
@Override
|
||||
public Coin getFeePerKb() {
|
||||
return DEFAULT_FEE_PER_KB;
|
||||
}
|
||||
|
||||
@Override
|
||||
public long getMinimumOrderAmount() {
|
||||
return MINIMUM_ORDER_AMOUNT;
|
||||
@@ -183,4 +184,14 @@ public class Litecoin extends Bitcoiny {
|
||||
return this.litecoinNet.getP2shFee(timestamp);
|
||||
}
|
||||
|
||||
@Override
|
||||
public long getFeeCeiling() {
|
||||
return this.litecoinNet.getFeeCeiling();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void setFeeCeiling(long fee) {
|
||||
|
||||
this.litecoinNet.setFeeCeiling( fee );
|
||||
}
|
||||
}
|
||||
|
@@ -51,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)
|
||||
);
|
||||
}
|
||||
|
||||
@@ -62,8 +67,7 @@ public class PirateChain extends Bitcoiny {
|
||||
|
||||
@Override
|
||||
public long getP2shFee(Long timestamp) {
|
||||
// TODO: This will need to be replaced with something better in the near future!
|
||||
return MAINNET_FEE;
|
||||
return this.getFeeCeiling();
|
||||
}
|
||||
},
|
||||
TEST3 {
|
||||
@@ -113,6 +117,16 @@ public class PirateChain extends Bitcoiny {
|
||||
}
|
||||
};
|
||||
|
||||
private long feeCeiling = MAINNET_FEE;
|
||||
|
||||
public long getFeeCeiling() {
|
||||
return feeCeiling;
|
||||
}
|
||||
|
||||
public void setFeeCeiling(long feeCeiling) {
|
||||
this.feeCeiling = feeCeiling;
|
||||
}
|
||||
|
||||
public abstract NetworkParameters getParams();
|
||||
public abstract Collection<Server> getServers();
|
||||
public abstract String getGenesisHash();
|
||||
@@ -126,7 +140,7 @@ public class PirateChain extends Bitcoiny {
|
||||
// Constructors and instance
|
||||
|
||||
private PirateChain(PirateChainNet pirateChainNet, BitcoinyBlockchainProvider blockchain, Context bitcoinjContext, String currencyCode) {
|
||||
super(blockchain, bitcoinjContext, currencyCode);
|
||||
super(blockchain, bitcoinjContext, currencyCode, DEFAULT_FEE_PER_KB);
|
||||
this.pirateChainNet = pirateChainNet;
|
||||
|
||||
LOGGER.info(() -> String.format("Starting Pirate Chain support using %s", this.pirateChainNet.name()));
|
||||
@@ -155,12 +169,6 @@ public class PirateChain extends Bitcoiny {
|
||||
|
||||
// Actual useful methods for use by other classes
|
||||
|
||||
/** Default Litecoin fee is lower than Bitcoin: only 10sats/byte. */
|
||||
@Override
|
||||
public Coin getFeePerKb() {
|
||||
return DEFAULT_FEE_PER_KB;
|
||||
}
|
||||
|
||||
@Override
|
||||
public long getMinimumOrderAmount() {
|
||||
return MINIMUM_ORDER_AMOUNT;
|
||||
@@ -177,6 +185,16 @@ public class PirateChain extends Bitcoiny {
|
||||
return this.pirateChainNet.getP2shFee(timestamp);
|
||||
}
|
||||
|
||||
@Override
|
||||
public long getFeeCeiling() {
|
||||
return this.pirateChainNet.getFeeCeiling();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void setFeeCeiling(long fee) {
|
||||
|
||||
this.pirateChainNet.setFeeCeiling( fee );
|
||||
}
|
||||
/**
|
||||
* Returns confirmed balance, based on passed payment script.
|
||||
* <p>
|
||||
|
@@ -14,6 +14,7 @@ import org.json.simple.JSONArray;
|
||||
import org.json.simple.JSONObject;
|
||||
import org.json.simple.parser.JSONParser;
|
||||
import org.json.simple.parser.ParseException;
|
||||
import org.qortal.api.resource.CrossChainUtils;
|
||||
import org.qortal.settings.Settings;
|
||||
import org.qortal.transform.TransformationException;
|
||||
|
||||
@@ -127,6 +128,8 @@ public class PirateLightClient extends BitcoinyBlockchainProvider {
|
||||
}
|
||||
});
|
||||
|
||||
private ChainableServerConnectionRecorder recorder = new ChainableServerConnectionRecorder(100);
|
||||
|
||||
// Constructors
|
||||
|
||||
public PirateLightClient(String netId, String genesisHash, Collection<Server> initialServerList, Map<Server.ConnectionType, Integer> defaultPorts) {
|
||||
@@ -443,12 +446,13 @@ public class PirateLightClient extends BitcoinyBlockchainProvider {
|
||||
// Update: it turns out that they were just using a different key - "address" instead of "addresses"
|
||||
// The code below can remain in place, just in case a peer returns a missing address in the future
|
||||
if (addresses == null || addresses.isEmpty()) {
|
||||
final String message = String.format("No output addresses returned for transaction %s", txHash);
|
||||
if (this.currentServer != null) {
|
||||
this.uselessServers.add(this.currentServer);
|
||||
this.closeServer(this.currentServer);
|
||||
this.closeServer(this.currentServer, message, this.getClass().getSimpleName());
|
||||
}
|
||||
LOGGER.info("No output addresses returned for transaction {}", txHash);
|
||||
throw new ForeignBlockchainException(String.format("No output addresses returned for transaction %s", txHash));
|
||||
LOGGER.info(message);
|
||||
throw new ForeignBlockchainException(message);
|
||||
}
|
||||
|
||||
outputs.add(new BitcoinyTransaction.Output(scriptPubKey, value, addresses));
|
||||
@@ -557,6 +561,42 @@ public class PirateLightClient extends BitcoinyBlockchainProvider {
|
||||
@Override
|
||||
public ChainableServer getCurrentServer() { return this.currentServer; }
|
||||
|
||||
@Override
|
||||
public boolean addServer(ChainableServer server) {
|
||||
return this.servers.add(server);
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean removeServer(ChainableServer server) {
|
||||
boolean removedServer = this.servers.remove(server);
|
||||
boolean removedRemaining = this.remainingServers.remove(server);
|
||||
|
||||
return removedServer || removedRemaining;
|
||||
}
|
||||
|
||||
@Override
|
||||
public Optional<ChainableServerConnection> setCurrentServer(ChainableServer server, String requestedBy) throws ForeignBlockchainException {
|
||||
|
||||
closeServer( requestedBy, "Connecting to different server by request." );
|
||||
Optional<ChainableServerConnection> connection = makeConnection(server, requestedBy);
|
||||
|
||||
if( !connection.isPresent() || !connection.get().isSuccess() ) {
|
||||
haveConnection();
|
||||
}
|
||||
|
||||
return connection;
|
||||
}
|
||||
|
||||
@Override
|
||||
public List<ChainableServerConnection> getServerConnections() {
|
||||
return this.recorder.getConnections();
|
||||
}
|
||||
|
||||
@Override
|
||||
public ChainableServer getServer(String hostName, ChainableServer.ConnectionType type, int port) {
|
||||
return new PirateLightClient.Server(hostName, type, port);
|
||||
}
|
||||
|
||||
// Class-private utility methods
|
||||
|
||||
|
||||
@@ -576,8 +616,9 @@ public class PirateLightClient extends BitcoinyBlockchainProvider {
|
||||
if (!this.remainingServers.isEmpty()) {
|
||||
long averageResponseTime = this.currentServer.averageResponseTime();
|
||||
if (averageResponseTime > MAX_AVG_RESPONSE_TIME) {
|
||||
LOGGER.info("Slow average response time {}ms from {} - trying another server...", averageResponseTime, this.currentServer.getHostName());
|
||||
this.closeServer();
|
||||
String message = String.format("Slow average response time %dms from %s - trying another server...", averageResponseTime, this.currentServer.getHostName());
|
||||
LOGGER.info(message);
|
||||
this.closeServer(this.getClass().getSimpleName(), message);
|
||||
continue;
|
||||
}
|
||||
}
|
||||
@@ -601,18 +642,27 @@ public class PirateLightClient extends BitcoinyBlockchainProvider {
|
||||
|
||||
while (!this.remainingServers.isEmpty()) {
|
||||
ChainableServer server = this.remainingServers.remove(RANDOM.nextInt(this.remainingServers.size()));
|
||||
LOGGER.trace(() -> String.format("Connecting to %s", server));
|
||||
|
||||
try {
|
||||
this.channel = ManagedChannelBuilder.forAddress(server.getHostName(), server.getPort()).build();
|
||||
Optional<ChainableServerConnection> chainableServerConnection = makeConnection(server, this.getClass().getSimpleName());
|
||||
if( chainableServerConnection.isPresent() && chainableServerConnection.get().isSuccess() ) return true;
|
||||
}
|
||||
|
||||
CompactTxStreamerGrpc.CompactTxStreamerBlockingStub stub = CompactTxStreamerGrpc.newBlockingStub(this.channel);
|
||||
LightdInfo lightdInfo = stub.getLightdInfo(Empty.newBuilder().build());
|
||||
return false;
|
||||
}
|
||||
|
||||
if (lightdInfo == null || lightdInfo.getBlockHeight() <= 0)
|
||||
continue;
|
||||
private Optional<ChainableServerConnection> makeConnection(ChainableServer server, String requestedBy) {
|
||||
LOGGER.info(() -> String.format("Connecting to %s", server));
|
||||
|
||||
// TODO: find a way to verify that the server is using the expected chain
|
||||
try {
|
||||
this.channel = ManagedChannelBuilder.forAddress(server.getHostName(), server.getPort()).build();
|
||||
|
||||
CompactTxStreamerGrpc.CompactTxStreamerBlockingStub stub = CompactTxStreamerGrpc.newBlockingStub(this.channel);
|
||||
LightdInfo lightdInfo = stub.getLightdInfo(Empty.newBuilder().build());
|
||||
|
||||
if (lightdInfo == null || lightdInfo.getBlockHeight() <= 0)
|
||||
return Optional.of( this.recorder.recordConnection(server, requestedBy,true, false, "lightd info issues") );
|
||||
|
||||
// TODO: find a way to verify that the server is using the expected chain
|
||||
|
||||
// if (featuresJson == null || Double.valueOf((String) featuresJson.get("protocol_min")) < MIN_PROTOCOL_VERSION)
|
||||
// continue;
|
||||
@@ -620,28 +670,31 @@ public class PirateLightClient extends BitcoinyBlockchainProvider {
|
||||
// if (this.expectedGenesisHash != null && !((String) featuresJson.get("genesis_hash")).equals(this.expectedGenesisHash))
|
||||
// continue;
|
||||
|
||||
LOGGER.debug(() -> String.format("Connected to %s", server));
|
||||
this.currentServer = server;
|
||||
return true;
|
||||
} catch (Exception e) {
|
||||
// Didn't work, try another server...
|
||||
closeServer();
|
||||
}
|
||||
LOGGER.info(() -> String.format("Connected to %s", server));
|
||||
this.currentServer = server;
|
||||
return Optional.of( this.recorder.recordConnection(server, requestedBy,true, true, EMPTY) );
|
||||
} catch (Exception e) {
|
||||
// Didn't work, try another server...
|
||||
return Optional.of( this.recorder.recordConnection( server, requestedBy, true, false, CrossChainUtils.getNotes(e)));
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
/**
|
||||
* Closes connection to <tt>server</tt> if it is currently connected server.
|
||||
*
|
||||
* @param server
|
||||
* @param requestedBy
|
||||
*/
|
||||
private void closeServer(ChainableServer server) {
|
||||
private Optional<ChainableServerConnection> closeServer(ChainableServer server, String notes, String requestedBy) {
|
||||
|
||||
final ChainableServerConnection connection;
|
||||
|
||||
synchronized (this.serverLock) {
|
||||
if (this.currentServer == null || !this.currentServer.equals(server) || this.channel == null) {
|
||||
return;
|
||||
return Optional.empty();
|
||||
}
|
||||
|
||||
connection = this.recorder.recordConnection(server, requestedBy, false, true, notes);
|
||||
|
||||
// Close the gRPC managed-channel if not shut down already.
|
||||
if (!this.channel.isShutdown()) {
|
||||
try {
|
||||
@@ -669,12 +722,14 @@ public class PirateLightClient extends BitcoinyBlockchainProvider {
|
||||
this.channel = null;
|
||||
this.currentServer = null;
|
||||
}
|
||||
|
||||
return Optional.of( connection );
|
||||
}
|
||||
|
||||
/** Closes connection to currently connected server (if any). */
|
||||
private void closeServer() {
|
||||
private Optional<ChainableServerConnection> closeServer(String requestedBy, String notes) {
|
||||
synchronized (this.serverLock) {
|
||||
this.closeServer(this.currentServer);
|
||||
return this.closeServer(this.currentServer, notes, requestedBy);
|
||||
}
|
||||
}
|
||||
|
||||
|
@@ -61,8 +61,7 @@ public class Ravencoin extends Bitcoiny {
|
||||
|
||||
@Override
|
||||
public long getP2shFee(Long timestamp) {
|
||||
// TODO: This will need to be replaced with something better in the near future!
|
||||
return MAINNET_FEE;
|
||||
return this.getFeeCeiling();
|
||||
}
|
||||
},
|
||||
TEST3 {
|
||||
@@ -112,6 +111,16 @@ public class Ravencoin extends Bitcoiny {
|
||||
}
|
||||
};
|
||||
|
||||
private long feeCeiling = MAINNET_FEE;
|
||||
|
||||
public long getFeeCeiling() {
|
||||
return feeCeiling;
|
||||
}
|
||||
|
||||
public void setFeeCeiling(long feeCeiling) {
|
||||
this.feeCeiling = feeCeiling;
|
||||
}
|
||||
|
||||
public abstract NetworkParameters getParams();
|
||||
public abstract Collection<Server> getServers();
|
||||
public abstract String getGenesisHash();
|
||||
@@ -125,7 +134,7 @@ public class Ravencoin extends Bitcoiny {
|
||||
// Constructors and instance
|
||||
|
||||
private Ravencoin(RavencoinNet ravencoinNet, BitcoinyBlockchainProvider blockchain, Context bitcoinjContext, String currencyCode) {
|
||||
super(blockchain, bitcoinjContext, currencyCode);
|
||||
super(blockchain, bitcoinjContext, currencyCode, DEFAULT_FEE_PER_KB);
|
||||
this.ravencoinNet = ravencoinNet;
|
||||
|
||||
LOGGER.info(() -> String.format("Starting Ravencoin support using %s", this.ravencoinNet.name()));
|
||||
@@ -154,11 +163,6 @@ public class Ravencoin extends Bitcoiny {
|
||||
|
||||
// Actual useful methods for use by other classes
|
||||
|
||||
@Override
|
||||
public Coin getFeePerKb() {
|
||||
return DEFAULT_FEE_PER_KB;
|
||||
}
|
||||
|
||||
@Override
|
||||
public long getMinimumOrderAmount() {
|
||||
return MINIMUM_ORDER_AMOUNT;
|
||||
@@ -175,4 +179,14 @@ public class Ravencoin extends Bitcoiny {
|
||||
return this.ravencoinNet.getP2shFee(timestamp);
|
||||
}
|
||||
|
||||
@Override
|
||||
public long getFeeCeiling() {
|
||||
return this.ravencoinNet.getFeeCeiling();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void setFeeCeiling(long fee) {
|
||||
|
||||
this.ravencoinNet.setFeeCeiling( fee );
|
||||
}
|
||||
}
|
||||
|
@@ -0,0 +1,82 @@
|
||||
package org.qortal.crosschain;
|
||||
|
||||
import javax.xml.bind.annotation.XmlAccessType;
|
||||
import javax.xml.bind.annotation.XmlAccessorType;
|
||||
import java.util.Objects;
|
||||
|
||||
@XmlAccessorType(XmlAccessType.FIELD)
|
||||
public class ServerConnectionInfo {
|
||||
|
||||
private ServerInfo serverInfo;
|
||||
|
||||
private String requestedBy;
|
||||
|
||||
private boolean open;
|
||||
|
||||
private boolean success;
|
||||
|
||||
private long timeInMillis;
|
||||
|
||||
private String notes;
|
||||
|
||||
public ServerConnectionInfo() {
|
||||
}
|
||||
|
||||
public ServerConnectionInfo(ServerInfo serverInfo, String requestedBy, boolean open, boolean success, long timeInMillis, String notes) {
|
||||
this.serverInfo = serverInfo;
|
||||
this.requestedBy = requestedBy;
|
||||
this.open = open;
|
||||
this.success = success;
|
||||
this.timeInMillis = timeInMillis;
|
||||
this.notes = notes;
|
||||
}
|
||||
|
||||
public ServerInfo getServerInfo() {
|
||||
return serverInfo;
|
||||
}
|
||||
|
||||
public String getRequestedBy() {
|
||||
return requestedBy;
|
||||
}
|
||||
|
||||
public boolean isOpen() {
|
||||
return open;
|
||||
}
|
||||
|
||||
public boolean isSuccess() {
|
||||
return success;
|
||||
}
|
||||
|
||||
public long getTimeInMillis() {
|
||||
return timeInMillis;
|
||||
}
|
||||
|
||||
public String getNotes() {
|
||||
return notes;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean equals(Object o) {
|
||||
if (this == o) return true;
|
||||
if (o == null || getClass() != o.getClass()) return false;
|
||||
ServerConnectionInfo that = (ServerConnectionInfo) o;
|
||||
return timeInMillis == that.timeInMillis && Objects.equals(serverInfo, that.serverInfo);
|
||||
}
|
||||
|
||||
@Override
|
||||
public int hashCode() {
|
||||
return Objects.hash(serverInfo, timeInMillis);
|
||||
}
|
||||
|
||||
@Override
|
||||
public String toString() {
|
||||
return "ServerConnectionInfo{" +
|
||||
"serverInfo=" + serverInfo +
|
||||
", requestedBy='" + requestedBy + '\'' +
|
||||
", open=" + open +
|
||||
", success=" + success +
|
||||
", timeInMillis=" + timeInMillis +
|
||||
", notes='" + notes + '\'' +
|
||||
'}';
|
||||
}
|
||||
}
|
@@ -0,0 +1,32 @@
|
||||
package org.qortal.data.crosschain;
|
||||
|
||||
import org.qortal.crosschain.BitcoinyTransaction;
|
||||
import org.qortal.crosschain.TransactionHash;
|
||||
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
|
||||
public class AtomicTransactionData {
|
||||
public final TransactionHash hash;
|
||||
public final Integer timestamp;
|
||||
public final List<BitcoinyTransaction.Input> inputs;
|
||||
public final Map<List<String>, Long> valueByAddress;
|
||||
public final long totalAmount;
|
||||
public final int size;
|
||||
|
||||
public AtomicTransactionData(
|
||||
TransactionHash hash,
|
||||
Integer timestamp,
|
||||
List<BitcoinyTransaction.Input> inputs,
|
||||
Map<List<String>, Long> valueByAddress,
|
||||
long totalAmount,
|
||||
int size) {
|
||||
|
||||
this.hash = hash;
|
||||
this.timestamp = timestamp;
|
||||
this.inputs = inputs;
|
||||
this.valueByAddress = valueByAddress;
|
||||
this.totalAmount = totalAmount;
|
||||
this.size = size;
|
||||
}
|
||||
}
|
106
src/main/java/org/qortal/data/crosschain/TransactionSummary.java
Normal file
106
src/main/java/org/qortal/data/crosschain/TransactionSummary.java
Normal file
@@ -0,0 +1,106 @@
|
||||
package org.qortal.data.crosschain;
|
||||
|
||||
import javax.xml.bind.annotation.XmlAccessType;
|
||||
import javax.xml.bind.annotation.XmlAccessorType;
|
||||
|
||||
@XmlAccessorType(XmlAccessType.FIELD)
|
||||
public class TransactionSummary {
|
||||
|
||||
private String atAddress;
|
||||
private String p2shValue;
|
||||
private String p2shAddress;
|
||||
private String lockingHash;
|
||||
private Integer lockingTimestamp;
|
||||
private long lockingTotalAmount;
|
||||
private long lockingFee;
|
||||
private int lockingSize;
|
||||
private String unlockingHash;
|
||||
private Integer unlockingTimestamp;
|
||||
private long unlockingTotalAmount;
|
||||
private long unlockingFee;
|
||||
private int unlockingSize;
|
||||
|
||||
public TransactionSummary(){}
|
||||
|
||||
public TransactionSummary(
|
||||
String atAddress,
|
||||
String p2shValue,
|
||||
String p2shAddress,
|
||||
String lockingHash,
|
||||
Integer lockingTimestamp,
|
||||
long lockingTotalAmount,
|
||||
long lockingFee,
|
||||
int lockingSize,
|
||||
String unlockingHash,
|
||||
Integer unlockingTimestamp,
|
||||
long unlockingTotalAmount,
|
||||
long unlockingFee,
|
||||
int unlockingSize) {
|
||||
|
||||
this.atAddress = atAddress;
|
||||
this.p2shValue = p2shValue;
|
||||
this.p2shAddress = p2shAddress;
|
||||
this.lockingHash = lockingHash;
|
||||
this.lockingTimestamp = lockingTimestamp;
|
||||
this.lockingTotalAmount = lockingTotalAmount;
|
||||
this.lockingFee = lockingFee;
|
||||
this.lockingSize = lockingSize;
|
||||
this.unlockingHash = unlockingHash;
|
||||
this.unlockingTimestamp = unlockingTimestamp;
|
||||
this.unlockingTotalAmount = unlockingTotalAmount;
|
||||
this.unlockingFee = unlockingFee;
|
||||
this.unlockingSize = unlockingSize;
|
||||
}
|
||||
|
||||
public String getAtAddress() {
|
||||
return atAddress;
|
||||
}
|
||||
|
||||
public String getP2shValue() {
|
||||
return p2shValue;
|
||||
}
|
||||
|
||||
public String getP2shAddress() {
|
||||
return p2shAddress;
|
||||
}
|
||||
|
||||
public String getLockingHash() {
|
||||
return lockingHash;
|
||||
}
|
||||
|
||||
public Integer getLockingTimestamp() {
|
||||
return lockingTimestamp;
|
||||
}
|
||||
|
||||
public long getLockingTotalAmount() {
|
||||
return lockingTotalAmount;
|
||||
}
|
||||
|
||||
public long getLockingFee() {
|
||||
return lockingFee;
|
||||
}
|
||||
|
||||
public int getLockingSize() {
|
||||
return lockingSize;
|
||||
}
|
||||
|
||||
public String getUnlockingHash() {
|
||||
return unlockingHash;
|
||||
}
|
||||
|
||||
public Integer getUnlockingTimestamp() {
|
||||
return unlockingTimestamp;
|
||||
}
|
||||
|
||||
public long getUnlockingTotalAmount() {
|
||||
return unlockingTotalAmount;
|
||||
}
|
||||
|
||||
public long getUnlockingFee() {
|
||||
return unlockingFee;
|
||||
}
|
||||
|
||||
public int getUnlockingSize() {
|
||||
return unlockingSize;
|
||||
}
|
||||
}
|
@@ -25,8 +25,8 @@ public class ArbitraryTransactionData extends TransactionData {
|
||||
// "data" field types
|
||||
public enum DataType {
|
||||
RAW_DATA,
|
||||
DATA_HASH;
|
||||
}
|
||||
DATA_HASH
|
||||
}
|
||||
|
||||
// Methods
|
||||
public enum Method {
|
||||
|
@@ -4,7 +4,6 @@ import org.apache.logging.log4j.LogManager;
|
||||
import org.apache.logging.log4j.Logger;
|
||||
import org.qortal.controller.Controller;
|
||||
import org.qortal.globalization.Translator;
|
||||
import org.qortal.settings.Settings;
|
||||
import org.qortal.utils.URLViewer;
|
||||
|
||||
import javax.swing.*;
|
||||
@@ -17,14 +16,11 @@ import java.awt.event.WindowEvent;
|
||||
import java.awt.event.WindowFocusListener;
|
||||
import java.io.IOException;
|
||||
import java.io.InputStream;
|
||||
import java.net.InetSocketAddress;
|
||||
import java.net.URL;
|
||||
import java.nio.channels.SocketChannel;
|
||||
import java.nio.file.Files;
|
||||
import java.nio.file.Path;
|
||||
import java.nio.file.Paths;
|
||||
import java.nio.file.StandardCopyOption;
|
||||
import java.util.ArrayList;
|
||||
import java.util.Arrays;
|
||||
import java.util.List;
|
||||
|
||||
|
@@ -70,15 +70,15 @@ public enum Handshake {
|
||||
peer.setPeersVersion(versionString, version);
|
||||
|
||||
// Ensure the peer is running at least the version specified in MIN_PEER_VERSION
|
||||
if (peer.isAtLeastVersion(MIN_PEER_VERSION) == false) {
|
||||
if (!peer.isAtLeastVersion(MIN_PEER_VERSION)) {
|
||||
LOGGER.debug(String.format("Ignoring peer %s because it is on an old version (%s)", peer, versionString));
|
||||
return null;
|
||||
}
|
||||
|
||||
if (Settings.getInstance().getAllowConnectionsWithOlderPeerVersions() == false) {
|
||||
if (!Settings.getInstance().getAllowConnectionsWithOlderPeerVersions()) {
|
||||
// Ensure the peer is running at least the minimum version allowed for connections
|
||||
final String minPeerVersion = Settings.getInstance().getMinPeerVersion();
|
||||
if (peer.isAtLeastVersion(minPeerVersion) == false) {
|
||||
if (!peer.isAtLeastVersion(minPeerVersion)) {
|
||||
LOGGER.debug(String.format("Ignoring peer %s because it is on an old version (%s)", peer, versionString));
|
||||
return null;
|
||||
}
|
||||
|
@@ -810,7 +810,7 @@ public class Network {
|
||||
.filter(peer -> peer.hasReachedMaxConnectionAge())
|
||||
.collect(Collectors.toList());
|
||||
|
||||
if (peersToDisconnect != null && peersToDisconnect.size() > 0) {
|
||||
if (peersToDisconnect != null && !peersToDisconnect.isEmpty()) {
|
||||
for (Peer peer : peersToDisconnect) {
|
||||
LOGGER.debug("Forcing disconnection of peer {} because connection age ({} ms) " +
|
||||
"has reached the maximum ({} ms)", peer, peer.getConnectionAge(), peer.getMaxConnectionAge());
|
||||
|
@@ -859,7 +859,7 @@ public class Peer {
|
||||
}
|
||||
}
|
||||
|
||||
if (logStats && this.receivedMessageStats.size() > 0) {
|
||||
if (logStats && !this.receivedMessageStats.isEmpty()) {
|
||||
StringBuilder statsBuilder = new StringBuilder(1024);
|
||||
statsBuilder.append("peer ").append(this).append(" message stats:\n=received=");
|
||||
appendMessageStats(statsBuilder, this.receivedMessageStats);
|
||||
|
@@ -205,6 +205,15 @@ public interface TransactionRepository {
|
||||
*/
|
||||
public List<String> getConfirmedRewardShareCreatorsExcludingSelfShares() throws DataException;
|
||||
|
||||
/**
|
||||
* Returns list of transfer asset transaction creators.
|
||||
* This uses confirmed transactions only.
|
||||
*
|
||||
* @return
|
||||
* @throws DataException
|
||||
*/
|
||||
public List<String> getConfirmedTransferAssetCreators() throws DataException;
|
||||
|
||||
/**
|
||||
* Returns list of transactions pending approval, with optional txGgroupId filtering.
|
||||
* <p>
|
||||
|
@@ -1024,7 +1024,7 @@ public class HSQLDBArbitraryRepository implements ArbitraryRepository {
|
||||
String tag5 = null;
|
||||
|
||||
if (tags != null) {
|
||||
if (tags.size() > 0) tag1 = tags.get(0);
|
||||
if (!tags.isEmpty()) tag1 = tags.get(0);
|
||||
if (tags.size() > 1) tag2 = tags.get(1);
|
||||
if (tags.size() > 2) tag3 = tags.get(2);
|
||||
if (tags.size() > 3) tag4 = tags.get(3);
|
||||
|
@@ -69,10 +69,10 @@ public class HSQLDBChatRepository implements ChatRepository {
|
||||
bindParams.add(chatReferenceBytes);
|
||||
}
|
||||
|
||||
if (hasChatReference != null && hasChatReference == true) {
|
||||
if (hasChatReference != null && hasChatReference) {
|
||||
whereClauses.add("chat_reference IS NOT NULL");
|
||||
}
|
||||
else if (hasChatReference != null && hasChatReference == false) {
|
||||
else if (hasChatReference != null && !hasChatReference) {
|
||||
whereClauses.add("chat_reference IS NULL");
|
||||
}
|
||||
|
||||
|
@@ -137,7 +137,7 @@ public class HSQLDBImportExport {
|
||||
String existingTradePrivateKey = (String) existingTradeBotDataItem.get("tradePrivateKey");
|
||||
// Check if we already have an entry for this trade
|
||||
boolean found = allTradeBotData.stream().anyMatch(tradeBotData -> Base58.encode(tradeBotData.getTradePrivateKey()).equals(existingTradePrivateKey));
|
||||
if (found == false)
|
||||
if (!found)
|
||||
// Add the data from the backup file to our "allTradeBotDataJson" array as it's not currently in the db
|
||||
allTradeBotDataJson.put(existingTradeBotDataItem);
|
||||
}
|
||||
|
@@ -1043,6 +1043,33 @@ public class HSQLDBTransactionRepository implements TransactionRepository {
|
||||
}
|
||||
}
|
||||
|
||||
public List<String> getConfirmedTransferAssetCreators() throws DataException {
|
||||
List<String> transferAssetCreators = new ArrayList<>();
|
||||
|
||||
String sql = "SELECT account "
|
||||
+ "FROM TransferAssetTransactions "
|
||||
+ "JOIN Accounts ON Accounts.public_key = TransferAssetTransactions.sender "
|
||||
+ "JOIN Transactions ON Transactions.signature = TransferAssetTransactions.signature "
|
||||
+ "WHERE block_height IS NOT NULL AND TransferAssetTransactions.recipient != Accounts.account "
|
||||
+ "GROUP BY account "
|
||||
+ "ORDER BY account";
|
||||
|
||||
try (ResultSet resultSet = this.repository.checkedExecute(sql)) {
|
||||
if (resultSet == null)
|
||||
return transferAssetCreators;
|
||||
|
||||
do {
|
||||
String address = resultSet.getString(1);
|
||||
|
||||
transferAssetCreators.add(address);
|
||||
} while (resultSet.next());
|
||||
|
||||
return transferAssetCreators;
|
||||
} catch (SQLException e) {
|
||||
throw new DataException("Unable to fetch transfer asset from repository", e);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public List<TransactionData> getApprovalPendingTransactions(Integer txGroupId, Integer limit, Integer offset, Boolean reverse) throws DataException {
|
||||
StringBuilder sql = new StringBuilder(512);
|
||||
|
@@ -211,7 +211,7 @@ public class Settings {
|
||||
public long recoveryModeTimeout = 9999999999999L;
|
||||
|
||||
/** Minimum peer version number required in order to sync with them */
|
||||
private String minPeerVersion = "4.4.2";
|
||||
private String minPeerVersion = "4.5.1";
|
||||
/** Whether to allow connections with peers below minPeerVersion
|
||||
* If true, we won't sync with them but they can still sync with us, and will show in the peers list
|
||||
* If false, sync will be blocked both ways, and they will not appear in the peers list */
|
||||
|
@@ -70,6 +70,10 @@ public class CancelGroupBanTransaction extends Transaction {
|
||||
if (!this.repository.getGroupRepository().adminExists(groupId, admin.getAddress()))
|
||||
return ValidationResult.NOT_GROUP_ADMIN;
|
||||
|
||||
// Can't unban if not group's current owner
|
||||
if (!admin.getAddress().equals(groupData.getOwner()))
|
||||
return ValidationResult.INVALID_GROUP_OWNER;
|
||||
|
||||
Account member = getMember();
|
||||
|
||||
// Check ban actually exists
|
||||
|
@@ -168,7 +168,7 @@ public class ChatTransaction extends Transaction {
|
||||
|
||||
// Check for blocked author by registered name
|
||||
List<NameData> names = this.repository.getNameRepository().getNamesByOwner(this.chatTransactionData.getSender());
|
||||
if (names != null && names.size() > 0) {
|
||||
if (names != null && !names.isEmpty()) {
|
||||
for (NameData nameData : names) {
|
||||
if (nameData != null && nameData.getName() != null) {
|
||||
if (ListUtils.isNameBlocked(nameData.getName())) {
|
||||
|
@@ -70,6 +70,10 @@ public class GroupBanTransaction extends Transaction {
|
||||
if (!this.repository.getGroupRepository().adminExists(groupId, admin.getAddress()))
|
||||
return ValidationResult.NOT_GROUP_ADMIN;
|
||||
|
||||
// Can't ban if not group's current owner
|
||||
if (!admin.getAddress().equals(groupData.getOwner()))
|
||||
return ValidationResult.INVALID_GROUP_OWNER;
|
||||
|
||||
Account offender = getOffender();
|
||||
|
||||
// Can't ban group owner
|
||||
|
@@ -82,6 +82,10 @@ public class GroupKickTransaction extends Transaction {
|
||||
if (!admin.getAddress().equals(groupData.getOwner()) && groupRepository.adminExists(groupId, member.getAddress()))
|
||||
return ValidationResult.INVALID_GROUP_OWNER;
|
||||
|
||||
// Can't kick if not group's current owner
|
||||
if (!admin.getAddress().equals(groupData.getOwner()))
|
||||
return ValidationResult.INVALID_GROUP_OWNER;
|
||||
|
||||
// Check creator has enough funds
|
||||
if (admin.getConfirmedBalance(Asset.QORT) < this.groupKickTransactionData.getFee())
|
||||
return ValidationResult.NO_BALANCE;
|
||||
|
@@ -54,6 +54,10 @@ public class RegisterNameTransaction extends Transaction {
|
||||
Account registrant = getRegistrant();
|
||||
String name = this.registerNameTransactionData.getName();
|
||||
|
||||
int blockchainHeight = this.repository.getBlockRepository().getBlockchainHeight();
|
||||
final int start = BlockChain.getInstance().getSelfSponsorshipAlgoV2Height() - 1180;
|
||||
final int end = BlockChain.getInstance().getSelfSponsorshipAlgoV3Height();
|
||||
|
||||
// Check name size bounds
|
||||
int nameLength = Utf8.encodedLength(name);
|
||||
if (nameLength < Name.MIN_NAME_SIZE || nameLength > Name.MAX_NAME_SIZE)
|
||||
@@ -76,6 +80,10 @@ public class RegisterNameTransaction extends Transaction {
|
||||
if (registrant.getConfirmedBalance(Asset.QORT) < this.registerNameTransactionData.getFee())
|
||||
return ValidationResult.NO_BALANCE;
|
||||
|
||||
// Check if we are on algo runs
|
||||
if (blockchainHeight >= start && blockchainHeight <= end)
|
||||
return ValidationResult.TEMPORARY_DISABLED;
|
||||
|
||||
return ValidationResult.OK;
|
||||
}
|
||||
|
||||
|
@@ -43,7 +43,7 @@ public class RewardShareTransaction extends Transaction {
|
||||
}
|
||||
|
||||
private RewardShareData getExistingRewardShare() throws DataException {
|
||||
if (this.haveCheckedForExistingRewardShare == false) {
|
||||
if (!this.haveCheckedForExistingRewardShare) {
|
||||
this.haveCheckedForExistingRewardShare = true;
|
||||
|
||||
// Look up any existing reward-share (using transaction's reward-share public key)
|
||||
@@ -183,18 +183,28 @@ public class RewardShareTransaction extends Transaction {
|
||||
|
||||
@Override
|
||||
public boolean isConfirmableAtHeight(int height) {
|
||||
final int min = BlockChain.getInstance().getPenaltyFixHeight() - 50;
|
||||
final int max = BlockChain.getInstance().getPenaltyFixHeight() + 50;
|
||||
final int startV2 = BlockChain.getInstance().getSelfSponsorshipAlgoV2Height() - 15;
|
||||
final int startV3 = BlockChain.getInstance().getSelfSponsorshipAlgoV3Height() - 15;
|
||||
final int endV2 = BlockChain.getInstance().getSelfSponsorshipAlgoV2Height() + 10;
|
||||
final int endV3 = BlockChain.getInstance().getSelfSponsorshipAlgoV3Height() + 10;
|
||||
|
||||
if (height >= BlockChain.getInstance().getUnconfirmableRewardSharesHeight()) {
|
||||
// Not confirmable in online accounts or distribution blocks
|
||||
if (Block.isOnlineAccountsBlock(height) || Block.isBatchRewardDistributionBlock(height)) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
// Not confirmable on penalty fix
|
||||
if (height > min && height < max) {
|
||||
|
||||
if (height >= startV2 && height <= endV2) {
|
||||
// Not confirmable on algo V2 run
|
||||
return false;
|
||||
}
|
||||
|
||||
if (height >= startV3 && height <= endV3) {
|
||||
// Not confirmable on algo V3 run
|
||||
return false;
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
|
@@ -173,6 +173,7 @@ public abstract class Transaction {
|
||||
INVALID_OPTION_LENGTH(20),
|
||||
DUPLICATE_OPTION(21),
|
||||
POLL_ALREADY_EXISTS(22),
|
||||
POLL_ALREADY_HAS_VOTES(23),
|
||||
POLL_DOES_NOT_EXIST(24),
|
||||
POLL_OPTION_DOES_NOT_EXIST(25),
|
||||
ALREADY_VOTED_FOR_THAT_OPTION(26),
|
||||
@@ -247,6 +248,7 @@ public abstract class Transaction {
|
||||
GROUP_APPROVAL_REQUIRED(98),
|
||||
ACCOUNT_NOT_TRANSFERABLE(99),
|
||||
TRANSFER_PRIVS_DISABLED(100),
|
||||
TEMPORARY_DISABLED(101),
|
||||
INVALID_BUT_OK(999),
|
||||
NOT_YET_RELEASED(1000),
|
||||
NOT_SUPPORTED(1001);
|
||||
|
@@ -1,6 +1,7 @@
|
||||
package org.qortal.transaction;
|
||||
|
||||
import org.qortal.account.Account;
|
||||
import org.qortal.block.BlockChain;
|
||||
import org.qortal.data.PaymentData;
|
||||
import org.qortal.data.transaction.TransactionData;
|
||||
import org.qortal.data.transaction.TransferAssetTransactionData;
|
||||
@@ -51,6 +52,14 @@ public class TransferAssetTransaction extends Transaction {
|
||||
|
||||
@Override
|
||||
public ValidationResult isValid() throws DataException {
|
||||
int blockchainHeight = this.repository.getBlockRepository().getBlockchainHeight();
|
||||
final int start = BlockChain.getInstance().getSelfSponsorshipAlgoV2Height();
|
||||
final int end = BlockChain.getInstance().getSelfSponsorshipAlgoV3Height();
|
||||
|
||||
// Check if we are on algo runs
|
||||
if (blockchainHeight >= start && blockchainHeight <= end)
|
||||
return ValidationResult.ASSET_NOT_SPENDABLE;
|
||||
|
||||
// Wrap asset transfer as a payment and delegate final payment checks to Payment class
|
||||
return new Payment(this.repository).isValid(this.transferAssetTransactionData.getSenderPublicKey(), getPaymentData(), this.transferAssetTransactionData.getFee());
|
||||
}
|
||||
|
@@ -83,6 +83,10 @@ public class UpdateGroupTransaction extends Transaction {
|
||||
|
||||
Account owner = getOwner();
|
||||
|
||||
// Check creator is group's current owner
|
||||
if (!owner.getAddress().equals(groupData.getOwner()))
|
||||
return ValidationResult.INVALID_GROUP_OWNER;
|
||||
|
||||
// Check creator has enough funds
|
||||
if (owner.getConfirmedBalance(Asset.QORT) < this.updateGroupTransactionData.getFee())
|
||||
return ValidationResult.NO_BALANCE;
|
||||
|
@@ -398,7 +398,7 @@ public class ArbitraryTransactionUtils {
|
||||
public static ArbitraryResourceStatus getStatus(Service service, String name, String identifier, Boolean build, boolean updateCache) {
|
||||
|
||||
// If "build" has been specified, build the resource before returning its status
|
||||
if (build != null && build == true) {
|
||||
if (build != null && build) {
|
||||
try {
|
||||
ArbitraryDataReader reader = new ArbitraryDataReader(name, ArbitraryDataFile.ResourceIdType.NAME, service, identifier);
|
||||
if (!reader.isBuilding()) {
|
||||
|
@@ -111,7 +111,7 @@ public class Base58 {
|
||||
//
|
||||
// Nothing to do if we have an empty string
|
||||
//
|
||||
if (string.length() == 0)
|
||||
if (string.isEmpty())
|
||||
return null;
|
||||
//
|
||||
// Convert the input string to a byte sequence
|
||||
|
@@ -59,7 +59,7 @@ public class BlockArchiveUtils {
|
||||
if (firstBlock == null || firstBlock.getBlockData().getHeight() != startHeight) {
|
||||
throw new IllegalStateException("Non matching first block when importing from archive");
|
||||
}
|
||||
if (blockInfoList.size() > 0) {
|
||||
if (!blockInfoList.isEmpty()) {
|
||||
BlockTransformation lastBlock = blockInfoList.get(blockInfoList.size() - 1);
|
||||
if (lastBlock == null || lastBlock.getBlockData().getHeight() != endHeight) {
|
||||
throw new IllegalStateException("Non matching last block when importing from archive");
|
||||
|
@@ -141,7 +141,7 @@ public abstract class Unicode {
|
||||
while ((line = bufferedReader.readLine()) != null) {
|
||||
line = line.trim();
|
||||
|
||||
if (line.startsWith("#") || line.length() == 0)
|
||||
if (line.startsWith("#") || line.isEmpty())
|
||||
continue;
|
||||
|
||||
String[] charCodes = line.split(",");
|
||||
|
@@ -30,12 +30,13 @@
|
||||
"onlineAccountSignaturesMaxLifetime": 86400000,
|
||||
"onlineAccountsModulusV2Timestamp": 1659801600000,
|
||||
"selfSponsorshipAlgoV1SnapshotTimestamp": 1670230000000,
|
||||
"selfSponsorshipAlgoV2SnapshotTimestamp": 1706745600000,
|
||||
"selfSponsorshipAlgoV2SnapshotTimestamp": 1708360200000,
|
||||
"selfSponsorshipAlgoV3SnapshotTimestamp": 1708432200000,
|
||||
"referenceTimestampBlock": 1670684455220,
|
||||
"mempowTransactionUpdatesTimestamp": 1693558800000,
|
||||
"blockRewardBatchStartHeight": 1508000,
|
||||
"blockRewardBatchSize": 1000,
|
||||
"blockRewardBatchAccountsBlockCount": 25,
|
||||
"penaltyFixHash": "DG23E4Y63wC8FquG9FKAnfKwatvdJV3Z3iYnozMHn2A3",
|
||||
"rewardsByHeight": [
|
||||
{ "height": 1, "reward": 5.00 },
|
||||
{ "height": 259201, "reward": 4.75 },
|
||||
@@ -95,15 +96,14 @@
|
||||
"increaseOnlineAccountsDifficultyTimestamp": 9999999999999,
|
||||
"onlineAccountMinterLevelValidationHeight": 1092000,
|
||||
"selfSponsorshipAlgoV1Height": 1092400,
|
||||
"selfSponsorshipAlgoV2Height": 9999999,
|
||||
"selfSponsorshipAlgoV2Height": 1611200,
|
||||
"selfSponsorshipAlgoV3Height": 1612200,
|
||||
"feeValidationFixTimestamp": 1671918000000,
|
||||
"chatReferenceTimestamp": 1674316800000,
|
||||
"arbitraryOptionalFeeTimestamp": 1680278400000,
|
||||
"unconfirmableRewardSharesHeight": 1575500,
|
||||
"disableTransferPrivsTimestamp": 1706745000000,
|
||||
"enableTransferPrivsTimestamp": 1709251200000,
|
||||
"penaltyFixHeight": 1589200
|
||||
|
||||
"enableTransferPrivsTimestamp": 1709251200000
|
||||
},
|
||||
"checkpoints": [
|
||||
{ "height": 1136300, "signature": "3BbwawEF2uN8Ni5ofpJXkukoU8ctAPxYoFB7whq9pKfBnjfZcpfEJT4R95NvBDoTP8WDyWvsUvbfHbcr9qSZuYpSKZjUQTvdFf6eqznHGEwhZApWfvXu6zjGCxYCp65F4jsVYYJjkzbjmkCg5WAwN5voudngA23kMK6PpTNygapCzXt" }
|
||||
|
@@ -195,3 +195,5 @@ TRANSACTION_UNKNOWN = Transaktion unbekannt
|
||||
TX_GROUP_ID_MISMATCH = die Gruppen-ID der Transaktion stimmt nicht überein
|
||||
|
||||
TRANSFER_PRIVS_DISABLED = Übertragungsberechtigungen deaktiviert
|
||||
|
||||
TEMPORARY_DISABLED = Namensregistrierung vorübergehend deaktiviert
|
||||
|
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user