Merge branch 'master' into arbitrary-resources-cache

This commit is contained in:
AlphaX-Projects 2023-11-04 16:20:39 +01:00 committed by GitHub
commit 1aab7d8d8f
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
93 changed files with 3407 additions and 7345 deletions

3
.vscode/settings.json vendored Normal file
View File

@ -0,0 +1,3 @@
{
"java.compile.nullAnalysis.mode": "automatic"
}

View File

@ -1,7 +1,7 @@
rootLogger.level = info rootLogger.level = info
# On Windows, uncomment next line to set dirname: # On Windows, uncomment next line to set dirname:
# property.dirname = ${sys:user.home}\\AppData\\Local\\qortal\\ # property.dirname = ${sys:user.home}\\AppData\\Local\\qortal\\
property.filename = ${sys:log4j2.filenameTemplate:-log.txt} # property.filename = ${sys:log4j2.filenameTemplate:-log.txt}
rootLogger.appenderRef.console.ref = stdout rootLogger.appenderRef.console.ref = stdout
rootLogger.appenderRef.rolling.ref = FILE rootLogger.appenderRef.rolling.ref = FILE
@ -59,11 +59,14 @@ appender.console.filter.threshold.level = error
appender.rolling.type = RollingFile appender.rolling.type = RollingFile
appender.rolling.name = FILE appender.rolling.name = FILE
appender.rolling.fileName = qortal.log
appender.rolling.filePattern = qortal.%d{dd-MMM}.log.gz
appender.rolling.layout.type = PatternLayout appender.rolling.layout.type = PatternLayout
appender.rolling.layout.pattern = %d{yyyy-MM-dd HH:mm:ss} %-5p %c{1}:%L - %m%n appender.rolling.layout.pattern = %d{yyyy-MM-dd HH:mm:ss} %-5p %c{1}:%L - %m%n
appender.rolling.filePattern = ./${filename}.%i
appender.rolling.policy.type = SizeBasedTriggeringPolicy appender.rolling.policy.type = SizeBasedTriggeringPolicy
appender.rolling.policy.size = 4MB appender.rolling.policy.size = 10MB
appender.rolling.strategy.type = DefaultRolloverStrategy
appender.rolling.strategy.max = 7
# Set the immediate flush to true (default) # Set the immediate flush to true (default)
# appender.rolling.immediateFlush = true # appender.rolling.immediateFlush = true
# Set the append to true (default), should not overwrite # Set the append to true (default), should not overwrite

Binary file not shown.

View File

@ -0,0 +1,124 @@
<project xmlns="http://maven.apache.org/POM/4.0.0"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>
<groupId>org.ciyam</groupId>
<artifactId>AT</artifactId>
<version>1.4.1</version>
<packaging>jar</packaging>
<properties>
<project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
<skipTests>false</skipTests>
<maven-compiler-plugin.version>3.8.1</maven-compiler-plugin.version>
<maven-source-plugin.version>3.2.0</maven-source-plugin.version>
<maven-javadoc-plugin.version>3.3.1</maven-javadoc-plugin.version>
<maven-surefire-plugin.version>3.0.0-M4</maven-surefire-plugin.version>
<maven-jar-plugin.version>3.2.0</maven-jar-plugin.version>
<bouncycastle.version>1.64</bouncycastle.version>
</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>4.13</version>
<scope>test</scope>
</dependency>
</dependencies>
</project>

View File

@ -3,7 +3,7 @@
<groupId>org.ciyam</groupId> <groupId>org.ciyam</groupId>
<artifactId>AT</artifactId> <artifactId>AT</artifactId>
<versioning> <versioning>
<release>1.4.0</release> <release>1.4.1</release>
<versions> <versions>
<version>1.3.4</version> <version>1.3.4</version>
<version>1.3.5</version> <version>1.3.5</version>
@ -11,7 +11,8 @@
<version>1.3.7</version> <version>1.3.7</version>
<version>1.3.8</version> <version>1.3.8</version>
<version>1.4.0</version> <version>1.4.0</version>
<version>1.4.1</version>
</versions> </versions>
<lastUpdated>20221105114346</lastUpdated> <lastUpdated>20230821074325</lastUpdated>
</versioning> </versioning>
</metadata> </metadata>

View File

@ -1,7 +1,7 @@
rootLogger.level = info rootLogger.level = info
# On Windows, uncomment next line to set dirname: # On Windows, uncomment next line to set dirname:
# property.dirname = ${sys:user.home}\\AppData\\Local\\qortal\\ # property.dirname = ${sys:user.home}\\AppData\\Local\\qortal\\
property.filename = ${sys:log4j2.filenameTemplate:-log.txt} # property.filename = ${sys:log4j2.filenameTemplate:-log.txt}
rootLogger.appenderRef.console.ref = stdout rootLogger.appenderRef.console.ref = stdout
rootLogger.appenderRef.rolling.ref = FILE rootLogger.appenderRef.rolling.ref = FILE
@ -59,11 +59,14 @@ appender.console.filter.threshold.level = error
appender.rolling.type = RollingFile appender.rolling.type = RollingFile
appender.rolling.name = FILE appender.rolling.name = FILE
appender.rolling.fileName = qortal.log
appender.rolling.filePattern = qortal.%d{dd-MMM}.log.gz
appender.rolling.layout.type = PatternLayout appender.rolling.layout.type = PatternLayout
appender.rolling.layout.pattern = %d{yyyy-MM-dd HH:mm:ss} %-5p %c{1}:%L - %m%n appender.rolling.layout.pattern = %d{yyyy-MM-dd HH:mm:ss} %-5p %c{1}:%L - %m%n
appender.rolling.filePattern = ./${filename}.%i
appender.rolling.policy.type = SizeBasedTriggeringPolicy appender.rolling.policy.type = SizeBasedTriggeringPolicy
appender.rolling.policy.size = 4MB appender.rolling.policy.size = 10MB
appender.rolling.strategy.type = DefaultRolloverStrategy
appender.rolling.strategy.max = 7
# Set the immediate flush to true (default) # Set the immediate flush to true (default)
# appender.rolling.immediateFlush = true # appender.rolling.immediateFlush = true
# Set the append to true (default), should not overwrite # Set the append to true (default), should not overwrite

132
pom.xml
View File

@ -3,7 +3,7 @@
<modelVersion>4.0.0</modelVersion> <modelVersion>4.0.0</modelVersion>
<groupId>org.qortal</groupId> <groupId>org.qortal</groupId>
<artifactId>qortal</artifactId> <artifactId>qortal</artifactId>
<version>4.2.2</version> <version>4.3.1</version>
<packaging>jar</packaging> <packaging>jar</packaging>
<properties> <properties>
<skipTests>true</skipTests> <skipTests>true</skipTests>
@ -11,32 +11,52 @@
<bitcoinj.version>0.15.10</bitcoinj.version> <bitcoinj.version>0.15.10</bitcoinj.version>
<bouncycastle.version>1.69</bouncycastle.version> <bouncycastle.version>1.69</bouncycastle.version>
<build.timestamp>${maven.build.timestamp}</build.timestamp> <build.timestamp>${maven.build.timestamp}</build.timestamp>
<ciyam-at.version>1.4.0</ciyam-at.version> <ciyam-at.version>1.4.1</ciyam-at.version>
<commons-net.version>3.6</commons-net.version> <commons-net.version>3.8.0</commons-net.version>
<commons-text.version>1.8</commons-text.version> <commons-text.version>1.10.0</commons-text.version>
<commons-io.version>2.6</commons-io.version> <commons-io.version>2.11.0</commons-io.version>
<commons-compress.version>1.21</commons-compress.version> <commons-compress.version>1.24.0</commons-compress.version>
<commons-lang3.version>3.12.0</commons-lang3.version> <commons-lang3.version>3.13.0</commons-lang3.version>
<xz.version>1.9</xz.version> <xz.version>1.9</xz.version>
<dagger.version>1.2.2</dagger.version> <dagger.version>1.2.2</dagger.version>
<guava.version>28.1-jre</guava.version> <guava.version>32.1.3-jre</guava.version>
<hsqldb.version>2.5.1</hsqldb.version> <hsqldb.version>2.5.1</hsqldb.version>
<homoglyph.version>1.2.1</homoglyph.version> <homoglyph.version>1.2.1</homoglyph.version>
<icu4j.version>70.1</icu4j.version> <icu4j.version>73.2</icu4j.version>
<upnp.version>1.1</upnp.version> <upnp.version>1.1</upnp.version>
<jersey.version>2.29.1</jersey.version> <jaxb-runtime.version>2.3.3</jaxb-runtime.version>
<jetty.version>9.4.29.v20200521</jetty.version> <jersey.version>2.40</jersey.version>
<log4j.version>2.17.1</log4j.version> <jetty.version>9.4.53.v20231009</jetty.version>
<log4j.version>2.20.0</log4j.version>
<project.build.sourceEncoding>UTF-8</project.build.sourceEncoding> <project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
<slf4j.version>1.7.12</slf4j.version> <slf4j.version>1.7.36</slf4j.version>
<swagger-api.version>2.0.9</swagger-api.version> <swagger-api.version>2.0.10</swagger-api.version>
<swagger-ui.version>3.23.8</swagger-ui.version> <swagger-ui.version>3.52.5</swagger-ui.version>
<package-info-maven-plugin.version>1.1.0</package-info-maven-plugin.version> <package-info-maven-plugin.version>1.1.0</package-info-maven-plugin.version>
<jsoup.version>1.13.1</jsoup.version> <jsoup.version>1.16.1</jsoup.version>
<java-diff-utils.version>4.10</java-diff-utils.version> <java-diff-utils.version>4.12</java-diff-utils.version>
<grpc.version>1.45.1</grpc.version> <grpc.version>1.58.0</grpc.version>
<protobuf.version>3.19.4</protobuf.version> <protobuf.version>3.24.4</protobuf.version>
<simplemagic.version>1.17</simplemagic.version> <simplemagic.version>1.17</simplemagic.version>
<versions-maven-plugin.version>2.16.1</versions-maven-plugin.version>
<maven-compiler-plugin.version>3.11.0</maven-compiler-plugin.version>
<git-commit-id-plugin.version>4.9.10</git-commit-id-plugin.version>
<replacer.version>1.5.3</replacer.version>
<maven-resources-plugin.version>3.3.1</maven-resources-plugin.version>
<build-helper-maven-plugin.version>3.4.0</build-helper-maven-plugin.version>
<maven-jar-plugin.version>3.3.0</maven-jar-plugin.version>
<maven-shade-plugin.version>3.5.1</maven-shade-plugin.version>
<reproducible-build-maven-plugin.version>0.16</reproducible-build-maven-plugin.version>
<maven-surefire-plugin.version>3.1.2</maven-surefire-plugin.version>
<lifecycle-mapping.version>1.0.0</lifecycle-mapping.version>
<build-helper-maven-plugin.version>3.4.0</build-helper-maven-plugin.version>
<json-simple.version>1.1.1</json-simple.version>
<json.version>20231013</json.version>
<extendedset.version>0.12.3</extendedset.version>
<javax.servlet-api.version>4.0.1</javax.servlet-api.version>
<mail.version>1.5.0-b01</mail.version>
<junit-jupiter-engine.version>5.3.1</junit-jupiter-engine.version>
<hamcrest-library.version>1.3</hamcrest-library.version>
</properties> </properties>
<build> <build>
<sourceDirectory>src/main/java</sourceDirectory> <sourceDirectory>src/main/java</sourceDirectory>
@ -51,14 +71,14 @@
<plugin> <plugin>
<groupId>org.codehaus.mojo</groupId> <groupId>org.codehaus.mojo</groupId>
<artifactId>versions-maven-plugin</artifactId> <artifactId>versions-maven-plugin</artifactId>
<version>2.5</version> <version>${versions-maven-plugin.version}</version>
<configuration> <configuration>
<generateBackupPoms>false</generateBackupPoms> <generateBackupPoms>false</generateBackupPoms>
</configuration> </configuration>
</plugin> </plugin>
<plugin> <plugin>
<artifactId>maven-compiler-plugin</artifactId> <artifactId>maven-compiler-plugin</artifactId>
<version>3.8.0</version> <version>${maven-compiler-plugin.version}</version>
<configuration> <configuration>
<release>11</release> <release>11</release>
</configuration> </configuration>
@ -89,7 +109,7 @@
<plugin> <plugin>
<groupId>pl.project13.maven</groupId> <groupId>pl.project13.maven</groupId>
<artifactId>git-commit-id-plugin</artifactId> <artifactId>git-commit-id-plugin</artifactId>
<version>4.0.0</version> <version>${git-commit-id-plugin.version}</version>
<executions> <executions>
<execution> <execution>
<id>get-the-git-infos</id> <id>get-the-git-infos</id>
@ -121,7 +141,7 @@
<plugin> <plugin>
<groupId>com.google.code.maven-replacer-plugin</groupId> <groupId>com.google.code.maven-replacer-plugin</groupId>
<artifactId>replacer</artifactId> <artifactId>replacer</artifactId>
<version>1.5.3</version> <version>${replacer.version}</version>
<executions> <executions>
<execution> <execution>
<id>replace-swagger-ui</id> <id>replace-swagger-ui</id>
@ -164,11 +184,13 @@
<configuration> <configuration>
<file>${project.build.outputDirectory}/git.properties</file> <file>${project.build.outputDirectory}/git.properties</file>
<regex>true</regex> <regex>true</regex>
<regexFlags><regexFlag>MULTILINE</regexFlag></regexFlags> <regexFlags>
<regexFlag>MULTILINE</regexFlag>
</regexFlags>
<replacements> <replacements>
<replacement> <replacement>
<token>^(#.*$[\n\r]*)</token> <token>^(#.*$[\n\r]*)</token>
<value></value> <value/>
</replacement> </replacement>
</replacements> </replacements>
</configuration> </configuration>
@ -178,7 +200,10 @@
<!-- add swagger-ui as resource to output package --> <!-- add swagger-ui as resource to output package -->
<plugin> <plugin>
<artifactId>maven-resources-plugin</artifactId> <artifactId>maven-resources-plugin</artifactId>
<version>3.1.0</version> <version>${maven-resources-plugin.version}</version>
<configuration>
<propertiesEncoding>ISO-8859-1</propertiesEncoding>
</configuration>
<executions> <executions>
<execution> <execution>
<id>copy-resources</id> <id>copy-resources</id>
@ -232,7 +257,7 @@
<plugin> <plugin>
<groupId>org.codehaus.mojo</groupId> <groupId>org.codehaus.mojo</groupId>
<artifactId>build-helper-maven-plugin</artifactId> <artifactId>build-helper-maven-plugin</artifactId>
<version>3.0.0</version> <version>${build-helper-maven-plugin.version}</version>
<executions> <executions>
<execution> <execution>
<phase>generate-sources</phase> <phase>generate-sources</phase>
@ -250,7 +275,7 @@
<plugin> <plugin>
<groupId>org.apache.maven.plugins</groupId> <groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-jar-plugin</artifactId> <artifactId>maven-jar-plugin</artifactId>
<version>3.2.0</version> <version>${maven-jar-plugin.version}</version>
<configuration> <configuration>
<archive> <archive>
<manifest> <manifest>
@ -268,13 +293,12 @@
<plugin> <plugin>
<groupId>org.apache.maven.plugins</groupId> <groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-shade-plugin</artifactId> <artifactId>maven-shade-plugin</artifactId>
<version>2.4.3</version> <version>${maven-shade-plugin.version}</version>
<configuration> <configuration>
<createDependencyReducedPom>false</createDependencyReducedPom> <createDependencyReducedPom>false</createDependencyReducedPom>
<artifactSet> <artifactSet>
<excludes> <excludes>
<!-- Don't include original swagger-UI as we're including our own <!-- Don't include original swagger-UI as we're including our own modified version -->
modified version -->
<exclude>org.webjars:swagger-ui</exclude> <exclude>org.webjars:swagger-ui</exclude>
<!-- Don't include JUnit as it's for testing only! --> <!-- Don't include JUnit as it's for testing only! -->
<exclude>junit:junit</exclude> <exclude>junit:junit</exclude>
@ -318,7 +342,7 @@
<plugin> <plugin>
<groupId>io.github.zlika</groupId> <groupId>io.github.zlika</groupId>
<artifactId>reproducible-build-maven-plugin</artifactId> <artifactId>reproducible-build-maven-plugin</artifactId>
<version>0.11</version> <version>${reproducible-build-maven-plugin.version}</version>
<executions> <executions>
<execution> <execution>
<phase>package</phase> <phase>package</phase>
@ -335,7 +359,7 @@
<plugin> <plugin>
<groupId>org.apache.maven.plugins</groupId> <groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-surefire-plugin</artifactId> <artifactId>maven-surefire-plugin</artifactId>
<version>2.22.2</version> <version>${maven-surefire-plugin.version}</version>
<configuration> <configuration>
<skipTests>${skipTests}</skipTests> <skipTests>${skipTests}</skipTests>
</configuration> </configuration>
@ -347,7 +371,7 @@
<plugin> <plugin>
<groupId>org.eclipse.m2e</groupId> <groupId>org.eclipse.m2e</groupId>
<artifactId>lifecycle-mapping</artifactId> <artifactId>lifecycle-mapping</artifactId>
<version>1.0.0</version> <version>${lifecycle-mapping.version}</version>
<configuration> <configuration>
<lifecycleMappingMetadata> <lifecycleMappingMetadata>
<pluginExecutions> <pluginExecutions>
@ -360,14 +384,14 @@
maven-dependency-plugin maven-dependency-plugin
</artifactId> </artifactId>
<versionRange> <versionRange>
[2.8,) [3.6.0,)
</versionRange> </versionRange>
<goals> <goals>
<goal>unpack</goal> <goal>unpack</goal>
</goals> </goals>
</pluginExecutionFilter> </pluginExecutionFilter>
<action> <action>
<execute></execute> <execute/>
</action> </action>
</pluginExecution> </pluginExecution>
<pluginExecution> <pluginExecution>
@ -386,7 +410,7 @@
</goals> </goals>
</pluginExecutionFilter> </pluginExecutionFilter>
<action> <action>
<execute></execute> <execute/>
</action> </action>
</pluginExecution> </pluginExecution>
</pluginExecutions> </pluginExecutions>
@ -413,15 +437,17 @@
<dependency> <dependency>
<groupId>org.codehaus.mojo</groupId> <groupId>org.codehaus.mojo</groupId>
<artifactId>build-helper-maven-plugin</artifactId> <artifactId>build-helper-maven-plugin</artifactId>
<version>3.0.0</version> <version>${build-helper-maven-plugin.version}</version>
<scope>provided</scope><!-- needed for build, not for runtime --> <scope>provided</scope>
<!-- needed for build, not for runtime -->
</dependency> </dependency>
<!-- https://mvnrepository.com/artifact/com.github.bohnman/package-info-maven-plugin --> <!-- https://mvnrepository.com/artifact/com.github.bohnman/package-info-maven-plugin -->
<dependency> <dependency>
<groupId>com.github.bohnman</groupId> <groupId>com.github.bohnman</groupId>
<artifactId>package-info-maven-plugin</artifactId> <artifactId>package-info-maven-plugin</artifactId>
<version>${package-info-maven-plugin.version}</version> <version>${package-info-maven-plugin.version}</version>
<scope>provided</scope><!-- needed for build, not for runtime --> <scope>provided</scope>
<!-- needed for build, not for runtime -->
</dependency> </dependency>
<!-- HSQLDB for repository --> <!-- HSQLDB for repository -->
<dependency> <dependency>
@ -457,12 +483,12 @@
<dependency> <dependency>
<groupId>com.googlecode.json-simple</groupId> <groupId>com.googlecode.json-simple</groupId>
<artifactId>json-simple</artifactId> <artifactId>json-simple</artifactId>
<version>1.1.1</version> <version>${json-simple.version}</version>
</dependency> </dependency>
<dependency> <dependency>
<groupId>org.json</groupId> <groupId>org.json</groupId>
<artifactId>json</artifactId> <artifactId>json</artifactId>
<version>20210307</version> <version>${json.version}</version>
</dependency> </dependency>
<dependency> <dependency>
<groupId>org.apache.commons</groupId> <groupId>org.apache.commons</groupId>
@ -493,7 +519,7 @@
<dependency> <dependency>
<groupId>io.druid</groupId> <groupId>io.druid</groupId>
<artifactId>extendedset</artifactId> <artifactId>extendedset</artifactId>
<version>0.12.3</version> <version>${extendedset.version}</version>
<exclusions> <exclusions>
<!-- exclude old versions of jackson-annotations / jackson-core --> <!-- exclude old versions of jackson-annotations / jackson-core -->
<exclusion> <exclusion>
@ -564,12 +590,12 @@
<dependency> <dependency>
<groupId>javax.servlet</groupId> <groupId>javax.servlet</groupId>
<artifactId>javax.servlet-api</artifactId> <artifactId>javax.servlet-api</artifactId>
<version>4.0.1</version> <version>${javax.servlet-api.version}</version>
</dependency> </dependency>
<dependency> <dependency>
<groupId>javax.mail</groupId> <groupId>javax.mail</groupId>
<artifactId>mail</artifactId> <artifactId>mail</artifactId>
<version>1.5.0-b01</version> <version>${mail.version}</version>
</dependency> </dependency>
<!-- Unicode homoglyph utilities --> <!-- Unicode homoglyph utilities -->
<dependency> <dependency>
@ -638,7 +664,8 @@
<artifactId>jersey-hk2</artifactId> <artifactId>jersey-hk2</artifactId>
<version>${jersey.version}</version> <version>${jersey.version}</version>
<exclusions> <exclusions>
<exclusion><!-- exclude javax.inject-1.jar because other jersey modules include javax.inject v2+ --> <exclusion>
<!-- exclude javax.inject-1.jar because other jersey modules include javax.inject v2+ -->
<groupId>javax.inject</groupId> <groupId>javax.inject</groupId>
<artifactId>javax.inject</artifactId> <artifactId>javax.inject</artifactId>
</exclusion> </exclusion>
@ -665,7 +692,8 @@
<artifactId>swagger-jaxrs2-servlet-initializer</artifactId> <artifactId>swagger-jaxrs2-servlet-initializer</artifactId>
<version>${swagger-api.version}</version> <version>${swagger-api.version}</version>
<exclusions> <exclusions>
<exclusion><!-- excluded because included in swagger-jaxrs2-servlet-initializer --> <exclusion>
<!-- excluded because included in swagger-jaxrs2-servlet-initializer -->
<groupId>io.swagger.core.v3</groupId> <groupId>io.swagger.core.v3</groupId>
<artifactId>swagger-integration</artifactId> <artifactId>swagger-integration</artifactId>
</exclusion> </exclusion>
@ -681,12 +709,12 @@
<dependency> <dependency>
<groupId>org.junit.jupiter</groupId> <groupId>org.junit.jupiter</groupId>
<artifactId>junit-jupiter-engine</artifactId> <artifactId>junit-jupiter-engine</artifactId>
<version>5.3.1</version> <version>${junit-jupiter-engine.version}</version>
</dependency> </dependency>
<dependency> <dependency>
<groupId>org.hamcrest</groupId> <groupId>org.hamcrest</groupId>
<artifactId>hamcrest-library</artifactId> <artifactId>hamcrest-library</artifactId>
<version>1.3</version> <version>${hamcrest-library.version}</version>
</dependency> </dependency>
--> -->
<!-- BouncyCastle for crypto, including TLS secure networking --> <!-- BouncyCastle for crypto, including TLS secure networking -->
@ -735,5 +763,11 @@
<artifactId>simplemagic</artifactId> <artifactId>simplemagic</artifactId>
<version>${simplemagic.version}</version> <version>${simplemagic.version}</version>
</dependency> </dependency>
<!-- JAXB runtime for WADL support -->
<dependency>
<groupId>org.glassfish.jaxb</groupId>
<artifactId>jaxb-runtime</artifactId>
<version>${jaxb-runtime.version}</version>
</dependency>
</dependencies> </dependencies>
</project> </project>

View File

@ -0,0 +1,102 @@
package org.qortal.api.model;
import io.swagger.v3.oas.annotations.media.Schema;
import javax.xml.bind.annotation.XmlAccessType;
import javax.xml.bind.annotation.XmlAccessorType;
import javax.xml.bind.annotation.XmlTransient;
import org.bouncycastle.util.encoders.Base64;
import org.bouncycastle.util.encoders.DecoderException;
@XmlAccessorType(XmlAccessType.FIELD)
public class AtCreationRequest {
@Schema(description = "CIYAM AT version", example = "2")
private short ciyamAtVersion;
@Schema(description = "base64-encoded code bytes", example = "")
private String codeBytesBase64;
@Schema(description = "base64-encoded data bytes", example = "")
private String dataBytesBase64;
private short numCallStackPages;
private short numUserStackPages;
private long minActivationAmount;
// Default constructor for JSON deserialization
public AtCreationRequest() {}
// Getters and setters
public short getCiyamAtVersion() {
return ciyamAtVersion;
}
public void setCiyamAtVersion(short ciyamAtVersion) {
this.ciyamAtVersion = ciyamAtVersion;
}
public String getCodeBytesBase64() {
return this.codeBytesBase64;
}
@XmlTransient
@Schema(hidden = true)
public byte[] getCodeBytes() {
if (this.codeBytesBase64 != null) {
try {
return Base64.decode(this.codeBytesBase64);
}
catch (DecoderException e) {
return null;
}
}
return null;
}
public String getDataBytesBase64() {
return this.dataBytesBase64;
}
@XmlTransient
@Schema(hidden = true)
public byte[] getDataBytes() {
if (this.dataBytesBase64 != null) {
try {
return Base64.decode(this.dataBytesBase64);
}
catch (DecoderException e) {
return null;
}
}
return null;
}
public short getNumCallStackPages() {
return numCallStackPages;
}
public void setNumCallStackPages(short numCallStackPages) {
this.numCallStackPages = numCallStackPages;
}
public short getNumUserStackPages() {
return numUserStackPages;
}
public void setNumUserStackPages(short numUserStackPages) {
this.numUserStackPages = numUserStackPages;
}
public long getMinActivationAmount() {
return minActivationAmount;
}
public void setMinActivationAmount(long minActivationAmount) {
this.minActivationAmount = minActivationAmount;
}
}

View File

@ -706,7 +706,7 @@ public class ArbitraryResource {
ArbitraryDataResource resource = new ArbitraryDataResource(name, ResourceIdType.NAME, service, identifier); ArbitraryDataResource resource = new ArbitraryDataResource(name, ResourceIdType.NAME, service, identifier);
try { try {
ArbitraryDataTransactionMetadata transactionMetadata = ArbitraryMetadataManager.getInstance().fetchMetadata(resource, false); ArbitraryDataTransactionMetadata transactionMetadata = ArbitraryMetadataManager.getInstance().fetchMetadata(resource, true);
if (transactionMetadata != null) { if (transactionMetadata != null) {
ArbitraryResourceMetadata resourceMetadata = ArbitraryResourceMetadata.fromTransactionMetadata(transactionMetadata, true); ArbitraryResourceMetadata resourceMetadata = ArbitraryResourceMetadata.fromTransactionMetadata(transactionMetadata, true);
if (resourceMetadata != null) { if (resourceMetadata != null) {

View File

@ -27,6 +27,7 @@ import org.qortal.api.ApiException;
import org.qortal.api.ApiExceptionFactory; import org.qortal.api.ApiExceptionFactory;
import org.qortal.data.at.ATData; import org.qortal.data.at.ATData;
import org.qortal.data.at.ATStateData; import org.qortal.data.at.ATStateData;
import org.qortal.api.model.AtCreationRequest;
import org.qortal.data.transaction.DeployAtTransactionData; import org.qortal.data.transaction.DeployAtTransactionData;
import org.qortal.repository.DataException; import org.qortal.repository.DataException;
import org.qortal.repository.Repository; import org.qortal.repository.Repository;
@ -38,9 +39,14 @@ import org.qortal.transform.TransformationException;
import org.qortal.transform.transaction.DeployAtTransactionTransformer; import org.qortal.transform.transaction.DeployAtTransactionTransformer;
import org.qortal.utils.Base58; import org.qortal.utils.Base58;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
@Path("/at") @Path("/at")
@Tag(name = "Automated Transactions") @Tag(name = "Automated Transactions")
public class AtResource { public class AtResource {
private static final Logger logger = LoggerFactory.getLogger(AtResource.class);
@Context @Context
HttpServletRequest request; HttpServletRequest request;
@ -156,6 +162,52 @@ public class AtResource {
} }
} }
@POST
@Path("/create")
@Operation(
summary = "Create base58-encoded AT creation bytes from the provided parameters",
requestBody = @RequestBody(
required = true,
content = @Content(
mediaType = MediaType.APPLICATION_JSON,
schema = @Schema(
implementation = AtCreationRequest.class
)
)
),
responses = {
@ApiResponse(
description = "AT creation bytes suitable for use in a DEPLOY_AT transaction",
content = @Content(
mediaType = MediaType.TEXT_PLAIN,
schema = @Schema(
type = "string"
)
)
)
}
)
public String create(AtCreationRequest atCreationRequest) {
if (atCreationRequest.getCiyamAtVersion() < 2) {
throw ApiExceptionFactory.INSTANCE.createCustomException(request, ApiError.INVALID_CRITERIA, "ciyamAtVersion must be at least 2");
}
if (atCreationRequest.getCodeBytes() == null) {
throw ApiExceptionFactory.INSTANCE.createCustomException(request, ApiError.INVALID_CRITERIA, "Valid codeBytesBase64 must be supplied");
}
if (atCreationRequest.getDataBytes() == null) {
throw ApiExceptionFactory.INSTANCE.createCustomException(request, ApiError.INVALID_CRITERIA, "Valid dataBytesBase64 must be supplied");
}
byte[] creationBytes = MachineState.toCreationBytes(
atCreationRequest.getCiyamAtVersion(),
atCreationRequest.getCodeBytes(),
atCreationRequest.getDataBytes(),
atCreationRequest.getNumCallStackPages(),
atCreationRequest.getNumUserStackPages(),
atCreationRequest.getMinActivationAmount()
);
return Base58.encode(creationBytes);
}
@POST @POST
@Operation( @Operation(
summary = "Build raw, unsigned, DEPLOY_AT transaction", summary = "Build raw, unsigned, DEPLOY_AT transaction",

View File

@ -16,11 +16,6 @@ import java.net.InetSocketAddress;
import java.net.UnknownHostException; import java.net.UnknownHostException;
import java.nio.file.Files; import java.nio.file.Files;
import java.nio.file.Paths; import java.nio.file.Paths;
import java.time.LocalDate;
import java.time.LocalTime;
import java.time.OffsetDateTime;
import java.time.ZoneOffset;
import java.util.Arrays;
import java.util.List; import java.util.List;
import java.util.concurrent.TimeUnit; import java.util.concurrent.TimeUnit;
import java.util.concurrent.TimeoutException; import java.util.concurrent.TimeoutException;
@ -38,7 +33,6 @@ import org.apache.logging.log4j.Logger;
import org.apache.logging.log4j.core.LoggerContext; import org.apache.logging.log4j.core.LoggerContext;
import org.apache.logging.log4j.core.appender.RollingFileAppender; import org.apache.logging.log4j.core.appender.RollingFileAppender;
import org.json.JSONArray; import org.json.JSONArray;
import org.json.JSONObject;
import org.qortal.account.Account; import org.qortal.account.Account;
import org.qortal.account.PrivateKeyAccount; import org.qortal.account.PrivateKeyAccount;
import org.qortal.api.*; import org.qortal.api.*;
@ -269,7 +263,7 @@ public class AdminResource {
@GET @GET
@Path("/summary") @Path("/summary")
@Operation( @Operation(
summary = "Summary of activity since midnight, UTC", summary = "Summary of activity past 24 hours",
responses = { responses = {
@ApiResponse( @ApiResponse(
content = @Content(schema = @Schema(implementation = ActivitySummary.class)) content = @Content(schema = @Schema(implementation = ActivitySummary.class))
@ -282,23 +276,21 @@ public class AdminResource {
Security.checkApiCallAllowed(request); Security.checkApiCallAllowed(request);
ActivitySummary summary = new ActivitySummary(); ActivitySummary summary = new ActivitySummary();
LocalDate date = LocalDate.now(); long now = NTP.getTime();
LocalTime time = LocalTime.of(0, 0); long oneday = now - 24 * 60 * 60 * 1000L;
ZoneOffset offset = ZoneOffset.UTC;
long start = OffsetDateTime.of(date, time, offset).toInstant().toEpochMilli();
try (final Repository repository = RepositoryManager.getRepository()) { try (final Repository repository = RepositoryManager.getRepository()) {
int startHeight = repository.getBlockRepository().getHeightFromTimestamp(start); int startHeight = repository.getBlockRepository().getHeightFromTimestamp(oneday);
int endHeight = repository.getBlockRepository().getBlockchainHeight(); int endHeight = repository.getBlockRepository().getBlockchainHeight();
summary.setBlockCount(endHeight - startHeight); summary.setBlockCount(endHeight - startHeight);
summary.setTransactionCountByType(repository.getTransactionRepository().getTransactionSummary(startHeight + 1, endHeight)); summary.setTransactionCountByType(repository.getTransactionRepository().getTransactionSummary(startHeight + 1, endHeight));
summary.setAssetsIssued(repository.getAssetRepository().getRecentAssetIds(start).size()); summary.setAssetsIssued(repository.getAssetRepository().getRecentAssetIds(oneday).size());
summary.setNamesRegistered (repository.getNameRepository().getRecentNames(start).size()); summary.setNamesRegistered (repository.getNameRepository().getRecentNames(oneday).size());
return summary; return summary;
} catch (DataException e) { } catch (DataException e) {

View File

@ -129,6 +129,11 @@ public class ArbitraryDataRenderer {
String filename = this.getFilename(unzippedPath, inPath); String filename = this.getFilename(unzippedPath, inPath);
Path filePath = Paths.get(unzippedPath, filename); Path filePath = Paths.get(unzippedPath, filename);
boolean usingCustomRouting = false; boolean usingCustomRouting = false;
if (Files.isDirectory(filePath) && (!inPath.endsWith("/"))) {
inPath = inPath + "/";
filename = this.getFilename(unzippedPath, inPath);
filePath = Paths.get(unzippedPath, filename);
}
// If the file doesn't exist, we may need to route the request elsewhere, or cleanup // If the file doesn't exist, we may need to route the request elsewhere, or cleanup
if (!Files.exists(filePath)) { if (!Files.exists(filePath)) {

View File

@ -84,6 +84,7 @@ public class Block {
TRANSACTION_PROCESSING_FAILED(53), TRANSACTION_PROCESSING_FAILED(53),
TRANSACTION_ALREADY_PROCESSED(54), TRANSACTION_ALREADY_PROCESSED(54),
TRANSACTION_NEEDS_APPROVAL(55), TRANSACTION_NEEDS_APPROVAL(55),
TRANSACTION_NOT_CONFIRMABLE(56),
AT_STATES_MISMATCH(61), AT_STATES_MISMATCH(61),
ONLINE_ACCOUNTS_INVALID(70), ONLINE_ACCOUNTS_INVALID(70),
ONLINE_ACCOUNT_UNKNOWN(71), ONLINE_ACCOUNT_UNKNOWN(71),
@ -130,6 +131,9 @@ public class Block {
/** Locally-generated AT fees */ /** Locally-generated AT fees */
protected long ourAtFees; // Generated locally protected long ourAtFees; // Generated locally
/** Cached online accounts validation decision, to avoid revalidating when true */
private boolean onlineAccountsAlreadyValid = false;
@FunctionalInterface @FunctionalInterface
private interface BlockRewardDistributor { private interface BlockRewardDistributor {
long distribute(long amount, Map<String, Long> balanceChanges) throws DataException; long distribute(long amount, Map<String, Long> balanceChanges) throws DataException;
@ -563,6 +567,13 @@ public class Block {
} }
/**
* Force online accounts to be revalidated, e.g. at final stage of block minting.
*/
public void clearOnlineAccountsValidationCache() {
this.onlineAccountsAlreadyValid = false;
}
// More information // More information
/** /**
@ -1043,6 +1054,10 @@ public class Block {
if (this.blockData.getHeight() != null && this.blockData.getHeight() == 1) if (this.blockData.getHeight() != null && this.blockData.getHeight() == 1)
return ValidationResult.OK; return ValidationResult.OK;
// Don't bother revalidating if accounts have already been validated in this block
if (this.onlineAccountsAlreadyValid)
return ValidationResult.OK;
// Expand block's online accounts indexes into actual accounts // Expand block's online accounts indexes into actual accounts
ConciseSet accountIndexes = BlockTransformer.decodeOnlineAccounts(this.blockData.getEncodedOnlineAccounts()); ConciseSet accountIndexes = BlockTransformer.decodeOnlineAccounts(this.blockData.getEncodedOnlineAccounts());
// We use count of online accounts to validate decoded account indexes // We use count of online accounts to validate decoded account indexes
@ -1130,6 +1145,9 @@ public class Block {
// All online accounts valid, so save our list of online accounts for potential later use // All online accounts valid, so save our list of online accounts for potential later use
this.cachedOnlineRewardShares = onlineRewardShares; this.cachedOnlineRewardShares = onlineRewardShares;
// Remember that the accounts are valid, to speed up subsequent checks
this.onlineAccountsAlreadyValid = true;
return ValidationResult.OK; return ValidationResult.OK;
} }
@ -1234,6 +1252,13 @@ public class Block {
|| transaction.getDeadline() <= this.blockData.getTimestamp()) || transaction.getDeadline() <= this.blockData.getTimestamp())
return ValidationResult.TRANSACTION_TIMESTAMP_INVALID; return ValidationResult.TRANSACTION_TIMESTAMP_INVALID;
// After feature trigger, check that this transaction is confirmable
if (transactionData.getTimestamp() >= BlockChain.getInstance().getMemPoWTransactionUpdatesTimestamp()) {
if (!transaction.isConfirmable()) {
return ValidationResult.TRANSACTION_NOT_CONFIRMABLE;
}
}
// Check transaction isn't already included in a block // Check transaction isn't already included in a block
if (this.repository.getTransactionRepository().isConfirmed(transactionData.getSignature())) if (this.repository.getTransactionRepository().isConfirmed(transactionData.getSignature()))
return ValidationResult.TRANSACTION_ALREADY_PROCESSED; return ValidationResult.TRANSACTION_ALREADY_PROCESSED;

View File

@ -48,9 +48,6 @@ public class BlockChain {
/** Transaction expiry period, starting from transaction's timestamp, in milliseconds. */ /** Transaction expiry period, starting from transaction's timestamp, in milliseconds. */
private long transactionExpiryPeriod; private long transactionExpiryPeriod;
@XmlJavaTypeAdapter(value = org.qortal.api.AmountTypeAdapter.class)
private long unitFee;
private int maxBytesPerUnitFee; private int maxBytesPerUnitFee;
/** Maximum acceptable timestamp disagreement offset in milliseconds. */ /** Maximum acceptable timestamp disagreement offset in milliseconds. */
@ -89,6 +86,7 @@ public class BlockChain {
@XmlJavaTypeAdapter(value = org.qortal.api.AmountTypeAdapter.class) @XmlJavaTypeAdapter(value = org.qortal.api.AmountTypeAdapter.class)
public long fee; public long fee;
} }
private List<UnitFeesByTimestamp> unitFees;
private List<UnitFeesByTimestamp> nameRegistrationUnitFees; private List<UnitFeesByTimestamp> nameRegistrationUnitFees;
/** Map of which blockchain features are enabled when (height/timestamp) */ /** Map of which blockchain features are enabled when (height/timestamp) */
@ -211,6 +209,9 @@ public class BlockChain {
/** Snapshot timestamp for self sponsorship algo V1 */ /** Snapshot timestamp for self sponsorship algo V1 */
private long selfSponsorshipAlgoV1SnapshotTimestamp; private long selfSponsorshipAlgoV1SnapshotTimestamp;
/** Feature-trigger timestamp to modify behaviour of various transactions that support mempow */
private long mempowTransactionUpdatesTimestamp;
/** Max reward shares by block height */ /** Max reward shares by block height */
public static class MaxRewardSharesByTimestamp { public static class MaxRewardSharesByTimestamp {
public long timestamp; public long timestamp;
@ -346,10 +347,6 @@ public class BlockChain {
return this.isTestChain; return this.isTestChain;
} }
public long getUnitFee() {
return this.unitFee;
}
public int getMaxBytesPerUnitFee() { public int getMaxBytesPerUnitFee() {
return this.maxBytesPerUnitFee; return this.maxBytesPerUnitFee;
} }
@ -376,6 +373,11 @@ public class BlockChain {
return this.selfSponsorshipAlgoV1SnapshotTimestamp; return this.selfSponsorshipAlgoV1SnapshotTimestamp;
} }
// Feature-trigger timestamp to modify behaviour of various transactions that support mempow
public long getMemPoWTransactionUpdatesTimestamp() {
return this.mempowTransactionUpdatesTimestamp;
}
/** Returns true if approval-needing transaction types require a txGroupId other than NO_GROUP. */ /** Returns true if approval-needing transaction types require a txGroupId other than NO_GROUP. */
public boolean getRequireGroupForApproval() { public boolean getRequireGroupForApproval() {
return this.requireGroupForApproval; return this.requireGroupForApproval;
@ -547,13 +549,22 @@ public class BlockChain {
throw new IllegalStateException(String.format("No block timing info available for height %d", ourHeight)); throw new IllegalStateException(String.format("No block timing info available for height %d", ourHeight));
} }
public long getUnitFeeAtTimestamp(long ourTimestamp) {
for (int i = unitFees.size() - 1; i >= 0; --i)
if (unitFees.get(i).timestamp <= ourTimestamp)
return unitFees.get(i).fee;
// Shouldn't happen, but set a sensible default just in case
return 100000;
}
public long getNameRegistrationUnitFeeAtTimestamp(long ourTimestamp) { public long getNameRegistrationUnitFeeAtTimestamp(long ourTimestamp) {
for (int i = nameRegistrationUnitFees.size() - 1; i >= 0; --i) for (int i = nameRegistrationUnitFees.size() - 1; i >= 0; --i)
if (nameRegistrationUnitFees.get(i).timestamp <= ourTimestamp) if (nameRegistrationUnitFees.get(i).timestamp <= ourTimestamp)
return nameRegistrationUnitFees.get(i).fee; return nameRegistrationUnitFees.get(i).fee;
// Default to system-wide unit fee // Shouldn't happen, but set a sensible default just in case
return this.getUnitFee(); return 100000;
} }
public int getMaxRewardSharesAtTimestamp(long ourTimestamp) { public int getMaxRewardSharesAtTimestamp(long ourTimestamp) {

View File

@ -562,6 +562,9 @@ public class BlockMinter extends Thread {
// Sign to create block's signature // Sign to create block's signature
newBlock.sign(); newBlock.sign();
// Ensure online accounts are fully re-validated in this final check
newBlock.clearOnlineAccountsValidationCache();
// Is newBlock still valid? // Is newBlock still valid?
ValidationResult validationResult = newBlock.isValid(); ValidationResult validationResult = newBlock.isValid();
if (validationResult != ValidationResult.OK) if (validationResult != ValidationResult.OK)

View File

@ -229,13 +229,6 @@ public class Synchronizer extends Thread {
peers.removeIf(Controller.hasOldVersion); peers.removeIf(Controller.hasOldVersion);
checkRecoveryModeForPeers(peers); checkRecoveryModeForPeers(peers);
if (recoveryMode) {
// Needs a mutable copy of the unmodifiableList
peers = new ArrayList<>(Network.getInstance().getImmutableHandshakedPeers());
peers.removeIf(Controller.hasOnlyGenesisBlock);
peers.removeIf(Controller.hasMisbehaved);
peers.removeIf(Controller.hasOldVersion);
}
// Check we have enough peers to potentially synchronize // Check we have enough peers to potentially synchronize
if (peers.size() < Settings.getInstance().getMinBlockchainPeers()) if (peers.size() < Settings.getInstance().getMinBlockchainPeers())
@ -262,10 +255,7 @@ public class Synchronizer extends Thread {
peers.removeIf(Controller.hasInferiorChainTip); peers.removeIf(Controller.hasInferiorChainTip);
// Remove any peers that are no longer on a recent block since the last check // Remove any peers that are no longer on a recent block since the last check
// Except for times when we're in recovery mode, in which case we need to keep them peers.removeIf(Controller.hasNoRecentBlock);
if (!recoveryMode) {
peers.removeIf(Controller.hasNoRecentBlock);
}
final int peersRemoved = peersBeforeComparison - peers.size(); final int peersRemoved = peersBeforeComparison - peers.size();
if (peersRemoved > 0 && peers.size() > 0) if (peersRemoved > 0 && peers.size() > 0)
@ -1340,8 +1330,8 @@ public class Synchronizer extends Thread {
return SynchronizationResult.INVALID_DATA; return SynchronizationResult.INVALID_DATA;
} }
// Final check to make sure the peer isn't out of date (except for when we're in recovery mode) // Final check to make sure the peer isn't out of date
if (!recoveryMode && peer.getChainTipData() != null) { if (peer.getChainTipData() != null) {
final Long minLatestBlockTimestamp = Controller.getMinimumLatestBlockTimestamp(); final Long minLatestBlockTimestamp = Controller.getMinimumLatestBlockTimestamp();
final Long peerLastBlockTimestamp = peer.getChainTipData().getTimestamp(); final Long peerLastBlockTimestamp = peer.getChainTipData().getTimestamp();
if (peerLastBlockTimestamp == null || peerLastBlockTimestamp < minLatestBlockTimestamp) { if (peerLastBlockTimestamp == null || peerLastBlockTimestamp < minLatestBlockTimestamp) {

View File

@ -47,6 +47,9 @@ public class TransactionImporter extends Thread {
/** Map of recent invalid unconfirmed transactions. Key is base58 transaction signature, value is do-not-request expiry timestamp. */ /** Map of recent invalid unconfirmed transactions. Key is base58 transaction signature, value is do-not-request expiry timestamp. */
private final Map<String, Long> invalidUnconfirmedTransactions = Collections.synchronizedMap(new HashMap<>()); private final Map<String, Long> invalidUnconfirmedTransactions = Collections.synchronizedMap(new HashMap<>());
/** Cached list of unconfirmed transactions, used when counting per creator. This is replaced regularly */
public static List<TransactionData> unconfirmedTransactionsCache = null;
public static synchronized TransactionImporter getInstance() { public static synchronized TransactionImporter getInstance() {
if (instance == null) { if (instance == null) {
@ -215,12 +218,6 @@ public class TransactionImporter extends Thread {
LOGGER.debug("Finished validating signatures in incoming transactions queue (valid this round: {}, total pending import: {})...", validatedCount, sigValidTransactions.size()); LOGGER.debug("Finished validating signatures in incoming transactions queue (valid this round: {}, total pending import: {})...", validatedCount, sigValidTransactions.size());
} }
if (!newlyValidSignatures.isEmpty()) {
LOGGER.debug("Broadcasting {} newly valid signatures ahead of import", newlyValidSignatures.size());
Message newTransactionSignatureMessage = new TransactionSignaturesMessage(newlyValidSignatures);
Network.getInstance().broadcast(broadcastPeer -> newTransactionSignatureMessage);
}
} catch (DataException e) { } catch (DataException e) {
LOGGER.error("Repository issue while processing incoming transactions", e); LOGGER.error("Repository issue while processing incoming transactions", e);
} }
@ -254,6 +251,15 @@ public class TransactionImporter extends Thread {
int processedCount = 0; int processedCount = 0;
try (final Repository repository = RepositoryManager.getRepository()) { try (final Repository repository = RepositoryManager.getRepository()) {
// Use a single copy of the unconfirmed transactions list for each cycle, to speed up constant lookups
// when counting unconfirmed transactions by creator.
List<TransactionData> unconfirmedTransactions = repository.getTransactionRepository().getUnconfirmedTransactions();
unconfirmedTransactions.removeIf(t -> t.getType() == Transaction.TransactionType.CHAT);
unconfirmedTransactionsCache = unconfirmedTransactions;
// A list of signatures were imported in this round
List<byte[]> newlyImportedSignatures = new ArrayList<>();
// Import transactions with valid signatures // Import transactions with valid signatures
try { try {
for (int i = 0; i < sigValidTransactions.size(); ++i) { for (int i = 0; i < sigValidTransactions.size(); ++i) {
@ -286,6 +292,15 @@ public class TransactionImporter extends Thread {
case OK: { case OK: {
LOGGER.debug(() -> String.format("Imported %s transaction %s", transactionData.getType().name(), Base58.encode(transactionData.getSignature()))); LOGGER.debug(() -> String.format("Imported %s transaction %s", transactionData.getType().name(), Base58.encode(transactionData.getSignature())));
// Add to the unconfirmed transactions cache
if (transactionData.getType() != Transaction.TransactionType.CHAT && unconfirmedTransactionsCache != null) {
unconfirmedTransactionsCache.add(transactionData);
}
// Signature imported in this round
newlyImportedSignatures.add(transactionData.getSignature());
break; break;
} }
@ -314,9 +329,18 @@ public class TransactionImporter extends Thread {
// Transaction has been processed, even if only to reject it // Transaction has been processed, even if only to reject it
removeIncomingTransaction(transactionData.getSignature()); removeIncomingTransaction(transactionData.getSignature());
} }
if (!newlyImportedSignatures.isEmpty()) {
LOGGER.debug("Broadcasting {} newly imported signatures", newlyImportedSignatures.size());
Message newTransactionSignatureMessage = new TransactionSignaturesMessage(newlyImportedSignatures);
Network.getInstance().broadcast(broadcastPeer -> newTransactionSignatureMessage);
}
} finally { } finally {
LOGGER.debug("Finished importing {} incoming transaction{}", processedCount, (processedCount == 1 ? "" : "s")); LOGGER.debug("Finished importing {} incoming transaction{}", processedCount, (processedCount == 1 ? "" : "s"));
blockchainLock.unlock(); blockchainLock.unlock();
// Clear the unconfirmed transaction cache so new data can be populated in the next cycle
unconfirmedTransactionsCache = null;
} }
} catch (DataException e) { } catch (DataException e) {
LOGGER.error("Repository issue while importing incoming transactions", e); LOGGER.error("Repository issue while importing incoming transactions", e);

View File

@ -276,7 +276,10 @@ public class ArbitraryDataManager extends Thread {
int offset = 0; int offset = 0;
while (!isStopping) { while (!isStopping) {
Thread.sleep(1000L); final int minSeconds = 3;
final int maxSeconds = 10;
final int randomSleepTime = new Random().nextInt((maxSeconds - minSeconds + 1)) + minSeconds;
Thread.sleep(randomSleepTime * 1000L);
// Any arbitrary transactions we want to fetch data for? // Any arbitrary transactions we want to fetch data for?
try (final Repository repository = RepositoryManager.getRepository()) { try (final Repository repository = RepositoryManager.getRepository()) {

View File

@ -19,6 +19,7 @@ import org.qortal.data.transaction.MessageTransactionData;
import org.qortal.group.Group; import org.qortal.group.Group;
import org.qortal.repository.DataException; import org.qortal.repository.DataException;
import org.qortal.repository.Repository; import org.qortal.repository.Repository;
import org.qortal.repository.RepositoryManager;
import org.qortal.transaction.DeployAtTransaction; import org.qortal.transaction.DeployAtTransaction;
import org.qortal.transaction.MessageTransaction; import org.qortal.transaction.MessageTransaction;
import org.qortal.transaction.Transaction.ValidationResult; import org.qortal.transaction.Transaction.ValidationResult;
@ -317,20 +318,36 @@ public class BitcoinACCTv3TradeBot implements AcctTradeBot {
boolean isMessageAlreadySent = repository.getMessageRepository().exists(tradeBotData.getTradeNativePublicKey(), messageRecipient, messageData); boolean isMessageAlreadySent = repository.getMessageRepository().exists(tradeBotData.getTradeNativePublicKey(), messageRecipient, messageData);
if (!isMessageAlreadySent) { if (!isMessageAlreadySent) {
PrivateKeyAccount sender = new PrivateKeyAccount(repository, tradeBotData.getTradePrivateKey()); // Do this in a new thread so caller doesn't have to wait for computeNonce()
MessageTransaction messageTransaction = MessageTransaction.build(repository, sender, Group.NO_GROUP, messageRecipient, messageData, false, false); // In the unlikely event that the transaction doesn't validate then the buy won't happen and eventually Alice's AT will be refunded
new Thread(() -> {
try (final Repository threadsRepository = RepositoryManager.getRepository()) {
PrivateKeyAccount sender = new PrivateKeyAccount(threadsRepository, tradeBotData.getTradePrivateKey());
MessageTransaction messageTransaction = MessageTransaction.build(threadsRepository, sender, Group.NO_GROUP, messageRecipient, messageData, false, false);
messageTransaction.computeNonce(); LOGGER.info("Computing nonce at difficulty {} for AT {} and recipient {}", messageTransaction.getPoWDifficulty(), tradeBotData.getAtAddress(), messageRecipient);
messageTransaction.sign(sender); messageTransaction.computeNonce();
MessageTransactionData newMessageTransactionData = (MessageTransactionData) messageTransaction.getTransactionData();
LOGGER.info("Computed nonce {} at difficulty {}", newMessageTransactionData.getNonce(), messageTransaction.getPoWDifficulty());
messageTransaction.sign(sender);
// reset repository state to prevent deadlock // reset repository state to prevent deadlock
repository.discardChanges(); threadsRepository.discardChanges();
ValidationResult result = messageTransaction.importAsUnconfirmed();
if (result != ValidationResult.OK) { if (messageTransaction.isSignatureValid()) {
LOGGER.warn(() -> String.format("Unable to send MESSAGE to Bob's trade-bot %s: %s", messageRecipient, result.name())); ValidationResult result = messageTransaction.importAsUnconfirmed();
return ResponseResult.NETWORK_ISSUE;
} if (result != ValidationResult.OK) {
LOGGER.warn(() -> String.format("Unable to send MESSAGE to Bob's trade-bot %s: %s", messageRecipient, result.name()));
}
}
else {
LOGGER.warn(() -> String.format("Unable to send MESSAGE to Bob's trade-bot %s: signature invalid", messageRecipient));
}
} catch (DataException e) {
LOGGER.warn(() -> String.format("Unable to send MESSAGE to Bob's trade-bot %s: %s", messageRecipient, e.getMessage()));
}
}, "TradeBot response").start();
} }
TradeBot.updateTradeBotState(repository, tradeBotData, () -> String.format("Funding P2SH-A %s. Messaged Bob. Waiting for AT-lock", p2shAddress)); TradeBot.updateTradeBotState(repository, tradeBotData, () -> String.format("Funding P2SH-A %s. Messaged Bob. Waiting for AT-lock", p2shAddress));
@ -548,15 +565,25 @@ public class BitcoinACCTv3TradeBot implements AcctTradeBot {
PrivateKeyAccount sender = new PrivateKeyAccount(repository, tradeBotData.getTradePrivateKey()); PrivateKeyAccount sender = new PrivateKeyAccount(repository, tradeBotData.getTradePrivateKey());
MessageTransaction outgoingMessageTransaction = MessageTransaction.build(repository, sender, Group.NO_GROUP, messageRecipient, outgoingMessageData, false, false); MessageTransaction outgoingMessageTransaction = MessageTransaction.build(repository, sender, Group.NO_GROUP, messageRecipient, outgoingMessageData, false, false);
LOGGER.info("Computing nonce at difficulty {} for AT {} and recipient {}", outgoingMessageTransaction.getPoWDifficulty(), tradeBotData.getAtAddress(), messageRecipient);
outgoingMessageTransaction.computeNonce(); outgoingMessageTransaction.computeNonce();
MessageTransactionData newMessageTransactionData = (MessageTransactionData) outgoingMessageTransaction.getTransactionData();
LOGGER.info("Computed nonce {} at difficulty {}", newMessageTransactionData.getNonce(), outgoingMessageTransaction.getPoWDifficulty());
outgoingMessageTransaction.sign(sender); outgoingMessageTransaction.sign(sender);
// reset repository state to prevent deadlock // reset repository state to prevent deadlock
repository.discardChanges(); repository.discardChanges();
ValidationResult result = outgoingMessageTransaction.importAsUnconfirmed();
if (result != ValidationResult.OK) { if (outgoingMessageTransaction.isSignatureValid()) {
LOGGER.warn(() -> String.format("Unable to send MESSAGE to AT %s: %s", messageRecipient, result.name())); ValidationResult result = outgoingMessageTransaction.importAsUnconfirmed();
if (result != ValidationResult.OK) {
LOGGER.warn(() -> String.format("Unable to send MESSAGE to AT %s: %s", messageRecipient, result.name()));
return;
}
}
else {
LOGGER.warn(() -> String.format("Unable to send MESSAGE to AT %s: signature invalid", messageRecipient));
return; return;
} }
} }
@ -668,15 +695,25 @@ public class BitcoinACCTv3TradeBot implements AcctTradeBot {
PrivateKeyAccount sender = new PrivateKeyAccount(repository, tradeBotData.getTradePrivateKey()); PrivateKeyAccount sender = new PrivateKeyAccount(repository, tradeBotData.getTradePrivateKey());
MessageTransaction messageTransaction = MessageTransaction.build(repository, sender, Group.NO_GROUP, messageRecipient, messageData, false, false); MessageTransaction messageTransaction = MessageTransaction.build(repository, sender, Group.NO_GROUP, messageRecipient, messageData, false, false);
LOGGER.info("Computing nonce at difficulty {} for AT {} and recipient {}", messageTransaction.getPoWDifficulty(), tradeBotData.getAtAddress(), messageRecipient);
messageTransaction.computeNonce(); messageTransaction.computeNonce();
MessageTransactionData newMessageTransactionData = (MessageTransactionData) messageTransaction.getTransactionData();
LOGGER.info("Computed nonce {} at difficulty {}", newMessageTransactionData.getNonce(), messageTransaction.getPoWDifficulty());
messageTransaction.sign(sender); messageTransaction.sign(sender);
// Reset repository state to prevent deadlock // Reset repository state to prevent deadlock
repository.discardChanges(); repository.discardChanges();
ValidationResult result = messageTransaction.importAsUnconfirmed();
if (result != ValidationResult.OK) { if (messageTransaction.isSignatureValid()) {
LOGGER.warn(() -> String.format("Unable to send MESSAGE to AT %s: %s", messageRecipient, result.name())); ValidationResult result = messageTransaction.importAsUnconfirmed();
if (result != ValidationResult.OK) {
LOGGER.warn(() -> String.format("Unable to send MESSAGE to AT %s: %s", messageRecipient, result.name()));
return;
}
}
else {
LOGGER.warn(() -> String.format("Unable to send MESSAGE to AT %s: signature invalid", messageRecipient));
return; return;
} }
} }

View File

@ -19,6 +19,7 @@ import org.qortal.data.transaction.MessageTransactionData;
import org.qortal.group.Group; import org.qortal.group.Group;
import org.qortal.repository.DataException; import org.qortal.repository.DataException;
import org.qortal.repository.Repository; import org.qortal.repository.Repository;
import org.qortal.repository.RepositoryManager;
import org.qortal.transaction.DeployAtTransaction; import org.qortal.transaction.DeployAtTransaction;
import org.qortal.transaction.MessageTransaction; import org.qortal.transaction.MessageTransaction;
import org.qortal.transaction.Transaction.ValidationResult; import org.qortal.transaction.Transaction.ValidationResult;
@ -317,20 +318,36 @@ public class DigibyteACCTv3TradeBot implements AcctTradeBot {
boolean isMessageAlreadySent = repository.getMessageRepository().exists(tradeBotData.getTradeNativePublicKey(), messageRecipient, messageData); boolean isMessageAlreadySent = repository.getMessageRepository().exists(tradeBotData.getTradeNativePublicKey(), messageRecipient, messageData);
if (!isMessageAlreadySent) { if (!isMessageAlreadySent) {
PrivateKeyAccount sender = new PrivateKeyAccount(repository, tradeBotData.getTradePrivateKey()); // Do this in a new thread so caller doesn't have to wait for computeNonce()
MessageTransaction messageTransaction = MessageTransaction.build(repository, sender, Group.NO_GROUP, messageRecipient, messageData, false, false); // In the unlikely event that the transaction doesn't validate then the buy won't happen and eventually Alice's AT will be refunded
new Thread(() -> {
try (final Repository threadsRepository = RepositoryManager.getRepository()) {
PrivateKeyAccount sender = new PrivateKeyAccount(threadsRepository, tradeBotData.getTradePrivateKey());
MessageTransaction messageTransaction = MessageTransaction.build(threadsRepository, sender, Group.NO_GROUP, messageRecipient, messageData, false, false);
messageTransaction.computeNonce(); LOGGER.info("Computing nonce at difficulty {} for AT {} and recipient {}", messageTransaction.getPoWDifficulty(), tradeBotData.getAtAddress(), messageRecipient);
messageTransaction.sign(sender); messageTransaction.computeNonce();
MessageTransactionData newMessageTransactionData = (MessageTransactionData) messageTransaction.getTransactionData();
LOGGER.info("Computed nonce {} at difficulty {}", newMessageTransactionData.getNonce(), messageTransaction.getPoWDifficulty());
messageTransaction.sign(sender);
// reset repository state to prevent deadlock // reset repository state to prevent deadlock
repository.discardChanges(); threadsRepository.discardChanges();
ValidationResult result = messageTransaction.importAsUnconfirmed();
if (result != ValidationResult.OK) { if (messageTransaction.isSignatureValid()) {
LOGGER.warn(() -> String.format("Unable to send MESSAGE to Bob's trade-bot %s: %s", messageRecipient, result.name())); ValidationResult result = messageTransaction.importAsUnconfirmed();
return ResponseResult.NETWORK_ISSUE;
} if (result != ValidationResult.OK) {
LOGGER.warn(() -> String.format("Unable to send MESSAGE to Bob's trade-bot %s: %s", messageRecipient, result.name()));
}
}
else {
LOGGER.warn(() -> String.format("Unable to send MESSAGE to Bob's trade-bot %s: signature invalid", messageRecipient));
}
} catch (DataException e) {
LOGGER.warn(() -> String.format("Unable to send MESSAGE to Bob's trade-bot %s: %s", messageRecipient, e.getMessage()));
}
}, "TradeBot response").start();
} }
TradeBot.updateTradeBotState(repository, tradeBotData, () -> String.format("Funding P2SH-A %s. Messaged Bob. Waiting for AT-lock", p2shAddress)); TradeBot.updateTradeBotState(repository, tradeBotData, () -> String.format("Funding P2SH-A %s. Messaged Bob. Waiting for AT-lock", p2shAddress));
@ -548,15 +565,25 @@ public class DigibyteACCTv3TradeBot implements AcctTradeBot {
PrivateKeyAccount sender = new PrivateKeyAccount(repository, tradeBotData.getTradePrivateKey()); PrivateKeyAccount sender = new PrivateKeyAccount(repository, tradeBotData.getTradePrivateKey());
MessageTransaction outgoingMessageTransaction = MessageTransaction.build(repository, sender, Group.NO_GROUP, messageRecipient, outgoingMessageData, false, false); MessageTransaction outgoingMessageTransaction = MessageTransaction.build(repository, sender, Group.NO_GROUP, messageRecipient, outgoingMessageData, false, false);
LOGGER.info("Computing nonce at difficulty {} for AT {} and recipient {}", outgoingMessageTransaction.getPoWDifficulty(), tradeBotData.getAtAddress(), messageRecipient);
outgoingMessageTransaction.computeNonce(); outgoingMessageTransaction.computeNonce();
MessageTransactionData newMessageTransactionData = (MessageTransactionData) outgoingMessageTransaction.getTransactionData();
LOGGER.info("Computed nonce {} at difficulty {}", newMessageTransactionData.getNonce(), outgoingMessageTransaction.getPoWDifficulty());
outgoingMessageTransaction.sign(sender); outgoingMessageTransaction.sign(sender);
// reset repository state to prevent deadlock // reset repository state to prevent deadlock
repository.discardChanges(); repository.discardChanges();
ValidationResult result = outgoingMessageTransaction.importAsUnconfirmed();
if (result != ValidationResult.OK) { if (outgoingMessageTransaction.isSignatureValid()) {
LOGGER.warn(() -> String.format("Unable to send MESSAGE to AT %s: %s", messageRecipient, result.name())); ValidationResult result = outgoingMessageTransaction.importAsUnconfirmed();
if (result != ValidationResult.OK) {
LOGGER.warn(() -> String.format("Unable to send MESSAGE to AT %s: %s", messageRecipient, result.name()));
return;
}
}
else {
LOGGER.warn(() -> String.format("Unable to send MESSAGE to AT %s: signature invalid", messageRecipient));
return; return;
} }
} }
@ -668,15 +695,25 @@ public class DigibyteACCTv3TradeBot implements AcctTradeBot {
PrivateKeyAccount sender = new PrivateKeyAccount(repository, tradeBotData.getTradePrivateKey()); PrivateKeyAccount sender = new PrivateKeyAccount(repository, tradeBotData.getTradePrivateKey());
MessageTransaction messageTransaction = MessageTransaction.build(repository, sender, Group.NO_GROUP, messageRecipient, messageData, false, false); MessageTransaction messageTransaction = MessageTransaction.build(repository, sender, Group.NO_GROUP, messageRecipient, messageData, false, false);
LOGGER.info("Computing nonce at difficulty {} for AT {} and recipient {}", messageTransaction.getPoWDifficulty(), tradeBotData.getAtAddress(), messageRecipient);
messageTransaction.computeNonce(); messageTransaction.computeNonce();
MessageTransactionData newMessageTransactionData = (MessageTransactionData) messageTransaction.getTransactionData();
LOGGER.info("Computed nonce {} at difficulty {}", newMessageTransactionData.getNonce(), messageTransaction.getPoWDifficulty());
messageTransaction.sign(sender); messageTransaction.sign(sender);
// Reset repository state to prevent deadlock // Reset repository state to prevent deadlock
repository.discardChanges(); repository.discardChanges();
ValidationResult result = messageTransaction.importAsUnconfirmed();
if (result != ValidationResult.OK) { if (messageTransaction.isSignatureValid()) {
LOGGER.warn(() -> String.format("Unable to send MESSAGE to AT %s: %s", messageRecipient, result.name())); ValidationResult result = messageTransaction.importAsUnconfirmed();
if (result != ValidationResult.OK) {
LOGGER.warn(() -> String.format("Unable to send MESSAGE to AT %s: %s", messageRecipient, result.name()));
return;
}
}
else {
LOGGER.warn(() -> String.format("Unable to send MESSAGE to AT %s: signature invalid", messageRecipient));
return; return;
} }
} }

View File

@ -19,6 +19,7 @@ import org.qortal.data.transaction.MessageTransactionData;
import org.qortal.group.Group; import org.qortal.group.Group;
import org.qortal.repository.DataException; import org.qortal.repository.DataException;
import org.qortal.repository.Repository; import org.qortal.repository.Repository;
import org.qortal.repository.RepositoryManager;
import org.qortal.transaction.DeployAtTransaction; import org.qortal.transaction.DeployAtTransaction;
import org.qortal.transaction.MessageTransaction; import org.qortal.transaction.MessageTransaction;
import org.qortal.transaction.Transaction.ValidationResult; import org.qortal.transaction.Transaction.ValidationResult;
@ -317,20 +318,36 @@ public class DogecoinACCTv1TradeBot implements AcctTradeBot {
boolean isMessageAlreadySent = repository.getMessageRepository().exists(tradeBotData.getTradeNativePublicKey(), messageRecipient, messageData); boolean isMessageAlreadySent = repository.getMessageRepository().exists(tradeBotData.getTradeNativePublicKey(), messageRecipient, messageData);
if (!isMessageAlreadySent) { if (!isMessageAlreadySent) {
PrivateKeyAccount sender = new PrivateKeyAccount(repository, tradeBotData.getTradePrivateKey()); // Do this in a new thread so caller doesn't have to wait for computeNonce()
MessageTransaction messageTransaction = MessageTransaction.build(repository, sender, Group.NO_GROUP, messageRecipient, messageData, false, false); // In the unlikely event that the transaction doesn't validate then the buy won't happen and eventually Alice's AT will be refunded
new Thread(() -> {
try (final Repository threadsRepository = RepositoryManager.getRepository()) {
PrivateKeyAccount sender = new PrivateKeyAccount(threadsRepository, tradeBotData.getTradePrivateKey());
MessageTransaction messageTransaction = MessageTransaction.build(threadsRepository, sender, Group.NO_GROUP, messageRecipient, messageData, false, false);
messageTransaction.computeNonce(); LOGGER.info("Computing nonce at difficulty {} for AT {} and recipient {}", messageTransaction.getPoWDifficulty(), tradeBotData.getAtAddress(), messageRecipient);
messageTransaction.sign(sender); messageTransaction.computeNonce();
MessageTransactionData newMessageTransactionData = (MessageTransactionData) messageTransaction.getTransactionData();
LOGGER.info("Computed nonce {} at difficulty {}", newMessageTransactionData.getNonce(), messageTransaction.getPoWDifficulty());
messageTransaction.sign(sender);
// reset repository state to prevent deadlock // reset repository state to prevent deadlock
repository.discardChanges(); threadsRepository.discardChanges();
ValidationResult result = messageTransaction.importAsUnconfirmed();
if (result != ValidationResult.OK) { if (messageTransaction.isSignatureValid()) {
LOGGER.warn(() -> String.format("Unable to send MESSAGE to Bob's trade-bot %s: %s", messageRecipient, result.name())); ValidationResult result = messageTransaction.importAsUnconfirmed();
return ResponseResult.NETWORK_ISSUE;
} if (result != ValidationResult.OK) {
LOGGER.warn(() -> String.format("Unable to send MESSAGE to Bob's trade-bot %s: %s", messageRecipient, result.name()));
}
}
else {
LOGGER.warn(() -> String.format("Unable to send MESSAGE to Bob's trade-bot %s: signature invalid", messageRecipient));
}
} catch (DataException e) {
LOGGER.warn(() -> String.format("Unable to send MESSAGE to Bob's trade-bot %s: %s", messageRecipient, e.getMessage()));
}
}, "TradeBot response").start();
} }
TradeBot.updateTradeBotState(repository, tradeBotData, () -> String.format("Funding P2SH-A %s. Messaged Bob. Waiting for AT-lock", p2shAddress)); TradeBot.updateTradeBotState(repository, tradeBotData, () -> String.format("Funding P2SH-A %s. Messaged Bob. Waiting for AT-lock", p2shAddress));
@ -548,15 +565,25 @@ public class DogecoinACCTv1TradeBot implements AcctTradeBot {
PrivateKeyAccount sender = new PrivateKeyAccount(repository, tradeBotData.getTradePrivateKey()); PrivateKeyAccount sender = new PrivateKeyAccount(repository, tradeBotData.getTradePrivateKey());
MessageTransaction outgoingMessageTransaction = MessageTransaction.build(repository, sender, Group.NO_GROUP, messageRecipient, outgoingMessageData, false, false); MessageTransaction outgoingMessageTransaction = MessageTransaction.build(repository, sender, Group.NO_GROUP, messageRecipient, outgoingMessageData, false, false);
LOGGER.info("Computing nonce at difficulty {} for AT {} and recipient {}", outgoingMessageTransaction.getPoWDifficulty(), tradeBotData.getAtAddress(), messageRecipient);
outgoingMessageTransaction.computeNonce(); outgoingMessageTransaction.computeNonce();
MessageTransactionData newMessageTransactionData = (MessageTransactionData) outgoingMessageTransaction.getTransactionData();
LOGGER.info("Computed nonce {} at difficulty {}", newMessageTransactionData.getNonce(), outgoingMessageTransaction.getPoWDifficulty());
outgoingMessageTransaction.sign(sender); outgoingMessageTransaction.sign(sender);
// reset repository state to prevent deadlock // reset repository state to prevent deadlock
repository.discardChanges(); repository.discardChanges();
ValidationResult result = outgoingMessageTransaction.importAsUnconfirmed();
if (result != ValidationResult.OK) { if (outgoingMessageTransaction.isSignatureValid()) {
LOGGER.warn(() -> String.format("Unable to send MESSAGE to AT %s: %s", messageRecipient, result.name())); ValidationResult result = outgoingMessageTransaction.importAsUnconfirmed();
if (result != ValidationResult.OK) {
LOGGER.warn(() -> String.format("Unable to send MESSAGE to AT %s: %s", messageRecipient, result.name()));
return;
}
}
else {
LOGGER.warn(() -> String.format("Unable to send MESSAGE to AT %s: signature invalid", messageRecipient));
return; return;
} }
} }
@ -668,15 +695,25 @@ public class DogecoinACCTv1TradeBot implements AcctTradeBot {
PrivateKeyAccount sender = new PrivateKeyAccount(repository, tradeBotData.getTradePrivateKey()); PrivateKeyAccount sender = new PrivateKeyAccount(repository, tradeBotData.getTradePrivateKey());
MessageTransaction messageTransaction = MessageTransaction.build(repository, sender, Group.NO_GROUP, messageRecipient, messageData, false, false); MessageTransaction messageTransaction = MessageTransaction.build(repository, sender, Group.NO_GROUP, messageRecipient, messageData, false, false);
LOGGER.info("Computing nonce at difficulty {} for AT {} and recipient {}", messageTransaction.getPoWDifficulty(), tradeBotData.getAtAddress(), messageRecipient);
messageTransaction.computeNonce(); messageTransaction.computeNonce();
MessageTransactionData newMessageTransactionData = (MessageTransactionData) messageTransaction.getTransactionData();
LOGGER.info("Computed nonce {} at difficulty {}", newMessageTransactionData.getNonce(), messageTransaction.getPoWDifficulty());
messageTransaction.sign(sender); messageTransaction.sign(sender);
// Reset repository state to prevent deadlock // Reset repository state to prevent deadlock
repository.discardChanges(); repository.discardChanges();
ValidationResult result = messageTransaction.importAsUnconfirmed();
if (result != ValidationResult.OK) { if (messageTransaction.isSignatureValid()) {
LOGGER.warn(() -> String.format("Unable to send MESSAGE to AT %s: %s", messageRecipient, result.name())); ValidationResult result = messageTransaction.importAsUnconfirmed();
if (result != ValidationResult.OK) {
LOGGER.warn(() -> String.format("Unable to send MESSAGE to AT %s: %s", messageRecipient, result.name()));
return;
}
}
else {
LOGGER.warn(() -> String.format("Unable to send MESSAGE to AT %s: signature invalid", messageRecipient));
return; return;
} }
} }

View File

@ -19,6 +19,7 @@ import org.qortal.data.transaction.MessageTransactionData;
import org.qortal.group.Group; import org.qortal.group.Group;
import org.qortal.repository.DataException; import org.qortal.repository.DataException;
import org.qortal.repository.Repository; import org.qortal.repository.Repository;
import org.qortal.repository.RepositoryManager;
import org.qortal.transaction.DeployAtTransaction; import org.qortal.transaction.DeployAtTransaction;
import org.qortal.transaction.MessageTransaction; import org.qortal.transaction.MessageTransaction;
import org.qortal.transaction.Transaction.ValidationResult; import org.qortal.transaction.Transaction.ValidationResult;
@ -317,20 +318,36 @@ public class DogecoinACCTv3TradeBot implements AcctTradeBot {
boolean isMessageAlreadySent = repository.getMessageRepository().exists(tradeBotData.getTradeNativePublicKey(), messageRecipient, messageData); boolean isMessageAlreadySent = repository.getMessageRepository().exists(tradeBotData.getTradeNativePublicKey(), messageRecipient, messageData);
if (!isMessageAlreadySent) { if (!isMessageAlreadySent) {
PrivateKeyAccount sender = new PrivateKeyAccount(repository, tradeBotData.getTradePrivateKey()); // Do this in a new thread so caller doesn't have to wait for computeNonce()
MessageTransaction messageTransaction = MessageTransaction.build(repository, sender, Group.NO_GROUP, messageRecipient, messageData, false, false); // In the unlikely event that the transaction doesn't validate then the buy won't happen and eventually Alice's AT will be refunded
new Thread(() -> {
try (final Repository threadsRepository = RepositoryManager.getRepository()) {
PrivateKeyAccount sender = new PrivateKeyAccount(threadsRepository, tradeBotData.getTradePrivateKey());
MessageTransaction messageTransaction = MessageTransaction.build(threadsRepository, sender, Group.NO_GROUP, messageRecipient, messageData, false, false);
messageTransaction.computeNonce(); LOGGER.info("Computing nonce at difficulty {} for AT {} and recipient {}", messageTransaction.getPoWDifficulty(), tradeBotData.getAtAddress(), messageRecipient);
messageTransaction.sign(sender); messageTransaction.computeNonce();
MessageTransactionData newMessageTransactionData = (MessageTransactionData) messageTransaction.getTransactionData();
LOGGER.info("Computed nonce {} at difficulty {}", newMessageTransactionData.getNonce(), messageTransaction.getPoWDifficulty());
messageTransaction.sign(sender);
// reset repository state to prevent deadlock // reset repository state to prevent deadlock
repository.discardChanges(); threadsRepository.discardChanges();
ValidationResult result = messageTransaction.importAsUnconfirmed();
if (result != ValidationResult.OK) { if (messageTransaction.isSignatureValid()) {
LOGGER.warn(() -> String.format("Unable to send MESSAGE to Bob's trade-bot %s: %s", messageRecipient, result.name())); ValidationResult result = messageTransaction.importAsUnconfirmed();
return ResponseResult.NETWORK_ISSUE;
} if (result != ValidationResult.OK) {
LOGGER.warn(() -> String.format("Unable to send MESSAGE to Bob's trade-bot %s: %s", messageRecipient, result.name()));
}
}
else {
LOGGER.warn(() -> String.format("Unable to send MESSAGE to Bob's trade-bot %s: signature invalid", messageRecipient));
}
} catch (DataException e) {
LOGGER.warn(() -> String.format("Unable to send MESSAGE to Bob's trade-bot %s: %s", messageRecipient, e.getMessage()));
}
}, "TradeBot response").start();
} }
TradeBot.updateTradeBotState(repository, tradeBotData, () -> String.format("Funding P2SH-A %s. Messaged Bob. Waiting for AT-lock", p2shAddress)); TradeBot.updateTradeBotState(repository, tradeBotData, () -> String.format("Funding P2SH-A %s. Messaged Bob. Waiting for AT-lock", p2shAddress));
@ -548,15 +565,25 @@ public class DogecoinACCTv3TradeBot implements AcctTradeBot {
PrivateKeyAccount sender = new PrivateKeyAccount(repository, tradeBotData.getTradePrivateKey()); PrivateKeyAccount sender = new PrivateKeyAccount(repository, tradeBotData.getTradePrivateKey());
MessageTransaction outgoingMessageTransaction = MessageTransaction.build(repository, sender, Group.NO_GROUP, messageRecipient, outgoingMessageData, false, false); MessageTransaction outgoingMessageTransaction = MessageTransaction.build(repository, sender, Group.NO_GROUP, messageRecipient, outgoingMessageData, false, false);
LOGGER.info("Computing nonce at difficulty {} for AT {} and recipient {}", outgoingMessageTransaction.getPoWDifficulty(), tradeBotData.getAtAddress(), messageRecipient);
outgoingMessageTransaction.computeNonce(); outgoingMessageTransaction.computeNonce();
MessageTransactionData newMessageTransactionData = (MessageTransactionData) outgoingMessageTransaction.getTransactionData();
LOGGER.info("Computed nonce {} at difficulty {}", newMessageTransactionData.getNonce(), outgoingMessageTransaction.getPoWDifficulty());
outgoingMessageTransaction.sign(sender); outgoingMessageTransaction.sign(sender);
// reset repository state to prevent deadlock // reset repository state to prevent deadlock
repository.discardChanges(); repository.discardChanges();
ValidationResult result = outgoingMessageTransaction.importAsUnconfirmed();
if (result != ValidationResult.OK) { if (outgoingMessageTransaction.isSignatureValid()) {
LOGGER.warn(() -> String.format("Unable to send MESSAGE to AT %s: %s", messageRecipient, result.name())); ValidationResult result = outgoingMessageTransaction.importAsUnconfirmed();
if (result != ValidationResult.OK) {
LOGGER.warn(() -> String.format("Unable to send MESSAGE to AT %s: %s", messageRecipient, result.name()));
return;
}
}
else {
LOGGER.warn(() -> String.format("Unable to send MESSAGE to AT %s: signature invalid", messageRecipient));
return; return;
} }
} }
@ -668,15 +695,25 @@ public class DogecoinACCTv3TradeBot implements AcctTradeBot {
PrivateKeyAccount sender = new PrivateKeyAccount(repository, tradeBotData.getTradePrivateKey()); PrivateKeyAccount sender = new PrivateKeyAccount(repository, tradeBotData.getTradePrivateKey());
MessageTransaction messageTransaction = MessageTransaction.build(repository, sender, Group.NO_GROUP, messageRecipient, messageData, false, false); MessageTransaction messageTransaction = MessageTransaction.build(repository, sender, Group.NO_GROUP, messageRecipient, messageData, false, false);
LOGGER.info("Computing nonce at difficulty {} for AT {} and recipient {}", messageTransaction.getPoWDifficulty(), tradeBotData.getAtAddress(), messageRecipient);
messageTransaction.computeNonce(); messageTransaction.computeNonce();
MessageTransactionData newMessageTransactionData = (MessageTransactionData) messageTransaction.getTransactionData();
LOGGER.info("Computed nonce {} at difficulty {}", newMessageTransactionData.getNonce(), messageTransaction.getPoWDifficulty());
messageTransaction.sign(sender); messageTransaction.sign(sender);
// Reset repository state to prevent deadlock // Reset repository state to prevent deadlock
repository.discardChanges(); repository.discardChanges();
ValidationResult result = messageTransaction.importAsUnconfirmed();
if (result != ValidationResult.OK) { if (messageTransaction.isSignatureValid()) {
LOGGER.warn(() -> String.format("Unable to send MESSAGE to AT %s: %s", messageRecipient, result.name())); ValidationResult result = messageTransaction.importAsUnconfirmed();
if (result != ValidationResult.OK) {
LOGGER.warn(() -> String.format("Unable to send MESSAGE to AT %s: %s", messageRecipient, result.name()));
return;
}
}
else {
LOGGER.warn(() -> String.format("Unable to send MESSAGE to AT %s: signature invalid", messageRecipient));
return; return;
} }
} }

View File

@ -325,15 +325,24 @@ public class LitecoinACCTv3TradeBot implements AcctTradeBot {
PrivateKeyAccount sender = new PrivateKeyAccount(threadsRepository, tradeBotData.getTradePrivateKey()); PrivateKeyAccount sender = new PrivateKeyAccount(threadsRepository, tradeBotData.getTradePrivateKey());
MessageTransaction messageTransaction = MessageTransaction.build(threadsRepository, sender, Group.NO_GROUP, messageRecipient, messageData, false, false); MessageTransaction messageTransaction = MessageTransaction.build(threadsRepository, sender, Group.NO_GROUP, messageRecipient, messageData, false, false);
LOGGER.info("Computing nonce at difficulty {} for AT {} and recipient {}", messageTransaction.getPoWDifficulty(), tradeBotData.getAtAddress(), messageRecipient);
messageTransaction.computeNonce(); messageTransaction.computeNonce();
MessageTransactionData newMessageTransactionData = (MessageTransactionData) messageTransaction.getTransactionData();
LOGGER.info("Computed nonce {} at difficulty {}", newMessageTransactionData.getNonce(), messageTransaction.getPoWDifficulty());
messageTransaction.sign(sender); messageTransaction.sign(sender);
// reset repository state to prevent deadlock // reset repository state to prevent deadlock
threadsRepository.discardChanges(); threadsRepository.discardChanges();
ValidationResult result = messageTransaction.importAsUnconfirmed();
if (result != ValidationResult.OK) { if (messageTransaction.isSignatureValid()) {
LOGGER.warn(() -> String.format("Unable to send MESSAGE to Bob's trade-bot %s: %s", messageRecipient, result.name())); ValidationResult result = messageTransaction.importAsUnconfirmed();
if (result != ValidationResult.OK) {
LOGGER.warn(() -> String.format("Unable to send MESSAGE to Bob's trade-bot %s: %s", messageRecipient, result.name()));
}
}
else {
LOGGER.warn(() -> String.format("Unable to send MESSAGE to Bob's trade-bot %s: signature invalid", messageRecipient));
} }
} catch (DataException e) { } catch (DataException e) {
LOGGER.warn(() -> String.format("Unable to send MESSAGE to Bob's trade-bot %s: %s", messageRecipient, e.getMessage())); LOGGER.warn(() -> String.format("Unable to send MESSAGE to Bob's trade-bot %s: %s", messageRecipient, e.getMessage()));
@ -556,15 +565,25 @@ public class LitecoinACCTv3TradeBot implements AcctTradeBot {
PrivateKeyAccount sender = new PrivateKeyAccount(repository, tradeBotData.getTradePrivateKey()); PrivateKeyAccount sender = new PrivateKeyAccount(repository, tradeBotData.getTradePrivateKey());
MessageTransaction outgoingMessageTransaction = MessageTransaction.build(repository, sender, Group.NO_GROUP, messageRecipient, outgoingMessageData, false, false); MessageTransaction outgoingMessageTransaction = MessageTransaction.build(repository, sender, Group.NO_GROUP, messageRecipient, outgoingMessageData, false, false);
LOGGER.info("Computing nonce at difficulty {} for AT {} and recipient {}", outgoingMessageTransaction.getPoWDifficulty(), tradeBotData.getAtAddress(), messageRecipient);
outgoingMessageTransaction.computeNonce(); outgoingMessageTransaction.computeNonce();
MessageTransactionData newMessageTransactionData = (MessageTransactionData) outgoingMessageTransaction.getTransactionData();
LOGGER.info("Computed nonce {} at difficulty {}", newMessageTransactionData.getNonce(), outgoingMessageTransaction.getPoWDifficulty());
outgoingMessageTransaction.sign(sender); outgoingMessageTransaction.sign(sender);
// reset repository state to prevent deadlock // reset repository state to prevent deadlock
repository.discardChanges(); repository.discardChanges();
ValidationResult result = outgoingMessageTransaction.importAsUnconfirmed();
if (result != ValidationResult.OK) { if (outgoingMessageTransaction.isSignatureValid()) {
LOGGER.warn(() -> String.format("Unable to send MESSAGE to AT %s: %s", messageRecipient, result.name())); ValidationResult result = outgoingMessageTransaction.importAsUnconfirmed();
if (result != ValidationResult.OK) {
LOGGER.warn(() -> String.format("Unable to send MESSAGE to AT %s: %s", messageRecipient, result.name()));
return;
}
}
else {
LOGGER.warn(() -> String.format("Unable to send MESSAGE to AT %s: signature invalid", messageRecipient));
return; return;
} }
} }
@ -676,15 +695,25 @@ public class LitecoinACCTv3TradeBot implements AcctTradeBot {
PrivateKeyAccount sender = new PrivateKeyAccount(repository, tradeBotData.getTradePrivateKey()); PrivateKeyAccount sender = new PrivateKeyAccount(repository, tradeBotData.getTradePrivateKey());
MessageTransaction messageTransaction = MessageTransaction.build(repository, sender, Group.NO_GROUP, messageRecipient, messageData, false, false); MessageTransaction messageTransaction = MessageTransaction.build(repository, sender, Group.NO_GROUP, messageRecipient, messageData, false, false);
LOGGER.info("Computing nonce at difficulty {} for AT {} and recipient {}", messageTransaction.getPoWDifficulty(), tradeBotData.getAtAddress(), messageRecipient);
messageTransaction.computeNonce(); messageTransaction.computeNonce();
MessageTransactionData newMessageTransactionData = (MessageTransactionData) messageTransaction.getTransactionData();
LOGGER.info("Computed nonce {} at difficulty {}", newMessageTransactionData.getNonce(), messageTransaction.getPoWDifficulty());
messageTransaction.sign(sender); messageTransaction.sign(sender);
// Reset repository state to prevent deadlock // Reset repository state to prevent deadlock
repository.discardChanges(); repository.discardChanges();
ValidationResult result = messageTransaction.importAsUnconfirmed();
if (result != ValidationResult.OK) { if (messageTransaction.isSignatureValid()) {
LOGGER.warn(() -> String.format("Unable to send MESSAGE to AT %s: %s", messageRecipient, result.name())); ValidationResult result = messageTransaction.importAsUnconfirmed();
if (result != ValidationResult.OK) {
LOGGER.warn(() -> String.format("Unable to send MESSAGE to AT %s: %s", messageRecipient, result.name()));
return;
}
}
else {
LOGGER.warn(() -> String.format("Unable to send MESSAGE to AT %s: signature invalid", messageRecipient));
return; return;
} }
} }

View File

@ -20,6 +20,7 @@ import org.qortal.data.transaction.MessageTransactionData;
import org.qortal.group.Group; import org.qortal.group.Group;
import org.qortal.repository.DataException; import org.qortal.repository.DataException;
import org.qortal.repository.Repository; import org.qortal.repository.Repository;
import org.qortal.repository.RepositoryManager;
import org.qortal.transaction.DeployAtTransaction; import org.qortal.transaction.DeployAtTransaction;
import org.qortal.transaction.MessageTransaction; import org.qortal.transaction.MessageTransaction;
import org.qortal.transaction.Transaction.ValidationResult; import org.qortal.transaction.Transaction.ValidationResult;
@ -320,20 +321,36 @@ public class PirateChainACCTv3TradeBot implements AcctTradeBot {
boolean isMessageAlreadySent = repository.getMessageRepository().exists(tradeBotData.getTradeNativePublicKey(), messageRecipient, messageData); boolean isMessageAlreadySent = repository.getMessageRepository().exists(tradeBotData.getTradeNativePublicKey(), messageRecipient, messageData);
if (!isMessageAlreadySent) { if (!isMessageAlreadySent) {
PrivateKeyAccount sender = new PrivateKeyAccount(repository, tradeBotData.getTradePrivateKey()); // Do this in a new thread so caller doesn't have to wait for computeNonce()
MessageTransaction messageTransaction = MessageTransaction.build(repository, sender, Group.NO_GROUP, messageRecipient, messageData, false, false); // In the unlikely event that the transaction doesn't validate then the buy won't happen and eventually Alice's AT will be refunded
new Thread(() -> {
try (final Repository threadsRepository = RepositoryManager.getRepository()) {
PrivateKeyAccount sender = new PrivateKeyAccount(threadsRepository, tradeBotData.getTradePrivateKey());
MessageTransaction messageTransaction = MessageTransaction.build(threadsRepository, sender, Group.NO_GROUP, messageRecipient, messageData, false, false);
messageTransaction.computeNonce(); LOGGER.info("Computing nonce at difficulty {} for AT {} and recipient {}", messageTransaction.getPoWDifficulty(), tradeBotData.getAtAddress(), messageRecipient);
messageTransaction.sign(sender); messageTransaction.computeNonce();
MessageTransactionData newMessageTransactionData = (MessageTransactionData) messageTransaction.getTransactionData();
LOGGER.info("Computed nonce {} at difficulty {}", newMessageTransactionData.getNonce(), messageTransaction.getPoWDifficulty());
messageTransaction.sign(sender);
// reset repository state to prevent deadlock // reset repository state to prevent deadlock
repository.discardChanges(); threadsRepository.discardChanges();
ValidationResult result = messageTransaction.importAsUnconfirmed();
if (result != ValidationResult.OK) { if (messageTransaction.isSignatureValid()) {
LOGGER.warn(() -> String.format("Unable to send MESSAGE to Bob's trade-bot %s: %s", messageRecipient, result.name())); ValidationResult result = messageTransaction.importAsUnconfirmed();
return ResponseResult.NETWORK_ISSUE;
} if (result != ValidationResult.OK) {
LOGGER.warn(() -> String.format("Unable to send MESSAGE to Bob's trade-bot %s: %s", messageRecipient, result.name()));
}
}
else {
LOGGER.warn(() -> String.format("Unable to send MESSAGE to Bob's trade-bot %s: signature invalid", messageRecipient));
}
} catch (DataException e) {
LOGGER.warn(() -> String.format("Unable to send MESSAGE to Bob's trade-bot %s: %s", messageRecipient, e.getMessage()));
}
}, "TradeBot response").start();
} }
TradeBot.updateTradeBotState(repository, tradeBotData, () -> String.format("Funding P2SH-A %s. Messaged Bob. Waiting for AT-lock", p2shAddressT3)); TradeBot.updateTradeBotState(repository, tradeBotData, () -> String.format("Funding P2SH-A %s. Messaged Bob. Waiting for AT-lock", p2shAddressT3));
@ -561,15 +578,25 @@ public class PirateChainACCTv3TradeBot implements AcctTradeBot {
PrivateKeyAccount sender = new PrivateKeyAccount(repository, tradeBotData.getTradePrivateKey()); PrivateKeyAccount sender = new PrivateKeyAccount(repository, tradeBotData.getTradePrivateKey());
MessageTransaction outgoingMessageTransaction = MessageTransaction.build(repository, sender, Group.NO_GROUP, messageRecipient, outgoingMessageData, false, false); MessageTransaction outgoingMessageTransaction = MessageTransaction.build(repository, sender, Group.NO_GROUP, messageRecipient, outgoingMessageData, false, false);
LOGGER.info("Computing nonce at difficulty {} for AT {} and recipient {}", outgoingMessageTransaction.getPoWDifficulty(), tradeBotData.getAtAddress(), messageRecipient);
outgoingMessageTransaction.computeNonce(); outgoingMessageTransaction.computeNonce();
MessageTransactionData newMessageTransactionData = (MessageTransactionData) outgoingMessageTransaction.getTransactionData();
LOGGER.info("Computed nonce {} at difficulty {}", newMessageTransactionData.getNonce(), outgoingMessageTransaction.getPoWDifficulty());
outgoingMessageTransaction.sign(sender); outgoingMessageTransaction.sign(sender);
// reset repository state to prevent deadlock // reset repository state to prevent deadlock
repository.discardChanges(); repository.discardChanges();
ValidationResult result = outgoingMessageTransaction.importAsUnconfirmed();
if (result != ValidationResult.OK) { if (outgoingMessageTransaction.isSignatureValid()) {
LOGGER.warn(() -> String.format("Unable to send MESSAGE to AT %s: %s", messageRecipient, result.name())); ValidationResult result = outgoingMessageTransaction.importAsUnconfirmed();
if (result != ValidationResult.OK) {
LOGGER.warn(() -> String.format("Unable to send MESSAGE to AT %s: %s", messageRecipient, result.name()));
return;
}
}
else {
LOGGER.warn(() -> String.format("Unable to send MESSAGE to AT %s: signature invalid", messageRecipient));
return; return;
} }
} }
@ -681,15 +708,25 @@ public class PirateChainACCTv3TradeBot implements AcctTradeBot {
PrivateKeyAccount sender = new PrivateKeyAccount(repository, tradeBotData.getTradePrivateKey()); PrivateKeyAccount sender = new PrivateKeyAccount(repository, tradeBotData.getTradePrivateKey());
MessageTransaction messageTransaction = MessageTransaction.build(repository, sender, Group.NO_GROUP, messageRecipient, messageData, false, false); MessageTransaction messageTransaction = MessageTransaction.build(repository, sender, Group.NO_GROUP, messageRecipient, messageData, false, false);
LOGGER.info("Computing nonce at difficulty {} for AT {} and recipient {}", messageTransaction.getPoWDifficulty(), tradeBotData.getAtAddress(), messageRecipient);
messageTransaction.computeNonce(); messageTransaction.computeNonce();
MessageTransactionData newMessageTransactionData = (MessageTransactionData) messageTransaction.getTransactionData();
LOGGER.info("Computed nonce {} at difficulty {}", newMessageTransactionData.getNonce(), messageTransaction.getPoWDifficulty());
messageTransaction.sign(sender); messageTransaction.sign(sender);
// Reset repository state to prevent deadlock // Reset repository state to prevent deadlock
repository.discardChanges(); repository.discardChanges();
ValidationResult result = messageTransaction.importAsUnconfirmed();
if (result != ValidationResult.OK) { if (messageTransaction.isSignatureValid()) {
LOGGER.warn(() -> String.format("Unable to send MESSAGE to AT %s: %s", messageRecipient, result.name())); ValidationResult result = messageTransaction.importAsUnconfirmed();
if (result != ValidationResult.OK) {
LOGGER.warn(() -> String.format("Unable to send MESSAGE to AT %s: %s", messageRecipient, result.name()));
return;
}
}
else {
LOGGER.warn(() -> String.format("Unable to send MESSAGE to AT %s: signature invalid", messageRecipient));
return; return;
} }
} }

View File

@ -19,6 +19,7 @@ import org.qortal.data.transaction.MessageTransactionData;
import org.qortal.group.Group; import org.qortal.group.Group;
import org.qortal.repository.DataException; import org.qortal.repository.DataException;
import org.qortal.repository.Repository; import org.qortal.repository.Repository;
import org.qortal.repository.RepositoryManager;
import org.qortal.transaction.DeployAtTransaction; import org.qortal.transaction.DeployAtTransaction;
import org.qortal.transaction.MessageTransaction; import org.qortal.transaction.MessageTransaction;
import org.qortal.transaction.Transaction.ValidationResult; import org.qortal.transaction.Transaction.ValidationResult;
@ -317,20 +318,36 @@ public class RavencoinACCTv3TradeBot implements AcctTradeBot {
boolean isMessageAlreadySent = repository.getMessageRepository().exists(tradeBotData.getTradeNativePublicKey(), messageRecipient, messageData); boolean isMessageAlreadySent = repository.getMessageRepository().exists(tradeBotData.getTradeNativePublicKey(), messageRecipient, messageData);
if (!isMessageAlreadySent) { if (!isMessageAlreadySent) {
PrivateKeyAccount sender = new PrivateKeyAccount(repository, tradeBotData.getTradePrivateKey()); // Do this in a new thread so caller doesn't have to wait for computeNonce()
MessageTransaction messageTransaction = MessageTransaction.build(repository, sender, Group.NO_GROUP, messageRecipient, messageData, false, false); // In the unlikely event that the transaction doesn't validate then the buy won't happen and eventually Alice's AT will be refunded
new Thread(() -> {
try (final Repository threadsRepository = RepositoryManager.getRepository()) {
PrivateKeyAccount sender = new PrivateKeyAccount(threadsRepository, tradeBotData.getTradePrivateKey());
MessageTransaction messageTransaction = MessageTransaction.build(threadsRepository, sender, Group.NO_GROUP, messageRecipient, messageData, false, false);
messageTransaction.computeNonce(); LOGGER.info("Computing nonce at difficulty {} for AT {} and recipient {}", messageTransaction.getPoWDifficulty(), tradeBotData.getAtAddress(), messageRecipient);
messageTransaction.sign(sender); messageTransaction.computeNonce();
MessageTransactionData newMessageTransactionData = (MessageTransactionData) messageTransaction.getTransactionData();
LOGGER.info("Computed nonce {} at difficulty {}", newMessageTransactionData.getNonce(), messageTransaction.getPoWDifficulty());
messageTransaction.sign(sender);
// reset repository state to prevent deadlock // reset repository state to prevent deadlock
repository.discardChanges(); threadsRepository.discardChanges();
ValidationResult result = messageTransaction.importAsUnconfirmed();
if (result != ValidationResult.OK) { if (messageTransaction.isSignatureValid()) {
LOGGER.warn(() -> String.format("Unable to send MESSAGE to Bob's trade-bot %s: %s", messageRecipient, result.name())); ValidationResult result = messageTransaction.importAsUnconfirmed();
return ResponseResult.NETWORK_ISSUE;
} if (result != ValidationResult.OK) {
LOGGER.warn(() -> String.format("Unable to send MESSAGE to Bob's trade-bot %s: %s", messageRecipient, result.name()));
}
}
else {
LOGGER.warn(() -> String.format("Unable to send MESSAGE to Bob's trade-bot %s: signature invalid", messageRecipient));
}
} catch (DataException e) {
LOGGER.warn(() -> String.format("Unable to send MESSAGE to Bob's trade-bot %s: %s", messageRecipient, e.getMessage()));
}
}, "TradeBot response").start();
} }
TradeBot.updateTradeBotState(repository, tradeBotData, () -> String.format("Funding P2SH-A %s. Messaged Bob. Waiting for AT-lock", p2shAddress)); TradeBot.updateTradeBotState(repository, tradeBotData, () -> String.format("Funding P2SH-A %s. Messaged Bob. Waiting for AT-lock", p2shAddress));
@ -548,15 +565,25 @@ public class RavencoinACCTv3TradeBot implements AcctTradeBot {
PrivateKeyAccount sender = new PrivateKeyAccount(repository, tradeBotData.getTradePrivateKey()); PrivateKeyAccount sender = new PrivateKeyAccount(repository, tradeBotData.getTradePrivateKey());
MessageTransaction outgoingMessageTransaction = MessageTransaction.build(repository, sender, Group.NO_GROUP, messageRecipient, outgoingMessageData, false, false); MessageTransaction outgoingMessageTransaction = MessageTransaction.build(repository, sender, Group.NO_GROUP, messageRecipient, outgoingMessageData, false, false);
LOGGER.info("Computing nonce at difficulty {} for AT {} and recipient {}", outgoingMessageTransaction.getPoWDifficulty(), tradeBotData.getAtAddress(), messageRecipient);
outgoingMessageTransaction.computeNonce(); outgoingMessageTransaction.computeNonce();
MessageTransactionData newMessageTransactionData = (MessageTransactionData) outgoingMessageTransaction.getTransactionData();
LOGGER.info("Computed nonce {} at difficulty {}", newMessageTransactionData.getNonce(), outgoingMessageTransaction.getPoWDifficulty());
outgoingMessageTransaction.sign(sender); outgoingMessageTransaction.sign(sender);
// reset repository state to prevent deadlock // reset repository state to prevent deadlock
repository.discardChanges(); repository.discardChanges();
ValidationResult result = outgoingMessageTransaction.importAsUnconfirmed();
if (result != ValidationResult.OK) { if (outgoingMessageTransaction.isSignatureValid()) {
LOGGER.warn(() -> String.format("Unable to send MESSAGE to AT %s: %s", messageRecipient, result.name())); ValidationResult result = outgoingMessageTransaction.importAsUnconfirmed();
if (result != ValidationResult.OK) {
LOGGER.warn(() -> String.format("Unable to send MESSAGE to AT %s: %s", messageRecipient, result.name()));
return;
}
}
else {
LOGGER.warn(() -> String.format("Unable to send MESSAGE to AT %s: signature invalid", messageRecipient));
return; return;
} }
} }
@ -668,15 +695,25 @@ public class RavencoinACCTv3TradeBot implements AcctTradeBot {
PrivateKeyAccount sender = new PrivateKeyAccount(repository, tradeBotData.getTradePrivateKey()); PrivateKeyAccount sender = new PrivateKeyAccount(repository, tradeBotData.getTradePrivateKey());
MessageTransaction messageTransaction = MessageTransaction.build(repository, sender, Group.NO_GROUP, messageRecipient, messageData, false, false); MessageTransaction messageTransaction = MessageTransaction.build(repository, sender, Group.NO_GROUP, messageRecipient, messageData, false, false);
LOGGER.info("Computing nonce at difficulty {} for AT {} and recipient {}", messageTransaction.getPoWDifficulty(), tradeBotData.getAtAddress(), messageRecipient);
messageTransaction.computeNonce(); messageTransaction.computeNonce();
MessageTransactionData newMessageTransactionData = (MessageTransactionData) messageTransaction.getTransactionData();
LOGGER.info("Computed nonce {} at difficulty {}", newMessageTransactionData.getNonce(), messageTransaction.getPoWDifficulty());
messageTransaction.sign(sender); messageTransaction.sign(sender);
// Reset repository state to prevent deadlock // Reset repository state to prevent deadlock
repository.discardChanges(); repository.discardChanges();
ValidationResult result = messageTransaction.importAsUnconfirmed();
if (result != ValidationResult.OK) { if (messageTransaction.isSignatureValid()) {
LOGGER.warn(() -> String.format("Unable to send MESSAGE to AT %s: %s", messageRecipient, result.name())); ValidationResult result = messageTransaction.importAsUnconfirmed();
if (result != ValidationResult.OK) {
LOGGER.warn(() -> String.format("Unable to send MESSAGE to AT %s: %s", messageRecipient, result.name()));
return;
}
}
else {
LOGGER.warn(() -> String.format("Unable to send MESSAGE to AT %s: signature invalid", messageRecipient));
return; return;
} }
} }

View File

@ -333,7 +333,7 @@ public class TradeBot implements Listener {
SysTray.getInstance().showMessage("Trade-Bot", String.format("%s: %s", tradeBotData.getAtAddress(), newState), MessageType.INFO); SysTray.getInstance().showMessage("Trade-Bot", String.format("%s: %s", tradeBotData.getAtAddress(), newState), MessageType.INFO);
if (logMessageSupplier != null) if (logMessageSupplier != null)
LOGGER.info(logMessageSupplier); LOGGER.info(logMessageSupplier.get());
LOGGER.debug(() -> String.format("new state for trade-bot entry based on AT %s: %s", tradeBotData.getAtAddress(), newState)); LOGGER.debug(() -> String.format("new state for trade-bot entry based on AT %s: %s", tradeBotData.getAtAddress(), newState));
@ -712,30 +712,16 @@ public class TradeBot implements Listener {
} }
try { try {
List<byte[]> signatures = repository.getTransactionRepository().getSignaturesMatchingCriteria(null, null, null, Arrays.asList(Transaction.TransactionType.MESSAGE), null, null, crossChainTradeData.qortalCreatorTradeAddress, TransactionsResource.ConfirmationStatus.CONFIRMED, null, null, null); List<TransactionData> transactions = repository.getTransactionRepository().getUnconfirmedTransactions(Arrays.asList(Transaction.TransactionType.MESSAGE), null, null, null, null);
if (signatures.size() < getMaxTradeOfferAttempts) {
// Less than 3 (or user-specified number of) MESSAGE transactions relate to this trade, so assume it is ok
validTrades.put(crossChainTradeData.qortalAtAddress, now);
continue;
}
List<TransactionData> transactions = new ArrayList<>(signatures.size()); for (TransactionData transactionData : transactions) {
for (byte[] signature : signatures) { // Treat as failed if buy attempt was more than 60 mins ago (as it's still in the OFFERING state)
transactions.add(repository.getTransactionRepository().fromSignature(signature)); if (transactionData.getRecipient().equals(crossChainTradeData.qortalCreatorTradeAddress) && now - transactionData.getTimestamp() > 60*60*1000L) {
} failedTrades.put(crossChainTradeData.qortalAtAddress, now);
transactions.sort(Transaction.getDataComparator()); updatedCrossChainTrades.remove(crossChainTradeData);
} else {
// Get timestamp of the first MESSAGE transaction validTrades.put(crossChainTradeData.qortalAtAddress, now);
long firstMessageTimestamp = transactions.get(0).getTimestamp(); }
// Treat as failed if first buy attempt was more than 60 mins ago (as it's still in the OFFERING state)
boolean isFailed = (now - firstMessageTimestamp > 60*60*1000L);
if (isFailed) {
failedTrades.put(crossChainTradeData.qortalAtAddress, now);
updatedCrossChainTrades.remove(crossChainTradeData);
}
else {
validTrades.put(crossChainTradeData.qortalAtAddress, now);
} }
} catch (DataException e) { } catch (DataException e) {

View File

@ -44,89 +44,78 @@ public class Bitcoin extends Bitcoiny {
@Override @Override
public Collection<ElectrumX.Server> getServers() { public Collection<ElectrumX.Server> getServers() {
return Arrays.asList( return Arrays.asList(
// Servers chosen on NO BASIS WHATSOEVER from various sources! // Servers chosen on NO BASIS WHATSOEVER from various sources!
// Status verified at https://1209k.com/bitcoin-eye/ele.php?chain=btc // Status verified at https://1209k.com/bitcoin-eye/ele.php?chain=btc
//CLOSED new Server("bitcoin.grey.pw", Server.ConnectionType.SSL, 50002), new Server("104.248.139.211", Server.ConnectionType.SSL, 50002),
//CLOSED new Server("btc.litepay.ch", Server.ConnectionType.SSL, 50002), new Server("128.0.190.26", Server.ConnectionType.SSL, 50002),
//CLOSED new Server("electrum.pabu.io", Server.ConnectionType.SSL, 50002), new Server("142.93.6.38", Server.ConnectionType.SSL, 50002),
//CLOSED new Server("electrumx.dev", Server.ConnectionType.SSL, 50002), new Server("157.245.172.236", Server.ConnectionType.SSL, 50002),
//CLOSED new Server("electrumx.hodlwallet.com", Server.ConnectionType.SSL, 50002), new Server("167.172.226.175", Server.ConnectionType.SSL, 50002),
//CLOSED new Server("gd42.org", Server.ConnectionType.SSL, 50002), new Server("167.172.42.31", Server.ConnectionType.SSL, 50002),
//CLOSED new Server("korea.electrum-server.com", Server.ConnectionType.SSL, 50002), new Server("178.62.80.20", Server.ConnectionType.SSL, 50002),
//CLOSED new Server("prospero.bitsrc.net", Server.ConnectionType.SSL, 50002), new Server("185.64.116.15", Server.ConnectionType.SSL, 50002),
//1.15.0 new Server("alviss.coinjoined.com", Server.ConnectionType.SSL, 50002), new Server("188.165.206.215", Server.ConnectionType.SSL, 50002),
//1.15.0 new Server("electrum.acinq.co", Server.ConnectionType.SSL, 50002), new Server("188.165.211.112", Server.ConnectionType.SSL, 50002),
//1.14.0 new Server("electrum.coinext.com.br", Server.ConnectionType.SSL, 50002), new Server("2azzarita.hopto.org", Server.ConnectionType.SSL, 50002),
//F1.7.0 new Server("btc.lastingcoin.net", Server.ConnectionType.SSL, 50002), new Server("2electrumx.hopto.me", Server.ConnectionType.SSL, 56022),
new Server("104.248.139.211", Server.ConnectionType.SSL, 50002), new Server("2ex.digitaleveryware.com", Server.ConnectionType.SSL, 50002),
new Server("128.0.190.26", Server.ConnectionType.SSL, 50002), new Server("65.39.140.37", Server.ConnectionType.SSL, 50002),
new Server("142.93.6.38", Server.ConnectionType.SSL, 50002), new Server("68.183.188.105", Server.ConnectionType.SSL, 50002),
new Server("157.245.172.236", Server.ConnectionType.SSL, 50002), new Server("71.73.14.254", Server.ConnectionType.SSL, 50002),
new Server("167.172.226.175", Server.ConnectionType.SSL, 50002), new Server("94.23.247.135", Server.ConnectionType.SSL, 50002),
new Server("167.172.42.31", Server.ConnectionType.SSL, 50002), new Server("assuredly.not.fyi", Server.ConnectionType.SSL, 50002),
new Server("178.62.80.20", Server.ConnectionType.SSL, 50002), new Server("ax101.blockeng.ch", Server.ConnectionType.SSL, 50002),
new Server("185.64.116.15", Server.ConnectionType.SSL, 50002), new Server("ax102.blockeng.ch", Server.ConnectionType.SSL, 50002),
new Server("188.165.206.215", Server.ConnectionType.SSL, 50002), new Server("b.1209k.com", Server.ConnectionType.SSL, 50002),
new Server("188.165.211.112", Server.ConnectionType.SSL, 50002), new Server("b6.1209k.com", Server.ConnectionType.SSL, 50002),
new Server("2azzarita.hopto.org", Server.ConnectionType.SSL, 50002), new Server("bitcoin.dermichi.com", Server.ConnectionType.SSL, 50002),
new Server("2electrumx.hopto.me", Server.ConnectionType.SSL, 56022), new Server("bitcoin.lu.ke", Server.ConnectionType.SSL, 50002),
new Server("2ex.digitaleveryware.com", Server.ConnectionType.SSL, 50002), new Server("bitcoin.lukechilds.co", Server.ConnectionType.SSL, 50002),
new Server("65.39.140.37", Server.ConnectionType.SSL, 50002), new Server("blkhub.net", Server.ConnectionType.SSL, 50002),
new Server("68.183.188.105", Server.ConnectionType.SSL, 50002), new Server("btc.electroncash.dk", Server.ConnectionType.SSL, 60002),
new Server("71.73.14.254", Server.ConnectionType.SSL, 50002), new Server("btc.ocf.sh", Server.ConnectionType.SSL, 50002),
new Server("94.23.247.135", Server.ConnectionType.SSL, 50002), new Server("btce.iiiiiii.biz", Server.ConnectionType.SSL, 50002),
new Server("assuredly.not.fyi", Server.ConnectionType.SSL, 50002), new Server("caleb.vegas", Server.ConnectionType.SSL, 50002),
new Server("ax101.blockeng.ch", Server.ConnectionType.SSL, 50002), new Server("eai.coincited.net", Server.ConnectionType.SSL, 50002),
new Server("ax102.blockeng.ch", Server.ConnectionType.SSL, 50002), new Server("electrum.bhoovd.com", Server.ConnectionType.SSL, 50002),
new Server("b.1209k.com", Server.ConnectionType.SSL, 50002), new Server("electrum.bitaroo.net", Server.ConnectionType.SSL, 50002),
new Server("b6.1209k.com", Server.ConnectionType.SSL, 50002), new Server("electrum.bitcoinlizard.net", Server.ConnectionType.SSL, 50002),
new Server("bitcoin.dermichi.com", Server.ConnectionType.SSL, 50002), new Server("electrum.blockstream.info", Server.ConnectionType.SSL, 50002),
new Server("bitcoin.lu.ke", Server.ConnectionType.SSL, 50002), new Server("electrum.emzy.de", Server.ConnectionType.SSL, 50002),
new Server("bitcoin.lukechilds.co", Server.ConnectionType.SSL, 50002), new Server("electrum.exan.tech", Server.ConnectionType.SSL, 50002),
new Server("blkhub.net", Server.ConnectionType.SSL, 50002), new Server("electrum.kendigisland.xyz", Server.ConnectionType.SSL, 50002),
new Server("btc.electroncash.dk", Server.ConnectionType.SSL, 60002), new Server("electrum.mmitech.info", Server.ConnectionType.SSL, 50002),
new Server("btc.ocf.sh", Server.ConnectionType.SSL, 50002), new Server("electrum.petrkr.net", Server.ConnectionType.SSL, 50002),
new Server("btce.iiiiiii.biz", Server.ConnectionType.SSL, 50002), new Server("electrum.stippy.com", Server.ConnectionType.SSL, 50002),
new Server("caleb.vegas", Server.ConnectionType.SSL, 50002), new Server("electrum.thomasfischbach.de", Server.ConnectionType.SSL, 50002),
new Server("eai.coincited.net", Server.ConnectionType.SSL, 50002), new Server("electrum0.snel.it", Server.ConnectionType.SSL, 50002),
new Server("electrum.bhoovd.com", Server.ConnectionType.SSL, 50002), new Server("electrum1.cipig.net", Server.ConnectionType.SSL, 50002),
new Server("electrum.bitaroo.net", Server.ConnectionType.SSL, 50002), new Server("electrum2.cipig.net", Server.ConnectionType.SSL, 50002),
new Server("electrum.bitcoinlizard.net", Server.ConnectionType.SSL, 50002), new Server("electrum3.cipig.net", Server.ConnectionType.SSL, 50002),
new Server("electrum.blockstream.info", Server.ConnectionType.SSL, 50002), new Server("electrumx.alexridevski.net", Server.ConnectionType.SSL, 50002),
new Server("electrum.emzy.de", Server.ConnectionType.SSL, 50002), new Server("electrumx-core.1209k.com", Server.ConnectionType.SSL, 50002),
new Server("electrum.exan.tech", Server.ConnectionType.SSL, 50002), new Server("elx.bitske.com", Server.ConnectionType.SSL, 50002),
new Server("electrum.kendigisland.xyz", Server.ConnectionType.SSL, 50002), new Server("ex03.axalgo.com", Server.ConnectionType.SSL, 50002),
new Server("electrum.mmitech.info", Server.ConnectionType.SSL, 50002), new Server("ex05.axalgo.com", Server.ConnectionType.SSL, 50002),
new Server("electrum.petrkr.net", Server.ConnectionType.SSL, 50002), new Server("ex07.axalgo.com", Server.ConnectionType.SSL, 50002),
new Server("electrum.stippy.com", Server.ConnectionType.SSL, 50002), new Server("fortress.qtornado.com", Server.ConnectionType.SSL, 50002),
new Server("electrum.thomasfischbach.de", Server.ConnectionType.SSL, 50002), new Server("fulcrum.grey.pw", Server.ConnectionType.SSL, 50002),
new Server("electrum0.snel.it", Server.ConnectionType.SSL, 50002), new Server("fulcrum.sethforprivacy.com", Server.ConnectionType.SSL, 51002),
new Server("electrum1.cipig.net", Server.ConnectionType.SSL, 50002), new Server("guichet.centure.cc", Server.ConnectionType.SSL, 50002),
new Server("electrum2.cipig.net", Server.ConnectionType.SSL, 50002), new Server("hodlers.beer", Server.ConnectionType.SSL, 50002),
new Server("electrum3.cipig.net", Server.ConnectionType.SSL, 50002), new Server("kareoke.qoppa.org", Server.ConnectionType.SSL, 50002),
new Server("electrumx.alexridevski.net", Server.ConnectionType.SSL, 50002), new Server("kirsche.emzy.de", Server.ConnectionType.SSL, 50002),
new Server("electrumx-core.1209k.com", Server.ConnectionType.SSL, 50002), new Server("node1.btccuracao.com", Server.ConnectionType.SSL, 50002),
new Server("elx.bitske.com", Server.ConnectionType.SSL, 50002), new Server("osr1ex1.compumundohipermegared.one", Server.ConnectionType.SSL, 50002),
new Server("ex03.axalgo.com", Server.ConnectionType.SSL, 50002), new Server("smmalis37.ddns.net", Server.ConnectionType.SSL, 50002),
new Server("ex05.axalgo.com", Server.ConnectionType.SSL, 50002), new Server("ulrichard.ch", Server.ConnectionType.SSL, 50002),
new Server("ex07.axalgo.com", Server.ConnectionType.SSL, 50002), new Server("vmd104012.contaboserver.net", Server.ConnectionType.SSL, 50002),
new Server("fortress.qtornado.com", Server.ConnectionType.SSL, 50002), new Server("vmd104014.contaboserver.net", Server.ConnectionType.SSL, 50002),
new Server("fulcrum.grey.pw", Server.ConnectionType.SSL, 50002), new Server("vmd63185.contaboserver.net", Server.ConnectionType.SSL, 50002),
new Server("fulcrum.sethforprivacy.com", Server.ConnectionType.SSL, 51002), new Server("vmd71287.contaboserver.net", Server.ConnectionType.SSL, 50002),
new Server("guichet.centure.cc", Server.ConnectionType.SSL, 50002), new Server("vmd84592.contaboserver.net", Server.ConnectionType.SSL, 50002),
new Server("hodlers.beer", Server.ConnectionType.SSL, 50002), new Server("xtrum.com", Server.ConnectionType.SSL, 50002)
new Server("kareoke.qoppa.org", Server.ConnectionType.SSL, 50002), );
new Server("kirsche.emzy.de", 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("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));
} }
@Override @Override
@ -152,12 +141,13 @@ public class Bitcoin extends Bitcoiny {
@Override @Override
public Collection<ElectrumX.Server> getServers() { public Collection<ElectrumX.Server> getServers() {
return Arrays.asList( return Arrays.asList(
new Server("tn.not.fyi", Server.ConnectionType.SSL, 55002), new Server("tn.not.fyi", Server.ConnectionType.SSL, 55002),
new Server("electrumx-test.1209k.com", Server.ConnectionType.SSL, 50002), new Server("electrumx-test.1209k.com", Server.ConnectionType.SSL, 50002),
new Server("testnet.qtornado.com", Server.ConnectionType.SSL, 51002), new Server("testnet.qtornado.com", Server.ConnectionType.SSL, 51002),
new Server("testnet.aranguren.org", Server.ConnectionType.TCP, 51001), new Server("testnet.aranguren.org", Server.ConnectionType.TCP, 51001),
new Server("testnet.aranguren.org", Server.ConnectionType.SSL, 51002), 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)
);
} }
@Override @Override
@ -179,8 +169,9 @@ public class Bitcoin extends Bitcoiny {
@Override @Override
public Collection<ElectrumX.Server> getServers() { public Collection<ElectrumX.Server> getServers() {
return Arrays.asList( return Arrays.asList(
new Server("localhost", Server.ConnectionType.TCP, 50001), new Server("localhost", Server.ConnectionType.TCP, 50001),
new Server("localhost", Server.ConnectionType.SSL, 50002)); new Server("localhost", Server.ConnectionType.SSL, 50002)
);
} }
@Override @Override

View File

@ -43,14 +43,17 @@ public class Digibyte extends Bitcoiny {
@Override @Override
public Collection<Server> getServers() { public Collection<Server> getServers() {
return Arrays.asList( return Arrays.asList(
// Servers chosen on NO BASIS WHATSOEVER from various sources! // Servers chosen on NO BASIS WHATSOEVER from various sources!
// Status verified at https://1209k.com/bitcoin-eye/ele.php?chain=dgb // Status verified at https://1209k.com/bitcoin-eye/ele.php?chain=dgb
new Server("electrum.qortal.link", Server.ConnectionType.SSL, 55002), new Server("electrum.qortal.link", Server.ConnectionType.SSL, 55002),
new Server("electrum-dgb.qortal.online", ConnectionType.SSL, 50002), new Server("electrum1-dgb.qortal.online", Server.ConnectionType.SSL, 50002),
new Server("electrum1-dgb.qortal.online", ConnectionType.SSL, 50002), new Server("electrum2-dgb.qortal.online", Server.ConnectionType.SSL, 50002),
new Server("electrum1.cipig.net", ConnectionType.SSL, 20059), new Server("electrum3-dgb.qortal.online", Server.ConnectionType.SSL, 40002),
new Server("electrum2.cipig.net", ConnectionType.SSL, 20059), new Server("electrum4-dgb.qortal.online", Server.ConnectionType.SSL, 40002),
new Server("electrum3.cipig.net", ConnectionType.SSL, 20059)); new Server("electrum1.cipig.net", Server.ConnectionType.SSL, 20059),
new Server("electrum2.cipig.net", Server.ConnectionType.SSL, 20059),
new Server("electrum3.cipig.net", Server.ConnectionType.SSL, 20059)
);
} }
@Override @Override
@ -94,8 +97,9 @@ public class Digibyte extends Bitcoiny {
@Override @Override
public Collection<Server> getServers() { public Collection<Server> getServers() {
return Arrays.asList( return Arrays.asList(
new Server("localhost", ConnectionType.TCP, 50001), new Server("localhost", Server.ConnectionType.TCP, 50001),
new Server("localhost", ConnectionType.SSL, 50002)); new Server("localhost", Server.ConnectionType.SSL, 50002)
);
} }
@Override @Override

View File

@ -4,7 +4,6 @@ import org.bitcoinj.core.Coin;
import org.bitcoinj.core.Context; import org.bitcoinj.core.Context;
import org.bitcoinj.core.NetworkParameters; import org.bitcoinj.core.NetworkParameters;
import org.libdohj.params.DogecoinMainNetParams; import org.libdohj.params.DogecoinMainNetParams;
//import org.libdohj.params.DogecoinRegTestParams;
import org.libdohj.params.DogecoinTestNet3Params; import org.libdohj.params.DogecoinTestNet3Params;
import org.qortal.crosschain.ElectrumX.Server; import org.qortal.crosschain.ElectrumX.Server;
import org.qortal.crosschain.ElectrumX.Server.ConnectionType; import org.qortal.crosschain.ElectrumX.Server.ConnectionType;
@ -44,14 +43,17 @@ public class Dogecoin extends Bitcoiny {
@Override @Override
public Collection<Server> getServers() { public Collection<Server> getServers() {
return Arrays.asList( return Arrays.asList(
// Servers chosen on NO BASIS WHATSOEVER from various sources! // Servers chosen on NO BASIS WHATSOEVER from various sources!
// Status verified at https://1209k.com/bitcoin-eye/ele.php?chain=doge // Status verified at https://1209k.com/bitcoin-eye/ele.php?chain=doge
new Server("electrum.qortal.link", Server.ConnectionType.SSL, 54002), new Server("electrum.qortal.link", Server.ConnectionType.SSL, 54002),
new Server("electrum-doge.qortal.online", ConnectionType.SSL, 50002), new Server("electrum1-doge.qortal.online", Server.ConnectionType.SSL, 50002),
new Server("electrum1-doge.qortal.online", ConnectionType.SSL, 50002), new Server("electrum2-doge.qortal.online", Server.ConnectionType.SSL, 50002),
new Server("electrum1.cipig.net", ConnectionType.SSL, 20060), new Server("electrum3-doge.qortal.online", Server.ConnectionType.SSL, 30002),
new Server("electrum2.cipig.net", ConnectionType.SSL, 20060), new Server("electrum4-doge.qortal.online", Server.ConnectionType.SSL, 30002),
new Server("electrum3.cipig.net", ConnectionType.SSL, 20060)); new Server("electrum1.cipig.net", Server.ConnectionType.SSL, 20060),
new Server("electrum2.cipig.net", Server.ConnectionType.SSL, 20060),
new Server("electrum3.cipig.net", Server.ConnectionType.SSL, 20060)
);
} }
@Override @Override
@ -95,8 +97,9 @@ public class Dogecoin extends Bitcoiny {
@Override @Override
public Collection<Server> getServers() { public Collection<Server> getServers() {
return Arrays.asList( return Arrays.asList(
new Server("localhost", ConnectionType.TCP, 50001), new Server("localhost", Server.ConnectionType.TCP, 50001),
new Server("localhost", ConnectionType.SSL, 50002)); new Server("localhost", Server.ConnectionType.SSL, 50002)
);
} }
@Override @Override

View File

@ -43,22 +43,21 @@ public class Litecoin extends Bitcoiny {
@Override @Override
public Collection<ElectrumX.Server> getServers() { public Collection<ElectrumX.Server> getServers() {
return Arrays.asList( return Arrays.asList(
// Servers chosen on NO BASIS WHATSOEVER from various sources! // Servers chosen on NO BASIS WHATSOEVER from various sources!
// Status verified at https://1209k.com/bitcoin-eye/ele.php?chain=ltc // Status verified at https://1209k.com/bitcoin-eye/ele.php?chain=ltc
//CLOSED new Server("electrum-ltc.someguy123.net", Server.ConnectionType.SSL, 50002), new Server("electrum.qortal.link", Server.ConnectionType.SSL, 50002),
//CLOSED new Server("ltc.litepay.ch", Server.ConnectionType.SSL, 50022), new Server("electrum1-ltc.qortal.online", Server.ConnectionType.SSL, 50002),
//BEHIND new Server("62.171.169.176", Server.ConnectionType.SSL, 50002), new Server("electrum2-ltc.qortal.online", Server.ConnectionType.SSL, 50002),
//PHISHY new Server("electrum-ltc.bysh.me", Server.ConnectionType.SSL, 50002), new Server("electrum3-ltc.qortal.online", Server.ConnectionType.SSL, 20002),
new Server("backup.electrum-ltc.org", Server.ConnectionType.SSL, 443), new Server("electrum4-ltc.qortal.online", Server.ConnectionType.SSL, 20002),
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.ltc.xurious.com", Server.ConnectionType.SSL, 50002),
new Server("electrum-ltc.petrkr.net", Server.ConnectionType.SSL, 60002), new Server("electrum-ltc.petrkr.net", Server.ConnectionType.SSL, 60002),
new Server("electrum-ltc.qortal.online", Server.ConnectionType.SSL, 50002), new Server("electrum1.cipig.net", Server.ConnectionType.SSL, 20063),
new Server("electrum1-ltc.qortal.online", Server.ConnectionType.SSL, 50002), new Server("electrum2.cipig.net", Server.ConnectionType.SSL, 20063),
new Server("electrum1.cipig.net", Server.ConnectionType.SSL, 20063), new Server("electrum3.cipig.net", Server.ConnectionType.SSL, 20063),
new Server("electrum2.cipig.net", Server.ConnectionType.SSL, 20063), new Server("ltc.rentonrisk.com", Server.ConnectionType.SSL, 50002)
new Server("electrum3.cipig.net", Server.ConnectionType.SSL, 20063), );
new Server("ltc.rentonrisk.com", Server.ConnectionType.SSL, 50002));
} }
@Override @Override
@ -81,10 +80,11 @@ public class Litecoin extends Bitcoiny {
@Override @Override
public Collection<ElectrumX.Server> getServers() { public Collection<ElectrumX.Server> getServers() {
return Arrays.asList( return Arrays.asList(
new Server("electrum-ltc.bysh.me", Server.ConnectionType.TCP, 51001), new Server("electrum-ltc.bysh.me", Server.ConnectionType.TCP, 51001),
new Server("electrum-ltc.bysh.me", Server.ConnectionType.SSL, 51002), 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.TCP, 51001),
new Server("electrum.ltc.xurious.com", Server.ConnectionType.SSL, 51002)); new Server("electrum.ltc.xurious.com", Server.ConnectionType.SSL, 51002)
);
} }
@Override @Override
@ -106,8 +106,9 @@ public class Litecoin extends Bitcoiny {
@Override @Override
public Collection<ElectrumX.Server> getServers() { public Collection<ElectrumX.Server> getServers() {
return Arrays.asList( return Arrays.asList(
new Server("localhost", Server.ConnectionType.TCP, 50001), new Server("localhost", Server.ConnectionType.TCP, 50001),
new Server("localhost", Server.ConnectionType.SSL, 50002)); new Server("localhost", Server.ConnectionType.SSL, 50002)
);
} }
@Override @Override

View File

@ -56,11 +56,14 @@ public class PirateChain extends Bitcoiny {
@Override @Override
public Collection<Server> getServers() { public Collection<Server> getServers() {
return Arrays.asList( return Arrays.asList(
// Servers chosen on NO BASIS WHATSOEVER from various sources! // Servers chosen on NO BASIS WHATSOEVER from various sources!
new Server("wallet-arrr1.qortal.online", ConnectionType.SSL, 443), new Server("wallet-arrr1.qortal.online", Server.ConnectionType.SSL, 443),
new Server("wallet-arrr2.qortal.online", ConnectionType.SSL, 443), new Server("wallet-arrr2.qortal.online", Server.ConnectionType.SSL, 443),
new Server("wallet-arrr3.qortal.online", ConnectionType.SSL, 443), new Server("wallet-arrr3.qortal.online", Server.ConnectionType.SSL, 443),
new Server("lightd.pirate.black", ConnectionType.SSL, 443)); new Server("wallet-arrr4.qortal.online", Server.ConnectionType.SSL, 443),
new Server("wallet-arrr5.qortal.online", Server.ConnectionType.SSL, 443),
new Server("lightd.pirate.black", Server.ConnectionType.SSL, 443)
);
} }
@Override @Override
@ -104,8 +107,9 @@ public class PirateChain extends Bitcoiny {
@Override @Override
public Collection<Server> getServers() { public Collection<Server> getServers() {
return Arrays.asList( return Arrays.asList(
new Server("localhost", ConnectionType.TCP, 9067), new Server("localhost", Server.ConnectionType.TCP, 9067),
new Server("localhost", ConnectionType.SSL, 443)); new Server("localhost", Server.ConnectionType.SSL, 443)
);
} }
@Override @Override

View File

@ -43,19 +43,19 @@ public class Ravencoin extends Bitcoiny {
@Override @Override
public Collection<Server> getServers() { public Collection<Server> getServers() {
return Arrays.asList( return Arrays.asList(
// Servers chosen on NO BASIS WHATSOEVER from various sources! // Servers chosen on NO BASIS WHATSOEVER from various sources!
// Status verified at https://1209k.com/bitcoin-eye/ele.php?chain=rvn // Status verified at https://1209k.com/bitcoin-eye/ele.php?chain=rvn
//CLOSED new Server("aethyn.com", ConnectionType.SSL, 50002), new Server("electrum.qortal.link", Server.ConnectionType.SSL, 56002),
//CLOSED new Server("electrum2.rvn.rocks", ConnectionType.SSL, 50002), new Server("electrum1-rvn.qortal.online", Server.ConnectionType.SSL, 50002),
//BEHIND new Server("electrum3.rvn.rocks", ConnectionType.SSL, 50002), new Server("electrum2-rvn.qortal.online", Server.ConnectionType.SSL, 50002),
new Server("electrum.qortal.link", Server.ConnectionType.SSL, 56002), new Server("electrum3-rvn.qortal.online", Server.ConnectionType.SSL, 50002),
new Server("electrum-rvn.qortal.online", ConnectionType.SSL, 50002), new Server("electrum4-rvn.qortal.online", Server.ConnectionType.SSL, 50002),
new Server("electrum1-rvn.qortal.online", ConnectionType.SSL, 50002), new Server("electrum1.cipig.net", Server.ConnectionType.SSL, 20051),
new Server("electrum1.cipig.net", ConnectionType.SSL, 20051), new Server("electrum2.cipig.net", Server.ConnectionType.SSL, 20051),
new Server("electrum2.cipig.net", ConnectionType.SSL, 20051), new Server("electrum3.cipig.net", Server.ConnectionType.SSL, 20051),
new Server("electrum3.cipig.net", ConnectionType.SSL, 20051), new Server("rvn-dashboard.com", Server.ConnectionType.SSL, 50002),
new Server("rvn-dashboard.com", ConnectionType.SSL, 50002), new Server("rvn4lyfe.com", Server.ConnectionType.SSL, 50002)
new Server("rvn4lyfe.com", ConnectionType.SSL, 50002)); );
} }
@Override @Override
@ -99,8 +99,9 @@ public class Ravencoin extends Bitcoiny {
@Override @Override
public Collection<Server> getServers() { public Collection<Server> getServers() {
return Arrays.asList( return Arrays.asList(
new Server("localhost", ConnectionType.TCP, 50001), new Server("localhost", Server.ConnectionType.TCP, 50001),
new Server("localhost", ConnectionType.SSL, 50002)); new Server("localhost", Server.ConnectionType.SSL, 50002)
);
} }
@Override @Override

View File

@ -75,6 +75,9 @@ public abstract class TransactionData {
@Schema(description = "groupID for this transaction") @Schema(description = "groupID for this transaction")
protected int txGroupId; protected int txGroupId;
@Schema(description = "recipient for this transaction")
protected String recipient;
// Not always present // Not always present
@Schema(accessMode = AccessMode.READ_ONLY, hidden = true, description = "height of block containing transaction") @Schema(accessMode = AccessMode.READ_ONLY, hidden = true, description = "height of block containing transaction")
protected Integer blockHeight; protected Integer blockHeight;
@ -105,7 +108,7 @@ public abstract class TransactionData {
/** Constructor for use by transaction subclasses. */ /** Constructor for use by transaction subclasses. */
protected TransactionData(TransactionType type, BaseTransactionData baseTransactionData) { protected TransactionData(TransactionType type, BaseTransactionData baseTransactionData) {
this.type = type; this.type = type;
this.recipient = baseTransactionData.recipient;
this.timestamp = baseTransactionData.timestamp; this.timestamp = baseTransactionData.timestamp;
this.txGroupId = baseTransactionData.txGroupId; this.txGroupId = baseTransactionData.txGroupId;
this.reference = baseTransactionData.reference; this.reference = baseTransactionData.reference;
@ -136,6 +139,10 @@ public abstract class TransactionData {
return this.txGroupId; return this.txGroupId;
} }
public String getRecipient() {
return this.recipient;
}
public void setTxGroupId(int txGroupId) { public void setTxGroupId(int txGroupId) {
this.txGroupId = txGroupId; this.txGroupId = txGroupId;
} }
@ -250,5 +257,4 @@ public abstract class TransactionData {
return Arrays.equals(this.signature, otherTransactionData.signature); return Arrays.equals(this.signature, otherTransactionData.signature);
} }
}
}

View File

@ -8,7 +8,6 @@ import org.bouncycastle.crypto.params.Ed25519PublicKeyParameters;
import org.qortal.block.BlockChain; import org.qortal.block.BlockChain;
import org.qortal.controller.Controller; import org.qortal.controller.Controller;
import org.qortal.controller.arbitrary.ArbitraryDataFileListManager; import org.qortal.controller.arbitrary.ArbitraryDataFileListManager;
import org.qortal.controller.arbitrary.ArbitraryDataManager;
import org.qortal.crypto.Crypto; import org.qortal.crypto.Crypto;
import org.qortal.data.block.BlockData; import org.qortal.data.block.BlockData;
import org.qortal.data.block.BlockSummaryData; import org.qortal.data.block.BlockSummaryData;
@ -122,6 +121,22 @@ public class Network {
private List<Peer> immutableOutboundHandshakedPeers = Collections.emptyList(); // always rebuilt from mutable, synced list above private List<Peer> immutableOutboundHandshakedPeers = Collections.emptyList(); // always rebuilt from mutable, synced list above
/**
* Count threads per message type in order to enforce limits
*/
private final Map<MessageType, Integer> threadsPerMessageType = Collections.synchronizedMap(new HashMap<>());
/**
* Keep track of total thread count, to warn when the thread pool is getting low
*/
private int totalThreadCount = 0;
/**
* Thresholds at which to warn about the number of active threads
*/
private final int threadCountWarningThreshold = (int) (Settings.getInstance().getMaxNetworkThreadPoolSize() * 0.9f);
private final Integer threadCountPerMessageTypeWarningThreshold = Settings.getInstance().getThreadCountPerMessageTypeWarningThreshold();
private final List<PeerAddress> selfPeers = new ArrayList<>(); private final List<PeerAddress> selfPeers = new ArrayList<>();
private String bindAddress = null; private String bindAddress = null;
@ -240,6 +255,16 @@ public class Network {
private static final Network INSTANCE = new Network(); private static final Network INSTANCE = new Network();
} }
public Map<MessageType, Integer> getThreadsPerMessageType() {
return this.threadsPerMessageType;
}
public int getTotalThreadCount() {
synchronized (this) {
return this.totalThreadCount;
}
}
public static Network getInstance() { public static Network getInstance() {
return SingletonContainer.INSTANCE; return SingletonContainer.INSTANCE;
} }
@ -952,6 +977,37 @@ public class Network {
// Should be non-handshaking messages from now on // Should be non-handshaking messages from now on
// Limit threads per message type and discard if there are already too many
Integer maxThreadsForMessageType = Settings.getInstance().getMaxThreadsForMessageType(message.getType());
if (maxThreadsForMessageType != null) {
Integer threadCount = threadsPerMessageType.get(message.getType());
if (threadCount != null && threadCount >= maxThreadsForMessageType) {
LOGGER.trace("Discarding {} message as there are already {} active threads", message.getType().name(), threadCount);
return;
}
}
// Warn if necessary
if (threadCountPerMessageTypeWarningThreshold != null) {
Integer threadCount = threadsPerMessageType.get(message.getType());
if (threadCount != null && threadCount > threadCountPerMessageTypeWarningThreshold) {
LOGGER.info("Warning: high thread count for {} message type: {}", message.getType().name(), threadCount);
}
}
// Add to per-message thread count (first initializing to 0 if not already present)
threadsPerMessageType.computeIfAbsent(message.getType(), key -> 0);
threadsPerMessageType.computeIfPresent(message.getType(), (key, value) -> value + 1);
// Add to total thread count
synchronized (this) {
totalThreadCount++;
if (totalThreadCount >= threadCountWarningThreshold) {
LOGGER.info("Warning: high total thread count: {} / {}", totalThreadCount, Settings.getInstance().getMaxNetworkThreadPoolSize());
}
}
// Ordered by message type value // Ordered by message type value
switch (message.getType()) { switch (message.getType()) {
case GET_PEERS: case GET_PEERS:
@ -979,6 +1035,15 @@ public class Network {
Controller.getInstance().onNetworkMessage(peer, message); Controller.getInstance().onNetworkMessage(peer, message);
break; break;
} }
// Remove from per-message thread count (first initializing to 0 if not already present)
threadsPerMessageType.computeIfAbsent(message.getType(), key -> 0);
threadsPerMessageType.computeIfPresent(message.getType(), (key, value) -> value - 1);
// Remove from total thread count
synchronized (this) {
totalThreadCount--;
}
} }
private void onHandshakingMessage(Peer peer, Message message, Handshake handshakeStatus) { private void onHandshakingMessage(Peer peer, Message message, Handshake handshakeStatus) {

View File

@ -1057,5 +1057,4 @@ public class HSQLDBDatabaseUpdates {
LOGGER.info(() -> String.format("HSQLDB repository updated to version %d", databaseVersion + 1)); LOGGER.info(() -> String.format("HSQLDB repository updated to version %d", databaseVersion + 1));
return true; return true;
} }
}
}

View File

@ -29,6 +29,7 @@ import org.qortal.crosschain.Dogecoin.DogecoinNet;
import org.qortal.crosschain.Digibyte.DigibyteNet; import org.qortal.crosschain.Digibyte.DigibyteNet;
import org.qortal.crosschain.Ravencoin.RavencoinNet; import org.qortal.crosschain.Ravencoin.RavencoinNet;
import org.qortal.crosschain.PirateChain.PirateChainNet; import org.qortal.crosschain.PirateChain.PirateChainNet;
import org.qortal.network.message.MessageType;
import org.qortal.utils.EnumUtils; import org.qortal.utils.EnumUtils;
// All properties to be converted to JSON via JAXB // All properties to be converted to JSON via JAXB
@ -47,8 +48,8 @@ public class Settings {
private static final int MAINNET_GATEWAY_PORT = 80; private static final int MAINNET_GATEWAY_PORT = 80;
private static final int TESTNET_GATEWAY_PORT = 8080; private static final int TESTNET_GATEWAY_PORT = 8080;
private static final int MAINNET_DEV_PROXY_PORT = 12393; private static final int MAINNET_DEV_PROXY_PORT = 12393;
private static final int TESTNET_DEV_PROXY_PORT = 62393; private static final int TESTNET_DEV_PROXY_PORT = 62393;
private static final Logger LOGGER = LogManager.getLogger(Settings.class); private static final Logger LOGGER = LogManager.getLogger(Settings.class);
private static final String SETTINGS_FILENAME = "settings.json"; private static final String SETTINGS_FILENAME = "settings.json";
@ -110,10 +111,9 @@ public class Settings {
private boolean gatewayLoggingEnabled = false; private boolean gatewayLoggingEnabled = false;
private boolean gatewayLoopbackEnabled = false; private boolean gatewayLoopbackEnabled = false;
// Developer Proxy // Developer Proxy
private Integer devProxyPort; private Integer devProxyPort;
private boolean devProxyLoggingEnabled = false; private boolean devProxyLoggingEnabled = false;
// Specific to this node // Specific to this node
private boolean wipeUnconfirmedOnStart = false; private boolean wipeUnconfirmedOnStart = false;
@ -147,7 +147,7 @@ public class Settings {
private int blockCacheSize = 10; private int blockCacheSize = 10;
/** Maximum number of transactions for the block minter to include in a block */ /** Maximum number of transactions for the block minter to include in a block */
private int maxTransactionsPerBlock = 25; private int maxTransactionsPerBlock = 50;
/** How long to keep old, full, AT state data (ms). */ /** How long to keep old, full, AT state data (ms). */
private long atStatesMaxLifetime = 5 * 24 * 60 * 60 * 1000L; // milliseconds private long atStatesMaxLifetime = 5 * 24 * 60 * 60 * 1000L; // milliseconds
@ -186,7 +186,6 @@ public class Settings {
* This has a significant effect on execution time. */ * This has a significant effect on execution time. */
private int blockPruneBatchSize = 10000; // blocks private int blockPruneBatchSize = 10000; // blocks
/** Whether we should archive old data to reduce the database size */ /** Whether we should archive old data to reduce the database size */
private boolean archiveEnabled = true; private boolean archiveEnabled = true;
/** How often to attempt archiving (ms). */ /** How often to attempt archiving (ms). */
@ -194,15 +193,12 @@ public class Settings {
/** Serialization version to use when building an archive */ /** Serialization version to use when building an archive */
private int defaultArchiveVersion = 2; private int defaultArchiveVersion = 2;
/** Whether to automatically bootstrap instead of syncing from genesis */ /** Whether to automatically bootstrap instead of syncing from genesis */
private boolean bootstrap = true; private boolean bootstrap = true;
/** Registered names integrity check */ /** Registered names integrity check */
private boolean namesIntegrityCheckEnabled = false; private boolean namesIntegrityCheckEnabled = false;
// Peer-to-peer related // Peer-to-peer related
private boolean isTestNet = false; private boolean isTestNet = false;
/** Single node testnet mode */ /** Single node testnet mode */
@ -227,10 +223,10 @@ public class Settings {
private int maxRetries = 2; private int maxRetries = 2;
/** The number of seconds of no activity before recovery mode begins */ /** The number of seconds of no activity before recovery mode begins */
public long recoveryModeTimeout = 24 * 60 * 60 * 1000L; public long recoveryModeTimeout = 9999999999999L;
/** Minimum peer version number required in order to sync with them */ /** Minimum peer version number required in order to sync with them */
private String minPeerVersion = "4.1.2"; private String minPeerVersion = "4.3.1";
/** Whether to allow connections with peers below minPeerVersion /** 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 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 */ * If false, sync will be blocked both ways, and they will not appear in the peers list */
@ -289,10 +285,10 @@ public class Settings {
// Bootstrap sources // Bootstrap sources
private String[] bootstrapHosts = new String[] { private String[] bootstrapHosts = new String[] {
"http://bootstrap.qortal.org", "http://bootstrap.qortal.org",
"http://bootstrap2.qortal.org", "http://bootstrap2.qortal.org",
"http://bootstrap3.qortal.org", "http://bootstrap3.qortal.org",
"http://bootstrap.qortal.online" "http://bootstrap.qortal.online"
}; };
// Auto-update sources // Auto-update sources
@ -311,17 +307,35 @@ public class Settings {
"1.pool.ntp.org", "1.pool.ntp.org",
"2.pool.ntp.org", "2.pool.ntp.org",
"3.pool.ntp.org", "3.pool.ntp.org",
"cn.pool.ntp.org", "asia.pool.ntp.org",
"0.cn.pool.ntp.org", "0.asia.pool.ntp.org",
"1.cn.pool.ntp.org", "1.asia.pool.ntp.org",
"2.cn.pool.ntp.org", "2.asia.pool.ntp.org",
"3.cn.pool.ntp.org" "3.asia.pool.ntp.org",
"europe.pool.ntp.org",
"0.europe.pool.ntp.org",
"1.europe.pool.ntp.org",
"2.europe.pool.ntp.org",
"3.europe.pool.ntp.org",
"north-america.pool.ntp.org",
"0.north-america.pool.ntp.org",
"1.north-america.pool.ntp.org",
"2.north-america.pool.ntp.org",
"3.north-america.pool.ntp.org",
"oceania.pool.ntp.org",
"0.oceania.pool.ntp.org",
"1.oceania.pool.ntp.org",
"2.oceania.pool.ntp.org",
"3.oceania.pool.ntp.org",
"south-america.pool.ntp.org",
"0.south-america.pool.ntp.org",
"1.south-america.pool.ntp.org",
"2.south-america.pool.ntp.org",
"3.south-america.pool.ntp.org"
}; };
/** Additional offset added to values returned by NTP.getTime() */ /** Additional offset added to values returned by NTP.getTime() */
private Long testNtpOffset = null; private Long testNtpOffset = null;
/* Foreign chains */ /* Foreign chains */
/** The number of consecutive empty addresses required before treating a wallet's transaction set as complete */ /** The number of consecutive empty addresses required before treating a wallet's transaction set as complete */
@ -330,8 +344,6 @@ public class Settings {
/** How many wallet keys to generate when using bitcoinj as the blockchain interface (e.g. when sending coins) */ /** How many wallet keys to generate when using bitcoinj as the blockchain interface (e.g. when sending coins) */
private int bitcoinjLookaheadSize = 50; private int bitcoinjLookaheadSize = 50;
// Data storage (QDN) // Data storage (QDN)
/** Data storage enabled/disabled*/ /** Data storage enabled/disabled*/
@ -371,6 +383,58 @@ public class Settings {
/** Whether to serve QDN data without authentication */ /** Whether to serve QDN data without authentication */
private boolean qdnAuthBypassEnabled = true; private boolean qdnAuthBypassEnabled = true;
/** Limit threads per message type */
private Set<ThreadLimit> maxThreadsPerMessageType = new HashSet<>();
/** The number of threads per message type at which a warning should be logged.
* Exclude from settings.json to disable this warning. */
private Integer threadCountPerMessageTypeWarningThreshold = null;
// Domain mapping
public static class ThreadLimit {
private String messageType;
private Integer limit;
private ThreadLimit() { // makes JAXB happy; will never be invoked
}
private ThreadLimit(String messageType, Integer limit) {
this.messageType = messageType;
this.limit = limit;
}
public String getMessageType() {
return messageType;
}
public void setMessageType(String messageType) {
this.messageType = messageType;
}
public Integer getLimit() {
return limit;
}
public void setLimit(Integer limit) {
this.limit = limit;
}
@Override
public boolean equals(Object other) {
if (!(other instanceof ThreadLimit))
return false;
return this.messageType.equals(((ThreadLimit) other).getMessageType());
}
@Override
public int hashCode() {
return Objects.hash(messageType);
}
}
// Domain mapping // Domain mapping
public static class DomainMap { public static class DomainMap {
private String domain; private String domain;
@ -396,7 +460,6 @@ public class Settings {
} }
} }
// Constructors // Constructors
private Settings() { private Settings() {
@ -497,6 +560,9 @@ public class Settings {
} }
} while (settings.userPath != null); } while (settings.userPath != null);
// Set some additional defaults if needed
settings.setAdditionalDefaults();
// Validate settings // Validate settings
settings.validate(); settings.validate();
@ -533,6 +599,22 @@ public class Settings {
} }
} }
private void setAdditionalDefaults() {
// Populate defaults for maxThreadsPerMessageType. If any are specified in settings.json, they will take priority.
maxThreadsPerMessageType.add(new ThreadLimit("ARBITRARY_DATA_FILE", 5));
maxThreadsPerMessageType.add(new ThreadLimit("GET_ARBITRARY_DATA_FILE", 5));
maxThreadsPerMessageType.add(new ThreadLimit("ARBITRARY_DATA", 5));
maxThreadsPerMessageType.add(new ThreadLimit("GET_ARBITRARY_DATA", 5));
maxThreadsPerMessageType.add(new ThreadLimit("ARBITRARY_DATA_FILE_LIST", 5));
maxThreadsPerMessageType.add(new ThreadLimit("GET_ARBITRARY_DATA_FILE_LIST", 5));
maxThreadsPerMessageType.add(new ThreadLimit("ARBITRARY_SIGNATURES", 5));
maxThreadsPerMessageType.add(new ThreadLimit("ARBITRARY_METADATA", 5));
maxThreadsPerMessageType.add(new ThreadLimit("GET_ARBITRARY_METADATA", 5));
maxThreadsPerMessageType.add(new ThreadLimit("GET_TRANSACTION", 10));
maxThreadsPerMessageType.add(new ThreadLimit("TRANSACTION_SIGNATURES", 5));
maxThreadsPerMessageType.add(new ThreadLimit("TRADE_PRESENCES", 5));
}
// Getters / setters // Getters / setters
public String getUserPath() { public String getUserPath() {
@ -660,17 +742,16 @@ public class Settings {
} }
public int getDevProxyPort() { public int getDevProxyPort() {
if (this.devProxyPort != null) if (this.devProxyPort != null)
return this.devProxyPort; return this.devProxyPort;
return this.isTestNet ? TESTNET_DEV_PROXY_PORT : MAINNET_DEV_PROXY_PORT; return this.isTestNet ? TESTNET_DEV_PROXY_PORT : MAINNET_DEV_PROXY_PORT;
} }
public boolean isDevProxyLoggingEnabled() {
return this.devProxyLoggingEnabled;
}
public boolean isDevProxyLoggingEnabled() {
return this.devProxyLoggingEnabled;
}
public boolean getWipeUnconfirmedOnStart() { public boolean getWipeUnconfirmedOnStart() {
return this.wipeUnconfirmedOnStart; return this.wipeUnconfirmedOnStart;
@ -1054,4 +1135,20 @@ public class Settings {
} }
return this.qdnAuthBypassEnabled; return this.qdnAuthBypassEnabled;
} }
public Integer getMaxThreadsForMessageType(MessageType messageType) {
if (maxThreadsPerMessageType != null) {
for (ThreadLimit threadLimit : maxThreadsPerMessageType) {
if (threadLimit.getMessageType().equals(messageType.name())) {
return threadLimit.getLimit();
}
}
}
// No entry, so assume unlimited
return null;
}
public Integer getThreadCountPerMessageTypeWarningThreshold() {
return this.threadCountPerMessageTypeWarningThreshold;
}
} }

View File

@ -99,8 +99,14 @@ public class ArbitraryTransaction extends Transaction {
if (this.transactionData.getFee() < 0) if (this.transactionData.getFee() < 0)
return ValidationResult.NEGATIVE_FEE; return ValidationResult.NEGATIVE_FEE;
// After the feature trigger, we require the fee to be sufficient if it's not 0. // As of the mempow transaction updates timestamp, a nonce is no longer supported, so a valid fee must be included
// If the fee is zero, then the nonce is validated in isSignatureValid() as an alternative to a fee if (this.transactionData.getTimestamp() >= BlockChain.getInstance().getMemPoWTransactionUpdatesTimestamp()) {
// Validate the fee
return super.isFeeValid();
}
// After the earlier "optional fee" feature trigger, we required the fee to be sufficient if it wasn't 0.
// If the fee was zero, then the nonce was validated in isSignatureValid() as an alternative to a fee
if (this.arbitraryTransactionData.getTimestamp() >= BlockChain.getInstance().getArbitraryOptionalFeeTimestamp() && this.arbitraryTransactionData.getFee() != 0L) { if (this.arbitraryTransactionData.getTimestamp() >= BlockChain.getInstance().getArbitraryOptionalFeeTimestamp() && this.arbitraryTransactionData.getFee() != 0L) {
return super.isFeeValid(); return super.isFeeValid();
} }
@ -225,7 +231,13 @@ public class ArbitraryTransaction extends Transaction {
// Clear nonce from transactionBytes // Clear nonce from transactionBytes
ArbitraryTransactionTransformer.clearNonce(transactionBytes); ArbitraryTransactionTransformer.clearNonce(transactionBytes);
// As of feature-trigger timestamp, we only require a nonce when the fee is zero // As of the mempow transaction updates timestamp, a nonce is no longer supported, so a fee must be included
if (this.transactionData.getTimestamp() >= BlockChain.getInstance().getMemPoWTransactionUpdatesTimestamp()) {
// Require that the fee is a positive number. Fee checking itself is performed in isFeeValid()
return (this.arbitraryTransactionData.getFee() > 0L);
}
// As of the earlier "optional fee" feature-trigger timestamp, we only required a nonce when the fee was zero
boolean beforeFeatureTrigger = this.arbitraryTransactionData.getTimestamp() < BlockChain.getInstance().getArbitraryOptionalFeeTimestamp(); boolean beforeFeatureTrigger = this.arbitraryTransactionData.getTimestamp() < BlockChain.getInstance().getArbitraryOptionalFeeTimestamp();
if (beforeFeatureTrigger || this.arbitraryTransactionData.getFee() == 0L) { if (beforeFeatureTrigger || this.arbitraryTransactionData.getFee() == 0L) {
// We only need to check nonce for recent transactions due to PoW verification overhead // We only need to check nonce for recent transactions due to PoW verification overhead

View File

@ -148,6 +148,12 @@ public class ChatTransaction extends Transaction {
// Nothing to do // Nothing to do
} }
@Override
public boolean isConfirmable() {
// CHAT transactions can't go into blocks
return false;
}
@Override @Override
public ValidationResult isValid() throws DataException { public ValidationResult isValid() throws DataException {
// Nonce checking is done via isSignatureValid() as that method is only called once per import // Nonce checking is done via isSignatureValid() as that method is only called once per import

View File

@ -33,7 +33,9 @@ public class MessageTransaction extends Transaction {
public static final int MAX_DATA_SIZE = 4000; public static final int MAX_DATA_SIZE = 4000;
public static final int POW_BUFFER_SIZE = 8 * 1024 * 1024; // bytes public static final int POW_BUFFER_SIZE = 8 * 1024 * 1024; // bytes
public static final int POW_DIFFICULTY = 14; // leading zero bits public static final int POW_DIFFICULTY_V1 = 14; // leading zero bits
public static final int POW_DIFFICULTY_V2_CONFIRMABLE = 16; // leading zero bits
public static final int POW_DIFFICULTY_V2_UNCONFIRMABLE = 12; // leading zero bits
// Properties // Properties
@ -109,7 +111,17 @@ public class MessageTransaction extends Transaction {
MessageTransactionTransformer.clearNonce(transactionBytes); MessageTransactionTransformer.clearNonce(transactionBytes);
// Calculate nonce // Calculate nonce
this.messageTransactionData.setNonce(MemoryPoW.compute2(transactionBytes, POW_BUFFER_SIZE, POW_DIFFICULTY)); this.messageTransactionData.setNonce(MemoryPoW.compute2(transactionBytes, POW_BUFFER_SIZE, getPoWDifficulty()));
}
public int getPoWDifficulty() {
// The difficulty changes at the "mempow transactions updates" timestamp
if (this.transactionData.getTimestamp() >= BlockChain.getInstance().getMemPoWTransactionUpdatesTimestamp()) {
// If this message is confirmable then require a higher difficulty
return this.isConfirmable() ? POW_DIFFICULTY_V2_CONFIRMABLE : POW_DIFFICULTY_V2_UNCONFIRMABLE;
}
// Before feature trigger timestamp, so use existing difficulty value
return POW_DIFFICULTY_V1;
} }
/** /**
@ -183,6 +195,18 @@ public class MessageTransaction extends Transaction {
return super.hasValidReference(); return super.hasValidReference();
} }
@Override
public boolean isConfirmable() {
// After feature trigger timestamp, only messages to an AT address can confirm
if (this.transactionData.getTimestamp() >= BlockChain.getInstance().getMemPoWTransactionUpdatesTimestamp()) {
if (this.messageTransactionData.getRecipient() == null || !this.messageTransactionData.getRecipient().toUpperCase().startsWith("A")) {
// Message isn't to an AT address, so this transaction is unconfirmable
return false;
}
}
return true;
}
@Override @Override
public ValidationResult isValid() throws DataException { public ValidationResult isValid() throws DataException {
// Nonce checking is done via isSignatureValid() as that method is only called once per import // Nonce checking is done via isSignatureValid() as that method is only called once per import
@ -235,7 +259,7 @@ public class MessageTransaction extends Transaction {
MessageTransactionTransformer.clearNonce(transactionBytes); MessageTransactionTransformer.clearNonce(transactionBytes);
// Check nonce // Check nonce
return MemoryPoW.verify2(transactionBytes, POW_BUFFER_SIZE, POW_DIFFICULTY, nonce); return MemoryPoW.verify2(transactionBytes, POW_BUFFER_SIZE, getPoWDifficulty(), nonce);
} }
@Override @Override
@ -256,6 +280,11 @@ public class MessageTransaction extends Transaction {
@Override @Override
public void process() throws DataException { public void process() throws DataException {
// Only certain MESSAGE transactions are able to confirm
if (!this.isConfirmable()) {
throw new DataException("Unconfirmable MESSAGE transactions should never be processed");
}
// If we have no amount then there's nothing to do // If we have no amount then there's nothing to do
if (this.messageTransactionData.getAmount() == 0L) if (this.messageTransactionData.getAmount() == 0L)
return; return;
@ -280,6 +309,11 @@ public class MessageTransaction extends Transaction {
@Override @Override
public void orphan() throws DataException { public void orphan() throws DataException {
// Only certain MESSAGE transactions are able to confirm
if (!this.isConfirmable()) {
throw new DataException("Unconfirmable MESSAGE transactions should never be orphaned");
}
// If we have no amount then there's nothing to do // If we have no amount then there's nothing to do
if (this.messageTransactionData.getAmount() == 0L) if (this.messageTransactionData.getAmount() == 0L)
return; return;

View File

@ -155,6 +155,12 @@ public class PresenceTransaction extends Transaction {
// Nothing to do // Nothing to do
} }
@Override
public boolean isConfirmable() {
// PRESENCE transactions can't go into blocks
return false;
}
@Override @Override
public ValidationResult isValid() throws DataException { public ValidationResult isValid() throws DataException {
// Nonce checking is done via isSignatureValid() as that method is only called once per import // Nonce checking is done via isSignatureValid() as that method is only called once per import

View File

@ -7,6 +7,7 @@ import org.qortal.account.Account;
import org.qortal.account.PublicKeyAccount; import org.qortal.account.PublicKeyAccount;
import org.qortal.api.resource.TransactionsResource.ConfirmationStatus; import org.qortal.api.resource.TransactionsResource.ConfirmationStatus;
import org.qortal.asset.Asset; import org.qortal.asset.Asset;
import org.qortal.block.BlockChain;
import org.qortal.crypto.MemoryPoW; import org.qortal.crypto.MemoryPoW;
import org.qortal.data.transaction.PublicizeTransactionData; import org.qortal.data.transaction.PublicizeTransactionData;
import org.qortal.data.transaction.TransactionData; import org.qortal.data.transaction.TransactionData;
@ -89,6 +90,12 @@ public class PublicizeTransaction extends Transaction {
@Override @Override
public ValidationResult isValid() throws DataException { public ValidationResult isValid() throws DataException {
// Disable completely after feature-trigger timestamp, at the same time that mempow difficulties are being increased.
// It could be enabled again in the future, but preferably with an enforced minimum fee instead of allowing a mempow nonce.
if (this.transactionData.getTimestamp() >= BlockChain.getInstance().getMemPoWTransactionUpdatesTimestamp()) {
return ValidationResult.NOT_SUPPORTED;
}
// There can be only one // There can be only one
List<byte[]> signatures = this.repository.getTransactionRepository().getSignaturesMatchingCriteria( List<byte[]> signatures = this.repository.getTransactionRepository().getSignaturesMatchingCriteria(
TransactionType.PUBLICIZE, TransactionType.PUBLICIZE,

View File

@ -13,6 +13,7 @@ import org.qortal.account.PublicKeyAccount;
import org.qortal.asset.Asset; import org.qortal.asset.Asset;
import org.qortal.block.BlockChain; import org.qortal.block.BlockChain;
import org.qortal.controller.Controller; import org.qortal.controller.Controller;
import org.qortal.controller.TransactionImporter;
import org.qortal.crypto.Crypto; import org.qortal.crypto.Crypto;
import org.qortal.data.block.BlockData; import org.qortal.data.block.BlockData;
import org.qortal.data.group.GroupApprovalData; import org.qortal.data.group.GroupApprovalData;
@ -247,7 +248,8 @@ public abstract class Transaction {
GROUP_APPROVAL_REQUIRED(98), GROUP_APPROVAL_REQUIRED(98),
ACCOUNT_NOT_TRANSFERABLE(99), ACCOUNT_NOT_TRANSFERABLE(99),
INVALID_BUT_OK(999), INVALID_BUT_OK(999),
NOT_YET_RELEASED(1000); NOT_YET_RELEASED(1000),
NOT_SUPPORTED(1001);
public final int value; public final int value;
@ -377,7 +379,7 @@ public abstract class Transaction {
* @return * @return
*/ */
public long getUnitFee(Long timestamp) { public long getUnitFee(Long timestamp) {
return BlockChain.getInstance().getUnitFee(); return BlockChain.getInstance().getUnitFeeAtTimestamp(timestamp);
} }
/** /**
@ -617,7 +619,10 @@ public abstract class Transaction {
} }
private int countUnconfirmedByCreator(PublicKeyAccount creator) throws DataException { private int countUnconfirmedByCreator(PublicKeyAccount creator) throws DataException {
List<TransactionData> unconfirmedTransactions = repository.getTransactionRepository().getUnconfirmedTransactions(); List<TransactionData> unconfirmedTransactions = TransactionImporter.getInstance().unconfirmedTransactionsCache;
if (unconfirmedTransactions == null) {
unconfirmedTransactions = repository.getTransactionRepository().getUnconfirmedTransactions();
}
// We exclude CHAT transactions as they never get included into blocks and // We exclude CHAT transactions as they never get included into blocks and
// have spam/DoS prevention by requiring proof of work // have spam/DoS prevention by requiring proof of work
@ -632,7 +637,7 @@ public abstract class Transaction {
} }
/** /**
* Returns sorted, unconfirmed transactions, excluding invalid. * Returns sorted, unconfirmed transactions, excluding invalid and unconfirmable.
* *
* @return sorted, unconfirmed transactions * @return sorted, unconfirmed transactions
* @throws DataException * @throws DataException
@ -650,7 +655,8 @@ public abstract class Transaction {
TransactionData transactionData = unconfirmedTransactionsIterator.next(); TransactionData transactionData = unconfirmedTransactionsIterator.next();
Transaction transaction = Transaction.fromData(repository, transactionData); Transaction transaction = Transaction.fromData(repository, transactionData);
if (transaction.isStillValidUnconfirmed(latestBlockData.getTimestamp()) != ValidationResult.OK) // Must be confirmable and valid
if (!transaction.isConfirmable() || transaction.isStillValidUnconfirmed(latestBlockData.getTimestamp()) != ValidationResult.OK)
unconfirmedTransactionsIterator.remove(); unconfirmedTransactionsIterator.remove();
} }
@ -888,6 +894,17 @@ public abstract class Transaction {
/* To be optionally overridden */ /* To be optionally overridden */
} }
/**
* Returns whether transaction is 'confirmable' - i.e. is of a type that
* can be included in a block. Some transactions are 'unconfirmable'
* and therefore must remain in the mempool until they expire.
* @return
*/
public boolean isConfirmable() {
/* To be optionally overridden */
return true;
}
/** /**
* Returns whether transaction can be added to the blockchain. * Returns whether transaction can be added to the blockchain.
* <p> * <p>

View File

@ -3,8 +3,12 @@
"transactionExpiryPeriod": 86400000, "transactionExpiryPeriod": 86400000,
"maxBlockSize": 2097152, "maxBlockSize": 2097152,
"maxBytesPerUnitFee": 1024, "maxBytesPerUnitFee": 1024,
"unitFee": "0.001", "unitFees": [
{ "timestamp": 0, "fee": "0.001" },
{ "timestamp": 1692118800000, "fee": "0.01" }
],
"nameRegistrationUnitFees": [ "nameRegistrationUnitFees": [
{ "timestamp": 0, "fee": "0.001" },
{ "timestamp": 1645372800000, "fee": "5" }, { "timestamp": 1645372800000, "fee": "5" },
{ "timestamp": 1651420800000, "fee": "1.25" } { "timestamp": 1651420800000, "fee": "1.25" }
], ],
@ -18,13 +22,15 @@
"maxRewardSharesPerFounderMintingAccount": 6, "maxRewardSharesPerFounderMintingAccount": 6,
"maxRewardSharesByTimestamp": [ "maxRewardSharesByTimestamp": [
{ "timestamp": 0, "maxShares": 6 }, { "timestamp": 0, "maxShares": 6 },
{ "timestamp": 1657382400000, "maxShares": 3 } { "timestamp": 1657382400000, "maxShares": 3 },
{ "timestamp": 1698508800000, "maxShares": 2 }
], ],
"founderEffectiveMintingLevel": 10, "founderEffectiveMintingLevel": 10,
"onlineAccountSignaturesMinLifetime": 43200000, "onlineAccountSignaturesMinLifetime": 43200000,
"onlineAccountSignaturesMaxLifetime": 86400000, "onlineAccountSignaturesMaxLifetime": 86400000,
"onlineAccountsModulusV2Timestamp": 1659801600000, "onlineAccountsModulusV2Timestamp": 1659801600000,
"selfSponsorshipAlgoV1SnapshotTimestamp": 1670230000000, "selfSponsorshipAlgoV1SnapshotTimestamp": 1670230000000,
"mempowTransactionUpdatesTimestamp": 1693558800000,
"rewardsByHeight": [ "rewardsByHeight": [
{ "height": 1, "reward": 5.00 }, { "height": 1, "reward": 5.00 },
{ "height": 259201, "reward": 4.75 }, { "height": 259201, "reward": 4.75 },

View File

@ -4,52 +4,52 @@
# "localeLang": "de", # "localeLang": "de",
### Common ### ### Common ###
JSON = JSON Nachricht konnte nicht geparst werden JSON = JSON-Nachricht konnte nicht geparst werden
INSUFFICIENT_BALANCE = Kein Ausgleich INSUFFICIENT_BALANCE = Guthaben reicht nicht aus
UNAUTHORIZED = API-Aufruf nicht autorisiert UNAUTHORIZED = API-Aufruf nicht autorisiert
REPOSITORY_ISSUE = Repository-Fehler REPOSITORY_ISSUE = Repository-Fehler
NON_PRODUCTION = Dieser APi-Aufruf ist nicht gestattet für Produtkion NON_PRODUCTION = dieser API-Aufruf ist für Produktionssysteme nicht gestattet
BLOCKCHAIN_NEEDS_SYNC = Blockchain muss sich erst verbinden BLOCKCHAIN_NEEDS_SYNC = Blockchain muss sich erst synchronisieren
NO_TIME_SYNC = noch keine Uhrensynchronisation NO_TIME_SYNC = Uhrzeit noch nicht synchronisiert
### Validation ### ### Validation ###
INVALID_SIGNATURE = ungültige Signatur INVALID_SIGNATURE = Signatur ungültig
INVALID_ADDRESS = ungültige Adresse INVALID_ADDRESS = Adresse ungültig
INVALID_PUBLIC_KEY = ungültiger public key INVALID_PUBLIC_KEY = öffentlicher Schlüssel ungültig
INVALID_DATA = ungültige Daten INVALID_DATA = Daten ungültig
INVALID_NETWORK_ADDRESS = ungültige Netzwerk Adresse INVALID_NETWORK_ADDRESS = Netzwerk Adresse ungültig
ADDRESS_UNKNOWN = Account Adresse unbekannt ADDRESS_UNKNOWN = Kontoadresse unbekannt
INVALID_CRITERIA = ungültige Suchkriterien INVALID_CRITERIA = Suchkriterien ungültig
INVALID_REFERENCE = ungültige Referenz INVALID_REFERENCE = Referenz ungültig
TRANSFORMATION_ERROR = konnte JSON nicht in eine Transaktion umwandeln TRANSFORMATION_ERROR = konnte JSON nicht in eine Transaktion umwandeln
INVALID_PRIVATE_KEY = ungültiger private key INVALID_PRIVATE_KEY = öffentlicher Schlüssel ungültig
INVALID_HEIGHT = ungültige block height INVALID_HEIGHT = Blockhöhe ungültig
CANNOT_MINT = Account kann nicht minten CANNOT_MINT = Konto kann nicht prägen
### Blocks ### ### Blocks ###
BLOCK_UNKNOWN = block unbekannt BLOCK_UNKNOWN = Block unbekannt
### Transactions ### ### Transactions ###
TRANSACTION_UNKNOWN = Transaktion unbekannt TRANSACTION_UNKNOWN = Transaktion unbekannt
PUBLIC_KEY_NOT_FOUND = public key wurde nicht gefunden PUBLIC_KEY_NOT_FOUND = öffentlicher Schlüssel wurde nicht gefunden
# this one is special in that caller expected to pass two additional strings, hence the two %s # this one is special in that caller expected to pass two additional strings, hence the two %s
TRANSACTION_INVALID = Transaktion ungültig: %s (%s) TRANSACTION_INVALID = Transaktion ungültig: %s (%s)
@ -58,19 +58,19 @@ TRANSACTION_INVALID = Transaktion ungültig: %s (%s)
NAME_UNKNOWN = Name unbekannt NAME_UNKNOWN = Name unbekannt
### Asset ### ### Asset ###
INVALID_ASSET_ID = ungültige asset ID INVALID_ASSET_ID = Vermögenswert-Kennung ungültig
INVALID_ORDER_ID = ungültige asset order ID INVALID_ORDER_ID = Vermögenswert-Auftragssnummer ungültig
ORDER_UNKNOWN = unbekannte asset order ID ORDER_UNKNOWN = Vermögenswert-Auftragssnummer unbekannt
### Groups ### ### Groups ###
GROUP_UNKNOWN = Gruppe unbekannt GROUP_UNKNOWN = Gruppe unbekannt
### Foreign Blockchain ### ### Foreign Blockchain ###
FOREIGN_BLOCKCHAIN_NETWORK_ISSUE = fremde Blockchain oder ElectrumX Netzwerk Problem FOREIGN_BLOCKCHAIN_NETWORK_ISSUE = fremde Blockchain oder ElectrumX Netzwerkproblem
FOREIGN_BLOCKCHAIN_BALANCE_ISSUE = unzureichend Bilanz auf fremde blockchain FOREIGN_BLOCKCHAIN_BALANCE_ISSUE = unzureichendes Guthaben auf fremder blockchain
FOREIGN_BLOCKCHAIN_TOO_SOON = zu früh um fremde Blockchain-Transaktionen zu übertragen (Sperrzeit/mittlere Blockzeit) FOREIGN_BLOCKCHAIN_TOO_SOON = zu früh um fremde Blockchain-Transaktionen zu übertragen (Sperrzeit/mittlere Blockzeit)
@ -80,4 +80,4 @@ ORDER_SIZE_TOO_SMALL = Bestellmenge zu niedrig
### Data ### ### Data ###
FILE_NOT_FOUND = Datei nicht gefunden FILE_NOT_FOUND = Datei nicht gefunden
NO_REPLY = Peer hat nicht mit Daten verbinden NO_REPLY = Peer hat nicht in vorgegebener Zeit geantwortet

View File

@ -4,36 +4,36 @@
# "localeLang": "nl", # "localeLang": "nl",
### Common ### ### Common ###
JSON = lezen van JSON bericht gefaald JSON = lezen van JSON bericht is mislukt
INSUFFICIENT_BALANCE = insufficient balance INSUFFICIENT_BALANCE = onvoldoende saldo
UNAUTHORIZED = ongeautoriseerde API call UNAUTHORIZED = ongeautoriseerde API call
REPOSITORY_ISSUE = repository fout REPOSITORY_ISSUE = fout in repository
NON_PRODUCTION = deze API call is niet toegestaan voor productiesystemen NON_PRODUCTION = deze API call is niet toegestaan voor productiesystemen
BLOCKCHAIN_NEEDS_SYNC = blockchain dient eerst gesynchronizeerd te worden BLOCKCHAIN_NEEDS_SYNC = blockchain dient eerst te synchronizeren
NO_TIME_SYNC = klok nog niet gesynchronizeerd NO_TIME_SYNC = klok is nog niet gesynchronizeerd
### Validation ### ### Validation ###
INVALID_SIGNATURE = ongeldige handtekening INVALID_SIGNATURE = ongeldige signature
INVALID_ADDRESS = ongeldig adres INVALID_ADDRESS = ongeldig adres
INVALID_PUBLIC_KEY = ongeldige public key INVALID_PUBLIC_KEY = ongeldige public key
INVALID_DATA = ongeldige gegevens INVALID_DATA = ongeldige data
INVALID_NETWORK_ADDRESS = ongeldig netwerkadres INVALID_NETWORK_ADDRESS = ongeldig netwerkadres
ADDRESS_UNKNOWN = account adres onbekend ADDRESS_UNKNOWN = account-adres onbekend
INVALID_CRITERIA = ongeldige zoekcriteria INVALID_CRITERIA = ongeldige zoekcriteria
INVALID_REFERENCE = ongeldige verwijzing INVALID_REFERENCE = ongeldige referentie
TRANSFORMATION_ERROR = JSON kon niet omgezet worden in transactie TRANSFORMATION_ERROR = JSON kon niet omgezet worden in transactie
@ -44,10 +44,10 @@ INVALID_HEIGHT = ongeldige blokhoogte
CANNOT_MINT = account kan niet minten CANNOT_MINT = account kan niet minten
### Blocks ### ### Blocks ###
BLOCK_UNKNOWN = blok onbekend BLOCK_UNKNOWN = blok niet gekend
### Transactions ### ### Transactions ###
TRANSACTION_UNKNOWN = onbekende transactie TRANSACTION_UNKNOWN = transactie niet gekend
PUBLIC_KEY_NOT_FOUND = public key niet gevonden PUBLIC_KEY_NOT_FOUND = public key niet gevonden
@ -55,29 +55,29 @@ PUBLIC_KEY_NOT_FOUND = public key niet gevonden
TRANSACTION_INVALID = ongeldige transactie: %s (%s) TRANSACTION_INVALID = ongeldige transactie: %s (%s)
### Naming ### ### Naming ###
NAME_UNKNOWN = onbekende naam NAME_UNKNOWN = naam niet gekend
### Asset ### ### Asset ###
INVALID_ASSET_ID = ongeldige asset ID INVALID_ASSET_ID = ongeldige asset ID
INVALID_ORDER_ID = ongeldige asset order ID INVALID_ORDER_ID = ongeldige asset order ID
ORDER_UNKNOWN = onbekende asset order ID ORDER_UNKNOWN = niet gekende asset order ID
### Groups ### ### Groups ###
GROUP_UNKNOWN = onbekende groep GROUP_UNKNOWN = groep niet gekend
### Foreign Blockchain ### ### Foreign Blockchain ###
FOREIGN_BLOCKCHAIN_NETWORK_ISSUE = blockchain of ElectrumX network probleem FOREIGN_BLOCKCHAIN_NETWORK_ISSUE = vreemde blockchain of ElectrumX networkprobleem
FOREIGN_BLOCKCHAIN_BALANCE_ISSUE = onvoldoende saldo blockchain FOREIGN_BLOCKCHAIN_BALANCE_ISSUE = onvoldoende saldo bij vreemde blockchain
FOREIGN_BLOCKCHAIN_TOO_SOON = nog niet gereed om de blockchain transactie uittevoeren (LockTime/median block time) FOREIGN_BLOCKCHAIN_TOO_SOON = te vroeg om de blockchain transactie uit te sturen (LockTime/median block time)
### Trade Portal ### ### Trade Portal ###
ORDER_SIZE_TOO_SMALL = order bedrag te laag ORDER_SIZE_TOO_SMALL = order-bedrag te laag
### Data ### ### Data ###
FILE_NOT_FOUND = file niet gevonden FILE_NOT_FOUND = bestand niet gevonden
NO_REPLY = peer reageerd niet met data NO_REPLY = peer reageerde niet binnen toegelaten tijd

View File

@ -1,7 +1,7 @@
#Generated by ResourceBundle Editor (http://essiembre.github.io/eclipse-rbe/) #Generated by ResourceBundle Editor (http://essiembre.github.io/eclipse-rbe/)
# SysTray pop-up menu # SysTray pop-up menu
APPLYING_UPDATE_AND_RESTARTING = Automatisches Update anwenden und neu starten APPLYING_UPDATE_AND_RESTARTING = Automatisches Update anwenden und neu starten...
AUTO_UPDATE = Automatisches Update AUTO_UPDATE = Automatisches Update
@ -19,7 +19,7 @@ CONNECTION = Verbindung
CONNECTIONS = Verbindungen CONNECTIONS = Verbindungen
CREATING_BACKUP_OF_DB_FILES = Erstellen Backup von Datenbank Dateien … CREATING_BACKUP_OF_DB_FILES = Erstelle Backup von Datenbank Dateien...
DB_BACKUP = Datenbank Backup DB_BACKUP = Datenbank Backup
@ -31,18 +31,18 @@ EXIT = Verlassen
LITE_NODE = Lite node LITE_NODE = Lite node
MINTING_DISABLED = Kein minting MINTING_DISABLED = Münzprägung inaktiv
MINTING_ENABLED = \u2714 Minting aktiviert MINTING_ENABLED = \u2714 Münzprägung aktiv
OPEN_UI = Öffne UI OPEN_UI = Öffne Benutzeroberfläche
PERFORMING_DB_CHECKPOINT = Speichern von unbestätigten Datenbankänderungen... PERFORMING_DB_CHECKPOINT = Speichere unerfasste Datenbankänderungen...
PERFORMING_DB_MAINTENANCE = Planmäßige Wartung durchführen... PERFORMING_DB_MAINTENANCE = Planmäßige Wartung wird durchgeführt...
SYNCHRONIZE_CLOCK = Synchronisiere Uhr SYNCHRONIZE_CLOCK = Synchronisiere Uhrzeit
SYNCHRONIZING_BLOCKCHAIN = Synchronisierung der Blockchain SYNCHRONIZING_BLOCKCHAIN = Synchronisiere
SYNCHRONIZING_CLOCK = Synchronisierung der Uhr SYNCHRONIZING_CLOCK = Uhrzeit wird synchronisiert

View File

@ -1,17 +1,17 @@
#Generated by ResourceBundle Editor (http://essiembre.github.io/eclipse-rbe/) #Generated by ResourceBundle Editor (http://essiembre.github.io/eclipse-rbe/)
# SysTray pop-up menu # SysTray pop-up menu
APPLYING_UPDATE_AND_RESTARTING = Automatische update en herstart worden uitgevoerd... APPLYING_UPDATE_AND_RESTARTING = Bezig met automatische update en herstart...
AUTO_UPDATE = Automatische Update AUTO_UPDATE = Automatische Update
BLOCK_HEIGHT = Block hoogte BLOCK_HEIGHT = blok-hoogte
BLOCKS_REMAINING = blocks remaining BLOCKS_REMAINING = overblijvende blokken
BUILD_VERSION = Versie nummer BUILD_VERSION = Versienummer
CHECK_TIME_ACCURACY = Controleer accuraatheid van de tijd CHECK_TIME_ACCURACY = Controleer of de tijd correct is
CONNECTING = Verbinden CONNECTING = Verbinden
@ -33,16 +33,16 @@ LITE_NODE = Lite node
MINTING_DISABLED = Minten is uitgeschakeld MINTING_DISABLED = Minten is uitgeschakeld
MINTING_ENABLED = \u2714 Minten is ingeschakeld MINTING_ENABLED = \u2714 Minten is actief
OPEN_UI = Open UI OPEN_UI = Open UI
PERFORMING_DB_CHECKPOINT = De database wordt gecontroleerd... PERFORMING_DB_CHECKPOINT = De database wordt bijgewerkt...
PERFORMING_DB_MAINTENANCE = Uitvoeren van gepland onderhoud... PERFORMING_DB_MAINTENANCE = Bezig met gepland onderhoud...
SYNCHRONIZE_CLOCK = Synchronizeer klok SYNCHRONIZE_CLOCK = Synchronizeer klok
SYNCHRONIZING_BLOCKCHAIN = Aan het synchronizeren SYNCHRONIZING_BLOCKCHAIN = Bezig met synchronizeren
SYNCHRONIZING_CLOCK = Klok wordt gesynchronizeerd SYNCHRONIZING_CLOCK = Klok wordt gesynchronizeerd

View File

@ -1,60 +1,60 @@
# #
ACCOUNT_ALREADY_EXISTS = Account existiert bereits ACCOUNT_ALREADY_EXISTS = Konto existiert bereits
ACCOUNT_CANNOT_REWARD_SHARE = Account kann keine Belohnung teilen ACCOUNT_CANNOT_REWARD_SHARE = Konto kann nicht an Belohnungsbeteiligung teilnehmen
ADDRESS_ABOVE_RATE_LIMIT = address hat das angegebene Geschwindigkeitlimit erreicht ADDRESS_ABOVE_RATE_LIMIT = Adresse hat festgelegtes Tarif-Limit erreicht
ADDRESS_BLOCKED = Addresse ist geblockt ADDRESS_BLOCKED = diese Adresse ist gesperrt
ALREADY_GROUP_ADMIN = bereits Gruppen Admin ALREADY_GROUP_ADMIN = bereits Gruppenadmin
ALREADY_GROUP_MEMBER = bereits Gruppen Mitglied ALREADY_GROUP_MEMBER = bereits Gruppenmitglied
ALREADY_VOTED_FOR_THAT_OPTION = bereits für diese Option gestimmt ALREADY_VOTED_FOR_THAT_OPTION = bereits für diese Option gestimmt
ASSET_ALREADY_EXISTS = asset existiert bereits ASSET_ALREADY_EXISTS = Vermögenswert existiert bereits
ASSET_DOES_NOT_EXIST = asset nicht gefunden ASSET_DOES_NOT_EXIST = Vermögenswert existiert nicht
ASSET_DOES_NOT_MATCH_AT = asset passt nicht mit AT's asset ASSET_DOES_NOT_MATCH_AT = Vermögenswert stimmt nicht mit dem Vermögenswert von AT überein
ASSET_NOT_SPENDABLE = asset ist nicht ausgabefähig ASSET_NOT_SPENDABLE = Vermögenswert ist nicht auszahlbar
AT_ALREADY_EXISTS = AT existiert bereits AT_ALREADY_EXISTS = AT existiert bereits
AT_IS_FINISHED = AT ist fertig AT_IS_FINISHED = AT ist beendet
AT_UNKNOWN = AT unbekannt AT_UNKNOWN = AT unbekannt
BAN_EXISTS = ban besteht bereits BAN_EXISTS = Bann ist bereits vorhanden
BAN_UNKNOWN = ban unbekannt BAN_UNKNOWN = Bann unbekannt
BANNED_FROM_GROUP = von der gruppe gebannt BANNED_FROM_GROUP = aus der Gruppe verbannt
BUYER_ALREADY_OWNER = Käufer ist bereits Besitzer BUYER_ALREADY_OWNER = Käufer ist bereits Eigentümer
CLOCK_NOT_SYNCED = Uhr nicht synchronisiert CLOCK_NOT_SYNCED = Uhrzeit ist nicht synchronisiert
DUPLICATE_MESSAGE = Adresse sendete doppelte Nachricht DUPLICATE_MESSAGE = Adresse hat doppelte Nachricht gesendet
DUPLICATE_OPTION = Duplizierungsmöglichkeit DUPLICATE_OPTION = doppelte Option
GROUP_ALREADY_EXISTS = Gruppe besteht bereits GROUP_ALREADY_EXISTS = Gruppe existiert bereits
GROUP_APPROVAL_DECIDED = Gruppenfreigabe bereits beschlossen GROUP_APPROVAL_DECIDED = Gruppenzulassung bereits beschlossen
GROUP_APPROVAL_NOT_REQUIRED = Gruppenfreigabe nicht erforderlich GROUP_APPROVAL_NOT_REQUIRED = Gruppenzustimmung nicht erforderlich
GROUP_DOES_NOT_EXIST = Gruppe nicht vorhanden GROUP_DOES_NOT_EXIST = Gruppe existiert nicht
GROUP_ID_MISMATCH = Gruppen-ID stimmt nicht überein GROUP_ID_MISMATCH = Gruppen-ID stimmt nicht überein
GROUP_OWNER_CANNOT_LEAVE = Gruppenbesitzer kann Gruppe nicht verlassen GROUP_OWNER_CANNOT_LEAVE = Gruppeneigentümer kann Gruppe nicht verlassen
HAVE_EQUALS_WANT = das bessesene-asset ist das selbe wie das gesuchte-asset HAVE_EQUALS_WANT = Haben-Vermögenswert ist derselbe wie Wollen-Vermögenswert
INCORRECT_NONCE = falsche PoW-Nonce INCORRECT_NONCE = falsche PoW-Nonce
@ -64,81 +64,81 @@ INVALID_ADDRESS = ungültige Adresse
INVALID_AMOUNT = ungültiger Betrag INVALID_AMOUNT = ungültiger Betrag
INVALID_ASSET_OWNER = Ungültiger Eigentümer INVALID_ASSET_OWNER = ungültiger Vermögenswert-Eigentümer
INVALID_AT_TRANSACTION = ungültige AT-Transaktion INVALID_AT_TRANSACTION = ungültige AT-Transaktion
INVALID_AT_TYPE_LENGTH = ungültige AT 'Typ' Länge INVALID_AT_TYPE_LENGTH = ungültige AT-Typ-Länge
INVALID_BUT_OK = ungültig aber OK INVALID_BUT_OK = ungültig, aber OK
INVALID_CREATION_BYTES = ungültige Erstellungs der bytes INVALID_CREATION_BYTES = ungültige Erstellungsbytes
INVALID_DATA_LENGTH = ungültige Datenlänge INVALID_DATA_LENGTH = unzulässige Datenlänge
INVALID_DESCRIPTION_LENGTH = ungültige Länge der Beschreibung INVALID_DESCRIPTION_LENGTH = unzulässige Länge der Beschreibung
INVALID_GROUP_APPROVAL_THRESHOLD = ungültiger Schwellenwert für die Gruppenzulassung INVALID_GROUP_APPROVAL_THRESHOLD = ungültiger Schwellenwert für die Gruppenzulassung
INVALID_GROUP_BLOCK_DELAY = Ungültige Blockverzögerung der Gruppenfreigabe INVALID_GROUP_BLOCK_DELAY = ungültige Blockverzögerung bei der Gruppenfreigabe
INVALID_GROUP_ID = ungültige Gruppen-ID INVALID_GROUP_ID = ungültige Gruppen-ID
INVALID_GROUP_OWNER = ungültiger Gruppenbesitzer INVALID_GROUP_OWNER = ungültiger Gruppeneigentümer
INVALID_LIFETIME = unzulässige Lebensdauer INVALID_LIFETIME = unzulässige Gültigkeitsdauer
INVALID_NAME_LENGTH = ungültige Namenslänge INVALID_NAME_LENGTH = ungültige Namenslänge
INVALID_NAME_OWNER = ungültiger Besitzername INVALID_NAME_OWNER = ungültiger Eigentümer des Namens
INVALID_OPTION_LENGTH = ungültige Länge der Optionen INVALID_OPTION_LENGTH = ungültige Länge der Optionen
INVALID_OPTIONS_COUNT = Anzahl ungültiger Optionen INVALID_OPTIONS_COUNT = ungültige Anzahl von Optionen
INVALID_ORDER_CREATOR = ungültiger Auftragsersteller INVALID_ORDER_CREATOR = ungültiger Auftragsersteller
INVALID_PAYMENTS_COUNT = Anzahl ungültiger Zahlungen INVALID_PAYMENTS_COUNT = ungültige Anzahl der Zahlungen
INVALID_PUBLIC_KEY = ungültiger öffentlicher Schlüssel INVALID_PUBLIC_KEY = ungültiger öffentlicher Schlüssel
INVALID_QUANTITY = unzulässige Menge INVALID_QUANTITY = ungültige Menge
INVALID_REFERENCE = ungültige Referenz INVALID_REFERENCE = ungültige Referenz
INVALID_RETURN = ungültige Rückgabe INVALID_RETURN = ungültige Rückgabe
INVALID_REWARD_SHARE_PERCENT = ungültig Prozent der Belohnunganteile INVALID_REWARD_SHARE_PERCENT = ungültiger Belohnungsbeteiligungs-Anteil
INVALID_SELLER = unzulässiger Verkäufer INVALID_SELLER = ungültiger Verkäufer
INVALID_TAGS_LENGTH = ungültige 'tags'-Länge INVALID_TAGS_LENGTH = ungültige 'tags'-Länge
INVALID_TIMESTAMP_SIGNATURE = Ungültige Zeitstempel-Signatur INVALID_TIMESTAMP_SIGNATURE = ungültige Zeitstempel-Signatur
INVALID_TX_GROUP_ID = Ungültige Transaktionsgruppen-ID INVALID_TX_GROUP_ID = ungültige Transaktionsgruppen-ID
INVALID_VALUE_LENGTH = ungültige 'Wert'-Länge INVALID_VALUE_LENGTH = ungültige 'value'-Länge
INVITE_UNKNOWN = Gruppeneinladung unbekannt INVITE_UNKNOWN = Gruppeneinladung unbekannt
JOIN_REQUEST_EXISTS = Gruppeneinladung existiert bereits JOIN_REQUEST_EXISTS = Gruppenverbindungsanfrage existiert bereits
MAXIMUM_REWARD_SHARES = die maximale Anzahl von Reward-Shares für dieses Konto erreicht MAXIMUM_REWARD_SHARES = maximale Anzahl von Belohnungsbeteiligungen für dieses Konto bereits erreicht
MISSING_CREATOR = fehlender Ersteller MISSING_CREATOR = fehlender Ersteller
MULTIPLE_NAMES_FORBIDDEN = mehrere registrierte Namen pro Konto sind untersagt MULTIPLE_NAMES_FORBIDDEN = mehrere registrierte Namen pro Konto sind verboten
NAME_ALREADY_FOR_SALE = Name bereits zum Verkauf NAME_ALREADY_FOR_SALE = Name steht bereits zum Verkauf
NAME_ALREADY_REGISTERED = Name bereits registriert NAME_ALREADY_REGISTERED = Name bereits registriert
NAME_BLOCKED = Name geblockt NAME_BLOCKED = dieser Name ist gesperrt
NAME_DOES_NOT_EXIST = Name nicht vorhanden NAME_DOES_NOT_EXIST = Name existiert nicht
NAME_NOT_FOR_SALE = Name ist unverkäuflich NAME_NOT_FOR_SALE = Name steht nicht zum Verkauf
NAME_NOT_NORMALIZED = Name nicht in Unicode-'normalisierter' Form NAME_NOT_NORMALIZED = Name nicht in Unicode-'normalisierter' Form
@ -150,46 +150,46 @@ NEGATIVE_PRICE = ungültiger/negativer Preis
NO_BALANCE = unzureichendes Guthaben NO_BALANCE = unzureichendes Guthaben
NO_BLOCKCHAIN_LOCK = die Blockchain des Knotens ist beschäftigt NO_BLOCKCHAIN_LOCK = die Blockchain des Knotens ist derzeit beschäftigt
NO_FLAG_PERMISSION = Konto hat diese Berechtigung nicht NO_FLAG_PERMISSION = Konto hat diese Berechtigung nicht
NOT_GROUP_ADMIN = Account ist kein Gruppenadmin NOT_GROUP_ADMIN = Konto ist kein Gruppenadmin
NOT_GROUP_MEMBER = Account kein Gruppenmitglied NOT_GROUP_MEMBER = Konto ist kein Gruppenmitglied
NOT_MINTING_ACCOUNT = Account kann nicht minten NOT_MINTING_ACCOUNT = Konto kann nicht prägen
NOT_YET_RELEASED = Funktion noch nicht freigegeben NOT_YET_RELEASED = Funktion noch nicht freigegeben
OK = OK OK = OK
ORDER_ALREADY_CLOSED = Asset Trade Order ist bereits geschlossen ORDER_ALREADY_CLOSED = Vermögenswert-Handelsauftrag ist bereits geschlossen
ORDER_DOES_NOT_EXIST = asset trade order existiert nicht ORDER_DOES_NOT_EXIST = Vermögenswert-Handelsauftrag existiert nicht
POLL_ALREADY_EXISTS = Umfrage bereits vorhanden POLL_ALREADY_EXISTS = Umfrage existiert bereits
POLL_DOES_NOT_EXIST = Umfrage nicht vorhanden POLL_DOES_NOT_EXIST = Umfrage existiert nicht
POLL_OPTION_DOES_NOT_EXIST = Umfrageoption existiert nicht POLL_OPTION_DOES_NOT_EXIST = Umfrageoption nicht vorhanden
PUBLIC_KEY_UNKNOWN = öffentlicher Schlüssel unbekannt PUBLIC_KEY_UNKNOWN = öffentlicher Schlüssel unbekannt
REWARD_SHARE_UNKNOWN = Geteilte Belohnungen unbekant REWARD_SHARE_UNKNOWN = Belohnungsbeteiligung unbekannt
SELF_SHARE_EXISTS = Selbstbeteiligung (Geteilte Belohnungen) sind breits vorhanden SELF_SHARE_EXISTS = Selbst-Beteiligung (Belohnungsbeteiligung) existiert bereits
TIMESTAMP_TOO_NEW = Zeitstempel zu neu TIMESTAMP_TOO_NEW = Zeitstempel zu neu
TIMESTAMP_TOO_OLD = Zeitstempel zu alt TIMESTAMP_TOO_OLD = Zeitstempel zu alt
TOO_MANY_UNCONFIRMED = Account hat zu viele unbestätigte Transaktionen am laufen TOO_MANY_UNCONFIRMED = Konto hat zu viele ausstehende unbestätigte Transaktionen
TRANSACTION_ALREADY_CONFIRMED = Transaktionen sind bereits bestätigt TRANSACTION_ALREADY_CONFIRMED = Transaktion wurde bereits bestätigt
TRANSACTION_ALREADY_EXISTS = Transaktionen existiert bereits TRANSACTION_ALREADY_EXISTS = Transaktion existiert bereits
TRANSACTION_UNKNOWN = Unbekante Transaktion TRANSACTION_UNKNOWN = Transaktion unbekannt
TX_GROUP_ID_MISMATCH = Transaktion Gruppen ID stimmt nicht überein TX_GROUP_ID_MISMATCH = die Gruppen-ID der Transaktion stimmt nicht überein

View File

@ -1,28 +1,28 @@
# #
ACCOUNT_ALREADY_EXISTS = account bestaat al ACCOUNT_ALREADY_EXISTS = account bestaat reeds
ACCOUNT_CANNOT_REWARD_SHARE = account kan geen beloningen delen ACCOUNT_CANNOT_REWARD_SHARE = account kan geen beloningen delen
ADDRESS_ABOVE_RATE_LIMIT = adres heeft een waarde limiet bereikt ADDRESS_ABOVE_RATE_LIMIT = adres heeft opgegeven limietwaarde bereikt
ADDRESS_BLOCKED = adres is geblokkeerd ADDRESS_BLOCKED = dit adres is geblokkeerd
ALREADY_GROUP_ADMIN = groeps administrator bestaat al ALREADY_GROUP_ADMIN = reeds gekend als groepsadministrator
ALREADY_GROUP_MEMBER = groeps lid bestaat al ALREADY_GROUP_MEMBER = reeds gekend als groepslid
ALREADY_VOTED_FOR_THAT_OPTION = reeds gestemd voor die optie ALREADY_VOTED_FOR_THAT_OPTION = reeds gestemd voor die optie
ASSET_ALREADY_EXISTS = asset bestaat al ASSET_ALREADY_EXISTS = asset bestaat reeds
ASSET_DOES_NOT_EXIST = asset bestaat niet ASSET_DOES_NOT_EXIST = asset bestaat niet
ASSET_DOES_NOT_MATCH_AT = asset komt niet overeen met de asset van de AT ASSET_DOES_NOT_MATCH_AT = asset komt niet overeen met de asset van de AT
ASSET_NOT_SPENDABLE = asset is niet toerijkend ASSET_NOT_SPENDABLE = asset kan niet uitbetaald worden
AT_ALREADY_EXISTS = AT bestaat al AT_ALREADY_EXISTS = AT bestaat reeds
AT_IS_FINISHED = AT is afgelopen AT_IS_FINISHED = AT is afgelopen
@ -38,25 +38,25 @@ BUYER_ALREADY_OWNER = koper is al de eigenaar
CLOCK_NOT_SYNCED = klok is niet gesynchronizeerd CLOCK_NOT_SYNCED = klok is niet gesynchronizeerd
DUPLICATE_MESSAGE = dubbel adres bericht DUPLICATE_MESSAGE = adres heeft dubbel bericht verzonden
DUPLICATE_OPTION = dubbele optie DUPLICATE_OPTION = dubbele optie
GROUP_ALREADY_EXISTS = groep bestaat reeds GROUP_ALREADY_EXISTS = groep bestaat reeds
GROUP_APPROVAL_DECIDED = groeps goedkeuring afgewezen GROUP_APPROVAL_DECIDED = groeps-goedkeuring afgewezen
GROUP_APPROVAL_NOT_REQUIRED = groeps goedkeuring niet vereist GROUP_APPROVAL_NOT_REQUIRED = groeps-goedkeuring niet vereist
GROUP_DOES_NOT_EXIST = groep bestaat niet GROUP_DOES_NOT_EXIST = groep bestaat niet
GROUP_ID_MISMATCH = groeps ID komt niet overeen GROUP_ID_MISMATCH = groeps ID komt niet overeen
GROUP_OWNER_CANNOT_LEAVE = groep eigenaar kan de groep niet verlaten GROUP_OWNER_CANNOT_LEAVE = groep-eigenaar kan groep niet verlaten
HAVE_EQUALS_WANT = asset is gelijk aan Want-asset HAVE_EQUALS_WANT = asset is gelijk aan Wens-asset
INCORRECT_NONCE = incorrecte PoW nonce INCORRECT_NONCE = foutieve PoW nonce
INSUFFICIENT_FEE = vergoeding te laag INSUFFICIENT_FEE = vergoeding te laag
@ -72,19 +72,19 @@ INVALID_AT_TYPE_LENGTH = ongeldige lengte voor AT type
INVALID_BUT_OK = ongeldig maar is in orde INVALID_BUT_OK = ongeldig maar is in orde
INVALID_CREATION_BYTES = ongeldige gecreerde bytes INVALID_CREATION_BYTES = ongeldige creatie-bytes
INVALID_DATA_LENGTH = ongeldige data lengte INVALID_DATA_LENGTH = ongeldige data-lengte
INVALID_DESCRIPTION_LENGTH = ongeldige lengte voor de beschrijving INVALID_DESCRIPTION_LENGTH = ongeldige lengte voor beschrijving
INVALID_GROUP_APPROVAL_THRESHOLD = ongeldige drempelwaarde voor groepsgoedkeuring INVALID_GROUP_APPROVAL_THRESHOLD = ongeldige drempelwaarde voor groepsgoedkeuring
INVALID_GROUP_BLOCK_DELAY = ongeldige groep blok vertraging INVALID_GROUP_BLOCK_DELAY = ongeldige blok-vertraging bij groepsgoedkeuring
INVALID_GROUP_ID = ongeldige groep-ID INVALID_GROUP_ID = ongeldige groep-ID
INVALID_GROUP_OWNER = ongeldige groep eigenaar INVALID_GROUP_OWNER = ongeldige groep-eigenaar
INVALID_LIFETIME = ongeldige levensduur INVALID_LIFETIME = ongeldige levensduur
@ -94,27 +94,27 @@ INVALID_NAME_OWNER = ongeldige naam voor eigenaar
INVALID_OPTION_LENGTH = ongeldige lengte voor opties INVALID_OPTION_LENGTH = ongeldige lengte voor opties
INVALID_OPTIONS_COUNT = ongeldige hoeveelheid opties INVALID_OPTIONS_COUNT = ongeldig aantal opties
INVALID_ORDER_CREATOR = ongeldige gebruiker voor deze order INVALID_ORDER_CREATOR = ongeldige order-creatie-gebruiker
INVALID_PAYMENTS_COUNT = ongeldige betalings waarde INVALID_PAYMENTS_COUNT = ongeldig aantal betalingen
INVALID_PUBLIC_KEY = ongeldige public key INVALID_PUBLIC_KEY = ongeldige public key
INVALID_QUANTITY = ongeldige hoeveelheid INVALID_QUANTITY = ongeldige hoeveelheid
INVALID_REFERENCE = ongeldige verwijzing INVALID_REFERENCE = ongeldige referentie
INVALID_RETURN = ongeldige return INVALID_RETURN = ongeldig resultaat
INVALID_REWARD_SHARE_PERCENT = ongeldig belonings percentage INVALID_REWARD_SHARE_PERCENT = ongeldig belonings-deelpercentage
INVALID_SELLER = ongeldige verkoper INVALID_SELLER = ongeldige verkoper
INVALID_TAGS_LENGTH = ongeldige lengte voor 'tags' INVALID_TAGS_LENGTH = ongeldige lengte voor 'tags'
INVALID_TIMESTAMP_SIGNATURE = ongeldig tijd aanduiding INVALID_TIMESTAMP_SIGNATURE = ongeldig tijd-aanduiding
INVALID_TX_GROUP_ID = ongeldige transactiegroep-ID INVALID_TX_GROUP_ID = ongeldige transactiegroep-ID
@ -124,15 +124,15 @@ INVITE_UNKNOWN = onbekende groepsuitnodiging
JOIN_REQUEST_EXISTS = aanvraag om lid van groep te worden bestaat al JOIN_REQUEST_EXISTS = aanvraag om lid van groep te worden bestaat al
MAXIMUM_REWARD_SHARES = limiet aan belonen voor dit account bereikt MAXIMUM_REWARD_SHARES = maximum bereikt voor beloning-delen voor dit account
MISSING_CREATOR = ontbrekende aanmaker MISSING_CREATOR = creator niet gekend
MULTIPLE_NAMES_FORBIDDEN = het registreren van meerdere namen op een account is niet toegestaan MULTIPLE_NAMES_FORBIDDEN = het registreren van meerdere namen op een account is niet toegestaan
NAME_ALREADY_FOR_SALE = naam reeds te koop NAME_ALREADY_FOR_SALE = naam is reeds te koop
NAME_ALREADY_REGISTERED = naam reeds geregistreerd NAME_ALREADY_REGISTERED = naam is reeds geregistreerd
NAME_BLOCKED = deze naam is geblokkeerd NAME_BLOCKED = deze naam is geblokkeerd
@ -140,7 +140,7 @@ NAME_DOES_NOT_EXIST = naam bestaat niet
NAME_NOT_FOR_SALE = naam is niet te koop NAME_NOT_FOR_SALE = naam is niet te koop
NAME_NOT_NORMALIZED = naam voldoet niet aan Unicode-vorm NAME_NOT_NORMALIZED = naam niet in Unicode-'nomaal'-vorm
NEGATIVE_AMOUNT = negatieve hoeveelheid NEGATIVE_AMOUNT = negatieve hoeveelheid
@ -148,9 +148,9 @@ NEGATIVE_FEE = negatieve vergoeding
NEGATIVE_PRICE = negatieve prijs NEGATIVE_PRICE = negatieve prijs
NO_BALANCE = onvoldoende balans NO_BALANCE = onvoldoende saldo
NO_BLOCKCHAIN_LOCK = geen blockchain slot NO_BLOCKCHAIN_LOCK = blockchain op node is momenteel bezet
NO_FLAG_PERMISSION = account heeft hier geen toestemming voor NO_FLAG_PERMISSION = account heeft hier geen toestemming voor
@ -176,9 +176,9 @@ POLL_OPTION_DOES_NOT_EXIST = peilingsoptie bestaat niet
PUBLIC_KEY_UNKNOWN = public key onbekend PUBLIC_KEY_UNKNOWN = public key onbekend
REWARD_SHARE_UNKNOWN = beloning vergoeding onbekend REWARD_SHARE_UNKNOWN = beloningsdeel is onbekend
SELF_SHARE_EXISTS = zelf vergoeding bestaat reeds SELF_SHARE_EXISTS = zelf-beloning (belonings-delen) bestaat reeds
TIMESTAMP_TOO_NEW = tijdstempel te nieuw TIMESTAMP_TOO_NEW = tijdstempel te nieuw
@ -186,10 +186,10 @@ TIMESTAMP_TOO_OLD = tijdstempel te oud
TOO_MANY_UNCONFIRMED = account heeft te veel onbevestigde transacties in afwachting TOO_MANY_UNCONFIRMED = account heeft te veel onbevestigde transacties in afwachting
TRANSACTION_ALREADY_CONFIRMED = transactie is reeds bevestigd TRANSACTION_ALREADY_CONFIRMED = transactie werd reeds bevestigd
TRANSACTION_ALREADY_EXISTS = transactie bestaat al TRANSACTION_ALREADY_EXISTS = transactie bestaat reeds
TRANSACTION_UNKNOWN = transactie onbekend TRANSACTION_UNKNOWN = transactie onbekend
TX_GROUP_ID_MISMATCH = groep ID komt niet overeen TX_GROUP_ID_MISMATCH = groep-ID komt niet overeen

View File

@ -3,6 +3,8 @@ package org.qortal.test;
import org.junit.Ignore; import org.junit.Ignore;
import org.junit.Test; import org.junit.Test;
import org.qortal.crypto.MemoryPoW; import org.qortal.crypto.MemoryPoW;
import org.qortal.repository.DataException;
import org.qortal.test.common.Common;
import static org.junit.Assert.*; import static org.junit.Assert.*;
@ -39,13 +41,14 @@ public class MemoryPoWTests {
} }
@Test @Test
public void testMultipleComputes() { public void testMultipleComputes() throws DataException {
Common.useDefaultSettings();
Random random = new Random(); Random random = new Random();
final int sampleSize = 20; final int sampleSize = 10;
final long stddevDivisor = sampleSize * (sampleSize - 1); final long stddevDivisor = sampleSize * (sampleSize - 1);
for (int difficulty = 8; difficulty < 16; difficulty += 2) { for (int difficulty = 8; difficulty <= 16; difficulty++) {
byte[] data = new byte[256]; byte[] data = new byte[256];
long[] times = new long[sampleSize]; long[] times = new long[sampleSize];

View File

@ -1,5 +1,6 @@
package org.qortal.test; package org.qortal.test;
import org.apache.commons.lang3.reflect.FieldUtils;
import org.junit.After; import org.junit.After;
import org.junit.Before; import org.junit.Before;
import org.junit.Test; import org.junit.Test;
@ -14,12 +15,9 @@ import org.qortal.group.Group.ApprovalThreshold;
import org.qortal.repository.DataException; import org.qortal.repository.DataException;
import org.qortal.repository.Repository; import org.qortal.repository.Repository;
import org.qortal.repository.RepositoryManager; import org.qortal.repository.RepositoryManager;
import org.qortal.test.common.BlockUtils; import org.qortal.test.common.*;
import org.qortal.test.common.Common;
import org.qortal.test.common.GroupUtils;
import org.qortal.test.common.TestAccount;
import org.qortal.test.common.TransactionUtils;
import org.qortal.test.common.transaction.TestTransaction; import org.qortal.test.common.transaction.TestTransaction;
import org.qortal.transaction.DeployAtTransaction;
import org.qortal.transaction.MessageTransaction; import org.qortal.transaction.MessageTransaction;
import org.qortal.transaction.Transaction; import org.qortal.transaction.Transaction;
import org.qortal.transaction.Transaction.TransactionType; import org.qortal.transaction.Transaction.TransactionType;
@ -31,6 +29,7 @@ import org.qortal.utils.NTP;
import static org.junit.Assert.*; import static org.junit.Assert.*;
import java.util.List;
import java.util.Random; import java.util.Random;
public class MessageTests extends Common { public class MessageTests extends Common {
@ -85,7 +84,7 @@ public class MessageTests extends Common {
byte[] randomReference = new byte[64]; byte[] randomReference = new byte[64];
random.nextBytes(randomReference); random.nextBytes(randomReference);
long minimumFee = BlockChain.getInstance().getUnitFee(); long minimumFee = BlockChain.getInstance().getUnitFeeAtTimestamp(System.currentTimeMillis());
try (final Repository repository = RepositoryManager.getRepository()) { try (final Repository repository = RepositoryManager.getRepository()) {
PrivateKeyAccount alice = Common.getTestAccount(repository, "alice"); PrivateKeyAccount alice = Common.getTestAccount(repository, "alice");
@ -139,7 +138,7 @@ public class MessageTests extends Common {
} }
@Test @Test
public void withRecipentWithAmount() throws DataException { public void withRecipientWithAmount() throws DataException {
testMessage(Group.NO_GROUP, recipient, 123L, Asset.QORT); testMessage(Group.NO_GROUP, recipient, 123L, Asset.QORT);
} }
@ -153,6 +152,140 @@ public class MessageTests extends Common {
testMessage(1, null, 0L, null); testMessage(1, null, 0L, null);
} }
@Test
public void atRecipientNoFeeWithNonce() throws DataException {
try (final Repository repository = RepositoryManager.getRepository()) {
PrivateKeyAccount alice = Common.getTestAccount(repository, "alice");
String atRecipient = deployAt();
MessageTransaction transaction = testFeeNonce(repository, false, true, atRecipient, true);
// Transaction should be confirmable because it's to an AT, and therefore should be present in a block
assertTrue(transaction.isConfirmable());
TransactionUtils.signAndMint(repository, transaction.getTransactionData(), alice);
assertTrue(isTransactionConfirmed(repository, transaction));
assertEquals(16, transaction.getPoWDifficulty());
BlockUtils.orphanLastBlock(repository);
}
}
@Test
public void regularRecipientNoFeeWithNonce() throws DataException {
try (final Repository repository = RepositoryManager.getRepository()) {
PrivateKeyAccount alice = Common.getTestAccount(repository, "alice");
// Transaction should not be present in db yet
List<MessageTransactionData> messageTransactionsData = repository.getMessageRepository().getMessagesByParticipants(null, recipient, null, null, null);
assertTrue(messageTransactionsData.isEmpty());
MessageTransaction transaction = testFeeNonce(repository, false, true, recipient, true);
// Transaction shouldn't be confirmable because it's not to an AT, and therefore shouldn't be present in a block
assertFalse(transaction.isConfirmable());
TransactionUtils.signAndMint(repository, transaction.getTransactionData(), alice);
assertFalse(isTransactionConfirmed(repository, transaction));
assertEquals(12, transaction.getPoWDifficulty());
// Transaction should be found when trade bot searches for it
messageTransactionsData = repository.getMessageRepository().getMessagesByParticipants(null, recipient, null, null, null);
assertEquals(1, messageTransactionsData.size());
BlockUtils.orphanLastBlock(repository);
}
}
@Test
public void noRecipientNoFeeWithNonce() throws DataException {
try (final Repository repository = RepositoryManager.getRepository()) {
PrivateKeyAccount alice = Common.getTestAccount(repository, "alice");
MessageTransaction transaction = testFeeNonce(repository, false, true, null, true);
// Transaction shouldn't be confirmable because it's not to an AT, and therefore shouldn't be present in a block
assertFalse(transaction.isConfirmable());
TransactionUtils.signAndMint(repository, transaction.getTransactionData(), alice);
assertFalse(isTransactionConfirmed(repository, transaction));
assertEquals(12, transaction.getPoWDifficulty());
BlockUtils.orphanLastBlock(repository);
}
}
@Test
public void atRecipientWithFeeNoNonce() throws DataException {
try (final Repository repository = RepositoryManager.getRepository()) {
PrivateKeyAccount alice = Common.getTestAccount(repository, "alice");
String atRecipient = deployAt();
MessageTransaction transaction = testFeeNonce(repository, true, false, atRecipient, true);
// Transaction should be confirmable because it's to an AT, and therefore should be present in a block
assertTrue(transaction.isConfirmable());
TransactionUtils.signAndMint(repository, transaction.getTransactionData(), alice);
assertTrue(isTransactionConfirmed(repository, transaction));
assertEquals(16, transaction.getPoWDifficulty());
BlockUtils.orphanLastBlock(repository);
}
}
@Test
public void regularRecipientWithFeeNoNonce() throws DataException {
try (final Repository repository = RepositoryManager.getRepository()) {
PrivateKeyAccount alice = Common.getTestAccount(repository, "alice");
MessageTransaction transaction = testFeeNonce(repository, true, false, recipient, true);
// Transaction shouldn't be confirmable because it's not to an AT, and therefore shouldn't be present in a block
assertFalse(transaction.isConfirmable());
TransactionUtils.signAndMint(repository, transaction.getTransactionData(), alice);
assertFalse(isTransactionConfirmed(repository, transaction));
assertEquals(12, transaction.getPoWDifficulty());
BlockUtils.orphanLastBlock(repository);
}
}
@Test
public void atRecipientNoFeeWithNonceLegacyDifficulty() throws DataException, IllegalAccessException {
try (final Repository repository = RepositoryManager.getRepository()) {
// Set mempowTransactionUpdatesTimestamp to a high value, so that it hasn't activated key
FieldUtils.writeField(BlockChain.getInstance(), "mempowTransactionUpdatesTimestamp", Long.MAX_VALUE, true);
PrivateKeyAccount alice = Common.getTestAccount(repository, "alice");
String atRecipient = deployAt();
MessageTransaction transaction = testFeeNonce(repository, false, true, atRecipient, true);
// Transaction should be confirmable because all MESSAGE transactions confirmed prior to the feature trigger
assertTrue(transaction.isConfirmable());
TransactionUtils.signAndMint(repository, transaction.getTransactionData(), alice);
assertTrue(isTransactionConfirmed(repository, transaction));
assertEquals(14, transaction.getPoWDifficulty()); // Legacy difficulty was 14 in all cases
BlockUtils.orphanLastBlock(repository);
}
}
@Test
public void regularRecipientNoFeeWithNonceLegacyDifficulty() throws DataException, IllegalAccessException {
try (final Repository repository = RepositoryManager.getRepository()) {
// Set mempowTransactionUpdatesTimestamp to a high value, so that it hasn't activated key
FieldUtils.writeField(BlockChain.getInstance(), "mempowTransactionUpdatesTimestamp", Long.MAX_VALUE, true);
PrivateKeyAccount alice = Common.getTestAccount(repository, "alice");
MessageTransaction transaction = testFeeNonce(repository, false, true, recipient, true);
// Transaction should be confirmable because all MESSAGE transactions confirmed prior to the feature trigger
assertTrue(transaction.isConfirmable());
TransactionUtils.signAndMint(repository, transaction.getTransactionData(), alice);
assertTrue(isTransactionConfirmed(repository, transaction)); // All MESSAGE transactions would confirm before feature trigger
assertEquals(14, transaction.getPoWDifficulty()); // Legacy difficulty was 14 in all cases
BlockUtils.orphanLastBlock(repository);
}
}
@Test @Test
public void serializationTests() throws DataException, TransformationException { public void serializationTests() throws DataException, TransformationException {
// with recipient, with amount // with recipient, with amount
@ -165,6 +298,24 @@ public class MessageTests extends Common {
testSerialization(null, 0L, null); testSerialization(null, 0L, null);
} }
private String deployAt() throws DataException {
try (final Repository repository = RepositoryManager.getRepository()) {
PrivateKeyAccount deployer = Common.getTestAccount(repository, "alice");
byte[] creationBytes = AtUtils.buildSimpleAT();
long fundingAmount = 1_00000000L;
DeployAtTransaction deployAtTransaction = AtUtils.doDeployAT(repository, deployer, creationBytes, fundingAmount);
String address = deployAtTransaction.getATAccount().getAddress();
assertNotNull(address);
return address;
}
}
private boolean isTransactionConfirmed(Repository repository, MessageTransaction transaction) throws DataException {
TransactionData queriedTransactionData = repository.getTransactionRepository().fromSignature(transaction.getTransactionData().getSignature());
return queriedTransactionData.getBlockHeight() != null && queriedTransactionData.getBlockHeight() > 0;
}
private boolean isValid(int txGroupId, String recipient, long amount, Long assetId) throws DataException { private boolean isValid(int txGroupId, String recipient, long amount, Long assetId) throws DataException {
try (final Repository repository = RepositoryManager.getRepository()) { try (final Repository repository = RepositoryManager.getRepository()) {
TestAccount alice = Common.getTestAccount(repository, "alice"); TestAccount alice = Common.getTestAccount(repository, "alice");
@ -195,41 +346,48 @@ public class MessageTests extends Common {
return messageTransaction.hasValidReference(); return messageTransaction.hasValidReference();
} }
private void testFeeNonce(boolean withFee, boolean withNonce, boolean isValid) throws DataException {
private MessageTransaction testFeeNonce(boolean withFee, boolean withNonce, boolean isValid) throws DataException {
try (final Repository repository = RepositoryManager.getRepository()) { try (final Repository repository = RepositoryManager.getRepository()) {
TestAccount alice = Common.getTestAccount(repository, "alice"); return testFeeNonce(repository, withFee, withNonce, recipient, isValid);
int txGroupId = 0;
int nonce = 0;
long amount = 0;
long assetId = Asset.QORT;
byte[] data = new byte[1];
boolean isText = false;
boolean isEncrypted = false;
MessageTransactionData transactionData = new MessageTransactionData(TestTransaction.generateBase(alice, txGroupId),
version, nonce, recipient, amount, assetId, data, isText, isEncrypted);
MessageTransaction transaction = new MessageTransaction(repository, transactionData);
if (withFee)
transactionData.setFee(transaction.calcRecommendedFee());
else
transactionData.setFee(0L);
if (withNonce) {
transaction.computeNonce();
} else {
transactionData.setNonce(-1);
}
transaction.sign(alice);
assertEquals(isValid, transaction.isSignatureValid());
} }
} }
private void testMessage(int txGroupId, String recipient, long amount, Long assetId) throws DataException { private MessageTransaction testFeeNonce(Repository repository, boolean withFee, boolean withNonce, String recipient, boolean isValid) throws DataException {
TestAccount alice = Common.getTestAccount(repository, "alice");
int txGroupId = 0;
int nonce = 0;
long amount = 0;
long assetId = Asset.QORT;
byte[] data = new byte[1];
boolean isText = false;
boolean isEncrypted = false;
MessageTransactionData transactionData = new MessageTransactionData(TestTransaction.generateBase(alice, txGroupId),
version, nonce, recipient, amount, assetId, data, isText, isEncrypted);
MessageTransaction transaction = new MessageTransaction(repository, transactionData);
if (withFee)
transactionData.setFee(transaction.calcRecommendedFee());
else
transactionData.setFee(0L);
if (withNonce) {
transaction.computeNonce();
} else {
transactionData.setNonce(-1);
}
transaction.sign(alice);
assertEquals(isValid, transaction.isSignatureValid());
return transaction;
}
private MessageTransaction testMessage(int txGroupId, String recipient, long amount, Long assetId) throws DataException {
try (final Repository repository = RepositoryManager.getRepository()) { try (final Repository repository = RepositoryManager.getRepository()) {
TestAccount alice = Common.getTestAccount(repository, "alice"); TestAccount alice = Common.getTestAccount(repository, "alice");
@ -244,6 +402,8 @@ public class MessageTests extends Common {
TransactionUtils.signAndMint(repository, transactionData, alice); TransactionUtils.signAndMint(repository, transactionData, alice);
BlockUtils.orphanLastBlock(repository); BlockUtils.orphanLastBlock(repository);
return new MessageTransaction(repository, transactionData);
} }
} }

View File

@ -11,6 +11,7 @@ import org.qortal.arbitrary.ArbitraryDataReader;
import org.qortal.arbitrary.exception.MissingDataException; import org.qortal.arbitrary.exception.MissingDataException;
import org.qortal.arbitrary.misc.Category; import org.qortal.arbitrary.misc.Category;
import org.qortal.arbitrary.misc.Service; import org.qortal.arbitrary.misc.Service;
import org.qortal.block.BlockChain;
import org.qortal.controller.arbitrary.ArbitraryDataManager; import org.qortal.controller.arbitrary.ArbitraryDataManager;
import org.qortal.data.arbitrary.ArbitraryResourceMetadata; import org.qortal.data.arbitrary.ArbitraryResourceMetadata;
import org.qortal.data.transaction.ArbitraryTransactionData; import org.qortal.data.transaction.ArbitraryTransactionData;
@ -24,6 +25,7 @@ import org.qortal.test.common.TransactionUtils;
import org.qortal.test.common.transaction.TestTransaction; import org.qortal.test.common.transaction.TestTransaction;
import org.qortal.transaction.RegisterNameTransaction; import org.qortal.transaction.RegisterNameTransaction;
import org.qortal.utils.Base58; import org.qortal.utils.Base58;
import org.qortal.utils.NTP;
import java.io.IOException; import java.io.IOException;
import java.nio.file.Files; import java.nio.file.Files;
@ -106,8 +108,9 @@ public class ArbitraryTransactionMetadataTests extends Common {
// Create PUT transaction // Create PUT transaction
Path path1 = ArbitraryUtils.generateRandomDataPath(dataLength); Path path1 = ArbitraryUtils.generateRandomDataPath(dataLength);
long fee = BlockChain.getInstance().getUnitFeeAtTimestamp(NTP.getTime());
ArbitraryDataFile arbitraryDataFile = ArbitraryUtils.createAndMintTxn(repository, publicKey58, path1, name, ArbitraryDataFile arbitraryDataFile = ArbitraryUtils.createAndMintTxn(repository, publicKey58, path1, name,
identifier, ArbitraryTransactionData.Method.PUT, service, alice, chunkSize, 0L, true, identifier, ArbitraryTransactionData.Method.PUT, service, alice, chunkSize, fee, false,
title, description, tags, category); title, description, tags, category);
// Check the chunk count is correct // Check the chunk count is correct
@ -157,8 +160,9 @@ public class ArbitraryTransactionMetadataTests extends Common {
// Create PUT transaction // Create PUT transaction
Path path1 = ArbitraryUtils.generateRandomDataPath(dataLength); Path path1 = ArbitraryUtils.generateRandomDataPath(dataLength);
long fee = BlockChain.getInstance().getUnitFeeAtTimestamp(NTP.getTime());
ArbitraryDataFile arbitraryDataFile = ArbitraryUtils.createAndMintTxn(repository, publicKey58, path1, name, ArbitraryDataFile arbitraryDataFile = ArbitraryUtils.createAndMintTxn(repository, publicKey58, path1, name,
identifier, ArbitraryTransactionData.Method.PUT, service, alice, chunkSize, 0L, true, identifier, ArbitraryTransactionData.Method.PUT, service, alice, chunkSize, fee, false,
title, description, tags, category); title, description, tags, category);
// Check the chunk count is correct // Check the chunk count is correct
@ -220,8 +224,9 @@ public class ArbitraryTransactionMetadataTests extends Common {
// Create PUT transaction // Create PUT transaction
Path path1 = ArbitraryUtils.generateRandomDataPath(dataLength); Path path1 = ArbitraryUtils.generateRandomDataPath(dataLength);
long fee = BlockChain.getInstance().getUnitFeeAtTimestamp(NTP.getTime());
ArbitraryDataFile arbitraryDataFile = ArbitraryUtils.createAndMintTxn(repository, publicKey58, path1, name, ArbitraryDataFile arbitraryDataFile = ArbitraryUtils.createAndMintTxn(repository, publicKey58, path1, name,
identifier, ArbitraryTransactionData.Method.PUT, service, alice, chunkSize, 0L, true, identifier, ArbitraryTransactionData.Method.PUT, service, alice, chunkSize, fee, false,
title, description, tags, category); title, description, tags, category);
// Check the chunk count is correct // Check the chunk count is correct
@ -272,8 +277,9 @@ public class ArbitraryTransactionMetadataTests extends Common {
// Create PUT transaction // Create PUT transaction
Path path1 = ArbitraryUtils.generateRandomDataPath(dataLength); Path path1 = ArbitraryUtils.generateRandomDataPath(dataLength);
long fee = BlockChain.getInstance().getUnitFeeAtTimestamp(NTP.getTime());
ArbitraryDataFile arbitraryDataFile = ArbitraryUtils.createAndMintTxn(repository, publicKey58, path1, name, ArbitraryDataFile arbitraryDataFile = ArbitraryUtils.createAndMintTxn(repository, publicKey58, path1, name,
identifier, ArbitraryTransactionData.Method.PUT, service, alice, chunkSize, 0L, true, identifier, ArbitraryTransactionData.Method.PUT, service, alice, chunkSize, fee, false,
title, description, tags, category); title, description, tags, category);
// Check the chunk count is correct // Check the chunk count is correct
@ -316,8 +322,9 @@ public class ArbitraryTransactionMetadataTests extends Common {
// Create PUT transaction // Create PUT transaction
Path path1 = ArbitraryUtils.generateRandomDataPath(dataLength); Path path1 = ArbitraryUtils.generateRandomDataPath(dataLength);
long fee = BlockChain.getInstance().getUnitFeeAtTimestamp(NTP.getTime());
ArbitraryDataFile arbitraryDataFile = ArbitraryUtils.createAndMintTxn(repository, publicKey58, path1, name, ArbitraryDataFile arbitraryDataFile = ArbitraryUtils.createAndMintTxn(repository, publicKey58, path1, name,
identifier, ArbitraryTransactionData.Method.PUT, service, alice, chunkSize, 0L, true, identifier, ArbitraryTransactionData.Method.PUT, service, alice, chunkSize, fee, false,
title, description, tags, category); title, description, tags, category);
// Check the metadata is correct // Check the metadata is correct

View File

@ -10,6 +10,7 @@ import org.qortal.arbitrary.ArbitraryDataTransactionBuilder;
import org.qortal.arbitrary.exception.MissingDataException; import org.qortal.arbitrary.exception.MissingDataException;
import org.qortal.arbitrary.misc.Category; import org.qortal.arbitrary.misc.Category;
import org.qortal.arbitrary.misc.Service; import org.qortal.arbitrary.misc.Service;
import org.qortal.block.BlockChain;
import org.qortal.controller.arbitrary.ArbitraryDataManager; import org.qortal.controller.arbitrary.ArbitraryDataManager;
import org.qortal.crypto.Crypto; import org.qortal.crypto.Crypto;
import org.qortal.data.PaymentData; import org.qortal.data.PaymentData;
@ -50,51 +51,6 @@ public class ArbitraryTransactionTests extends Common {
Common.useDefaultSettings(); Common.useDefaultSettings();
} }
@Test
public void testDifficultyTooLow() throws IllegalAccessException, DataException, IOException {
try (final Repository repository = RepositoryManager.getRepository()) {
PrivateKeyAccount alice = Common.getTestAccount(repository, "alice");
String publicKey58 = Base58.encode(alice.getPublicKey());
String name = "TEST"; // Can be anything for this test
String identifier = null; // Not used for this test
Service service = Service.ARBITRARY_DATA;
int chunkSize = 100;
int dataLength = 900; // Actual data length will be longer due to encryption
// Register the name to Alice
RegisterNameTransactionData registerNameTransactionData = new RegisterNameTransactionData(TestTransaction.generateBase(alice), name, "");
registerNameTransactionData.setFee(new RegisterNameTransaction(null, null).getUnitFee(registerNameTransactionData.getTimestamp()));
TransactionUtils.signAndMint(repository, registerNameTransactionData, alice);
// Set difficulty to 1
FieldUtils.writeField(ArbitraryDataManager.getInstance(), "powDifficulty", 1, true);
// Create PUT transaction
Path path1 = ArbitraryUtils.generateRandomDataPath(dataLength);
ArbitraryDataFile arbitraryDataFile = ArbitraryUtils.createAndMintTxn(repository, publicKey58, path1, name, identifier, ArbitraryTransactionData.Method.PUT, service, alice, chunkSize);
// Check that nonce validation succeeds
byte[] signature = arbitraryDataFile.getSignature();
TransactionData transactionData = repository.getTransactionRepository().fromSignature(signature);
ArbitraryTransaction transaction = new ArbitraryTransaction(repository, transactionData);
assertTrue(transaction.isSignatureValid());
// Increase difficulty to 15
FieldUtils.writeField(ArbitraryDataManager.getInstance(), "powDifficulty", 15, true);
// Make sure the nonce validation fails
// Note: there is a very tiny chance this could succeed due to being extremely lucky
// and finding a high difficulty nonce in the first couple of cycles. It will be rare
// enough that we shouldn't need to account for it.
assertFalse(transaction.isSignatureValid());
// Reduce difficulty back to 1, to double check
FieldUtils.writeField(ArbitraryDataManager.getInstance(), "powDifficulty", 1, true);
assertTrue(transaction.isSignatureValid());
}
}
@Test @Test
public void testNonceAndFee() throws IllegalAccessException, DataException, IOException { public void testNonceAndFee() throws IllegalAccessException, DataException, IOException {
try (final Repository repository = RepositoryManager.getRepository()) { try (final Repository repository = RepositoryManager.getRepository()) {
@ -497,8 +453,9 @@ public class ArbitraryTransactionTests extends Common {
// Create PUT transaction // Create PUT transaction
Path path1 = ArbitraryUtils.generateRandomDataPath(dataLength, true); Path path1 = ArbitraryUtils.generateRandomDataPath(dataLength, true);
long fee = BlockChain.getInstance().getUnitFeeAtTimestamp(NTP.getTime());
ArbitraryDataFile arbitraryDataFile = ArbitraryUtils.createAndMintTxn(repository, publicKey58, path1, name, ArbitraryDataFile arbitraryDataFile = ArbitraryUtils.createAndMintTxn(repository, publicKey58, path1, name,
identifier, ArbitraryTransactionData.Method.PUT, service, alice, chunkSize, 0L, true, identifier, ArbitraryTransactionData.Method.PUT, service, alice, chunkSize, fee, false,
null, null, null, null); null, null, null, null);
byte[] signature = arbitraryDataFile.getSignature(); byte[] signature = arbitraryDataFile.getSignature();
@ -556,8 +513,9 @@ public class ArbitraryTransactionTests extends Common {
// Create PUT transaction // Create PUT transaction
Path path1 = ArbitraryUtils.generateRandomDataPath(dataLength, true); Path path1 = ArbitraryUtils.generateRandomDataPath(dataLength, true);
long fee = BlockChain.getInstance().getUnitFeeAtTimestamp(NTP.getTime());
ArbitraryDataFile arbitraryDataFile = ArbitraryUtils.createAndMintTxn(repository, publicKey58, path1, name, ArbitraryDataFile arbitraryDataFile = ArbitraryUtils.createAndMintTxn(repository, publicKey58, path1, name,
identifier, ArbitraryTransactionData.Method.PUT, service, alice, chunkSize, 0L, true, identifier, ArbitraryTransactionData.Method.PUT, service, alice, chunkSize, fee, false,
title, description, tags, category); title, description, tags, category);
byte[] signature = arbitraryDataFile.getSignature(); byte[] signature = arbitraryDataFile.getSignature();
@ -614,8 +572,9 @@ public class ArbitraryTransactionTests extends Common {
// Create PUT transaction // Create PUT transaction
Path path1 = ArbitraryUtils.generateRandomDataPath(dataLength, true); Path path1 = ArbitraryUtils.generateRandomDataPath(dataLength, true);
long fee = BlockChain.getInstance().getUnitFeeAtTimestamp(NTP.getTime());
ArbitraryDataFile arbitraryDataFile = ArbitraryUtils.createAndMintTxn(repository, publicKey58, path1, name, ArbitraryDataFile arbitraryDataFile = ArbitraryUtils.createAndMintTxn(repository, publicKey58, path1, name,
identifier, ArbitraryTransactionData.Method.PUT, service, alice, chunkSize, 0L, true, identifier, ArbitraryTransactionData.Method.PUT, service, alice, chunkSize, fee, false,
null, null, null, null); null, null, null, null);
byte[] signature = arbitraryDataFile.getSignature(); byte[] signature = arbitraryDataFile.getSignature();

View File

@ -5,10 +5,12 @@ import org.qortal.arbitrary.ArbitraryDataFile;
import org.qortal.arbitrary.ArbitraryDataTransactionBuilder; import org.qortal.arbitrary.ArbitraryDataTransactionBuilder;
import org.qortal.arbitrary.misc.Category; import org.qortal.arbitrary.misc.Category;
import org.qortal.arbitrary.misc.Service; import org.qortal.arbitrary.misc.Service;
import org.qortal.block.BlockChain;
import org.qortal.data.transaction.ArbitraryTransactionData; import org.qortal.data.transaction.ArbitraryTransactionData;
import org.qortal.repository.DataException; import org.qortal.repository.DataException;
import org.qortal.repository.Repository; import org.qortal.repository.Repository;
import org.qortal.transaction.Transaction; import org.qortal.transaction.Transaction;
import org.qortal.utils.NTP;
import java.io.BufferedWriter; import java.io.BufferedWriter;
import java.io.File; import java.io.File;
@ -20,16 +22,15 @@ import java.nio.file.Paths;
import java.util.List; import java.util.List;
import java.util.Random; import java.util.Random;
import static org.junit.Assert.assertEquals;
public class ArbitraryUtils { public class ArbitraryUtils {
public static ArbitraryDataFile createAndMintTxn(Repository repository, String publicKey58, Path path, String name, String identifier, public static ArbitraryDataFile createAndMintTxn(Repository repository, String publicKey58, Path path, String name, String identifier,
ArbitraryTransactionData.Method method, Service service, PrivateKeyAccount account, ArbitraryTransactionData.Method method, Service service, PrivateKeyAccount account,
int chunkSize) throws DataException { int chunkSize) throws DataException {
long fee = BlockChain.getInstance().getUnitFeeAtTimestamp(NTP.getTime());
return ArbitraryUtils.createAndMintTxn(repository, publicKey58, path, name, identifier, method, service, return ArbitraryUtils.createAndMintTxn(repository, publicKey58, path, name, identifier, method, service,
account, chunkSize, 0L, true, null, null, null, null); account, chunkSize, fee, false, null, null, null, null);
} }
public static ArbitraryDataFile createAndMintTxn(Repository repository, String publicKey58, Path path, String name, String identifier, public static ArbitraryDataFile createAndMintTxn(Repository repository, String publicKey58, Path path, String name, String identifier,
@ -47,7 +48,9 @@ public class ArbitraryUtils {
} }
ArbitraryTransactionData transactionData = txnBuilder.getArbitraryTransactionData(); ArbitraryTransactionData transactionData = txnBuilder.getArbitraryTransactionData();
Transaction.ValidationResult result = TransactionUtils.signAndImport(repository, transactionData, account); Transaction.ValidationResult result = TransactionUtils.signAndImport(repository, transactionData, account);
assertEquals(Transaction.ValidationResult.OK, result); if (result != Transaction.ValidationResult.OK) {
throw new DataException(String.format("Arbitrary transaction invalid: %s", result.toString()));
}
BlockUtils.mintBlock(repository); BlockUtils.mintBlock(repository);
// We need a new ArbitraryDataFile instance because the files will have been moved to the signature's folder // We need a new ArbitraryDataFile instance because the files will have been moved to the signature's folder

View File

@ -7,13 +7,15 @@ import org.qortal.block.BlockChain;
import org.qortal.data.transaction.BaseTransactionData; import org.qortal.data.transaction.BaseTransactionData;
import org.qortal.group.Group; import org.qortal.group.Group;
import org.qortal.repository.DataException; import org.qortal.repository.DataException;
import org.qortal.utils.NTP;
public abstract class TestTransaction { public abstract class TestTransaction {
protected static final Random random = new Random(); protected static final Random random = new Random();
public static BaseTransactionData generateBase(PrivateKeyAccount account, int txGroupId) throws DataException { public static BaseTransactionData generateBase(PrivateKeyAccount account, int txGroupId) throws DataException {
return new BaseTransactionData(System.currentTimeMillis(), txGroupId, account.getLastReference(), account.getPublicKey(), BlockChain.getInstance().getUnitFee(), null); long timestamp = System.currentTimeMillis();
return new BaseTransactionData(timestamp, txGroupId, account.getLastReference(), account.getPublicKey(), BlockChain.getInstance().getUnitFeeAtTimestamp(timestamp), null);
} }
public static BaseTransactionData generateBase(PrivateKeyAccount account) throws DataException { public static BaseTransactionData generateBase(PrivateKeyAccount account) throws DataException {

View File

@ -0,0 +1,790 @@
package org.qortal.test.crosschain;
import com.google.common.hash.HashCode;
import com.google.common.primitives.Bytes;
import org.junit.Before;
import org.junit.Test;
import org.qortal.account.Account;
import org.qortal.account.PrivateKeyAccount;
import org.qortal.asset.Asset;
import org.qortal.block.Block;
import org.qortal.crosschain.ACCT;
import org.qortal.crosschain.AcctMode;
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.transaction.BaseTransactionData;
import org.qortal.data.transaction.DeployAtTransactionData;
import org.qortal.data.transaction.MessageTransactionData;
import org.qortal.data.transaction.TransactionData;
import org.qortal.group.Group;
import org.qortal.repository.DataException;
import org.qortal.repository.Repository;
import org.qortal.repository.RepositoryManager;
import org.qortal.test.common.BlockUtils;
import org.qortal.test.common.Common;
import org.qortal.test.common.TransactionUtils;
import org.qortal.transaction.DeployAtTransaction;
import org.qortal.transaction.MessageTransaction;
import org.qortal.utils.Amounts;
import java.time.Instant;
import java.time.LocalDateTime;
import java.time.ZoneOffset;
import java.time.format.DateTimeFormatter;
import java.time.format.FormatStyle;
import java.util.Arrays;
import java.util.List;
import java.util.Random;
import java.util.function.Function;
import static org.junit.Assert.*;
public abstract class ACCTTests extends Common {
public static final byte[] secretA = "This string is exactly 32 bytes!".getBytes();
public static final byte[] hashOfSecretA = Crypto.hash160(secretA); // daf59884b4d1aec8c1b17102530909ee43c0151a
public static final int tradeTimeout = 20; // blocks
public static final long redeemAmount = 80_40200000L;
public static final long fundingAmount = 123_45600000L;
public static final long foreignAmount = 864200L; // 0.00864200 foreign units
protected static final Random RANDOM = new Random();
protected abstract byte[] getPublicKey();
protected abstract byte[] buildQortalAT(String address, byte[] publicKey, long redeemAmount, long foreignAmount, int tradeTimeout);
protected abstract ACCT getInstance();
protected abstract int calcRefundTimeout(long partnersOfferMessageTransactionTimestamp, int lockTimeA);
protected abstract byte[] buildTradeMessage(String address, byte[] publicKey, byte[] hashOfSecretA, int lockTimeA, int refundTimeout);
protected abstract byte[] buildRedeemMessage(byte[] secretA, String address);
protected abstract byte[] getCodeBytesHash();
protected abstract String getSymbol();
protected abstract String getName();
@Before
public void beforeTest() throws DataException {
Common.useDefaultSettings();
}
@Test
public void testCompile() {
PrivateKeyAccount tradeAccount = createTradeAccount(null);
byte[] creationBytes = buildQortalAT(tradeAccount.getAddress(), getPublicKey(), redeemAmount, foreignAmount, tradeTimeout);
assertNotNull(creationBytes);
System.out.println("AT creation bytes: " + HashCode.fromBytes(creationBytes).toString());
}
@Test
public void testDeploy() throws DataException {
try (final Repository repository = RepositoryManager.getRepository()) {
PrivateKeyAccount deployer = Common.getTestAccount(repository, "chloe");
PrivateKeyAccount tradeAccount = createTradeAccount(repository);
PrivateKeyAccount partner = Common.getTestAccount(repository, "dilbert");
long deployersInitialBalance = deployer.getConfirmedBalance(Asset.QORT);
long partnersInitialBalance = partner.getConfirmedBalance(Asset.QORT);
DeployAtTransaction deployAtTransaction = doDeploy(repository, deployer, tradeAccount.getAddress());
long expectedBalance = deployersInitialBalance - fundingAmount - deployAtTransaction.getTransactionData().getFee();
long actualBalance = deployer.getConfirmedBalance(Asset.QORT);
assertEquals("Deployer's post-deployment balance incorrect", expectedBalance, actualBalance);
expectedBalance = fundingAmount;
actualBalance = deployAtTransaction.getATAccount().getConfirmedBalance(Asset.QORT);
assertEquals("AT's post-deployment balance incorrect", expectedBalance, actualBalance);
expectedBalance = partnersInitialBalance;
actualBalance = partner.getConfirmedBalance(Asset.QORT);
assertEquals("Partner's post-deployment balance incorrect", expectedBalance, actualBalance);
// Test orphaning
BlockUtils.orphanLastBlock(repository);
expectedBalance = deployersInitialBalance;
actualBalance = deployer.getConfirmedBalance(Asset.QORT);
assertEquals("Deployer's post-orphan/pre-deployment balance incorrect", expectedBalance, actualBalance);
expectedBalance = 0;
actualBalance = deployAtTransaction.getATAccount().getConfirmedBalance(Asset.QORT);
assertEquals("AT's post-orphan/pre-deployment balance incorrect", expectedBalance, actualBalance);
expectedBalance = partnersInitialBalance;
actualBalance = partner.getConfirmedBalance(Asset.QORT);
assertEquals("Partner's post-orphan/pre-deployment balance incorrect", expectedBalance, actualBalance);
}
}
@SuppressWarnings("unused")
@Test
public void testOfferCancel() throws DataException {
try (final Repository repository = RepositoryManager.getRepository()) {
PrivateKeyAccount deployer = Common.getTestAccount(repository, "chloe");
PrivateKeyAccount tradeAccount = createTradeAccount(repository);
PrivateKeyAccount partner = Common.getTestAccount(repository, "dilbert");
long deployersInitialBalance = deployer.getConfirmedBalance(Asset.QORT);
long partnersInitialBalance = partner.getConfirmedBalance(Asset.QORT);
DeployAtTransaction deployAtTransaction = doDeploy(repository, deployer, tradeAccount.getAddress());
Account at = deployAtTransaction.getATAccount();
String atAddress = at.getAddress();
long deployAtFee = deployAtTransaction.getTransactionData().getFee();
long deployersPostDeploymentBalance = deployersInitialBalance - fundingAmount - deployAtFee;
// Send creator's address to AT, instead of typical partner's address
byte[] messageData = getInstance().buildCancelMessage(deployer.getAddress());
MessageTransaction messageTransaction = sendMessage(repository, deployer, messageData, atAddress);
long messageFee = messageTransaction.getTransactionData().getFee();
// AT should process 'cancel' message in next block
BlockUtils.mintBlock(repository);
describeAt(repository, atAddress);
// Check AT is finished
ATData atData = repository.getATRepository().fromATAddress(atAddress);
assertTrue(atData.getIsFinished());
// AT should be in CANCELLED mode
CrossChainTradeData tradeData = getInstance().populateTradeData(repository, atData);
assertEquals(AcctMode.CANCELLED, tradeData.mode);
// Check balances
long expectedMinimumBalance = deployersPostDeploymentBalance;
long expectedMaximumBalance = deployersInitialBalance - deployAtFee - messageFee;
long actualBalance = deployer.getConfirmedBalance(Asset.QORT);
assertTrue(String.format("Deployer's balance %s should be above minimum %s", actualBalance, expectedMinimumBalance), actualBalance > expectedMinimumBalance);
assertTrue(String.format("Deployer's balance %s should be below maximum %s", actualBalance, expectedMaximumBalance), actualBalance < expectedMaximumBalance);
// Test orphaning
BlockUtils.orphanLastBlock(repository);
// Check balances
long expectedBalance = deployersPostDeploymentBalance - messageFee;
actualBalance = deployer.getConfirmedBalance(Asset.QORT);
assertEquals("Deployer's post-orphan/pre-refund balance incorrect", expectedBalance, actualBalance);
}
}
@SuppressWarnings("unused")
@Test
public void testOfferCancelInvalidLength() throws DataException {
try (final Repository repository = RepositoryManager.getRepository()) {
PrivateKeyAccount deployer = Common.getTestAccount(repository, "chloe");
PrivateKeyAccount tradeAccount = createTradeAccount(repository);
PrivateKeyAccount partner = Common.getTestAccount(repository, "dilbert");
long deployersInitialBalance = deployer.getConfirmedBalance(Asset.QORT);
long partnersInitialBalance = partner.getConfirmedBalance(Asset.QORT);
DeployAtTransaction deployAtTransaction = doDeploy(repository, deployer, tradeAccount.getAddress());
Account at = deployAtTransaction.getATAccount();
String atAddress = at.getAddress();
long deployAtFee = deployAtTransaction.getTransactionData().getFee();
long deployersPostDeploymentBalance = deployersInitialBalance - fundingAmount - deployAtFee;
// Instead of sending creator's address to AT, send too-short/invalid message
byte[] messageData = new byte[7];
RANDOM.nextBytes(messageData);
MessageTransaction messageTransaction = sendMessage(repository, deployer, messageData, atAddress);
long messageFee = messageTransaction.getTransactionData().getFee();
// AT should process 'cancel' message in next block
// As message is too short, it will be padded to 32bytes but cancel code doesn't care about message content, so should be ok
BlockUtils.mintBlock(repository);
describeAt(repository, atAddress);
// Check AT is finished
ATData atData = repository.getATRepository().fromATAddress(atAddress);
assertTrue(atData.getIsFinished());
// AT should be in CANCELLED mode
CrossChainTradeData tradeData = getInstance().populateTradeData(repository, atData);
assertEquals(AcctMode.CANCELLED, tradeData.mode);
}
}
@SuppressWarnings("unused")
@Test
public void testTradingInfoProcessing() throws DataException {
try (final Repository repository = RepositoryManager.getRepository()) {
PrivateKeyAccount deployer = Common.getTestAccount(repository, "chloe");
PrivateKeyAccount tradeAccount = createTradeAccount(repository);
PrivateKeyAccount partner = Common.getTestAccount(repository, "dilbert");
long deployersInitialBalance = deployer.getConfirmedBalance(Asset.QORT);
long partnersInitialBalance = partner.getConfirmedBalance(Asset.QORT);
DeployAtTransaction deployAtTransaction = doDeploy(repository, deployer, tradeAccount.getAddress());
Account at = deployAtTransaction.getATAccount();
String atAddress = at.getAddress();
long partnersOfferMessageTransactionTimestamp = System.currentTimeMillis();
int lockTimeA = calcTestLockTimeA(partnersOfferMessageTransactionTimestamp);
int refundTimeout = calcRefundTimeout(partnersOfferMessageTransactionTimestamp, lockTimeA);
// Send trade info to AT
byte[] messageData = buildTradeMessage(partner.getAddress(), getPublicKey(), hashOfSecretA, lockTimeA, refundTimeout);
MessageTransaction messageTransaction = sendMessage(repository, tradeAccount, messageData, atAddress);
Block postDeploymentBlock = BlockUtils.mintBlock(repository);
int postDeploymentBlockHeight = postDeploymentBlock.getBlockData().getHeight();
long deployAtFee = deployAtTransaction.getTransactionData().getFee();
long deployersPostDeploymentBalance = deployersInitialBalance - fundingAmount - deployAtFee;
describeAt(repository, atAddress);
ATData atData = repository.getATRepository().fromATAddress(atAddress);
CrossChainTradeData tradeData = getInstance().populateTradeData(repository, atData);
// AT should be in TRADE mode
assertEquals(AcctMode.TRADING, tradeData.mode);
// Check hashOfSecretA was extracted correctly
assertTrue(Arrays.equals(hashOfSecretA, tradeData.hashOfSecretA));
// Check trade partner Qortal address was extracted correctly
assertEquals(partner.getAddress(), tradeData.qortalPartnerAddress);
// Check trade partner's Foreign Coin PKH was extracted correctly
assertTrue(Arrays.equals(getPublicKey(), tradeData.partnerForeignPKH));
// Test orphaning
BlockUtils.orphanToBlock(repository, postDeploymentBlockHeight);
// Check balances
long expectedBalance = deployersPostDeploymentBalance;
long actualBalance = deployer.getConfirmedBalance(Asset.QORT);
assertEquals("Deployer's post-orphan/pre-refund balance incorrect", expectedBalance, actualBalance);
}
}
// TEST SENDING TRADING INFO BUT NOT FROM AT CREATOR (SHOULD BE IGNORED)
@SuppressWarnings("unused")
@Test
public void testIncorrectTradeSender() throws DataException {
try (final Repository repository = RepositoryManager.getRepository()) {
PrivateKeyAccount deployer = Common.getTestAccount(repository, "chloe");
PrivateKeyAccount tradeAccount = createTradeAccount(repository);
PrivateKeyAccount partner = Common.getTestAccount(repository, "dilbert");
PrivateKeyAccount bystander = Common.getTestAccount(repository, "bob");
long deployersInitialBalance = deployer.getConfirmedBalance(Asset.QORT);
long partnersInitialBalance = partner.getConfirmedBalance(Asset.QORT);
DeployAtTransaction deployAtTransaction = doDeploy(repository, deployer, tradeAccount.getAddress());
Account at = deployAtTransaction.getATAccount();
String atAddress = at.getAddress();
long partnersOfferMessageTransactionTimestamp = System.currentTimeMillis();
int lockTimeA = calcTestLockTimeA(partnersOfferMessageTransactionTimestamp);
int refundTimeout = calcRefundTimeout(partnersOfferMessageTransactionTimestamp, lockTimeA);
// Send trade info to AT BUT NOT FROM AT CREATOR
byte[] messageData = buildTradeMessage(partner.getAddress(), getPublicKey(), hashOfSecretA, lockTimeA, refundTimeout);
MessageTransaction messageTransaction = sendMessage(repository, bystander, messageData, atAddress);
BlockUtils.mintBlock(repository);
long expectedBalance = partnersInitialBalance;
long actualBalance = partner.getConfirmedBalance(Asset.QORT);
assertEquals("Partner's post-initial-payout balance incorrect", expectedBalance, actualBalance);
describeAt(repository, atAddress);
ATData atData = repository.getATRepository().fromATAddress(atAddress);
CrossChainTradeData tradeData = getInstance().populateTradeData(repository, atData);
// AT should still be in OFFER mode
assertEquals(AcctMode.OFFERING, tradeData.mode);
}
}
@SuppressWarnings("unused")
@Test
public void testAutomaticTradeRefund() throws DataException {
try (final Repository repository = RepositoryManager.getRepository()) {
PrivateKeyAccount deployer = Common.getTestAccount(repository, "chloe");
PrivateKeyAccount tradeAccount = createTradeAccount(repository);
PrivateKeyAccount partner = Common.getTestAccount(repository, "dilbert");
long deployersInitialBalance = deployer.getConfirmedBalance(Asset.QORT);
long partnersInitialBalance = partner.getConfirmedBalance(Asset.QORT);
DeployAtTransaction deployAtTransaction = doDeploy(repository, deployer, tradeAccount.getAddress());
Account at = deployAtTransaction.getATAccount();
String atAddress = at.getAddress();
long partnersOfferMessageTransactionTimestamp = System.currentTimeMillis();
int lockTimeA = calcTestLockTimeA(partnersOfferMessageTransactionTimestamp);
int refundTimeout = calcRefundTimeout(partnersOfferMessageTransactionTimestamp, lockTimeA);
// Send trade info to AT
byte[] messageData = buildTradeMessage(partner.getAddress(), getPublicKey(), hashOfSecretA, lockTimeA, refundTimeout);
MessageTransaction messageTransaction = sendMessage(repository, tradeAccount, messageData, atAddress);
Block postDeploymentBlock = BlockUtils.mintBlock(repository);
int postDeploymentBlockHeight = postDeploymentBlock.getBlockData().getHeight();
// Check refund
long deployAtFee = deployAtTransaction.getTransactionData().getFee();
long deployersPostDeploymentBalance = deployersInitialBalance - fundingAmount - deployAtFee;
checkTradeRefund(repository, deployer, deployersInitialBalance, deployAtFee);
describeAt(repository, atAddress);
// Check AT is finished
ATData atData = repository.getATRepository().fromATAddress(atAddress);
assertTrue(atData.getIsFinished());
// AT should be in REFUNDED mode
CrossChainTradeData tradeData = getInstance().populateTradeData(repository, atData);
assertEquals(AcctMode.REFUNDED, tradeData.mode);
// Test orphaning
BlockUtils.orphanToBlock(repository, postDeploymentBlockHeight);
// Check balances
long expectedBalance = deployersPostDeploymentBalance;
long actualBalance = deployer.getConfirmedBalance(Asset.QORT);
assertEquals("Deployer's post-orphan/pre-refund balance incorrect", expectedBalance, actualBalance);
}
}
@SuppressWarnings("unused")
@Test
public void testCorrectSecretCorrectSender() throws DataException {
try (final Repository repository = RepositoryManager.getRepository()) {
PrivateKeyAccount deployer = Common.getTestAccount(repository, "chloe");
PrivateKeyAccount tradeAccount = createTradeAccount(repository);
PrivateKeyAccount partner = Common.getTestAccount(repository, "dilbert");
long deployersInitialBalance = deployer.getConfirmedBalance(Asset.QORT);
long partnersInitialBalance = partner.getConfirmedBalance(Asset.QORT);
DeployAtTransaction deployAtTransaction = doDeploy(repository, deployer, tradeAccount.getAddress());
Account at = deployAtTransaction.getATAccount();
String atAddress = at.getAddress();
long partnersOfferMessageTransactionTimestamp = System.currentTimeMillis();
int lockTimeA = calcTestLockTimeA(partnersOfferMessageTransactionTimestamp);
int refundTimeout = calcRefundTimeout(partnersOfferMessageTransactionTimestamp, lockTimeA);
// Send trade info to AT
byte[] messageData = buildTradeMessage(partner.getAddress(), getPublicKey(), hashOfSecretA, lockTimeA, refundTimeout);
MessageTransaction messageTransaction = sendMessage(repository, tradeAccount, messageData, atAddress);
// Give AT time to process message
BlockUtils.mintBlock(repository);
// Send correct secret to AT, from correct account
messageData = buildRedeemMessage(secretA, partner.getAddress());
messageTransaction = sendMessage(repository, partner, messageData, atAddress);
// AT should send funds in the next block
ATStateData preRedeemAtStateData = repository.getATRepository().getLatestATState(atAddress);
BlockUtils.mintBlock(repository);
describeAt(repository, atAddress);
// Check AT is finished
ATData atData = repository.getATRepository().fromATAddress(atAddress);
assertTrue(atData.getIsFinished());
// AT should be in REDEEMED mode
CrossChainTradeData tradeData = getInstance().populateTradeData(repository, atData);
assertEquals(AcctMode.REDEEMED, tradeData.mode);
// Check balances
long expectedBalance = partnersInitialBalance - messageTransaction.getTransactionData().getFee() + redeemAmount;
long actualBalance = partner.getConfirmedBalance(Asset.QORT);
assertEquals("Partner's post-redeem balance incorrect", expectedBalance, actualBalance);
// Orphan redeem
BlockUtils.orphanLastBlock(repository);
// Check balances
expectedBalance = partnersInitialBalance - messageTransaction.getTransactionData().getFee();
actualBalance = partner.getConfirmedBalance(Asset.QORT);
assertEquals("Partner's post-orphan/pre-redeem balance incorrect", expectedBalance, actualBalance);
// Check AT state
ATStateData postOrphanAtStateData = repository.getATRepository().getLatestATState(atAddress);
assertTrue("AT states mismatch", Arrays.equals(preRedeemAtStateData.getStateData(), postOrphanAtStateData.getStateData()));
}
}
@SuppressWarnings("unused")
@Test
public void testCorrectSecretIncorrectSender() throws DataException {
try (final Repository repository = RepositoryManager.getRepository()) {
PrivateKeyAccount deployer = Common.getTestAccount(repository, "chloe");
PrivateKeyAccount tradeAccount = createTradeAccount(repository);
PrivateKeyAccount partner = Common.getTestAccount(repository, "dilbert");
PrivateKeyAccount bystander = Common.getTestAccount(repository, "bob");
long deployersInitialBalance = deployer.getConfirmedBalance(Asset.QORT);
long partnersInitialBalance = partner.getConfirmedBalance(Asset.QORT);
DeployAtTransaction deployAtTransaction = doDeploy(repository, deployer, tradeAccount.getAddress());
long deployAtFee = deployAtTransaction.getTransactionData().getFee();
Account at = deployAtTransaction.getATAccount();
String atAddress = at.getAddress();
long partnersOfferMessageTransactionTimestamp = System.currentTimeMillis();
int lockTimeA = calcTestLockTimeA(partnersOfferMessageTransactionTimestamp);
int refundTimeout = calcRefundTimeout(partnersOfferMessageTransactionTimestamp, lockTimeA);
// Send trade info to AT
byte[] messageData = buildTradeMessage(partner.getAddress(), getPublicKey(), hashOfSecretA, lockTimeA, refundTimeout);
MessageTransaction messageTransaction = sendMessage(repository, tradeAccount, messageData, atAddress);
// Give AT time to process message
BlockUtils.mintBlock(repository);
// Send correct secret to AT, but from wrong account
messageData = buildRedeemMessage(secretA, partner.getAddress());
messageTransaction = sendMessage(repository, bystander, messageData, atAddress);
// AT should NOT send funds in the next block
ATStateData preRedeemAtStateData = repository.getATRepository().getLatestATState(atAddress);
BlockUtils.mintBlock(repository);
describeAt(repository, atAddress);
// Check AT is NOT finished
ATData atData = repository.getATRepository().fromATAddress(atAddress);
assertFalse(atData.getIsFinished());
// AT should still be in TRADE mode
CrossChainTradeData tradeData = getInstance().populateTradeData(repository, atData);
assertEquals(AcctMode.TRADING, tradeData.mode);
// Check balances
long expectedBalance = partnersInitialBalance;
long actualBalance = partner.getConfirmedBalance(Asset.QORT);
assertEquals("Partner's balance incorrect", expectedBalance, actualBalance);
// Check eventual refund
checkTradeRefund(repository, deployer, deployersInitialBalance, deployAtFee);
}
}
@SuppressWarnings("unused")
@Test
public void testIncorrectSecretCorrectSender() throws DataException {
try (final Repository repository = RepositoryManager.getRepository()) {
PrivateKeyAccount deployer = Common.getTestAccount(repository, "chloe");
PrivateKeyAccount tradeAccount = createTradeAccount(repository);
PrivateKeyAccount partner = Common.getTestAccount(repository, "dilbert");
long deployersInitialBalance = deployer.getConfirmedBalance(Asset.QORT);
long partnersInitialBalance = partner.getConfirmedBalance(Asset.QORT);
DeployAtTransaction deployAtTransaction = doDeploy(repository, deployer, tradeAccount.getAddress());
long deployAtFee = deployAtTransaction.getTransactionData().getFee();
Account at = deployAtTransaction.getATAccount();
String atAddress = at.getAddress();
long partnersOfferMessageTransactionTimestamp = System.currentTimeMillis();
int lockTimeA = calcTestLockTimeA(partnersOfferMessageTransactionTimestamp);
int refundTimeout = calcRefundTimeout(partnersOfferMessageTransactionTimestamp, lockTimeA);
// Send trade info to AT
byte[] messageData = buildTradeMessage(partner.getAddress(), getPublicKey(), hashOfSecretA, lockTimeA, refundTimeout);
MessageTransaction messageTransaction = sendMessage(repository, tradeAccount, messageData, atAddress);
// Give AT time to process message
BlockUtils.mintBlock(repository);
// Send incorrect secret to AT, from correct account
byte[] wrongSecret = new byte[32];
RANDOM.nextBytes(wrongSecret);
messageData = buildRedeemMessage(wrongSecret, partner.getAddress());
messageTransaction = sendMessage(repository, partner, messageData, atAddress);
// AT should NOT send funds in the next block
ATStateData preRedeemAtStateData = repository.getATRepository().getLatestATState(atAddress);
BlockUtils.mintBlock(repository);
describeAt(repository, atAddress);
// Check AT is NOT finished
ATData atData = repository.getATRepository().fromATAddress(atAddress);
assertFalse(atData.getIsFinished());
// AT should still be in TRADE mode
CrossChainTradeData tradeData = getInstance().populateTradeData(repository, atData);
assertEquals(AcctMode.TRADING, tradeData.mode);
long expectedBalance = partnersInitialBalance - messageTransaction.getTransactionData().getFee();
long actualBalance = partner.getConfirmedBalance(Asset.QORT);
assertEquals("Partner's balance incorrect", expectedBalance, actualBalance);
// Check eventual refund
checkTradeRefund(repository, deployer, deployersInitialBalance, deployAtFee);
}
}
@SuppressWarnings("unused")
@Test
public void testCorrectSecretCorrectSenderInvalidMessageLength() throws DataException {
try (final Repository repository = RepositoryManager.getRepository()) {
PrivateKeyAccount deployer = Common.getTestAccount(repository, "chloe");
PrivateKeyAccount tradeAccount = createTradeAccount(repository);
PrivateKeyAccount partner = Common.getTestAccount(repository, "dilbert");
long deployersInitialBalance = deployer.getConfirmedBalance(Asset.QORT);
long partnersInitialBalance = partner.getConfirmedBalance(Asset.QORT);
DeployAtTransaction deployAtTransaction = doDeploy(repository, deployer, tradeAccount.getAddress());
Account at = deployAtTransaction.getATAccount();
String atAddress = at.getAddress();
long partnersOfferMessageTransactionTimestamp = System.currentTimeMillis();
int lockTimeA = calcTestLockTimeA(partnersOfferMessageTransactionTimestamp);
int refundTimeout = calcRefundTimeout(partnersOfferMessageTransactionTimestamp, lockTimeA);
// Send trade info to AT
byte[] messageData = buildTradeMessage(partner.getAddress(), getPublicKey(), hashOfSecretA, lockTimeA, refundTimeout);
MessageTransaction messageTransaction = sendMessage(repository, tradeAccount, messageData, atAddress);
// Give AT time to process message
BlockUtils.mintBlock(repository);
// Send correct secret to AT, from correct account, but missing receive address, hence incorrect length
messageData = Bytes.concat(secretA);
messageTransaction = sendMessage(repository, partner, messageData, atAddress);
// AT should NOT send funds in the next block
ATStateData preRedeemAtStateData = repository.getATRepository().getLatestATState(atAddress);
BlockUtils.mintBlock(repository);
describeAt(repository, atAddress);
// Check AT is NOT finished
ATData atData = repository.getATRepository().fromATAddress(atAddress);
assertFalse(atData.getIsFinished());
// AT should be in TRADING mode
CrossChainTradeData tradeData = getInstance().populateTradeData(repository, atData);
assertEquals(AcctMode.TRADING, tradeData.mode);
}
}
@SuppressWarnings("unused")
@Test
public void testDescribeDeployed() throws DataException {
try (final Repository repository = RepositoryManager.getRepository()) {
PrivateKeyAccount deployer = Common.getTestAccount(repository, "chloe");
PrivateKeyAccount tradeAccount = createTradeAccount(repository);
PrivateKeyAccount partner = Common.getTestAccount(repository, "dilbert");
long deployersInitialBalance = deployer.getConfirmedBalance(Asset.QORT);
long partnersInitialBalance = partner.getConfirmedBalance(Asset.QORT);
DeployAtTransaction deployAtTransaction = doDeploy(repository, deployer, tradeAccount.getAddress());
List<ATData> executableAts = repository.getATRepository().getAllExecutableATs();
for (ATData atData : executableAts) {
String atAddress = atData.getATAddress();
byte[] codeBytes = atData.getCodeBytes();
byte[] codeHash = Crypto.digest(codeBytes);
System.out.println(String.format("%s: code length: %d byte%s, code hash: %s",
atAddress,
codeBytes.length,
(codeBytes.length != 1 ? "s": ""),
HashCode.fromBytes(codeHash)));
// Not one of ours?
if (!Arrays.equals(codeHash, getCodeBytesHash()))
continue;
describeAt(repository, atAddress);
}
}
}
protected int calcTestLockTimeA(long messageTimestamp) {
return (int) (messageTimestamp / 1000L + tradeTimeout * 60);
}
protected DeployAtTransaction doDeploy(Repository repository, PrivateKeyAccount deployer, String tradeAddress) throws DataException {
byte[] creationBytes = buildQortalAT(tradeAddress, getPublicKey(), redeemAmount, foreignAmount, tradeTimeout);
long txTimestamp = System.currentTimeMillis();
byte[] lastReference = deployer.getLastReference();
if (lastReference == null) {
System.err.println(String.format("Qortal account %s has no last reference", deployer.getAddress()));
System.exit(2);
}
Long fee = null;
String name = "QORT-" + getSymbol() + " cross-chain trade";
String description = String.format("Qortal-" + getName() + " cross-chain trade");
String atType = "ACCT";
String tags = "QORT-" + getSymbol() + " ACCT";
BaseTransactionData baseTransactionData = new BaseTransactionData(txTimestamp, Group.NO_GROUP, lastReference, deployer.getPublicKey(), fee, null);
TransactionData deployAtTransactionData = new DeployAtTransactionData(baseTransactionData, name, description, atType, tags, creationBytes, fundingAmount, Asset.QORT);
DeployAtTransaction deployAtTransaction = new DeployAtTransaction(repository, deployAtTransactionData);
fee = deployAtTransaction.calcRecommendedFee();
deployAtTransactionData.setFee(fee);
TransactionUtils.signAndMint(repository, deployAtTransactionData, deployer);
return deployAtTransaction;
}
protected MessageTransaction sendMessage(Repository repository, PrivateKeyAccount sender, byte[] data, String recipient) throws DataException {
long txTimestamp = System.currentTimeMillis();
byte[] lastReference = sender.getLastReference();
if (lastReference == null) {
System.err.println(String.format("Qortal account %s has no last reference", sender.getAddress()));
System.exit(2);
}
Long fee = null;
int version = 4;
int nonce = 0;
long amount = 0;
Long assetId = null; // because amount is zero
BaseTransactionData baseTransactionData = new BaseTransactionData(txTimestamp, Group.NO_GROUP, lastReference, sender.getPublicKey(), fee, null);
TransactionData messageTransactionData = new MessageTransactionData(baseTransactionData, version, nonce, recipient, amount, assetId, data, false, false);
MessageTransaction messageTransaction = new MessageTransaction(repository, messageTransactionData);
fee = messageTransaction.calcRecommendedFee();
messageTransactionData.setFee(fee);
TransactionUtils.signAndMint(repository, messageTransactionData, sender);
return messageTransaction;
}
protected void checkTradeRefund(Repository repository, Account deployer, long deployersInitialBalance, long deployAtFee) throws DataException {
long deployersPostDeploymentBalance = deployersInitialBalance - fundingAmount - deployAtFee;
int refundTimeout = tradeTimeout / 2 + 1; // close enough
// AT should automatically refund deployer after 'refundTimeout' blocks
for (int blockCount = 0; blockCount <= refundTimeout; ++blockCount)
BlockUtils.mintBlock(repository);
// We don't bother to exactly calculate QORT spent running AT for several blocks, but we do know the expected range
long expectedMinimumBalance = deployersPostDeploymentBalance;
long expectedMaximumBalance = deployersInitialBalance - deployAtFee;
long actualBalance = deployer.getConfirmedBalance(Asset.QORT);
assertTrue(String.format("Deployer's balance %s should be above minimum %s", actualBalance, expectedMinimumBalance), actualBalance > expectedMinimumBalance);
assertTrue(String.format("Deployer's balance %s should be below maximum %s", actualBalance, expectedMaximumBalance), actualBalance < expectedMaximumBalance);
}
protected void describeAt(Repository repository, String atAddress) throws DataException {
ATData atData = repository.getATRepository().fromATAddress(atAddress);
CrossChainTradeData tradeData = getInstance().populateTradeData(repository, atData);
Function<Long, String> epochMilliFormatter = (timestamp) -> LocalDateTime.ofInstant(Instant.ofEpochMilli(timestamp), ZoneOffset.UTC).format(DateTimeFormatter.ofLocalizedDateTime(FormatStyle.MEDIUM));
int currentBlockHeight = repository.getBlockRepository().getBlockchainHeight();
System.out.print(String.format("%s:\n"
+ "\tmode: %s\n"
+ "\tcreator: %s,\n"
+ "\tcreation timestamp: %s,\n"
+ "\tcurrent balance: %s QORT,\n"
+ "\tis finished: %b,\n"
+ "\tredeem payout: %s QORT,\n"
+ "\texpected " + getName() + ": %s " + getSymbol() + ",\n"
+ "\tcurrent block height: %d,\n",
tradeData.qortalAtAddress,
tradeData.mode,
tradeData.qortalCreator,
epochMilliFormatter.apply(tradeData.creationTimestamp),
Amounts.prettyAmount(tradeData.qortBalance),
atData.getIsFinished(),
Amounts.prettyAmount(tradeData.qortAmount),
Amounts.prettyAmount(tradeData.expectedForeignAmount),
currentBlockHeight));
describeRefundAt(tradeData, epochMilliFormatter);
}
protected void describeRefundAt(CrossChainTradeData tradeData, Function<Long, String> epochMilliFormatter) {
if (tradeData.mode != AcctMode.OFFERING && tradeData.mode != AcctMode.CANCELLED) {
System.out.println(String.format("\trefund timeout: %d minutes,\n"
+ "\trefund height: block %d,\n"
+ "\tHASH160 of secret-A: %s,\n"
+ "\t" + getName() + " P2SH-A nLockTime: %d (%s),\n"
+ "\ttrade partner: %s\n"
+ "\tpartner's receiving address: %s",
tradeData.refundTimeout,
tradeData.tradeRefundHeight,
HashCode.fromBytes(tradeData.hashOfSecretA).toString().substring(0, 40),
tradeData.lockTimeA, epochMilliFormatter.apply(tradeData.lockTimeA * 1000L),
tradeData.qortalPartnerAddress,
tradeData.qortalPartnerReceivingAddress));
}
}
protected PrivateKeyAccount createTradeAccount(Repository repository) {
// We actually use a known test account with funds to avoid PoW compute
return Common.getTestAccount(repository, "alice");
}
}

View File

@ -1,121 +1,59 @@
package org.qortal.test.crosschain; package org.qortal.test.crosschain;
import static org.junit.Assert.*;
import java.util.Arrays;
import org.bitcoinj.core.Transaction;
import org.bitcoinj.store.BlockStoreException;
import org.junit.After;
import org.junit.Before;
import org.junit.Ignore; import org.junit.Ignore;
import org.junit.Test; import org.junit.Test;
import org.qortal.crosschain.Bitcoin; import org.qortal.crosschain.Bitcoin;
import org.qortal.crosschain.ForeignBlockchainException; import org.qortal.crosschain.Bitcoiny;
import org.qortal.crosschain.BitcoinyHTLC;
import org.qortal.repository.DataException;
import org.qortal.test.common.Common;
public class BitcoinTests extends Common { public class BitcoinTests extends BitcoinyTests {
private Bitcoin bitcoin; @Override
protected String getCoinName() {
@Before return "Bitcoin";
public void beforeTest() throws DataException {
Common.useDefaultSettings(); // TestNet3
bitcoin = Bitcoin.getInstance();
} }
@After @Override
public void afterTest() { protected String getCoinSymbol() {
return "BTC";
}
@Override
protected Bitcoiny getCoin() {
return Bitcoin.getInstance();
}
@Override
protected void resetCoinForTesting() {
Bitcoin.resetForTesting(); Bitcoin.resetForTesting();
bitcoin = null; }
@Override
protected String getDeterministicKey58() {
return "tprv8ZgxMBicQKsPdahhFSrCdvC1bsWyzHHZfTneTVqUXN6s1wEtZLwAkZXzFP6TYLg2aQMecZLXLre5bTVGajEB55L1HYJcawpdFG66STVAWPJ";
}
@Override
protected String getRecipient() {
return "2N8WCg52ULCtDSMjkgVTm5mtPdCsUptkHWE";
} }
@Test @Test
@Ignore("Often fails due to unreliable BTC testnet ElectrumX servers") @Ignore("Often fails due to unreliable BTC testnet ElectrumX servers")
public void testGetMedianBlockTime() throws BlockStoreException, ForeignBlockchainException { public void testGetMedianBlockTime() {}
System.out.println(String.format("Starting BTC instance..."));
System.out.println(String.format("BTC instance started"));
long before = System.currentTimeMillis();
System.out.println(String.format("Bitcoin median blocktime: %d", bitcoin.getMedianBlockTime()));
long afterFirst = System.currentTimeMillis();
System.out.println(String.format("Bitcoin median blocktime: %d", bitcoin.getMedianBlockTime()));
long afterSecond = System.currentTimeMillis();
long firstPeriod = afterFirst - before;
long secondPeriod = afterSecond - afterFirst;
System.out.println(String.format("1st call: %d ms, 2nd call: %d ms", firstPeriod, secondPeriod));
assertTrue("2nd call should be quicker than 1st", secondPeriod < firstPeriod);
assertTrue("2nd call should take less than 5 seconds", secondPeriod < 5000L);
}
@Test @Test
@Ignore("Often fails due to unreliable BTC testnet ElectrumX servers") @Ignore("Often fails due to unreliable BTC testnet ElectrumX servers")
public void testFindHtlcSecret() throws ForeignBlockchainException { public void testFindHtlcSecret() {}
// This actually exists on TEST3 but can take a while to fetch
String p2shAddress = "2N8WCg52ULCtDSMjkgVTm5mtPdCsUptkHWE";
byte[] expectedSecret = "This string is exactly 32 bytes!".getBytes();
byte[] secret = BitcoinyHTLC.findHtlcSecret(bitcoin, p2shAddress);
assertNotNull(secret);
assertTrue("secret incorrect", Arrays.equals(expectedSecret, secret));
}
@Test @Test
@Ignore("Often fails due to unreliable BTC testnet ElectrumX servers") @Ignore("Often fails due to unreliable BTC testnet ElectrumX servers")
public void testBuildSpend() { public void testBuildSpend() {}
String xprv58 = "tprv8ZgxMBicQKsPdahhFSrCdvC1bsWyzHHZfTneTVqUXN6s1wEtZLwAkZXzFP6TYLg2aQMecZLXLre5bTVGajEB55L1HYJcawpdFG66STVAWPJ";
String recipient = "2N8WCg52ULCtDSMjkgVTm5mtPdCsUptkHWE";
long amount = 1000L;
Transaction transaction = bitcoin.buildSpend(xprv58, recipient, amount);
assertNotNull(transaction);
// Check spent key caching doesn't affect outcome
transaction = bitcoin.buildSpend(xprv58, recipient, amount);
assertNotNull(transaction);
}
@Test @Test
@Ignore("Often fails due to unreliable BTC testnet ElectrumX servers") @Ignore("Often fails due to unreliable BTC testnet ElectrumX servers")
public void testGetWalletBalance() throws ForeignBlockchainException { public void testGetWalletBalance() {}
String xprv58 = "tprv8ZgxMBicQKsPdahhFSrCdvC1bsWyzHHZfTneTVqUXN6s1wEtZLwAkZXzFP6TYLg2aQMecZLXLre5bTVGajEB55L1HYJcawpdFG66STVAWPJ";
Long balance = bitcoin.getWalletBalance(xprv58);
assertNotNull(balance);
System.out.println(bitcoin.format(balance));
// Check spent key caching doesn't affect outcome
Long repeatBalance = bitcoin.getWalletBalance(xprv58);
assertNotNull(repeatBalance);
System.out.println(bitcoin.format(repeatBalance));
assertEquals(balance, repeatBalance);
}
@Test @Test
@Ignore("Often fails due to unreliable BTC testnet ElectrumX servers") @Ignore("Often fails due to unreliable BTC testnet ElectrumX servers")
public void testGetUnusedReceiveAddress() throws ForeignBlockchainException { public void testGetUnusedReceiveAddress() {}
String xprv58 = "tprv8ZgxMBicQKsPdahhFSrCdvC1bsWyzHHZfTneTVqUXN6s1wEtZLwAkZXzFP6TYLg2aQMecZLXLre5bTVGajEB55L1HYJcawpdFG66STVAWPJ";
String address = bitcoin.getUnusedReceiveAddress(xprv58);
assertNotNull(address);
System.out.println(address);
}
} }

View File

@ -0,0 +1,130 @@
package org.qortal.test.crosschain;
import org.bitcoinj.core.Transaction;
import org.junit.After;
import org.junit.Before;
import org.junit.Test;
import org.qortal.crosschain.Bitcoiny;
import org.qortal.crosschain.BitcoinyHTLC;
import org.qortal.crosschain.ForeignBlockchainException;
import org.qortal.repository.DataException;
import org.qortal.test.common.Common;
import java.util.Arrays;
import static org.junit.Assert.*;
public abstract class BitcoinyTests extends Common {
protected Bitcoiny bitcoiny;
protected abstract String getCoinName();
protected abstract String getCoinSymbol();
protected abstract Bitcoiny getCoin();
protected abstract void resetCoinForTesting();
protected abstract String getDeterministicKey58();
protected abstract String getRecipient();
@Before
public void beforeTest() throws DataException {
Common.useDefaultSettings(); // TestNet3
bitcoiny = getCoin();
}
@After
public void afterTest() {
resetCoinForTesting();
bitcoiny = null;
}
@Test
public void testGetMedianBlockTime() throws ForeignBlockchainException {
System.out.println(String.format("Starting " + getCoinSymbol() + " instance..."));
System.out.println(String.format(getCoinSymbol() + " instance started"));
long before = System.currentTimeMillis();
System.out.println(String.format(getCoinName() + " median blocktime: %d", bitcoiny.getMedianBlockTime()));
long afterFirst = System.currentTimeMillis();
System.out.println(String.format(getCoinName() + " median blocktime: %d", bitcoiny.getMedianBlockTime()));
long afterSecond = System.currentTimeMillis();
long firstPeriod = afterFirst - before;
long secondPeriod = afterSecond - afterFirst;
System.out.println(String.format("1st call: %d ms, 2nd call: %d ms", firstPeriod, secondPeriod));
makeGetMedianBlockTimeAssertions(firstPeriod, secondPeriod);
}
public void makeGetMedianBlockTimeAssertions(long firstPeriod, long secondPeriod) {
assertTrue("2nd call should be quicker than 1st", secondPeriod < firstPeriod);
assertTrue("2nd call should take less than 5 seconds", secondPeriod < 5000L);
}
@Test
public void testFindHtlcSecret() throws ForeignBlockchainException {
// This actually exists on TEST3 but can take a while to fetch
String p2shAddress = "2N8WCg52ULCtDSMjkgVTm5mtPdCsUptkHWE";
byte[] expectedSecret = "This string is exactly 32 bytes!".getBytes();
byte[] secret = BitcoinyHTLC.findHtlcSecret(bitcoiny, p2shAddress);
assertNotNull(secret);
assertTrue("secret incorrect", Arrays.equals(expectedSecret, secret));
}
@Test
public void testBuildSpend() {
String xprv58 = getDeterministicKey58();
String recipient = getRecipient();
long amount = 1000L;
Transaction transaction = bitcoiny.buildSpend(xprv58, recipient, amount);
assertNotNull(transaction);
// Check spent key caching doesn't affect outcome
transaction = bitcoiny.buildSpend(xprv58, recipient, amount);
assertNotNull(transaction);
}
@Test
public void testGetWalletBalance() throws ForeignBlockchainException {
String xprv58 = getDeterministicKey58();
Long balance = bitcoiny.getWalletBalance(xprv58);
assertNotNull(balance);
System.out.println(bitcoiny.format(balance));
// Check spent key caching doesn't affect outcome
Long repeatBalance = bitcoiny.getWalletBalance(xprv58);
assertNotNull(repeatBalance);
System.out.println(bitcoiny.format(repeatBalance));
assertEquals(balance, repeatBalance);
}
@Test
public void testGetUnusedReceiveAddress() throws ForeignBlockchainException {
String xprv58 = getDeterministicKey58();
String address = bitcoiny.getUnusedReceiveAddress(xprv58);
assertNotNull(address);
System.out.println(address);
}
}

View File

@ -1,115 +1,48 @@
package org.qortal.test.crosschain; package org.qortal.test.crosschain;
import static org.junit.Assert.*;
import java.util.Arrays;
import org.bitcoinj.core.Transaction;
import org.bitcoinj.store.BlockStoreException;
import org.junit.After;
import org.junit.Before;
import org.junit.Ignore; import org.junit.Ignore;
import org.junit.Test; import org.junit.Test;
import org.qortal.crosschain.ForeignBlockchainException; import org.qortal.crosschain.Bitcoiny;
import org.qortal.crosschain.Digibyte; import org.qortal.crosschain.Digibyte;
import org.qortal.crosschain.BitcoinyHTLC;
import org.qortal.repository.DataException;
import org.qortal.test.common.Common;
public class DigibyteTests extends Common { public class DigibyteTests extends BitcoinyTests {
private Digibyte digibyte; @Override
protected String getCoinName() {
@Before return "Digibyte";
public void beforeTest() throws DataException {
Common.useDefaultSettings(); // TestNet3
digibyte = Digibyte.getInstance();
} }
@After @Override
public void afterTest() { protected String getCoinSymbol() {
return "DGB";
}
@Override
protected Bitcoiny getCoin() {
return Digibyte.getInstance();
}
@Override
protected void resetCoinForTesting() {
Digibyte.resetForTesting(); Digibyte.resetForTesting();
digibyte = null;
} }
@Test @Override
public void testGetMedianBlockTime() throws BlockStoreException, ForeignBlockchainException { protected String getDeterministicKey58() {
long before = System.currentTimeMillis(); return "xpub661MyMwAqRbcEnabTLX5uebYcsE3uG5y7ve9jn1VK8iY1MaU3YLoLJEe8sTu2YVav5Zka5qf2dmMssfxmXJTqZnazZL2kL7M2tNKwEoC34R";
System.out.println(String.format("Digibyte median blocktime: %d", digibyte.getMedianBlockTime())); }
long afterFirst = System.currentTimeMillis();
System.out.println(String.format("Digibyte median blocktime: %d", digibyte.getMedianBlockTime())); @Override
long afterSecond = System.currentTimeMillis(); protected String getRecipient() {
return "2N8WCg52ULCtDSMjkgVTm5mtPdCsUptkHWE";
long firstPeriod = afterFirst - before;
long secondPeriod = afterSecond - afterFirst;
System.out.println(String.format("1st call: %d ms, 2nd call: %d ms", firstPeriod, secondPeriod));
assertTrue("2nd call should be quicker than 1st", secondPeriod < firstPeriod);
assertTrue("2nd call should take less than 5 seconds", secondPeriod < 5000L);
} }
@Test @Test
@Ignore(value = "Doesn't work, to be fixed later") @Ignore(value = "Doesn't work, to be fixed later")
public void testFindHtlcSecret() throws ForeignBlockchainException { public void testFindHtlcSecret() {}
// This actually exists on TEST3 but can take a while to fetch
String p2shAddress = "2N8WCg52ULCtDSMjkgVTm5mtPdCsUptkHWE";
byte[] expectedSecret = "This string is exactly 32 bytes!".getBytes();
byte[] secret = BitcoinyHTLC.findHtlcSecret(digibyte, p2shAddress);
assertNotNull("secret not found", secret);
assertTrue("secret incorrect", Arrays.equals(expectedSecret, secret));
}
@Test @Test
@Ignore(value = "No testnet nodes available, so we can't regularly test buildSpend yet") @Ignore(value = "No testnet nodes available, so we can't regularly test buildSpend yet")
public void testBuildSpend() { public void testBuildSpend() {}
String xprv58 = "tprv8ZgxMBicQKsPdahhFSrCdvC1bsWyzHHZfTneTVqUXN6s1wEtZLwAkZXzFP6TYLg2aQMecZLXLre5bTVGajEB55L1HYJcawpdFG66STVAWPJ";
String recipient = "2N8WCg52ULCtDSMjkgVTm5mtPdCsUptkHWE";
long amount = 1000L;
Transaction transaction = digibyte.buildSpend(xprv58, recipient, amount);
assertNotNull("insufficient funds", transaction);
// Check spent key caching doesn't affect outcome
transaction = digibyte.buildSpend(xprv58, recipient, amount);
assertNotNull("insufficient funds", transaction);
} }
@Test
public void testGetWalletBalance() throws ForeignBlockchainException {
String xprv58 = "xpub661MyMwAqRbcEnabTLX5uebYcsE3uG5y7ve9jn1VK8iY1MaU3YLoLJEe8sTu2YVav5Zka5qf2dmMssfxmXJTqZnazZL2kL7M2tNKwEoC34R";
Long balance = digibyte.getWalletBalance(xprv58);
assertNotNull(balance);
System.out.println(digibyte.format(balance));
// Check spent key caching doesn't affect outcome
Long repeatBalance = digibyte.getWalletBalance(xprv58);
assertNotNull(repeatBalance);
System.out.println(digibyte.format(repeatBalance));
assertEquals(balance, repeatBalance);
}
@Test
public void testGetUnusedReceiveAddress() throws ForeignBlockchainException {
String xprv58 = "xpub661MyMwAqRbcEnabTLX5uebYcsE3uG5y7ve9jn1VK8iY1MaU3YLoLJEe8sTu2YVav5Zka5qf2dmMssfxmXJTqZnazZL2kL7M2tNKwEoC34R";
String address = digibyte.getUnusedReceiveAddress(xprv58);
assertNotNull(address);
System.out.println(address);
}
}

View File

@ -1,115 +1,47 @@
package org.qortal.test.crosschain; package org.qortal.test.crosschain;
import org.bitcoinj.core.Transaction;
import org.bitcoinj.store.BlockStoreException;
import org.junit.After;
import org.junit.Before;
import org.junit.Ignore; import org.junit.Ignore;
import org.junit.Test; import org.junit.Test;
import org.qortal.crosschain.BitcoinyHTLC; import org.qortal.crosschain.Bitcoiny;
import org.qortal.crosschain.ForeignBlockchainException;
import org.qortal.crosschain.Dogecoin; import org.qortal.crosschain.Dogecoin;
import org.qortal.repository.DataException;
import org.qortal.test.common.Common;
import java.util.Arrays; public class DogecoinTests extends BitcoinyTests {
import static org.junit.Assert.*; @Override
protected String getCoinName() {
public class DogecoinTests extends Common { return "Dogecoin";
private Dogecoin dogecoin;
@Before
public void beforeTest() throws DataException {
Common.useDefaultSettings(); // TestNet3
dogecoin = Dogecoin.getInstance();
} }
@After @Override
public void afterTest() { protected String getCoinSymbol() {
return "DOGE";
}
@Override
protected Bitcoiny getCoin() {
return Dogecoin.getInstance();
}
@Override
protected void resetCoinForTesting() {
Dogecoin.resetForTesting(); Dogecoin.resetForTesting();
dogecoin = null;
} }
@Test @Override
public void testGetMedianBlockTime() throws BlockStoreException, ForeignBlockchainException { protected String getDeterministicKey58() {
long before = System.currentTimeMillis(); return "dgpv51eADS3spNJh9drNeW1Tc1P9z2LyaQRXPBortsq6yice1k47C2u2Prvgxycr2ihNBWzKZ2LthcBBGiYkWZ69KUTVkcLVbnjq7pD8mnApEru";
System.out.println(String.format("Dogecoin median blocktime: %d", dogecoin.getMedianBlockTime())); }
long afterFirst = System.currentTimeMillis();
System.out.println(String.format("Dogecoin median blocktime: %d", dogecoin.getMedianBlockTime())); @Override
long afterSecond = System.currentTimeMillis(); protected String getRecipient() {
return null;
long firstPeriod = afterFirst - before;
long secondPeriod = afterSecond - afterFirst;
System.out.println(String.format("1st call: %d ms, 2nd call: %d ms", firstPeriod, secondPeriod));
assertTrue("2nd call should be quicker than 1st", secondPeriod < firstPeriod);
assertTrue("2nd call should take less than 5 seconds", secondPeriod < 5000L);
} }
@Test @Test
@Ignore(value = "Doesn't work, to be fixed later") @Ignore(value = "Doesn't work, to be fixed later")
public void testFindHtlcSecret() throws ForeignBlockchainException { public void testFindHtlcSecret() {}
// This actually exists on TEST3 but can take a while to fetch
String p2shAddress = "2N8WCg52ULCtDSMjkgVTm5mtPdCsUptkHWE";
byte[] expectedSecret = "This string is exactly 32 bytes!".getBytes();
byte[] secret = BitcoinyHTLC.findHtlcSecret(dogecoin, p2shAddress);
assertNotNull("secret not found", secret);
assertTrue("secret incorrect", Arrays.equals(expectedSecret, secret));
}
@Test @Test
@Ignore(value = "No testnet nodes available, so we can't regularly test buildSpend yet") @Ignore(value = "No testnet nodes available, so we can't regularly test buildSpend yet")
public void testBuildSpend() { public void testBuildSpend() {}
String xprv58 = "dgpv51eADS3spNJh9drNeW1Tc1P9z2LyaQRXPBortsq6yice1k47C2u2Prvgxycr2ihNBWzKZ2LthcBBGiYkWZ69KUTVkcLVbnjq7pD8mnApEru";
String recipient = "DP1iFao33xdEPa5vaArpj7sykfzKNeiJeX";
long amount = 1000L;
Transaction transaction = dogecoin.buildSpend(xprv58, recipient, amount);
assertNotNull("insufficient funds", transaction);
// Check spent key caching doesn't affect outcome
transaction = dogecoin.buildSpend(xprv58, recipient, amount);
assertNotNull("insufficient funds", transaction);
}
@Test
public void testGetWalletBalance() throws ForeignBlockchainException {
String xprv58 = "dgpv51eADS3spNJh9drNeW1Tc1P9z2LyaQRXPBortsq6yice1k47C2u2Prvgxycr2ihNBWzKZ2LthcBBGiYkWZ69KUTVkcLVbnjq7pD8mnApEru";
Long balance = dogecoin.getWalletBalance(xprv58);
assertNotNull(balance);
System.out.println(dogecoin.format(balance));
// Check spent key caching doesn't affect outcome
Long repeatBalance = dogecoin.getWalletBalance(xprv58);
assertNotNull(repeatBalance);
System.out.println(dogecoin.format(repeatBalance));
assertEquals(balance, repeatBalance);
}
@Test
public void testGetUnusedReceiveAddress() throws ForeignBlockchainException {
String xprv58 = "dgpv51eADS3spNJh9drNeW1Tc1P9z2LyaQRXPBortsq6yice1k47C2u2Prvgxycr2ihNBWzKZ2LthcBBGiYkWZ69KUTVkcLVbnjq7pD8mnApEru";
String address = dogecoin.getUnusedReceiveAddress(xprv58);
assertNotNull(address);
System.out.println(address);
}
} }

View File

@ -1,113 +1,43 @@
package org.qortal.test.crosschain; package org.qortal.test.crosschain;
import static org.junit.Assert.*;
import java.util.Arrays;
import org.bitcoinj.core.Transaction;
import org.junit.After;
import org.junit.Before;
import org.junit.Ignore; import org.junit.Ignore;
import org.junit.Test; import org.junit.Test;
import org.qortal.crosschain.ForeignBlockchainException; import org.qortal.crosschain.Bitcoiny;
import org.qortal.crosschain.Litecoin; import org.qortal.crosschain.Litecoin;
import org.qortal.crosschain.BitcoinyHTLC;
import org.qortal.repository.DataException;
import org.qortal.test.common.Common;
public class LitecoinTests extends Common { public class LitecoinTests extends BitcoinyTests {
private Litecoin litecoin; @Override
protected String getCoinName() {
@Before return "Litecoin";
public void beforeTest() throws DataException {
Common.useDefaultSettings(); // TestNet3
litecoin = Litecoin.getInstance();
} }
@After @Override
public void afterTest() { protected String getCoinSymbol() {
return "LTC";
}
@Override
protected Bitcoiny getCoin() {
return Litecoin.getInstance();
}
@Override
protected void resetCoinForTesting() {
Litecoin.resetForTesting(); Litecoin.resetForTesting();
litecoin = null;
} }
@Test @Override
public void testGetMedianBlockTime() throws ForeignBlockchainException { protected String getDeterministicKey58() {
long before = System.currentTimeMillis(); return "tprv8ZgxMBicQKsPdahhFSrCdvC1bsWyzHHZfTneTVqUXN6s1wEtZLwAkZXzFP6TYLg2aQMecZLXLre5bTVGajEB55L1HYJcawpdFG66STVAWPJ";
System.out.println(String.format("Litecoin median blocktime: %d", litecoin.getMedianBlockTime())); }
long afterFirst = System.currentTimeMillis();
System.out.println(String.format("Litecoin median blocktime: %d", litecoin.getMedianBlockTime())); @Override
long afterSecond = System.currentTimeMillis(); protected String getRecipient() {
return "2N8WCg52ULCtDSMjkgVTm5mtPdCsUptkHWE";
long firstPeriod = afterFirst - before;
long secondPeriod = afterSecond - afterFirst;
System.out.println(String.format("1st call: %d ms, 2nd call: %d ms", firstPeriod, secondPeriod));
assertTrue("2nd call should be quicker than 1st", secondPeriod < firstPeriod);
assertTrue("2nd call should take less than 5 seconds", secondPeriod < 5000L);
} }
@Test @Test
@Ignore(value = "Doesn't work, to be fixed later") @Ignore(value = "Doesn't work, to be fixed later")
public void testFindHtlcSecret() throws ForeignBlockchainException { public void testFindHtlcSecret() {}
// This actually exists on TEST3 but can take a while to fetch
String p2shAddress = "2N8WCg52ULCtDSMjkgVTm5mtPdCsUptkHWE";
byte[] expectedSecret = "This string is exactly 32 bytes!".getBytes();
byte[] secret = BitcoinyHTLC.findHtlcSecret(litecoin, p2shAddress);
assertNotNull("secret not found", secret);
assertTrue("secret incorrect", Arrays.equals(expectedSecret, secret));
}
@Test
public void testBuildSpend() {
String xprv58 = "tprv8ZgxMBicQKsPdahhFSrCdvC1bsWyzHHZfTneTVqUXN6s1wEtZLwAkZXzFP6TYLg2aQMecZLXLre5bTVGajEB55L1HYJcawpdFG66STVAWPJ";
String recipient = "2N8WCg52ULCtDSMjkgVTm5mtPdCsUptkHWE";
long amount = 1000L;
Transaction transaction = litecoin.buildSpend(xprv58, recipient, amount);
assertNotNull("insufficient funds", transaction);
// Check spent key caching doesn't affect outcome
transaction = litecoin.buildSpend(xprv58, recipient, amount);
assertNotNull("insufficient funds", transaction);
}
@Test
public void testGetWalletBalance() throws ForeignBlockchainException {
String xprv58 = "tprv8ZgxMBicQKsPdahhFSrCdvC1bsWyzHHZfTneTVqUXN6s1wEtZLwAkZXzFP6TYLg2aQMecZLXLre5bTVGajEB55L1HYJcawpdFG66STVAWPJ";
Long balance = litecoin.getWalletBalance(xprv58);
assertNotNull(balance);
System.out.println(litecoin.format(balance));
// Check spent key caching doesn't affect outcome
Long repeatBalance = litecoin.getWalletBalance(xprv58);
assertNotNull(repeatBalance);
System.out.println(litecoin.format(repeatBalance));
assertEquals(balance, repeatBalance);
}
@Test
public void testGetUnusedReceiveAddress() throws ForeignBlockchainException {
String xprv58 = "tprv8ZgxMBicQKsPdahhFSrCdvC1bsWyzHHZfTneTVqUXN6s1wEtZLwAkZXzFP6TYLg2aQMecZLXLre5bTVGajEB55L1HYJcawpdFG66STVAWPJ";
String address = litecoin.getUnusedReceiveAddress(xprv58);
assertNotNull(address);
System.out.println(address);
}
} }

View File

@ -3,57 +3,53 @@ package org.qortal.test.crosschain;
import cash.z.wallet.sdk.rpc.CompactFormats.*; import cash.z.wallet.sdk.rpc.CompactFormats.*;
import com.google.common.hash.HashCode; import com.google.common.hash.HashCode;
import com.google.common.primitives.Bytes; import com.google.common.primitives.Bytes;
import org.bitcoinj.core.Transaction;
import org.bitcoinj.store.BlockStoreException;
import org.junit.After;
import org.junit.Before;
import org.junit.Ignore; import org.junit.Ignore;
import org.junit.Test; import org.junit.Test;
import org.qortal.controller.tradebot.TradeBot; import org.qortal.controller.tradebot.TradeBot;
import org.qortal.crosschain.*; import org.qortal.crosschain.*;
import org.qortal.crypto.Crypto; import org.qortal.crypto.Crypto;
import org.qortal.repository.DataException;
import org.qortal.test.common.Common;
import org.qortal.transform.TransformationException; import org.qortal.transform.TransformationException;
import java.util.Arrays;
import java.util.List; import java.util.List;
import static org.junit.Assert.*; import static org.junit.Assert.*;
import static org.qortal.crosschain.BitcoinyHTLC.Status.*; import static org.qortal.crosschain.BitcoinyHTLC.Status.*;
public class PirateChainTests extends Common { public class PirateChainTests extends BitcoinyTests {
private PirateChain pirateChain; @Override
protected String getCoinName() {
@Before return "PirateChain";
public void beforeTest() throws DataException {
Common.useDefaultSettings();
pirateChain = PirateChain.getInstance();
} }
@After @Override
public void afterTest() { protected String getCoinSymbol() {
return "ARRR";
}
@Override
protected Bitcoiny getCoin() {
return PirateChain.getInstance();
}
@Override
protected void resetCoinForTesting() {
Litecoin.resetForTesting(); Litecoin.resetForTesting();
pirateChain = null;
} }
@Test @Override
public void testGetMedianBlockTime() throws BlockStoreException, ForeignBlockchainException { protected String getDeterministicKey58() {
long before = System.currentTimeMillis(); return null;
System.out.println(String.format("Pirate Chain median blocktime: %d", pirateChain.getMedianBlockTime())); }
long afterFirst = System.currentTimeMillis();
System.out.println(String.format("Pirate Chain median blocktime: %d", pirateChain.getMedianBlockTime())); @Override
long afterSecond = System.currentTimeMillis(); protected String getRecipient() {
return null;
}
long firstPeriod = afterFirst - before; public void makeGetMedianBlockTimeAssertions(long firstPeriod, long secondPeriod) {
long secondPeriod = afterSecond - afterFirst; assertTrue("1st call should take less than 5 seconds", firstPeriod < 5000L);
assertTrue("2nd call should take less than 5 seconds", secondPeriod < 5000L);
System.out.println(String.format("1st call: %d ms, 2nd call: %d ms", firstPeriod, secondPeriod));
assertTrue("1st call should take less than 5 seconds", firstPeriod < 5000L);
assertTrue("2nd call should take less than 5 seconds", secondPeriod < 5000L);
} }
@Test @Test
@ -62,7 +58,7 @@ public class PirateChainTests extends Common {
int count = 20; int count = 20;
long before = System.currentTimeMillis(); long before = System.currentTimeMillis();
List<CompactBlock> compactBlocks = pirateChain.getCompactBlocks(startHeight, count); List<CompactBlock> compactBlocks = ((PirateChain) bitcoiny).getCompactBlocks(startHeight, count);
long after = System.currentTimeMillis(); long after = System.currentTimeMillis();
System.out.println(String.format("Retrieval took: %d ms", after-before)); System.out.println(String.format("Retrieval took: %d ms", after-before));
@ -82,7 +78,7 @@ public class PirateChainTests extends Common {
Bytes.reverse(txBytes); Bytes.reverse(txBytes);
String txHashBE = HashCode.fromBytes(txBytes).toString(); String txHashBE = HashCode.fromBytes(txBytes).toString();
byte[] rawTransaction = pirateChain.getBlockchainProvider().getRawTransaction(txHashBE); byte[] rawTransaction = bitcoiny.getBlockchainProvider().getRawTransaction(txHashBE);
assertNotNull(rawTransaction); assertNotNull(rawTransaction);
} }
@ -121,7 +117,7 @@ public class PirateChainTests extends Common {
String p2shAddress = "ba6Q5HWrWtmfU2WZqQbrFdRYsafA45cUAt"; String p2shAddress = "ba6Q5HWrWtmfU2WZqQbrFdRYsafA45cUAt";
long p2shFee = 10000; long p2shFee = 10000;
final long minimumAmount = 10000 + p2shFee; final long minimumAmount = 10000 + p2shFee;
BitcoinyHTLC.Status htlcStatus = PirateChainHTLC.determineHtlcStatus(pirateChain.getBlockchainProvider(), p2shAddress, minimumAmount); BitcoinyHTLC.Status htlcStatus = PirateChainHTLC.determineHtlcStatus(bitcoiny.getBlockchainProvider(), p2shAddress, minimumAmount);
assertEquals(FUNDED, htlcStatus); assertEquals(FUNDED, htlcStatus);
} }
@ -130,7 +126,7 @@ public class PirateChainTests extends Common {
String p2shAddress = "bYZrzSSgGp8aEGvihukoMGU8sXYrx19Wka"; String p2shAddress = "bYZrzSSgGp8aEGvihukoMGU8sXYrx19Wka";
long p2shFee = 10000; long p2shFee = 10000;
final long minimumAmount = 10000 + p2shFee; final long minimumAmount = 10000 + p2shFee;
BitcoinyHTLC.Status htlcStatus = PirateChainHTLC.determineHtlcStatus(pirateChain.getBlockchainProvider(), p2shAddress, minimumAmount); BitcoinyHTLC.Status htlcStatus = PirateChainHTLC.determineHtlcStatus(bitcoiny.getBlockchainProvider(), p2shAddress, minimumAmount);
assertEquals(REDEEMED, htlcStatus); assertEquals(REDEEMED, htlcStatus);
} }
@ -139,14 +135,14 @@ public class PirateChainTests extends Common {
String p2shAddress = "bE49izfVxz8odhu8c2BcUaVFUnt7NLFRgv"; String p2shAddress = "bE49izfVxz8odhu8c2BcUaVFUnt7NLFRgv";
long p2shFee = 10000; long p2shFee = 10000;
final long minimumAmount = 10000 + p2shFee; final long minimumAmount = 10000 + p2shFee;
BitcoinyHTLC.Status htlcStatus = PirateChainHTLC.determineHtlcStatus(pirateChain.getBlockchainProvider(), p2shAddress, minimumAmount); BitcoinyHTLC.Status htlcStatus = PirateChainHTLC.determineHtlcStatus(bitcoiny.getBlockchainProvider(), p2shAddress, minimumAmount);
assertEquals(REFUNDED, htlcStatus); assertEquals(REFUNDED, htlcStatus);
} }
@Test @Test
public void testGetTxidForUnspentAddress() throws ForeignBlockchainException { public void testGetTxidForUnspentAddress() throws ForeignBlockchainException {
String p2shAddress = "ba6Q5HWrWtmfU2WZqQbrFdRYsafA45cUAt"; String p2shAddress = "ba6Q5HWrWtmfU2WZqQbrFdRYsafA45cUAt";
String txid = PirateChainHTLC.getFundingTxid(pirateChain.getBlockchainProvider(), p2shAddress); String txid = PirateChainHTLC.getFundingTxid(bitcoiny.getBlockchainProvider(), p2shAddress);
// Reverse the byte order of the txid used by block explorers, to get to big-endian form // Reverse the byte order of the txid used by block explorers, to get to big-endian form
byte[] expectedTxidLE = HashCode.fromString("fea4b0c1abcf8f0f3ddc2fa2f9438501ee102aad62a9ff18a5ce7d08774755c0").asBytes(); byte[] expectedTxidLE = HashCode.fromString("fea4b0c1abcf8f0f3ddc2fa2f9438501ee102aad62a9ff18a5ce7d08774755c0").asBytes();
@ -161,7 +157,7 @@ public class PirateChainTests extends Common {
String p2shAddress = "ba6Q5HWrWtmfU2WZqQbrFdRYsafA45cUAt"; String p2shAddress = "ba6Q5HWrWtmfU2WZqQbrFdRYsafA45cUAt";
long p2shFee = 10000; long p2shFee = 10000;
final long minimumAmount = 10000 + p2shFee; final long minimumAmount = 10000 + p2shFee;
String txid = PirateChainHTLC.getUnspentFundingTxid(pirateChain.getBlockchainProvider(), p2shAddress, minimumAmount); String txid = PirateChainHTLC.getUnspentFundingTxid(bitcoiny.getBlockchainProvider(), p2shAddress, minimumAmount);
// Reverse the byte order of the txid used by block explorers, to get to big-endian form // Reverse the byte order of the txid used by block explorers, to get to big-endian form
byte[] expectedTxidLE = HashCode.fromString("fea4b0c1abcf8f0f3ddc2fa2f9438501ee102aad62a9ff18a5ce7d08774755c0").asBytes(); byte[] expectedTxidLE = HashCode.fromString("fea4b0c1abcf8f0f3ddc2fa2f9438501ee102aad62a9ff18a5ce7d08774755c0").asBytes();
@ -174,7 +170,7 @@ public class PirateChainTests extends Common {
@Test @Test
public void testGetTxidForSpentAddress() throws ForeignBlockchainException { public void testGetTxidForSpentAddress() throws ForeignBlockchainException {
String p2shAddress = "bE49izfVxz8odhu8c2BcUaVFUnt7NLFRgv"; //"t3KtVxeEb8srJofo6atMEpMpEP6TjEi8VqA"; String p2shAddress = "bE49izfVxz8odhu8c2BcUaVFUnt7NLFRgv"; //"t3KtVxeEb8srJofo6atMEpMpEP6TjEi8VqA";
String txid = PirateChainHTLC.getFundingTxid(pirateChain.getBlockchainProvider(), p2shAddress); String txid = PirateChainHTLC.getFundingTxid(bitcoiny.getBlockchainProvider(), p2shAddress);
// Reverse the byte order of the txid used by block explorers, to get to big-endian form // Reverse the byte order of the txid used by block explorers, to get to big-endian form
byte[] expectedTxidLE = HashCode.fromString("fb386fc8eea0fbf3ea37047726b92c39441652b32d8d62a274331687f7a1eca8").asBytes(); byte[] expectedTxidLE = HashCode.fromString("fb386fc8eea0fbf3ea37047726b92c39441652b32d8d62a274331687f7a1eca8").asBytes();
@ -187,7 +183,7 @@ public class PirateChainTests extends Common {
@Test @Test
public void testGetTransactionsForAddress() throws ForeignBlockchainException { public void testGetTransactionsForAddress() throws ForeignBlockchainException {
String p2shAddress = "bE49izfVxz8odhu8c2BcUaVFUnt7NLFRgv"; //"t3KtVxeEb8srJofo6atMEpMpEP6TjEi8VqA"; String p2shAddress = "bE49izfVxz8odhu8c2BcUaVFUnt7NLFRgv"; //"t3KtVxeEb8srJofo6atMEpMpEP6TjEi8VqA";
List<BitcoinyTransaction> transactions = pirateChain.getBlockchainProvider() List<BitcoinyTransaction> transactions = bitcoiny.getBlockchainProvider()
.getAddressBitcoinyTransactions(p2shAddress, false); .getAddressBitcoinyTransactions(p2shAddress, false);
assertEquals(2, transactions.size()); assertEquals(2, transactions.size());
@ -232,66 +228,17 @@ public class PirateChainTests extends Common {
@Test @Test
@Ignore(value = "Doesn't work, to be fixed later") @Ignore(value = "Doesn't work, to be fixed later")
public void testFindHtlcSecret() throws ForeignBlockchainException { public void testFindHtlcSecret() {}
// This actually exists on TEST3 but can take a while to fetch
String p2shAddress = "2N8WCg52ULCtDSMjkgVTm5mtPdCsUptkHWE";
byte[] expectedSecret = "This string is exactly 32 bytes!".getBytes();
byte[] secret = BitcoinyHTLC.findHtlcSecret(pirateChain, p2shAddress);
assertNotNull("secret not found", secret);
assertTrue("secret incorrect", Arrays.equals(expectedSecret, secret));
}
@Test @Test
@Ignore(value = "Needs adapting for Pirate Chain") @Ignore(value = "Needs adapting for Pirate Chain")
public void testBuildSpend() { public void testBuildSpend() {}
String xprv58 = "tprv8ZgxMBicQKsPdahhFSrCdvC1bsWyzHHZfTneTVqUXN6s1wEtZLwAkZXzFP6TYLg2aQMecZLXLre5bTVGajEB55L1HYJcawpdFG66STVAWPJ";
String recipient = "2N8WCg52ULCtDSMjkgVTm5mtPdCsUptkHWE";
long amount = 1000L;
Transaction transaction = pirateChain.buildSpend(xprv58, recipient, amount);
assertNotNull("insufficient funds", transaction);
// Check spent key caching doesn't affect outcome
transaction = pirateChain.buildSpend(xprv58, recipient, amount);
assertNotNull("insufficient funds", transaction);
}
@Test @Test
@Ignore(value = "Needs adapting for Pirate Chain") @Ignore(value = "Needs adapting for Pirate Chain")
public void testGetWalletBalance() throws ForeignBlockchainException { public void testGetWalletBalance() {}
String xprv58 = "tprv8ZgxMBicQKsPdahhFSrCdvC1bsWyzHHZfTneTVqUXN6s1wEtZLwAkZXzFP6TYLg2aQMecZLXLre5bTVGajEB55L1HYJcawpdFG66STVAWPJ";
Long balance = pirateChain.getWalletBalance(xprv58);
assertNotNull(balance);
System.out.println(pirateChain.format(balance));
// Check spent key caching doesn't affect outcome
Long repeatBalance = pirateChain.getWalletBalance(xprv58);
assertNotNull(repeatBalance);
System.out.println(pirateChain.format(repeatBalance));
assertEquals(balance, repeatBalance);
}
@Test @Test
@Ignore(value = "Needs adapting for Pirate Chain") @Ignore(value = "Needs adapting for Pirate Chain")
public void testGetUnusedReceiveAddress() throws ForeignBlockchainException { public void testGetUnusedReceiveAddress() {}
String xprv58 = "tprv8ZgxMBicQKsPdahhFSrCdvC1bsWyzHHZfTneTVqUXN6s1wEtZLwAkZXzFP6TYLg2aQMecZLXLre5bTVGajEB55L1HYJcawpdFG66STVAWPJ"; }
String address = pirateChain.getUnusedReceiveAddress(xprv58);
assertNotNull(address);
System.out.println(address);
}
}

View File

@ -1,115 +1,47 @@
package org.qortal.test.crosschain; package org.qortal.test.crosschain;
import static org.junit.Assert.*;
import java.util.Arrays;
import org.bitcoinj.core.Transaction;
import org.bitcoinj.store.BlockStoreException;
import org.junit.After;
import org.junit.Before;
import org.junit.Ignore; import org.junit.Ignore;
import org.junit.Test; import org.junit.Test;
import org.qortal.crosschain.ForeignBlockchainException; import org.qortal.crosschain.Bitcoiny;
import org.qortal.crosschain.Ravencoin; import org.qortal.crosschain.Ravencoin;
import org.qortal.crosschain.BitcoinyHTLC;
import org.qortal.repository.DataException;
import org.qortal.test.common.Common;
public class RavencoinTests extends Common { public class RavencoinTests extends BitcoinyTests {
private Ravencoin ravencoin; @Override
protected String getCoinName() {
@Before return "Ravencoin";
public void beforeTest() throws DataException {
Common.useDefaultSettings(); // TestNet3
ravencoin = Ravencoin.getInstance();
} }
@After @Override
public void afterTest() { protected String getCoinSymbol() {
return "RVN";
}
@Override
protected Bitcoiny getCoin() {
return Ravencoin.getInstance();
}
@Override
protected void resetCoinForTesting() {
Ravencoin.resetForTesting(); Ravencoin.resetForTesting();
ravencoin = null;
} }
@Test @Override
public void testGetMedianBlockTime() throws BlockStoreException, ForeignBlockchainException { protected String getDeterministicKey58() {
long before = System.currentTimeMillis(); return "xpub661MyMwAqRbcEt3Ge1wNmkagyb1J7yTQu4Kquvy77Ycg2iPoh7Urg8s9Jdwp7YmrqGkDKJpUVjsZXSSsQgmAVUC17ZVQQeoWMzm7vDTt1y7";
System.out.println(String.format("Ravencoin median blocktime: %d", ravencoin.getMedianBlockTime())); }
long afterFirst = System.currentTimeMillis();
System.out.println(String.format("Ravencoin median blocktime: %d", ravencoin.getMedianBlockTime())); @Override
long afterSecond = System.currentTimeMillis(); protected String getRecipient() {
return null;
long firstPeriod = afterFirst - before;
long secondPeriod = afterSecond - afterFirst;
System.out.println(String.format("1st call: %d ms, 2nd call: %d ms", firstPeriod, secondPeriod));
assertTrue("2nd call should be quicker than 1st", secondPeriod < firstPeriod);
assertTrue("2nd call should take less than 5 seconds", secondPeriod < 5000L);
} }
@Test @Test
@Ignore(value = "Doesn't work, to be fixed later") @Ignore(value = "Doesn't work, to be fixed later")
public void testFindHtlcSecret() throws ForeignBlockchainException { public void testFindHtlcSecret() {}
// This actually exists on TEST3 but can take a while to fetch
String p2shAddress = "2N8WCg52ULCtDSMjkgVTm5mtPdCsUptkHWE";
byte[] expectedSecret = "This string is exactly 32 bytes!".getBytes();
byte[] secret = BitcoinyHTLC.findHtlcSecret(ravencoin, p2shAddress);
assertNotNull("secret not found", secret);
assertTrue("secret incorrect", Arrays.equals(expectedSecret, secret));
}
@Test @Test
@Ignore(value = "No testnet nodes available, so we can't regularly test buildSpend yet") @Ignore(value = "No testnet nodes available, so we can't regularly test buildSpend yet")
public void testBuildSpend() { public void testBuildSpend() {}
String xprv58 = "tprv8ZgxMBicQKsPdahhFSrCdvC1bsWyzHHZfTneTVqUXN6s1wEtZLwAkZXzFP6TYLg2aQMecZLXLre5bTVGajEB55L1HYJcawpdFG66STVAWPJ";
String recipient = "2N8WCg52ULCtDSMjkgVTm5mtPdCsUptkHWE";
long amount = 1000L;
Transaction transaction = ravencoin.buildSpend(xprv58, recipient, amount);
assertNotNull("insufficient funds", transaction);
// Check spent key caching doesn't affect outcome
transaction = ravencoin.buildSpend(xprv58, recipient, amount);
assertNotNull("insufficient funds", transaction);
}
@Test
public void testGetWalletBalance() throws ForeignBlockchainException {
String xprv58 = "xpub661MyMwAqRbcEt3Ge1wNmkagyb1J7yTQu4Kquvy77Ycg2iPoh7Urg8s9Jdwp7YmrqGkDKJpUVjsZXSSsQgmAVUC17ZVQQeoWMzm7vDTt1y7";
Long balance = ravencoin.getWalletBalance(xprv58);
assertNotNull(balance);
System.out.println(ravencoin.format(balance));
// Check spent key caching doesn't affect outcome
Long repeatBalance = ravencoin.getWalletBalance(xprv58);
assertNotNull(repeatBalance);
System.out.println(ravencoin.format(repeatBalance));
assertEquals(balance, repeatBalance);
}
@Test
public void testGetUnusedReceiveAddress() throws ForeignBlockchainException {
String xprv58 = "xpub661MyMwAqRbcEt3Ge1wNmkagyb1J7yTQu4Kquvy77Ycg2iPoh7Urg8s9Jdwp7YmrqGkDKJpUVjsZXSSsQgmAVUC17ZVQQeoWMzm7vDTt1y7";
String address = ravencoin.getUnusedReceiveAddress(xprv58);
assertNotNull(address);
System.out.println(address);
}
} }

View File

@ -2,507 +2,89 @@ package org.qortal.test.crosschain.bitcoinv1;
import static org.junit.Assert.*; import static org.junit.Assert.*;
import java.time.Instant;
import java.time.LocalDateTime;
import java.time.ZoneOffset;
import java.time.format.DateTimeFormatter;
import java.time.format.FormatStyle;
import java.util.Arrays;
import java.util.List;
import java.util.Random;
import java.util.function.Function; import java.util.function.Function;
import org.junit.Before;
import org.junit.Test; import org.junit.Test;
import org.qortal.account.Account; import org.qortal.account.Account;
import org.qortal.account.PrivateKeyAccount; import org.qortal.account.PrivateKeyAccount;
import org.qortal.asset.Asset; import org.qortal.asset.Asset;
import org.qortal.block.Block; import org.qortal.crosschain.ACCT;
import org.qortal.crosschain.BitcoinACCTv1; import org.qortal.crosschain.BitcoinACCTv1;
import org.qortal.crosschain.AcctMode; import org.qortal.crosschain.AcctMode;
import org.qortal.crypto.Crypto; import org.qortal.crypto.Crypto;
import org.qortal.data.at.ATData; import org.qortal.data.at.ATData;
import org.qortal.data.at.ATStateData; import org.qortal.data.at.ATStateData;
import org.qortal.data.crosschain.CrossChainTradeData; import org.qortal.data.crosschain.CrossChainTradeData;
import org.qortal.data.transaction.BaseTransactionData;
import org.qortal.data.transaction.DeployAtTransactionData;
import org.qortal.data.transaction.MessageTransactionData;
import org.qortal.data.transaction.TransactionData;
import org.qortal.group.Group;
import org.qortal.repository.DataException; import org.qortal.repository.DataException;
import org.qortal.repository.Repository; import org.qortal.repository.Repository;
import org.qortal.repository.RepositoryManager; import org.qortal.repository.RepositoryManager;
import org.qortal.test.common.BlockUtils; import org.qortal.test.common.BlockUtils;
import org.qortal.test.common.Common; import org.qortal.test.common.Common;
import org.qortal.test.common.TransactionUtils; import org.qortal.test.crosschain.ACCTTests;
import org.qortal.transaction.DeployAtTransaction; import org.qortal.transaction.DeployAtTransaction;
import org.qortal.transaction.MessageTransaction; import org.qortal.transaction.MessageTransaction;
import org.qortal.utils.Amounts;
import com.google.common.hash.HashCode; import com.google.common.hash.HashCode;
import com.google.common.primitives.Bytes;
public class BitcoinACCTv1Tests extends Common { public class BitcoinACCTv1Tests extends ACCTTests {
public static final byte[] secretA = "This string is exactly 32 bytes!".getBytes();
public static final byte[] hashOfSecretA = Crypto.hash160(secretA); // daf59884b4d1aec8c1b17102530909ee43c0151a
public static final byte[] secretB = "This string is roughly 32 bytes?".getBytes(); public static final byte[] secretB = "This string is roughly 32 bytes?".getBytes();
public static final byte[] hashOfSecretB = Crypto.hash160(secretB); // 31f0dd71decf59bbc8ef0661f4030479255cfa58 public static final byte[] hashOfSecretB = Crypto.hash160(secretB); // 31f0dd71decf59bbc8ef0661f4030479255cfa58
public static final byte[] bitcoinPublicKeyHash = HashCode.fromString("bb00bb11bb22bb33bb44bb55bb66bb77bb88bb99").asBytes(); public static final byte[] bitcoinPublicKeyHash = HashCode.fromString("bb00bb11bb22bb33bb44bb55bb66bb77bb88bb99").asBytes();
public static final int tradeTimeout = 20; // blocks
public static final long redeemAmount = 80_40200000L;
public static final long fundingAmount = 123_45600000L;
public static final long bitcoinAmount = 864200L; // 0.00864200 BTC
private static final Random RANDOM = new Random(); private static final String SYMBOL = "BTC";
@Before private static final String NAME = "Bitcoin";
public void beforeTest() throws DataException {
Common.useDefaultSettings(); @Override
protected byte[] getPublicKey() {
return bitcoinPublicKeyHash;
} }
@Test @Override
public void testCompile() { protected byte[] buildQortalAT(String address, byte[] publicKey, long redeemAmount, long foreignAmount, int tradeTimeout) {
PrivateKeyAccount tradeAccount = createTradeAccount(null); return BitcoinACCTv1.buildQortalAT(address,publicKey, hashOfSecretB, redeemAmount, foreignAmount,tradeTimeout);
byte[] creationBytes = BitcoinACCTv1.buildQortalAT(tradeAccount.getAddress(), bitcoinPublicKeyHash, hashOfSecretB, redeemAmount, bitcoinAmount, tradeTimeout);
assertNotNull(creationBytes);
System.out.println("AT creation bytes: " + HashCode.fromBytes(creationBytes).toString());
} }
@Test @Override
public void testDeploy() throws DataException { protected ACCT getInstance() {
try (final Repository repository = RepositoryManager.getRepository()) { return BitcoinACCTv1.getInstance();
PrivateKeyAccount deployer = Common.getTestAccount(repository, "chloe"); }
PrivateKeyAccount tradeAccount = createTradeAccount(repository);
PrivateKeyAccount partner = Common.getTestAccount(repository, "dilbert"); @Override
protected int calcRefundTimeout(long partnersOfferMessageTransactionTimestamp, int lockTimeA) {
return BitcoinACCTv1.calcLockTimeB(partnersOfferMessageTransactionTimestamp, lockTimeA);
}
long deployersInitialBalance = deployer.getConfirmedBalance(Asset.QORT); @Override
long partnersInitialBalance = partner.getConfirmedBalance(Asset.QORT); protected byte[] buildTradeMessage(String address, byte[] publicKey, byte[] hashOfSecretA, int lockTimeA, int refundTimeout) {
return BitcoinACCTv1.buildTradeMessage(address, publicKey, hashOfSecretA, lockTimeA, refundTimeout);
}
DeployAtTransaction deployAtTransaction = doDeploy(repository, deployer, tradeAccount.getAddress()); @Override
protected byte[] buildRedeemMessage(byte[] secretA, String address) {
return BitcoinACCTv1.buildRedeemMessage(secretA,secretB,address);
}
long expectedBalance = deployersInitialBalance - fundingAmount - deployAtTransaction.getTransactionData().getFee(); @Override
long actualBalance = deployer.getConfirmedBalance(Asset.QORT); protected byte[] getCodeBytesHash() {
return BitcoinACCTv1.CODE_BYTES_HASH;
}
assertEquals("Deployer's post-deployment balance incorrect", expectedBalance, actualBalance); @Override
protected String getSymbol() {
return SYMBOL;
}
expectedBalance = fundingAmount; @Override
actualBalance = deployAtTransaction.getATAccount().getConfirmedBalance(Asset.QORT); protected String getName() {
return NAME;
assertEquals("AT's post-deployment balance incorrect", expectedBalance, actualBalance);
expectedBalance = partnersInitialBalance;
actualBalance = partner.getConfirmedBalance(Asset.QORT);
assertEquals("Partner's post-deployment balance incorrect", expectedBalance, actualBalance);
// Test orphaning
BlockUtils.orphanLastBlock(repository);
expectedBalance = deployersInitialBalance;
actualBalance = deployer.getConfirmedBalance(Asset.QORT);
assertEquals("Deployer's post-orphan/pre-deployment balance incorrect", expectedBalance, actualBalance);
expectedBalance = 0;
actualBalance = deployAtTransaction.getATAccount().getConfirmedBalance(Asset.QORT);
assertEquals("AT's post-orphan/pre-deployment balance incorrect", expectedBalance, actualBalance);
expectedBalance = partnersInitialBalance;
actualBalance = partner.getConfirmedBalance(Asset.QORT);
assertEquals("Partner's post-orphan/pre-deployment balance incorrect", expectedBalance, actualBalance);
}
} }
@SuppressWarnings("unused") @SuppressWarnings("unused")
@Override
@Test @Test
public void testOfferCancel() throws DataException { public void testIncorrectSecretCorrectSender() throws DataException {
try (final Repository repository = RepositoryManager.getRepository()) {
PrivateKeyAccount deployer = Common.getTestAccount(repository, "chloe");
PrivateKeyAccount tradeAccount = createTradeAccount(repository);
PrivateKeyAccount partner = Common.getTestAccount(repository, "dilbert");
long deployersInitialBalance = deployer.getConfirmedBalance(Asset.QORT);
long partnersInitialBalance = partner.getConfirmedBalance(Asset.QORT);
DeployAtTransaction deployAtTransaction = doDeploy(repository, deployer, tradeAccount.getAddress());
Account at = deployAtTransaction.getATAccount();
String atAddress = at.getAddress();
long deployAtFee = deployAtTransaction.getTransactionData().getFee();
long deployersPostDeploymentBalance = deployersInitialBalance - fundingAmount - deployAtFee;
// Send creator's address to AT, instead of typical partner's address
byte[] messageData = BitcoinACCTv1.getInstance().buildCancelMessage(deployer.getAddress());
MessageTransaction messageTransaction = sendMessage(repository, deployer, messageData, atAddress);
long messageFee = messageTransaction.getTransactionData().getFee();
// AT should process 'cancel' message in next block
BlockUtils.mintBlock(repository);
describeAt(repository, atAddress);
// Check AT is finished
ATData atData = repository.getATRepository().fromATAddress(atAddress);
assertTrue(atData.getIsFinished());
// AT should be in CANCELLED mode
CrossChainTradeData tradeData = BitcoinACCTv1.getInstance().populateTradeData(repository, atData);
assertEquals(AcctMode.CANCELLED, tradeData.mode);
// Check balances
long expectedMinimumBalance = deployersPostDeploymentBalance;
long expectedMaximumBalance = deployersInitialBalance - deployAtFee - messageFee;
long actualBalance = deployer.getConfirmedBalance(Asset.QORT);
assertTrue(String.format("Deployer's balance %s should be above minimum %s", actualBalance, expectedMinimumBalance), actualBalance > expectedMinimumBalance);
assertTrue(String.format("Deployer's balance %s should be below maximum %s", actualBalance, expectedMaximumBalance), actualBalance < expectedMaximumBalance);
// Test orphaning
BlockUtils.orphanLastBlock(repository);
// Check balances
long expectedBalance = deployersPostDeploymentBalance - messageFee;
actualBalance = deployer.getConfirmedBalance(Asset.QORT);
assertEquals("Deployer's post-orphan/pre-refund balance incorrect", expectedBalance, actualBalance);
}
}
@SuppressWarnings("unused")
@Test
public void testOfferCancelInvalidLength() throws DataException {
try (final Repository repository = RepositoryManager.getRepository()) {
PrivateKeyAccount deployer = Common.getTestAccount(repository, "chloe");
PrivateKeyAccount tradeAccount = createTradeAccount(repository);
PrivateKeyAccount partner = Common.getTestAccount(repository, "dilbert");
long deployersInitialBalance = deployer.getConfirmedBalance(Asset.QORT);
long partnersInitialBalance = partner.getConfirmedBalance(Asset.QORT);
DeployAtTransaction deployAtTransaction = doDeploy(repository, deployer, tradeAccount.getAddress());
Account at = deployAtTransaction.getATAccount();
String atAddress = at.getAddress();
long deployAtFee = deployAtTransaction.getTransactionData().getFee();
long deployersPostDeploymentBalance = deployersInitialBalance - fundingAmount - deployAtFee;
// Instead of sending creator's address to AT, send too-short/invalid message
byte[] messageData = new byte[7];
RANDOM.nextBytes(messageData);
MessageTransaction messageTransaction = sendMessage(repository, deployer, messageData, atAddress);
long messageFee = messageTransaction.getTransactionData().getFee();
// AT should process 'cancel' message in next block
// As message is too short, it will be padded to 32bytes but cancel code doesn't care about message content, so should be ok
BlockUtils.mintBlock(repository);
describeAt(repository, atAddress);
// Check AT is finished
ATData atData = repository.getATRepository().fromATAddress(atAddress);
assertTrue(atData.getIsFinished());
// AT should be in CANCELLED mode
CrossChainTradeData tradeData = BitcoinACCTv1.getInstance().populateTradeData(repository, atData);
assertEquals(AcctMode.CANCELLED, tradeData.mode);
}
}
@SuppressWarnings("unused")
@Test
public void testTradingInfoProcessing() throws DataException {
try (final Repository repository = RepositoryManager.getRepository()) {
PrivateKeyAccount deployer = Common.getTestAccount(repository, "chloe");
PrivateKeyAccount tradeAccount = createTradeAccount(repository);
PrivateKeyAccount partner = Common.getTestAccount(repository, "dilbert");
long deployersInitialBalance = deployer.getConfirmedBalance(Asset.QORT);
long partnersInitialBalance = partner.getConfirmedBalance(Asset.QORT);
DeployAtTransaction deployAtTransaction = doDeploy(repository, deployer, tradeAccount.getAddress());
Account at = deployAtTransaction.getATAccount();
String atAddress = at.getAddress();
long partnersOfferMessageTransactionTimestamp = System.currentTimeMillis();
int lockTimeA = calcTestLockTimeA(partnersOfferMessageTransactionTimestamp);
int lockTimeB = BitcoinACCTv1.calcLockTimeB(partnersOfferMessageTransactionTimestamp, lockTimeA);
// Send trade info to AT
byte[] messageData = BitcoinACCTv1.buildTradeMessage(partner.getAddress(), bitcoinPublicKeyHash, hashOfSecretA, lockTimeA, lockTimeB);
MessageTransaction messageTransaction = sendMessage(repository, tradeAccount, messageData, atAddress);
Block postDeploymentBlock = BlockUtils.mintBlock(repository);
int postDeploymentBlockHeight = postDeploymentBlock.getBlockData().getHeight();
long deployAtFee = deployAtTransaction.getTransactionData().getFee();
long deployersPostDeploymentBalance = deployersInitialBalance - fundingAmount - deployAtFee;
describeAt(repository, atAddress);
ATData atData = repository.getATRepository().fromATAddress(atAddress);
CrossChainTradeData tradeData = BitcoinACCTv1.getInstance().populateTradeData(repository, atData);
// AT should be in TRADE mode
assertEquals(AcctMode.TRADING, tradeData.mode);
// Check hashOfSecretA was extracted correctly
assertTrue(Arrays.equals(hashOfSecretA, tradeData.hashOfSecretA));
// Check trade partner Qortal address was extracted correctly
assertEquals(partner.getAddress(), tradeData.qortalPartnerAddress);
// Check trade partner's Bitcoin PKH was extracted correctly
assertTrue(Arrays.equals(bitcoinPublicKeyHash, tradeData.partnerForeignPKH));
// Test orphaning
BlockUtils.orphanToBlock(repository, postDeploymentBlockHeight);
// Check balances
long expectedBalance = deployersPostDeploymentBalance;
long actualBalance = deployer.getConfirmedBalance(Asset.QORT);
assertEquals("Deployer's post-orphan/pre-refund balance incorrect", expectedBalance, actualBalance);
}
}
// TEST SENDING TRADING INFO BUT NOT FROM AT CREATOR (SHOULD BE IGNORED)
@SuppressWarnings("unused")
@Test
public void testIncorrectTradeSender() throws DataException {
try (final Repository repository = RepositoryManager.getRepository()) {
PrivateKeyAccount deployer = Common.getTestAccount(repository, "chloe");
PrivateKeyAccount tradeAccount = createTradeAccount(repository);
PrivateKeyAccount partner = Common.getTestAccount(repository, "dilbert");
PrivateKeyAccount bystander = Common.getTestAccount(repository, "bob");
long deployersInitialBalance = deployer.getConfirmedBalance(Asset.QORT);
long partnersInitialBalance = partner.getConfirmedBalance(Asset.QORT);
DeployAtTransaction deployAtTransaction = doDeploy(repository, deployer, tradeAccount.getAddress());
Account at = deployAtTransaction.getATAccount();
String atAddress = at.getAddress();
long partnersOfferMessageTransactionTimestamp = System.currentTimeMillis();
int lockTimeA = calcTestLockTimeA(partnersOfferMessageTransactionTimestamp);
int lockTimeB = BitcoinACCTv1.calcLockTimeB(partnersOfferMessageTransactionTimestamp, lockTimeA);
// Send trade info to AT BUT NOT FROM AT CREATOR
byte[] messageData = BitcoinACCTv1.buildTradeMessage(partner.getAddress(), bitcoinPublicKeyHash, hashOfSecretA, lockTimeA, lockTimeB);
MessageTransaction messageTransaction = sendMessage(repository, bystander, messageData, atAddress);
BlockUtils.mintBlock(repository);
long expectedBalance = partnersInitialBalance;
long actualBalance = partner.getConfirmedBalance(Asset.QORT);
assertEquals("Partner's post-initial-payout balance incorrect", expectedBalance, actualBalance);
describeAt(repository, atAddress);
ATData atData = repository.getATRepository().fromATAddress(atAddress);
CrossChainTradeData tradeData = BitcoinACCTv1.getInstance().populateTradeData(repository, atData);
// AT should still be in OFFER mode
assertEquals(AcctMode.OFFERING, tradeData.mode);
}
}
@SuppressWarnings("unused")
@Test
public void testAutomaticTradeRefund() throws DataException {
try (final Repository repository = RepositoryManager.getRepository()) {
PrivateKeyAccount deployer = Common.getTestAccount(repository, "chloe");
PrivateKeyAccount tradeAccount = createTradeAccount(repository);
PrivateKeyAccount partner = Common.getTestAccount(repository, "dilbert");
long deployersInitialBalance = deployer.getConfirmedBalance(Asset.QORT);
long partnersInitialBalance = partner.getConfirmedBalance(Asset.QORT);
DeployAtTransaction deployAtTransaction = doDeploy(repository, deployer, tradeAccount.getAddress());
Account at = deployAtTransaction.getATAccount();
String atAddress = at.getAddress();
long partnersOfferMessageTransactionTimestamp = System.currentTimeMillis();
int lockTimeA = calcTestLockTimeA(partnersOfferMessageTransactionTimestamp);
int lockTimeB = BitcoinACCTv1.calcLockTimeB(partnersOfferMessageTransactionTimestamp, lockTimeA);
// Send trade info to AT
byte[] messageData = BitcoinACCTv1.buildTradeMessage(partner.getAddress(), bitcoinPublicKeyHash, hashOfSecretA, lockTimeA, lockTimeB);
MessageTransaction messageTransaction = sendMessage(repository, tradeAccount, messageData, atAddress);
Block postDeploymentBlock = BlockUtils.mintBlock(repository);
int postDeploymentBlockHeight = postDeploymentBlock.getBlockData().getHeight();
// Check refund
long deployAtFee = deployAtTransaction.getTransactionData().getFee();
long deployersPostDeploymentBalance = deployersInitialBalance - fundingAmount - deployAtFee;
checkTradeRefund(repository, deployer, deployersInitialBalance, deployAtFee);
describeAt(repository, atAddress);
// Check AT is finished
ATData atData = repository.getATRepository().fromATAddress(atAddress);
assertTrue(atData.getIsFinished());
// AT should be in REFUNDED mode
CrossChainTradeData tradeData = BitcoinACCTv1.getInstance().populateTradeData(repository, atData);
assertEquals(AcctMode.REFUNDED, tradeData.mode);
// Test orphaning
BlockUtils.orphanToBlock(repository, postDeploymentBlockHeight);
// Check balances
long expectedBalance = deployersPostDeploymentBalance;
long actualBalance = deployer.getConfirmedBalance(Asset.QORT);
assertEquals("Deployer's post-orphan/pre-refund balance incorrect", expectedBalance, actualBalance);
}
}
@SuppressWarnings("unused")
@Test
public void testCorrectSecretsCorrectSender() throws DataException {
try (final Repository repository = RepositoryManager.getRepository()) {
PrivateKeyAccount deployer = Common.getTestAccount(repository, "chloe");
PrivateKeyAccount tradeAccount = createTradeAccount(repository);
PrivateKeyAccount partner = Common.getTestAccount(repository, "dilbert");
long deployersInitialBalance = deployer.getConfirmedBalance(Asset.QORT);
long partnersInitialBalance = partner.getConfirmedBalance(Asset.QORT);
DeployAtTransaction deployAtTransaction = doDeploy(repository, deployer, tradeAccount.getAddress());
Account at = deployAtTransaction.getATAccount();
String atAddress = at.getAddress();
long partnersOfferMessageTransactionTimestamp = System.currentTimeMillis();
int lockTimeA = calcTestLockTimeA(partnersOfferMessageTransactionTimestamp);
int lockTimeB = BitcoinACCTv1.calcLockTimeB(partnersOfferMessageTransactionTimestamp, lockTimeA);
// Send trade info to AT
byte[] messageData = BitcoinACCTv1.buildTradeMessage(partner.getAddress(), bitcoinPublicKeyHash, hashOfSecretA, lockTimeA, lockTimeB);
MessageTransaction messageTransaction = sendMessage(repository, tradeAccount, messageData, atAddress);
// Give AT time to process message
BlockUtils.mintBlock(repository);
// Send correct secrets to AT, from correct account
messageData = BitcoinACCTv1.buildRedeemMessage(secretA, secretB, partner.getAddress());
messageTransaction = sendMessage(repository, partner, messageData, atAddress);
// AT should send funds in the next block
ATStateData preRedeemAtStateData = repository.getATRepository().getLatestATState(atAddress);
BlockUtils.mintBlock(repository);
describeAt(repository, atAddress);
// Check AT is finished
ATData atData = repository.getATRepository().fromATAddress(atAddress);
assertTrue(atData.getIsFinished());
// AT should be in REDEEMED mode
CrossChainTradeData tradeData = BitcoinACCTv1.getInstance().populateTradeData(repository, atData);
assertEquals(AcctMode.REDEEMED, tradeData.mode);
// Check balances
long expectedBalance = partnersInitialBalance - messageTransaction.getTransactionData().getFee() + redeemAmount;
long actualBalance = partner.getConfirmedBalance(Asset.QORT);
assertEquals("Partner's post-redeem balance incorrect", expectedBalance, actualBalance);
// Orphan redeem
BlockUtils.orphanLastBlock(repository);
// Check balances
expectedBalance = partnersInitialBalance - messageTransaction.getTransactionData().getFee();
actualBalance = partner.getConfirmedBalance(Asset.QORT);
assertEquals("Partner's post-orphan/pre-redeem balance incorrect", expectedBalance, actualBalance);
// Check AT state
ATStateData postOrphanAtStateData = repository.getATRepository().getLatestATState(atAddress);
assertTrue("AT states mismatch", Arrays.equals(preRedeemAtStateData.getStateData(), postOrphanAtStateData.getStateData()));
}
}
@SuppressWarnings("unused")
@Test
public void testCorrectSecretsIncorrectSender() throws DataException {
try (final Repository repository = RepositoryManager.getRepository()) {
PrivateKeyAccount deployer = Common.getTestAccount(repository, "chloe");
PrivateKeyAccount tradeAccount = createTradeAccount(repository);
PrivateKeyAccount partner = Common.getTestAccount(repository, "dilbert");
PrivateKeyAccount bystander = Common.getTestAccount(repository, "bob");
long deployersInitialBalance = deployer.getConfirmedBalance(Asset.QORT);
long partnersInitialBalance = partner.getConfirmedBalance(Asset.QORT);
DeployAtTransaction deployAtTransaction = doDeploy(repository, deployer, tradeAccount.getAddress());
long deployAtFee = deployAtTransaction.getTransactionData().getFee();
Account at = deployAtTransaction.getATAccount();
String atAddress = at.getAddress();
long partnersOfferMessageTransactionTimestamp = System.currentTimeMillis();
int lockTimeA = calcTestLockTimeA(partnersOfferMessageTransactionTimestamp);
int lockTimeB = BitcoinACCTv1.calcLockTimeB(partnersOfferMessageTransactionTimestamp, lockTimeA);
// Send trade info to AT
byte[] messageData = BitcoinACCTv1.buildTradeMessage(partner.getAddress(), bitcoinPublicKeyHash, hashOfSecretA, lockTimeA, lockTimeB);
MessageTransaction messageTransaction = sendMessage(repository, tradeAccount, messageData, atAddress);
// Give AT time to process message
BlockUtils.mintBlock(repository);
// Send correct secrets to AT, but from wrong account
messageData = BitcoinACCTv1.buildRedeemMessage(secretA, secretB, partner.getAddress());
messageTransaction = sendMessage(repository, bystander, messageData, atAddress);
// AT should NOT send funds in the next block
ATStateData preRedeemAtStateData = repository.getATRepository().getLatestATState(atAddress);
BlockUtils.mintBlock(repository);
describeAt(repository, atAddress);
// Check AT is NOT finished
ATData atData = repository.getATRepository().fromATAddress(atAddress);
assertFalse(atData.getIsFinished());
// AT should still be in TRADE mode
CrossChainTradeData tradeData = BitcoinACCTv1.getInstance().populateTradeData(repository, atData);
assertEquals(AcctMode.TRADING, tradeData.mode);
// Check balances
long expectedBalance = partnersInitialBalance;
long actualBalance = partner.getConfirmedBalance(Asset.QORT);
assertEquals("Partner's balance incorrect", expectedBalance, actualBalance);
// Check eventual refund
checkTradeRefund(repository, deployer, deployersInitialBalance, deployAtFee);
}
}
@SuppressWarnings("unused")
@Test
public void testIncorrectSecretsCorrectSender() throws DataException {
try (final Repository repository = RepositoryManager.getRepository()) { try (final Repository repository = RepositoryManager.getRepository()) {
PrivateKeyAccount deployer = Common.getTestAccount(repository, "chloe"); PrivateKeyAccount deployer = Common.getTestAccount(repository, "chloe");
PrivateKeyAccount tradeAccount = createTradeAccount(repository); PrivateKeyAccount tradeAccount = createTradeAccount(repository);
@ -582,197 +164,8 @@ public class BitcoinACCTv1Tests extends Common {
} }
} }
@SuppressWarnings("unused") @Override
@Test protected void describeRefundAt(CrossChainTradeData tradeData, Function<Long, String> epochMilliFormatter) {
public void testCorrectSecretsCorrectSenderInvalidMessageLength() throws DataException {
try (final Repository repository = RepositoryManager.getRepository()) {
PrivateKeyAccount deployer = Common.getTestAccount(repository, "chloe");
PrivateKeyAccount tradeAccount = createTradeAccount(repository);
PrivateKeyAccount partner = Common.getTestAccount(repository, "dilbert");
long deployersInitialBalance = deployer.getConfirmedBalance(Asset.QORT);
long partnersInitialBalance = partner.getConfirmedBalance(Asset.QORT);
DeployAtTransaction deployAtTransaction = doDeploy(repository, deployer, tradeAccount.getAddress());
Account at = deployAtTransaction.getATAccount();
String atAddress = at.getAddress();
long partnersOfferMessageTransactionTimestamp = System.currentTimeMillis();
int lockTimeA = calcTestLockTimeA(partnersOfferMessageTransactionTimestamp);
int lockTimeB = BitcoinACCTv1.calcLockTimeB(partnersOfferMessageTransactionTimestamp, lockTimeA);
// Send trade info to AT
byte[] messageData = BitcoinACCTv1.buildTradeMessage(partner.getAddress(), bitcoinPublicKeyHash, hashOfSecretA, lockTimeA, lockTimeB);
MessageTransaction messageTransaction = sendMessage(repository, tradeAccount, messageData, atAddress);
// Give AT time to process message
BlockUtils.mintBlock(repository);
// Send correct secrets to AT, from correct account, but missing receive address, hence incorrect length
messageData = Bytes.concat(secretA, secretB);
messageTransaction = sendMessage(repository, partner, messageData, atAddress);
// AT should NOT send funds in the next block
ATStateData preRedeemAtStateData = repository.getATRepository().getLatestATState(atAddress);
BlockUtils.mintBlock(repository);
describeAt(repository, atAddress);
// Check AT is NOT finished
ATData atData = repository.getATRepository().fromATAddress(atAddress);
assertFalse(atData.getIsFinished());
// AT should be in TRADING mode
CrossChainTradeData tradeData = BitcoinACCTv1.getInstance().populateTradeData(repository, atData);
assertEquals(AcctMode.TRADING, tradeData.mode);
}
}
@SuppressWarnings("unused")
@Test
public void testDescribeDeployed() throws DataException {
try (final Repository repository = RepositoryManager.getRepository()) {
PrivateKeyAccount deployer = Common.getTestAccount(repository, "chloe");
PrivateKeyAccount tradeAccount = createTradeAccount(repository);
PrivateKeyAccount partner = Common.getTestAccount(repository, "dilbert");
long deployersInitialBalance = deployer.getConfirmedBalance(Asset.QORT);
long partnersInitialBalance = partner.getConfirmedBalance(Asset.QORT);
DeployAtTransaction deployAtTransaction = doDeploy(repository, deployer, tradeAccount.getAddress());
List<ATData> executableAts = repository.getATRepository().getAllExecutableATs();
for (ATData atData : executableAts) {
String atAddress = atData.getATAddress();
byte[] codeBytes = atData.getCodeBytes();
byte[] codeHash = Crypto.digest(codeBytes);
System.out.println(String.format("%s: code length: %d byte%s, code hash: %s",
atAddress,
codeBytes.length,
(codeBytes.length != 1 ? "s": ""),
HashCode.fromBytes(codeHash)));
// Not one of ours?
if (!Arrays.equals(codeHash, BitcoinACCTv1.CODE_BYTES_HASH))
continue;
describeAt(repository, atAddress);
}
}
}
private int calcTestLockTimeA(long messageTimestamp) {
return (int) (messageTimestamp / 1000L + tradeTimeout * 60);
}
private DeployAtTransaction doDeploy(Repository repository, PrivateKeyAccount deployer, String tradeAddress) throws DataException {
byte[] creationBytes = BitcoinACCTv1.buildQortalAT(tradeAddress, bitcoinPublicKeyHash, hashOfSecretB, redeemAmount, bitcoinAmount, tradeTimeout);
long txTimestamp = System.currentTimeMillis();
byte[] lastReference = deployer.getLastReference();
if (lastReference == null) {
System.err.println(String.format("Qortal account %s has no last reference", deployer.getAddress()));
System.exit(2);
}
Long fee = null;
String name = "QORT-BTC cross-chain trade";
String description = String.format("Qortal-Bitcoin cross-chain trade");
String atType = "ACCT";
String tags = "QORT-BTC ACCT";
BaseTransactionData baseTransactionData = new BaseTransactionData(txTimestamp, Group.NO_GROUP, lastReference, deployer.getPublicKey(), fee, null);
TransactionData deployAtTransactionData = new DeployAtTransactionData(baseTransactionData, name, description, atType, tags, creationBytes, fundingAmount, Asset.QORT);
DeployAtTransaction deployAtTransaction = new DeployAtTransaction(repository, deployAtTransactionData);
fee = deployAtTransaction.calcRecommendedFee();
deployAtTransactionData.setFee(fee);
TransactionUtils.signAndMint(repository, deployAtTransactionData, deployer);
return deployAtTransaction;
}
private MessageTransaction sendMessage(Repository repository, PrivateKeyAccount sender, byte[] data, String recipient) throws DataException {
long txTimestamp = System.currentTimeMillis();
byte[] lastReference = sender.getLastReference();
if (lastReference == null) {
System.err.println(String.format("Qortal account %s has no last reference", sender.getAddress()));
System.exit(2);
}
Long fee = null;
int version = 4;
int nonce = 0;
long amount = 0;
Long assetId = null; // because amount is zero
BaseTransactionData baseTransactionData = new BaseTransactionData(txTimestamp, Group.NO_GROUP, lastReference, sender.getPublicKey(), fee, null);
TransactionData messageTransactionData = new MessageTransactionData(baseTransactionData, version, nonce, recipient, amount, assetId, data, false, false);
MessageTransaction messageTransaction = new MessageTransaction(repository, messageTransactionData);
fee = messageTransaction.calcRecommendedFee();
messageTransactionData.setFee(fee);
TransactionUtils.signAndMint(repository, messageTransactionData, sender);
return messageTransaction;
}
private void checkTradeRefund(Repository repository, Account deployer, long deployersInitialBalance, long deployAtFee) throws DataException {
long deployersPostDeploymentBalance = deployersInitialBalance - fundingAmount - deployAtFee;
int refundTimeout = tradeTimeout * 3 / 4 + 1; // close enough
// AT should automatically refund deployer after 'refundTimeout' blocks
for (int blockCount = 0; blockCount <= refundTimeout; ++blockCount)
BlockUtils.mintBlock(repository);
// We don't bother to exactly calculate QORT spent running AT for several blocks, but we do know the expected range
long expectedMinimumBalance = deployersPostDeploymentBalance;
long expectedMaximumBalance = deployersInitialBalance - deployAtFee;
long actualBalance = deployer.getConfirmedBalance(Asset.QORT);
assertTrue(String.format("Deployer's balance %s should be above minimum %s", actualBalance, expectedMinimumBalance), actualBalance > expectedMinimumBalance);
assertTrue(String.format("Deployer's balance %s should be below maximum %s", actualBalance, expectedMaximumBalance), actualBalance < expectedMaximumBalance);
}
private void describeAt(Repository repository, String atAddress) throws DataException {
ATData atData = repository.getATRepository().fromATAddress(atAddress);
CrossChainTradeData tradeData = BitcoinACCTv1.getInstance().populateTradeData(repository, atData);
Function<Long, String> epochMilliFormatter = (timestamp) -> LocalDateTime.ofInstant(Instant.ofEpochMilli(timestamp), ZoneOffset.UTC).format(DateTimeFormatter.ofLocalizedDateTime(FormatStyle.MEDIUM));
int currentBlockHeight = repository.getBlockRepository().getBlockchainHeight();
System.out.print(String.format("%s:\n"
+ "\tmode: %s\n"
+ "\tcreator: %s,\n"
+ "\tcreation timestamp: %s,\n"
+ "\tcurrent balance: %s QORT,\n"
+ "\tis finished: %b,\n"
+ "\tHASH160 of secret-B: %s,\n"
+ "\tredeem payout: %s QORT,\n"
+ "\texpected bitcoin: %s BTC,\n"
+ "\tcurrent block height: %d,\n",
tradeData.qortalAtAddress,
tradeData.mode,
tradeData.qortalCreator,
epochMilliFormatter.apply(tradeData.creationTimestamp),
Amounts.prettyAmount(tradeData.qortBalance),
atData.getIsFinished(),
HashCode.fromBytes(tradeData.hashOfSecretB).toString().substring(0, 40),
Amounts.prettyAmount(tradeData.qortAmount),
Amounts.prettyAmount(tradeData.expectedForeignAmount),
currentBlockHeight));
if (tradeData.mode != AcctMode.OFFERING && tradeData.mode != AcctMode.CANCELLED) { if (tradeData.mode != AcctMode.OFFERING && tradeData.mode != AcctMode.CANCELLED) {
System.out.println(String.format("\trefund height: block %d,\n" System.out.println(String.format("\trefund height: block %d,\n"
+ "\tHASH160 of secret-A: %s,\n" + "\tHASH160 of secret-A: %s,\n"
@ -786,10 +179,4 @@ public class BitcoinACCTv1Tests extends Common {
tradeData.qortalPartnerAddress)); tradeData.qortalPartnerAddress));
} }
} }
private PrivateKeyAccount createTradeAccount(Repository repository) {
// We actually use a known test account with funds to avoid PoW compute
return Common.getTestAccount(repository, "alice");
}
} }

View File

@ -1,769 +1,58 @@
package org.qortal.test.crosschain.bitcoinv3; package org.qortal.test.crosschain.bitcoinv3;
import com.google.common.hash.HashCode; import com.google.common.hash.HashCode;
import com.google.common.primitives.Bytes; import org.qortal.crosschain.ACCT;
import org.junit.Before;
import org.junit.Test;
import org.qortal.account.Account;
import org.qortal.account.PrivateKeyAccount;
import org.qortal.asset.Asset;
import org.qortal.block.Block;
import org.qortal.crosschain.AcctMode;
import org.qortal.crosschain.BitcoinACCTv3; import org.qortal.crosschain.BitcoinACCTv3;
import org.qortal.crypto.Crypto; import org.qortal.test.crosschain.ACCTTests;
import org.qortal.data.at.ATData;
import org.qortal.data.at.ATStateData;
import org.qortal.data.crosschain.CrossChainTradeData;
import org.qortal.data.transaction.BaseTransactionData;
import org.qortal.data.transaction.DeployAtTransactionData;
import org.qortal.data.transaction.MessageTransactionData;
import org.qortal.data.transaction.TransactionData;
import org.qortal.group.Group;
import org.qortal.repository.DataException;
import org.qortal.repository.Repository;
import org.qortal.repository.RepositoryManager;
import org.qortal.test.common.BlockUtils;
import org.qortal.test.common.Common;
import org.qortal.test.common.TransactionUtils;
import org.qortal.transaction.DeployAtTransaction;
import org.qortal.transaction.MessageTransaction;
import org.qortal.utils.Amounts;
import java.time.Instant; public class BitcoinACCTv3Tests extends ACCTTests {
import java.time.LocalDateTime;
import java.time.ZoneOffset;
import java.time.format.DateTimeFormatter;
import java.time.format.FormatStyle;
import java.util.Arrays;
import java.util.List;
import java.util.Random;
import java.util.function.Function;
import static org.junit.Assert.*;
public class BitcoinACCTv3Tests extends Common {
public static final byte[] secretA = "This string is exactly 32 bytes!".getBytes();
public static final byte[] hashOfSecretA = Crypto.hash160(secretA); // daf59884b4d1aec8c1b17102530909ee43c0151a
public static final byte[] bitcoinPublicKeyHash = HashCode.fromString("bb00bb11bb22bb33bb44bb55bb66bb77bb88bb99").asBytes(); public static final byte[] bitcoinPublicKeyHash = HashCode.fromString("bb00bb11bb22bb33bb44bb55bb66bb77bb88bb99").asBytes();
public static final int tradeTimeout = 20; // blocks private static final String SYMBOL = "BTC";
public static final long redeemAmount = 80_40200000L; private static final String NAME = "Bitcoin";
public static final long fundingAmount = 123_45600000L;
public static final long bitcoinAmount = 864200L; // 0.00864200 BTC
private static final Random RANDOM = new Random(); @Override
protected byte[] getPublicKey() {
@Before return bitcoinPublicKeyHash;
public void beforeTest() throws DataException {
Common.useDefaultSettings();
} }
@Test @Override
public void testCompile() { protected byte[] buildQortalAT(String address, byte[] publicKey, long redeemAmount, long foreignAmount, int tradeTimeout) {
PrivateKeyAccount tradeAccount = createTradeAccount(null); return BitcoinACCTv3.buildQortalAT(address, publicKey, redeemAmount, foreignAmount, tradeTimeout);
byte[] creationBytes = BitcoinACCTv3.buildQortalAT(tradeAccount.getAddress(), bitcoinPublicKeyHash, redeemAmount, bitcoinAmount, tradeTimeout);
assertNotNull(creationBytes);
System.out.println("AT creation bytes: " + HashCode.fromBytes(creationBytes).toString());
} }
@Test @Override
public void testDeploy() throws DataException { protected ACCT getInstance() {
try (final Repository repository = RepositoryManager.getRepository()) { return BitcoinACCTv3.getInstance();
PrivateKeyAccount deployer = Common.getTestAccount(repository, "chloe");
PrivateKeyAccount tradeAccount = createTradeAccount(repository);
PrivateKeyAccount partner = Common.getTestAccount(repository, "dilbert");
long deployersInitialBalance = deployer.getConfirmedBalance(Asset.QORT);
long partnersInitialBalance = partner.getConfirmedBalance(Asset.QORT);
DeployAtTransaction deployAtTransaction = doDeploy(repository, deployer, tradeAccount.getAddress());
long expectedBalance = deployersInitialBalance - fundingAmount - deployAtTransaction.getTransactionData().getFee();
long actualBalance = deployer.getConfirmedBalance(Asset.QORT);
assertEquals("Deployer's post-deployment balance incorrect", expectedBalance, actualBalance);
expectedBalance = fundingAmount;
actualBalance = deployAtTransaction.getATAccount().getConfirmedBalance(Asset.QORT);
assertEquals("AT's post-deployment balance incorrect", expectedBalance, actualBalance);
expectedBalance = partnersInitialBalance;
actualBalance = partner.getConfirmedBalance(Asset.QORT);
assertEquals("Partner's post-deployment balance incorrect", expectedBalance, actualBalance);
// Test orphaning
BlockUtils.orphanLastBlock(repository);
expectedBalance = deployersInitialBalance;
actualBalance = deployer.getConfirmedBalance(Asset.QORT);
assertEquals("Deployer's post-orphan/pre-deployment balance incorrect", expectedBalance, actualBalance);
expectedBalance = 0;
actualBalance = deployAtTransaction.getATAccount().getConfirmedBalance(Asset.QORT);
assertEquals("AT's post-orphan/pre-deployment balance incorrect", expectedBalance, actualBalance);
expectedBalance = partnersInitialBalance;
actualBalance = partner.getConfirmedBalance(Asset.QORT);
assertEquals("Partner's post-orphan/pre-deployment balance incorrect", expectedBalance, actualBalance);
}
} }
@SuppressWarnings("unused") @Override
@Test protected int calcRefundTimeout(long partnersOfferMessageTransactionTimestamp, int lockTimeA) {
public void testOfferCancel() throws DataException { return BitcoinACCTv3.calcRefundTimeout(partnersOfferMessageTransactionTimestamp, lockTimeA);
try (final Repository repository = RepositoryManager.getRepository()) {
PrivateKeyAccount deployer = Common.getTestAccount(repository, "chloe");
PrivateKeyAccount tradeAccount = createTradeAccount(repository);
PrivateKeyAccount partner = Common.getTestAccount(repository, "dilbert");
long deployersInitialBalance = deployer.getConfirmedBalance(Asset.QORT);
long partnersInitialBalance = partner.getConfirmedBalance(Asset.QORT);
DeployAtTransaction deployAtTransaction = doDeploy(repository, deployer, tradeAccount.getAddress());
Account at = deployAtTransaction.getATAccount();
String atAddress = at.getAddress();
long deployAtFee = deployAtTransaction.getTransactionData().getFee();
long deployersPostDeploymentBalance = deployersInitialBalance - fundingAmount - deployAtFee;
// Send creator's address to AT, instead of typical partner's address
byte[] messageData = BitcoinACCTv3.getInstance().buildCancelMessage(deployer.getAddress());
MessageTransaction messageTransaction = sendMessage(repository, deployer, messageData, atAddress);
long messageFee = messageTransaction.getTransactionData().getFee();
// AT should process 'cancel' message in next block
BlockUtils.mintBlock(repository);
describeAt(repository, atAddress);
// Check AT is finished
ATData atData = repository.getATRepository().fromATAddress(atAddress);
assertTrue(atData.getIsFinished());
// AT should be in CANCELLED mode
CrossChainTradeData tradeData = BitcoinACCTv3.getInstance().populateTradeData(repository, atData);
assertEquals(AcctMode.CANCELLED, tradeData.mode);
// Check balances
long expectedMinimumBalance = deployersPostDeploymentBalance;
long expectedMaximumBalance = deployersInitialBalance - deployAtFee - messageFee;
long actualBalance = deployer.getConfirmedBalance(Asset.QORT);
assertTrue(String.format("Deployer's balance %s should be above minimum %s", actualBalance, expectedMinimumBalance), actualBalance > expectedMinimumBalance);
assertTrue(String.format("Deployer's balance %s should be below maximum %s", actualBalance, expectedMaximumBalance), actualBalance < expectedMaximumBalance);
// Test orphaning
BlockUtils.orphanLastBlock(repository);
// Check balances
long expectedBalance = deployersPostDeploymentBalance - messageFee;
actualBalance = deployer.getConfirmedBalance(Asset.QORT);
assertEquals("Deployer's post-orphan/pre-refund balance incorrect", expectedBalance, actualBalance);
}
} }
@SuppressWarnings("unused") @Override
@Test protected byte[] buildTradeMessage(String address, byte[] publicKey, byte[] hashOfSecretA, int lockTimeA, int refundTimeout) {
public void testOfferCancelInvalidLength() throws DataException { return BitcoinACCTv3.buildTradeMessage(address, publicKey, hashOfSecretA, lockTimeA, refundTimeout);
try (final Repository repository = RepositoryManager.getRepository()) {
PrivateKeyAccount deployer = Common.getTestAccount(repository, "chloe");
PrivateKeyAccount tradeAccount = createTradeAccount(repository);
PrivateKeyAccount partner = Common.getTestAccount(repository, "dilbert");
long deployersInitialBalance = deployer.getConfirmedBalance(Asset.QORT);
long partnersInitialBalance = partner.getConfirmedBalance(Asset.QORT);
DeployAtTransaction deployAtTransaction = doDeploy(repository, deployer, tradeAccount.getAddress());
Account at = deployAtTransaction.getATAccount();
String atAddress = at.getAddress();
long deployAtFee = deployAtTransaction.getTransactionData().getFee();
long deployersPostDeploymentBalance = deployersInitialBalance - fundingAmount - deployAtFee;
// Instead of sending creator's address to AT, send too-short/invalid message
byte[] messageData = new byte[7];
RANDOM.nextBytes(messageData);
MessageTransaction messageTransaction = sendMessage(repository, deployer, messageData, atAddress);
long messageFee = messageTransaction.getTransactionData().getFee();
// AT should process 'cancel' message in next block
// As message is too short, it will be padded to 32bytes but cancel code doesn't care about message content, so should be ok
BlockUtils.mintBlock(repository);
describeAt(repository, atAddress);
// Check AT is finished
ATData atData = repository.getATRepository().fromATAddress(atAddress);
assertTrue(atData.getIsFinished());
// AT should be in CANCELLED mode
CrossChainTradeData tradeData = BitcoinACCTv3.getInstance().populateTradeData(repository, atData);
assertEquals(AcctMode.CANCELLED, tradeData.mode);
}
} }
@SuppressWarnings("unused") @Override
@Test protected byte[] buildRedeemMessage(byte[] secretA, String address) {
public void testTradingInfoProcessing() throws DataException { return BitcoinACCTv3.buildRedeemMessage(secretA, address);
try (final Repository repository = RepositoryManager.getRepository()) {
PrivateKeyAccount deployer = Common.getTestAccount(repository, "chloe");
PrivateKeyAccount tradeAccount = createTradeAccount(repository);
PrivateKeyAccount partner = Common.getTestAccount(repository, "dilbert");
long deployersInitialBalance = deployer.getConfirmedBalance(Asset.QORT);
long partnersInitialBalance = partner.getConfirmedBalance(Asset.QORT);
DeployAtTransaction deployAtTransaction = doDeploy(repository, deployer, tradeAccount.getAddress());
Account at = deployAtTransaction.getATAccount();
String atAddress = at.getAddress();
long partnersOfferMessageTransactionTimestamp = System.currentTimeMillis();
int lockTimeA = calcTestLockTimeA(partnersOfferMessageTransactionTimestamp);
int refundTimeout = BitcoinACCTv3.calcRefundTimeout(partnersOfferMessageTransactionTimestamp, lockTimeA);
// Send trade info to AT
byte[] messageData = BitcoinACCTv3.buildTradeMessage(partner.getAddress(), bitcoinPublicKeyHash, hashOfSecretA, lockTimeA, refundTimeout);
MessageTransaction messageTransaction = sendMessage(repository, tradeAccount, messageData, atAddress);
Block postDeploymentBlock = BlockUtils.mintBlock(repository);
int postDeploymentBlockHeight = postDeploymentBlock.getBlockData().getHeight();
long deployAtFee = deployAtTransaction.getTransactionData().getFee();
long deployersPostDeploymentBalance = deployersInitialBalance - fundingAmount - deployAtFee;
describeAt(repository, atAddress);
ATData atData = repository.getATRepository().fromATAddress(atAddress);
CrossChainTradeData tradeData = BitcoinACCTv3.getInstance().populateTradeData(repository, atData);
// AT should be in TRADE mode
assertEquals(AcctMode.TRADING, tradeData.mode);
// Check hashOfSecretA was extracted correctly
assertTrue(Arrays.equals(hashOfSecretA, tradeData.hashOfSecretA));
// Check trade partner Qortal address was extracted correctly
assertEquals(partner.getAddress(), tradeData.qortalPartnerAddress);
// Check trade partner's Bitcoin PKH was extracted correctly
assertTrue(Arrays.equals(bitcoinPublicKeyHash, tradeData.partnerForeignPKH));
// Test orphaning
BlockUtils.orphanToBlock(repository, postDeploymentBlockHeight);
// Check balances
long expectedBalance = deployersPostDeploymentBalance;
long actualBalance = deployer.getConfirmedBalance(Asset.QORT);
assertEquals("Deployer's post-orphan/pre-refund balance incorrect", expectedBalance, actualBalance);
}
} }
// TEST SENDING TRADING INFO BUT NOT FROM AT CREATOR (SHOULD BE IGNORED) @Override
@SuppressWarnings("unused") protected byte[] getCodeBytesHash() {
@Test return BitcoinACCTv3.CODE_BYTES_HASH;
public void testIncorrectTradeSender() throws DataException {
try (final Repository repository = RepositoryManager.getRepository()) {
PrivateKeyAccount deployer = Common.getTestAccount(repository, "chloe");
PrivateKeyAccount tradeAccount = createTradeAccount(repository);
PrivateKeyAccount partner = Common.getTestAccount(repository, "dilbert");
PrivateKeyAccount bystander = Common.getTestAccount(repository, "bob");
long deployersInitialBalance = deployer.getConfirmedBalance(Asset.QORT);
long partnersInitialBalance = partner.getConfirmedBalance(Asset.QORT);
DeployAtTransaction deployAtTransaction = doDeploy(repository, deployer, tradeAccount.getAddress());
Account at = deployAtTransaction.getATAccount();
String atAddress = at.getAddress();
long partnersOfferMessageTransactionTimestamp = System.currentTimeMillis();
int lockTimeA = calcTestLockTimeA(partnersOfferMessageTransactionTimestamp);
int refundTimeout = BitcoinACCTv3.calcRefundTimeout(partnersOfferMessageTransactionTimestamp, lockTimeA);
// Send trade info to AT BUT NOT FROM AT CREATOR
byte[] messageData = BitcoinACCTv3.buildTradeMessage(partner.getAddress(), bitcoinPublicKeyHash, hashOfSecretA, lockTimeA, refundTimeout);
MessageTransaction messageTransaction = sendMessage(repository, bystander, messageData, atAddress);
BlockUtils.mintBlock(repository);
long expectedBalance = partnersInitialBalance;
long actualBalance = partner.getConfirmedBalance(Asset.QORT);
assertEquals("Partner's post-initial-payout balance incorrect", expectedBalance, actualBalance);
describeAt(repository, atAddress);
ATData atData = repository.getATRepository().fromATAddress(atAddress);
CrossChainTradeData tradeData = BitcoinACCTv3.getInstance().populateTradeData(repository, atData);
// AT should still be in OFFER mode
assertEquals(AcctMode.OFFERING, tradeData.mode);
}
} }
@SuppressWarnings("unused") @Override
@Test protected String getSymbol() {
public void testAutomaticTradeRefund() throws DataException { return SYMBOL;
try (final Repository repository = RepositoryManager.getRepository()) {
PrivateKeyAccount deployer = Common.getTestAccount(repository, "chloe");
PrivateKeyAccount tradeAccount = createTradeAccount(repository);
PrivateKeyAccount partner = Common.getTestAccount(repository, "dilbert");
long deployersInitialBalance = deployer.getConfirmedBalance(Asset.QORT);
long partnersInitialBalance = partner.getConfirmedBalance(Asset.QORT);
DeployAtTransaction deployAtTransaction = doDeploy(repository, deployer, tradeAccount.getAddress());
Account at = deployAtTransaction.getATAccount();
String atAddress = at.getAddress();
long partnersOfferMessageTransactionTimestamp = System.currentTimeMillis();
int lockTimeA = calcTestLockTimeA(partnersOfferMessageTransactionTimestamp);
int refundTimeout = BitcoinACCTv3.calcRefundTimeout(partnersOfferMessageTransactionTimestamp, lockTimeA);
// Send trade info to AT
byte[] messageData = BitcoinACCTv3.buildTradeMessage(partner.getAddress(), bitcoinPublicKeyHash, hashOfSecretA, lockTimeA, refundTimeout);
MessageTransaction messageTransaction = sendMessage(repository, tradeAccount, messageData, atAddress);
Block postDeploymentBlock = BlockUtils.mintBlock(repository);
int postDeploymentBlockHeight = postDeploymentBlock.getBlockData().getHeight();
// Check refund
long deployAtFee = deployAtTransaction.getTransactionData().getFee();
long deployersPostDeploymentBalance = deployersInitialBalance - fundingAmount - deployAtFee;
checkTradeRefund(repository, deployer, deployersInitialBalance, deployAtFee);
describeAt(repository, atAddress);
// Check AT is finished
ATData atData = repository.getATRepository().fromATAddress(atAddress);
assertTrue(atData.getIsFinished());
// AT should be in REFUNDED mode
CrossChainTradeData tradeData = BitcoinACCTv3.getInstance().populateTradeData(repository, atData);
assertEquals(AcctMode.REFUNDED, tradeData.mode);
// Test orphaning
BlockUtils.orphanToBlock(repository, postDeploymentBlockHeight);
// Check balances
long expectedBalance = deployersPostDeploymentBalance;
long actualBalance = deployer.getConfirmedBalance(Asset.QORT);
assertEquals("Deployer's post-orphan/pre-refund balance incorrect", expectedBalance, actualBalance);
}
} }
@SuppressWarnings("unused") @Override
@Test protected String getName() {
public void testCorrectSecretCorrectSender() throws DataException { return NAME;
try (final Repository repository = RepositoryManager.getRepository()) {
PrivateKeyAccount deployer = Common.getTestAccount(repository, "chloe");
PrivateKeyAccount tradeAccount = createTradeAccount(repository);
PrivateKeyAccount partner = Common.getTestAccount(repository, "dilbert");
long deployersInitialBalance = deployer.getConfirmedBalance(Asset.QORT);
long partnersInitialBalance = partner.getConfirmedBalance(Asset.QORT);
DeployAtTransaction deployAtTransaction = doDeploy(repository, deployer, tradeAccount.getAddress());
Account at = deployAtTransaction.getATAccount();
String atAddress = at.getAddress();
long partnersOfferMessageTransactionTimestamp = System.currentTimeMillis();
int lockTimeA = calcTestLockTimeA(partnersOfferMessageTransactionTimestamp);
int refundTimeout = BitcoinACCTv3.calcRefundTimeout(partnersOfferMessageTransactionTimestamp, lockTimeA);
// Send trade info to AT
byte[] messageData = BitcoinACCTv3.buildTradeMessage(partner.getAddress(), bitcoinPublicKeyHash, hashOfSecretA, lockTimeA, refundTimeout);
MessageTransaction messageTransaction = sendMessage(repository, tradeAccount, messageData, atAddress);
// Give AT time to process message
BlockUtils.mintBlock(repository);
// Send correct secret to AT, from correct account
messageData = BitcoinACCTv3.buildRedeemMessage(secretA, partner.getAddress());
messageTransaction = sendMessage(repository, partner, messageData, atAddress);
// AT should send funds in the next block
ATStateData preRedeemAtStateData = repository.getATRepository().getLatestATState(atAddress);
BlockUtils.mintBlock(repository);
describeAt(repository, atAddress);
// Check AT is finished
ATData atData = repository.getATRepository().fromATAddress(atAddress);
assertTrue(atData.getIsFinished());
// AT should be in REDEEMED mode
CrossChainTradeData tradeData = BitcoinACCTv3.getInstance().populateTradeData(repository, atData);
assertEquals(AcctMode.REDEEMED, tradeData.mode);
// Check balances
long expectedBalance = partnersInitialBalance - messageTransaction.getTransactionData().getFee() + redeemAmount;
long actualBalance = partner.getConfirmedBalance(Asset.QORT);
assertEquals("Partner's post-redeem balance incorrect", expectedBalance, actualBalance);
// Orphan redeem
BlockUtils.orphanLastBlock(repository);
// Check balances
expectedBalance = partnersInitialBalance - messageTransaction.getTransactionData().getFee();
actualBalance = partner.getConfirmedBalance(Asset.QORT);
assertEquals("Partner's post-orphan/pre-redeem balance incorrect", expectedBalance, actualBalance);
// Check AT state
ATStateData postOrphanAtStateData = repository.getATRepository().getLatestATState(atAddress);
assertTrue("AT states mismatch", Arrays.equals(preRedeemAtStateData.getStateData(), postOrphanAtStateData.getStateData()));
}
} }
@SuppressWarnings("unused")
@Test
public void testCorrectSecretIncorrectSender() throws DataException {
try (final Repository repository = RepositoryManager.getRepository()) {
PrivateKeyAccount deployer = Common.getTestAccount(repository, "chloe");
PrivateKeyAccount tradeAccount = createTradeAccount(repository);
PrivateKeyAccount partner = Common.getTestAccount(repository, "dilbert");
PrivateKeyAccount bystander = Common.getTestAccount(repository, "bob");
long deployersInitialBalance = deployer.getConfirmedBalance(Asset.QORT);
long partnersInitialBalance = partner.getConfirmedBalance(Asset.QORT);
DeployAtTransaction deployAtTransaction = doDeploy(repository, deployer, tradeAccount.getAddress());
long deployAtFee = deployAtTransaction.getTransactionData().getFee();
Account at = deployAtTransaction.getATAccount();
String atAddress = at.getAddress();
long partnersOfferMessageTransactionTimestamp = System.currentTimeMillis();
int lockTimeA = calcTestLockTimeA(partnersOfferMessageTransactionTimestamp);
int refundTimeout = BitcoinACCTv3.calcRefundTimeout(partnersOfferMessageTransactionTimestamp, lockTimeA);
// Send trade info to AT
byte[] messageData = BitcoinACCTv3.buildTradeMessage(partner.getAddress(), bitcoinPublicKeyHash, hashOfSecretA, lockTimeA, refundTimeout);
MessageTransaction messageTransaction = sendMessage(repository, tradeAccount, messageData, atAddress);
// Give AT time to process message
BlockUtils.mintBlock(repository);
// Send correct secret to AT, but from wrong account
messageData = BitcoinACCTv3.buildRedeemMessage(secretA, partner.getAddress());
messageTransaction = sendMessage(repository, bystander, messageData, atAddress);
// AT should NOT send funds in the next block
ATStateData preRedeemAtStateData = repository.getATRepository().getLatestATState(atAddress);
BlockUtils.mintBlock(repository);
describeAt(repository, atAddress);
// Check AT is NOT finished
ATData atData = repository.getATRepository().fromATAddress(atAddress);
assertFalse(atData.getIsFinished());
// AT should still be in TRADE mode
CrossChainTradeData tradeData = BitcoinACCTv3.getInstance().populateTradeData(repository, atData);
assertEquals(AcctMode.TRADING, tradeData.mode);
// Check balances
long expectedBalance = partnersInitialBalance;
long actualBalance = partner.getConfirmedBalance(Asset.QORT);
assertEquals("Partner's balance incorrect", expectedBalance, actualBalance);
// Check eventual refund
checkTradeRefund(repository, deployer, deployersInitialBalance, deployAtFee);
}
}
@SuppressWarnings("unused")
@Test
public void testIncorrectSecretCorrectSender() throws DataException {
try (final Repository repository = RepositoryManager.getRepository()) {
PrivateKeyAccount deployer = Common.getTestAccount(repository, "chloe");
PrivateKeyAccount tradeAccount = createTradeAccount(repository);
PrivateKeyAccount partner = Common.getTestAccount(repository, "dilbert");
long deployersInitialBalance = deployer.getConfirmedBalance(Asset.QORT);
long partnersInitialBalance = partner.getConfirmedBalance(Asset.QORT);
DeployAtTransaction deployAtTransaction = doDeploy(repository, deployer, tradeAccount.getAddress());
long deployAtFee = deployAtTransaction.getTransactionData().getFee();
Account at = deployAtTransaction.getATAccount();
String atAddress = at.getAddress();
long partnersOfferMessageTransactionTimestamp = System.currentTimeMillis();
int lockTimeA = calcTestLockTimeA(partnersOfferMessageTransactionTimestamp);
int refundTimeout = BitcoinACCTv3.calcRefundTimeout(partnersOfferMessageTransactionTimestamp, lockTimeA);
// Send trade info to AT
byte[] messageData = BitcoinACCTv3.buildTradeMessage(partner.getAddress(), bitcoinPublicKeyHash, hashOfSecretA, lockTimeA, refundTimeout);
MessageTransaction messageTransaction = sendMessage(repository, tradeAccount, messageData, atAddress);
// Give AT time to process message
BlockUtils.mintBlock(repository);
// Send incorrect secret to AT, from correct account
byte[] wrongSecret = new byte[32];
RANDOM.nextBytes(wrongSecret);
messageData = BitcoinACCTv3.buildRedeemMessage(wrongSecret, partner.getAddress());
messageTransaction = sendMessage(repository, partner, messageData, atAddress);
// AT should NOT send funds in the next block
ATStateData preRedeemAtStateData = repository.getATRepository().getLatestATState(atAddress);
BlockUtils.mintBlock(repository);
describeAt(repository, atAddress);
// Check AT is NOT finished
ATData atData = repository.getATRepository().fromATAddress(atAddress);
assertFalse(atData.getIsFinished());
// AT should still be in TRADE mode
CrossChainTradeData tradeData = BitcoinACCTv3.getInstance().populateTradeData(repository, atData);
assertEquals(AcctMode.TRADING, tradeData.mode);
long expectedBalance = partnersInitialBalance - messageTransaction.getTransactionData().getFee();
long actualBalance = partner.getConfirmedBalance(Asset.QORT);
assertEquals("Partner's balance incorrect", expectedBalance, actualBalance);
// Check eventual refund
checkTradeRefund(repository, deployer, deployersInitialBalance, deployAtFee);
}
}
@SuppressWarnings("unused")
@Test
public void testCorrectSecretCorrectSenderInvalidMessageLength() throws DataException {
try (final Repository repository = RepositoryManager.getRepository()) {
PrivateKeyAccount deployer = Common.getTestAccount(repository, "chloe");
PrivateKeyAccount tradeAccount = createTradeAccount(repository);
PrivateKeyAccount partner = Common.getTestAccount(repository, "dilbert");
long deployersInitialBalance = deployer.getConfirmedBalance(Asset.QORT);
long partnersInitialBalance = partner.getConfirmedBalance(Asset.QORT);
DeployAtTransaction deployAtTransaction = doDeploy(repository, deployer, tradeAccount.getAddress());
Account at = deployAtTransaction.getATAccount();
String atAddress = at.getAddress();
long partnersOfferMessageTransactionTimestamp = System.currentTimeMillis();
int lockTimeA = calcTestLockTimeA(partnersOfferMessageTransactionTimestamp);
int refundTimeout = BitcoinACCTv3.calcRefundTimeout(partnersOfferMessageTransactionTimestamp, lockTimeA);
// Send trade info to AT
byte[] messageData = BitcoinACCTv3.buildTradeMessage(partner.getAddress(), bitcoinPublicKeyHash, hashOfSecretA, lockTimeA, refundTimeout);
MessageTransaction messageTransaction = sendMessage(repository, tradeAccount, messageData, atAddress);
// Give AT time to process message
BlockUtils.mintBlock(repository);
// Send correct secret to AT, from correct account, but missing receive address, hence incorrect length
messageData = Bytes.concat(secretA);
messageTransaction = sendMessage(repository, partner, messageData, atAddress);
// AT should NOT send funds in the next block
ATStateData preRedeemAtStateData = repository.getATRepository().getLatestATState(atAddress);
BlockUtils.mintBlock(repository);
describeAt(repository, atAddress);
// Check AT is NOT finished
ATData atData = repository.getATRepository().fromATAddress(atAddress);
assertFalse(atData.getIsFinished());
// AT should be in TRADING mode
CrossChainTradeData tradeData = BitcoinACCTv3.getInstance().populateTradeData(repository, atData);
assertEquals(AcctMode.TRADING, tradeData.mode);
}
}
@SuppressWarnings("unused")
@Test
public void testDescribeDeployed() throws DataException {
try (final Repository repository = RepositoryManager.getRepository()) {
PrivateKeyAccount deployer = Common.getTestAccount(repository, "chloe");
PrivateKeyAccount tradeAccount = createTradeAccount(repository);
PrivateKeyAccount partner = Common.getTestAccount(repository, "dilbert");
long deployersInitialBalance = deployer.getConfirmedBalance(Asset.QORT);
long partnersInitialBalance = partner.getConfirmedBalance(Asset.QORT);
DeployAtTransaction deployAtTransaction = doDeploy(repository, deployer, tradeAccount.getAddress());
List<ATData> executableAts = repository.getATRepository().getAllExecutableATs();
for (ATData atData : executableAts) {
String atAddress = atData.getATAddress();
byte[] codeBytes = atData.getCodeBytes();
byte[] codeHash = Crypto.digest(codeBytes);
System.out.println(String.format("%s: code length: %d byte%s, code hash: %s",
atAddress,
codeBytes.length,
(codeBytes.length != 1 ? "s": ""),
HashCode.fromBytes(codeHash)));
// Not one of ours?
if (!Arrays.equals(codeHash, BitcoinACCTv3.CODE_BYTES_HASH))
continue;
describeAt(repository, atAddress);
}
}
}
private int calcTestLockTimeA(long messageTimestamp) {
return (int) (messageTimestamp / 1000L + tradeTimeout * 60);
}
private DeployAtTransaction doDeploy(Repository repository, PrivateKeyAccount deployer, String tradeAddress) throws DataException {
byte[] creationBytes = BitcoinACCTv3.buildQortalAT(tradeAddress, bitcoinPublicKeyHash, redeemAmount, bitcoinAmount, tradeTimeout);
long txTimestamp = System.currentTimeMillis();
byte[] lastReference = deployer.getLastReference();
if (lastReference == null) {
System.err.println(String.format("Qortal account %s has no last reference", deployer.getAddress()));
System.exit(2);
}
Long fee = null;
String name = "QORT-BTC cross-chain trade";
String description = String.format("Qortal-Bitcoin cross-chain trade");
String atType = "ACCT";
String tags = "QORT-BTC ACCT";
BaseTransactionData baseTransactionData = new BaseTransactionData(txTimestamp, Group.NO_GROUP, lastReference, deployer.getPublicKey(), fee, null);
TransactionData deployAtTransactionData = new DeployAtTransactionData(baseTransactionData, name, description, atType, tags, creationBytes, fundingAmount, Asset.QORT);
DeployAtTransaction deployAtTransaction = new DeployAtTransaction(repository, deployAtTransactionData);
fee = deployAtTransaction.calcRecommendedFee();
deployAtTransactionData.setFee(fee);
TransactionUtils.signAndMint(repository, deployAtTransactionData, deployer);
return deployAtTransaction;
}
private MessageTransaction sendMessage(Repository repository, PrivateKeyAccount sender, byte[] data, String recipient) throws DataException {
long txTimestamp = System.currentTimeMillis();
byte[] lastReference = sender.getLastReference();
if (lastReference == null) {
System.err.println(String.format("Qortal account %s has no last reference", sender.getAddress()));
System.exit(2);
}
Long fee = null;
int version = 4;
int nonce = 0;
long amount = 0;
Long assetId = null; // because amount is zero
BaseTransactionData baseTransactionData = new BaseTransactionData(txTimestamp, Group.NO_GROUP, lastReference, sender.getPublicKey(), fee, null);
TransactionData messageTransactionData = new MessageTransactionData(baseTransactionData, version, nonce, recipient, amount, assetId, data, false, false);
MessageTransaction messageTransaction = new MessageTransaction(repository, messageTransactionData);
fee = messageTransaction.calcRecommendedFee();
messageTransactionData.setFee(fee);
TransactionUtils.signAndMint(repository, messageTransactionData, sender);
return messageTransaction;
}
private void checkTradeRefund(Repository repository, Account deployer, long deployersInitialBalance, long deployAtFee) throws DataException {
long deployersPostDeploymentBalance = deployersInitialBalance - fundingAmount - deployAtFee;
int refundTimeout = tradeTimeout / 2 + 1; // close enough
// AT should automatically refund deployer after 'refundTimeout' blocks
for (int blockCount = 0; blockCount <= refundTimeout; ++blockCount)
BlockUtils.mintBlock(repository);
// We don't bother to exactly calculate QORT spent running AT for several blocks, but we do know the expected range
long expectedMinimumBalance = deployersPostDeploymentBalance;
long expectedMaximumBalance = deployersInitialBalance - deployAtFee;
long actualBalance = deployer.getConfirmedBalance(Asset.QORT);
assertTrue(String.format("Deployer's balance %s should be above minimum %s", actualBalance, expectedMinimumBalance), actualBalance > expectedMinimumBalance);
assertTrue(String.format("Deployer's balance %s should be below maximum %s", actualBalance, expectedMaximumBalance), actualBalance < expectedMaximumBalance);
}
private void describeAt(Repository repository, String atAddress) throws DataException {
ATData atData = repository.getATRepository().fromATAddress(atAddress);
CrossChainTradeData tradeData = BitcoinACCTv3.getInstance().populateTradeData(repository, atData);
Function<Long, String> epochMilliFormatter = (timestamp) -> LocalDateTime.ofInstant(Instant.ofEpochMilli(timestamp), ZoneOffset.UTC).format(DateTimeFormatter.ofLocalizedDateTime(FormatStyle.MEDIUM));
int currentBlockHeight = repository.getBlockRepository().getBlockchainHeight();
System.out.print(String.format("%s:\n"
+ "\tmode: %s\n"
+ "\tcreator: %s,\n"
+ "\tcreation timestamp: %s,\n"
+ "\tcurrent balance: %s QORT,\n"
+ "\tis finished: %b,\n"
+ "\tredeem payout: %s QORT,\n"
+ "\texpected Bitcoin: %s BTC,\n"
+ "\tcurrent block height: %d,\n",
tradeData.qortalAtAddress,
tradeData.mode,
tradeData.qortalCreator,
epochMilliFormatter.apply(tradeData.creationTimestamp),
Amounts.prettyAmount(tradeData.qortBalance),
atData.getIsFinished(),
Amounts.prettyAmount(tradeData.qortAmount),
Amounts.prettyAmount(tradeData.expectedForeignAmount),
currentBlockHeight));
if (tradeData.mode != AcctMode.OFFERING && tradeData.mode != AcctMode.CANCELLED) {
System.out.println(String.format("\trefund timeout: %d minutes,\n"
+ "\trefund height: block %d,\n"
+ "\tHASH160 of secret-A: %s,\n"
+ "\tBitcoin P2SH-A nLockTime: %d (%s),\n"
+ "\ttrade partner: %s\n"
+ "\tpartner's receiving address: %s",
tradeData.refundTimeout,
tradeData.tradeRefundHeight,
HashCode.fromBytes(tradeData.hashOfSecretA).toString().substring(0, 40),
tradeData.lockTimeA, epochMilliFormatter.apply(tradeData.lockTimeA * 1000L),
tradeData.qortalPartnerAddress,
tradeData.qortalPartnerReceivingAddress));
}
}
private PrivateKeyAccount createTradeAccount(Repository repository) {
// We actually use a known test account with funds to avoid PoW compute
return Common.getTestAccount(repository, "alice");
}
} }

View File

@ -1,769 +1,58 @@
package org.qortal.test.crosschain.digibytev3; package org.qortal.test.crosschain.digibytev3;
import com.google.common.hash.HashCode; import com.google.common.hash.HashCode;
import com.google.common.primitives.Bytes; import org.qortal.crosschain.ACCT;
import org.junit.Before;
import org.junit.Test;
import org.qortal.account.Account;
import org.qortal.account.PrivateKeyAccount;
import org.qortal.asset.Asset;
import org.qortal.block.Block;
import org.qortal.crosschain.AcctMode;
import org.qortal.crosschain.DigibyteACCTv3; import org.qortal.crosschain.DigibyteACCTv3;
import org.qortal.crypto.Crypto; import org.qortal.test.crosschain.ACCTTests;
import org.qortal.data.at.ATData;
import org.qortal.data.at.ATStateData;
import org.qortal.data.crosschain.CrossChainTradeData;
import org.qortal.data.transaction.BaseTransactionData;
import org.qortal.data.transaction.DeployAtTransactionData;
import org.qortal.data.transaction.MessageTransactionData;
import org.qortal.data.transaction.TransactionData;
import org.qortal.group.Group;
import org.qortal.repository.DataException;
import org.qortal.repository.Repository;
import org.qortal.repository.RepositoryManager;
import org.qortal.test.common.BlockUtils;
import org.qortal.test.common.Common;
import org.qortal.test.common.TransactionUtils;
import org.qortal.transaction.DeployAtTransaction;
import org.qortal.transaction.MessageTransaction;
import org.qortal.utils.Amounts;
import java.time.Instant; public class DigibyteACCTv3Tests extends ACCTTests {
import java.time.LocalDateTime;
import java.time.ZoneOffset;
import java.time.format.DateTimeFormatter;
import java.time.format.FormatStyle;
import java.util.Arrays;
import java.util.List;
import java.util.Random;
import java.util.function.Function;
import static org.junit.Assert.*;
public class DigibyteACCTv3Tests extends Common {
public static final byte[] secretA = "This string is exactly 32 bytes!".getBytes();
public static final byte[] hashOfSecretA = Crypto.hash160(secretA); // daf59884b4d1aec8c1b17102530909ee43c0151a
public static final byte[] digibytePublicKeyHash = HashCode.fromString("bb00bb11bb22bb33bb44bb55bb66bb77bb88bb99").asBytes(); public static final byte[] digibytePublicKeyHash = HashCode.fromString("bb00bb11bb22bb33bb44bb55bb66bb77bb88bb99").asBytes();
public static final int tradeTimeout = 20; // blocks private static final String SYMBOL = "DGB";
public static final long redeemAmount = 80_40200000L; private static final String NAME = "DigiByte";
public static final long fundingAmount = 123_45600000L;
public static final long digibyteAmount = 864200L; // 0.00864200 DGB
private static final Random RANDOM = new Random(); @Override
protected byte[] getPublicKey() {
@Before return digibytePublicKeyHash;
public void beforeTest() throws DataException {
Common.useDefaultSettings();
} }
@Test @Override
public void testCompile() { protected byte[] buildQortalAT(String address, byte[] publicKey, long redeemAmount, long foreignAmount, int tradeTimeout) {
PrivateKeyAccount tradeAccount = createTradeAccount(null); return DigibyteACCTv3.buildQortalAT(address, publicKey, redeemAmount, foreignAmount, tradeTimeout);
byte[] creationBytes = DigibyteACCTv3.buildQortalAT(tradeAccount.getAddress(), digibytePublicKeyHash, redeemAmount, digibyteAmount, tradeTimeout);
assertNotNull(creationBytes);
System.out.println("AT creation bytes: " + HashCode.fromBytes(creationBytes).toString());
} }
@Test @Override
public void testDeploy() throws DataException { protected ACCT getInstance() {
try (final Repository repository = RepositoryManager.getRepository()) { return DigibyteACCTv3.getInstance();
PrivateKeyAccount deployer = Common.getTestAccount(repository, "chloe");
PrivateKeyAccount tradeAccount = createTradeAccount(repository);
PrivateKeyAccount partner = Common.getTestAccount(repository, "dilbert");
long deployersInitialBalance = deployer.getConfirmedBalance(Asset.QORT);
long partnersInitialBalance = partner.getConfirmedBalance(Asset.QORT);
DeployAtTransaction deployAtTransaction = doDeploy(repository, deployer, tradeAccount.getAddress());
long expectedBalance = deployersInitialBalance - fundingAmount - deployAtTransaction.getTransactionData().getFee();
long actualBalance = deployer.getConfirmedBalance(Asset.QORT);
assertEquals("Deployer's post-deployment balance incorrect", expectedBalance, actualBalance);
expectedBalance = fundingAmount;
actualBalance = deployAtTransaction.getATAccount().getConfirmedBalance(Asset.QORT);
assertEquals("AT's post-deployment balance incorrect", expectedBalance, actualBalance);
expectedBalance = partnersInitialBalance;
actualBalance = partner.getConfirmedBalance(Asset.QORT);
assertEquals("Partner's post-deployment balance incorrect", expectedBalance, actualBalance);
// Test orphaning
BlockUtils.orphanLastBlock(repository);
expectedBalance = deployersInitialBalance;
actualBalance = deployer.getConfirmedBalance(Asset.QORT);
assertEquals("Deployer's post-orphan/pre-deployment balance incorrect", expectedBalance, actualBalance);
expectedBalance = 0;
actualBalance = deployAtTransaction.getATAccount().getConfirmedBalance(Asset.QORT);
assertEquals("AT's post-orphan/pre-deployment balance incorrect", expectedBalance, actualBalance);
expectedBalance = partnersInitialBalance;
actualBalance = partner.getConfirmedBalance(Asset.QORT);
assertEquals("Partner's post-orphan/pre-deployment balance incorrect", expectedBalance, actualBalance);
}
} }
@SuppressWarnings("unused") @Override
@Test protected int calcRefundTimeout(long partnersOfferMessageTransactionTimestamp, int lockTimeA) {
public void testOfferCancel() throws DataException { return DigibyteACCTv3.calcRefundTimeout(partnersOfferMessageTransactionTimestamp, lockTimeA);
try (final Repository repository = RepositoryManager.getRepository()) {
PrivateKeyAccount deployer = Common.getTestAccount(repository, "chloe");
PrivateKeyAccount tradeAccount = createTradeAccount(repository);
PrivateKeyAccount partner = Common.getTestAccount(repository, "dilbert");
long deployersInitialBalance = deployer.getConfirmedBalance(Asset.QORT);
long partnersInitialBalance = partner.getConfirmedBalance(Asset.QORT);
DeployAtTransaction deployAtTransaction = doDeploy(repository, deployer, tradeAccount.getAddress());
Account at = deployAtTransaction.getATAccount();
String atAddress = at.getAddress();
long deployAtFee = deployAtTransaction.getTransactionData().getFee();
long deployersPostDeploymentBalance = deployersInitialBalance - fundingAmount - deployAtFee;
// Send creator's address to AT, instead of typical partner's address
byte[] messageData = DigibyteACCTv3.getInstance().buildCancelMessage(deployer.getAddress());
MessageTransaction messageTransaction = sendMessage(repository, deployer, messageData, atAddress);
long messageFee = messageTransaction.getTransactionData().getFee();
// AT should process 'cancel' message in next block
BlockUtils.mintBlock(repository);
describeAt(repository, atAddress);
// Check AT is finished
ATData atData = repository.getATRepository().fromATAddress(atAddress);
assertTrue(atData.getIsFinished());
// AT should be in CANCELLED mode
CrossChainTradeData tradeData = DigibyteACCTv3.getInstance().populateTradeData(repository, atData);
assertEquals(AcctMode.CANCELLED, tradeData.mode);
// Check balances
long expectedMinimumBalance = deployersPostDeploymentBalance;
long expectedMaximumBalance = deployersInitialBalance - deployAtFee - messageFee;
long actualBalance = deployer.getConfirmedBalance(Asset.QORT);
assertTrue(String.format("Deployer's balance %s should be above minimum %s", actualBalance, expectedMinimumBalance), actualBalance > expectedMinimumBalance);
assertTrue(String.format("Deployer's balance %s should be below maximum %s", actualBalance, expectedMaximumBalance), actualBalance < expectedMaximumBalance);
// Test orphaning
BlockUtils.orphanLastBlock(repository);
// Check balances
long expectedBalance = deployersPostDeploymentBalance - messageFee;
actualBalance = deployer.getConfirmedBalance(Asset.QORT);
assertEquals("Deployer's post-orphan/pre-refund balance incorrect", expectedBalance, actualBalance);
}
} }
@SuppressWarnings("unused") @Override
@Test protected byte[] buildTradeMessage(String address, byte[] publicKey, byte[] hashOfSecretA, int lockTimeA, int refundTimeout) {
public void testOfferCancelInvalidLength() throws DataException { return DigibyteACCTv3.buildTradeMessage(address, publicKey, hashOfSecretA, lockTimeA, refundTimeout);
try (final Repository repository = RepositoryManager.getRepository()) {
PrivateKeyAccount deployer = Common.getTestAccount(repository, "chloe");
PrivateKeyAccount tradeAccount = createTradeAccount(repository);
PrivateKeyAccount partner = Common.getTestAccount(repository, "dilbert");
long deployersInitialBalance = deployer.getConfirmedBalance(Asset.QORT);
long partnersInitialBalance = partner.getConfirmedBalance(Asset.QORT);
DeployAtTransaction deployAtTransaction = doDeploy(repository, deployer, tradeAccount.getAddress());
Account at = deployAtTransaction.getATAccount();
String atAddress = at.getAddress();
long deployAtFee = deployAtTransaction.getTransactionData().getFee();
long deployersPostDeploymentBalance = deployersInitialBalance - fundingAmount - deployAtFee;
// Instead of sending creator's address to AT, send too-short/invalid message
byte[] messageData = new byte[7];
RANDOM.nextBytes(messageData);
MessageTransaction messageTransaction = sendMessage(repository, deployer, messageData, atAddress);
long messageFee = messageTransaction.getTransactionData().getFee();
// AT should process 'cancel' message in next block
// As message is too short, it will be padded to 32bytes but cancel code doesn't care about message content, so should be ok
BlockUtils.mintBlock(repository);
describeAt(repository, atAddress);
// Check AT is finished
ATData atData = repository.getATRepository().fromATAddress(atAddress);
assertTrue(atData.getIsFinished());
// AT should be in CANCELLED mode
CrossChainTradeData tradeData = DigibyteACCTv3.getInstance().populateTradeData(repository, atData);
assertEquals(AcctMode.CANCELLED, tradeData.mode);
}
} }
@SuppressWarnings("unused") @Override
@Test protected byte[] buildRedeemMessage(byte[] secretA, String address) {
public void testTradingInfoProcessing() throws DataException { return DigibyteACCTv3.buildRedeemMessage(secretA, address);
try (final Repository repository = RepositoryManager.getRepository()) {
PrivateKeyAccount deployer = Common.getTestAccount(repository, "chloe");
PrivateKeyAccount tradeAccount = createTradeAccount(repository);
PrivateKeyAccount partner = Common.getTestAccount(repository, "dilbert");
long deployersInitialBalance = deployer.getConfirmedBalance(Asset.QORT);
long partnersInitialBalance = partner.getConfirmedBalance(Asset.QORT);
DeployAtTransaction deployAtTransaction = doDeploy(repository, deployer, tradeAccount.getAddress());
Account at = deployAtTransaction.getATAccount();
String atAddress = at.getAddress();
long partnersOfferMessageTransactionTimestamp = System.currentTimeMillis();
int lockTimeA = calcTestLockTimeA(partnersOfferMessageTransactionTimestamp);
int refundTimeout = DigibyteACCTv3.calcRefundTimeout(partnersOfferMessageTransactionTimestamp, lockTimeA);
// Send trade info to AT
byte[] messageData = DigibyteACCTv3.buildTradeMessage(partner.getAddress(), digibytePublicKeyHash, hashOfSecretA, lockTimeA, refundTimeout);
MessageTransaction messageTransaction = sendMessage(repository, tradeAccount, messageData, atAddress);
Block postDeploymentBlock = BlockUtils.mintBlock(repository);
int postDeploymentBlockHeight = postDeploymentBlock.getBlockData().getHeight();
long deployAtFee = deployAtTransaction.getTransactionData().getFee();
long deployersPostDeploymentBalance = deployersInitialBalance - fundingAmount - deployAtFee;
describeAt(repository, atAddress);
ATData atData = repository.getATRepository().fromATAddress(atAddress);
CrossChainTradeData tradeData = DigibyteACCTv3.getInstance().populateTradeData(repository, atData);
// AT should be in TRADE mode
assertEquals(AcctMode.TRADING, tradeData.mode);
// Check hashOfSecretA was extracted correctly
assertTrue(Arrays.equals(hashOfSecretA, tradeData.hashOfSecretA));
// Check trade partner Qortal address was extracted correctly
assertEquals(partner.getAddress(), tradeData.qortalPartnerAddress);
// Check trade partner's digibyte PKH was extracted correctly
assertTrue(Arrays.equals(digibytePublicKeyHash, tradeData.partnerForeignPKH));
// Test orphaning
BlockUtils.orphanToBlock(repository, postDeploymentBlockHeight);
// Check balances
long expectedBalance = deployersPostDeploymentBalance;
long actualBalance = deployer.getConfirmedBalance(Asset.QORT);
assertEquals("Deployer's post-orphan/pre-refund balance incorrect", expectedBalance, actualBalance);
}
} }
// TEST SENDING TRADING INFO BUT NOT FROM AT CREATOR (SHOULD BE IGNORED) @Override
@SuppressWarnings("unused") protected byte[] getCodeBytesHash() {
@Test return DigibyteACCTv3.CODE_BYTES_HASH;
public void testIncorrectTradeSender() throws DataException {
try (final Repository repository = RepositoryManager.getRepository()) {
PrivateKeyAccount deployer = Common.getTestAccount(repository, "chloe");
PrivateKeyAccount tradeAccount = createTradeAccount(repository);
PrivateKeyAccount partner = Common.getTestAccount(repository, "dilbert");
PrivateKeyAccount bystander = Common.getTestAccount(repository, "bob");
long deployersInitialBalance = deployer.getConfirmedBalance(Asset.QORT);
long partnersInitialBalance = partner.getConfirmedBalance(Asset.QORT);
DeployAtTransaction deployAtTransaction = doDeploy(repository, deployer, tradeAccount.getAddress());
Account at = deployAtTransaction.getATAccount();
String atAddress = at.getAddress();
long partnersOfferMessageTransactionTimestamp = System.currentTimeMillis();
int lockTimeA = calcTestLockTimeA(partnersOfferMessageTransactionTimestamp);
int refundTimeout = DigibyteACCTv3.calcRefundTimeout(partnersOfferMessageTransactionTimestamp, lockTimeA);
// Send trade info to AT BUT NOT FROM AT CREATOR
byte[] messageData = DigibyteACCTv3.buildTradeMessage(partner.getAddress(), digibytePublicKeyHash, hashOfSecretA, lockTimeA, refundTimeout);
MessageTransaction messageTransaction = sendMessage(repository, bystander, messageData, atAddress);
BlockUtils.mintBlock(repository);
long expectedBalance = partnersInitialBalance;
long actualBalance = partner.getConfirmedBalance(Asset.QORT);
assertEquals("Partner's post-initial-payout balance incorrect", expectedBalance, actualBalance);
describeAt(repository, atAddress);
ATData atData = repository.getATRepository().fromATAddress(atAddress);
CrossChainTradeData tradeData = DigibyteACCTv3.getInstance().populateTradeData(repository, atData);
// AT should still be in OFFER mode
assertEquals(AcctMode.OFFERING, tradeData.mode);
}
} }
@SuppressWarnings("unused") @Override
@Test protected String getSymbol() {
public void testAutomaticTradeRefund() throws DataException { return SYMBOL;
try (final Repository repository = RepositoryManager.getRepository()) {
PrivateKeyAccount deployer = Common.getTestAccount(repository, "chloe");
PrivateKeyAccount tradeAccount = createTradeAccount(repository);
PrivateKeyAccount partner = Common.getTestAccount(repository, "dilbert");
long deployersInitialBalance = deployer.getConfirmedBalance(Asset.QORT);
long partnersInitialBalance = partner.getConfirmedBalance(Asset.QORT);
DeployAtTransaction deployAtTransaction = doDeploy(repository, deployer, tradeAccount.getAddress());
Account at = deployAtTransaction.getATAccount();
String atAddress = at.getAddress();
long partnersOfferMessageTransactionTimestamp = System.currentTimeMillis();
int lockTimeA = calcTestLockTimeA(partnersOfferMessageTransactionTimestamp);
int refundTimeout = DigibyteACCTv3.calcRefundTimeout(partnersOfferMessageTransactionTimestamp, lockTimeA);
// Send trade info to AT
byte[] messageData = DigibyteACCTv3.buildTradeMessage(partner.getAddress(), digibytePublicKeyHash, hashOfSecretA, lockTimeA, refundTimeout);
MessageTransaction messageTransaction = sendMessage(repository, tradeAccount, messageData, atAddress);
Block postDeploymentBlock = BlockUtils.mintBlock(repository);
int postDeploymentBlockHeight = postDeploymentBlock.getBlockData().getHeight();
// Check refund
long deployAtFee = deployAtTransaction.getTransactionData().getFee();
long deployersPostDeploymentBalance = deployersInitialBalance - fundingAmount - deployAtFee;
checkTradeRefund(repository, deployer, deployersInitialBalance, deployAtFee);
describeAt(repository, atAddress);
// Check AT is finished
ATData atData = repository.getATRepository().fromATAddress(atAddress);
assertTrue(atData.getIsFinished());
// AT should be in REFUNDED mode
CrossChainTradeData tradeData = DigibyteACCTv3.getInstance().populateTradeData(repository, atData);
assertEquals(AcctMode.REFUNDED, tradeData.mode);
// Test orphaning
BlockUtils.orphanToBlock(repository, postDeploymentBlockHeight);
// Check balances
long expectedBalance = deployersPostDeploymentBalance;
long actualBalance = deployer.getConfirmedBalance(Asset.QORT);
assertEquals("Deployer's post-orphan/pre-refund balance incorrect", expectedBalance, actualBalance);
}
} }
@SuppressWarnings("unused") @Override
@Test protected String getName() {
public void testCorrectSecretCorrectSender() throws DataException { return NAME;
try (final Repository repository = RepositoryManager.getRepository()) {
PrivateKeyAccount deployer = Common.getTestAccount(repository, "chloe");
PrivateKeyAccount tradeAccount = createTradeAccount(repository);
PrivateKeyAccount partner = Common.getTestAccount(repository, "dilbert");
long deployersInitialBalance = deployer.getConfirmedBalance(Asset.QORT);
long partnersInitialBalance = partner.getConfirmedBalance(Asset.QORT);
DeployAtTransaction deployAtTransaction = doDeploy(repository, deployer, tradeAccount.getAddress());
Account at = deployAtTransaction.getATAccount();
String atAddress = at.getAddress();
long partnersOfferMessageTransactionTimestamp = System.currentTimeMillis();
int lockTimeA = calcTestLockTimeA(partnersOfferMessageTransactionTimestamp);
int refundTimeout = DigibyteACCTv3.calcRefundTimeout(partnersOfferMessageTransactionTimestamp, lockTimeA);
// Send trade info to AT
byte[] messageData = DigibyteACCTv3.buildTradeMessage(partner.getAddress(), digibytePublicKeyHash, hashOfSecretA, lockTimeA, refundTimeout);
MessageTransaction messageTransaction = sendMessage(repository, tradeAccount, messageData, atAddress);
// Give AT time to process message
BlockUtils.mintBlock(repository);
// Send correct secret to AT, from correct account
messageData = DigibyteACCTv3.buildRedeemMessage(secretA, partner.getAddress());
messageTransaction = sendMessage(repository, partner, messageData, atAddress);
// AT should send funds in the next block
ATStateData preRedeemAtStateData = repository.getATRepository().getLatestATState(atAddress);
BlockUtils.mintBlock(repository);
describeAt(repository, atAddress);
// Check AT is finished
ATData atData = repository.getATRepository().fromATAddress(atAddress);
assertTrue(atData.getIsFinished());
// AT should be in REDEEMED mode
CrossChainTradeData tradeData = DigibyteACCTv3.getInstance().populateTradeData(repository, atData);
assertEquals(AcctMode.REDEEMED, tradeData.mode);
// Check balances
long expectedBalance = partnersInitialBalance - messageTransaction.getTransactionData().getFee() + redeemAmount;
long actualBalance = partner.getConfirmedBalance(Asset.QORT);
assertEquals("Partner's post-redeem balance incorrect", expectedBalance, actualBalance);
// Orphan redeem
BlockUtils.orphanLastBlock(repository);
// Check balances
expectedBalance = partnersInitialBalance - messageTransaction.getTransactionData().getFee();
actualBalance = partner.getConfirmedBalance(Asset.QORT);
assertEquals("Partner's post-orphan/pre-redeem balance incorrect", expectedBalance, actualBalance);
// Check AT state
ATStateData postOrphanAtStateData = repository.getATRepository().getLatestATState(atAddress);
assertTrue("AT states mismatch", Arrays.equals(preRedeemAtStateData.getStateData(), postOrphanAtStateData.getStateData()));
}
} }
@SuppressWarnings("unused")
@Test
public void testCorrectSecretIncorrectSender() throws DataException {
try (final Repository repository = RepositoryManager.getRepository()) {
PrivateKeyAccount deployer = Common.getTestAccount(repository, "chloe");
PrivateKeyAccount tradeAccount = createTradeAccount(repository);
PrivateKeyAccount partner = Common.getTestAccount(repository, "dilbert");
PrivateKeyAccount bystander = Common.getTestAccount(repository, "bob");
long deployersInitialBalance = deployer.getConfirmedBalance(Asset.QORT);
long partnersInitialBalance = partner.getConfirmedBalance(Asset.QORT);
DeployAtTransaction deployAtTransaction = doDeploy(repository, deployer, tradeAccount.getAddress());
long deployAtFee = deployAtTransaction.getTransactionData().getFee();
Account at = deployAtTransaction.getATAccount();
String atAddress = at.getAddress();
long partnersOfferMessageTransactionTimestamp = System.currentTimeMillis();
int lockTimeA = calcTestLockTimeA(partnersOfferMessageTransactionTimestamp);
int refundTimeout = DigibyteACCTv3.calcRefundTimeout(partnersOfferMessageTransactionTimestamp, lockTimeA);
// Send trade info to AT
byte[] messageData = DigibyteACCTv3.buildTradeMessage(partner.getAddress(), digibytePublicKeyHash, hashOfSecretA, lockTimeA, refundTimeout);
MessageTransaction messageTransaction = sendMessage(repository, tradeAccount, messageData, atAddress);
// Give AT time to process message
BlockUtils.mintBlock(repository);
// Send correct secret to AT, but from wrong account
messageData = DigibyteACCTv3.buildRedeemMessage(secretA, partner.getAddress());
messageTransaction = sendMessage(repository, bystander, messageData, atAddress);
// AT should NOT send funds in the next block
ATStateData preRedeemAtStateData = repository.getATRepository().getLatestATState(atAddress);
BlockUtils.mintBlock(repository);
describeAt(repository, atAddress);
// Check AT is NOT finished
ATData atData = repository.getATRepository().fromATAddress(atAddress);
assertFalse(atData.getIsFinished());
// AT should still be in TRADE mode
CrossChainTradeData tradeData = DigibyteACCTv3.getInstance().populateTradeData(repository, atData);
assertEquals(AcctMode.TRADING, tradeData.mode);
// Check balances
long expectedBalance = partnersInitialBalance;
long actualBalance = partner.getConfirmedBalance(Asset.QORT);
assertEquals("Partner's balance incorrect", expectedBalance, actualBalance);
// Check eventual refund
checkTradeRefund(repository, deployer, deployersInitialBalance, deployAtFee);
}
}
@SuppressWarnings("unused")
@Test
public void testIncorrectSecretCorrectSender() throws DataException {
try (final Repository repository = RepositoryManager.getRepository()) {
PrivateKeyAccount deployer = Common.getTestAccount(repository, "chloe");
PrivateKeyAccount tradeAccount = createTradeAccount(repository);
PrivateKeyAccount partner = Common.getTestAccount(repository, "dilbert");
long deployersInitialBalance = deployer.getConfirmedBalance(Asset.QORT);
long partnersInitialBalance = partner.getConfirmedBalance(Asset.QORT);
DeployAtTransaction deployAtTransaction = doDeploy(repository, deployer, tradeAccount.getAddress());
long deployAtFee = deployAtTransaction.getTransactionData().getFee();
Account at = deployAtTransaction.getATAccount();
String atAddress = at.getAddress();
long partnersOfferMessageTransactionTimestamp = System.currentTimeMillis();
int lockTimeA = calcTestLockTimeA(partnersOfferMessageTransactionTimestamp);
int refundTimeout = DigibyteACCTv3.calcRefundTimeout(partnersOfferMessageTransactionTimestamp, lockTimeA);
// Send trade info to AT
byte[] messageData = DigibyteACCTv3.buildTradeMessage(partner.getAddress(), digibytePublicKeyHash, hashOfSecretA, lockTimeA, refundTimeout);
MessageTransaction messageTransaction = sendMessage(repository, tradeAccount, messageData, atAddress);
// Give AT time to process message
BlockUtils.mintBlock(repository);
// Send incorrect secret to AT, from correct account
byte[] wrongSecret = new byte[32];
RANDOM.nextBytes(wrongSecret);
messageData = DigibyteACCTv3.buildRedeemMessage(wrongSecret, partner.getAddress());
messageTransaction = sendMessage(repository, partner, messageData, atAddress);
// AT should NOT send funds in the next block
ATStateData preRedeemAtStateData = repository.getATRepository().getLatestATState(atAddress);
BlockUtils.mintBlock(repository);
describeAt(repository, atAddress);
// Check AT is NOT finished
ATData atData = repository.getATRepository().fromATAddress(atAddress);
assertFalse(atData.getIsFinished());
// AT should still be in TRADE mode
CrossChainTradeData tradeData = DigibyteACCTv3.getInstance().populateTradeData(repository, atData);
assertEquals(AcctMode.TRADING, tradeData.mode);
long expectedBalance = partnersInitialBalance - messageTransaction.getTransactionData().getFee();
long actualBalance = partner.getConfirmedBalance(Asset.QORT);
assertEquals("Partner's balance incorrect", expectedBalance, actualBalance);
// Check eventual refund
checkTradeRefund(repository, deployer, deployersInitialBalance, deployAtFee);
}
}
@SuppressWarnings("unused")
@Test
public void testCorrectSecretCorrectSenderInvalidMessageLength() throws DataException {
try (final Repository repository = RepositoryManager.getRepository()) {
PrivateKeyAccount deployer = Common.getTestAccount(repository, "chloe");
PrivateKeyAccount tradeAccount = createTradeAccount(repository);
PrivateKeyAccount partner = Common.getTestAccount(repository, "dilbert");
long deployersInitialBalance = deployer.getConfirmedBalance(Asset.QORT);
long partnersInitialBalance = partner.getConfirmedBalance(Asset.QORT);
DeployAtTransaction deployAtTransaction = doDeploy(repository, deployer, tradeAccount.getAddress());
Account at = deployAtTransaction.getATAccount();
String atAddress = at.getAddress();
long partnersOfferMessageTransactionTimestamp = System.currentTimeMillis();
int lockTimeA = calcTestLockTimeA(partnersOfferMessageTransactionTimestamp);
int refundTimeout = DigibyteACCTv3.calcRefundTimeout(partnersOfferMessageTransactionTimestamp, lockTimeA);
// Send trade info to AT
byte[] messageData = DigibyteACCTv3.buildTradeMessage(partner.getAddress(), digibytePublicKeyHash, hashOfSecretA, lockTimeA, refundTimeout);
MessageTransaction messageTransaction = sendMessage(repository, tradeAccount, messageData, atAddress);
// Give AT time to process message
BlockUtils.mintBlock(repository);
// Send correct secret to AT, from correct account, but missing receive address, hence incorrect length
messageData = Bytes.concat(secretA);
messageTransaction = sendMessage(repository, partner, messageData, atAddress);
// AT should NOT send funds in the next block
ATStateData preRedeemAtStateData = repository.getATRepository().getLatestATState(atAddress);
BlockUtils.mintBlock(repository);
describeAt(repository, atAddress);
// Check AT is NOT finished
ATData atData = repository.getATRepository().fromATAddress(atAddress);
assertFalse(atData.getIsFinished());
// AT should be in TRADING mode
CrossChainTradeData tradeData = DigibyteACCTv3.getInstance().populateTradeData(repository, atData);
assertEquals(AcctMode.TRADING, tradeData.mode);
}
}
@SuppressWarnings("unused")
@Test
public void testDescribeDeployed() throws DataException {
try (final Repository repository = RepositoryManager.getRepository()) {
PrivateKeyAccount deployer = Common.getTestAccount(repository, "chloe");
PrivateKeyAccount tradeAccount = createTradeAccount(repository);
PrivateKeyAccount partner = Common.getTestAccount(repository, "dilbert");
long deployersInitialBalance = deployer.getConfirmedBalance(Asset.QORT);
long partnersInitialBalance = partner.getConfirmedBalance(Asset.QORT);
DeployAtTransaction deployAtTransaction = doDeploy(repository, deployer, tradeAccount.getAddress());
List<ATData> executableAts = repository.getATRepository().getAllExecutableATs();
for (ATData atData : executableAts) {
String atAddress = atData.getATAddress();
byte[] codeBytes = atData.getCodeBytes();
byte[] codeHash = Crypto.digest(codeBytes);
System.out.println(String.format("%s: code length: %d byte%s, code hash: %s",
atAddress,
codeBytes.length,
(codeBytes.length != 1 ? "s": ""),
HashCode.fromBytes(codeHash)));
// Not one of ours?
if (!Arrays.equals(codeHash, DigibyteACCTv3.CODE_BYTES_HASH))
continue;
describeAt(repository, atAddress);
}
}
}
private int calcTestLockTimeA(long messageTimestamp) {
return (int) (messageTimestamp / 1000L + tradeTimeout * 60);
}
private DeployAtTransaction doDeploy(Repository repository, PrivateKeyAccount deployer, String tradeAddress) throws DataException {
byte[] creationBytes = DigibyteACCTv3.buildQortalAT(tradeAddress, digibytePublicKeyHash, redeemAmount, digibyteAmount, tradeTimeout);
long txTimestamp = System.currentTimeMillis();
byte[] lastReference = deployer.getLastReference();
if (lastReference == null) {
System.err.println(String.format("Qortal account %s has no last reference", deployer.getAddress()));
System.exit(2);
}
Long fee = null;
String name = "QORT-DGB cross-chain trade";
String description = String.format("Qortal-Digibyte cross-chain trade");
String atType = "ACCT";
String tags = "QORT-DGB ACCT";
BaseTransactionData baseTransactionData = new BaseTransactionData(txTimestamp, Group.NO_GROUP, lastReference, deployer.getPublicKey(), fee, null);
TransactionData deployAtTransactionData = new DeployAtTransactionData(baseTransactionData, name, description, atType, tags, creationBytes, fundingAmount, Asset.QORT);
DeployAtTransaction deployAtTransaction = new DeployAtTransaction(repository, deployAtTransactionData);
fee = deployAtTransaction.calcRecommendedFee();
deployAtTransactionData.setFee(fee);
TransactionUtils.signAndMint(repository, deployAtTransactionData, deployer);
return deployAtTransaction;
}
private MessageTransaction sendMessage(Repository repository, PrivateKeyAccount sender, byte[] data, String recipient) throws DataException {
long txTimestamp = System.currentTimeMillis();
byte[] lastReference = sender.getLastReference();
if (lastReference == null) {
System.err.println(String.format("Qortal account %s has no last reference", sender.getAddress()));
System.exit(2);
}
Long fee = null;
int version = 4;
int nonce = 0;
long amount = 0;
Long assetId = null; // because amount is zero
BaseTransactionData baseTransactionData = new BaseTransactionData(txTimestamp, Group.NO_GROUP, lastReference, sender.getPublicKey(), fee, null);
TransactionData messageTransactionData = new MessageTransactionData(baseTransactionData, version, nonce, recipient, amount, assetId, data, false, false);
MessageTransaction messageTransaction = new MessageTransaction(repository, messageTransactionData);
fee = messageTransaction.calcRecommendedFee();
messageTransactionData.setFee(fee);
TransactionUtils.signAndMint(repository, messageTransactionData, sender);
return messageTransaction;
}
private void checkTradeRefund(Repository repository, Account deployer, long deployersInitialBalance, long deployAtFee) throws DataException {
long deployersPostDeploymentBalance = deployersInitialBalance - fundingAmount - deployAtFee;
int refundTimeout = tradeTimeout / 2 + 1; // close enough
// AT should automatically refund deployer after 'refundTimeout' blocks
for (int blockCount = 0; blockCount <= refundTimeout; ++blockCount)
BlockUtils.mintBlock(repository);
// We don't bother to exactly calculate QORT spent running AT for several blocks, but we do know the expected range
long expectedMinimumBalance = deployersPostDeploymentBalance;
long expectedMaximumBalance = deployersInitialBalance - deployAtFee;
long actualBalance = deployer.getConfirmedBalance(Asset.QORT);
assertTrue(String.format("Deployer's balance %s should be above minimum %s", actualBalance, expectedMinimumBalance), actualBalance > expectedMinimumBalance);
assertTrue(String.format("Deployer's balance %s should be below maximum %s", actualBalance, expectedMaximumBalance), actualBalance < expectedMaximumBalance);
}
private void describeAt(Repository repository, String atAddress) throws DataException {
ATData atData = repository.getATRepository().fromATAddress(atAddress);
CrossChainTradeData tradeData = DigibyteACCTv3.getInstance().populateTradeData(repository, atData);
Function<Long, String> epochMilliFormatter = (timestamp) -> LocalDateTime.ofInstant(Instant.ofEpochMilli(timestamp), ZoneOffset.UTC).format(DateTimeFormatter.ofLocalizedDateTime(FormatStyle.MEDIUM));
int currentBlockHeight = repository.getBlockRepository().getBlockchainHeight();
System.out.print(String.format("%s:\n"
+ "\tmode: %s\n"
+ "\tcreator: %s,\n"
+ "\tcreation timestamp: %s,\n"
+ "\tcurrent balance: %s QORT,\n"
+ "\tis finished: %b,\n"
+ "\tredeem payout: %s QORT,\n"
+ "\texpected digibyte: %s DGB,\n"
+ "\tcurrent block height: %d,\n",
tradeData.qortalAtAddress,
tradeData.mode,
tradeData.qortalCreator,
epochMilliFormatter.apply(tradeData.creationTimestamp),
Amounts.prettyAmount(tradeData.qortBalance),
atData.getIsFinished(),
Amounts.prettyAmount(tradeData.qortAmount),
Amounts.prettyAmount(tradeData.expectedForeignAmount),
currentBlockHeight));
if (tradeData.mode != AcctMode.OFFERING && tradeData.mode != AcctMode.CANCELLED) {
System.out.println(String.format("\trefund timeout: %d minutes,\n"
+ "\trefund height: block %d,\n"
+ "\tHASH160 of secret-A: %s,\n"
+ "\tDigibyte P2SH-A nLockTime: %d (%s),\n"
+ "\ttrade partner: %s\n"
+ "\tpartner's receiving address: %s",
tradeData.refundTimeout,
tradeData.tradeRefundHeight,
HashCode.fromBytes(tradeData.hashOfSecretA).toString().substring(0, 40),
tradeData.lockTimeA, epochMilliFormatter.apply(tradeData.lockTimeA * 1000L),
tradeData.qortalPartnerAddress,
tradeData.qortalPartnerReceivingAddress));
}
}
private PrivateKeyAccount createTradeAccount(Repository repository) {
// We actually use a known test account with funds to avoid PoW compute
return Common.getTestAccount(repository, "alice");
}
} }

View File

@ -1,769 +1,58 @@
package org.qortal.test.crosschain.dogecoinv3; package org.qortal.test.crosschain.dogecoinv3;
import com.google.common.hash.HashCode; import com.google.common.hash.HashCode;
import com.google.common.primitives.Bytes; import org.qortal.crosschain.ACCT;
import org.junit.Before;
import org.junit.Test;
import org.qortal.account.Account;
import org.qortal.account.PrivateKeyAccount;
import org.qortal.asset.Asset;
import org.qortal.block.Block;
import org.qortal.crosschain.AcctMode;
import org.qortal.crosschain.DogecoinACCTv3; import org.qortal.crosschain.DogecoinACCTv3;
import org.qortal.crypto.Crypto; import org.qortal.test.crosschain.ACCTTests;
import org.qortal.data.at.ATData;
import org.qortal.data.at.ATStateData;
import org.qortal.data.crosschain.CrossChainTradeData;
import org.qortal.data.transaction.BaseTransactionData;
import org.qortal.data.transaction.DeployAtTransactionData;
import org.qortal.data.transaction.MessageTransactionData;
import org.qortal.data.transaction.TransactionData;
import org.qortal.group.Group;
import org.qortal.repository.DataException;
import org.qortal.repository.Repository;
import org.qortal.repository.RepositoryManager;
import org.qortal.test.common.BlockUtils;
import org.qortal.test.common.Common;
import org.qortal.test.common.TransactionUtils;
import org.qortal.transaction.DeployAtTransaction;
import org.qortal.transaction.MessageTransaction;
import org.qortal.utils.Amounts;
import java.time.Instant; public class DogecoinACCTv3Tests extends ACCTTests {
import java.time.LocalDateTime;
import java.time.ZoneOffset;
import java.time.format.DateTimeFormatter;
import java.time.format.FormatStyle;
import java.util.Arrays;
import java.util.List;
import java.util.Random;
import java.util.function.Function;
import static org.junit.Assert.*;
public class DogecoinACCTv3Tests extends Common {
public static final byte[] secretA = "This string is exactly 32 bytes!".getBytes();
public static final byte[] hashOfSecretA = Crypto.hash160(secretA); // daf59884b4d1aec8c1b17102530909ee43c0151a
public static final byte[] dogecoinPublicKeyHash = HashCode.fromString("bb00bb11bb22bb33bb44bb55bb66bb77bb88bb99").asBytes(); public static final byte[] dogecoinPublicKeyHash = HashCode.fromString("bb00bb11bb22bb33bb44bb55bb66bb77bb88bb99").asBytes();
public static final int tradeTimeout = 20; // blocks private static final String SYMBOL = "DOGE";
public static final long redeemAmount = 80_40200000L; private static final String NAME = "Dogecoin";
public static final long fundingAmount = 123_45600000L;
public static final long dogecoinAmount = 864200L; // 0.00864200 DOGE
private static final Random RANDOM = new Random(); @Override
protected byte[] getPublicKey() {
@Before return dogecoinPublicKeyHash;
public void beforeTest() throws DataException {
Common.useDefaultSettings();
} }
@Test @Override
public void testCompile() { protected byte[] buildQortalAT(String address, byte[] publicKey, long redeemAmount, long foreignAmount, int tradeTimeout) {
PrivateKeyAccount tradeAccount = createTradeAccount(null); return DogecoinACCTv3.buildQortalAT(address, publicKey, redeemAmount, foreignAmount, tradeTimeout);
byte[] creationBytes = DogecoinACCTv3.buildQortalAT(tradeAccount.getAddress(), dogecoinPublicKeyHash, redeemAmount, dogecoinAmount, tradeTimeout);
assertNotNull(creationBytes);
System.out.println("AT creation bytes: " + HashCode.fromBytes(creationBytes).toString());
} }
@Test @Override
public void testDeploy() throws DataException { protected ACCT getInstance() {
try (final Repository repository = RepositoryManager.getRepository()) { return DogecoinACCTv3.getInstance();
PrivateKeyAccount deployer = Common.getTestAccount(repository, "chloe");
PrivateKeyAccount tradeAccount = createTradeAccount(repository);
PrivateKeyAccount partner = Common.getTestAccount(repository, "dilbert");
long deployersInitialBalance = deployer.getConfirmedBalance(Asset.QORT);
long partnersInitialBalance = partner.getConfirmedBalance(Asset.QORT);
DeployAtTransaction deployAtTransaction = doDeploy(repository, deployer, tradeAccount.getAddress());
long expectedBalance = deployersInitialBalance - fundingAmount - deployAtTransaction.getTransactionData().getFee();
long actualBalance = deployer.getConfirmedBalance(Asset.QORT);
assertEquals("Deployer's post-deployment balance incorrect", expectedBalance, actualBalance);
expectedBalance = fundingAmount;
actualBalance = deployAtTransaction.getATAccount().getConfirmedBalance(Asset.QORT);
assertEquals("AT's post-deployment balance incorrect", expectedBalance, actualBalance);
expectedBalance = partnersInitialBalance;
actualBalance = partner.getConfirmedBalance(Asset.QORT);
assertEquals("Partner's post-deployment balance incorrect", expectedBalance, actualBalance);
// Test orphaning
BlockUtils.orphanLastBlock(repository);
expectedBalance = deployersInitialBalance;
actualBalance = deployer.getConfirmedBalance(Asset.QORT);
assertEquals("Deployer's post-orphan/pre-deployment balance incorrect", expectedBalance, actualBalance);
expectedBalance = 0;
actualBalance = deployAtTransaction.getATAccount().getConfirmedBalance(Asset.QORT);
assertEquals("AT's post-orphan/pre-deployment balance incorrect", expectedBalance, actualBalance);
expectedBalance = partnersInitialBalance;
actualBalance = partner.getConfirmedBalance(Asset.QORT);
assertEquals("Partner's post-orphan/pre-deployment balance incorrect", expectedBalance, actualBalance);
}
} }
@SuppressWarnings("unused") @Override
@Test protected int calcRefundTimeout(long partnersOfferMessageTransactionTimestamp, int lockTimeA) {
public void testOfferCancel() throws DataException { return DogecoinACCTv3.calcRefundTimeout(partnersOfferMessageTransactionTimestamp, lockTimeA);
try (final Repository repository = RepositoryManager.getRepository()) {
PrivateKeyAccount deployer = Common.getTestAccount(repository, "chloe");
PrivateKeyAccount tradeAccount = createTradeAccount(repository);
PrivateKeyAccount partner = Common.getTestAccount(repository, "dilbert");
long deployersInitialBalance = deployer.getConfirmedBalance(Asset.QORT);
long partnersInitialBalance = partner.getConfirmedBalance(Asset.QORT);
DeployAtTransaction deployAtTransaction = doDeploy(repository, deployer, tradeAccount.getAddress());
Account at = deployAtTransaction.getATAccount();
String atAddress = at.getAddress();
long deployAtFee = deployAtTransaction.getTransactionData().getFee();
long deployersPostDeploymentBalance = deployersInitialBalance - fundingAmount - deployAtFee;
// Send creator's address to AT, instead of typical partner's address
byte[] messageData = DogecoinACCTv3.getInstance().buildCancelMessage(deployer.getAddress());
MessageTransaction messageTransaction = sendMessage(repository, deployer, messageData, atAddress);
long messageFee = messageTransaction.getTransactionData().getFee();
// AT should process 'cancel' message in next block
BlockUtils.mintBlock(repository);
describeAt(repository, atAddress);
// Check AT is finished
ATData atData = repository.getATRepository().fromATAddress(atAddress);
assertTrue(atData.getIsFinished());
// AT should be in CANCELLED mode
CrossChainTradeData tradeData = DogecoinACCTv3.getInstance().populateTradeData(repository, atData);
assertEquals(AcctMode.CANCELLED, tradeData.mode);
// Check balances
long expectedMinimumBalance = deployersPostDeploymentBalance;
long expectedMaximumBalance = deployersInitialBalance - deployAtFee - messageFee;
long actualBalance = deployer.getConfirmedBalance(Asset.QORT);
assertTrue(String.format("Deployer's balance %s should be above minimum %s", actualBalance, expectedMinimumBalance), actualBalance > expectedMinimumBalance);
assertTrue(String.format("Deployer's balance %s should be below maximum %s", actualBalance, expectedMaximumBalance), actualBalance < expectedMaximumBalance);
// Test orphaning
BlockUtils.orphanLastBlock(repository);
// Check balances
long expectedBalance = deployersPostDeploymentBalance - messageFee;
actualBalance = deployer.getConfirmedBalance(Asset.QORT);
assertEquals("Deployer's post-orphan/pre-refund balance incorrect", expectedBalance, actualBalance);
}
} }
@SuppressWarnings("unused") @Override
@Test protected byte[] buildTradeMessage(String address, byte[] publicKey, byte[] hashOfSecretA, int lockTimeA, int refundTimeout) {
public void testOfferCancelInvalidLength() throws DataException { return DogecoinACCTv3.buildTradeMessage(address, publicKey, hashOfSecretA, lockTimeA, refundTimeout);
try (final Repository repository = RepositoryManager.getRepository()) {
PrivateKeyAccount deployer = Common.getTestAccount(repository, "chloe");
PrivateKeyAccount tradeAccount = createTradeAccount(repository);
PrivateKeyAccount partner = Common.getTestAccount(repository, "dilbert");
long deployersInitialBalance = deployer.getConfirmedBalance(Asset.QORT);
long partnersInitialBalance = partner.getConfirmedBalance(Asset.QORT);
DeployAtTransaction deployAtTransaction = doDeploy(repository, deployer, tradeAccount.getAddress());
Account at = deployAtTransaction.getATAccount();
String atAddress = at.getAddress();
long deployAtFee = deployAtTransaction.getTransactionData().getFee();
long deployersPostDeploymentBalance = deployersInitialBalance - fundingAmount - deployAtFee;
// Instead of sending creator's address to AT, send too-short/invalid message
byte[] messageData = new byte[7];
RANDOM.nextBytes(messageData);
MessageTransaction messageTransaction = sendMessage(repository, deployer, messageData, atAddress);
long messageFee = messageTransaction.getTransactionData().getFee();
// AT should process 'cancel' message in next block
// As message is too short, it will be padded to 32bytes but cancel code doesn't care about message content, so should be ok
BlockUtils.mintBlock(repository);
describeAt(repository, atAddress);
// Check AT is finished
ATData atData = repository.getATRepository().fromATAddress(atAddress);
assertTrue(atData.getIsFinished());
// AT should be in CANCELLED mode
CrossChainTradeData tradeData = DogecoinACCTv3.getInstance().populateTradeData(repository, atData);
assertEquals(AcctMode.CANCELLED, tradeData.mode);
}
} }
@SuppressWarnings("unused") @Override
@Test protected byte[] buildRedeemMessage(byte[] secretA, String address) {
public void testTradingInfoProcessing() throws DataException { return DogecoinACCTv3.buildRedeemMessage(secretA, address);
try (final Repository repository = RepositoryManager.getRepository()) {
PrivateKeyAccount deployer = Common.getTestAccount(repository, "chloe");
PrivateKeyAccount tradeAccount = createTradeAccount(repository);
PrivateKeyAccount partner = Common.getTestAccount(repository, "dilbert");
long deployersInitialBalance = deployer.getConfirmedBalance(Asset.QORT);
long partnersInitialBalance = partner.getConfirmedBalance(Asset.QORT);
DeployAtTransaction deployAtTransaction = doDeploy(repository, deployer, tradeAccount.getAddress());
Account at = deployAtTransaction.getATAccount();
String atAddress = at.getAddress();
long partnersOfferMessageTransactionTimestamp = System.currentTimeMillis();
int lockTimeA = calcTestLockTimeA(partnersOfferMessageTransactionTimestamp);
int refundTimeout = DogecoinACCTv3.calcRefundTimeout(partnersOfferMessageTransactionTimestamp, lockTimeA);
// Send trade info to AT
byte[] messageData = DogecoinACCTv3.buildTradeMessage(partner.getAddress(), dogecoinPublicKeyHash, hashOfSecretA, lockTimeA, refundTimeout);
MessageTransaction messageTransaction = sendMessage(repository, tradeAccount, messageData, atAddress);
Block postDeploymentBlock = BlockUtils.mintBlock(repository);
int postDeploymentBlockHeight = postDeploymentBlock.getBlockData().getHeight();
long deployAtFee = deployAtTransaction.getTransactionData().getFee();
long deployersPostDeploymentBalance = deployersInitialBalance - fundingAmount - deployAtFee;
describeAt(repository, atAddress);
ATData atData = repository.getATRepository().fromATAddress(atAddress);
CrossChainTradeData tradeData = DogecoinACCTv3.getInstance().populateTradeData(repository, atData);
// AT should be in TRADE mode
assertEquals(AcctMode.TRADING, tradeData.mode);
// Check hashOfSecretA was extracted correctly
assertTrue(Arrays.equals(hashOfSecretA, tradeData.hashOfSecretA));
// Check trade partner Qortal address was extracted correctly
assertEquals(partner.getAddress(), tradeData.qortalPartnerAddress);
// Check trade partner's dogecoin PKH was extracted correctly
assertTrue(Arrays.equals(dogecoinPublicKeyHash, tradeData.partnerForeignPKH));
// Test orphaning
BlockUtils.orphanToBlock(repository, postDeploymentBlockHeight);
// Check balances
long expectedBalance = deployersPostDeploymentBalance;
long actualBalance = deployer.getConfirmedBalance(Asset.QORT);
assertEquals("Deployer's post-orphan/pre-refund balance incorrect", expectedBalance, actualBalance);
}
} }
// TEST SENDING TRADING INFO BUT NOT FROM AT CREATOR (SHOULD BE IGNORED) @Override
@SuppressWarnings("unused") protected byte[] getCodeBytesHash() {
@Test return DogecoinACCTv3.CODE_BYTES_HASH;
public void testIncorrectTradeSender() throws DataException {
try (final Repository repository = RepositoryManager.getRepository()) {
PrivateKeyAccount deployer = Common.getTestAccount(repository, "chloe");
PrivateKeyAccount tradeAccount = createTradeAccount(repository);
PrivateKeyAccount partner = Common.getTestAccount(repository, "dilbert");
PrivateKeyAccount bystander = Common.getTestAccount(repository, "bob");
long deployersInitialBalance = deployer.getConfirmedBalance(Asset.QORT);
long partnersInitialBalance = partner.getConfirmedBalance(Asset.QORT);
DeployAtTransaction deployAtTransaction = doDeploy(repository, deployer, tradeAccount.getAddress());
Account at = deployAtTransaction.getATAccount();
String atAddress = at.getAddress();
long partnersOfferMessageTransactionTimestamp = System.currentTimeMillis();
int lockTimeA = calcTestLockTimeA(partnersOfferMessageTransactionTimestamp);
int refundTimeout = DogecoinACCTv3.calcRefundTimeout(partnersOfferMessageTransactionTimestamp, lockTimeA);
// Send trade info to AT BUT NOT FROM AT CREATOR
byte[] messageData = DogecoinACCTv3.buildTradeMessage(partner.getAddress(), dogecoinPublicKeyHash, hashOfSecretA, lockTimeA, refundTimeout);
MessageTransaction messageTransaction = sendMessage(repository, bystander, messageData, atAddress);
BlockUtils.mintBlock(repository);
long expectedBalance = partnersInitialBalance;
long actualBalance = partner.getConfirmedBalance(Asset.QORT);
assertEquals("Partner's post-initial-payout balance incorrect", expectedBalance, actualBalance);
describeAt(repository, atAddress);
ATData atData = repository.getATRepository().fromATAddress(atAddress);
CrossChainTradeData tradeData = DogecoinACCTv3.getInstance().populateTradeData(repository, atData);
// AT should still be in OFFER mode
assertEquals(AcctMode.OFFERING, tradeData.mode);
}
} }
@SuppressWarnings("unused") @Override
@Test protected String getSymbol() {
public void testAutomaticTradeRefund() throws DataException { return SYMBOL;
try (final Repository repository = RepositoryManager.getRepository()) {
PrivateKeyAccount deployer = Common.getTestAccount(repository, "chloe");
PrivateKeyAccount tradeAccount = createTradeAccount(repository);
PrivateKeyAccount partner = Common.getTestAccount(repository, "dilbert");
long deployersInitialBalance = deployer.getConfirmedBalance(Asset.QORT);
long partnersInitialBalance = partner.getConfirmedBalance(Asset.QORT);
DeployAtTransaction deployAtTransaction = doDeploy(repository, deployer, tradeAccount.getAddress());
Account at = deployAtTransaction.getATAccount();
String atAddress = at.getAddress();
long partnersOfferMessageTransactionTimestamp = System.currentTimeMillis();
int lockTimeA = calcTestLockTimeA(partnersOfferMessageTransactionTimestamp);
int refundTimeout = DogecoinACCTv3.calcRefundTimeout(partnersOfferMessageTransactionTimestamp, lockTimeA);
// Send trade info to AT
byte[] messageData = DogecoinACCTv3.buildTradeMessage(partner.getAddress(), dogecoinPublicKeyHash, hashOfSecretA, lockTimeA, refundTimeout);
MessageTransaction messageTransaction = sendMessage(repository, tradeAccount, messageData, atAddress);
Block postDeploymentBlock = BlockUtils.mintBlock(repository);
int postDeploymentBlockHeight = postDeploymentBlock.getBlockData().getHeight();
// Check refund
long deployAtFee = deployAtTransaction.getTransactionData().getFee();
long deployersPostDeploymentBalance = deployersInitialBalance - fundingAmount - deployAtFee;
checkTradeRefund(repository, deployer, deployersInitialBalance, deployAtFee);
describeAt(repository, atAddress);
// Check AT is finished
ATData atData = repository.getATRepository().fromATAddress(atAddress);
assertTrue(atData.getIsFinished());
// AT should be in REFUNDED mode
CrossChainTradeData tradeData = DogecoinACCTv3.getInstance().populateTradeData(repository, atData);
assertEquals(AcctMode.REFUNDED, tradeData.mode);
// Test orphaning
BlockUtils.orphanToBlock(repository, postDeploymentBlockHeight);
// Check balances
long expectedBalance = deployersPostDeploymentBalance;
long actualBalance = deployer.getConfirmedBalance(Asset.QORT);
assertEquals("Deployer's post-orphan/pre-refund balance incorrect", expectedBalance, actualBalance);
}
} }
@SuppressWarnings("unused") @Override
@Test protected String getName() {
public void testCorrectSecretCorrectSender() throws DataException { return NAME;
try (final Repository repository = RepositoryManager.getRepository()) {
PrivateKeyAccount deployer = Common.getTestAccount(repository, "chloe");
PrivateKeyAccount tradeAccount = createTradeAccount(repository);
PrivateKeyAccount partner = Common.getTestAccount(repository, "dilbert");
long deployersInitialBalance = deployer.getConfirmedBalance(Asset.QORT);
long partnersInitialBalance = partner.getConfirmedBalance(Asset.QORT);
DeployAtTransaction deployAtTransaction = doDeploy(repository, deployer, tradeAccount.getAddress());
Account at = deployAtTransaction.getATAccount();
String atAddress = at.getAddress();
long partnersOfferMessageTransactionTimestamp = System.currentTimeMillis();
int lockTimeA = calcTestLockTimeA(partnersOfferMessageTransactionTimestamp);
int refundTimeout = DogecoinACCTv3.calcRefundTimeout(partnersOfferMessageTransactionTimestamp, lockTimeA);
// Send trade info to AT
byte[] messageData = DogecoinACCTv3.buildTradeMessage(partner.getAddress(), dogecoinPublicKeyHash, hashOfSecretA, lockTimeA, refundTimeout);
MessageTransaction messageTransaction = sendMessage(repository, tradeAccount, messageData, atAddress);
// Give AT time to process message
BlockUtils.mintBlock(repository);
// Send correct secret to AT, from correct account
messageData = DogecoinACCTv3.buildRedeemMessage(secretA, partner.getAddress());
messageTransaction = sendMessage(repository, partner, messageData, atAddress);
// AT should send funds in the next block
ATStateData preRedeemAtStateData = repository.getATRepository().getLatestATState(atAddress);
BlockUtils.mintBlock(repository);
describeAt(repository, atAddress);
// Check AT is finished
ATData atData = repository.getATRepository().fromATAddress(atAddress);
assertTrue(atData.getIsFinished());
// AT should be in REDEEMED mode
CrossChainTradeData tradeData = DogecoinACCTv3.getInstance().populateTradeData(repository, atData);
assertEquals(AcctMode.REDEEMED, tradeData.mode);
// Check balances
long expectedBalance = partnersInitialBalance - messageTransaction.getTransactionData().getFee() + redeemAmount;
long actualBalance = partner.getConfirmedBalance(Asset.QORT);
assertEquals("Partner's post-redeem balance incorrect", expectedBalance, actualBalance);
// Orphan redeem
BlockUtils.orphanLastBlock(repository);
// Check balances
expectedBalance = partnersInitialBalance - messageTransaction.getTransactionData().getFee();
actualBalance = partner.getConfirmedBalance(Asset.QORT);
assertEquals("Partner's post-orphan/pre-redeem balance incorrect", expectedBalance, actualBalance);
// Check AT state
ATStateData postOrphanAtStateData = repository.getATRepository().getLatestATState(atAddress);
assertTrue("AT states mismatch", Arrays.equals(preRedeemAtStateData.getStateData(), postOrphanAtStateData.getStateData()));
}
} }
@SuppressWarnings("unused")
@Test
public void testCorrectSecretIncorrectSender() throws DataException {
try (final Repository repository = RepositoryManager.getRepository()) {
PrivateKeyAccount deployer = Common.getTestAccount(repository, "chloe");
PrivateKeyAccount tradeAccount = createTradeAccount(repository);
PrivateKeyAccount partner = Common.getTestAccount(repository, "dilbert");
PrivateKeyAccount bystander = Common.getTestAccount(repository, "bob");
long deployersInitialBalance = deployer.getConfirmedBalance(Asset.QORT);
long partnersInitialBalance = partner.getConfirmedBalance(Asset.QORT);
DeployAtTransaction deployAtTransaction = doDeploy(repository, deployer, tradeAccount.getAddress());
long deployAtFee = deployAtTransaction.getTransactionData().getFee();
Account at = deployAtTransaction.getATAccount();
String atAddress = at.getAddress();
long partnersOfferMessageTransactionTimestamp = System.currentTimeMillis();
int lockTimeA = calcTestLockTimeA(partnersOfferMessageTransactionTimestamp);
int refundTimeout = DogecoinACCTv3.calcRefundTimeout(partnersOfferMessageTransactionTimestamp, lockTimeA);
// Send trade info to AT
byte[] messageData = DogecoinACCTv3.buildTradeMessage(partner.getAddress(), dogecoinPublicKeyHash, hashOfSecretA, lockTimeA, refundTimeout);
MessageTransaction messageTransaction = sendMessage(repository, tradeAccount, messageData, atAddress);
// Give AT time to process message
BlockUtils.mintBlock(repository);
// Send correct secret to AT, but from wrong account
messageData = DogecoinACCTv3.buildRedeemMessage(secretA, partner.getAddress());
messageTransaction = sendMessage(repository, bystander, messageData, atAddress);
// AT should NOT send funds in the next block
ATStateData preRedeemAtStateData = repository.getATRepository().getLatestATState(atAddress);
BlockUtils.mintBlock(repository);
describeAt(repository, atAddress);
// Check AT is NOT finished
ATData atData = repository.getATRepository().fromATAddress(atAddress);
assertFalse(atData.getIsFinished());
// AT should still be in TRADE mode
CrossChainTradeData tradeData = DogecoinACCTv3.getInstance().populateTradeData(repository, atData);
assertEquals(AcctMode.TRADING, tradeData.mode);
// Check balances
long expectedBalance = partnersInitialBalance;
long actualBalance = partner.getConfirmedBalance(Asset.QORT);
assertEquals("Partner's balance incorrect", expectedBalance, actualBalance);
// Check eventual refund
checkTradeRefund(repository, deployer, deployersInitialBalance, deployAtFee);
}
}
@SuppressWarnings("unused")
@Test
public void testIncorrectSecretCorrectSender() throws DataException {
try (final Repository repository = RepositoryManager.getRepository()) {
PrivateKeyAccount deployer = Common.getTestAccount(repository, "chloe");
PrivateKeyAccount tradeAccount = createTradeAccount(repository);
PrivateKeyAccount partner = Common.getTestAccount(repository, "dilbert");
long deployersInitialBalance = deployer.getConfirmedBalance(Asset.QORT);
long partnersInitialBalance = partner.getConfirmedBalance(Asset.QORT);
DeployAtTransaction deployAtTransaction = doDeploy(repository, deployer, tradeAccount.getAddress());
long deployAtFee = deployAtTransaction.getTransactionData().getFee();
Account at = deployAtTransaction.getATAccount();
String atAddress = at.getAddress();
long partnersOfferMessageTransactionTimestamp = System.currentTimeMillis();
int lockTimeA = calcTestLockTimeA(partnersOfferMessageTransactionTimestamp);
int refundTimeout = DogecoinACCTv3.calcRefundTimeout(partnersOfferMessageTransactionTimestamp, lockTimeA);
// Send trade info to AT
byte[] messageData = DogecoinACCTv3.buildTradeMessage(partner.getAddress(), dogecoinPublicKeyHash, hashOfSecretA, lockTimeA, refundTimeout);
MessageTransaction messageTransaction = sendMessage(repository, tradeAccount, messageData, atAddress);
// Give AT time to process message
BlockUtils.mintBlock(repository);
// Send incorrect secret to AT, from correct account
byte[] wrongSecret = new byte[32];
RANDOM.nextBytes(wrongSecret);
messageData = DogecoinACCTv3.buildRedeemMessage(wrongSecret, partner.getAddress());
messageTransaction = sendMessage(repository, partner, messageData, atAddress);
// AT should NOT send funds in the next block
ATStateData preRedeemAtStateData = repository.getATRepository().getLatestATState(atAddress);
BlockUtils.mintBlock(repository);
describeAt(repository, atAddress);
// Check AT is NOT finished
ATData atData = repository.getATRepository().fromATAddress(atAddress);
assertFalse(atData.getIsFinished());
// AT should still be in TRADE mode
CrossChainTradeData tradeData = DogecoinACCTv3.getInstance().populateTradeData(repository, atData);
assertEquals(AcctMode.TRADING, tradeData.mode);
long expectedBalance = partnersInitialBalance - messageTransaction.getTransactionData().getFee();
long actualBalance = partner.getConfirmedBalance(Asset.QORT);
assertEquals("Partner's balance incorrect", expectedBalance, actualBalance);
// Check eventual refund
checkTradeRefund(repository, deployer, deployersInitialBalance, deployAtFee);
}
}
@SuppressWarnings("unused")
@Test
public void testCorrectSecretCorrectSenderInvalidMessageLength() throws DataException {
try (final Repository repository = RepositoryManager.getRepository()) {
PrivateKeyAccount deployer = Common.getTestAccount(repository, "chloe");
PrivateKeyAccount tradeAccount = createTradeAccount(repository);
PrivateKeyAccount partner = Common.getTestAccount(repository, "dilbert");
long deployersInitialBalance = deployer.getConfirmedBalance(Asset.QORT);
long partnersInitialBalance = partner.getConfirmedBalance(Asset.QORT);
DeployAtTransaction deployAtTransaction = doDeploy(repository, deployer, tradeAccount.getAddress());
Account at = deployAtTransaction.getATAccount();
String atAddress = at.getAddress();
long partnersOfferMessageTransactionTimestamp = System.currentTimeMillis();
int lockTimeA = calcTestLockTimeA(partnersOfferMessageTransactionTimestamp);
int refundTimeout = DogecoinACCTv3.calcRefundTimeout(partnersOfferMessageTransactionTimestamp, lockTimeA);
// Send trade info to AT
byte[] messageData = DogecoinACCTv3.buildTradeMessage(partner.getAddress(), dogecoinPublicKeyHash, hashOfSecretA, lockTimeA, refundTimeout);
MessageTransaction messageTransaction = sendMessage(repository, tradeAccount, messageData, atAddress);
// Give AT time to process message
BlockUtils.mintBlock(repository);
// Send correct secret to AT, from correct account, but missing receive address, hence incorrect length
messageData = Bytes.concat(secretA);
messageTransaction = sendMessage(repository, partner, messageData, atAddress);
// AT should NOT send funds in the next block
ATStateData preRedeemAtStateData = repository.getATRepository().getLatestATState(atAddress);
BlockUtils.mintBlock(repository);
describeAt(repository, atAddress);
// Check AT is NOT finished
ATData atData = repository.getATRepository().fromATAddress(atAddress);
assertFalse(atData.getIsFinished());
// AT should be in TRADING mode
CrossChainTradeData tradeData = DogecoinACCTv3.getInstance().populateTradeData(repository, atData);
assertEquals(AcctMode.TRADING, tradeData.mode);
}
}
@SuppressWarnings("unused")
@Test
public void testDescribeDeployed() throws DataException {
try (final Repository repository = RepositoryManager.getRepository()) {
PrivateKeyAccount deployer = Common.getTestAccount(repository, "chloe");
PrivateKeyAccount tradeAccount = createTradeAccount(repository);
PrivateKeyAccount partner = Common.getTestAccount(repository, "dilbert");
long deployersInitialBalance = deployer.getConfirmedBalance(Asset.QORT);
long partnersInitialBalance = partner.getConfirmedBalance(Asset.QORT);
DeployAtTransaction deployAtTransaction = doDeploy(repository, deployer, tradeAccount.getAddress());
List<ATData> executableAts = repository.getATRepository().getAllExecutableATs();
for (ATData atData : executableAts) {
String atAddress = atData.getATAddress();
byte[] codeBytes = atData.getCodeBytes();
byte[] codeHash = Crypto.digest(codeBytes);
System.out.println(String.format("%s: code length: %d byte%s, code hash: %s",
atAddress,
codeBytes.length,
(codeBytes.length != 1 ? "s": ""),
HashCode.fromBytes(codeHash)));
// Not one of ours?
if (!Arrays.equals(codeHash, DogecoinACCTv3.CODE_BYTES_HASH))
continue;
describeAt(repository, atAddress);
}
}
}
private int calcTestLockTimeA(long messageTimestamp) {
return (int) (messageTimestamp / 1000L + tradeTimeout * 60);
}
private DeployAtTransaction doDeploy(Repository repository, PrivateKeyAccount deployer, String tradeAddress) throws DataException {
byte[] creationBytes = DogecoinACCTv3.buildQortalAT(tradeAddress, dogecoinPublicKeyHash, redeemAmount, dogecoinAmount, tradeTimeout);
long txTimestamp = System.currentTimeMillis();
byte[] lastReference = deployer.getLastReference();
if (lastReference == null) {
System.err.println(String.format("Qortal account %s has no last reference", deployer.getAddress()));
System.exit(2);
}
Long fee = null;
String name = "QORT-DOGE cross-chain trade";
String description = String.format("Qortal-Dogecoin cross-chain trade");
String atType = "ACCT";
String tags = "QORT-DOGE ACCT";
BaseTransactionData baseTransactionData = new BaseTransactionData(txTimestamp, Group.NO_GROUP, lastReference, deployer.getPublicKey(), fee, null);
TransactionData deployAtTransactionData = new DeployAtTransactionData(baseTransactionData, name, description, atType, tags, creationBytes, fundingAmount, Asset.QORT);
DeployAtTransaction deployAtTransaction = new DeployAtTransaction(repository, deployAtTransactionData);
fee = deployAtTransaction.calcRecommendedFee();
deployAtTransactionData.setFee(fee);
TransactionUtils.signAndMint(repository, deployAtTransactionData, deployer);
return deployAtTransaction;
}
private MessageTransaction sendMessage(Repository repository, PrivateKeyAccount sender, byte[] data, String recipient) throws DataException {
long txTimestamp = System.currentTimeMillis();
byte[] lastReference = sender.getLastReference();
if (lastReference == null) {
System.err.println(String.format("Qortal account %s has no last reference", sender.getAddress()));
System.exit(2);
}
Long fee = null;
int version = 4;
int nonce = 0;
long amount = 0;
Long assetId = null; // because amount is zero
BaseTransactionData baseTransactionData = new BaseTransactionData(txTimestamp, Group.NO_GROUP, lastReference, sender.getPublicKey(), fee, null);
TransactionData messageTransactionData = new MessageTransactionData(baseTransactionData, version, nonce, recipient, amount, assetId, data, false, false);
MessageTransaction messageTransaction = new MessageTransaction(repository, messageTransactionData);
fee = messageTransaction.calcRecommendedFee();
messageTransactionData.setFee(fee);
TransactionUtils.signAndMint(repository, messageTransactionData, sender);
return messageTransaction;
}
private void checkTradeRefund(Repository repository, Account deployer, long deployersInitialBalance, long deployAtFee) throws DataException {
long deployersPostDeploymentBalance = deployersInitialBalance - fundingAmount - deployAtFee;
int refundTimeout = tradeTimeout / 2 + 1; // close enough
// AT should automatically refund deployer after 'refundTimeout' blocks
for (int blockCount = 0; blockCount <= refundTimeout; ++blockCount)
BlockUtils.mintBlock(repository);
// We don't bother to exactly calculate QORT spent running AT for several blocks, but we do know the expected range
long expectedMinimumBalance = deployersPostDeploymentBalance;
long expectedMaximumBalance = deployersInitialBalance - deployAtFee;
long actualBalance = deployer.getConfirmedBalance(Asset.QORT);
assertTrue(String.format("Deployer's balance %s should be above minimum %s", actualBalance, expectedMinimumBalance), actualBalance > expectedMinimumBalance);
assertTrue(String.format("Deployer's balance %s should be below maximum %s", actualBalance, expectedMaximumBalance), actualBalance < expectedMaximumBalance);
}
private void describeAt(Repository repository, String atAddress) throws DataException {
ATData atData = repository.getATRepository().fromATAddress(atAddress);
CrossChainTradeData tradeData = DogecoinACCTv3.getInstance().populateTradeData(repository, atData);
Function<Long, String> epochMilliFormatter = (timestamp) -> LocalDateTime.ofInstant(Instant.ofEpochMilli(timestamp), ZoneOffset.UTC).format(DateTimeFormatter.ofLocalizedDateTime(FormatStyle.MEDIUM));
int currentBlockHeight = repository.getBlockRepository().getBlockchainHeight();
System.out.print(String.format("%s:\n"
+ "\tmode: %s\n"
+ "\tcreator: %s,\n"
+ "\tcreation timestamp: %s,\n"
+ "\tcurrent balance: %s QORT,\n"
+ "\tis finished: %b,\n"
+ "\tredeem payout: %s QORT,\n"
+ "\texpected dogecoin: %s DOGE,\n"
+ "\tcurrent block height: %d,\n",
tradeData.qortalAtAddress,
tradeData.mode,
tradeData.qortalCreator,
epochMilliFormatter.apply(tradeData.creationTimestamp),
Amounts.prettyAmount(tradeData.qortBalance),
atData.getIsFinished(),
Amounts.prettyAmount(tradeData.qortAmount),
Amounts.prettyAmount(tradeData.expectedForeignAmount),
currentBlockHeight));
if (tradeData.mode != AcctMode.OFFERING && tradeData.mode != AcctMode.CANCELLED) {
System.out.println(String.format("\trefund timeout: %d minutes,\n"
+ "\trefund height: block %d,\n"
+ "\tHASH160 of secret-A: %s,\n"
+ "\tDogecoin P2SH-A nLockTime: %d (%s),\n"
+ "\ttrade partner: %s\n"
+ "\tpartner's receiving address: %s",
tradeData.refundTimeout,
tradeData.tradeRefundHeight,
HashCode.fromBytes(tradeData.hashOfSecretA).toString().substring(0, 40),
tradeData.lockTimeA, epochMilliFormatter.apply(tradeData.lockTimeA * 1000L),
tradeData.qortalPartnerAddress,
tradeData.qortalPartnerReceivingAddress));
}
}
private PrivateKeyAccount createTradeAccount(Repository repository) {
// We actually use a known test account with funds to avoid PoW compute
return Common.getTestAccount(repository, "alice");
}
} }

View File

@ -1,770 +1,60 @@
package org.qortal.test.crosschain.litecoinv1; package org.qortal.test.crosschain.litecoinv1;
import static org.junit.Assert.*; import org.qortal.crosschain.ACCT;
import java.time.Instant;
import java.time.LocalDateTime;
import java.time.ZoneOffset;
import java.time.format.DateTimeFormatter;
import java.time.format.FormatStyle;
import java.util.Arrays;
import java.util.List;
import java.util.Random;
import java.util.function.Function;
import org.junit.Before;
import org.junit.Test;
import org.qortal.account.Account;
import org.qortal.account.PrivateKeyAccount;
import org.qortal.asset.Asset;
import org.qortal.block.Block;
import org.qortal.crosschain.LitecoinACCTv1; import org.qortal.crosschain.LitecoinACCTv1;
import org.qortal.crosschain.AcctMode; import org.qortal.test.crosschain.ACCTTests;
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.transaction.BaseTransactionData;
import org.qortal.data.transaction.DeployAtTransactionData;
import org.qortal.data.transaction.MessageTransactionData;
import org.qortal.data.transaction.TransactionData;
import org.qortal.group.Group;
import org.qortal.repository.DataException;
import org.qortal.repository.Repository;
import org.qortal.repository.RepositoryManager;
import org.qortal.test.common.BlockUtils;
import org.qortal.test.common.Common;
import org.qortal.test.common.TransactionUtils;
import org.qortal.transaction.DeployAtTransaction;
import org.qortal.transaction.MessageTransaction;
import org.qortal.utils.Amounts;
import com.google.common.hash.HashCode; import com.google.common.hash.HashCode;
import com.google.common.primitives.Bytes;
public class LitecoinACCTv1Tests extends Common { public class LitecoinACCTv1Tests extends ACCTTests {
public static final byte[] secretA = "This string is exactly 32 bytes!".getBytes();
public static final byte[] hashOfSecretA = Crypto.hash160(secretA); // daf59884b4d1aec8c1b17102530909ee43c0151a
public static final byte[] litecoinPublicKeyHash = HashCode.fromString("bb00bb11bb22bb33bb44bb55bb66bb77bb88bb99").asBytes(); public static final byte[] litecoinPublicKeyHash = HashCode.fromString("bb00bb11bb22bb33bb44bb55bb66bb77bb88bb99").asBytes();
public static final int tradeTimeout = 20; // blocks private static final String SYMBOL = "LTC";
public static final long redeemAmount = 80_40200000L;
public static final long fundingAmount = 123_45600000L;
public static final long litecoinAmount = 864200L; // 0.00864200 LTC
private static final Random RANDOM = new Random(); private static final String NAME = "Litecoin";
@Before @Override
public void beforeTest() throws DataException { protected byte[] getPublicKey() {
Common.useDefaultSettings(); return litecoinPublicKeyHash;
} }
@Test @Override
public void testCompile() { protected byte[] buildQortalAT(String address, byte[] publicKey, long redeemAmount, long foreignAmount, int tradeTimeout) {
PrivateKeyAccount tradeAccount = createTradeAccount(null); return LitecoinACCTv1.buildQortalAT(address, publicKey, redeemAmount, foreignAmount, tradeTimeout);
byte[] creationBytes = LitecoinACCTv1.buildQortalAT(tradeAccount.getAddress(), litecoinPublicKeyHash, redeemAmount, litecoinAmount, tradeTimeout);
assertNotNull(creationBytes);
System.out.println("AT creation bytes: " + HashCode.fromBytes(creationBytes).toString());
} }
@Test @Override
public void testDeploy() throws DataException { protected ACCT getInstance() {
try (final Repository repository = RepositoryManager.getRepository()) { return LitecoinACCTv1.getInstance();
PrivateKeyAccount deployer = Common.getTestAccount(repository, "chloe");
PrivateKeyAccount tradeAccount = createTradeAccount(repository);
PrivateKeyAccount partner = Common.getTestAccount(repository, "dilbert");
long deployersInitialBalance = deployer.getConfirmedBalance(Asset.QORT);
long partnersInitialBalance = partner.getConfirmedBalance(Asset.QORT);
DeployAtTransaction deployAtTransaction = doDeploy(repository, deployer, tradeAccount.getAddress());
long expectedBalance = deployersInitialBalance - fundingAmount - deployAtTransaction.getTransactionData().getFee();
long actualBalance = deployer.getConfirmedBalance(Asset.QORT);
assertEquals("Deployer's post-deployment balance incorrect", expectedBalance, actualBalance);
expectedBalance = fundingAmount;
actualBalance = deployAtTransaction.getATAccount().getConfirmedBalance(Asset.QORT);
assertEquals("AT's post-deployment balance incorrect", expectedBalance, actualBalance);
expectedBalance = partnersInitialBalance;
actualBalance = partner.getConfirmedBalance(Asset.QORT);
assertEquals("Partner's post-deployment balance incorrect", expectedBalance, actualBalance);
// Test orphaning
BlockUtils.orphanLastBlock(repository);
expectedBalance = deployersInitialBalance;
actualBalance = deployer.getConfirmedBalance(Asset.QORT);
assertEquals("Deployer's post-orphan/pre-deployment balance incorrect", expectedBalance, actualBalance);
expectedBalance = 0;
actualBalance = deployAtTransaction.getATAccount().getConfirmedBalance(Asset.QORT);
assertEquals("AT's post-orphan/pre-deployment balance incorrect", expectedBalance, actualBalance);
expectedBalance = partnersInitialBalance;
actualBalance = partner.getConfirmedBalance(Asset.QORT);
assertEquals("Partner's post-orphan/pre-deployment balance incorrect", expectedBalance, actualBalance);
}
} }
@SuppressWarnings("unused") @Override
@Test protected int calcRefundTimeout(long partnersOfferMessageTransactionTimestamp, int lockTimeA) {
public void testOfferCancel() throws DataException { return LitecoinACCTv1.calcRefundTimeout(partnersOfferMessageTransactionTimestamp, lockTimeA);
try (final Repository repository = RepositoryManager.getRepository()) {
PrivateKeyAccount deployer = Common.getTestAccount(repository, "chloe");
PrivateKeyAccount tradeAccount = createTradeAccount(repository);
PrivateKeyAccount partner = Common.getTestAccount(repository, "dilbert");
long deployersInitialBalance = deployer.getConfirmedBalance(Asset.QORT);
long partnersInitialBalance = partner.getConfirmedBalance(Asset.QORT);
DeployAtTransaction deployAtTransaction = doDeploy(repository, deployer, tradeAccount.getAddress());
Account at = deployAtTransaction.getATAccount();
String atAddress = at.getAddress();
long deployAtFee = deployAtTransaction.getTransactionData().getFee();
long deployersPostDeploymentBalance = deployersInitialBalance - fundingAmount - deployAtFee;
// Send creator's address to AT, instead of typical partner's address
byte[] messageData = LitecoinACCTv1.getInstance().buildCancelMessage(deployer.getAddress());
MessageTransaction messageTransaction = sendMessage(repository, deployer, messageData, atAddress);
long messageFee = messageTransaction.getTransactionData().getFee();
// AT should process 'cancel' message in next block
BlockUtils.mintBlock(repository);
describeAt(repository, atAddress);
// Check AT is finished
ATData atData = repository.getATRepository().fromATAddress(atAddress);
assertTrue(atData.getIsFinished());
// AT should be in CANCELLED mode
CrossChainTradeData tradeData = LitecoinACCTv1.getInstance().populateTradeData(repository, atData);
assertEquals(AcctMode.CANCELLED, tradeData.mode);
// Check balances
long expectedMinimumBalance = deployersPostDeploymentBalance;
long expectedMaximumBalance = deployersInitialBalance - deployAtFee - messageFee;
long actualBalance = deployer.getConfirmedBalance(Asset.QORT);
assertTrue(String.format("Deployer's balance %s should be above minimum %s", actualBalance, expectedMinimumBalance), actualBalance > expectedMinimumBalance);
assertTrue(String.format("Deployer's balance %s should be below maximum %s", actualBalance, expectedMaximumBalance), actualBalance < expectedMaximumBalance);
// Test orphaning
BlockUtils.orphanLastBlock(repository);
// Check balances
long expectedBalance = deployersPostDeploymentBalance - messageFee;
actualBalance = deployer.getConfirmedBalance(Asset.QORT);
assertEquals("Deployer's post-orphan/pre-refund balance incorrect", expectedBalance, actualBalance);
}
} }
@SuppressWarnings("unused") @Override
@Test protected byte[] buildTradeMessage(String address, byte[] publicKey, byte[] hashOfSecretA, int lockTimeA, int refundTimeout) {
public void testOfferCancelInvalidLength() throws DataException { return LitecoinACCTv1.buildTradeMessage(address, publicKey, hashOfSecretA, lockTimeA, refundTimeout);
try (final Repository repository = RepositoryManager.getRepository()) {
PrivateKeyAccount deployer = Common.getTestAccount(repository, "chloe");
PrivateKeyAccount tradeAccount = createTradeAccount(repository);
PrivateKeyAccount partner = Common.getTestAccount(repository, "dilbert");
long deployersInitialBalance = deployer.getConfirmedBalance(Asset.QORT);
long partnersInitialBalance = partner.getConfirmedBalance(Asset.QORT);
DeployAtTransaction deployAtTransaction = doDeploy(repository, deployer, tradeAccount.getAddress());
Account at = deployAtTransaction.getATAccount();
String atAddress = at.getAddress();
long deployAtFee = deployAtTransaction.getTransactionData().getFee();
long deployersPostDeploymentBalance = deployersInitialBalance - fundingAmount - deployAtFee;
// Instead of sending creator's address to AT, send too-short/invalid message
byte[] messageData = new byte[7];
RANDOM.nextBytes(messageData);
MessageTransaction messageTransaction = sendMessage(repository, deployer, messageData, atAddress);
long messageFee = messageTransaction.getTransactionData().getFee();
// AT should process 'cancel' message in next block
// As message is too short, it will be padded to 32bytes but cancel code doesn't care about message content, so should be ok
BlockUtils.mintBlock(repository);
describeAt(repository, atAddress);
// Check AT is finished
ATData atData = repository.getATRepository().fromATAddress(atAddress);
assertTrue(atData.getIsFinished());
// AT should be in CANCELLED mode
CrossChainTradeData tradeData = LitecoinACCTv1.getInstance().populateTradeData(repository, atData);
assertEquals(AcctMode.CANCELLED, tradeData.mode);
}
} }
@SuppressWarnings("unused") @Override
@Test protected byte[] buildRedeemMessage(byte[] secretA, String address) {
public void testTradingInfoProcessing() throws DataException { return LitecoinACCTv1.buildRedeemMessage(secretA, address);
try (final Repository repository = RepositoryManager.getRepository()) {
PrivateKeyAccount deployer = Common.getTestAccount(repository, "chloe");
PrivateKeyAccount tradeAccount = createTradeAccount(repository);
PrivateKeyAccount partner = Common.getTestAccount(repository, "dilbert");
long deployersInitialBalance = deployer.getConfirmedBalance(Asset.QORT);
long partnersInitialBalance = partner.getConfirmedBalance(Asset.QORT);
DeployAtTransaction deployAtTransaction = doDeploy(repository, deployer, tradeAccount.getAddress());
Account at = deployAtTransaction.getATAccount();
String atAddress = at.getAddress();
long partnersOfferMessageTransactionTimestamp = System.currentTimeMillis();
int lockTimeA = calcTestLockTimeA(partnersOfferMessageTransactionTimestamp);
int refundTimeout = LitecoinACCTv1.calcRefundTimeout(partnersOfferMessageTransactionTimestamp, lockTimeA);
// Send trade info to AT
byte[] messageData = LitecoinACCTv1.buildTradeMessage(partner.getAddress(), litecoinPublicKeyHash, hashOfSecretA, lockTimeA, refundTimeout);
MessageTransaction messageTransaction = sendMessage(repository, tradeAccount, messageData, atAddress);
Block postDeploymentBlock = BlockUtils.mintBlock(repository);
int postDeploymentBlockHeight = postDeploymentBlock.getBlockData().getHeight();
long deployAtFee = deployAtTransaction.getTransactionData().getFee();
long deployersPostDeploymentBalance = deployersInitialBalance - fundingAmount - deployAtFee;
describeAt(repository, atAddress);
ATData atData = repository.getATRepository().fromATAddress(atAddress);
CrossChainTradeData tradeData = LitecoinACCTv1.getInstance().populateTradeData(repository, atData);
// AT should be in TRADE mode
assertEquals(AcctMode.TRADING, tradeData.mode);
// Check hashOfSecretA was extracted correctly
assertTrue(Arrays.equals(hashOfSecretA, tradeData.hashOfSecretA));
// Check trade partner Qortal address was extracted correctly
assertEquals(partner.getAddress(), tradeData.qortalPartnerAddress);
// Check trade partner's Litecoin PKH was extracted correctly
assertTrue(Arrays.equals(litecoinPublicKeyHash, tradeData.partnerForeignPKH));
// Test orphaning
BlockUtils.orphanToBlock(repository, postDeploymentBlockHeight);
// Check balances
long expectedBalance = deployersPostDeploymentBalance;
long actualBalance = deployer.getConfirmedBalance(Asset.QORT);
assertEquals("Deployer's post-orphan/pre-refund balance incorrect", expectedBalance, actualBalance);
}
} }
// TEST SENDING TRADING INFO BUT NOT FROM AT CREATOR (SHOULD BE IGNORED) @Override
@SuppressWarnings("unused") protected byte[] getCodeBytesHash() {
@Test return LitecoinACCTv1.CODE_BYTES_HASH;
public void testIncorrectTradeSender() throws DataException {
try (final Repository repository = RepositoryManager.getRepository()) {
PrivateKeyAccount deployer = Common.getTestAccount(repository, "chloe");
PrivateKeyAccount tradeAccount = createTradeAccount(repository);
PrivateKeyAccount partner = Common.getTestAccount(repository, "dilbert");
PrivateKeyAccount bystander = Common.getTestAccount(repository, "bob");
long deployersInitialBalance = deployer.getConfirmedBalance(Asset.QORT);
long partnersInitialBalance = partner.getConfirmedBalance(Asset.QORT);
DeployAtTransaction deployAtTransaction = doDeploy(repository, deployer, tradeAccount.getAddress());
Account at = deployAtTransaction.getATAccount();
String atAddress = at.getAddress();
long partnersOfferMessageTransactionTimestamp = System.currentTimeMillis();
int lockTimeA = calcTestLockTimeA(partnersOfferMessageTransactionTimestamp);
int refundTimeout = LitecoinACCTv1.calcRefundTimeout(partnersOfferMessageTransactionTimestamp, lockTimeA);
// Send trade info to AT BUT NOT FROM AT CREATOR
byte[] messageData = LitecoinACCTv1.buildTradeMessage(partner.getAddress(), litecoinPublicKeyHash, hashOfSecretA, lockTimeA, refundTimeout);
MessageTransaction messageTransaction = sendMessage(repository, bystander, messageData, atAddress);
BlockUtils.mintBlock(repository);
long expectedBalance = partnersInitialBalance;
long actualBalance = partner.getConfirmedBalance(Asset.QORT);
assertEquals("Partner's post-initial-payout balance incorrect", expectedBalance, actualBalance);
describeAt(repository, atAddress);
ATData atData = repository.getATRepository().fromATAddress(atAddress);
CrossChainTradeData tradeData = LitecoinACCTv1.getInstance().populateTradeData(repository, atData);
// AT should still be in OFFER mode
assertEquals(AcctMode.OFFERING, tradeData.mode);
}
} }
@SuppressWarnings("unused") @Override
@Test protected String getSymbol() {
public void testAutomaticTradeRefund() throws DataException { return SYMBOL;
try (final Repository repository = RepositoryManager.getRepository()) {
PrivateKeyAccount deployer = Common.getTestAccount(repository, "chloe");
PrivateKeyAccount tradeAccount = createTradeAccount(repository);
PrivateKeyAccount partner = Common.getTestAccount(repository, "dilbert");
long deployersInitialBalance = deployer.getConfirmedBalance(Asset.QORT);
long partnersInitialBalance = partner.getConfirmedBalance(Asset.QORT);
DeployAtTransaction deployAtTransaction = doDeploy(repository, deployer, tradeAccount.getAddress());
Account at = deployAtTransaction.getATAccount();
String atAddress = at.getAddress();
long partnersOfferMessageTransactionTimestamp = System.currentTimeMillis();
int lockTimeA = calcTestLockTimeA(partnersOfferMessageTransactionTimestamp);
int refundTimeout = LitecoinACCTv1.calcRefundTimeout(partnersOfferMessageTransactionTimestamp, lockTimeA);
// Send trade info to AT
byte[] messageData = LitecoinACCTv1.buildTradeMessage(partner.getAddress(), litecoinPublicKeyHash, hashOfSecretA, lockTimeA, refundTimeout);
MessageTransaction messageTransaction = sendMessage(repository, tradeAccount, messageData, atAddress);
Block postDeploymentBlock = BlockUtils.mintBlock(repository);
int postDeploymentBlockHeight = postDeploymentBlock.getBlockData().getHeight();
// Check refund
long deployAtFee = deployAtTransaction.getTransactionData().getFee();
long deployersPostDeploymentBalance = deployersInitialBalance - fundingAmount - deployAtFee;
checkTradeRefund(repository, deployer, deployersInitialBalance, deployAtFee);
describeAt(repository, atAddress);
// Check AT is finished
ATData atData = repository.getATRepository().fromATAddress(atAddress);
assertTrue(atData.getIsFinished());
// AT should be in REFUNDED mode
CrossChainTradeData tradeData = LitecoinACCTv1.getInstance().populateTradeData(repository, atData);
assertEquals(AcctMode.REFUNDED, tradeData.mode);
// Test orphaning
BlockUtils.orphanToBlock(repository, postDeploymentBlockHeight);
// Check balances
long expectedBalance = deployersPostDeploymentBalance;
long actualBalance = deployer.getConfirmedBalance(Asset.QORT);
assertEquals("Deployer's post-orphan/pre-refund balance incorrect", expectedBalance, actualBalance);
}
} }
@SuppressWarnings("unused") @Override
@Test protected String getName() {
public void testCorrectSecretCorrectSender() throws DataException { return NAME;
try (final Repository repository = RepositoryManager.getRepository()) {
PrivateKeyAccount deployer = Common.getTestAccount(repository, "chloe");
PrivateKeyAccount tradeAccount = createTradeAccount(repository);
PrivateKeyAccount partner = Common.getTestAccount(repository, "dilbert");
long deployersInitialBalance = deployer.getConfirmedBalance(Asset.QORT);
long partnersInitialBalance = partner.getConfirmedBalance(Asset.QORT);
DeployAtTransaction deployAtTransaction = doDeploy(repository, deployer, tradeAccount.getAddress());
Account at = deployAtTransaction.getATAccount();
String atAddress = at.getAddress();
long partnersOfferMessageTransactionTimestamp = System.currentTimeMillis();
int lockTimeA = calcTestLockTimeA(partnersOfferMessageTransactionTimestamp);
int refundTimeout = LitecoinACCTv1.calcRefundTimeout(partnersOfferMessageTransactionTimestamp, lockTimeA);
// Send trade info to AT
byte[] messageData = LitecoinACCTv1.buildTradeMessage(partner.getAddress(), litecoinPublicKeyHash, hashOfSecretA, lockTimeA, refundTimeout);
MessageTransaction messageTransaction = sendMessage(repository, tradeAccount, messageData, atAddress);
// Give AT time to process message
BlockUtils.mintBlock(repository);
// Send correct secret to AT, from correct account
messageData = LitecoinACCTv1.buildRedeemMessage(secretA, partner.getAddress());
messageTransaction = sendMessage(repository, partner, messageData, atAddress);
// AT should send funds in the next block
ATStateData preRedeemAtStateData = repository.getATRepository().getLatestATState(atAddress);
BlockUtils.mintBlock(repository);
describeAt(repository, atAddress);
// Check AT is finished
ATData atData = repository.getATRepository().fromATAddress(atAddress);
assertTrue(atData.getIsFinished());
// AT should be in REDEEMED mode
CrossChainTradeData tradeData = LitecoinACCTv1.getInstance().populateTradeData(repository, atData);
assertEquals(AcctMode.REDEEMED, tradeData.mode);
// Check balances
long expectedBalance = partnersInitialBalance - messageTransaction.getTransactionData().getFee() + redeemAmount;
long actualBalance = partner.getConfirmedBalance(Asset.QORT);
assertEquals("Partner's post-redeem balance incorrect", expectedBalance, actualBalance);
// Orphan redeem
BlockUtils.orphanLastBlock(repository);
// Check balances
expectedBalance = partnersInitialBalance - messageTransaction.getTransactionData().getFee();
actualBalance = partner.getConfirmedBalance(Asset.QORT);
assertEquals("Partner's post-orphan/pre-redeem balance incorrect", expectedBalance, actualBalance);
// Check AT state
ATStateData postOrphanAtStateData = repository.getATRepository().getLatestATState(atAddress);
assertTrue("AT states mismatch", Arrays.equals(preRedeemAtStateData.getStateData(), postOrphanAtStateData.getStateData()));
}
} }
@SuppressWarnings("unused")
@Test
public void testCorrectSecretIncorrectSender() throws DataException {
try (final Repository repository = RepositoryManager.getRepository()) {
PrivateKeyAccount deployer = Common.getTestAccount(repository, "chloe");
PrivateKeyAccount tradeAccount = createTradeAccount(repository);
PrivateKeyAccount partner = Common.getTestAccount(repository, "dilbert");
PrivateKeyAccount bystander = Common.getTestAccount(repository, "bob");
long deployersInitialBalance = deployer.getConfirmedBalance(Asset.QORT);
long partnersInitialBalance = partner.getConfirmedBalance(Asset.QORT);
DeployAtTransaction deployAtTransaction = doDeploy(repository, deployer, tradeAccount.getAddress());
long deployAtFee = deployAtTransaction.getTransactionData().getFee();
Account at = deployAtTransaction.getATAccount();
String atAddress = at.getAddress();
long partnersOfferMessageTransactionTimestamp = System.currentTimeMillis();
int lockTimeA = calcTestLockTimeA(partnersOfferMessageTransactionTimestamp);
int refundTimeout = LitecoinACCTv1.calcRefundTimeout(partnersOfferMessageTransactionTimestamp, lockTimeA);
// Send trade info to AT
byte[] messageData = LitecoinACCTv1.buildTradeMessage(partner.getAddress(), litecoinPublicKeyHash, hashOfSecretA, lockTimeA, refundTimeout);
MessageTransaction messageTransaction = sendMessage(repository, tradeAccount, messageData, atAddress);
// Give AT time to process message
BlockUtils.mintBlock(repository);
// Send correct secret to AT, but from wrong account
messageData = LitecoinACCTv1.buildRedeemMessage(secretA, partner.getAddress());
messageTransaction = sendMessage(repository, bystander, messageData, atAddress);
// AT should NOT send funds in the next block
ATStateData preRedeemAtStateData = repository.getATRepository().getLatestATState(atAddress);
BlockUtils.mintBlock(repository);
describeAt(repository, atAddress);
// Check AT is NOT finished
ATData atData = repository.getATRepository().fromATAddress(atAddress);
assertFalse(atData.getIsFinished());
// AT should still be in TRADE mode
CrossChainTradeData tradeData = LitecoinACCTv1.getInstance().populateTradeData(repository, atData);
assertEquals(AcctMode.TRADING, tradeData.mode);
// Check balances
long expectedBalance = partnersInitialBalance;
long actualBalance = partner.getConfirmedBalance(Asset.QORT);
assertEquals("Partner's balance incorrect", expectedBalance, actualBalance);
// Check eventual refund
checkTradeRefund(repository, deployer, deployersInitialBalance, deployAtFee);
}
}
@SuppressWarnings("unused")
@Test
public void testIncorrectSecretCorrectSender() throws DataException {
try (final Repository repository = RepositoryManager.getRepository()) {
PrivateKeyAccount deployer = Common.getTestAccount(repository, "chloe");
PrivateKeyAccount tradeAccount = createTradeAccount(repository);
PrivateKeyAccount partner = Common.getTestAccount(repository, "dilbert");
long deployersInitialBalance = deployer.getConfirmedBalance(Asset.QORT);
long partnersInitialBalance = partner.getConfirmedBalance(Asset.QORT);
DeployAtTransaction deployAtTransaction = doDeploy(repository, deployer, tradeAccount.getAddress());
long deployAtFee = deployAtTransaction.getTransactionData().getFee();
Account at = deployAtTransaction.getATAccount();
String atAddress = at.getAddress();
long partnersOfferMessageTransactionTimestamp = System.currentTimeMillis();
int lockTimeA = calcTestLockTimeA(partnersOfferMessageTransactionTimestamp);
int refundTimeout = LitecoinACCTv1.calcRefundTimeout(partnersOfferMessageTransactionTimestamp, lockTimeA);
// Send trade info to AT
byte[] messageData = LitecoinACCTv1.buildTradeMessage(partner.getAddress(), litecoinPublicKeyHash, hashOfSecretA, lockTimeA, refundTimeout);
MessageTransaction messageTransaction = sendMessage(repository, tradeAccount, messageData, atAddress);
// Give AT time to process message
BlockUtils.mintBlock(repository);
// Send incorrect secret to AT, from correct account
byte[] wrongSecret = new byte[32];
RANDOM.nextBytes(wrongSecret);
messageData = LitecoinACCTv1.buildRedeemMessage(wrongSecret, partner.getAddress());
messageTransaction = sendMessage(repository, partner, messageData, atAddress);
// AT should NOT send funds in the next block
ATStateData preRedeemAtStateData = repository.getATRepository().getLatestATState(atAddress);
BlockUtils.mintBlock(repository);
describeAt(repository, atAddress);
// Check AT is NOT finished
ATData atData = repository.getATRepository().fromATAddress(atAddress);
assertFalse(atData.getIsFinished());
// AT should still be in TRADE mode
CrossChainTradeData tradeData = LitecoinACCTv1.getInstance().populateTradeData(repository, atData);
assertEquals(AcctMode.TRADING, tradeData.mode);
long expectedBalance = partnersInitialBalance - messageTransaction.getTransactionData().getFee();
long actualBalance = partner.getConfirmedBalance(Asset.QORT);
assertEquals("Partner's balance incorrect", expectedBalance, actualBalance);
// Check eventual refund
checkTradeRefund(repository, deployer, deployersInitialBalance, deployAtFee);
}
}
@SuppressWarnings("unused")
@Test
public void testCorrectSecretCorrectSenderInvalidMessageLength() throws DataException {
try (final Repository repository = RepositoryManager.getRepository()) {
PrivateKeyAccount deployer = Common.getTestAccount(repository, "chloe");
PrivateKeyAccount tradeAccount = createTradeAccount(repository);
PrivateKeyAccount partner = Common.getTestAccount(repository, "dilbert");
long deployersInitialBalance = deployer.getConfirmedBalance(Asset.QORT);
long partnersInitialBalance = partner.getConfirmedBalance(Asset.QORT);
DeployAtTransaction deployAtTransaction = doDeploy(repository, deployer, tradeAccount.getAddress());
Account at = deployAtTransaction.getATAccount();
String atAddress = at.getAddress();
long partnersOfferMessageTransactionTimestamp = System.currentTimeMillis();
int lockTimeA = calcTestLockTimeA(partnersOfferMessageTransactionTimestamp);
int refundTimeout = LitecoinACCTv1.calcRefundTimeout(partnersOfferMessageTransactionTimestamp, lockTimeA);
// Send trade info to AT
byte[] messageData = LitecoinACCTv1.buildTradeMessage(partner.getAddress(), litecoinPublicKeyHash, hashOfSecretA, lockTimeA, refundTimeout);
MessageTransaction messageTransaction = sendMessage(repository, tradeAccount, messageData, atAddress);
// Give AT time to process message
BlockUtils.mintBlock(repository);
// Send correct secret to AT, from correct account, but missing receive address, hence incorrect length
messageData = Bytes.concat(secretA);
messageTransaction = sendMessage(repository, partner, messageData, atAddress);
// AT should NOT send funds in the next block
ATStateData preRedeemAtStateData = repository.getATRepository().getLatestATState(atAddress);
BlockUtils.mintBlock(repository);
describeAt(repository, atAddress);
// Check AT is NOT finished
ATData atData = repository.getATRepository().fromATAddress(atAddress);
assertFalse(atData.getIsFinished());
// AT should be in TRADING mode
CrossChainTradeData tradeData = LitecoinACCTv1.getInstance().populateTradeData(repository, atData);
assertEquals(AcctMode.TRADING, tradeData.mode);
}
}
@SuppressWarnings("unused")
@Test
public void testDescribeDeployed() throws DataException {
try (final Repository repository = RepositoryManager.getRepository()) {
PrivateKeyAccount deployer = Common.getTestAccount(repository, "chloe");
PrivateKeyAccount tradeAccount = createTradeAccount(repository);
PrivateKeyAccount partner = Common.getTestAccount(repository, "dilbert");
long deployersInitialBalance = deployer.getConfirmedBalance(Asset.QORT);
long partnersInitialBalance = partner.getConfirmedBalance(Asset.QORT);
DeployAtTransaction deployAtTransaction = doDeploy(repository, deployer, tradeAccount.getAddress());
List<ATData> executableAts = repository.getATRepository().getAllExecutableATs();
for (ATData atData : executableAts) {
String atAddress = atData.getATAddress();
byte[] codeBytes = atData.getCodeBytes();
byte[] codeHash = Crypto.digest(codeBytes);
System.out.println(String.format("%s: code length: %d byte%s, code hash: %s",
atAddress,
codeBytes.length,
(codeBytes.length != 1 ? "s": ""),
HashCode.fromBytes(codeHash)));
// Not one of ours?
if (!Arrays.equals(codeHash, LitecoinACCTv1.CODE_BYTES_HASH))
continue;
describeAt(repository, atAddress);
}
}
}
private int calcTestLockTimeA(long messageTimestamp) {
return (int) (messageTimestamp / 1000L + tradeTimeout * 60);
}
private DeployAtTransaction doDeploy(Repository repository, PrivateKeyAccount deployer, String tradeAddress) throws DataException {
byte[] creationBytes = LitecoinACCTv1.buildQortalAT(tradeAddress, litecoinPublicKeyHash, redeemAmount, litecoinAmount, tradeTimeout);
long txTimestamp = System.currentTimeMillis();
byte[] lastReference = deployer.getLastReference();
if (lastReference == null) {
System.err.println(String.format("Qortal account %s has no last reference", deployer.getAddress()));
System.exit(2);
}
Long fee = null;
String name = "QORT-LTC cross-chain trade";
String description = String.format("Qortal-Litecoin cross-chain trade");
String atType = "ACCT";
String tags = "QORT-LTC ACCT";
BaseTransactionData baseTransactionData = new BaseTransactionData(txTimestamp, Group.NO_GROUP, lastReference, deployer.getPublicKey(), fee, null);
TransactionData deployAtTransactionData = new DeployAtTransactionData(baseTransactionData, name, description, atType, tags, creationBytes, fundingAmount, Asset.QORT);
DeployAtTransaction deployAtTransaction = new DeployAtTransaction(repository, deployAtTransactionData);
fee = deployAtTransaction.calcRecommendedFee();
deployAtTransactionData.setFee(fee);
TransactionUtils.signAndMint(repository, deployAtTransactionData, deployer);
return deployAtTransaction;
}
private MessageTransaction sendMessage(Repository repository, PrivateKeyAccount sender, byte[] data, String recipient) throws DataException {
long txTimestamp = System.currentTimeMillis();
byte[] lastReference = sender.getLastReference();
if (lastReference == null) {
System.err.println(String.format("Qortal account %s has no last reference", sender.getAddress()));
System.exit(2);
}
Long fee = null;
int version = 4;
int nonce = 0;
long amount = 0;
Long assetId = null; // because amount is zero
BaseTransactionData baseTransactionData = new BaseTransactionData(txTimestamp, Group.NO_GROUP, lastReference, sender.getPublicKey(), fee, null);
TransactionData messageTransactionData = new MessageTransactionData(baseTransactionData, version, nonce, recipient, amount, assetId, data, false, false);
MessageTransaction messageTransaction = new MessageTransaction(repository, messageTransactionData);
fee = messageTransaction.calcRecommendedFee();
messageTransactionData.setFee(fee);
TransactionUtils.signAndMint(repository, messageTransactionData, sender);
return messageTransaction;
}
private void checkTradeRefund(Repository repository, Account deployer, long deployersInitialBalance, long deployAtFee) throws DataException {
long deployersPostDeploymentBalance = deployersInitialBalance - fundingAmount - deployAtFee;
int refundTimeout = tradeTimeout / 2 + 1; // close enough
// AT should automatically refund deployer after 'refundTimeout' blocks
for (int blockCount = 0; blockCount <= refundTimeout; ++blockCount)
BlockUtils.mintBlock(repository);
// We don't bother to exactly calculate QORT spent running AT for several blocks, but we do know the expected range
long expectedMinimumBalance = deployersPostDeploymentBalance;
long expectedMaximumBalance = deployersInitialBalance - deployAtFee;
long actualBalance = deployer.getConfirmedBalance(Asset.QORT);
assertTrue(String.format("Deployer's balance %s should be above minimum %s", actualBalance, expectedMinimumBalance), actualBalance > expectedMinimumBalance);
assertTrue(String.format("Deployer's balance %s should be below maximum %s", actualBalance, expectedMaximumBalance), actualBalance < expectedMaximumBalance);
}
private void describeAt(Repository repository, String atAddress) throws DataException {
ATData atData = repository.getATRepository().fromATAddress(atAddress);
CrossChainTradeData tradeData = LitecoinACCTv1.getInstance().populateTradeData(repository, atData);
Function<Long, String> epochMilliFormatter = (timestamp) -> LocalDateTime.ofInstant(Instant.ofEpochMilli(timestamp), ZoneOffset.UTC).format(DateTimeFormatter.ofLocalizedDateTime(FormatStyle.MEDIUM));
int currentBlockHeight = repository.getBlockRepository().getBlockchainHeight();
System.out.print(String.format("%s:\n"
+ "\tmode: %s\n"
+ "\tcreator: %s,\n"
+ "\tcreation timestamp: %s,\n"
+ "\tcurrent balance: %s QORT,\n"
+ "\tis finished: %b,\n"
+ "\tredeem payout: %s QORT,\n"
+ "\texpected Litecoin: %s LTC,\n"
+ "\tcurrent block height: %d,\n",
tradeData.qortalAtAddress,
tradeData.mode,
tradeData.qortalCreator,
epochMilliFormatter.apply(tradeData.creationTimestamp),
Amounts.prettyAmount(tradeData.qortBalance),
atData.getIsFinished(),
Amounts.prettyAmount(tradeData.qortAmount),
Amounts.prettyAmount(tradeData.expectedForeignAmount),
currentBlockHeight));
if (tradeData.mode != AcctMode.OFFERING && tradeData.mode != AcctMode.CANCELLED) {
System.out.println(String.format("\trefund timeout: %d minutes,\n"
+ "\trefund height: block %d,\n"
+ "\tHASH160 of secret-A: %s,\n"
+ "\tLitecoin P2SH-A nLockTime: %d (%s),\n"
+ "\ttrade partner: %s\n"
+ "\tpartner's receiving address: %s",
tradeData.refundTimeout,
tradeData.tradeRefundHeight,
HashCode.fromBytes(tradeData.hashOfSecretA).toString().substring(0, 40),
tradeData.lockTimeA, epochMilliFormatter.apply(tradeData.lockTimeA * 1000L),
tradeData.qortalPartnerAddress,
tradeData.qortalPartnerReceivingAddress));
}
}
private PrivateKeyAccount createTradeAccount(Repository repository) {
// We actually use a known test account with funds to avoid PoW compute
return Common.getTestAccount(repository, "alice");
}
} }

View File

@ -1,769 +1,58 @@
package org.qortal.test.crosschain.litecoinv3; package org.qortal.test.crosschain.litecoinv3;
import com.google.common.hash.HashCode; import com.google.common.hash.HashCode;
import com.google.common.primitives.Bytes; import org.qortal.crosschain.ACCT;
import org.junit.Before; import org.qortal.crosschain.LitecoinACCTv1;
import org.junit.Test; import org.qortal.test.crosschain.ACCTTests;
import org.qortal.account.Account;
import org.qortal.account.PrivateKeyAccount;
import org.qortal.asset.Asset;
import org.qortal.block.Block;
import org.qortal.crosschain.AcctMode;
import org.qortal.crosschain.LitecoinACCTv3;
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.transaction.BaseTransactionData;
import org.qortal.data.transaction.DeployAtTransactionData;
import org.qortal.data.transaction.MessageTransactionData;
import org.qortal.data.transaction.TransactionData;
import org.qortal.group.Group;
import org.qortal.repository.DataException;
import org.qortal.repository.Repository;
import org.qortal.repository.RepositoryManager;
import org.qortal.test.common.BlockUtils;
import org.qortal.test.common.Common;
import org.qortal.test.common.TransactionUtils;
import org.qortal.transaction.DeployAtTransaction;
import org.qortal.transaction.MessageTransaction;
import org.qortal.utils.Amounts;
import java.time.Instant; public class LitecoinACCTv3Tests extends ACCTTests {
import java.time.LocalDateTime;
import java.time.ZoneOffset;
import java.time.format.DateTimeFormatter;
import java.time.format.FormatStyle;
import java.util.Arrays;
import java.util.List;
import java.util.Random;
import java.util.function.Function;
import static org.junit.Assert.*;
public class LitecoinACCTv3Tests extends Common {
public static final byte[] secretA = "This string is exactly 32 bytes!".getBytes();
public static final byte[] hashOfSecretA = Crypto.hash160(secretA); // daf59884b4d1aec8c1b17102530909ee43c0151a
public static final byte[] litecoinPublicKeyHash = HashCode.fromString("bb00bb11bb22bb33bb44bb55bb66bb77bb88bb99").asBytes(); public static final byte[] litecoinPublicKeyHash = HashCode.fromString("bb00bb11bb22bb33bb44bb55bb66bb77bb88bb99").asBytes();
public static final int tradeTimeout = 20; // blocks private static final String SYMBOL = "LTC";
public static final long redeemAmount = 80_40200000L; private static final String NAME = "Litecoin";
public static final long fundingAmount = 123_45600000L;
public static final long litecoinAmount = 864200L; // 0.00864200 LTC
private static final Random RANDOM = new Random(); @Override
protected byte[] getPublicKey() {
@Before return litecoinPublicKeyHash;
public void beforeTest() throws DataException {
Common.useDefaultSettings();
} }
@Test @Override
public void testCompile() { protected byte[] buildQortalAT(String address, byte[] publicKey, long redeemAmount, long foreignAmount, int tradeTimeout) {
PrivateKeyAccount tradeAccount = createTradeAccount(null); return LitecoinACCTv1.buildQortalAT(address, publicKey, redeemAmount, foreignAmount, tradeTimeout);
byte[] creationBytes = LitecoinACCTv3.buildQortalAT(tradeAccount.getAddress(), litecoinPublicKeyHash, redeemAmount, litecoinAmount, tradeTimeout);
assertNotNull(creationBytes);
System.out.println("AT creation bytes: " + HashCode.fromBytes(creationBytes).toString());
} }
@Test @Override
public void testDeploy() throws DataException { protected ACCT getInstance() {
try (final Repository repository = RepositoryManager.getRepository()) { return LitecoinACCTv1.getInstance();
PrivateKeyAccount deployer = Common.getTestAccount(repository, "chloe");
PrivateKeyAccount tradeAccount = createTradeAccount(repository);
PrivateKeyAccount partner = Common.getTestAccount(repository, "dilbert");
long deployersInitialBalance = deployer.getConfirmedBalance(Asset.QORT);
long partnersInitialBalance = partner.getConfirmedBalance(Asset.QORT);
DeployAtTransaction deployAtTransaction = doDeploy(repository, deployer, tradeAccount.getAddress());
long expectedBalance = deployersInitialBalance - fundingAmount - deployAtTransaction.getTransactionData().getFee();
long actualBalance = deployer.getConfirmedBalance(Asset.QORT);
assertEquals("Deployer's post-deployment balance incorrect", expectedBalance, actualBalance);
expectedBalance = fundingAmount;
actualBalance = deployAtTransaction.getATAccount().getConfirmedBalance(Asset.QORT);
assertEquals("AT's post-deployment balance incorrect", expectedBalance, actualBalance);
expectedBalance = partnersInitialBalance;
actualBalance = partner.getConfirmedBalance(Asset.QORT);
assertEquals("Partner's post-deployment balance incorrect", expectedBalance, actualBalance);
// Test orphaning
BlockUtils.orphanLastBlock(repository);
expectedBalance = deployersInitialBalance;
actualBalance = deployer.getConfirmedBalance(Asset.QORT);
assertEquals("Deployer's post-orphan/pre-deployment balance incorrect", expectedBalance, actualBalance);
expectedBalance = 0;
actualBalance = deployAtTransaction.getATAccount().getConfirmedBalance(Asset.QORT);
assertEquals("AT's post-orphan/pre-deployment balance incorrect", expectedBalance, actualBalance);
expectedBalance = partnersInitialBalance;
actualBalance = partner.getConfirmedBalance(Asset.QORT);
assertEquals("Partner's post-orphan/pre-deployment balance incorrect", expectedBalance, actualBalance);
}
} }
@SuppressWarnings("unused") @Override
@Test protected int calcRefundTimeout(long partnersOfferMessageTransactionTimestamp, int lockTimeA) {
public void testOfferCancel() throws DataException { return LitecoinACCTv1.calcRefundTimeout(partnersOfferMessageTransactionTimestamp, lockTimeA);
try (final Repository repository = RepositoryManager.getRepository()) {
PrivateKeyAccount deployer = Common.getTestAccount(repository, "chloe");
PrivateKeyAccount tradeAccount = createTradeAccount(repository);
PrivateKeyAccount partner = Common.getTestAccount(repository, "dilbert");
long deployersInitialBalance = deployer.getConfirmedBalance(Asset.QORT);
long partnersInitialBalance = partner.getConfirmedBalance(Asset.QORT);
DeployAtTransaction deployAtTransaction = doDeploy(repository, deployer, tradeAccount.getAddress());
Account at = deployAtTransaction.getATAccount();
String atAddress = at.getAddress();
long deployAtFee = deployAtTransaction.getTransactionData().getFee();
long deployersPostDeploymentBalance = deployersInitialBalance - fundingAmount - deployAtFee;
// Send creator's address to AT, instead of typical partner's address
byte[] messageData = LitecoinACCTv3.getInstance().buildCancelMessage(deployer.getAddress());
MessageTransaction messageTransaction = sendMessage(repository, deployer, messageData, atAddress);
long messageFee = messageTransaction.getTransactionData().getFee();
// AT should process 'cancel' message in next block
BlockUtils.mintBlock(repository);
describeAt(repository, atAddress);
// Check AT is finished
ATData atData = repository.getATRepository().fromATAddress(atAddress);
assertTrue(atData.getIsFinished());
// AT should be in CANCELLED mode
CrossChainTradeData tradeData = LitecoinACCTv3.getInstance().populateTradeData(repository, atData);
assertEquals(AcctMode.CANCELLED, tradeData.mode);
// Check balances
long expectedMinimumBalance = deployersPostDeploymentBalance;
long expectedMaximumBalance = deployersInitialBalance - deployAtFee - messageFee;
long actualBalance = deployer.getConfirmedBalance(Asset.QORT);
assertTrue(String.format("Deployer's balance %s should be above minimum %s", actualBalance, expectedMinimumBalance), actualBalance > expectedMinimumBalance);
assertTrue(String.format("Deployer's balance %s should be below maximum %s", actualBalance, expectedMaximumBalance), actualBalance < expectedMaximumBalance);
// Test orphaning
BlockUtils.orphanLastBlock(repository);
// Check balances
long expectedBalance = deployersPostDeploymentBalance - messageFee;
actualBalance = deployer.getConfirmedBalance(Asset.QORT);
assertEquals("Deployer's post-orphan/pre-refund balance incorrect", expectedBalance, actualBalance);
}
} }
@SuppressWarnings("unused") @Override
@Test protected byte[] buildTradeMessage(String address, byte[] publicKey, byte[] hashOfSecretA, int lockTimeA, int refundTimeout) {
public void testOfferCancelInvalidLength() throws DataException { return LitecoinACCTv1.buildTradeMessage(address, publicKey, hashOfSecretA, lockTimeA, refundTimeout);
try (final Repository repository = RepositoryManager.getRepository()) {
PrivateKeyAccount deployer = Common.getTestAccount(repository, "chloe");
PrivateKeyAccount tradeAccount = createTradeAccount(repository);
PrivateKeyAccount partner = Common.getTestAccount(repository, "dilbert");
long deployersInitialBalance = deployer.getConfirmedBalance(Asset.QORT);
long partnersInitialBalance = partner.getConfirmedBalance(Asset.QORT);
DeployAtTransaction deployAtTransaction = doDeploy(repository, deployer, tradeAccount.getAddress());
Account at = deployAtTransaction.getATAccount();
String atAddress = at.getAddress();
long deployAtFee = deployAtTransaction.getTransactionData().getFee();
long deployersPostDeploymentBalance = deployersInitialBalance - fundingAmount - deployAtFee;
// Instead of sending creator's address to AT, send too-short/invalid message
byte[] messageData = new byte[7];
RANDOM.nextBytes(messageData);
MessageTransaction messageTransaction = sendMessage(repository, deployer, messageData, atAddress);
long messageFee = messageTransaction.getTransactionData().getFee();
// AT should process 'cancel' message in next block
// As message is too short, it will be padded to 32bytes but cancel code doesn't care about message content, so should be ok
BlockUtils.mintBlock(repository);
describeAt(repository, atAddress);
// Check AT is finished
ATData atData = repository.getATRepository().fromATAddress(atAddress);
assertTrue(atData.getIsFinished());
// AT should be in CANCELLED mode
CrossChainTradeData tradeData = LitecoinACCTv3.getInstance().populateTradeData(repository, atData);
assertEquals(AcctMode.CANCELLED, tradeData.mode);
}
} }
@SuppressWarnings("unused") @Override
@Test protected byte[] buildRedeemMessage(byte[] secretA, String address) {
public void testTradingInfoProcessing() throws DataException { return LitecoinACCTv1.buildRedeemMessage(secretA, address);
try (final Repository repository = RepositoryManager.getRepository()) {
PrivateKeyAccount deployer = Common.getTestAccount(repository, "chloe");
PrivateKeyAccount tradeAccount = createTradeAccount(repository);
PrivateKeyAccount partner = Common.getTestAccount(repository, "dilbert");
long deployersInitialBalance = deployer.getConfirmedBalance(Asset.QORT);
long partnersInitialBalance = partner.getConfirmedBalance(Asset.QORT);
DeployAtTransaction deployAtTransaction = doDeploy(repository, deployer, tradeAccount.getAddress());
Account at = deployAtTransaction.getATAccount();
String atAddress = at.getAddress();
long partnersOfferMessageTransactionTimestamp = System.currentTimeMillis();
int lockTimeA = calcTestLockTimeA(partnersOfferMessageTransactionTimestamp);
int refundTimeout = LitecoinACCTv3.calcRefundTimeout(partnersOfferMessageTransactionTimestamp, lockTimeA);
// Send trade info to AT
byte[] messageData = LitecoinACCTv3.buildTradeMessage(partner.getAddress(), litecoinPublicKeyHash, hashOfSecretA, lockTimeA, refundTimeout);
MessageTransaction messageTransaction = sendMessage(repository, tradeAccount, messageData, atAddress);
Block postDeploymentBlock = BlockUtils.mintBlock(repository);
int postDeploymentBlockHeight = postDeploymentBlock.getBlockData().getHeight();
long deployAtFee = deployAtTransaction.getTransactionData().getFee();
long deployersPostDeploymentBalance = deployersInitialBalance - fundingAmount - deployAtFee;
describeAt(repository, atAddress);
ATData atData = repository.getATRepository().fromATAddress(atAddress);
CrossChainTradeData tradeData = LitecoinACCTv3.getInstance().populateTradeData(repository, atData);
// AT should be in TRADE mode
assertEquals(AcctMode.TRADING, tradeData.mode);
// Check hashOfSecretA was extracted correctly
assertTrue(Arrays.equals(hashOfSecretA, tradeData.hashOfSecretA));
// Check trade partner Qortal address was extracted correctly
assertEquals(partner.getAddress(), tradeData.qortalPartnerAddress);
// Check trade partner's Litecoin PKH was extracted correctly
assertTrue(Arrays.equals(litecoinPublicKeyHash, tradeData.partnerForeignPKH));
// Test orphaning
BlockUtils.orphanToBlock(repository, postDeploymentBlockHeight);
// Check balances
long expectedBalance = deployersPostDeploymentBalance;
long actualBalance = deployer.getConfirmedBalance(Asset.QORT);
assertEquals("Deployer's post-orphan/pre-refund balance incorrect", expectedBalance, actualBalance);
}
} }
// TEST SENDING TRADING INFO BUT NOT FROM AT CREATOR (SHOULD BE IGNORED) @Override
@SuppressWarnings("unused") protected byte[] getCodeBytesHash() {
@Test return LitecoinACCTv1.CODE_BYTES_HASH;
public void testIncorrectTradeSender() throws DataException {
try (final Repository repository = RepositoryManager.getRepository()) {
PrivateKeyAccount deployer = Common.getTestAccount(repository, "chloe");
PrivateKeyAccount tradeAccount = createTradeAccount(repository);
PrivateKeyAccount partner = Common.getTestAccount(repository, "dilbert");
PrivateKeyAccount bystander = Common.getTestAccount(repository, "bob");
long deployersInitialBalance = deployer.getConfirmedBalance(Asset.QORT);
long partnersInitialBalance = partner.getConfirmedBalance(Asset.QORT);
DeployAtTransaction deployAtTransaction = doDeploy(repository, deployer, tradeAccount.getAddress());
Account at = deployAtTransaction.getATAccount();
String atAddress = at.getAddress();
long partnersOfferMessageTransactionTimestamp = System.currentTimeMillis();
int lockTimeA = calcTestLockTimeA(partnersOfferMessageTransactionTimestamp);
int refundTimeout = LitecoinACCTv3.calcRefundTimeout(partnersOfferMessageTransactionTimestamp, lockTimeA);
// Send trade info to AT BUT NOT FROM AT CREATOR
byte[] messageData = LitecoinACCTv3.buildTradeMessage(partner.getAddress(), litecoinPublicKeyHash, hashOfSecretA, lockTimeA, refundTimeout);
MessageTransaction messageTransaction = sendMessage(repository, bystander, messageData, atAddress);
BlockUtils.mintBlock(repository);
long expectedBalance = partnersInitialBalance;
long actualBalance = partner.getConfirmedBalance(Asset.QORT);
assertEquals("Partner's post-initial-payout balance incorrect", expectedBalance, actualBalance);
describeAt(repository, atAddress);
ATData atData = repository.getATRepository().fromATAddress(atAddress);
CrossChainTradeData tradeData = LitecoinACCTv3.getInstance().populateTradeData(repository, atData);
// AT should still be in OFFER mode
assertEquals(AcctMode.OFFERING, tradeData.mode);
}
} }
@SuppressWarnings("unused") @Override
@Test protected String getSymbol() {
public void testAutomaticTradeRefund() throws DataException { return SYMBOL;
try (final Repository repository = RepositoryManager.getRepository()) {
PrivateKeyAccount deployer = Common.getTestAccount(repository, "chloe");
PrivateKeyAccount tradeAccount = createTradeAccount(repository);
PrivateKeyAccount partner = Common.getTestAccount(repository, "dilbert");
long deployersInitialBalance = deployer.getConfirmedBalance(Asset.QORT);
long partnersInitialBalance = partner.getConfirmedBalance(Asset.QORT);
DeployAtTransaction deployAtTransaction = doDeploy(repository, deployer, tradeAccount.getAddress());
Account at = deployAtTransaction.getATAccount();
String atAddress = at.getAddress();
long partnersOfferMessageTransactionTimestamp = System.currentTimeMillis();
int lockTimeA = calcTestLockTimeA(partnersOfferMessageTransactionTimestamp);
int refundTimeout = LitecoinACCTv3.calcRefundTimeout(partnersOfferMessageTransactionTimestamp, lockTimeA);
// Send trade info to AT
byte[] messageData = LitecoinACCTv3.buildTradeMessage(partner.getAddress(), litecoinPublicKeyHash, hashOfSecretA, lockTimeA, refundTimeout);
MessageTransaction messageTransaction = sendMessage(repository, tradeAccount, messageData, atAddress);
Block postDeploymentBlock = BlockUtils.mintBlock(repository);
int postDeploymentBlockHeight = postDeploymentBlock.getBlockData().getHeight();
// Check refund
long deployAtFee = deployAtTransaction.getTransactionData().getFee();
long deployersPostDeploymentBalance = deployersInitialBalance - fundingAmount - deployAtFee;
checkTradeRefund(repository, deployer, deployersInitialBalance, deployAtFee);
describeAt(repository, atAddress);
// Check AT is finished
ATData atData = repository.getATRepository().fromATAddress(atAddress);
assertTrue(atData.getIsFinished());
// AT should be in REFUNDED mode
CrossChainTradeData tradeData = LitecoinACCTv3.getInstance().populateTradeData(repository, atData);
assertEquals(AcctMode.REFUNDED, tradeData.mode);
// Test orphaning
BlockUtils.orphanToBlock(repository, postDeploymentBlockHeight);
// Check balances
long expectedBalance = deployersPostDeploymentBalance;
long actualBalance = deployer.getConfirmedBalance(Asset.QORT);
assertEquals("Deployer's post-orphan/pre-refund balance incorrect", expectedBalance, actualBalance);
}
} }
@SuppressWarnings("unused") @Override
@Test protected String getName() {
public void testCorrectSecretCorrectSender() throws DataException { return NAME;
try (final Repository repository = RepositoryManager.getRepository()) {
PrivateKeyAccount deployer = Common.getTestAccount(repository, "chloe");
PrivateKeyAccount tradeAccount = createTradeAccount(repository);
PrivateKeyAccount partner = Common.getTestAccount(repository, "dilbert");
long deployersInitialBalance = deployer.getConfirmedBalance(Asset.QORT);
long partnersInitialBalance = partner.getConfirmedBalance(Asset.QORT);
DeployAtTransaction deployAtTransaction = doDeploy(repository, deployer, tradeAccount.getAddress());
Account at = deployAtTransaction.getATAccount();
String atAddress = at.getAddress();
long partnersOfferMessageTransactionTimestamp = System.currentTimeMillis();
int lockTimeA = calcTestLockTimeA(partnersOfferMessageTransactionTimestamp);
int refundTimeout = LitecoinACCTv3.calcRefundTimeout(partnersOfferMessageTransactionTimestamp, lockTimeA);
// Send trade info to AT
byte[] messageData = LitecoinACCTv3.buildTradeMessage(partner.getAddress(), litecoinPublicKeyHash, hashOfSecretA, lockTimeA, refundTimeout);
MessageTransaction messageTransaction = sendMessage(repository, tradeAccount, messageData, atAddress);
// Give AT time to process message
BlockUtils.mintBlock(repository);
// Send correct secret to AT, from correct account
messageData = LitecoinACCTv3.buildRedeemMessage(secretA, partner.getAddress());
messageTransaction = sendMessage(repository, partner, messageData, atAddress);
// AT should send funds in the next block
ATStateData preRedeemAtStateData = repository.getATRepository().getLatestATState(atAddress);
BlockUtils.mintBlock(repository);
describeAt(repository, atAddress);
// Check AT is finished
ATData atData = repository.getATRepository().fromATAddress(atAddress);
assertTrue(atData.getIsFinished());
// AT should be in REDEEMED mode
CrossChainTradeData tradeData = LitecoinACCTv3.getInstance().populateTradeData(repository, atData);
assertEquals(AcctMode.REDEEMED, tradeData.mode);
// Check balances
long expectedBalance = partnersInitialBalance - messageTransaction.getTransactionData().getFee() + redeemAmount;
long actualBalance = partner.getConfirmedBalance(Asset.QORT);
assertEquals("Partner's post-redeem balance incorrect", expectedBalance, actualBalance);
// Orphan redeem
BlockUtils.orphanLastBlock(repository);
// Check balances
expectedBalance = partnersInitialBalance - messageTransaction.getTransactionData().getFee();
actualBalance = partner.getConfirmedBalance(Asset.QORT);
assertEquals("Partner's post-orphan/pre-redeem balance incorrect", expectedBalance, actualBalance);
// Check AT state
ATStateData postOrphanAtStateData = repository.getATRepository().getLatestATState(atAddress);
assertTrue("AT states mismatch", Arrays.equals(preRedeemAtStateData.getStateData(), postOrphanAtStateData.getStateData()));
}
} }
@SuppressWarnings("unused")
@Test
public void testCorrectSecretIncorrectSender() throws DataException {
try (final Repository repository = RepositoryManager.getRepository()) {
PrivateKeyAccount deployer = Common.getTestAccount(repository, "chloe");
PrivateKeyAccount tradeAccount = createTradeAccount(repository);
PrivateKeyAccount partner = Common.getTestAccount(repository, "dilbert");
PrivateKeyAccount bystander = Common.getTestAccount(repository, "bob");
long deployersInitialBalance = deployer.getConfirmedBalance(Asset.QORT);
long partnersInitialBalance = partner.getConfirmedBalance(Asset.QORT);
DeployAtTransaction deployAtTransaction = doDeploy(repository, deployer, tradeAccount.getAddress());
long deployAtFee = deployAtTransaction.getTransactionData().getFee();
Account at = deployAtTransaction.getATAccount();
String atAddress = at.getAddress();
long partnersOfferMessageTransactionTimestamp = System.currentTimeMillis();
int lockTimeA = calcTestLockTimeA(partnersOfferMessageTransactionTimestamp);
int refundTimeout = LitecoinACCTv3.calcRefundTimeout(partnersOfferMessageTransactionTimestamp, lockTimeA);
// Send trade info to AT
byte[] messageData = LitecoinACCTv3.buildTradeMessage(partner.getAddress(), litecoinPublicKeyHash, hashOfSecretA, lockTimeA, refundTimeout);
MessageTransaction messageTransaction = sendMessage(repository, tradeAccount, messageData, atAddress);
// Give AT time to process message
BlockUtils.mintBlock(repository);
// Send correct secret to AT, but from wrong account
messageData = LitecoinACCTv3.buildRedeemMessage(secretA, partner.getAddress());
messageTransaction = sendMessage(repository, bystander, messageData, atAddress);
// AT should NOT send funds in the next block
ATStateData preRedeemAtStateData = repository.getATRepository().getLatestATState(atAddress);
BlockUtils.mintBlock(repository);
describeAt(repository, atAddress);
// Check AT is NOT finished
ATData atData = repository.getATRepository().fromATAddress(atAddress);
assertFalse(atData.getIsFinished());
// AT should still be in TRADE mode
CrossChainTradeData tradeData = LitecoinACCTv3.getInstance().populateTradeData(repository, atData);
assertEquals(AcctMode.TRADING, tradeData.mode);
// Check balances
long expectedBalance = partnersInitialBalance;
long actualBalance = partner.getConfirmedBalance(Asset.QORT);
assertEquals("Partner's balance incorrect", expectedBalance, actualBalance);
// Check eventual refund
checkTradeRefund(repository, deployer, deployersInitialBalance, deployAtFee);
}
}
@SuppressWarnings("unused")
@Test
public void testIncorrectSecretCorrectSender() throws DataException {
try (final Repository repository = RepositoryManager.getRepository()) {
PrivateKeyAccount deployer = Common.getTestAccount(repository, "chloe");
PrivateKeyAccount tradeAccount = createTradeAccount(repository);
PrivateKeyAccount partner = Common.getTestAccount(repository, "dilbert");
long deployersInitialBalance = deployer.getConfirmedBalance(Asset.QORT);
long partnersInitialBalance = partner.getConfirmedBalance(Asset.QORT);
DeployAtTransaction deployAtTransaction = doDeploy(repository, deployer, tradeAccount.getAddress());
long deployAtFee = deployAtTransaction.getTransactionData().getFee();
Account at = deployAtTransaction.getATAccount();
String atAddress = at.getAddress();
long partnersOfferMessageTransactionTimestamp = System.currentTimeMillis();
int lockTimeA = calcTestLockTimeA(partnersOfferMessageTransactionTimestamp);
int refundTimeout = LitecoinACCTv3.calcRefundTimeout(partnersOfferMessageTransactionTimestamp, lockTimeA);
// Send trade info to AT
byte[] messageData = LitecoinACCTv3.buildTradeMessage(partner.getAddress(), litecoinPublicKeyHash, hashOfSecretA, lockTimeA, refundTimeout);
MessageTransaction messageTransaction = sendMessage(repository, tradeAccount, messageData, atAddress);
// Give AT time to process message
BlockUtils.mintBlock(repository);
// Send incorrect secret to AT, from correct account
byte[] wrongSecret = new byte[32];
RANDOM.nextBytes(wrongSecret);
messageData = LitecoinACCTv3.buildRedeemMessage(wrongSecret, partner.getAddress());
messageTransaction = sendMessage(repository, partner, messageData, atAddress);
// AT should NOT send funds in the next block
ATStateData preRedeemAtStateData = repository.getATRepository().getLatestATState(atAddress);
BlockUtils.mintBlock(repository);
describeAt(repository, atAddress);
// Check AT is NOT finished
ATData atData = repository.getATRepository().fromATAddress(atAddress);
assertFalse(atData.getIsFinished());
// AT should still be in TRADE mode
CrossChainTradeData tradeData = LitecoinACCTv3.getInstance().populateTradeData(repository, atData);
assertEquals(AcctMode.TRADING, tradeData.mode);
long expectedBalance = partnersInitialBalance - messageTransaction.getTransactionData().getFee();
long actualBalance = partner.getConfirmedBalance(Asset.QORT);
assertEquals("Partner's balance incorrect", expectedBalance, actualBalance);
// Check eventual refund
checkTradeRefund(repository, deployer, deployersInitialBalance, deployAtFee);
}
}
@SuppressWarnings("unused")
@Test
public void testCorrectSecretCorrectSenderInvalidMessageLength() throws DataException {
try (final Repository repository = RepositoryManager.getRepository()) {
PrivateKeyAccount deployer = Common.getTestAccount(repository, "chloe");
PrivateKeyAccount tradeAccount = createTradeAccount(repository);
PrivateKeyAccount partner = Common.getTestAccount(repository, "dilbert");
long deployersInitialBalance = deployer.getConfirmedBalance(Asset.QORT);
long partnersInitialBalance = partner.getConfirmedBalance(Asset.QORT);
DeployAtTransaction deployAtTransaction = doDeploy(repository, deployer, tradeAccount.getAddress());
Account at = deployAtTransaction.getATAccount();
String atAddress = at.getAddress();
long partnersOfferMessageTransactionTimestamp = System.currentTimeMillis();
int lockTimeA = calcTestLockTimeA(partnersOfferMessageTransactionTimestamp);
int refundTimeout = LitecoinACCTv3.calcRefundTimeout(partnersOfferMessageTransactionTimestamp, lockTimeA);
// Send trade info to AT
byte[] messageData = LitecoinACCTv3.buildTradeMessage(partner.getAddress(), litecoinPublicKeyHash, hashOfSecretA, lockTimeA, refundTimeout);
MessageTransaction messageTransaction = sendMessage(repository, tradeAccount, messageData, atAddress);
// Give AT time to process message
BlockUtils.mintBlock(repository);
// Send correct secret to AT, from correct account, but missing receive address, hence incorrect length
messageData = Bytes.concat(secretA);
messageTransaction = sendMessage(repository, partner, messageData, atAddress);
// AT should NOT send funds in the next block
ATStateData preRedeemAtStateData = repository.getATRepository().getLatestATState(atAddress);
BlockUtils.mintBlock(repository);
describeAt(repository, atAddress);
// Check AT is NOT finished
ATData atData = repository.getATRepository().fromATAddress(atAddress);
assertFalse(atData.getIsFinished());
// AT should be in TRADING mode
CrossChainTradeData tradeData = LitecoinACCTv3.getInstance().populateTradeData(repository, atData);
assertEquals(AcctMode.TRADING, tradeData.mode);
}
}
@SuppressWarnings("unused")
@Test
public void testDescribeDeployed() throws DataException {
try (final Repository repository = RepositoryManager.getRepository()) {
PrivateKeyAccount deployer = Common.getTestAccount(repository, "chloe");
PrivateKeyAccount tradeAccount = createTradeAccount(repository);
PrivateKeyAccount partner = Common.getTestAccount(repository, "dilbert");
long deployersInitialBalance = deployer.getConfirmedBalance(Asset.QORT);
long partnersInitialBalance = partner.getConfirmedBalance(Asset.QORT);
DeployAtTransaction deployAtTransaction = doDeploy(repository, deployer, tradeAccount.getAddress());
List<ATData> executableAts = repository.getATRepository().getAllExecutableATs();
for (ATData atData : executableAts) {
String atAddress = atData.getATAddress();
byte[] codeBytes = atData.getCodeBytes();
byte[] codeHash = Crypto.digest(codeBytes);
System.out.println(String.format("%s: code length: %d byte%s, code hash: %s",
atAddress,
codeBytes.length,
(codeBytes.length != 1 ? "s": ""),
HashCode.fromBytes(codeHash)));
// Not one of ours?
if (!Arrays.equals(codeHash, LitecoinACCTv3.CODE_BYTES_HASH))
continue;
describeAt(repository, atAddress);
}
}
}
private int calcTestLockTimeA(long messageTimestamp) {
return (int) (messageTimestamp / 1000L + tradeTimeout * 60);
}
private DeployAtTransaction doDeploy(Repository repository, PrivateKeyAccount deployer, String tradeAddress) throws DataException {
byte[] creationBytes = LitecoinACCTv3.buildQortalAT(tradeAddress, litecoinPublicKeyHash, redeemAmount, litecoinAmount, tradeTimeout);
long txTimestamp = System.currentTimeMillis();
byte[] lastReference = deployer.getLastReference();
if (lastReference == null) {
System.err.println(String.format("Qortal account %s has no last reference", deployer.getAddress()));
System.exit(2);
}
Long fee = null;
String name = "QORT-LTC cross-chain trade";
String description = String.format("Qortal-Litecoin cross-chain trade");
String atType = "ACCT";
String tags = "QORT-LTC ACCT";
BaseTransactionData baseTransactionData = new BaseTransactionData(txTimestamp, Group.NO_GROUP, lastReference, deployer.getPublicKey(), fee, null);
TransactionData deployAtTransactionData = new DeployAtTransactionData(baseTransactionData, name, description, atType, tags, creationBytes, fundingAmount, Asset.QORT);
DeployAtTransaction deployAtTransaction = new DeployAtTransaction(repository, deployAtTransactionData);
fee = deployAtTransaction.calcRecommendedFee();
deployAtTransactionData.setFee(fee);
TransactionUtils.signAndMint(repository, deployAtTransactionData, deployer);
return deployAtTransaction;
}
private MessageTransaction sendMessage(Repository repository, PrivateKeyAccount sender, byte[] data, String recipient) throws DataException {
long txTimestamp = System.currentTimeMillis();
byte[] lastReference = sender.getLastReference();
if (lastReference == null) {
System.err.println(String.format("Qortal account %s has no last reference", sender.getAddress()));
System.exit(2);
}
Long fee = null;
int version = 4;
int nonce = 0;
long amount = 0;
Long assetId = null; // because amount is zero
BaseTransactionData baseTransactionData = new BaseTransactionData(txTimestamp, Group.NO_GROUP, lastReference, sender.getPublicKey(), fee, null);
TransactionData messageTransactionData = new MessageTransactionData(baseTransactionData, version, nonce, recipient, amount, assetId, data, false, false);
MessageTransaction messageTransaction = new MessageTransaction(repository, messageTransactionData);
fee = messageTransaction.calcRecommendedFee();
messageTransactionData.setFee(fee);
TransactionUtils.signAndMint(repository, messageTransactionData, sender);
return messageTransaction;
}
private void checkTradeRefund(Repository repository, Account deployer, long deployersInitialBalance, long deployAtFee) throws DataException {
long deployersPostDeploymentBalance = deployersInitialBalance - fundingAmount - deployAtFee;
int refundTimeout = tradeTimeout / 2 + 1; // close enough
// AT should automatically refund deployer after 'refundTimeout' blocks
for (int blockCount = 0; blockCount <= refundTimeout; ++blockCount)
BlockUtils.mintBlock(repository);
// We don't bother to exactly calculate QORT spent running AT for several blocks, but we do know the expected range
long expectedMinimumBalance = deployersPostDeploymentBalance;
long expectedMaximumBalance = deployersInitialBalance - deployAtFee;
long actualBalance = deployer.getConfirmedBalance(Asset.QORT);
assertTrue(String.format("Deployer's balance %s should be above minimum %s", actualBalance, expectedMinimumBalance), actualBalance > expectedMinimumBalance);
assertTrue(String.format("Deployer's balance %s should be below maximum %s", actualBalance, expectedMaximumBalance), actualBalance < expectedMaximumBalance);
}
private void describeAt(Repository repository, String atAddress) throws DataException {
ATData atData = repository.getATRepository().fromATAddress(atAddress);
CrossChainTradeData tradeData = LitecoinACCTv3.getInstance().populateTradeData(repository, atData);
Function<Long, String> epochMilliFormatter = (timestamp) -> LocalDateTime.ofInstant(Instant.ofEpochMilli(timestamp), ZoneOffset.UTC).format(DateTimeFormatter.ofLocalizedDateTime(FormatStyle.MEDIUM));
int currentBlockHeight = repository.getBlockRepository().getBlockchainHeight();
System.out.print(String.format("%s:\n"
+ "\tmode: %s\n"
+ "\tcreator: %s,\n"
+ "\tcreation timestamp: %s,\n"
+ "\tcurrent balance: %s QORT,\n"
+ "\tis finished: %b,\n"
+ "\tredeem payout: %s QORT,\n"
+ "\texpected Litecoin: %s LTC,\n"
+ "\tcurrent block height: %d,\n",
tradeData.qortalAtAddress,
tradeData.mode,
tradeData.qortalCreator,
epochMilliFormatter.apply(tradeData.creationTimestamp),
Amounts.prettyAmount(tradeData.qortBalance),
atData.getIsFinished(),
Amounts.prettyAmount(tradeData.qortAmount),
Amounts.prettyAmount(tradeData.expectedForeignAmount),
currentBlockHeight));
if (tradeData.mode != AcctMode.OFFERING && tradeData.mode != AcctMode.CANCELLED) {
System.out.println(String.format("\trefund timeout: %d minutes,\n"
+ "\trefund height: block %d,\n"
+ "\tHASH160 of secret-A: %s,\n"
+ "\tLitecoin P2SH-A nLockTime: %d (%s),\n"
+ "\ttrade partner: %s\n"
+ "\tpartner's receiving address: %s",
tradeData.refundTimeout,
tradeData.tradeRefundHeight,
HashCode.fromBytes(tradeData.hashOfSecretA).toString().substring(0, 40),
tradeData.lockTimeA, epochMilliFormatter.apply(tradeData.lockTimeA * 1000L),
tradeData.qortalPartnerAddress,
tradeData.qortalPartnerReceivingAddress));
}
}
private PrivateKeyAccount createTradeAccount(Repository repository) {
// We actually use a known test account with funds to avoid PoW compute
return Common.getTestAccount(repository, "alice");
}
} }

View File

@ -1,771 +1,58 @@
package org.qortal.test.crosschain.piratechainv3; package org.qortal.test.crosschain.piratechainv3;
import com.google.common.hash.HashCode; import com.google.common.hash.HashCode;
import com.google.common.primitives.Bytes; import org.qortal.crosschain.ACCT;
import org.junit.Before;
import org.junit.Test;
import org.qortal.account.Account;
import org.qortal.account.PrivateKeyAccount;
import org.qortal.asset.Asset;
import org.qortal.block.Block;
import org.qortal.crosschain.AcctMode;
import org.qortal.crosschain.PirateChainACCTv3; import org.qortal.crosschain.PirateChainACCTv3;
import org.qortal.crypto.Crypto; import org.qortal.test.crosschain.ACCTTests;
import org.qortal.data.at.ATData;
import org.qortal.data.at.ATStateData;
import org.qortal.data.crosschain.CrossChainTradeData;
import org.qortal.data.transaction.BaseTransactionData;
import org.qortal.data.transaction.DeployAtTransactionData;
import org.qortal.data.transaction.MessageTransactionData;
import org.qortal.data.transaction.TransactionData;
import org.qortal.group.Group;
import org.qortal.repository.DataException;
import org.qortal.repository.Repository;
import org.qortal.repository.RepositoryManager;
import org.qortal.test.common.BlockUtils;
import org.qortal.test.common.Common;
import org.qortal.test.common.TransactionUtils;
import org.qortal.transaction.DeployAtTransaction;
import org.qortal.transaction.MessageTransaction;
import org.qortal.utils.Amounts;
import java.time.Instant; public class PirateChainACCTv3Tests extends ACCTTests {
import java.time.LocalDateTime;
import java.time.ZoneOffset;
import java.time.format.DateTimeFormatter;
import java.time.format.FormatStyle;
import java.util.Arrays;
import java.util.List;
import java.util.Random;
import java.util.function.Function;
import static org.junit.Assert.*;
public class PirateChainACCTv3Tests extends Common {
public static final byte[] secretA = "This string is exactly 32 bytes!".getBytes();
public static final byte[] hashOfSecretA = Crypto.hash160(secretA); // daf59884b4d1aec8c1b17102530909ee43c0151a
public static final byte[] pirateChainPublicKey = HashCode.fromString("aabb00bb11bb22bb33bb44bb55bb66bb77bb88bb99cc00cc11cc22cc33cc44cc55").asBytes(); // 33 bytes public static final byte[] pirateChainPublicKey = HashCode.fromString("aabb00bb11bb22bb33bb44bb55bb66bb77bb88bb99cc00cc11cc22cc33cc44cc55").asBytes(); // 33 bytes
public static final int tradeTimeout = 20; // blocks private static final String SYMBOL = "ARRR";
public static final long redeemAmount = 80_40200000L; private static final String NAME = "Pirate Chain";
public static final long fundingAmount = 123_45600000L;
public static final long arrrAmount = 864200L; // 0.00864200 ARRR
private static final Random RANDOM = new Random(); @Override
protected byte[] getPublicKey() {
@Before return pirateChainPublicKey;
public void beforeTest() throws DataException {
Common.useDefaultSettings();
} }
@Test @Override
public void testCompile() { protected byte[] buildQortalAT(String address, byte[] publicKey, long redeemAmount, long foreignAmount, int tradeTimeout) {
PrivateKeyAccount tradeAccount = createTradeAccount(null); return PirateChainACCTv3.buildQortalAT(address, publicKey, redeemAmount, foreignAmount, tradeTimeout);
byte[] creationBytes = PirateChainACCTv3.buildQortalAT(tradeAccount.getAddress(), pirateChainPublicKey, redeemAmount, arrrAmount, tradeTimeout);
assertNotNull(creationBytes);
System.out.println("AT creation bytes: " + HashCode.fromBytes(creationBytes).toString());
} }
@Test @Override
public void testDeploy() throws DataException { protected ACCT getInstance() {
try (final Repository repository = RepositoryManager.getRepository()) { return PirateChainACCTv3.getInstance();
PrivateKeyAccount deployer = Common.getTestAccount(repository, "chloe");
PrivateKeyAccount tradeAccount = createTradeAccount(repository);
PrivateKeyAccount partner = Common.getTestAccount(repository, "dilbert");
long deployersInitialBalance = deployer.getConfirmedBalance(Asset.QORT);
long partnersInitialBalance = partner.getConfirmedBalance(Asset.QORT);
DeployAtTransaction deployAtTransaction = doDeploy(repository, deployer, tradeAccount.getAddress());
long expectedBalance = deployersInitialBalance - fundingAmount - deployAtTransaction.getTransactionData().getFee();
long actualBalance = deployer.getConfirmedBalance(Asset.QORT);
assertEquals("Deployer's post-deployment balance incorrect", expectedBalance, actualBalance);
expectedBalance = fundingAmount;
actualBalance = deployAtTransaction.getATAccount().getConfirmedBalance(Asset.QORT);
assertEquals("AT's post-deployment balance incorrect", expectedBalance, actualBalance);
expectedBalance = partnersInitialBalance;
actualBalance = partner.getConfirmedBalance(Asset.QORT);
assertEquals("Partner's post-deployment balance incorrect", expectedBalance, actualBalance);
// Test orphaning
BlockUtils.orphanLastBlock(repository);
expectedBalance = deployersInitialBalance;
actualBalance = deployer.getConfirmedBalance(Asset.QORT);
assertEquals("Deployer's post-orphan/pre-deployment balance incorrect", expectedBalance, actualBalance);
expectedBalance = 0;
actualBalance = deployAtTransaction.getATAccount().getConfirmedBalance(Asset.QORT);
assertEquals("AT's post-orphan/pre-deployment balance incorrect", expectedBalance, actualBalance);
expectedBalance = partnersInitialBalance;
actualBalance = partner.getConfirmedBalance(Asset.QORT);
assertEquals("Partner's post-orphan/pre-deployment balance incorrect", expectedBalance, actualBalance);
}
} }
@SuppressWarnings("unused") @Override
@Test protected int calcRefundTimeout(long partnersOfferMessageTransactionTimestamp, int lockTimeA) {
public void testOfferCancel() throws DataException { return PirateChainACCTv3.calcRefundTimeout(partnersOfferMessageTransactionTimestamp, lockTimeA);
try (final Repository repository = RepositoryManager.getRepository()) {
PrivateKeyAccount deployer = Common.getTestAccount(repository, "chloe");
PrivateKeyAccount tradeAccount = createTradeAccount(repository);
PrivateKeyAccount partner = Common.getTestAccount(repository, "dilbert");
long deployersInitialBalance = deployer.getConfirmedBalance(Asset.QORT);
long partnersInitialBalance = partner.getConfirmedBalance(Asset.QORT);
DeployAtTransaction deployAtTransaction = doDeploy(repository, deployer, tradeAccount.getAddress());
Account at = deployAtTransaction.getATAccount();
String atAddress = at.getAddress();
long deployAtFee = deployAtTransaction.getTransactionData().getFee();
long deployersPostDeploymentBalance = deployersInitialBalance - fundingAmount - deployAtFee;
// Send creator's address to AT, instead of typical partner's address
byte[] messageData = PirateChainACCTv3.getInstance().buildCancelMessage(deployer.getAddress());
MessageTransaction messageTransaction = sendMessage(repository, deployer, messageData, atAddress);
long messageFee = messageTransaction.getTransactionData().getFee();
// AT should process 'cancel' message in next block
BlockUtils.mintBlock(repository);
describeAt(repository, atAddress);
// Check AT is finished
ATData atData = repository.getATRepository().fromATAddress(atAddress);
assertTrue(atData.getIsFinished());
// AT should be in CANCELLED mode
CrossChainTradeData tradeData = PirateChainACCTv3.getInstance().populateTradeData(repository, atData);
assertEquals(AcctMode.CANCELLED, tradeData.mode);
// Check balances
long expectedMinimumBalance = deployersPostDeploymentBalance;
long expectedMaximumBalance = deployersInitialBalance - deployAtFee - messageFee;
long actualBalance = deployer.getConfirmedBalance(Asset.QORT);
assertTrue(String.format("Deployer's balance %s should be above minimum %s", actualBalance, expectedMinimumBalance), actualBalance > expectedMinimumBalance);
assertTrue(String.format("Deployer's balance %s should be below maximum %s", actualBalance, expectedMaximumBalance), actualBalance < expectedMaximumBalance);
// Test orphaning
BlockUtils.orphanLastBlock(repository);
// Check balances
long expectedBalance = deployersPostDeploymentBalance - messageFee;
actualBalance = deployer.getConfirmedBalance(Asset.QORT);
assertEquals("Deployer's post-orphan/pre-refund balance incorrect", expectedBalance, actualBalance);
}
} }
@SuppressWarnings("unused") @Override
@Test protected byte[] buildTradeMessage(String address, byte[] publicKey, byte[] hashOfSecretA, int lockTimeA, int refundTimeout) {
public void testOfferCancelInvalidLength() throws DataException { return PirateChainACCTv3.buildTradeMessage(address, publicKey, hashOfSecretA, lockTimeA, refundTimeout);
try (final Repository repository = RepositoryManager.getRepository()) {
PrivateKeyAccount deployer = Common.getTestAccount(repository, "chloe");
PrivateKeyAccount tradeAccount = createTradeAccount(repository);
PrivateKeyAccount partner = Common.getTestAccount(repository, "dilbert");
long deployersInitialBalance = deployer.getConfirmedBalance(Asset.QORT);
long partnersInitialBalance = partner.getConfirmedBalance(Asset.QORT);
DeployAtTransaction deployAtTransaction = doDeploy(repository, deployer, tradeAccount.getAddress());
Account at = deployAtTransaction.getATAccount();
String atAddress = at.getAddress();
long deployAtFee = deployAtTransaction.getTransactionData().getFee();
long deployersPostDeploymentBalance = deployersInitialBalance - fundingAmount - deployAtFee;
// Instead of sending creator's address to AT, send too-short/invalid message
byte[] messageData = new byte[7];
RANDOM.nextBytes(messageData);
MessageTransaction messageTransaction = sendMessage(repository, deployer, messageData, atAddress);
long messageFee = messageTransaction.getTransactionData().getFee();
// AT should process 'cancel' message in next block
// As message is too short, it will be padded to 32bytes but cancel code doesn't care about message content, so should be ok
BlockUtils.mintBlock(repository);
describeAt(repository, atAddress);
// Check AT is finished
ATData atData = repository.getATRepository().fromATAddress(atAddress);
assertTrue(atData.getIsFinished());
// AT should be in CANCELLED mode
CrossChainTradeData tradeData = PirateChainACCTv3.getInstance().populateTradeData(repository, atData);
assertEquals(AcctMode.CANCELLED, tradeData.mode);
}
} }
@SuppressWarnings("unused") @Override
@Test protected byte[] buildRedeemMessage(byte[] secretA, String address) {
public void testTradingInfoProcessing() throws DataException { return PirateChainACCTv3.buildRedeemMessage(secretA, address);
try (final Repository repository = RepositoryManager.getRepository()) {
PrivateKeyAccount deployer = Common.getTestAccount(repository, "chloe");
PrivateKeyAccount tradeAccount = createTradeAccount(repository);
PrivateKeyAccount partner = Common.getTestAccount(repository, "dilbert");
long deployersInitialBalance = deployer.getConfirmedBalance(Asset.QORT);
long partnersInitialBalance = partner.getConfirmedBalance(Asset.QORT);
DeployAtTransaction deployAtTransaction = doDeploy(repository, deployer, tradeAccount.getAddress());
Account at = deployAtTransaction.getATAccount();
String atAddress = at.getAddress();
long partnersOfferMessageTransactionTimestamp = System.currentTimeMillis();
int lockTimeA = calcTestLockTimeA(partnersOfferMessageTransactionTimestamp);
int refundTimeout = PirateChainACCTv3.calcRefundTimeout(partnersOfferMessageTransactionTimestamp, lockTimeA);
// Send trade info to AT
byte[] messageData = PirateChainACCTv3.buildTradeMessage(partner.getAddress(), pirateChainPublicKey, hashOfSecretA, lockTimeA, refundTimeout);
MessageTransaction messageTransaction = sendMessage(repository, tradeAccount, messageData, atAddress);
Block postDeploymentBlock = BlockUtils.mintBlock(repository);
int postDeploymentBlockHeight = postDeploymentBlock.getBlockData().getHeight();
long deployAtFee = deployAtTransaction.getTransactionData().getFee();
long deployersPostDeploymentBalance = deployersInitialBalance - fundingAmount - deployAtFee;
describeAt(repository, atAddress);
System.out.println(String.format("pirateChainPublicKey: %s", HashCode.fromBytes(pirateChainPublicKey)));
ATData atData = repository.getATRepository().fromATAddress(atAddress);
CrossChainTradeData tradeData = PirateChainACCTv3.getInstance().populateTradeData(repository, atData);
// AT should be in TRADE mode
assertEquals(AcctMode.TRADING, tradeData.mode);
// Check hashOfSecretA was extracted correctly
assertTrue(Arrays.equals(hashOfSecretA, tradeData.hashOfSecretA));
// Check trade partner Qortal address was extracted correctly
assertEquals(partner.getAddress(), tradeData.qortalPartnerAddress);
// Check trade partner's Litecoin PKH was extracted correctly
assertTrue(Arrays.equals(pirateChainPublicKey, tradeData.partnerForeignPKH));
// Test orphaning
BlockUtils.orphanToBlock(repository, postDeploymentBlockHeight);
// Check balances
long expectedBalance = deployersPostDeploymentBalance;
long actualBalance = deployer.getConfirmedBalance(Asset.QORT);
assertEquals("Deployer's post-orphan/pre-refund balance incorrect", expectedBalance, actualBalance);
}
} }
// TEST SENDING TRADING INFO BUT NOT FROM AT CREATOR (SHOULD BE IGNORED) @Override
@SuppressWarnings("unused") protected byte[] getCodeBytesHash() {
@Test return PirateChainACCTv3.CODE_BYTES_HASH;
public void testIncorrectTradeSender() throws DataException {
try (final Repository repository = RepositoryManager.getRepository()) {
PrivateKeyAccount deployer = Common.getTestAccount(repository, "chloe");
PrivateKeyAccount tradeAccount = createTradeAccount(repository);
PrivateKeyAccount partner = Common.getTestAccount(repository, "dilbert");
PrivateKeyAccount bystander = Common.getTestAccount(repository, "bob");
long deployersInitialBalance = deployer.getConfirmedBalance(Asset.QORT);
long partnersInitialBalance = partner.getConfirmedBalance(Asset.QORT);
DeployAtTransaction deployAtTransaction = doDeploy(repository, deployer, tradeAccount.getAddress());
Account at = deployAtTransaction.getATAccount();
String atAddress = at.getAddress();
long partnersOfferMessageTransactionTimestamp = System.currentTimeMillis();
int lockTimeA = calcTestLockTimeA(partnersOfferMessageTransactionTimestamp);
int refundTimeout = PirateChainACCTv3.calcRefundTimeout(partnersOfferMessageTransactionTimestamp, lockTimeA);
// Send trade info to AT BUT NOT FROM AT CREATOR
byte[] messageData = PirateChainACCTv3.buildTradeMessage(partner.getAddress(), pirateChainPublicKey, hashOfSecretA, lockTimeA, refundTimeout);
MessageTransaction messageTransaction = sendMessage(repository, bystander, messageData, atAddress);
BlockUtils.mintBlock(repository);
long expectedBalance = partnersInitialBalance;
long actualBalance = partner.getConfirmedBalance(Asset.QORT);
assertEquals("Partner's post-initial-payout balance incorrect", expectedBalance, actualBalance);
describeAt(repository, atAddress);
ATData atData = repository.getATRepository().fromATAddress(atAddress);
CrossChainTradeData tradeData = PirateChainACCTv3.getInstance().populateTradeData(repository, atData);
// AT should still be in OFFER mode
assertEquals(AcctMode.OFFERING, tradeData.mode);
}
} }
@SuppressWarnings("unused") @Override
@Test protected String getSymbol() {
public void testAutomaticTradeRefund() throws DataException { return SYMBOL;
try (final Repository repository = RepositoryManager.getRepository()) {
PrivateKeyAccount deployer = Common.getTestAccount(repository, "chloe");
PrivateKeyAccount tradeAccount = createTradeAccount(repository);
PrivateKeyAccount partner = Common.getTestAccount(repository, "dilbert");
long deployersInitialBalance = deployer.getConfirmedBalance(Asset.QORT);
long partnersInitialBalance = partner.getConfirmedBalance(Asset.QORT);
DeployAtTransaction deployAtTransaction = doDeploy(repository, deployer, tradeAccount.getAddress());
Account at = deployAtTransaction.getATAccount();
String atAddress = at.getAddress();
long partnersOfferMessageTransactionTimestamp = System.currentTimeMillis();
int lockTimeA = calcTestLockTimeA(partnersOfferMessageTransactionTimestamp);
int refundTimeout = PirateChainACCTv3.calcRefundTimeout(partnersOfferMessageTransactionTimestamp, lockTimeA);
// Send trade info to AT
byte[] messageData = PirateChainACCTv3.buildTradeMessage(partner.getAddress(), pirateChainPublicKey, hashOfSecretA, lockTimeA, refundTimeout);
MessageTransaction messageTransaction = sendMessage(repository, tradeAccount, messageData, atAddress);
Block postDeploymentBlock = BlockUtils.mintBlock(repository);
int postDeploymentBlockHeight = postDeploymentBlock.getBlockData().getHeight();
// Check refund
long deployAtFee = deployAtTransaction.getTransactionData().getFee();
long deployersPostDeploymentBalance = deployersInitialBalance - fundingAmount - deployAtFee;
checkTradeRefund(repository, deployer, deployersInitialBalance, deployAtFee);
describeAt(repository, atAddress);
// Check AT is finished
ATData atData = repository.getATRepository().fromATAddress(atAddress);
assertTrue(atData.getIsFinished());
// AT should be in REFUNDED mode
CrossChainTradeData tradeData = PirateChainACCTv3.getInstance().populateTradeData(repository, atData);
assertEquals(AcctMode.REFUNDED, tradeData.mode);
// Test orphaning
BlockUtils.orphanToBlock(repository, postDeploymentBlockHeight);
// Check balances
long expectedBalance = deployersPostDeploymentBalance;
long actualBalance = deployer.getConfirmedBalance(Asset.QORT);
assertEquals("Deployer's post-orphan/pre-refund balance incorrect", expectedBalance, actualBalance);
}
} }
@SuppressWarnings("unused") @Override
@Test protected String getName() {
public void testCorrectSecretCorrectSender() throws DataException { return NAME;
try (final Repository repository = RepositoryManager.getRepository()) {
PrivateKeyAccount deployer = Common.getTestAccount(repository, "chloe");
PrivateKeyAccount tradeAccount = createTradeAccount(repository);
PrivateKeyAccount partner = Common.getTestAccount(repository, "dilbert");
long deployersInitialBalance = deployer.getConfirmedBalance(Asset.QORT);
long partnersInitialBalance = partner.getConfirmedBalance(Asset.QORT);
DeployAtTransaction deployAtTransaction = doDeploy(repository, deployer, tradeAccount.getAddress());
Account at = deployAtTransaction.getATAccount();
String atAddress = at.getAddress();
long partnersOfferMessageTransactionTimestamp = System.currentTimeMillis();
int lockTimeA = calcTestLockTimeA(partnersOfferMessageTransactionTimestamp);
int refundTimeout = PirateChainACCTv3.calcRefundTimeout(partnersOfferMessageTransactionTimestamp, lockTimeA);
// Send trade info to AT
byte[] messageData = PirateChainACCTv3.buildTradeMessage(partner.getAddress(), pirateChainPublicKey, hashOfSecretA, lockTimeA, refundTimeout);
MessageTransaction messageTransaction = sendMessage(repository, tradeAccount, messageData, atAddress);
// Give AT time to process message
BlockUtils.mintBlock(repository);
// Send correct secret to AT, from correct account
messageData = PirateChainACCTv3.buildRedeemMessage(secretA, partner.getAddress());
messageTransaction = sendMessage(repository, partner, messageData, atAddress);
// AT should send funds in the next block
ATStateData preRedeemAtStateData = repository.getATRepository().getLatestATState(atAddress);
BlockUtils.mintBlock(repository);
describeAt(repository, atAddress);
// Check AT is finished
ATData atData = repository.getATRepository().fromATAddress(atAddress);
assertTrue(atData.getIsFinished());
// AT should be in REDEEMED mode
CrossChainTradeData tradeData = PirateChainACCTv3.getInstance().populateTradeData(repository, atData);
assertEquals(AcctMode.REDEEMED, tradeData.mode);
// Check balances
long expectedBalance = partnersInitialBalance - messageTransaction.getTransactionData().getFee() + redeemAmount;
long actualBalance = partner.getConfirmedBalance(Asset.QORT);
assertEquals("Partner's post-redeem balance incorrect", expectedBalance, actualBalance);
// Orphan redeem
BlockUtils.orphanLastBlock(repository);
// Check balances
expectedBalance = partnersInitialBalance - messageTransaction.getTransactionData().getFee();
actualBalance = partner.getConfirmedBalance(Asset.QORT);
assertEquals("Partner's post-orphan/pre-redeem balance incorrect", expectedBalance, actualBalance);
// Check AT state
ATStateData postOrphanAtStateData = repository.getATRepository().getLatestATState(atAddress);
assertTrue("AT states mismatch", Arrays.equals(preRedeemAtStateData.getStateData(), postOrphanAtStateData.getStateData()));
}
} }
@SuppressWarnings("unused")
@Test
public void testCorrectSecretIncorrectSender() throws DataException {
try (final Repository repository = RepositoryManager.getRepository()) {
PrivateKeyAccount deployer = Common.getTestAccount(repository, "chloe");
PrivateKeyAccount tradeAccount = createTradeAccount(repository);
PrivateKeyAccount partner = Common.getTestAccount(repository, "dilbert");
PrivateKeyAccount bystander = Common.getTestAccount(repository, "bob");
long deployersInitialBalance = deployer.getConfirmedBalance(Asset.QORT);
long partnersInitialBalance = partner.getConfirmedBalance(Asset.QORT);
DeployAtTransaction deployAtTransaction = doDeploy(repository, deployer, tradeAccount.getAddress());
long deployAtFee = deployAtTransaction.getTransactionData().getFee();
Account at = deployAtTransaction.getATAccount();
String atAddress = at.getAddress();
long partnersOfferMessageTransactionTimestamp = System.currentTimeMillis();
int lockTimeA = calcTestLockTimeA(partnersOfferMessageTransactionTimestamp);
int refundTimeout = PirateChainACCTv3.calcRefundTimeout(partnersOfferMessageTransactionTimestamp, lockTimeA);
// Send trade info to AT
byte[] messageData = PirateChainACCTv3.buildTradeMessage(partner.getAddress(), pirateChainPublicKey, hashOfSecretA, lockTimeA, refundTimeout);
MessageTransaction messageTransaction = sendMessage(repository, tradeAccount, messageData, atAddress);
// Give AT time to process message
BlockUtils.mintBlock(repository);
// Send correct secret to AT, but from wrong account
messageData = PirateChainACCTv3.buildRedeemMessage(secretA, partner.getAddress());
messageTransaction = sendMessage(repository, bystander, messageData, atAddress);
// AT should NOT send funds in the next block
ATStateData preRedeemAtStateData = repository.getATRepository().getLatestATState(atAddress);
BlockUtils.mintBlock(repository);
describeAt(repository, atAddress);
// Check AT is NOT finished
ATData atData = repository.getATRepository().fromATAddress(atAddress);
assertFalse(atData.getIsFinished());
// AT should still be in TRADE mode
CrossChainTradeData tradeData = PirateChainACCTv3.getInstance().populateTradeData(repository, atData);
assertEquals(AcctMode.TRADING, tradeData.mode);
// Check balances
long expectedBalance = partnersInitialBalance;
long actualBalance = partner.getConfirmedBalance(Asset.QORT);
assertEquals("Partner's balance incorrect", expectedBalance, actualBalance);
// Check eventual refund
checkTradeRefund(repository, deployer, deployersInitialBalance, deployAtFee);
}
}
@SuppressWarnings("unused")
@Test
public void testIncorrectSecretCorrectSender() throws DataException {
try (final Repository repository = RepositoryManager.getRepository()) {
PrivateKeyAccount deployer = Common.getTestAccount(repository, "chloe");
PrivateKeyAccount tradeAccount = createTradeAccount(repository);
PrivateKeyAccount partner = Common.getTestAccount(repository, "dilbert");
long deployersInitialBalance = deployer.getConfirmedBalance(Asset.QORT);
long partnersInitialBalance = partner.getConfirmedBalance(Asset.QORT);
DeployAtTransaction deployAtTransaction = doDeploy(repository, deployer, tradeAccount.getAddress());
long deployAtFee = deployAtTransaction.getTransactionData().getFee();
Account at = deployAtTransaction.getATAccount();
String atAddress = at.getAddress();
long partnersOfferMessageTransactionTimestamp = System.currentTimeMillis();
int lockTimeA = calcTestLockTimeA(partnersOfferMessageTransactionTimestamp);
int refundTimeout = PirateChainACCTv3.calcRefundTimeout(partnersOfferMessageTransactionTimestamp, lockTimeA);
// Send trade info to AT
byte[] messageData = PirateChainACCTv3.buildTradeMessage(partner.getAddress(), pirateChainPublicKey, hashOfSecretA, lockTimeA, refundTimeout);
MessageTransaction messageTransaction = sendMessage(repository, tradeAccount, messageData, atAddress);
// Give AT time to process message
BlockUtils.mintBlock(repository);
// Send incorrect secret to AT, from correct account
byte[] wrongSecret = new byte[32];
RANDOM.nextBytes(wrongSecret);
messageData = PirateChainACCTv3.buildRedeemMessage(wrongSecret, partner.getAddress());
messageTransaction = sendMessage(repository, partner, messageData, atAddress);
// AT should NOT send funds in the next block
ATStateData preRedeemAtStateData = repository.getATRepository().getLatestATState(atAddress);
BlockUtils.mintBlock(repository);
describeAt(repository, atAddress);
// Check AT is NOT finished
ATData atData = repository.getATRepository().fromATAddress(atAddress);
assertFalse(atData.getIsFinished());
// AT should still be in TRADE mode
CrossChainTradeData tradeData = PirateChainACCTv3.getInstance().populateTradeData(repository, atData);
assertEquals(AcctMode.TRADING, tradeData.mode);
long expectedBalance = partnersInitialBalance - messageTransaction.getTransactionData().getFee();
long actualBalance = partner.getConfirmedBalance(Asset.QORT);
assertEquals("Partner's balance incorrect", expectedBalance, actualBalance);
// Check eventual refund
checkTradeRefund(repository, deployer, deployersInitialBalance, deployAtFee);
}
}
@SuppressWarnings("unused")
@Test
public void testCorrectSecretCorrectSenderInvalidMessageLength() throws DataException {
try (final Repository repository = RepositoryManager.getRepository()) {
PrivateKeyAccount deployer = Common.getTestAccount(repository, "chloe");
PrivateKeyAccount tradeAccount = createTradeAccount(repository);
PrivateKeyAccount partner = Common.getTestAccount(repository, "dilbert");
long deployersInitialBalance = deployer.getConfirmedBalance(Asset.QORT);
long partnersInitialBalance = partner.getConfirmedBalance(Asset.QORT);
DeployAtTransaction deployAtTransaction = doDeploy(repository, deployer, tradeAccount.getAddress());
Account at = deployAtTransaction.getATAccount();
String atAddress = at.getAddress();
long partnersOfferMessageTransactionTimestamp = System.currentTimeMillis();
int lockTimeA = calcTestLockTimeA(partnersOfferMessageTransactionTimestamp);
int refundTimeout = PirateChainACCTv3.calcRefundTimeout(partnersOfferMessageTransactionTimestamp, lockTimeA);
// Send trade info to AT
byte[] messageData = PirateChainACCTv3.buildTradeMessage(partner.getAddress(), pirateChainPublicKey, hashOfSecretA, lockTimeA, refundTimeout);
MessageTransaction messageTransaction = sendMessage(repository, tradeAccount, messageData, atAddress);
// Give AT time to process message
BlockUtils.mintBlock(repository);
// Send correct secret to AT, from correct account, but missing receive address, hence incorrect length
messageData = Bytes.concat(secretA);
messageTransaction = sendMessage(repository, partner, messageData, atAddress);
// AT should NOT send funds in the next block
ATStateData preRedeemAtStateData = repository.getATRepository().getLatestATState(atAddress);
BlockUtils.mintBlock(repository);
describeAt(repository, atAddress);
// Check AT is NOT finished
ATData atData = repository.getATRepository().fromATAddress(atAddress);
assertFalse(atData.getIsFinished());
// AT should be in TRADING mode
CrossChainTradeData tradeData = PirateChainACCTv3.getInstance().populateTradeData(repository, atData);
assertEquals(AcctMode.TRADING, tradeData.mode);
}
}
@SuppressWarnings("unused")
@Test
public void testDescribeDeployed() throws DataException {
try (final Repository repository = RepositoryManager.getRepository()) {
PrivateKeyAccount deployer = Common.getTestAccount(repository, "chloe");
PrivateKeyAccount tradeAccount = createTradeAccount(repository);
PrivateKeyAccount partner = Common.getTestAccount(repository, "dilbert");
long deployersInitialBalance = deployer.getConfirmedBalance(Asset.QORT);
long partnersInitialBalance = partner.getConfirmedBalance(Asset.QORT);
DeployAtTransaction deployAtTransaction = doDeploy(repository, deployer, tradeAccount.getAddress());
List<ATData> executableAts = repository.getATRepository().getAllExecutableATs();
for (ATData atData : executableAts) {
String atAddress = atData.getATAddress();
byte[] codeBytes = atData.getCodeBytes();
byte[] codeHash = Crypto.digest(codeBytes);
System.out.println(String.format("%s: code length: %d byte%s, code hash: %s",
atAddress,
codeBytes.length,
(codeBytes.length != 1 ? "s": ""),
HashCode.fromBytes(codeHash)));
// Not one of ours?
if (!Arrays.equals(codeHash, PirateChainACCTv3.CODE_BYTES_HASH))
continue;
describeAt(repository, atAddress);
}
}
}
private int calcTestLockTimeA(long messageTimestamp) {
return (int) (messageTimestamp / 1000L + tradeTimeout * 60);
}
private DeployAtTransaction doDeploy(Repository repository, PrivateKeyAccount deployer, String tradeAddress) throws DataException {
byte[] creationBytes = PirateChainACCTv3.buildQortalAT(tradeAddress, pirateChainPublicKey, redeemAmount, arrrAmount, tradeTimeout);
long txTimestamp = System.currentTimeMillis();
byte[] lastReference = deployer.getLastReference();
if (lastReference == null) {
System.err.println(String.format("Qortal account %s has no last reference", deployer.getAddress()));
System.exit(2);
}
Long fee = null;
String name = "QORT-ARRR cross-chain trade";
String description = String.format("Qortal-PirateChain cross-chain trade");
String atType = "ACCT";
String tags = "QORT-ARRR ACCT";
BaseTransactionData baseTransactionData = new BaseTransactionData(txTimestamp, Group.NO_GROUP, lastReference, deployer.getPublicKey(), fee, null);
TransactionData deployAtTransactionData = new DeployAtTransactionData(baseTransactionData, name, description, atType, tags, creationBytes, fundingAmount, Asset.QORT);
DeployAtTransaction deployAtTransaction = new DeployAtTransaction(repository, deployAtTransactionData);
fee = deployAtTransaction.calcRecommendedFee();
deployAtTransactionData.setFee(fee);
TransactionUtils.signAndMint(repository, deployAtTransactionData, deployer);
return deployAtTransaction;
}
private MessageTransaction sendMessage(Repository repository, PrivateKeyAccount sender, byte[] data, String recipient) throws DataException {
long txTimestamp = System.currentTimeMillis();
byte[] lastReference = sender.getLastReference();
if (lastReference == null) {
System.err.println(String.format("Qortal account %s has no last reference", sender.getAddress()));
System.exit(2);
}
Long fee = null;
int version = 4;
int nonce = 0;
long amount = 0;
Long assetId = null; // because amount is zero
BaseTransactionData baseTransactionData = new BaseTransactionData(txTimestamp, Group.NO_GROUP, lastReference, sender.getPublicKey(), fee, null);
TransactionData messageTransactionData = new MessageTransactionData(baseTransactionData, version, nonce, recipient, amount, assetId, data, false, false);
MessageTransaction messageTransaction = new MessageTransaction(repository, messageTransactionData);
fee = messageTransaction.calcRecommendedFee();
messageTransactionData.setFee(fee);
TransactionUtils.signAndMint(repository, messageTransactionData, sender);
return messageTransaction;
}
private void checkTradeRefund(Repository repository, Account deployer, long deployersInitialBalance, long deployAtFee) throws DataException {
long deployersPostDeploymentBalance = deployersInitialBalance - fundingAmount - deployAtFee;
int refundTimeout = tradeTimeout / 2 + 1; // close enough
// AT should automatically refund deployer after 'refundTimeout' blocks
for (int blockCount = 0; blockCount <= refundTimeout; ++blockCount)
BlockUtils.mintBlock(repository);
// We don't bother to exactly calculate QORT spent running AT for several blocks, but we do know the expected range
long expectedMinimumBalance = deployersPostDeploymentBalance;
long expectedMaximumBalance = deployersInitialBalance - deployAtFee;
long actualBalance = deployer.getConfirmedBalance(Asset.QORT);
assertTrue(String.format("Deployer's balance %s should be above minimum %s", actualBalance, expectedMinimumBalance), actualBalance > expectedMinimumBalance);
assertTrue(String.format("Deployer's balance %s should be below maximum %s", actualBalance, expectedMaximumBalance), actualBalance < expectedMaximumBalance);
}
private void describeAt(Repository repository, String atAddress) throws DataException {
ATData atData = repository.getATRepository().fromATAddress(atAddress);
CrossChainTradeData tradeData = PirateChainACCTv3.getInstance().populateTradeData(repository, atData);
Function<Long, String> epochMilliFormatter = (timestamp) -> LocalDateTime.ofInstant(Instant.ofEpochMilli(timestamp), ZoneOffset.UTC).format(DateTimeFormatter.ofLocalizedDateTime(FormatStyle.MEDIUM));
int currentBlockHeight = repository.getBlockRepository().getBlockchainHeight();
System.out.print(String.format("%s:\n"
+ "\tmode: %s\n"
+ "\tcreator: %s,\n"
+ "\tcreation timestamp: %s,\n"
+ "\tcurrent balance: %s QORT,\n"
+ "\tis finished: %b,\n"
+ "\tredeem payout: %s QORT,\n"
+ "\texpected ARRR: %s ARRR,\n"
+ "\tcurrent block height: %d,\n",
tradeData.qortalAtAddress,
tradeData.mode,
tradeData.qortalCreator,
epochMilliFormatter.apply(tradeData.creationTimestamp),
Amounts.prettyAmount(tradeData.qortBalance),
atData.getIsFinished(),
Amounts.prettyAmount(tradeData.qortAmount),
Amounts.prettyAmount(tradeData.expectedForeignAmount),
currentBlockHeight));
if (tradeData.mode != AcctMode.OFFERING && tradeData.mode != AcctMode.CANCELLED) {
System.out.println(String.format("\trefund timeout: %d minutes,\n"
+ "\trefund height: block %d,\n"
+ "\tHASH160 of secret-A: %s,\n"
+ "\tPirate Chain P2SH-A nLockTime: %d (%s),\n"
+ "\ttrade partner: %s\n"
+ "\tpartner's receiving address: %s",
tradeData.refundTimeout,
tradeData.tradeRefundHeight,
HashCode.fromBytes(tradeData.hashOfSecretA).toString().substring(0, 40),
tradeData.lockTimeA, epochMilliFormatter.apply(tradeData.lockTimeA * 1000L),
tradeData.qortalPartnerAddress,
tradeData.qortalPartnerReceivingAddress));
}
}
private PrivateKeyAccount createTradeAccount(Repository repository) {
// We actually use a known test account with funds to avoid PoW compute
return Common.getTestAccount(repository, "alice");
}
} }

View File

@ -1,769 +1,58 @@
package org.qortal.test.crosschain.ravencoinv3; package org.qortal.test.crosschain.ravencoinv3;
import com.google.common.hash.HashCode; import com.google.common.hash.HashCode;
import com.google.common.primitives.Bytes; import org.qortal.crosschain.ACCT;
import org.junit.Before;
import org.junit.Test;
import org.qortal.account.Account;
import org.qortal.account.PrivateKeyAccount;
import org.qortal.asset.Asset;
import org.qortal.block.Block;
import org.qortal.crosschain.AcctMode;
import org.qortal.crosschain.RavencoinACCTv3; import org.qortal.crosschain.RavencoinACCTv3;
import org.qortal.crypto.Crypto; import org.qortal.test.crosschain.ACCTTests;
import org.qortal.data.at.ATData;
import org.qortal.data.at.ATStateData;
import org.qortal.data.crosschain.CrossChainTradeData;
import org.qortal.data.transaction.BaseTransactionData;
import org.qortal.data.transaction.DeployAtTransactionData;
import org.qortal.data.transaction.MessageTransactionData;
import org.qortal.data.transaction.TransactionData;
import org.qortal.group.Group;
import org.qortal.repository.DataException;
import org.qortal.repository.Repository;
import org.qortal.repository.RepositoryManager;
import org.qortal.test.common.BlockUtils;
import org.qortal.test.common.Common;
import org.qortal.test.common.TransactionUtils;
import org.qortal.transaction.DeployAtTransaction;
import org.qortal.transaction.MessageTransaction;
import org.qortal.utils.Amounts;
import java.time.Instant; public class RavencoinACCTv3Tests extends ACCTTests {
import java.time.LocalDateTime;
import java.time.ZoneOffset;
import java.time.format.DateTimeFormatter;
import java.time.format.FormatStyle;
import java.util.Arrays;
import java.util.List;
import java.util.Random;
import java.util.function.Function;
import static org.junit.Assert.*;
public class RavencoinACCTv3Tests extends Common {
public static final byte[] secretA = "This string is exactly 32 bytes!".getBytes();
public static final byte[] hashOfSecretA = Crypto.hash160(secretA); // daf59884b4d1aec8c1b17102530909ee43c0151a
public static final byte[] ravencoinPublicKeyHash = HashCode.fromString("bb00bb11bb22bb33bb44bb55bb66bb77bb88bb99").asBytes(); public static final byte[] ravencoinPublicKeyHash = HashCode.fromString("bb00bb11bb22bb33bb44bb55bb66bb77bb88bb99").asBytes();
public static final int tradeTimeout = 20; // blocks private static final String SYMBOL = "RVN";
public static final long redeemAmount = 80_40200000L; private static final String NAME = "Ravencoin";
public static final long fundingAmount = 123_45600000L;
public static final long ravencoinAmount = 864200L; // 0.00864200 RVN
private static final Random RANDOM = new Random(); @Override
protected byte[] getPublicKey() {
@Before return ravencoinPublicKeyHash;
public void beforeTest() throws DataException {
Common.useDefaultSettings();
} }
@Test @Override
public void testCompile() { protected byte[] buildQortalAT(String address, byte[] publicKey, long redeemAmount, long foreignAmount, int tradeTimeout) {
PrivateKeyAccount tradeAccount = createTradeAccount(null); return RavencoinACCTv3.buildQortalAT(address, publicKey, redeemAmount, foreignAmount, tradeTimeout);
byte[] creationBytes = RavencoinACCTv3.buildQortalAT(tradeAccount.getAddress(), ravencoinPublicKeyHash, redeemAmount, ravencoinAmount, tradeTimeout);
assertNotNull(creationBytes);
System.out.println("AT creation bytes: " + HashCode.fromBytes(creationBytes).toString());
} }
@Test @Override
public void testDeploy() throws DataException { protected ACCT getInstance() {
try (final Repository repository = RepositoryManager.getRepository()) { return RavencoinACCTv3.getInstance();
PrivateKeyAccount deployer = Common.getTestAccount(repository, "chloe");
PrivateKeyAccount tradeAccount = createTradeAccount(repository);
PrivateKeyAccount partner = Common.getTestAccount(repository, "dilbert");
long deployersInitialBalance = deployer.getConfirmedBalance(Asset.QORT);
long partnersInitialBalance = partner.getConfirmedBalance(Asset.QORT);
DeployAtTransaction deployAtTransaction = doDeploy(repository, deployer, tradeAccount.getAddress());
long expectedBalance = deployersInitialBalance - fundingAmount - deployAtTransaction.getTransactionData().getFee();
long actualBalance = deployer.getConfirmedBalance(Asset.QORT);
assertEquals("Deployer's post-deployment balance incorrect", expectedBalance, actualBalance);
expectedBalance = fundingAmount;
actualBalance = deployAtTransaction.getATAccount().getConfirmedBalance(Asset.QORT);
assertEquals("AT's post-deployment balance incorrect", expectedBalance, actualBalance);
expectedBalance = partnersInitialBalance;
actualBalance = partner.getConfirmedBalance(Asset.QORT);
assertEquals("Partner's post-deployment balance incorrect", expectedBalance, actualBalance);
// Test orphaning
BlockUtils.orphanLastBlock(repository);
expectedBalance = deployersInitialBalance;
actualBalance = deployer.getConfirmedBalance(Asset.QORT);
assertEquals("Deployer's post-orphan/pre-deployment balance incorrect", expectedBalance, actualBalance);
expectedBalance = 0;
actualBalance = deployAtTransaction.getATAccount().getConfirmedBalance(Asset.QORT);
assertEquals("AT's post-orphan/pre-deployment balance incorrect", expectedBalance, actualBalance);
expectedBalance = partnersInitialBalance;
actualBalance = partner.getConfirmedBalance(Asset.QORT);
assertEquals("Partner's post-orphan/pre-deployment balance incorrect", expectedBalance, actualBalance);
}
} }
@SuppressWarnings("unused") @Override
@Test protected int calcRefundTimeout(long partnersOfferMessageTransactionTimestamp, int lockTimeA) {
public void testOfferCancel() throws DataException { return RavencoinACCTv3.calcRefundTimeout(partnersOfferMessageTransactionTimestamp, lockTimeA);
try (final Repository repository = RepositoryManager.getRepository()) {
PrivateKeyAccount deployer = Common.getTestAccount(repository, "chloe");
PrivateKeyAccount tradeAccount = createTradeAccount(repository);
PrivateKeyAccount partner = Common.getTestAccount(repository, "dilbert");
long deployersInitialBalance = deployer.getConfirmedBalance(Asset.QORT);
long partnersInitialBalance = partner.getConfirmedBalance(Asset.QORT);
DeployAtTransaction deployAtTransaction = doDeploy(repository, deployer, tradeAccount.getAddress());
Account at = deployAtTransaction.getATAccount();
String atAddress = at.getAddress();
long deployAtFee = deployAtTransaction.getTransactionData().getFee();
long deployersPostDeploymentBalance = deployersInitialBalance - fundingAmount - deployAtFee;
// Send creator's address to AT, instead of typical partner's address
byte[] messageData = RavencoinACCTv3.getInstance().buildCancelMessage(deployer.getAddress());
MessageTransaction messageTransaction = sendMessage(repository, deployer, messageData, atAddress);
long messageFee = messageTransaction.getTransactionData().getFee();
// AT should process 'cancel' message in next block
BlockUtils.mintBlock(repository);
describeAt(repository, atAddress);
// Check AT is finished
ATData atData = repository.getATRepository().fromATAddress(atAddress);
assertTrue(atData.getIsFinished());
// AT should be in CANCELLED mode
CrossChainTradeData tradeData = RavencoinACCTv3.getInstance().populateTradeData(repository, atData);
assertEquals(AcctMode.CANCELLED, tradeData.mode);
// Check balances
long expectedMinimumBalance = deployersPostDeploymentBalance;
long expectedMaximumBalance = deployersInitialBalance - deployAtFee - messageFee;
long actualBalance = deployer.getConfirmedBalance(Asset.QORT);
assertTrue(String.format("Deployer's balance %s should be above minimum %s", actualBalance, expectedMinimumBalance), actualBalance > expectedMinimumBalance);
assertTrue(String.format("Deployer's balance %s should be below maximum %s", actualBalance, expectedMaximumBalance), actualBalance < expectedMaximumBalance);
// Test orphaning
BlockUtils.orphanLastBlock(repository);
// Check balances
long expectedBalance = deployersPostDeploymentBalance - messageFee;
actualBalance = deployer.getConfirmedBalance(Asset.QORT);
assertEquals("Deployer's post-orphan/pre-refund balance incorrect", expectedBalance, actualBalance);
}
} }
@SuppressWarnings("unused") @Override
@Test protected byte[] buildTradeMessage(String address, byte[] publicKey, byte[] hashOfSecretA, int lockTimeA, int refundTimeout) {
public void testOfferCancelInvalidLength() throws DataException { return RavencoinACCTv3.buildTradeMessage(address, publicKey, hashOfSecretA, lockTimeA, refundTimeout);
try (final Repository repository = RepositoryManager.getRepository()) {
PrivateKeyAccount deployer = Common.getTestAccount(repository, "chloe");
PrivateKeyAccount tradeAccount = createTradeAccount(repository);
PrivateKeyAccount partner = Common.getTestAccount(repository, "dilbert");
long deployersInitialBalance = deployer.getConfirmedBalance(Asset.QORT);
long partnersInitialBalance = partner.getConfirmedBalance(Asset.QORT);
DeployAtTransaction deployAtTransaction = doDeploy(repository, deployer, tradeAccount.getAddress());
Account at = deployAtTransaction.getATAccount();
String atAddress = at.getAddress();
long deployAtFee = deployAtTransaction.getTransactionData().getFee();
long deployersPostDeploymentBalance = deployersInitialBalance - fundingAmount - deployAtFee;
// Instead of sending creator's address to AT, send too-short/invalid message
byte[] messageData = new byte[7];
RANDOM.nextBytes(messageData);
MessageTransaction messageTransaction = sendMessage(repository, deployer, messageData, atAddress);
long messageFee = messageTransaction.getTransactionData().getFee();
// AT should process 'cancel' message in next block
// As message is too short, it will be padded to 32bytes but cancel code doesn't care about message content, so should be ok
BlockUtils.mintBlock(repository);
describeAt(repository, atAddress);
// Check AT is finished
ATData atData = repository.getATRepository().fromATAddress(atAddress);
assertTrue(atData.getIsFinished());
// AT should be in CANCELLED mode
CrossChainTradeData tradeData = RavencoinACCTv3.getInstance().populateTradeData(repository, atData);
assertEquals(AcctMode.CANCELLED, tradeData.mode);
}
} }
@SuppressWarnings("unused") @Override
@Test protected byte[] buildRedeemMessage(byte[] secretA, String address) {
public void testTradingInfoProcessing() throws DataException { return RavencoinACCTv3.buildRedeemMessage(secretA, address);
try (final Repository repository = RepositoryManager.getRepository()) {
PrivateKeyAccount deployer = Common.getTestAccount(repository, "chloe");
PrivateKeyAccount tradeAccount = createTradeAccount(repository);
PrivateKeyAccount partner = Common.getTestAccount(repository, "dilbert");
long deployersInitialBalance = deployer.getConfirmedBalance(Asset.QORT);
long partnersInitialBalance = partner.getConfirmedBalance(Asset.QORT);
DeployAtTransaction deployAtTransaction = doDeploy(repository, deployer, tradeAccount.getAddress());
Account at = deployAtTransaction.getATAccount();
String atAddress = at.getAddress();
long partnersOfferMessageTransactionTimestamp = System.currentTimeMillis();
int lockTimeA = calcTestLockTimeA(partnersOfferMessageTransactionTimestamp);
int refundTimeout = RavencoinACCTv3.calcRefundTimeout(partnersOfferMessageTransactionTimestamp, lockTimeA);
// Send trade info to AT
byte[] messageData = RavencoinACCTv3.buildTradeMessage(partner.getAddress(), ravencoinPublicKeyHash, hashOfSecretA, lockTimeA, refundTimeout);
MessageTransaction messageTransaction = sendMessage(repository, tradeAccount, messageData, atAddress);
Block postDeploymentBlock = BlockUtils.mintBlock(repository);
int postDeploymentBlockHeight = postDeploymentBlock.getBlockData().getHeight();
long deployAtFee = deployAtTransaction.getTransactionData().getFee();
long deployersPostDeploymentBalance = deployersInitialBalance - fundingAmount - deployAtFee;
describeAt(repository, atAddress);
ATData atData = repository.getATRepository().fromATAddress(atAddress);
CrossChainTradeData tradeData = RavencoinACCTv3.getInstance().populateTradeData(repository, atData);
// AT should be in TRADE mode
assertEquals(AcctMode.TRADING, tradeData.mode);
// Check hashOfSecretA was extracted correctly
assertTrue(Arrays.equals(hashOfSecretA, tradeData.hashOfSecretA));
// Check trade partner Qortal address was extracted correctly
assertEquals(partner.getAddress(), tradeData.qortalPartnerAddress);
// Check trade partner's ravencoin PKH was extracted correctly
assertTrue(Arrays.equals(ravencoinPublicKeyHash, tradeData.partnerForeignPKH));
// Test orphaning
BlockUtils.orphanToBlock(repository, postDeploymentBlockHeight);
// Check balances
long expectedBalance = deployersPostDeploymentBalance;
long actualBalance = deployer.getConfirmedBalance(Asset.QORT);
assertEquals("Deployer's post-orphan/pre-refund balance incorrect", expectedBalance, actualBalance);
}
} }
// TEST SENDING TRADING INFO BUT NOT FROM AT CREATOR (SHOULD BE IGNORED) @Override
@SuppressWarnings("unused") protected byte[] getCodeBytesHash() {
@Test return RavencoinACCTv3.CODE_BYTES_HASH;
public void testIncorrectTradeSender() throws DataException {
try (final Repository repository = RepositoryManager.getRepository()) {
PrivateKeyAccount deployer = Common.getTestAccount(repository, "chloe");
PrivateKeyAccount tradeAccount = createTradeAccount(repository);
PrivateKeyAccount partner = Common.getTestAccount(repository, "dilbert");
PrivateKeyAccount bystander = Common.getTestAccount(repository, "bob");
long deployersInitialBalance = deployer.getConfirmedBalance(Asset.QORT);
long partnersInitialBalance = partner.getConfirmedBalance(Asset.QORT);
DeployAtTransaction deployAtTransaction = doDeploy(repository, deployer, tradeAccount.getAddress());
Account at = deployAtTransaction.getATAccount();
String atAddress = at.getAddress();
long partnersOfferMessageTransactionTimestamp = System.currentTimeMillis();
int lockTimeA = calcTestLockTimeA(partnersOfferMessageTransactionTimestamp);
int refundTimeout = RavencoinACCTv3.calcRefundTimeout(partnersOfferMessageTransactionTimestamp, lockTimeA);
// Send trade info to AT BUT NOT FROM AT CREATOR
byte[] messageData = RavencoinACCTv3.buildTradeMessage(partner.getAddress(), ravencoinPublicKeyHash, hashOfSecretA, lockTimeA, refundTimeout);
MessageTransaction messageTransaction = sendMessage(repository, bystander, messageData, atAddress);
BlockUtils.mintBlock(repository);
long expectedBalance = partnersInitialBalance;
long actualBalance = partner.getConfirmedBalance(Asset.QORT);
assertEquals("Partner's post-initial-payout balance incorrect", expectedBalance, actualBalance);
describeAt(repository, atAddress);
ATData atData = repository.getATRepository().fromATAddress(atAddress);
CrossChainTradeData tradeData = RavencoinACCTv3.getInstance().populateTradeData(repository, atData);
// AT should still be in OFFER mode
assertEquals(AcctMode.OFFERING, tradeData.mode);
}
} }
@SuppressWarnings("unused") @Override
@Test protected String getSymbol() {
public void testAutomaticTradeRefund() throws DataException { return SYMBOL;
try (final Repository repository = RepositoryManager.getRepository()) {
PrivateKeyAccount deployer = Common.getTestAccount(repository, "chloe");
PrivateKeyAccount tradeAccount = createTradeAccount(repository);
PrivateKeyAccount partner = Common.getTestAccount(repository, "dilbert");
long deployersInitialBalance = deployer.getConfirmedBalance(Asset.QORT);
long partnersInitialBalance = partner.getConfirmedBalance(Asset.QORT);
DeployAtTransaction deployAtTransaction = doDeploy(repository, deployer, tradeAccount.getAddress());
Account at = deployAtTransaction.getATAccount();
String atAddress = at.getAddress();
long partnersOfferMessageTransactionTimestamp = System.currentTimeMillis();
int lockTimeA = calcTestLockTimeA(partnersOfferMessageTransactionTimestamp);
int refundTimeout = RavencoinACCTv3.calcRefundTimeout(partnersOfferMessageTransactionTimestamp, lockTimeA);
// Send trade info to AT
byte[] messageData = RavencoinACCTv3.buildTradeMessage(partner.getAddress(), ravencoinPublicKeyHash, hashOfSecretA, lockTimeA, refundTimeout);
MessageTransaction messageTransaction = sendMessage(repository, tradeAccount, messageData, atAddress);
Block postDeploymentBlock = BlockUtils.mintBlock(repository);
int postDeploymentBlockHeight = postDeploymentBlock.getBlockData().getHeight();
// Check refund
long deployAtFee = deployAtTransaction.getTransactionData().getFee();
long deployersPostDeploymentBalance = deployersInitialBalance - fundingAmount - deployAtFee;
checkTradeRefund(repository, deployer, deployersInitialBalance, deployAtFee);
describeAt(repository, atAddress);
// Check AT is finished
ATData atData = repository.getATRepository().fromATAddress(atAddress);
assertTrue(atData.getIsFinished());
// AT should be in REFUNDED mode
CrossChainTradeData tradeData = RavencoinACCTv3.getInstance().populateTradeData(repository, atData);
assertEquals(AcctMode.REFUNDED, tradeData.mode);
// Test orphaning
BlockUtils.orphanToBlock(repository, postDeploymentBlockHeight);
// Check balances
long expectedBalance = deployersPostDeploymentBalance;
long actualBalance = deployer.getConfirmedBalance(Asset.QORT);
assertEquals("Deployer's post-orphan/pre-refund balance incorrect", expectedBalance, actualBalance);
}
} }
@SuppressWarnings("unused") @Override
@Test protected String getName() {
public void testCorrectSecretCorrectSender() throws DataException { return NAME;
try (final Repository repository = RepositoryManager.getRepository()) {
PrivateKeyAccount deployer = Common.getTestAccount(repository, "chloe");
PrivateKeyAccount tradeAccount = createTradeAccount(repository);
PrivateKeyAccount partner = Common.getTestAccount(repository, "dilbert");
long deployersInitialBalance = deployer.getConfirmedBalance(Asset.QORT);
long partnersInitialBalance = partner.getConfirmedBalance(Asset.QORT);
DeployAtTransaction deployAtTransaction = doDeploy(repository, deployer, tradeAccount.getAddress());
Account at = deployAtTransaction.getATAccount();
String atAddress = at.getAddress();
long partnersOfferMessageTransactionTimestamp = System.currentTimeMillis();
int lockTimeA = calcTestLockTimeA(partnersOfferMessageTransactionTimestamp);
int refundTimeout = RavencoinACCTv3.calcRefundTimeout(partnersOfferMessageTransactionTimestamp, lockTimeA);
// Send trade info to AT
byte[] messageData = RavencoinACCTv3.buildTradeMessage(partner.getAddress(), ravencoinPublicKeyHash, hashOfSecretA, lockTimeA, refundTimeout);
MessageTransaction messageTransaction = sendMessage(repository, tradeAccount, messageData, atAddress);
// Give AT time to process message
BlockUtils.mintBlock(repository);
// Send correct secret to AT, from correct account
messageData = RavencoinACCTv3.buildRedeemMessage(secretA, partner.getAddress());
messageTransaction = sendMessage(repository, partner, messageData, atAddress);
// AT should send funds in the next block
ATStateData preRedeemAtStateData = repository.getATRepository().getLatestATState(atAddress);
BlockUtils.mintBlock(repository);
describeAt(repository, atAddress);
// Check AT is finished
ATData atData = repository.getATRepository().fromATAddress(atAddress);
assertTrue(atData.getIsFinished());
// AT should be in REDEEMED mode
CrossChainTradeData tradeData = RavencoinACCTv3.getInstance().populateTradeData(repository, atData);
assertEquals(AcctMode.REDEEMED, tradeData.mode);
// Check balances
long expectedBalance = partnersInitialBalance - messageTransaction.getTransactionData().getFee() + redeemAmount;
long actualBalance = partner.getConfirmedBalance(Asset.QORT);
assertEquals("Partner's post-redeem balance incorrect", expectedBalance, actualBalance);
// Orphan redeem
BlockUtils.orphanLastBlock(repository);
// Check balances
expectedBalance = partnersInitialBalance - messageTransaction.getTransactionData().getFee();
actualBalance = partner.getConfirmedBalance(Asset.QORT);
assertEquals("Partner's post-orphan/pre-redeem balance incorrect", expectedBalance, actualBalance);
// Check AT state
ATStateData postOrphanAtStateData = repository.getATRepository().getLatestATState(atAddress);
assertTrue("AT states mismatch", Arrays.equals(preRedeemAtStateData.getStateData(), postOrphanAtStateData.getStateData()));
}
} }
@SuppressWarnings("unused")
@Test
public void testCorrectSecretIncorrectSender() throws DataException {
try (final Repository repository = RepositoryManager.getRepository()) {
PrivateKeyAccount deployer = Common.getTestAccount(repository, "chloe");
PrivateKeyAccount tradeAccount = createTradeAccount(repository);
PrivateKeyAccount partner = Common.getTestAccount(repository, "dilbert");
PrivateKeyAccount bystander = Common.getTestAccount(repository, "bob");
long deployersInitialBalance = deployer.getConfirmedBalance(Asset.QORT);
long partnersInitialBalance = partner.getConfirmedBalance(Asset.QORT);
DeployAtTransaction deployAtTransaction = doDeploy(repository, deployer, tradeAccount.getAddress());
long deployAtFee = deployAtTransaction.getTransactionData().getFee();
Account at = deployAtTransaction.getATAccount();
String atAddress = at.getAddress();
long partnersOfferMessageTransactionTimestamp = System.currentTimeMillis();
int lockTimeA = calcTestLockTimeA(partnersOfferMessageTransactionTimestamp);
int refundTimeout = RavencoinACCTv3.calcRefundTimeout(partnersOfferMessageTransactionTimestamp, lockTimeA);
// Send trade info to AT
byte[] messageData = RavencoinACCTv3.buildTradeMessage(partner.getAddress(), ravencoinPublicKeyHash, hashOfSecretA, lockTimeA, refundTimeout);
MessageTransaction messageTransaction = sendMessage(repository, tradeAccount, messageData, atAddress);
// Give AT time to process message
BlockUtils.mintBlock(repository);
// Send correct secret to AT, but from wrong account
messageData = RavencoinACCTv3.buildRedeemMessage(secretA, partner.getAddress());
messageTransaction = sendMessage(repository, bystander, messageData, atAddress);
// AT should NOT send funds in the next block
ATStateData preRedeemAtStateData = repository.getATRepository().getLatestATState(atAddress);
BlockUtils.mintBlock(repository);
describeAt(repository, atAddress);
// Check AT is NOT finished
ATData atData = repository.getATRepository().fromATAddress(atAddress);
assertFalse(atData.getIsFinished());
// AT should still be in TRADE mode
CrossChainTradeData tradeData = RavencoinACCTv3.getInstance().populateTradeData(repository, atData);
assertEquals(AcctMode.TRADING, tradeData.mode);
// Check balances
long expectedBalance = partnersInitialBalance;
long actualBalance = partner.getConfirmedBalance(Asset.QORT);
assertEquals("Partner's balance incorrect", expectedBalance, actualBalance);
// Check eventual refund
checkTradeRefund(repository, deployer, deployersInitialBalance, deployAtFee);
}
}
@SuppressWarnings("unused")
@Test
public void testIncorrectSecretCorrectSender() throws DataException {
try (final Repository repository = RepositoryManager.getRepository()) {
PrivateKeyAccount deployer = Common.getTestAccount(repository, "chloe");
PrivateKeyAccount tradeAccount = createTradeAccount(repository);
PrivateKeyAccount partner = Common.getTestAccount(repository, "dilbert");
long deployersInitialBalance = deployer.getConfirmedBalance(Asset.QORT);
long partnersInitialBalance = partner.getConfirmedBalance(Asset.QORT);
DeployAtTransaction deployAtTransaction = doDeploy(repository, deployer, tradeAccount.getAddress());
long deployAtFee = deployAtTransaction.getTransactionData().getFee();
Account at = deployAtTransaction.getATAccount();
String atAddress = at.getAddress();
long partnersOfferMessageTransactionTimestamp = System.currentTimeMillis();
int lockTimeA = calcTestLockTimeA(partnersOfferMessageTransactionTimestamp);
int refundTimeout = RavencoinACCTv3.calcRefundTimeout(partnersOfferMessageTransactionTimestamp, lockTimeA);
// Send trade info to AT
byte[] messageData = RavencoinACCTv3.buildTradeMessage(partner.getAddress(), ravencoinPublicKeyHash, hashOfSecretA, lockTimeA, refundTimeout);
MessageTransaction messageTransaction = sendMessage(repository, tradeAccount, messageData, atAddress);
// Give AT time to process message
BlockUtils.mintBlock(repository);
// Send incorrect secret to AT, from correct account
byte[] wrongSecret = new byte[32];
RANDOM.nextBytes(wrongSecret);
messageData = RavencoinACCTv3.buildRedeemMessage(wrongSecret, partner.getAddress());
messageTransaction = sendMessage(repository, partner, messageData, atAddress);
// AT should NOT send funds in the next block
ATStateData preRedeemAtStateData = repository.getATRepository().getLatestATState(atAddress);
BlockUtils.mintBlock(repository);
describeAt(repository, atAddress);
// Check AT is NOT finished
ATData atData = repository.getATRepository().fromATAddress(atAddress);
assertFalse(atData.getIsFinished());
// AT should still be in TRADE mode
CrossChainTradeData tradeData = RavencoinACCTv3.getInstance().populateTradeData(repository, atData);
assertEquals(AcctMode.TRADING, tradeData.mode);
long expectedBalance = partnersInitialBalance - messageTransaction.getTransactionData().getFee();
long actualBalance = partner.getConfirmedBalance(Asset.QORT);
assertEquals("Partner's balance incorrect", expectedBalance, actualBalance);
// Check eventual refund
checkTradeRefund(repository, deployer, deployersInitialBalance, deployAtFee);
}
}
@SuppressWarnings("unused")
@Test
public void testCorrectSecretCorrectSenderInvalidMessageLength() throws DataException {
try (final Repository repository = RepositoryManager.getRepository()) {
PrivateKeyAccount deployer = Common.getTestAccount(repository, "chloe");
PrivateKeyAccount tradeAccount = createTradeAccount(repository);
PrivateKeyAccount partner = Common.getTestAccount(repository, "dilbert");
long deployersInitialBalance = deployer.getConfirmedBalance(Asset.QORT);
long partnersInitialBalance = partner.getConfirmedBalance(Asset.QORT);
DeployAtTransaction deployAtTransaction = doDeploy(repository, deployer, tradeAccount.getAddress());
Account at = deployAtTransaction.getATAccount();
String atAddress = at.getAddress();
long partnersOfferMessageTransactionTimestamp = System.currentTimeMillis();
int lockTimeA = calcTestLockTimeA(partnersOfferMessageTransactionTimestamp);
int refundTimeout = RavencoinACCTv3.calcRefundTimeout(partnersOfferMessageTransactionTimestamp, lockTimeA);
// Send trade info to AT
byte[] messageData = RavencoinACCTv3.buildTradeMessage(partner.getAddress(), ravencoinPublicKeyHash, hashOfSecretA, lockTimeA, refundTimeout);
MessageTransaction messageTransaction = sendMessage(repository, tradeAccount, messageData, atAddress);
// Give AT time to process message
BlockUtils.mintBlock(repository);
// Send correct secret to AT, from correct account, but missing receive address, hence incorrect length
messageData = Bytes.concat(secretA);
messageTransaction = sendMessage(repository, partner, messageData, atAddress);
// AT should NOT send funds in the next block
ATStateData preRedeemAtStateData = repository.getATRepository().getLatestATState(atAddress);
BlockUtils.mintBlock(repository);
describeAt(repository, atAddress);
// Check AT is NOT finished
ATData atData = repository.getATRepository().fromATAddress(atAddress);
assertFalse(atData.getIsFinished());
// AT should be in TRADING mode
CrossChainTradeData tradeData = RavencoinACCTv3.getInstance().populateTradeData(repository, atData);
assertEquals(AcctMode.TRADING, tradeData.mode);
}
}
@SuppressWarnings("unused")
@Test
public void testDescribeDeployed() throws DataException {
try (final Repository repository = RepositoryManager.getRepository()) {
PrivateKeyAccount deployer = Common.getTestAccount(repository, "chloe");
PrivateKeyAccount tradeAccount = createTradeAccount(repository);
PrivateKeyAccount partner = Common.getTestAccount(repository, "dilbert");
long deployersInitialBalance = deployer.getConfirmedBalance(Asset.QORT);
long partnersInitialBalance = partner.getConfirmedBalance(Asset.QORT);
DeployAtTransaction deployAtTransaction = doDeploy(repository, deployer, tradeAccount.getAddress());
List<ATData> executableAts = repository.getATRepository().getAllExecutableATs();
for (ATData atData : executableAts) {
String atAddress = atData.getATAddress();
byte[] codeBytes = atData.getCodeBytes();
byte[] codeHash = Crypto.digest(codeBytes);
System.out.println(String.format("%s: code length: %d byte%s, code hash: %s",
atAddress,
codeBytes.length,
(codeBytes.length != 1 ? "s": ""),
HashCode.fromBytes(codeHash)));
// Not one of ours?
if (!Arrays.equals(codeHash, RavencoinACCTv3.CODE_BYTES_HASH))
continue;
describeAt(repository, atAddress);
}
}
}
private int calcTestLockTimeA(long messageTimestamp) {
return (int) (messageTimestamp / 1000L + tradeTimeout * 60);
}
private DeployAtTransaction doDeploy(Repository repository, PrivateKeyAccount deployer, String tradeAddress) throws DataException {
byte[] creationBytes = RavencoinACCTv3.buildQortalAT(tradeAddress, ravencoinPublicKeyHash, redeemAmount, ravencoinAmount, tradeTimeout);
long txTimestamp = System.currentTimeMillis();
byte[] lastReference = deployer.getLastReference();
if (lastReference == null) {
System.err.println(String.format("Qortal account %s has no last reference", deployer.getAddress()));
System.exit(2);
}
Long fee = null;
String name = "QORT-RVN cross-chain trade";
String description = String.format("Qortal-Ravencoin cross-chain trade");
String atType = "ACCT";
String tags = "QORT-RVN ACCT";
BaseTransactionData baseTransactionData = new BaseTransactionData(txTimestamp, Group.NO_GROUP, lastReference, deployer.getPublicKey(), fee, null);
TransactionData deployAtTransactionData = new DeployAtTransactionData(baseTransactionData, name, description, atType, tags, creationBytes, fundingAmount, Asset.QORT);
DeployAtTransaction deployAtTransaction = new DeployAtTransaction(repository, deployAtTransactionData);
fee = deployAtTransaction.calcRecommendedFee();
deployAtTransactionData.setFee(fee);
TransactionUtils.signAndMint(repository, deployAtTransactionData, deployer);
return deployAtTransaction;
}
private MessageTransaction sendMessage(Repository repository, PrivateKeyAccount sender, byte[] data, String recipient) throws DataException {
long txTimestamp = System.currentTimeMillis();
byte[] lastReference = sender.getLastReference();
if (lastReference == null) {
System.err.println(String.format("Qortal account %s has no last reference", sender.getAddress()));
System.exit(2);
}
Long fee = null;
int version = 4;
int nonce = 0;
long amount = 0;
Long assetId = null; // because amount is zero
BaseTransactionData baseTransactionData = new BaseTransactionData(txTimestamp, Group.NO_GROUP, lastReference, sender.getPublicKey(), fee, null);
TransactionData messageTransactionData = new MessageTransactionData(baseTransactionData, version, nonce, recipient, amount, assetId, data, false, false);
MessageTransaction messageTransaction = new MessageTransaction(repository, messageTransactionData);
fee = messageTransaction.calcRecommendedFee();
messageTransactionData.setFee(fee);
TransactionUtils.signAndMint(repository, messageTransactionData, sender);
return messageTransaction;
}
private void checkTradeRefund(Repository repository, Account deployer, long deployersInitialBalance, long deployAtFee) throws DataException {
long deployersPostDeploymentBalance = deployersInitialBalance - fundingAmount - deployAtFee;
int refundTimeout = tradeTimeout / 2 + 1; // close enough
// AT should automatically refund deployer after 'refundTimeout' blocks
for (int blockCount = 0; blockCount <= refundTimeout; ++blockCount)
BlockUtils.mintBlock(repository);
// We don't bother to exactly calculate QORT spent running AT for several blocks, but we do know the expected range
long expectedMinimumBalance = deployersPostDeploymentBalance;
long expectedMaximumBalance = deployersInitialBalance - deployAtFee;
long actualBalance = deployer.getConfirmedBalance(Asset.QORT);
assertTrue(String.format("Deployer's balance %s should be above minimum %s", actualBalance, expectedMinimumBalance), actualBalance > expectedMinimumBalance);
assertTrue(String.format("Deployer's balance %s should be below maximum %s", actualBalance, expectedMaximumBalance), actualBalance < expectedMaximumBalance);
}
private void describeAt(Repository repository, String atAddress) throws DataException {
ATData atData = repository.getATRepository().fromATAddress(atAddress);
CrossChainTradeData tradeData = RavencoinACCTv3.getInstance().populateTradeData(repository, atData);
Function<Long, String> epochMilliFormatter = (timestamp) -> LocalDateTime.ofInstant(Instant.ofEpochMilli(timestamp), ZoneOffset.UTC).format(DateTimeFormatter.ofLocalizedDateTime(FormatStyle.MEDIUM));
int currentBlockHeight = repository.getBlockRepository().getBlockchainHeight();
System.out.print(String.format("%s:\n"
+ "\tmode: %s\n"
+ "\tcreator: %s,\n"
+ "\tcreation timestamp: %s,\n"
+ "\tcurrent balance: %s QORT,\n"
+ "\tis finished: %b,\n"
+ "\tredeem payout: %s QORT,\n"
+ "\texpected ravencoin: %s RVN,\n"
+ "\tcurrent block height: %d,\n",
tradeData.qortalAtAddress,
tradeData.mode,
tradeData.qortalCreator,
epochMilliFormatter.apply(tradeData.creationTimestamp),
Amounts.prettyAmount(tradeData.qortBalance),
atData.getIsFinished(),
Amounts.prettyAmount(tradeData.qortAmount),
Amounts.prettyAmount(tradeData.expectedForeignAmount),
currentBlockHeight));
if (tradeData.mode != AcctMode.OFFERING && tradeData.mode != AcctMode.CANCELLED) {
System.out.println(String.format("\trefund timeout: %d minutes,\n"
+ "\trefund height: block %d,\n"
+ "\tHASH160 of secret-A: %s,\n"
+ "\tRavencoin P2SH-A nLockTime: %d (%s),\n"
+ "\ttrade partner: %s\n"
+ "\tpartner's receiving address: %s",
tradeData.refundTimeout,
tradeData.tradeRefundHeight,
HashCode.fromBytes(tradeData.hashOfSecretA).toString().substring(0, 40),
tradeData.lockTimeA, epochMilliFormatter.apply(tradeData.lockTimeA * 1000L),
tradeData.qortalPartnerAddress,
tradeData.qortalPartnerReceivingAddress));
}
}
private PrivateKeyAccount createTradeAccount(Repository repository) {
// We actually use a known test account with funds to avoid PoW compute
return Common.getTestAccount(repository, "alice");
}
} }

View File

@ -44,7 +44,7 @@ public class BuySellTests extends Common {
bob = Common.getTestAccount(repository, "bob"); bob = Common.getTestAccount(repository, "bob");
name = "test name" + " " + random.nextInt(1_000_000); name = "test name" + " " + random.nextInt(1_000_000);
price = random.nextInt(1000) * Amounts.MULTIPLIER; price = (random.nextInt(1000) + 1) * Amounts.MULTIPLIER;
} }
@After @After

View File

@ -20,6 +20,7 @@ import org.qortal.repository.Repository;
import org.qortal.repository.RepositoryManager; import org.qortal.repository.RepositoryManager;
import org.qortal.test.common.*; import org.qortal.test.common.*;
import org.qortal.test.common.transaction.TestTransaction; import org.qortal.test.common.transaction.TestTransaction;
import org.qortal.transaction.PaymentTransaction;
import org.qortal.transaction.RegisterNameTransaction; import org.qortal.transaction.RegisterNameTransaction;
import org.qortal.transaction.Transaction; import org.qortal.transaction.Transaction;
import org.qortal.transaction.Transaction.ValidationResult; import org.qortal.transaction.Transaction.ValidationResult;
@ -329,15 +330,19 @@ public class MiscTests extends Common {
public void testRegisterNameFeeIncrease() throws Exception { public void testRegisterNameFeeIncrease() throws Exception {
try (final Repository repository = RepositoryManager.getRepository()) { try (final Repository repository = RepositoryManager.getRepository()) {
// Set nameRegistrationUnitFeeTimestamp to a time far in the future // Add original fee to nameRegistrationUnitFees
UnitFeesByTimestamp originalFee = new UnitFeesByTimestamp();
originalFee.timestamp = 0L;
originalFee.fee = new AmountTypeAdapter().unmarshal("0.1");
// Add a time far in the future to nameRegistrationUnitFees
UnitFeesByTimestamp futureFeeIncrease = new UnitFeesByTimestamp(); UnitFeesByTimestamp futureFeeIncrease = new UnitFeesByTimestamp();
futureFeeIncrease.timestamp = 9999999999999L; // 20 Nov 2286 futureFeeIncrease.timestamp = 9999999999999L; // 20 Nov 2286
futureFeeIncrease.fee = new AmountTypeAdapter().unmarshal("5"); futureFeeIncrease.fee = new AmountTypeAdapter().unmarshal("5");
FieldUtils.writeField(BlockChain.getInstance(), "nameRegistrationUnitFees", Arrays.asList(futureFeeIncrease), true); FieldUtils.writeField(BlockChain.getInstance(), "nameRegistrationUnitFees", Arrays.asList(originalFee, futureFeeIncrease), true);
assertEquals(futureFeeIncrease.fee, BlockChain.getInstance().getNameRegistrationUnitFeeAtTimestamp(futureFeeIncrease.timestamp)); assertEquals(futureFeeIncrease.fee, BlockChain.getInstance().getNameRegistrationUnitFeeAtTimestamp(futureFeeIncrease.timestamp));
// Validate unit fees pre and post timestamp // Validate unit fees pre and post timestamp
assertEquals(10000000, BlockChain.getInstance().getUnitFee()); // 0.1 QORT
assertEquals(10000000, BlockChain.getInstance().getNameRegistrationUnitFeeAtTimestamp(futureFeeIncrease.timestamp - 1)); // 0.1 QORT assertEquals(10000000, BlockChain.getInstance().getNameRegistrationUnitFeeAtTimestamp(futureFeeIncrease.timestamp - 1)); // 0.1 QORT
assertEquals(500000000, BlockChain.getInstance().getNameRegistrationUnitFeeAtTimestamp(futureFeeIncrease.timestamp)); // 5 QORT assertEquals(500000000, BlockChain.getInstance().getNameRegistrationUnitFeeAtTimestamp(futureFeeIncrease.timestamp)); // 5 QORT
@ -362,7 +367,7 @@ public class MiscTests extends Common {
futureFeeIncrease.timestamp = now + (60 * 60 * 1000L); // 1 hour in the future futureFeeIncrease.timestamp = now + (60 * 60 * 1000L); // 1 hour in the future
futureFeeIncrease.fee = new AmountTypeAdapter().unmarshal("10"); futureFeeIncrease.fee = new AmountTypeAdapter().unmarshal("10");
FieldUtils.writeField(BlockChain.getInstance(), "nameRegistrationUnitFees", Arrays.asList(pastFeeIncrease, futureFeeIncrease), true); FieldUtils.writeField(BlockChain.getInstance(), "nameRegistrationUnitFees", Arrays.asList(originalFee, pastFeeIncrease, futureFeeIncrease), true);
assertEquals(pastFeeIncrease.fee, BlockChain.getInstance().getNameRegistrationUnitFeeAtTimestamp(pastFeeIncrease.timestamp)); assertEquals(pastFeeIncrease.fee, BlockChain.getInstance().getNameRegistrationUnitFeeAtTimestamp(pastFeeIncrease.timestamp));
assertEquals(futureFeeIncrease.fee, BlockChain.getInstance().getNameRegistrationUnitFeeAtTimestamp(futureFeeIncrease.timestamp)); assertEquals(futureFeeIncrease.fee, BlockChain.getInstance().getNameRegistrationUnitFeeAtTimestamp(futureFeeIncrease.timestamp));
@ -387,4 +392,124 @@ public class MiscTests extends Common {
} }
} }
// test reading the name registration fee schedule from blockchain.json / test-chain-v2.json
@Test
public void testRegisterNameFeeScheduleInTestchainData() throws Exception {
try (final Repository repository = RepositoryManager.getRepository()) {
final long expectedFutureFeeIncreaseTimestamp = 9999999999999L; // 20 Nov 2286, as per test-chain-v2.json
final long expectedFutureFeeIncreaseValue = new AmountTypeAdapter().unmarshal("5");
assertEquals(expectedFutureFeeIncreaseValue, BlockChain.getInstance().getNameRegistrationUnitFeeAtTimestamp(expectedFutureFeeIncreaseTimestamp));
// Validate unit fees pre and post timestamp
assertEquals(10000000, BlockChain.getInstance().getNameRegistrationUnitFeeAtTimestamp(expectedFutureFeeIncreaseTimestamp - 1)); // 0.1 QORT
assertEquals(500000000, BlockChain.getInstance().getNameRegistrationUnitFeeAtTimestamp(expectedFutureFeeIncreaseTimestamp)); // 5 QORT
// Register-name
PrivateKeyAccount alice = Common.getTestAccount(repository, "alice");
String name = "test-name";
String data = "{\"age\":30}";
RegisterNameTransactionData transactionData = new RegisterNameTransactionData(TestTransaction.generateBase(alice), name, data);
transactionData.setFee(new RegisterNameTransaction(null, null).getUnitFee(transactionData.getTimestamp()));
assertEquals(10000000L, transactionData.getFee().longValue());
TransactionUtils.signAndMint(repository, transactionData, alice);
}
}
// test general unit fee increase
@Test
public void testUnitFeeIncrease() throws Exception {
try (final Repository repository = RepositoryManager.getRepository()) {
// Add original fee to unitFees
UnitFeesByTimestamp originalFee = new UnitFeesByTimestamp();
originalFee.timestamp = 0L;
originalFee.fee = new AmountTypeAdapter().unmarshal("0.1");
// Add a time far in the future to unitFees
UnitFeesByTimestamp futureFeeIncrease = new UnitFeesByTimestamp();
futureFeeIncrease.timestamp = 9999999999999L; // 20 Nov 2286
futureFeeIncrease.fee = new AmountTypeAdapter().unmarshal("1");
FieldUtils.writeField(BlockChain.getInstance(), "unitFees", Arrays.asList(originalFee, futureFeeIncrease), true);
assertEquals(futureFeeIncrease.fee, BlockChain.getInstance().getUnitFeeAtTimestamp(futureFeeIncrease.timestamp));
// Validate unit fees pre and post timestamp
assertEquals(10000000, BlockChain.getInstance().getUnitFeeAtTimestamp(futureFeeIncrease.timestamp - 1)); // 0.1 QORT
assertEquals(100000000, BlockChain.getInstance().getUnitFeeAtTimestamp(futureFeeIncrease.timestamp)); // 1 QORT
// Payment
PrivateKeyAccount alice = Common.getTestAccount(repository, "alice");
PrivateKeyAccount bob = Common.getTestAccount(repository, "bob");
PrivateKeyAccount chloe = Common.getTestAccount(repository, "chloe");
PaymentTransactionData transactionData = new PaymentTransactionData(TestTransaction.generateBase(alice), bob.getAddress(), 100000);
transactionData.setFee(new PaymentTransaction(null, null).getUnitFee(transactionData.getTimestamp()));
assertEquals(10000000L, transactionData.getFee().longValue());
TransactionUtils.signAndMint(repository, transactionData, alice);
// Set fee increase to a time in the past
Long now = NTP.getTime();
UnitFeesByTimestamp pastFeeIncrease = new UnitFeesByTimestamp();
pastFeeIncrease.timestamp = now - 1000L; // 1 second ago
pastFeeIncrease.fee = new AmountTypeAdapter().unmarshal("3");
// Set another increase in the future
futureFeeIncrease = new UnitFeesByTimestamp();
futureFeeIncrease.timestamp = now + (60 * 60 * 1000L); // 1 hour in the future
futureFeeIncrease.fee = new AmountTypeAdapter().unmarshal("10");
FieldUtils.writeField(BlockChain.getInstance(), "unitFees", Arrays.asList(originalFee, pastFeeIncrease, futureFeeIncrease), true);
assertEquals(originalFee.fee, BlockChain.getInstance().getUnitFeeAtTimestamp(originalFee.timestamp));
assertEquals(pastFeeIncrease.fee, BlockChain.getInstance().getUnitFeeAtTimestamp(pastFeeIncrease.timestamp));
assertEquals(futureFeeIncrease.fee, BlockChain.getInstance().getUnitFeeAtTimestamp(futureFeeIncrease.timestamp));
// Send another payment transaction
// Fee should be determined automatically
transactionData = new PaymentTransactionData(TestTransaction.generateBase(alice), bob.getAddress(), 50000);
assertEquals(300000000L, transactionData.getFee().longValue());
Transaction transaction = Transaction.fromData(repository, transactionData);
transaction.sign(alice);
ValidationResult result = transaction.importAsUnconfirmed();
assertEquals("Transaction should be valid", ValidationResult.OK, result);
// Now try fetching and setting fee manually
transactionData = new PaymentTransactionData(TestTransaction.generateBase(alice), chloe.getAddress(), 50000);
transactionData.setFee(new PaymentTransaction(null, null).getUnitFee(transactionData.getTimestamp()));
assertEquals(300000000L, transactionData.getFee().longValue());
transaction = Transaction.fromData(repository, transactionData);
transaction.sign(alice);
result = transaction.importAsUnconfirmed();
assertEquals("Transaction should be valid", ValidationResult.OK, result);
}
}
// test reading the fee schedule from blockchain.json / test-chain-v2.json
@Test
public void testFeeScheduleInTestchainData() throws Exception {
try (final Repository repository = RepositoryManager.getRepository()) {
final long expectedFutureFeeIncreaseTimestamp = 9999999999999L; // 20 Nov 2286, as per test-chain-v2.json
final long expectedFutureFeeIncreaseValue = new AmountTypeAdapter().unmarshal("1");
assertEquals(expectedFutureFeeIncreaseValue, BlockChain.getInstance().getUnitFeeAtTimestamp(expectedFutureFeeIncreaseTimestamp));
// Validate unit fees pre and post timestamp
assertEquals(10000000, BlockChain.getInstance().getUnitFeeAtTimestamp(expectedFutureFeeIncreaseTimestamp - 1)); // 0.1 QORT
assertEquals(100000000, BlockChain.getInstance().getUnitFeeAtTimestamp(expectedFutureFeeIncreaseTimestamp)); // 1 QORT
// Payment
PrivateKeyAccount alice = Common.getTestAccount(repository, "alice");
PrivateKeyAccount bob = Common.getTestAccount(repository, "bob");
PaymentTransactionData transactionData = new PaymentTransactionData(TestTransaction.generateBase(alice), bob.getAddress(), 100000);
transactionData.setFee(new PaymentTransaction(null, null).getUnitFee(transactionData.getTimestamp()));
assertEquals(10000000L, transactionData.getFee().longValue());
TransactionUtils.signAndMint(repository, transactionData, alice);
}
}
} }

View File

@ -4,8 +4,11 @@
"transactionExpiryPeriod": 86400000, "transactionExpiryPeriod": 86400000,
"maxBlockSize": 2097152, "maxBlockSize": 2097152,
"maxBytesPerUnitFee": 1024, "maxBytesPerUnitFee": 1024,
"unitFee": "0.1", "unitFees": [
{ "timestamp": 0, "fee": "0.1" }
],
"nameRegistrationUnitFees": [ "nameRegistrationUnitFees": [
{ "timestamp": 0, "fee": "0.1" },
{ "timestamp": 1645372800000, "fee": "5" } { "timestamp": 1645372800000, "fee": "5" }
], ],
"requireGroupForApproval": false, "requireGroupForApproval": false,
@ -15,6 +18,7 @@
"onlineAccountSignaturesMinLifetime": 3600000, "onlineAccountSignaturesMinLifetime": 3600000,
"onlineAccountSignaturesMaxLifetime": 86400000, "onlineAccountSignaturesMaxLifetime": 86400000,
"selfSponsorshipAlgoV1SnapshotTimestamp": 9999999999999, "selfSponsorshipAlgoV1SnapshotTimestamp": 9999999999999,
"mempowTransactionUpdatesTimestamp": 0,
"rewardsByHeight": [ "rewardsByHeight": [
{ "height": 1, "reward": 100 }, { "height": 1, "reward": 100 },
{ "height": 11, "reward": 10 }, { "height": 11, "reward": 10 },

View File

@ -4,8 +4,11 @@
"transactionExpiryPeriod": 86400000, "transactionExpiryPeriod": 86400000,
"maxBlockSize": 2097152, "maxBlockSize": 2097152,
"maxBytesPerUnitFee": 1024, "maxBytesPerUnitFee": 1024,
"unitFee": "0.1", "unitFees": [
{ "timestamp": 0, "fee": "0.1" }
],
"nameRegistrationUnitFees": [ "nameRegistrationUnitFees": [
{ "timestamp": 0, "fee": "0.1" },
{ "timestamp": 1645372800000, "fee": "5" } { "timestamp": 1645372800000, "fee": "5" }
], ],
"requireGroupForApproval": false, "requireGroupForApproval": false,
@ -19,6 +22,7 @@
"onlineAccountSignaturesMinLifetime": 3600000, "onlineAccountSignaturesMinLifetime": 3600000,
"onlineAccountSignaturesMaxLifetime": 86400000, "onlineAccountSignaturesMaxLifetime": 86400000,
"selfSponsorshipAlgoV1SnapshotTimestamp": 9999999999999, "selfSponsorshipAlgoV1SnapshotTimestamp": 9999999999999,
"mempowTransactionUpdatesTimestamp": 0,
"rewardsByHeight": [ "rewardsByHeight": [
{ "height": 1, "reward": 100 }, { "height": 1, "reward": 100 },
{ "height": 11, "reward": 10 }, { "height": 11, "reward": 10 },

View File

@ -4,8 +4,11 @@
"transactionExpiryPeriod": 86400000, "transactionExpiryPeriod": 86400000,
"maxBlockSize": 2097152, "maxBlockSize": 2097152,
"maxBytesPerUnitFee": 1024, "maxBytesPerUnitFee": 1024,
"unitFee": "0.1", "unitFees": [
{ "timestamp": 0, "fee": "0.1" }
],
"nameRegistrationUnitFees": [ "nameRegistrationUnitFees": [
{ "timestamp": 0, "fee": "0.1" },
{ "timestamp": 1645372800000, "fee": "5" } { "timestamp": 1645372800000, "fee": "5" }
], ],
"requireGroupForApproval": false, "requireGroupForApproval": false,
@ -20,6 +23,7 @@
"onlineAccountSignaturesMaxLifetime": 86400000, "onlineAccountSignaturesMaxLifetime": 86400000,
"onlineAccountsModulusV2Timestamp": 9999999999999, "onlineAccountsModulusV2Timestamp": 9999999999999,
"selfSponsorshipAlgoV1SnapshotTimestamp": 9999999999999, "selfSponsorshipAlgoV1SnapshotTimestamp": 9999999999999,
"mempowTransactionUpdatesTimestamp": 0,
"rewardsByHeight": [ "rewardsByHeight": [
{ "height": 1, "reward": 100 }, { "height": 1, "reward": 100 },
{ "height": 11, "reward": 10 }, { "height": 11, "reward": 10 },

View File

@ -4,8 +4,11 @@
"transactionExpiryPeriod": 86400000, "transactionExpiryPeriod": 86400000,
"maxBlockSize": 2097152, "maxBlockSize": 2097152,
"maxBytesPerUnitFee": 1024, "maxBytesPerUnitFee": 1024,
"unitFee": "0.1", "unitFees": [
{ "timestamp": 0, "fee": "0.1" }
],
"nameRegistrationUnitFees": [ "nameRegistrationUnitFees": [
{ "timestamp": 0, "fee": "0.1" },
{ "timestamp": 1645372800000, "fee": "5" } { "timestamp": 1645372800000, "fee": "5" }
], ],
"requireGroupForApproval": false, "requireGroupForApproval": false,
@ -20,6 +23,7 @@
"onlineAccountSignaturesMaxLifetime": 86400000, "onlineAccountSignaturesMaxLifetime": 86400000,
"onlineAccountsModulusV2Timestamp": 9999999999999, "onlineAccountsModulusV2Timestamp": 9999999999999,
"selfSponsorshipAlgoV1SnapshotTimestamp": 9999999999999, "selfSponsorshipAlgoV1SnapshotTimestamp": 9999999999999,
"mempowTransactionUpdatesTimestamp": 0,
"rewardsByHeight": [ "rewardsByHeight": [
{ "height": 1, "reward": 100 }, { "height": 1, "reward": 100 },
{ "height": 11, "reward": 10 }, { "height": 11, "reward": 10 },

View File

@ -4,8 +4,11 @@
"transactionExpiryPeriod": 86400000, "transactionExpiryPeriod": 86400000,
"maxBlockSize": 2097152, "maxBlockSize": 2097152,
"maxBytesPerUnitFee": 1024, "maxBytesPerUnitFee": 1024,
"unitFee": "0.1", "unitFees": [
{ "timestamp": 0, "fee": "0.1" }
],
"nameRegistrationUnitFees": [ "nameRegistrationUnitFees": [
{ "timestamp": 0, "fee": "0.1" },
{ "timestamp": 1645372800000, "fee": "5" } { "timestamp": 1645372800000, "fee": "5" }
], ],
"requireGroupForApproval": false, "requireGroupForApproval": false,
@ -20,6 +23,7 @@
"onlineAccountSignaturesMaxLifetime": 86400000, "onlineAccountSignaturesMaxLifetime": 86400000,
"onlineAccountsModulusV2Timestamp": 9999999999999, "onlineAccountsModulusV2Timestamp": 9999999999999,
"selfSponsorshipAlgoV1SnapshotTimestamp": 9999999999999, "selfSponsorshipAlgoV1SnapshotTimestamp": 9999999999999,
"mempowTransactionUpdatesTimestamp": 9999999999999,
"rewardsByHeight": [ "rewardsByHeight": [
{ "height": 1, "reward": 100 }, { "height": 1, "reward": 100 },
{ "height": 11, "reward": 10 }, { "height": 11, "reward": 10 },

View File

@ -4,8 +4,11 @@
"transactionExpiryPeriod": 86400000, "transactionExpiryPeriod": 86400000,
"maxBlockSize": 2097152, "maxBlockSize": 2097152,
"maxBytesPerUnitFee": 1024, "maxBytesPerUnitFee": 1024,
"unitFee": "0.1", "unitFees": [
{ "timestamp": 0, "fee": "0.1" }
],
"nameRegistrationUnitFees": [ "nameRegistrationUnitFees": [
{ "timestamp": 0, "fee": "0.1" },
{ "timestamp": 1645372800000, "fee": "5" } { "timestamp": 1645372800000, "fee": "5" }
], ],
"requireGroupForApproval": false, "requireGroupForApproval": false,
@ -20,6 +23,7 @@
"onlineAccountSignaturesMaxLifetime": 86400000, "onlineAccountSignaturesMaxLifetime": 86400000,
"onlineAccountsModulusV2Timestamp": 9999999999999, "onlineAccountsModulusV2Timestamp": 9999999999999,
"selfSponsorshipAlgoV1SnapshotTimestamp": 9999999999999, "selfSponsorshipAlgoV1SnapshotTimestamp": 9999999999999,
"mempowTransactionUpdatesTimestamp": 0,
"rewardsByHeight": [ "rewardsByHeight": [
{ "height": 1, "reward": 100 }, { "height": 1, "reward": 100 },
{ "height": 11, "reward": 10 }, { "height": 11, "reward": 10 },

View File

@ -4,8 +4,11 @@
"transactionExpiryPeriod": 86400000, "transactionExpiryPeriod": 86400000,
"maxBlockSize": 2097152, "maxBlockSize": 2097152,
"maxBytesPerUnitFee": 1024, "maxBytesPerUnitFee": 1024,
"unitFee": "0.1", "unitFees": [
{ "timestamp": 0, "fee": "0.1" }
],
"nameRegistrationUnitFees": [ "nameRegistrationUnitFees": [
{ "timestamp": 0, "fee": "0.1" },
{ "timestamp": 1645372800000, "fee": "5" } { "timestamp": 1645372800000, "fee": "5" }
], ],
"requireGroupForApproval": false, "requireGroupForApproval": false,
@ -20,6 +23,7 @@
"onlineAccountSignaturesMaxLifetime": 86400000, "onlineAccountSignaturesMaxLifetime": 86400000,
"onlineAccountsModulusV2Timestamp": 9999999999999, "onlineAccountsModulusV2Timestamp": 9999999999999,
"selfSponsorshipAlgoV1SnapshotTimestamp": 9999999999999, "selfSponsorshipAlgoV1SnapshotTimestamp": 9999999999999,
"mempowTransactionUpdatesTimestamp": 0,
"rewardsByHeight": [ "rewardsByHeight": [
{ "height": 1, "reward": 100 }, { "height": 1, "reward": 100 },
{ "height": 11, "reward": 10 }, { "height": 11, "reward": 10 },

View File

@ -4,8 +4,11 @@
"transactionExpiryPeriod": 86400000, "transactionExpiryPeriod": 86400000,
"maxBlockSize": 2097152, "maxBlockSize": 2097152,
"maxBytesPerUnitFee": 1024, "maxBytesPerUnitFee": 1024,
"unitFee": "0.1", "unitFees": [
{ "timestamp": 0, "fee": "0.1" }
],
"nameRegistrationUnitFees": [ "nameRegistrationUnitFees": [
{ "timestamp": 0, "fee": "0.1" },
{ "timestamp": 1645372800000, "fee": "5" } { "timestamp": 1645372800000, "fee": "5" }
], ],
"requireGroupForApproval": false, "requireGroupForApproval": false,
@ -20,6 +23,7 @@
"onlineAccountSignaturesMaxLifetime": 86400000, "onlineAccountSignaturesMaxLifetime": 86400000,
"onlineAccountsModulusV2Timestamp": 9999999999999, "onlineAccountsModulusV2Timestamp": 9999999999999,
"selfSponsorshipAlgoV1SnapshotTimestamp": 9999999999999, "selfSponsorshipAlgoV1SnapshotTimestamp": 9999999999999,
"mempowTransactionUpdatesTimestamp": 0,
"rewardsByHeight": [ "rewardsByHeight": [
{ "height": 1, "reward": 100 }, { "height": 1, "reward": 100 },
{ "height": 11, "reward": 10 }, { "height": 11, "reward": 10 },

View File

@ -4,8 +4,11 @@
"transactionExpiryPeriod": 86400000, "transactionExpiryPeriod": 86400000,
"maxBlockSize": 2097152, "maxBlockSize": 2097152,
"maxBytesPerUnitFee": 1024, "maxBytesPerUnitFee": 1024,
"unitFee": "0.1", "unitFees": [
{ "timestamp": 0, "fee": "0.1" }
],
"nameRegistrationUnitFees": [ "nameRegistrationUnitFees": [
{ "timestamp": 0, "fee": "0.1" },
{ "timestamp": 1645372800000, "fee": "5" } { "timestamp": 1645372800000, "fee": "5" }
], ],
"requireGroupForApproval": false, "requireGroupForApproval": false,
@ -20,6 +23,7 @@
"onlineAccountSignaturesMaxLifetime": 86400000, "onlineAccountSignaturesMaxLifetime": 86400000,
"onlineAccountsModulusV2Timestamp": 9999999999999, "onlineAccountsModulusV2Timestamp": 9999999999999,
"selfSponsorshipAlgoV1SnapshotTimestamp": 9999999999999, "selfSponsorshipAlgoV1SnapshotTimestamp": 9999999999999,
"mempowTransactionUpdatesTimestamp": 0,
"rewardsByHeight": [ "rewardsByHeight": [
{ "height": 1, "reward": 100 }, { "height": 1, "reward": 100 },
{ "height": 11, "reward": 10 }, { "height": 11, "reward": 10 },

View File

@ -4,8 +4,11 @@
"transactionExpiryPeriod": 86400000, "transactionExpiryPeriod": 86400000,
"maxBlockSize": 2097152, "maxBlockSize": 2097152,
"maxBytesPerUnitFee": 1024, "maxBytesPerUnitFee": 1024,
"unitFee": "0.1", "unitFees": [
{ "timestamp": 0, "fee": "0.1" }
],
"nameRegistrationUnitFees": [ "nameRegistrationUnitFees": [
{ "timestamp": 0, "fee": "0.1" },
{ "timestamp": 1645372800000, "fee": "5" } { "timestamp": 1645372800000, "fee": "5" }
], ],
"requireGroupForApproval": false, "requireGroupForApproval": false,
@ -20,6 +23,7 @@
"onlineAccountSignaturesMaxLifetime": 86400000, "onlineAccountSignaturesMaxLifetime": 86400000,
"onlineAccountsModulusV2Timestamp": 9999999999999, "onlineAccountsModulusV2Timestamp": 9999999999999,
"selfSponsorshipAlgoV1SnapshotTimestamp": 9999999999999, "selfSponsorshipAlgoV1SnapshotTimestamp": 9999999999999,
"mempowTransactionUpdatesTimestamp": 0,
"rewardsByHeight": [ "rewardsByHeight": [
{ "height": 1, "reward": 100 }, { "height": 1, "reward": 100 },
{ "height": 11, "reward": 10 }, { "height": 11, "reward": 10 },

View File

@ -4,8 +4,11 @@
"transactionExpiryPeriod": 86400000, "transactionExpiryPeriod": 86400000,
"maxBlockSize": 2097152, "maxBlockSize": 2097152,
"maxBytesPerUnitFee": 1024, "maxBytesPerUnitFee": 1024,
"unitFee": "0.1", "unitFees": [
{ "timestamp": 0, "fee": "0.1" }
],
"nameRegistrationUnitFees": [ "nameRegistrationUnitFees": [
{ "timestamp": 0, "fee": "0.1" },
{ "timestamp": 1645372800000, "fee": "5" } { "timestamp": 1645372800000, "fee": "5" }
], ],
"requireGroupForApproval": false, "requireGroupForApproval": false,
@ -19,6 +22,7 @@
"onlineAccountSignaturesMinLifetime": 3600000, "onlineAccountSignaturesMinLifetime": 3600000,
"onlineAccountSignaturesMaxLifetime": 86400000, "onlineAccountSignaturesMaxLifetime": 86400000,
"selfSponsorshipAlgoV1SnapshotTimestamp": 9999999999999, "selfSponsorshipAlgoV1SnapshotTimestamp": 9999999999999,
"mempowTransactionUpdatesTimestamp": 0,
"rewardsByHeight": [ "rewardsByHeight": [
{ "height": 1, "reward": 100 }, { "height": 1, "reward": 100 },
{ "height": 11, "reward": 10 }, { "height": 11, "reward": 10 },

View File

@ -4,8 +4,11 @@
"transactionExpiryPeriod": 86400000, "transactionExpiryPeriod": 86400000,
"maxBlockSize": 2097152, "maxBlockSize": 2097152,
"maxBytesPerUnitFee": 0, "maxBytesPerUnitFee": 0,
"unitFee": "0.00000001", "unitFees": [
{ "timestamp": 0, "fee": "0.00000001" }
],
"nameRegistrationUnitFees": [ "nameRegistrationUnitFees": [
{ "timestamp": 0, "fee": "0.00000001" },
{ "timestamp": 1645372800000, "fee": "5" } { "timestamp": 1645372800000, "fee": "5" }
], ],
"requireGroupForApproval": false, "requireGroupForApproval": false,
@ -20,6 +23,7 @@
"onlineAccountSignaturesMaxLifetime": 86400000, "onlineAccountSignaturesMaxLifetime": 86400000,
"onlineAccountsModulusV2Timestamp": 9999999999999, "onlineAccountsModulusV2Timestamp": 9999999999999,
"selfSponsorshipAlgoV1SnapshotTimestamp": 9999999999999, "selfSponsorshipAlgoV1SnapshotTimestamp": 9999999999999,
"mempowTransactionUpdatesTimestamp": 0,
"rewardsByHeight": [ "rewardsByHeight": [
{ "height": 1, "reward": 100 }, { "height": 1, "reward": 100 },
{ "height": 11, "reward": 10 }, { "height": 11, "reward": 10 },

View File

@ -4,9 +4,13 @@
"transactionExpiryPeriod": 86400000, "transactionExpiryPeriod": 86400000,
"maxBlockSize": 2097152, "maxBlockSize": 2097152,
"maxBytesPerUnitFee": 1024, "maxBytesPerUnitFee": 1024,
"unitFee": "0.1", "unitFees": [
{ "timestamp": 0, "fee": "0.1" },
{ "timestamp": 9999999999999, "fee": "1" }
],
"nameRegistrationUnitFees": [ "nameRegistrationUnitFees": [
{ "timestamp": 1645372800000, "fee": "5" } { "timestamp": 0, "fee": "0.1" },
{ "timestamp": 9999999999999, "fee": "5" }
], ],
"requireGroupForApproval": false, "requireGroupForApproval": false,
"minAccountLevelToRewardShare": 5, "minAccountLevelToRewardShare": 5,
@ -20,6 +24,7 @@
"onlineAccountSignaturesMaxLifetime": 86400000, "onlineAccountSignaturesMaxLifetime": 86400000,
"onlineAccountsModulusV2Timestamp": 9999999999999, "onlineAccountsModulusV2Timestamp": 9999999999999,
"selfSponsorshipAlgoV1SnapshotTimestamp": 9999999999999, "selfSponsorshipAlgoV1SnapshotTimestamp": 9999999999999,
"mempowTransactionUpdatesTimestamp": 0,
"rewardsByHeight": [ "rewardsByHeight": [
{ "height": 1, "reward": 100 }, { "height": 1, "reward": 100 },
{ "height": 11, "reward": 10 }, { "height": 11, "reward": 10 },

View File

@ -4,7 +4,9 @@
"transactionExpiryPeriod": 86400000, "transactionExpiryPeriod": 86400000,
"maxBlockSize": 2097152, "maxBlockSize": 2097152,
"maxBytesPerUnitFee": 1024, "maxBytesPerUnitFee": 1024,
"unitFee": "0.001", "unitFees": [
{ "timestamp": 0, "fee": "0.001" }
],
"nameRegistrationUnitFees": [ "nameRegistrationUnitFees": [
{ "timestamp": 0, "fee": "1.25" } { "timestamp": 0, "fee": "1.25" }
], ],
@ -24,6 +26,7 @@
"onlineAccountSignaturesMaxLifetime": 86400000, "onlineAccountSignaturesMaxLifetime": 86400000,
"onlineAccountsModulusV2Timestamp": 0, "onlineAccountsModulusV2Timestamp": 0,
"selfSponsorshipAlgoV1SnapshotTimestamp": 9999999999999, "selfSponsorshipAlgoV1SnapshotTimestamp": 9999999999999,
"mempowTransactionUpdatesTimestamp": 1692554400000,
"rewardsByHeight": [ "rewardsByHeight": [
{ "height": 1, "reward": 5.00 }, { "height": 1, "reward": 5.00 },
{ "height": 259201, "reward": 4.75 }, { "height": 259201, "reward": 4.75 },

View File

@ -50,7 +50,7 @@ tx_json=$( cat <<TX_END
{ {
"timestamp": ${timestamp}, "timestamp": ${timestamp},
"reference": "${lastref}", "reference": "${lastref}",
"fee": 0.001, "fee": 0.01,
"txGroupId": 0, "txGroupId": 0,
"adminPublicKey": "${pubkey}", "adminPublicKey": "${pubkey}",
"pendingSignature": "${sig}", "pendingSignature": "${sig}",

View File

@ -50,7 +50,7 @@ tx_json=$( cat <<TX_END
{ {
"timestamp": ${timestamp}, "timestamp": ${timestamp},
"reference": "${lastref}", "reference": "${lastref}",
"fee": 0.001, "fee": 0.01,
"txGroupId": 0, "txGroupId": 0,
"adminPublicKey": "${pubkey}", "adminPublicKey": "${pubkey}",
"pendingSignature": "${sig}", "pendingSignature": "${sig}",

View File

@ -1,163 +0,0 @@
#!/usr/bin/env perl
use strict;
use warnings;
use POSIX;
use Getopt::Std;
use File::Slurp;
sub usage() {
die("usage: $0 [-p api-port] dev-private-key [short-commit-hash]\n");
}
my %opt;
getopts('p:', \%opt);
usage() if @ARGV < 1 || @ARGV > 2;
my $port = $opt{p} || 12391;
my $privkey = shift @ARGV;
my $commit_hash = shift @ARGV;
my $git_dir = `git rev-parse --show-toplevel`;
die("Cannot determine git top level dir\n") unless $git_dir;
chomp $git_dir;
chdir($git_dir) || die("Can't change directory to $git_dir: $!\n");
open(POM, '<', 'pom.xml') || die ("Can't open 'pom.xml': $!\n");
my $project;
while (<POM>) {
if (m/<artifactId>(\w+)<.artifactId>/o) {
$project = $1;
last;
}
}
close(POM);
my $apikey = read_file('apikey.txt');
# Do we need to determine commit hash?
unless ($commit_hash) {
# determine git branch
my $branch_name = ` git symbolic-ref -q HEAD `;
chomp $branch_name;
$branch_name =~ s|^refs/heads/||; # ${branch_name##refs/heads/}
# short-form commit hash on base branch (non-auto-update)
$commit_hash ||= `git show --no-patch --format=%h`;
die("Can't find commit hash\n") if ! defined $commit_hash;
chomp $commit_hash;
printf "Commit hash on '%s' branch: %s\n", $branch_name, $commit_hash;
} else {
printf "Using given commit hash: %s\n", $commit_hash;
}
# build timestamp / commit timestamp on base branch
my $timestamp = `git show --no-patch --format=%ct ${commit_hash}`;
die("Can't determine commit timestamp\n") if ! defined $timestamp;
$timestamp *= 1000; # Convert to milliseconds
# locate sha256 utility
my $SHA256 = `which sha256sum || which sha256`;
chomp $SHA256;
die("Can't find sha256sum or sha256\n") unless length($SHA256) > 0;
# SHA256 of actual update file
my $sha256 = `git show auto-update-${commit_hash}:${project}.update | ${SHA256} | head -c 64`;
die("Can't calculate SHA256 of ${project}.update\n") unless $sha256 =~ m/(\S{64})/;
chomp $sha256;
# long-form commit hash of HEAD on auto-update branch
my $update_hash = `git rev-parse refs/heads/auto-update-${commit_hash}`;
die("Can't find commit hash for HEAD on auto-update-${commit_hash} branch\n") if ! defined $update_hash;
chomp $update_hash;
printf "Build timestamp (ms): %d / 0x%016x\n", $timestamp, $timestamp;
printf "Auto-update commit hash: %s\n", $update_hash;
printf "SHA256 of ${project}.update: %s\n", $sha256;
my $tx_type = 10;
my $tx_timestamp = time() * 1000;
my $tx_group_id = 1;
my $service = 1;
printf "\nARBITRARY(%d) transaction with timestamp %d, txGroupID %d and service %d\n", $tx_type, $tx_timestamp, $tx_group_id, $service;
my $data_hex = sprintf "%016x%s%s", $timestamp, $update_hash, $sha256;
printf "\nARBITRARY transaction data payload: %s\n", $data_hex;
my $n_payments = 0;
my $data_type = 1; # RAW_DATA
my $data_length = length($data_hex) / 2; # two hex chars per byte
my $fee = 0;
my $nonce = 0;
my $name_length = 0;
my $identifier_length = 0;
my $method = 0; # PUT
my $secret_length = 0;
my $compression = 0; # None
my $metadata_hash_length = 0;
die("Something's wrong: data length is not 60 bytes!\n") if $data_length != 60;
my $pubkey = `curl --silent --url http://localhost:${port}/utils/publickey --data ${privkey}`;
die("Can't convert private key to public key:\n$pubkey\n") unless $pubkey =~ m/^\w{44}$/;
printf "\nPublic key: %s\n", $pubkey;
my $pubkey_hex = `curl --silent --url http://localhost:${port}/utils/frombase58 --data ${pubkey}`;
die("Can't convert base58 public key to hex:\n$pubkey_hex\n") unless $pubkey_hex =~ m/^[A-Za-z0-9]{64}$/;
printf "Public key hex: %s\n", $pubkey_hex;
my $address = `curl --silent --url http://localhost:${port}/addresses/convert/${pubkey}`;
die("Can't convert base58 public key to address:\n$address\n") unless $address =~ m/^\w{33,34}$/;
printf "Address: %s\n", $address;
my $reference = `curl --silent --url http://localhost:${port}/addresses/lastreference/${address}`;
die("Can't fetch last reference for $address:\n$reference\n") unless $reference =~ m/^\w{87,88}$/;
printf "Last reference: %s\n", $reference;
my $reference_hex = `curl --silent --url http://localhost:${port}/utils/frombase58 --data ${reference}`;
die("Can't convert base58 reference to hex:\n$reference_hex\n") unless $reference_hex =~ m/^[A-Za-z0-9]{128}$/;
printf "Last reference hex: %s\n", $reference_hex;
my $raw_tx_hex = sprintf("%08x%016x%08x%s%s%08x%08x%08x%08x%08x%08x%08x%08x%02x%08x%s%08x%08x%016x", $tx_type, $tx_timestamp, $tx_group_id, $reference_hex, $pubkey_hex, $nonce, $name_length, $identifier_length, $method, $secret_length, $compression, $n_payments, $service, $data_type, $data_length, $data_hex, $data_length, $metadata_hash_length, $fee);
printf "\nRaw transaction hex:\n%s\n", $raw_tx_hex;
my $raw_tx = `curl --silent --url http://localhost:${port}/utils/tobase58/${raw_tx_hex}`;
die("Can't convert raw transaction hex to base58:\n$raw_tx\n") unless $raw_tx =~ m/^\w{300,320}$/; # Roughly 305 to 320 base58 chars
printf "\nRaw transaction (base58):\n%s\n", $raw_tx;
my $computed_tx = `curl --silent -X POST --url http://localhost:${port}/arbitrary/compute -H "X-API-KEY: ${apikey}" -d "${raw_tx}"`;
die("Can't compute nonce for transaction:\n$computed_tx\n") unless $computed_tx =~ m/^\w{300,320}$/; # Roughly 300 to 320 base58 chars
printf "\nRaw computed transaction (base58):\n%s\n", $computed_tx;
my $sign_data = qq|' { "privateKey": "${privkey}", "transactionBytes": "${computed_tx}" } '|;
my $signed_tx = `curl --silent -H "accept: text/plain" -H "Content-Type: application/json" --url http://localhost:${port}/transactions/sign --data ${sign_data}`;
die("Can't sign raw transaction:\n$signed_tx\n") unless $signed_tx =~ m/^\w{390,410}$/; # +90ish longer than $raw_tx
printf "\nSigned transaction:\n%s\n", $signed_tx;
# Check we can actually fetch update
my $origin = `git remote get-url origin`;
die("Unable to get github url for 'origin'?\n") unless $origin && $origin =~ m/:(.*)\.git$/;
my $repo = $1;
my $update_url = "https://github.com/${repo}/raw/${update_hash}/${project}.update";
my $fetch_result = `curl --silent -o /dev/null --location --range 0-1 --head --write-out '%{http_code}' --url ${update_url}`;
die("\nUnable to fetch update from ${update_url}\n") if $fetch_result ne '200';
printf "\nUpdate fetchable from ${update_url}\n";
# Flush STDOUT after every output
$| = 1;
print "\n";
for (my $delay = 5; $delay > 0; --$delay) {
printf "\rSubmitting transaction in %d second%s... CTRL-C to abort ", $delay, ($delay != 1 ? 's' : '');
sleep 1;
}
printf "\rSubmitting transaction NOW... \n";
my $result = `curl --silent --url http://localhost:${port}/transactions/process --data ${signed_tx}`;
chomp $result;
die("Transaction wasn't accepted:\n$result\n") unless $result eq 'true';
my $decoded_tx = `curl --silent -H "Content-Type: application/json" --url http://localhost:${port}/transactions/decode --data ${signed_tx}`;
printf "\nTransaction accepted:\n$decoded_tx\n";

View File

@ -4,6 +4,7 @@ use strict;
use warnings; use warnings;
use POSIX; use POSIX;
use Getopt::Std; use Getopt::Std;
use File::Slurp;
sub usage() { sub usage() {
die("usage: $0 [-p api-port] dev-private-key [short-commit-hash]\n"); die("usage: $0 [-p api-port] dev-private-key [short-commit-hash]\n");
@ -34,6 +35,8 @@ while (<POM>) {
} }
close(POM); close(POM);
my $apikey = read_file('apikey.txt');
# Do we need to determine commit hash? # Do we need to determine commit hash?
unless ($commit_hash) { unless ($commit_hash) {
# determine git branch # determine git branch
@ -84,9 +87,16 @@ my $data_hex = sprintf "%016x%s%s", $timestamp, $update_hash, $sha256;
printf "\nARBITRARY transaction data payload: %s\n", $data_hex; printf "\nARBITRARY transaction data payload: %s\n", $data_hex;
my $n_payments = 0; my $n_payments = 0;
my $is_raw = 1; # RAW_DATA my $data_type = 1; # RAW_DATA
my $data_length = length($data_hex) / 2; # two hex chars per byte my $data_length = length($data_hex) / 2; # two hex chars per byte
my $fee = 0.001 * 1e8; my $fee = 0.01 * 1e8;
my $nonce = 0;
my $name_length = 0;
my $identifier_length = 0;
my $method = 0; # PUT
my $secret_length = 0;
my $compression = 0; # None
my $metadata_hash_length = 0;
die("Something's wrong: data length is not 60 bytes!\n") if $data_length != 60; die("Something's wrong: data length is not 60 bytes!\n") if $data_length != 60;
@ -110,16 +120,16 @@ my $reference_hex = `curl --silent --url http://localhost:${port}/utils/frombase
die("Can't convert base58 reference to hex:\n$reference_hex\n") unless $reference_hex =~ m/^[A-Za-z0-9]{128}$/; die("Can't convert base58 reference to hex:\n$reference_hex\n") unless $reference_hex =~ m/^[A-Za-z0-9]{128}$/;
printf "Last reference hex: %s\n", $reference_hex; printf "Last reference hex: %s\n", $reference_hex;
my $raw_tx_hex = sprintf("%08x%016x%08x%s%s%08x%08x%02x%08x%s%016x", $tx_type, $tx_timestamp, $tx_group_id, $reference_hex, $pubkey_hex, $n_payments, $service, $is_raw, $data_length, $data_hex, $fee); my $raw_tx_hex = sprintf("%08x%016x%08x%s%s%08x%08x%08x%08x%08x%08x%08x%08x%02x%08x%s%08x%08x%016x", $tx_type, $tx_timestamp, $tx_group_id, $reference_hex, $pubkey_hex, $nonce, $name_length, $identifier_length, $method, $secret_length, $compression, $n_payments, $service, $data_type, $data_length, $data_hex, $data_length, $metadata_hash_length, $fee);
printf "\nRaw transaction hex:\n%s\n", $raw_tx_hex; printf "\nRaw transaction hex:\n%s\n", $raw_tx_hex;
my $raw_tx = `curl --silent --url http://localhost:${port}/utils/tobase58/${raw_tx_hex}`; my $raw_tx = `curl --silent --url http://localhost:${port}/utils/tobase58/${raw_tx_hex}`;
die("Can't convert raw transaction hex to base58:\n$raw_tx\n") unless $raw_tx =~ m/^\w{255,265}$/; # Roughly 255 to 265 base58 chars die("Can't convert raw transaction hex to base58:\n$raw_tx\n") unless $raw_tx =~ m/^\w{300,320}$/; # Roughly 305 to 320 base58 chars
printf "\nRaw transaction (base58):\n%s\n", $raw_tx; printf "\nRaw transaction (base58):\n%s\n", $raw_tx;
my $sign_data = qq|' { "privateKey": "${privkey}", "transactionBytes": "${raw_tx}" } '|; my $sign_data = qq|' { "privateKey": "${privkey}", "transactionBytes": "${raw_tx}" } '|;
my $signed_tx = `curl --silent -H "accept: text/plain" -H "Content-Type: application/json" --url http://localhost:${port}/transactions/sign --data ${sign_data}`; my $signed_tx = `curl --silent -H "accept: text/plain" -H "Content-Type: application/json" --url http://localhost:${port}/transactions/sign --data ${sign_data}`;
die("Can't sign raw transaction:\n$signed_tx\n") unless $signed_tx =~ m/^\w{345,355}$/; # +90ish longer than $raw_tx die("Can't sign raw transaction:\n$signed_tx\n") unless $signed_tx =~ m/^\w{390,410}$/; # +90ish longer than $raw_tx
printf "\nSigned transaction:\n%s\n", $signed_tx; printf "\nSigned transaction:\n%s\n", $signed_tx;
# Check we can actually fetch update # Check we can actually fetch update

122
tools/qdn
View File

@ -5,10 +5,10 @@ host="localhost"
port=12391 port=12391
if [ -z "$*" ]; then if [ -z "$*" ]; then
echo "Usage:" echo "Usage:"
echo echo
echo "Host/update data:" echo "Host/update data:"
echo "qdn POST [service] [name] PATH [dirpath] <identifier>" echo "qdn POST [service] [name] PATH [dirpath] <identifier> <title> <description> <tags=tag1,tag2,tag3> <category> <fee> <preview (true or false)>"
echo "qdn POST [service] [name] STRING [data-string] <identifier>" echo "qdn POST [service] [name] STRING [data-string] <identifier>"
echo echo
echo "Fetch data:" echo "Fetch data:"
@ -22,6 +22,21 @@ if [ -z "$*" ]; then
exit exit
fi fi
# Default ports for Qortal
mainnet_port=12391
testnet_port=62391
# Check if the '-t' operator is passed, if so change to utilizing testnet.
if [[ "$1" == "-t" ]]; then
# Use testnet port
port=$testnet_port
shift
else
# Use mainnet port
port=$mainnet_port
fi
script_dir=$( cd -- "$( dirname -- "${BASH_SOURCE[0]}" )" &> /dev/null && pwd ) script_dir=$( cd -- "$( dirname -- "${BASH_SOURCE[0]}" )" &> /dev/null && pwd )
if [ -f "apikey.txt" ]; then if [ -f "apikey.txt" ]; then
@ -37,32 +52,46 @@ service=$2
name=$3 name=$3
if [ -z "${method}" ]; then if [ -z "${method}" ]; then
echo "Error: missing method"; exit echo "Error: missing method"
exit 1
fi fi
if [ -z "${service}" ]; then if [ -z "${service}" ]; then
echo "Error: missing service"; exit echo "Error: missing service"
exit 1
fi fi
if [ -z "${name}" ]; then if [ -z "${name}" ]; then
echo "Error: missing name"; exit echo "Error: missing name"
exit 1
fi fi
if [[ "${method}" == "POST" ]]; then if [[ "${method}" == "POST" ]]; then
type=$4 type=$4
data=$5 data=$5
identifier=$6 identifier=$6
title=$7
description=$8
tags=$9
category=${10}
fee=${11}
preview=${12}
if [ -z "${data}" ]; then if [ -z "${data}" ]; then
if [[ "${type}" == "PATH" ]]; then if [[ "${type}" == "PATH" ]]; then
echo "Error: missing directory"; exit echo "Error: missing directory - please use a path to a directory with a SINGLE file wishing to be published"
exit 1
elif [[ "${type}" == "STRING" ]]; then elif [[ "${type}" == "STRING" ]]; then
echo "Error: missing data string"; exit echo "Error: missing data string - please input the data string you wish to publish"
exit 1
else else
echo "Error: unrecognized type"; exit echo "Error: unrecognized type"
exit 1
fi fi
fi fi
if [ -z "${QORTAL_PRIVKEY}" ]; then if [ -z "${QORTAL_PRIVKEY}" ]; then
echo "Error: missing private key. Set it by running: export QORTAL_PRIVKEY=privkeyhere"; exit echo "Error: missing private key. Set it by running: export QORTAL_PRIVKEY=privkeyhere"
exit 1
fi fi
if [ -z "${identifier}" ]; then if [ -z "${identifier}" ]; then
@ -75,30 +104,88 @@ if [[ "${method}" == "POST" ]]; then
elif [[ "${type}" == "STRING" ]]; then elif [[ "${type}" == "STRING" ]]; then
type_component="/string" type_component="/string"
fi fi
# Create tags component in URL, comma-separated list of tags, will be added to the tags call.
tags_component=""
if [ -n "${tags}" ]; then
IFS=',' read -ra tag_array <<< "${tags}"
for tag in "${tag_array[@]}"; do
tags_component+="&tags=${tag}"
done
fi
if [ -z ${tags_component} ]; then
tags_component=""
echo "nothing in tags, using empty tags"
fi
#Create category component with pre-defined list of categories. Error if category is specified but not in list.
allowed_categories=("ART" "AUTOMOTIVE" "BEAUTY" "BOOKS" "BUSINESS" "COMMUNICATIONS" "CRYPTOCURRENCY" "CULTURE" "DATING" "DESIGN" "ENTERTAINMENT" "EVENTS" "FAITH" "FASHION" "FINANCE" "FOOD" "GAMING" "GEOGRAPHY" "HEALTH" "HISTORY" "HOME" "KNOWLEDGE" "LANGUAGE" "LIFESTYLE" "MANUFACTURING" "MAPS" "MUSIC" "NEWS" "OTHER" "PETS" "PHILOSOPHY" "PHOTOGRAPHY" "POLITICS" "PRODUCE" "PRODUCTIVITY" "PSYCHOLOGY" "QORTAL" "SCIENCE" "SELF_CARE" "SELF_SUFFICIENCY" "SHOPPING" "SOCIAL" "SOFTWARE" "SPIRITUALITY" "SPORTS" "STORYTELLING" "TECHNOLOGY" "TOOLS" "TRAVEL" "UNCATEGORIZED" "VIDEO" "WEATHER")
if [[ -n "$category" && ! " ${allowed_categories[@]} " =~ " $category " ]]; then
echo "Error: Invalid category. Allowed categories are: ${allowed_categories[*]} be sure to place your overall script inputs in the correct order"
exit 1
elif [ -z "$category" ]; then
category=""
echo "No category is being set"
fi
if [ -n "$fee" ]; then
if [[ "$fee" == "1" || "$fee" == ".01" ]]; then
fee="1000000"
elif [ -z "$fee" ]; then
fee=""
else
echo "Error: Invalid fee value. Expected '1', '.01' or no input."
exit 1
fi
final_fee="${fee}"
fi
# check that preview is true/false
if [[ -n "$preview" && ! ( "$preview" == "true" || "$preview" == "false" ) ]]; then
echo "Error: Invalid preview value. Expected 'true' or 'false'. Please retry with boolean as preview entry."
exit 1
elif [ -z "$preview" ]; then
preview=""
fi
# Build the API URL
api_url="http://${host}:${port}/arbitrary/${service}/${name}/${identifier}${type_component}"
api_url+="?title=${title}&description=${description}&tags=${tags_component}&category=${category}&fee=${final_fee}&preview=${preview}"
echo "Creating transaction - this can take a while..." echo "Creating transaction - this can take a while..."
tx_data=$(curl --silent --insecure -X ${method} "http://${host}:${port}/arbitrary/${service}/${name}/${identifier}${type_component}" -H "X-API-KEY: ${apikey}" -d "${data}") tx_data=$(curl --silent --insecure -X ${method} "${api_url}" -H "accept: text/plain" -H "X-API-KEY: ${apikey}" -H "Content-Type: text/plain" -d "${data}")
if [[ "${tx_data}" == *"error"* || "${tx_data}" == *"ERROR"* ]]; then if [[ "${tx_data}" == *"error"* || "${tx_data}" == *"ERROR"* ]]; then
echo "${tx_data}"; exit echo "Error creating transaction: ${tx_data}"
exit 1
elif [ -z "${tx_data}" ]; then elif [ -z "${tx_data}" ]; then
echo "Error: no transaction data returned"; exit echo "Error: no transaction data returned"
exit 1
fi fi
echo "Computing nonce..." echo "Computing nonce..."
computed_tx_data=$(curl --silent --insecure -X POST "http://${host}:${port}/arbitrary/compute" -H "Content-Type: application/json" -H "X-API-KEY: ${apikey}" -d "${tx_data}") computed_tx_data=$(curl --silent --insecure -X POST "http://${host}:${port}/arbitrary/compute" -H "Content-Type: application/json" -H "X-API-KEY: ${apikey}" -d "${tx_data}")
if [[ "${computed_tx_data}" == *"error"* || "${computed_tx_data}" == *"ERROR"* ]]; then if [[ "${computed_tx_data}" == *"error"* || "${computed_tx_data}" == *"ERROR"* ]]; then
echo "${computed_tx_data}"; exit echo "Error computing nonce: ${computed_tx_data}"
exit 1
fi fi
echo "Signing..." echo "Signing..."
signed_tx_data=$(curl --silent --insecure -X POST "http://${host}:${port}/transactions/sign" -H "Content-Type: application/json" -d "{\"privateKey\":\"${QORTAL_PRIVKEY}\",\"transactionBytes\":\"${computed_tx_data}\"}") signed_tx_data=$(curl --silent --insecure -X POST "http://${host}:${port}/transactions/sign" -H "Content-Type: application/json" -d "{\"privateKey\":\"${QORTAL_PRIVKEY}\",\"transactionBytes\":\"${computed_tx_data}\"}")
if [[ "${signed_tx_data}" == *"error"* || "${signed_tx_data}" == *"ERROR"* ]]; then if [[ "${signed_tx_data}" == *"error"* || "${signed_tx_data}" == *"ERROR"* ]]; then
echo "${signed_tx_data}"; exit echo "Error signing transaction: ${signed_tx_data}"
exit 1
fi fi
echo "Broadcasting..." echo "Broadcasting..."
success=$(curl --silent --insecure -X POST "http://${host}:${port}/transactions/process" -H "Content-Type: text/plain" -d "${signed_tx_data}") success=$(curl --silent --insecure -X POST "http://${host}:${port}/transactions/process" -H "Content-Type: text/plain" -d "${signed_tx_data}")
if [[ "${success}" == "true" ]]; then if [[ "${success}" == "true" ]]; then
echo "Transaction broadcast successfully" echo "Transaction broadcast successfully"
else else
@ -131,9 +218,10 @@ elif [[ "${method}" == "GET" ]]; then
echo "Empty response from ${host}:${port}" echo "Empty response from ${host}:${port}"
fi fi
if [[ "${response}" == *"error"* || "${response}" == *"ERROR"* ]]; then if [[ "${response}" == *"error"* || "${response}" == *"ERROR"* ]]; then
echo "${response}"; exit echo "${response}"
exit 1
fi fi
echo "${response}" echo "${response}"
fi fi

View File

@ -40,7 +40,7 @@ our %b58 = map { $b58[$_] => $_ } 0 .. 57;
our %reverseb58 = reverse %b58; our %reverseb58 = reverse %b58;
our $BASE_URL = $ENV{BASE_URL} || ($opt{t} ? 'http://localhost:62391' : 'http://localhost:12391'); our $BASE_URL = $ENV{BASE_URL} || ($opt{t} ? 'http://localhost:62391' : 'http://localhost:12391');
our $DEFAULT_FEE = 0.001; our $DEFAULT_FEE = 0.01;
our %TRANSACTION_TYPES = ( our %TRANSACTION_TYPES = (
payment => { payment => {
@ -92,7 +92,7 @@ our %TRANSACTION_TYPES = (
}, },
remove_group_admin => { remove_group_admin => {
url => 'groups/removeadmin', url => 'groups/removeadmin',
required => [qw(groupId txGroupId member)], required => [qw(groupId txGroupId admin)],
key_name => 'ownerPublicKey', key_name => 'ownerPublicKey',
}, },
group_approval => { group_approval => {