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
# On Windows, uncomment next line to set dirname:
# 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.rolling.ref = FILE
@ -59,11 +59,14 @@ appender.console.filter.threshold.level = error
appender.rolling.type = RollingFile
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.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.size = 4MB
appender.rolling.policy.size = 10MB
appender.rolling.strategy.type = DefaultRolloverStrategy
appender.rolling.strategy.max = 7
# Set the immediate flush to true (default)
# appender.rolling.immediateFlush = true
# 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>
<artifactId>AT</artifactId>
<versioning>
<release>1.4.0</release>
<release>1.4.1</release>
<versions>
<version>1.3.4</version>
<version>1.3.5</version>
@ -11,7 +11,8 @@
<version>1.3.7</version>
<version>1.3.8</version>
<version>1.4.0</version>
<version>1.4.1</version>
</versions>
<lastUpdated>20221105114346</lastUpdated>
<lastUpdated>20230821074325</lastUpdated>
</versioning>
</metadata>

View File

@ -1,7 +1,7 @@
rootLogger.level = info
# On Windows, uncomment next line to set dirname:
# 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.rolling.ref = FILE
@ -59,11 +59,14 @@ appender.console.filter.threshold.level = error
appender.rolling.type = RollingFile
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.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.size = 4MB
appender.rolling.policy.size = 10MB
appender.rolling.strategy.type = DefaultRolloverStrategy
appender.rolling.strategy.max = 7
# Set the immediate flush to true (default)
# appender.rolling.immediateFlush = true
# Set the append to true (default), should not overwrite

132
pom.xml
View File

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

View File

@ -27,6 +27,7 @@ import org.qortal.api.ApiException;
import org.qortal.api.ApiExceptionFactory;
import org.qortal.data.at.ATData;
import org.qortal.data.at.ATStateData;
import org.qortal.api.model.AtCreationRequest;
import org.qortal.data.transaction.DeployAtTransactionData;
import org.qortal.repository.DataException;
import org.qortal.repository.Repository;
@ -38,9 +39,14 @@ import org.qortal.transform.TransformationException;
import org.qortal.transform.transaction.DeployAtTransactionTransformer;
import org.qortal.utils.Base58;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
@Path("/at")
@Tag(name = "Automated Transactions")
public class AtResource {
private static final Logger logger = LoggerFactory.getLogger(AtResource.class);
@Context
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
@Operation(
summary = "Build raw, unsigned, DEPLOY_AT transaction",

View File

@ -16,11 +16,6 @@ import java.net.InetSocketAddress;
import java.net.UnknownHostException;
import java.nio.file.Files;
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.concurrent.TimeUnit;
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.appender.RollingFileAppender;
import org.json.JSONArray;
import org.json.JSONObject;
import org.qortal.account.Account;
import org.qortal.account.PrivateKeyAccount;
import org.qortal.api.*;
@ -269,7 +263,7 @@ public class AdminResource {
@GET
@Path("/summary")
@Operation(
summary = "Summary of activity since midnight, UTC",
summary = "Summary of activity past 24 hours",
responses = {
@ApiResponse(
content = @Content(schema = @Schema(implementation = ActivitySummary.class))
@ -283,22 +277,20 @@ public class AdminResource {
ActivitySummary summary = new ActivitySummary();
LocalDate date = LocalDate.now();
LocalTime time = LocalTime.of(0, 0);
ZoneOffset offset = ZoneOffset.UTC;
long start = OffsetDateTime.of(date, time, offset).toInstant().toEpochMilli();
long now = NTP.getTime();
long oneday = now - 24 * 60 * 60 * 1000L;
try (final Repository repository = RepositoryManager.getRepository()) {
int startHeight = repository.getBlockRepository().getHeightFromTimestamp(start);
int startHeight = repository.getBlockRepository().getHeightFromTimestamp(oneday);
int endHeight = repository.getBlockRepository().getBlockchainHeight();
summary.setBlockCount(endHeight - startHeight);
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;
} catch (DataException e) {

View File

@ -129,6 +129,11 @@ public class ArbitraryDataRenderer {
String filename = this.getFilename(unzippedPath, inPath);
Path filePath = Paths.get(unzippedPath, filename);
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 (!Files.exists(filePath)) {

View File

@ -84,6 +84,7 @@ public class Block {
TRANSACTION_PROCESSING_FAILED(53),
TRANSACTION_ALREADY_PROCESSED(54),
TRANSACTION_NEEDS_APPROVAL(55),
TRANSACTION_NOT_CONFIRMABLE(56),
AT_STATES_MISMATCH(61),
ONLINE_ACCOUNTS_INVALID(70),
ONLINE_ACCOUNT_UNKNOWN(71),
@ -130,6 +131,9 @@ public class Block {
/** Locally-generated AT fees */
protected long ourAtFees; // Generated locally
/** Cached online accounts validation decision, to avoid revalidating when true */
private boolean onlineAccountsAlreadyValid = false;
@FunctionalInterface
private interface BlockRewardDistributor {
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
/**
@ -1043,6 +1054,10 @@ public class Block {
if (this.blockData.getHeight() != null && this.blockData.getHeight() == 1)
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
ConciseSet accountIndexes = BlockTransformer.decodeOnlineAccounts(this.blockData.getEncodedOnlineAccounts());
// 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
this.cachedOnlineRewardShares = onlineRewardShares;
// Remember that the accounts are valid, to speed up subsequent checks
this.onlineAccountsAlreadyValid = true;
return ValidationResult.OK;
}
@ -1234,6 +1252,13 @@ public class Block {
|| transaction.getDeadline() <= this.blockData.getTimestamp())
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
if (this.repository.getTransactionRepository().isConfirmed(transactionData.getSignature()))
return ValidationResult.TRANSACTION_ALREADY_PROCESSED;

View File

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

View File

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

View File

@ -229,13 +229,6 @@ public class Synchronizer extends Thread {
peers.removeIf(Controller.hasOldVersion);
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
if (peers.size() < Settings.getInstance().getMinBlockchainPeers())
@ -262,10 +255,7 @@ public class Synchronizer extends Thread {
peers.removeIf(Controller.hasInferiorChainTip);
// 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
if (!recoveryMode) {
peers.removeIf(Controller.hasNoRecentBlock);
}
final int peersRemoved = peersBeforeComparison - peers.size();
if (peersRemoved > 0 && peers.size() > 0)
@ -1340,8 +1330,8 @@ public class Synchronizer extends Thread {
return SynchronizationResult.INVALID_DATA;
}
// Final check to make sure the peer isn't out of date (except for when we're in recovery mode)
if (!recoveryMode && peer.getChainTipData() != null) {
// Final check to make sure the peer isn't out of date
if (peer.getChainTipData() != null) {
final Long minLatestBlockTimestamp = Controller.getMinimumLatestBlockTimestamp();
final Long peerLastBlockTimestamp = peer.getChainTipData().getTimestamp();
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. */
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() {
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());
}
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) {
LOGGER.error("Repository issue while processing incoming transactions", e);
}
@ -254,6 +251,15 @@ public class TransactionImporter extends Thread {
int processedCount = 0;
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
try {
for (int i = 0; i < sigValidTransactions.size(); ++i) {
@ -286,6 +292,15 @@ public class TransactionImporter extends Thread {
case OK: {
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;
}
@ -314,9 +329,18 @@ public class TransactionImporter extends Thread {
// Transaction has been processed, even if only to reject it
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 {
LOGGER.debug("Finished importing {} incoming transaction{}", processedCount, (processedCount == 1 ? "" : "s"));
blockchainLock.unlock();
// Clear the unconfirmed transaction cache so new data can be populated in the next cycle
unconfirmedTransactionsCache = null;
}
} catch (DataException e) {
LOGGER.error("Repository issue while importing incoming transactions", e);

View File

@ -276,7 +276,10 @@ public class ArbitraryDataManager extends Thread {
int offset = 0;
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?
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.repository.DataException;
import org.qortal.repository.Repository;
import org.qortal.repository.RepositoryManager;
import org.qortal.transaction.DeployAtTransaction;
import org.qortal.transaction.MessageTransaction;
import org.qortal.transaction.Transaction.ValidationResult;
@ -317,21 +318,37 @@ public class BitcoinACCTv3TradeBot implements AcctTradeBot {
boolean isMessageAlreadySent = repository.getMessageRepository().exists(tradeBotData.getTradeNativePublicKey(), messageRecipient, messageData);
if (!isMessageAlreadySent) {
PrivateKeyAccount sender = new PrivateKeyAccount(repository, tradeBotData.getTradePrivateKey());
MessageTransaction messageTransaction = MessageTransaction.build(repository, sender, Group.NO_GROUP, messageRecipient, messageData, false, false);
// Do this in a new thread so caller doesn't have to wait for computeNonce()
// 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);
LOGGER.info("Computing nonce at difficulty {} for AT {} and recipient {}", messageTransaction.getPoWDifficulty(), tradeBotData.getAtAddress(), messageRecipient);
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
repository.discardChanges();
threadsRepository.discardChanges();
if (messageTransaction.isSignatureValid()) {
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()));
return ResponseResult.NETWORK_ISSUE;
}
}
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));
@ -548,11 +565,16 @@ public class BitcoinACCTv3TradeBot implements AcctTradeBot {
PrivateKeyAccount sender = new PrivateKeyAccount(repository, tradeBotData.getTradePrivateKey());
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();
MessageTransactionData newMessageTransactionData = (MessageTransactionData) outgoingMessageTransaction.getTransactionData();
LOGGER.info("Computed nonce {} at difficulty {}", newMessageTransactionData.getNonce(), outgoingMessageTransaction.getPoWDifficulty());
outgoingMessageTransaction.sign(sender);
// reset repository state to prevent deadlock
repository.discardChanges();
if (outgoingMessageTransaction.isSignatureValid()) {
ValidationResult result = outgoingMessageTransaction.importAsUnconfirmed();
if (result != ValidationResult.OK) {
@ -560,6 +582,11 @@ public class BitcoinACCTv3TradeBot implements AcctTradeBot {
return;
}
}
else {
LOGGER.warn(() -> String.format("Unable to send MESSAGE to AT %s: signature invalid", messageRecipient));
return;
}
}
TradeBot.updateTradeBotState(repository, tradeBotData, State.BOB_WAITING_FOR_AT_REDEEM,
() -> String.format("Locked AT %s to %s. Waiting for AT redeem", tradeBotData.getAtAddress(), aliceNativeAddress));
@ -668,11 +695,16 @@ public class BitcoinACCTv3TradeBot implements AcctTradeBot {
PrivateKeyAccount sender = new PrivateKeyAccount(repository, tradeBotData.getTradePrivateKey());
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();
MessageTransactionData newMessageTransactionData = (MessageTransactionData) messageTransaction.getTransactionData();
LOGGER.info("Computed nonce {} at difficulty {}", newMessageTransactionData.getNonce(), messageTransaction.getPoWDifficulty());
messageTransaction.sign(sender);
// Reset repository state to prevent deadlock
repository.discardChanges();
if (messageTransaction.isSignatureValid()) {
ValidationResult result = messageTransaction.importAsUnconfirmed();
if (result != ValidationResult.OK) {
@ -680,6 +712,11 @@ public class BitcoinACCTv3TradeBot implements AcctTradeBot {
return;
}
}
else {
LOGGER.warn(() -> String.format("Unable to send MESSAGE to AT %s: signature invalid", messageRecipient));
return;
}
}
TradeBot.updateTradeBotState(repository, tradeBotData, State.ALICE_DONE,
() -> String.format("Redeeming AT %s. Funds should arrive at %s",

View File

@ -19,6 +19,7 @@ import org.qortal.data.transaction.MessageTransactionData;
import org.qortal.group.Group;
import org.qortal.repository.DataException;
import org.qortal.repository.Repository;
import org.qortal.repository.RepositoryManager;
import org.qortal.transaction.DeployAtTransaction;
import org.qortal.transaction.MessageTransaction;
import org.qortal.transaction.Transaction.ValidationResult;
@ -317,21 +318,37 @@ public class DigibyteACCTv3TradeBot implements AcctTradeBot {
boolean isMessageAlreadySent = repository.getMessageRepository().exists(tradeBotData.getTradeNativePublicKey(), messageRecipient, messageData);
if (!isMessageAlreadySent) {
PrivateKeyAccount sender = new PrivateKeyAccount(repository, tradeBotData.getTradePrivateKey());
MessageTransaction messageTransaction = MessageTransaction.build(repository, sender, Group.NO_GROUP, messageRecipient, messageData, false, false);
// Do this in a new thread so caller doesn't have to wait for computeNonce()
// 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);
LOGGER.info("Computing nonce at difficulty {} for AT {} and recipient {}", messageTransaction.getPoWDifficulty(), tradeBotData.getAtAddress(), messageRecipient);
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
repository.discardChanges();
threadsRepository.discardChanges();
if (messageTransaction.isSignatureValid()) {
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()));
return ResponseResult.NETWORK_ISSUE;
}
}
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));
@ -548,11 +565,16 @@ public class DigibyteACCTv3TradeBot implements AcctTradeBot {
PrivateKeyAccount sender = new PrivateKeyAccount(repository, tradeBotData.getTradePrivateKey());
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();
MessageTransactionData newMessageTransactionData = (MessageTransactionData) outgoingMessageTransaction.getTransactionData();
LOGGER.info("Computed nonce {} at difficulty {}", newMessageTransactionData.getNonce(), outgoingMessageTransaction.getPoWDifficulty());
outgoingMessageTransaction.sign(sender);
// reset repository state to prevent deadlock
repository.discardChanges();
if (outgoingMessageTransaction.isSignatureValid()) {
ValidationResult result = outgoingMessageTransaction.importAsUnconfirmed();
if (result != ValidationResult.OK) {
@ -560,6 +582,11 @@ public class DigibyteACCTv3TradeBot implements AcctTradeBot {
return;
}
}
else {
LOGGER.warn(() -> String.format("Unable to send MESSAGE to AT %s: signature invalid", messageRecipient));
return;
}
}
TradeBot.updateTradeBotState(repository, tradeBotData, State.BOB_WAITING_FOR_AT_REDEEM,
() -> String.format("Locked AT %s to %s. Waiting for AT redeem", tradeBotData.getAtAddress(), aliceNativeAddress));
@ -668,11 +695,16 @@ public class DigibyteACCTv3TradeBot implements AcctTradeBot {
PrivateKeyAccount sender = new PrivateKeyAccount(repository, tradeBotData.getTradePrivateKey());
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();
MessageTransactionData newMessageTransactionData = (MessageTransactionData) messageTransaction.getTransactionData();
LOGGER.info("Computed nonce {} at difficulty {}", newMessageTransactionData.getNonce(), messageTransaction.getPoWDifficulty());
messageTransaction.sign(sender);
// Reset repository state to prevent deadlock
repository.discardChanges();
if (messageTransaction.isSignatureValid()) {
ValidationResult result = messageTransaction.importAsUnconfirmed();
if (result != ValidationResult.OK) {
@ -680,6 +712,11 @@ public class DigibyteACCTv3TradeBot implements AcctTradeBot {
return;
}
}
else {
LOGGER.warn(() -> String.format("Unable to send MESSAGE to AT %s: signature invalid", messageRecipient));
return;
}
}
TradeBot.updateTradeBotState(repository, tradeBotData, State.ALICE_DONE,
() -> String.format("Redeeming AT %s. Funds should arrive at %s",

View File

@ -19,6 +19,7 @@ import org.qortal.data.transaction.MessageTransactionData;
import org.qortal.group.Group;
import org.qortal.repository.DataException;
import org.qortal.repository.Repository;
import org.qortal.repository.RepositoryManager;
import org.qortal.transaction.DeployAtTransaction;
import org.qortal.transaction.MessageTransaction;
import org.qortal.transaction.Transaction.ValidationResult;
@ -317,21 +318,37 @@ public class DogecoinACCTv1TradeBot implements AcctTradeBot {
boolean isMessageAlreadySent = repository.getMessageRepository().exists(tradeBotData.getTradeNativePublicKey(), messageRecipient, messageData);
if (!isMessageAlreadySent) {
PrivateKeyAccount sender = new PrivateKeyAccount(repository, tradeBotData.getTradePrivateKey());
MessageTransaction messageTransaction = MessageTransaction.build(repository, sender, Group.NO_GROUP, messageRecipient, messageData, false, false);
// Do this in a new thread so caller doesn't have to wait for computeNonce()
// 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);
LOGGER.info("Computing nonce at difficulty {} for AT {} and recipient {}", messageTransaction.getPoWDifficulty(), tradeBotData.getAtAddress(), messageRecipient);
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
repository.discardChanges();
threadsRepository.discardChanges();
if (messageTransaction.isSignatureValid()) {
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()));
return ResponseResult.NETWORK_ISSUE;
}
}
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));
@ -548,11 +565,16 @@ public class DogecoinACCTv1TradeBot implements AcctTradeBot {
PrivateKeyAccount sender = new PrivateKeyAccount(repository, tradeBotData.getTradePrivateKey());
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();
MessageTransactionData newMessageTransactionData = (MessageTransactionData) outgoingMessageTransaction.getTransactionData();
LOGGER.info("Computed nonce {} at difficulty {}", newMessageTransactionData.getNonce(), outgoingMessageTransaction.getPoWDifficulty());
outgoingMessageTransaction.sign(sender);
// reset repository state to prevent deadlock
repository.discardChanges();
if (outgoingMessageTransaction.isSignatureValid()) {
ValidationResult result = outgoingMessageTransaction.importAsUnconfirmed();
if (result != ValidationResult.OK) {
@ -560,6 +582,11 @@ public class DogecoinACCTv1TradeBot implements AcctTradeBot {
return;
}
}
else {
LOGGER.warn(() -> String.format("Unable to send MESSAGE to AT %s: signature invalid", messageRecipient));
return;
}
}
TradeBot.updateTradeBotState(repository, tradeBotData, State.BOB_WAITING_FOR_AT_REDEEM,
() -> String.format("Locked AT %s to %s. Waiting for AT redeem", tradeBotData.getAtAddress(), aliceNativeAddress));
@ -668,11 +695,16 @@ public class DogecoinACCTv1TradeBot implements AcctTradeBot {
PrivateKeyAccount sender = new PrivateKeyAccount(repository, tradeBotData.getTradePrivateKey());
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();
MessageTransactionData newMessageTransactionData = (MessageTransactionData) messageTransaction.getTransactionData();
LOGGER.info("Computed nonce {} at difficulty {}", newMessageTransactionData.getNonce(), messageTransaction.getPoWDifficulty());
messageTransaction.sign(sender);
// Reset repository state to prevent deadlock
repository.discardChanges();
if (messageTransaction.isSignatureValid()) {
ValidationResult result = messageTransaction.importAsUnconfirmed();
if (result != ValidationResult.OK) {
@ -680,6 +712,11 @@ public class DogecoinACCTv1TradeBot implements AcctTradeBot {
return;
}
}
else {
LOGGER.warn(() -> String.format("Unable to send MESSAGE to AT %s: signature invalid", messageRecipient));
return;
}
}
TradeBot.updateTradeBotState(repository, tradeBotData, State.ALICE_DONE,
() -> String.format("Redeeming AT %s. Funds should arrive at %s",

View File

@ -19,6 +19,7 @@ import org.qortal.data.transaction.MessageTransactionData;
import org.qortal.group.Group;
import org.qortal.repository.DataException;
import org.qortal.repository.Repository;
import org.qortal.repository.RepositoryManager;
import org.qortal.transaction.DeployAtTransaction;
import org.qortal.transaction.MessageTransaction;
import org.qortal.transaction.Transaction.ValidationResult;
@ -317,21 +318,37 @@ public class DogecoinACCTv3TradeBot implements AcctTradeBot {
boolean isMessageAlreadySent = repository.getMessageRepository().exists(tradeBotData.getTradeNativePublicKey(), messageRecipient, messageData);
if (!isMessageAlreadySent) {
PrivateKeyAccount sender = new PrivateKeyAccount(repository, tradeBotData.getTradePrivateKey());
MessageTransaction messageTransaction = MessageTransaction.build(repository, sender, Group.NO_GROUP, messageRecipient, messageData, false, false);
// Do this in a new thread so caller doesn't have to wait for computeNonce()
// 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);
LOGGER.info("Computing nonce at difficulty {} for AT {} and recipient {}", messageTransaction.getPoWDifficulty(), tradeBotData.getAtAddress(), messageRecipient);
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
repository.discardChanges();
threadsRepository.discardChanges();
if (messageTransaction.isSignatureValid()) {
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()));
return ResponseResult.NETWORK_ISSUE;
}
}
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));
@ -548,11 +565,16 @@ public class DogecoinACCTv3TradeBot implements AcctTradeBot {
PrivateKeyAccount sender = new PrivateKeyAccount(repository, tradeBotData.getTradePrivateKey());
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();
MessageTransactionData newMessageTransactionData = (MessageTransactionData) outgoingMessageTransaction.getTransactionData();
LOGGER.info("Computed nonce {} at difficulty {}", newMessageTransactionData.getNonce(), outgoingMessageTransaction.getPoWDifficulty());
outgoingMessageTransaction.sign(sender);
// reset repository state to prevent deadlock
repository.discardChanges();
if (outgoingMessageTransaction.isSignatureValid()) {
ValidationResult result = outgoingMessageTransaction.importAsUnconfirmed();
if (result != ValidationResult.OK) {
@ -560,6 +582,11 @@ public class DogecoinACCTv3TradeBot implements AcctTradeBot {
return;
}
}
else {
LOGGER.warn(() -> String.format("Unable to send MESSAGE to AT %s: signature invalid", messageRecipient));
return;
}
}
TradeBot.updateTradeBotState(repository, tradeBotData, State.BOB_WAITING_FOR_AT_REDEEM,
() -> String.format("Locked AT %s to %s. Waiting for AT redeem", tradeBotData.getAtAddress(), aliceNativeAddress));
@ -668,11 +695,16 @@ public class DogecoinACCTv3TradeBot implements AcctTradeBot {
PrivateKeyAccount sender = new PrivateKeyAccount(repository, tradeBotData.getTradePrivateKey());
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();
MessageTransactionData newMessageTransactionData = (MessageTransactionData) messageTransaction.getTransactionData();
LOGGER.info("Computed nonce {} at difficulty {}", newMessageTransactionData.getNonce(), messageTransaction.getPoWDifficulty());
messageTransaction.sign(sender);
// Reset repository state to prevent deadlock
repository.discardChanges();
if (messageTransaction.isSignatureValid()) {
ValidationResult result = messageTransaction.importAsUnconfirmed();
if (result != ValidationResult.OK) {
@ -680,6 +712,11 @@ public class DogecoinACCTv3TradeBot implements AcctTradeBot {
return;
}
}
else {
LOGGER.warn(() -> String.format("Unable to send MESSAGE to AT %s: signature invalid", messageRecipient));
return;
}
}
TradeBot.updateTradeBotState(repository, tradeBotData, State.ALICE_DONE,
() -> String.format("Redeeming AT %s. Funds should arrive at %s",

View File

@ -325,16 +325,25 @@ public class LitecoinACCTv3TradeBot implements AcctTradeBot {
PrivateKeyAccount sender = new PrivateKeyAccount(threadsRepository, tradeBotData.getTradePrivateKey());
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();
MessageTransactionData newMessageTransactionData = (MessageTransactionData) messageTransaction.getTransactionData();
LOGGER.info("Computed nonce {} at difficulty {}", newMessageTransactionData.getNonce(), messageTransaction.getPoWDifficulty());
messageTransaction.sign(sender);
// reset repository state to prevent deadlock
threadsRepository.discardChanges();
if (messageTransaction.isSignatureValid()) {
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) {
LOGGER.warn(() -> String.format("Unable to send MESSAGE to Bob's trade-bot %s: %s", messageRecipient, e.getMessage()));
}
@ -556,11 +565,16 @@ public class LitecoinACCTv3TradeBot implements AcctTradeBot {
PrivateKeyAccount sender = new PrivateKeyAccount(repository, tradeBotData.getTradePrivateKey());
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();
MessageTransactionData newMessageTransactionData = (MessageTransactionData) outgoingMessageTransaction.getTransactionData();
LOGGER.info("Computed nonce {} at difficulty {}", newMessageTransactionData.getNonce(), outgoingMessageTransaction.getPoWDifficulty());
outgoingMessageTransaction.sign(sender);
// reset repository state to prevent deadlock
repository.discardChanges();
if (outgoingMessageTransaction.isSignatureValid()) {
ValidationResult result = outgoingMessageTransaction.importAsUnconfirmed();
if (result != ValidationResult.OK) {
@ -568,6 +582,11 @@ public class LitecoinACCTv3TradeBot implements AcctTradeBot {
return;
}
}
else {
LOGGER.warn(() -> String.format("Unable to send MESSAGE to AT %s: signature invalid", messageRecipient));
return;
}
}
TradeBot.updateTradeBotState(repository, tradeBotData, State.BOB_WAITING_FOR_AT_REDEEM,
() -> String.format("Locked AT %s to %s. Waiting for AT redeem", tradeBotData.getAtAddress(), aliceNativeAddress));
@ -676,11 +695,16 @@ public class LitecoinACCTv3TradeBot implements AcctTradeBot {
PrivateKeyAccount sender = new PrivateKeyAccount(repository, tradeBotData.getTradePrivateKey());
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();
MessageTransactionData newMessageTransactionData = (MessageTransactionData) messageTransaction.getTransactionData();
LOGGER.info("Computed nonce {} at difficulty {}", newMessageTransactionData.getNonce(), messageTransaction.getPoWDifficulty());
messageTransaction.sign(sender);
// Reset repository state to prevent deadlock
repository.discardChanges();
if (messageTransaction.isSignatureValid()) {
ValidationResult result = messageTransaction.importAsUnconfirmed();
if (result != ValidationResult.OK) {
@ -688,6 +712,11 @@ public class LitecoinACCTv3TradeBot implements AcctTradeBot {
return;
}
}
else {
LOGGER.warn(() -> String.format("Unable to send MESSAGE to AT %s: signature invalid", messageRecipient));
return;
}
}
TradeBot.updateTradeBotState(repository, tradeBotData, State.ALICE_DONE,
() -> String.format("Redeeming AT %s. Funds should arrive at %s",

View File

@ -20,6 +20,7 @@ import org.qortal.data.transaction.MessageTransactionData;
import org.qortal.group.Group;
import org.qortal.repository.DataException;
import org.qortal.repository.Repository;
import org.qortal.repository.RepositoryManager;
import org.qortal.transaction.DeployAtTransaction;
import org.qortal.transaction.MessageTransaction;
import org.qortal.transaction.Transaction.ValidationResult;
@ -320,21 +321,37 @@ public class PirateChainACCTv3TradeBot implements AcctTradeBot {
boolean isMessageAlreadySent = repository.getMessageRepository().exists(tradeBotData.getTradeNativePublicKey(), messageRecipient, messageData);
if (!isMessageAlreadySent) {
PrivateKeyAccount sender = new PrivateKeyAccount(repository, tradeBotData.getTradePrivateKey());
MessageTransaction messageTransaction = MessageTransaction.build(repository, sender, Group.NO_GROUP, messageRecipient, messageData, false, false);
// Do this in a new thread so caller doesn't have to wait for computeNonce()
// 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);
LOGGER.info("Computing nonce at difficulty {} for AT {} and recipient {}", messageTransaction.getPoWDifficulty(), tradeBotData.getAtAddress(), messageRecipient);
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
repository.discardChanges();
threadsRepository.discardChanges();
if (messageTransaction.isSignatureValid()) {
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()));
return ResponseResult.NETWORK_ISSUE;
}
}
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));
@ -561,11 +578,16 @@ public class PirateChainACCTv3TradeBot implements AcctTradeBot {
PrivateKeyAccount sender = new PrivateKeyAccount(repository, tradeBotData.getTradePrivateKey());
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();
MessageTransactionData newMessageTransactionData = (MessageTransactionData) outgoingMessageTransaction.getTransactionData();
LOGGER.info("Computed nonce {} at difficulty {}", newMessageTransactionData.getNonce(), outgoingMessageTransaction.getPoWDifficulty());
outgoingMessageTransaction.sign(sender);
// reset repository state to prevent deadlock
repository.discardChanges();
if (outgoingMessageTransaction.isSignatureValid()) {
ValidationResult result = outgoingMessageTransaction.importAsUnconfirmed();
if (result != ValidationResult.OK) {
@ -573,6 +595,11 @@ public class PirateChainACCTv3TradeBot implements AcctTradeBot {
return;
}
}
else {
LOGGER.warn(() -> String.format("Unable to send MESSAGE to AT %s: signature invalid", messageRecipient));
return;
}
}
TradeBot.updateTradeBotState(repository, tradeBotData, State.BOB_WAITING_FOR_AT_REDEEM,
() -> String.format("Locked AT %s to %s. Waiting for AT redeem", tradeBotData.getAtAddress(), aliceNativeAddress));
@ -681,11 +708,16 @@ public class PirateChainACCTv3TradeBot implements AcctTradeBot {
PrivateKeyAccount sender = new PrivateKeyAccount(repository, tradeBotData.getTradePrivateKey());
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();
MessageTransactionData newMessageTransactionData = (MessageTransactionData) messageTransaction.getTransactionData();
LOGGER.info("Computed nonce {} at difficulty {}", newMessageTransactionData.getNonce(), messageTransaction.getPoWDifficulty());
messageTransaction.sign(sender);
// Reset repository state to prevent deadlock
repository.discardChanges();
if (messageTransaction.isSignatureValid()) {
ValidationResult result = messageTransaction.importAsUnconfirmed();
if (result != ValidationResult.OK) {
@ -693,6 +725,11 @@ public class PirateChainACCTv3TradeBot implements AcctTradeBot {
return;
}
}
else {
LOGGER.warn(() -> String.format("Unable to send MESSAGE to AT %s: signature invalid", messageRecipient));
return;
}
}
TradeBot.updateTradeBotState(repository, tradeBotData, State.ALICE_DONE,
() -> String.format("Redeeming AT %s. Funds should arrive at %s",

View File

@ -19,6 +19,7 @@ import org.qortal.data.transaction.MessageTransactionData;
import org.qortal.group.Group;
import org.qortal.repository.DataException;
import org.qortal.repository.Repository;
import org.qortal.repository.RepositoryManager;
import org.qortal.transaction.DeployAtTransaction;
import org.qortal.transaction.MessageTransaction;
import org.qortal.transaction.Transaction.ValidationResult;
@ -317,21 +318,37 @@ public class RavencoinACCTv3TradeBot implements AcctTradeBot {
boolean isMessageAlreadySent = repository.getMessageRepository().exists(tradeBotData.getTradeNativePublicKey(), messageRecipient, messageData);
if (!isMessageAlreadySent) {
PrivateKeyAccount sender = new PrivateKeyAccount(repository, tradeBotData.getTradePrivateKey());
MessageTransaction messageTransaction = MessageTransaction.build(repository, sender, Group.NO_GROUP, messageRecipient, messageData, false, false);
// Do this in a new thread so caller doesn't have to wait for computeNonce()
// 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);
LOGGER.info("Computing nonce at difficulty {} for AT {} and recipient {}", messageTransaction.getPoWDifficulty(), tradeBotData.getAtAddress(), messageRecipient);
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
repository.discardChanges();
threadsRepository.discardChanges();
if (messageTransaction.isSignatureValid()) {
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()));
return ResponseResult.NETWORK_ISSUE;
}
}
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));
@ -548,11 +565,16 @@ public class RavencoinACCTv3TradeBot implements AcctTradeBot {
PrivateKeyAccount sender = new PrivateKeyAccount(repository, tradeBotData.getTradePrivateKey());
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();
MessageTransactionData newMessageTransactionData = (MessageTransactionData) outgoingMessageTransaction.getTransactionData();
LOGGER.info("Computed nonce {} at difficulty {}", newMessageTransactionData.getNonce(), outgoingMessageTransaction.getPoWDifficulty());
outgoingMessageTransaction.sign(sender);
// reset repository state to prevent deadlock
repository.discardChanges();
if (outgoingMessageTransaction.isSignatureValid()) {
ValidationResult result = outgoingMessageTransaction.importAsUnconfirmed();
if (result != ValidationResult.OK) {
@ -560,6 +582,11 @@ public class RavencoinACCTv3TradeBot implements AcctTradeBot {
return;
}
}
else {
LOGGER.warn(() -> String.format("Unable to send MESSAGE to AT %s: signature invalid", messageRecipient));
return;
}
}
TradeBot.updateTradeBotState(repository, tradeBotData, State.BOB_WAITING_FOR_AT_REDEEM,
() -> String.format("Locked AT %s to %s. Waiting for AT redeem", tradeBotData.getAtAddress(), aliceNativeAddress));
@ -668,11 +695,16 @@ public class RavencoinACCTv3TradeBot implements AcctTradeBot {
PrivateKeyAccount sender = new PrivateKeyAccount(repository, tradeBotData.getTradePrivateKey());
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();
MessageTransactionData newMessageTransactionData = (MessageTransactionData) messageTransaction.getTransactionData();
LOGGER.info("Computed nonce {} at difficulty {}", newMessageTransactionData.getNonce(), messageTransaction.getPoWDifficulty());
messageTransaction.sign(sender);
// Reset repository state to prevent deadlock
repository.discardChanges();
if (messageTransaction.isSignatureValid()) {
ValidationResult result = messageTransaction.importAsUnconfirmed();
if (result != ValidationResult.OK) {
@ -680,6 +712,11 @@ public class RavencoinACCTv3TradeBot implements AcctTradeBot {
return;
}
}
else {
LOGGER.warn(() -> String.format("Unable to send MESSAGE to AT %s: signature invalid", messageRecipient));
return;
}
}
TradeBot.updateTradeBotState(repository, tradeBotData, State.ALICE_DONE,
() -> String.format("Redeeming AT %s. Funds should arrive at %s",

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);
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));
@ -712,31 +712,17 @@ public class TradeBot implements Listener {
}
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);
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 = repository.getTransactionRepository().getUnconfirmedTransactions(Arrays.asList(Transaction.TransactionType.MESSAGE), null, null, null, null);
List<TransactionData> transactions = new ArrayList<>(signatures.size());
for (byte[] signature : signatures) {
transactions.add(repository.getTransactionRepository().fromSignature(signature));
}
transactions.sort(Transaction.getDataComparator());
// Get timestamp of the first MESSAGE transaction
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) {
for (TransactionData transactionData : transactions) {
// Treat as failed if buy attempt was more than 60 mins ago (as it's still in the OFFERING state)
if (transactionData.getRecipient().equals(crossChainTradeData.qortalCreatorTradeAddress) && now - transactionData.getTimestamp() > 60*60*1000L) {
failedTrades.put(crossChainTradeData.qortalAtAddress, now);
updatedCrossChainTrades.remove(crossChainTradeData);
}
else {
} else {
validTrades.put(crossChainTradeData.qortalAtAddress, now);
}
}
} catch (DataException e) {
LOGGER.info("Unable to determine failed state of AT {}", crossChainTradeData.qortalAtAddress);

View File

@ -46,18 +46,6 @@ public class Bitcoin extends Bitcoiny {
return Arrays.asList(
// Servers chosen on NO BASIS WHATSOEVER from various sources!
// Status verified at https://1209k.com/bitcoin-eye/ele.php?chain=btc
//CLOSED new Server("bitcoin.grey.pw", Server.ConnectionType.SSL, 50002),
//CLOSED new Server("btc.litepay.ch", Server.ConnectionType.SSL, 50002),
//CLOSED new Server("electrum.pabu.io", Server.ConnectionType.SSL, 50002),
//CLOSED new Server("electrumx.dev", Server.ConnectionType.SSL, 50002),
//CLOSED new Server("electrumx.hodlwallet.com", Server.ConnectionType.SSL, 50002),
//CLOSED new Server("gd42.org", Server.ConnectionType.SSL, 50002),
//CLOSED new Server("korea.electrum-server.com", Server.ConnectionType.SSL, 50002),
//CLOSED new Server("prospero.bitsrc.net", Server.ConnectionType.SSL, 50002),
//1.15.0 new Server("alviss.coinjoined.com", Server.ConnectionType.SSL, 50002),
//1.15.0 new Server("electrum.acinq.co", Server.ConnectionType.SSL, 50002),
//1.14.0 new Server("electrum.coinext.com.br", Server.ConnectionType.SSL, 50002),
//F1.7.0 new Server("btc.lastingcoin.net", Server.ConnectionType.SSL, 50002),
new Server("104.248.139.211", Server.ConnectionType.SSL, 50002),
new Server("128.0.190.26", Server.ConnectionType.SSL, 50002),
new Server("142.93.6.38", Server.ConnectionType.SSL, 50002),
@ -126,7 +114,8 @@ public class Bitcoin extends Bitcoiny {
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));
new Server("xtrum.com", Server.ConnectionType.SSL, 50002)
);
}
@Override
@ -157,7 +146,8 @@ public class Bitcoin extends Bitcoiny {
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.SSL, 51002),
new Server("testnet.hsmiths.com", Server.ConnectionType.SSL, 53012));
new Server("testnet.hsmiths.com", Server.ConnectionType.SSL, 53012)
);
}
@Override
@ -180,7 +170,8 @@ public class Bitcoin extends Bitcoiny {
public Collection<ElectrumX.Server> getServers() {
return Arrays.asList(
new Server("localhost", Server.ConnectionType.TCP, 50001),
new Server("localhost", Server.ConnectionType.SSL, 50002));
new Server("localhost", Server.ConnectionType.SSL, 50002)
);
}
@Override

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -75,6 +75,9 @@ public abstract class TransactionData {
@Schema(description = "groupID for this transaction")
protected int txGroupId;
@Schema(description = "recipient for this transaction")
protected String recipient;
// Not always present
@Schema(accessMode = AccessMode.READ_ONLY, hidden = true, description = "height of block containing transaction")
protected Integer blockHeight;
@ -105,7 +108,7 @@ public abstract class TransactionData {
/** Constructor for use by transaction subclasses. */
protected TransactionData(TransactionType type, BaseTransactionData baseTransactionData) {
this.type = type;
this.recipient = baseTransactionData.recipient;
this.timestamp = baseTransactionData.timestamp;
this.txGroupId = baseTransactionData.txGroupId;
this.reference = baseTransactionData.reference;
@ -136,6 +139,10 @@ public abstract class TransactionData {
return this.txGroupId;
}
public String getRecipient() {
return this.recipient;
}
public void setTxGroupId(int txGroupId) {
this.txGroupId = txGroupId;
}
@ -250,5 +257,4 @@ public abstract class TransactionData {
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.controller.Controller;
import org.qortal.controller.arbitrary.ArbitraryDataFileListManager;
import org.qortal.controller.arbitrary.ArbitraryDataManager;
import org.qortal.crypto.Crypto;
import org.qortal.data.block.BlockData;
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
/**
* 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 String bindAddress = null;
@ -240,6 +255,16 @@ public class 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() {
return SingletonContainer.INSTANCE;
}
@ -952,6 +977,37 @@ public class Network {
// 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
switch (message.getType()) {
case GET_PEERS:
@ -979,6 +1035,15 @@ public class Network {
Controller.getInstance().onNetworkMessage(peer, message);
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) {

View File

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

View File

@ -29,6 +29,7 @@ import org.qortal.crosschain.Dogecoin.DogecoinNet;
import org.qortal.crosschain.Digibyte.DigibyteNet;
import org.qortal.crosschain.Ravencoin.RavencoinNet;
import org.qortal.crosschain.PirateChain.PirateChainNet;
import org.qortal.network.message.MessageType;
import org.qortal.utils.EnumUtils;
// All properties to be converted to JSON via JAXB
@ -114,7 +115,6 @@ public class Settings {
private Integer devProxyPort;
private boolean devProxyLoggingEnabled = false;
// Specific to this node
private boolean wipeUnconfirmedOnStart = false;
/** Maximum number of unconfirmed transactions allowed per account */
@ -147,7 +147,7 @@ public class Settings {
private int blockCacheSize = 10;
/** 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). */
private long atStatesMaxLifetime = 5 * 24 * 60 * 60 * 1000L; // milliseconds
@ -186,7 +186,6 @@ public class Settings {
* This has a significant effect on execution time. */
private int blockPruneBatchSize = 10000; // blocks
/** Whether we should archive old data to reduce the database size */
private boolean archiveEnabled = true;
/** How often to attempt archiving (ms). */
@ -194,15 +193,12 @@ public class Settings {
/** Serialization version to use when building an archive */
private int defaultArchiveVersion = 2;
/** Whether to automatically bootstrap instead of syncing from genesis */
private boolean bootstrap = true;
/** Registered names integrity check */
private boolean namesIntegrityCheckEnabled = false;
// Peer-to-peer related
private boolean isTestNet = false;
/** Single node testnet mode */
@ -227,10 +223,10 @@ public class Settings {
private int maxRetries = 2;
/** 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 */
private String minPeerVersion = "4.1.2";
private String minPeerVersion = "4.3.1";
/** Whether to allow connections with peers below minPeerVersion
* If true, we won't sync with them but they can still sync with us, and will show in the peers list
* If false, sync will be blocked both ways, and they will not appear in the peers list */
@ -311,17 +307,35 @@ public class Settings {
"1.pool.ntp.org",
"2.pool.ntp.org",
"3.pool.ntp.org",
"cn.pool.ntp.org",
"0.cn.pool.ntp.org",
"1.cn.pool.ntp.org",
"2.cn.pool.ntp.org",
"3.cn.pool.ntp.org"
"asia.pool.ntp.org",
"0.asia.pool.ntp.org",
"1.asia.pool.ntp.org",
"2.asia.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() */
private Long testNtpOffset = null;
/* Foreign chains */
/** 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) */
private int bitcoinjLookaheadSize = 50;
// Data storage (QDN)
/** Data storage enabled/disabled*/
@ -371,6 +383,58 @@ public class Settings {
/** Whether to serve QDN data without authentication */
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
public static class DomainMap {
private String domain;
@ -396,7 +460,6 @@ public class Settings {
}
}
// Constructors
private Settings() {
@ -497,6 +560,9 @@ public class Settings {
}
} while (settings.userPath != null);
// Set some additional defaults if needed
settings.setAdditionalDefaults();
// Validate settings
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
public String getUserPath() {
@ -671,7 +753,6 @@ public class Settings {
return this.devProxyLoggingEnabled;
}
public boolean getWipeUnconfirmedOnStart() {
return this.wipeUnconfirmedOnStart;
}
@ -1054,4 +1135,20 @@ public class Settings {
}
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)
return ValidationResult.NEGATIVE_FEE;
// After the feature trigger, we require the fee to be sufficient if it's not 0.
// If the fee is zero, then the nonce is validated in isSignatureValid() as an alternative to a fee
// As of the mempow transaction updates timestamp, a nonce is no longer supported, so a valid fee must be included
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) {
return super.isFeeValid();
}
@ -225,7 +231,13 @@ public class ArbitraryTransaction extends Transaction {
// Clear nonce from 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();
if (beforeFeatureTrigger || this.arbitraryTransactionData.getFee() == 0L) {
// 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
}
@Override
public boolean isConfirmable() {
// CHAT transactions can't go into blocks
return false;
}
@Override
public ValidationResult isValid() throws DataException {
// 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 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
@ -109,7 +111,17 @@ public class MessageTransaction extends Transaction {
MessageTransactionTransformer.clearNonce(transactionBytes);
// 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();
}
@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
public ValidationResult isValid() throws DataException {
// 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);
// Check nonce
return MemoryPoW.verify2(transactionBytes, POW_BUFFER_SIZE, POW_DIFFICULTY, nonce);
return MemoryPoW.verify2(transactionBytes, POW_BUFFER_SIZE, getPoWDifficulty(), nonce);
}
@Override
@ -256,6 +280,11 @@ public class MessageTransaction extends Transaction {
@Override
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 (this.messageTransactionData.getAmount() == 0L)
return;
@ -280,6 +309,11 @@ public class MessageTransaction extends Transaction {
@Override
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 (this.messageTransactionData.getAmount() == 0L)
return;

View File

@ -155,6 +155,12 @@ public class PresenceTransaction extends Transaction {
// Nothing to do
}
@Override
public boolean isConfirmable() {
// PRESENCE transactions can't go into blocks
return false;
}
@Override
public ValidationResult isValid() throws DataException {
// 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.api.resource.TransactionsResource.ConfirmationStatus;
import org.qortal.asset.Asset;
import org.qortal.block.BlockChain;
import org.qortal.crypto.MemoryPoW;
import org.qortal.data.transaction.PublicizeTransactionData;
import org.qortal.data.transaction.TransactionData;
@ -89,6 +90,12 @@ public class PublicizeTransaction extends Transaction {
@Override
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
List<byte[]> signatures = this.repository.getTransactionRepository().getSignaturesMatchingCriteria(
TransactionType.PUBLICIZE,

View File

@ -13,6 +13,7 @@ import org.qortal.account.PublicKeyAccount;
import org.qortal.asset.Asset;
import org.qortal.block.BlockChain;
import org.qortal.controller.Controller;
import org.qortal.controller.TransactionImporter;
import org.qortal.crypto.Crypto;
import org.qortal.data.block.BlockData;
import org.qortal.data.group.GroupApprovalData;
@ -247,7 +248,8 @@ public abstract class Transaction {
GROUP_APPROVAL_REQUIRED(98),
ACCOUNT_NOT_TRANSFERABLE(99),
INVALID_BUT_OK(999),
NOT_YET_RELEASED(1000);
NOT_YET_RELEASED(1000),
NOT_SUPPORTED(1001);
public final int value;
@ -377,7 +379,7 @@ public abstract class Transaction {
* @return
*/
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 {
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
// 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
* @throws DataException
@ -650,7 +655,8 @@ public abstract class Transaction {
TransactionData transactionData = unconfirmedTransactionsIterator.next();
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();
}
@ -888,6 +894,17 @@ public abstract class Transaction {
/* 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.
* <p>

View File

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

View File

@ -4,52 +4,52 @@
# "localeLang": "de",
### 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
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 ###
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
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 ###
BLOCK_UNKNOWN = block unbekannt
BLOCK_UNKNOWN = Block unbekannt
### Transactions ###
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
TRANSACTION_INVALID = Transaktion ungültig: %s (%s)
@ -58,19 +58,19 @@ TRANSACTION_INVALID = Transaktion ungültig: %s (%s)
NAME_UNKNOWN = Name unbekannt
### 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 ###
GROUP_UNKNOWN = Gruppe unbekannt
### 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)
@ -80,4 +80,4 @@ ORDER_SIZE_TOO_SMALL = Bestellmenge zu niedrig
### Data ###
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",
### 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
REPOSITORY_ISSUE = repository fout
REPOSITORY_ISSUE = fout in repository
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 ###
INVALID_SIGNATURE = ongeldige handtekening
INVALID_SIGNATURE = ongeldige signature
INVALID_ADDRESS = ongeldig adres
INVALID_PUBLIC_KEY = ongeldige public key
INVALID_DATA = ongeldige gegevens
INVALID_DATA = ongeldige data
INVALID_NETWORK_ADDRESS = ongeldig netwerkadres
ADDRESS_UNKNOWN = account adres onbekend
ADDRESS_UNKNOWN = account-adres onbekend
INVALID_CRITERIA = ongeldige zoekcriteria
INVALID_REFERENCE = ongeldige verwijzing
INVALID_REFERENCE = ongeldige referentie
TRANSFORMATION_ERROR = JSON kon niet omgezet worden in transactie
@ -44,10 +44,10 @@ INVALID_HEIGHT = ongeldige blokhoogte
CANNOT_MINT = account kan niet minten
### Blocks ###
BLOCK_UNKNOWN = blok onbekend
BLOCK_UNKNOWN = blok niet gekend
### Transactions ###
TRANSACTION_UNKNOWN = onbekende transactie
TRANSACTION_UNKNOWN = transactie niet gekend
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)
### Naming ###
NAME_UNKNOWN = onbekende naam
NAME_UNKNOWN = naam niet gekend
### Asset ###
INVALID_ASSET_ID = ongeldige asset ID
INVALID_ORDER_ID = ongeldige asset order ID
ORDER_UNKNOWN = onbekende asset order ID
ORDER_UNKNOWN = niet gekende asset order ID
### Groups ###
GROUP_UNKNOWN = onbekende groep
GROUP_UNKNOWN = groep niet gekend
### 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 ###
ORDER_SIZE_TOO_SMALL = order bedrag te laag
ORDER_SIZE_TOO_SMALL = order-bedrag te laag
### 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/)
# 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
@ -19,7 +19,7 @@ CONNECTION = Verbindung
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
@ -31,18 +31,18 @@ EXIT = Verlassen
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/)
# 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
BLOCK_HEIGHT = Block hoogte
BLOCK_HEIGHT = blok-hoogte
BLOCKS_REMAINING = blocks remaining
BLOCKS_REMAINING = overblijvende blokken
BUILD_VERSION = Versienummer
CHECK_TIME_ACCURACY = Controleer accuraatheid van de tijd
CHECK_TIME_ACCURACY = Controleer of de tijd correct is
CONNECTING = Verbinden
@ -33,16 +33,16 @@ LITE_NODE = Lite node
MINTING_DISABLED = Minten is uitgeschakeld
MINTING_ENABLED = \u2714 Minten is ingeschakeld
MINTING_ENABLED = \u2714 Minten is actief
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
SYNCHRONIZING_BLOCKCHAIN = Aan het synchronizeren
SYNCHRONIZING_BLOCKCHAIN = Bezig met synchronizeren
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
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_IS_FINISHED = AT ist fertig
AT_IS_FINISHED = AT ist beendet
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_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
@ -64,81 +64,81 @@ INVALID_ADDRESS = ungültige Adresse
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_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_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_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_OWNER = ungültiger Besitzername
INVALID_NAME_OWNER = ungültiger Eigentümer des Namens
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_PAYMENTS_COUNT = Anzahl ungültiger Zahlungen
INVALID_PAYMENTS_COUNT = ungültige Anzahl der Zahlungen
INVALID_PUBLIC_KEY = ungültiger öffentlicher Schlüssel
INVALID_QUANTITY = unzulässige Menge
INVALID_QUANTITY = ungültige Menge
INVALID_REFERENCE = ungültige Referenz
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_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
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
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_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
@ -150,46 +150,46 @@ NEGATIVE_PRICE = ungültiger/negativer Preis
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
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
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
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_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
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
ASSET_ALREADY_EXISTS = asset bestaat al
ASSET_ALREADY_EXISTS = asset bestaat reeds
ASSET_DOES_NOT_EXIST = asset bestaat niet
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
@ -38,25 +38,25 @@ BUYER_ALREADY_OWNER = koper is al de eigenaar
CLOCK_NOT_SYNCED = klok is niet gesynchronizeerd
DUPLICATE_MESSAGE = dubbel adres bericht
DUPLICATE_MESSAGE = adres heeft dubbel bericht verzonden
DUPLICATE_OPTION = dubbele optie
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_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
@ -72,19 +72,19 @@ INVALID_AT_TYPE_LENGTH = ongeldige lengte voor AT type
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_BLOCK_DELAY = ongeldige groep blok vertraging
INVALID_GROUP_BLOCK_DELAY = ongeldige blok-vertraging bij groepsgoedkeuring
INVALID_GROUP_ID = ongeldige groep-ID
INVALID_GROUP_OWNER = ongeldige groep eigenaar
INVALID_GROUP_OWNER = ongeldige groep-eigenaar
INVALID_LIFETIME = ongeldige levensduur
@ -94,27 +94,27 @@ INVALID_NAME_OWNER = ongeldige naam voor eigenaar
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_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_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
@ -124,15 +124,15 @@ INVITE_UNKNOWN = onbekende groepsuitnodiging
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
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
@ -140,7 +140,7 @@ NAME_DOES_NOT_EXIST = naam bestaat niet
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
@ -148,9 +148,9 @@ NEGATIVE_FEE = negatieve vergoeding
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
@ -176,9 +176,9 @@ POLL_OPTION_DOES_NOT_EXIST = peilingsoptie bestaat niet
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
@ -186,10 +186,10 @@ TIMESTAMP_TOO_OLD = tijdstempel te oud
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
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.Test;
import org.qortal.crypto.MemoryPoW;
import org.qortal.repository.DataException;
import org.qortal.test.common.Common;
import static org.junit.Assert.*;
@ -39,13 +41,14 @@ public class MemoryPoWTests {
}
@Test
public void testMultipleComputes() {
public void testMultipleComputes() throws DataException {
Common.useDefaultSettings();
Random random = new Random();
final int sampleSize = 20;
final int sampleSize = 10;
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];
long[] times = new long[sampleSize];

View File

@ -1,5 +1,6 @@
package org.qortal.test;
import org.apache.commons.lang3.reflect.FieldUtils;
import org.junit.After;
import org.junit.Before;
import org.junit.Test;
@ -14,12 +15,9 @@ import org.qortal.group.Group.ApprovalThreshold;
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.GroupUtils;
import org.qortal.test.common.TestAccount;
import org.qortal.test.common.TransactionUtils;
import org.qortal.test.common.*;
import org.qortal.test.common.transaction.TestTransaction;
import org.qortal.transaction.DeployAtTransaction;
import org.qortal.transaction.MessageTransaction;
import org.qortal.transaction.Transaction;
import org.qortal.transaction.Transaction.TransactionType;
@ -31,6 +29,7 @@ import org.qortal.utils.NTP;
import static org.junit.Assert.*;
import java.util.List;
import java.util.Random;
public class MessageTests extends Common {
@ -85,7 +84,7 @@ public class MessageTests extends Common {
byte[] randomReference = new byte[64];
random.nextBytes(randomReference);
long minimumFee = BlockChain.getInstance().getUnitFee();
long minimumFee = BlockChain.getInstance().getUnitFeeAtTimestamp(System.currentTimeMillis());
try (final Repository repository = RepositoryManager.getRepository()) {
PrivateKeyAccount alice = Common.getTestAccount(repository, "alice");
@ -139,7 +138,7 @@ public class MessageTests extends Common {
}
@Test
public void withRecipentWithAmount() throws DataException {
public void withRecipientWithAmount() throws DataException {
testMessage(Group.NO_GROUP, recipient, 123L, Asset.QORT);
}
@ -153,6 +152,140 @@ public class MessageTests extends Common {
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
public void serializationTests() throws DataException, TransformationException {
// with recipient, with amount
@ -165,6 +298,24 @@ public class MessageTests extends Common {
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 {
try (final Repository repository = RepositoryManager.getRepository()) {
TestAccount alice = Common.getTestAccount(repository, "alice");
@ -195,8 +346,14 @@ public class MessageTests extends Common {
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()) {
return testFeeNonce(repository, withFee, withNonce, recipient, isValid);
}
}
private MessageTransaction testFeeNonce(Repository repository, boolean withFee, boolean withNonce, String recipient, boolean isValid) throws DataException {
TestAccount alice = Common.getTestAccount(repository, "alice");
int txGroupId = 0;
@ -226,10 +383,11 @@ public class MessageTests extends Common {
transaction.sign(alice);
assertEquals(isValid, transaction.isSignatureValid());
}
return transaction;
}
private void testMessage(int txGroupId, String recipient, long amount, Long assetId) throws DataException {
private MessageTransaction testMessage(int txGroupId, String recipient, long amount, Long assetId) throws DataException {
try (final Repository repository = RepositoryManager.getRepository()) {
TestAccount alice = Common.getTestAccount(repository, "alice");
@ -244,6 +402,8 @@ public class MessageTests extends Common {
TransactionUtils.signAndMint(repository, transactionData, alice);
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.misc.Category;
import org.qortal.arbitrary.misc.Service;
import org.qortal.block.BlockChain;
import org.qortal.controller.arbitrary.ArbitraryDataManager;
import org.qortal.data.arbitrary.ArbitraryResourceMetadata;
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.transaction.RegisterNameTransaction;
import org.qortal.utils.Base58;
import org.qortal.utils.NTP;
import java.io.IOException;
import java.nio.file.Files;
@ -106,8 +108,9 @@ public class ArbitraryTransactionMetadataTests extends Common {
// Create PUT transaction
Path path1 = ArbitraryUtils.generateRandomDataPath(dataLength);
long fee = BlockChain.getInstance().getUnitFeeAtTimestamp(NTP.getTime());
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);
// Check the chunk count is correct
@ -157,8 +160,9 @@ public class ArbitraryTransactionMetadataTests extends Common {
// Create PUT transaction
Path path1 = ArbitraryUtils.generateRandomDataPath(dataLength);
long fee = BlockChain.getInstance().getUnitFeeAtTimestamp(NTP.getTime());
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);
// Check the chunk count is correct
@ -220,8 +224,9 @@ public class ArbitraryTransactionMetadataTests extends Common {
// Create PUT transaction
Path path1 = ArbitraryUtils.generateRandomDataPath(dataLength);
long fee = BlockChain.getInstance().getUnitFeeAtTimestamp(NTP.getTime());
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);
// Check the chunk count is correct
@ -272,8 +277,9 @@ public class ArbitraryTransactionMetadataTests extends Common {
// Create PUT transaction
Path path1 = ArbitraryUtils.generateRandomDataPath(dataLength);
long fee = BlockChain.getInstance().getUnitFeeAtTimestamp(NTP.getTime());
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);
// Check the chunk count is correct
@ -316,8 +322,9 @@ public class ArbitraryTransactionMetadataTests extends Common {
// Create PUT transaction
Path path1 = ArbitraryUtils.generateRandomDataPath(dataLength);
long fee = BlockChain.getInstance().getUnitFeeAtTimestamp(NTP.getTime());
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);
// 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.misc.Category;
import org.qortal.arbitrary.misc.Service;
import org.qortal.block.BlockChain;
import org.qortal.controller.arbitrary.ArbitraryDataManager;
import org.qortal.crypto.Crypto;
import org.qortal.data.PaymentData;
@ -50,51 +51,6 @@ public class ArbitraryTransactionTests extends Common {
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
public void testNonceAndFee() throws IllegalAccessException, DataException, IOException {
try (final Repository repository = RepositoryManager.getRepository()) {
@ -497,8 +453,9 @@ public class ArbitraryTransactionTests extends Common {
// Create PUT transaction
Path path1 = ArbitraryUtils.generateRandomDataPath(dataLength, true);
long fee = BlockChain.getInstance().getUnitFeeAtTimestamp(NTP.getTime());
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);
byte[] signature = arbitraryDataFile.getSignature();
@ -556,8 +513,9 @@ public class ArbitraryTransactionTests extends Common {
// Create PUT transaction
Path path1 = ArbitraryUtils.generateRandomDataPath(dataLength, true);
long fee = BlockChain.getInstance().getUnitFeeAtTimestamp(NTP.getTime());
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);
byte[] signature = arbitraryDataFile.getSignature();
@ -614,8 +572,9 @@ public class ArbitraryTransactionTests extends Common {
// Create PUT transaction
Path path1 = ArbitraryUtils.generateRandomDataPath(dataLength, true);
long fee = BlockChain.getInstance().getUnitFeeAtTimestamp(NTP.getTime());
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);
byte[] signature = arbitraryDataFile.getSignature();

View File

@ -5,10 +5,12 @@ import org.qortal.arbitrary.ArbitraryDataFile;
import org.qortal.arbitrary.ArbitraryDataTransactionBuilder;
import org.qortal.arbitrary.misc.Category;
import org.qortal.arbitrary.misc.Service;
import org.qortal.block.BlockChain;
import org.qortal.data.transaction.ArbitraryTransactionData;
import org.qortal.repository.DataException;
import org.qortal.repository.Repository;
import org.qortal.transaction.Transaction;
import org.qortal.utils.NTP;
import java.io.BufferedWriter;
import java.io.File;
@ -20,16 +22,15 @@ import java.nio.file.Paths;
import java.util.List;
import java.util.Random;
import static org.junit.Assert.assertEquals;
public class ArbitraryUtils {
public static ArbitraryDataFile createAndMintTxn(Repository repository, String publicKey58, Path path, String name, String identifier,
ArbitraryTransactionData.Method method, Service service, PrivateKeyAccount account,
int chunkSize) throws DataException {
long fee = BlockChain.getInstance().getUnitFeeAtTimestamp(NTP.getTime());
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,
@ -47,7 +48,9 @@ public class ArbitraryUtils {
}
ArbitraryTransactionData transactionData = txnBuilder.getArbitraryTransactionData();
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);
// 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.group.Group;
import org.qortal.repository.DataException;
import org.qortal.utils.NTP;
public abstract class TestTransaction {
protected static final Random random = new Random();
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 {

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;
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.Test;
import org.qortal.crosschain.Bitcoin;
import org.qortal.crosschain.ForeignBlockchainException;
import org.qortal.crosschain.BitcoinyHTLC;
import org.qortal.repository.DataException;
import org.qortal.test.common.Common;
import org.qortal.crosschain.Bitcoiny;
public class BitcoinTests extends Common {
public class BitcoinTests extends BitcoinyTests {
private Bitcoin bitcoin;
@Before
public void beforeTest() throws DataException {
Common.useDefaultSettings(); // TestNet3
bitcoin = Bitcoin.getInstance();
@Override
protected String getCoinName() {
return "Bitcoin";
}
@After
public void afterTest() {
@Override
protected String getCoinSymbol() {
return "BTC";
}
@Override
protected Bitcoiny getCoin() {
return Bitcoin.getInstance();
}
@Override
protected void resetCoinForTesting() {
Bitcoin.resetForTesting();
bitcoin = null;
}
@Override
protected String getDeterministicKey58() {
return "tprv8ZgxMBicQKsPdahhFSrCdvC1bsWyzHHZfTneTVqUXN6s1wEtZLwAkZXzFP6TYLg2aQMecZLXLre5bTVGajEB55L1HYJcawpdFG66STVAWPJ";
}
@Override
protected String getRecipient() {
return "2N8WCg52ULCtDSMjkgVTm5mtPdCsUptkHWE";
}
@Test
@Ignore("Often fails due to unreliable BTC testnet ElectrumX servers")
public void testGetMedianBlockTime() throws BlockStoreException, ForeignBlockchainException {
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);
}
public void testGetMedianBlockTime() {}
@Test
@Ignore("Often fails due to unreliable BTC testnet ElectrumX servers")
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(bitcoin, p2shAddress);
assertNotNull(secret);
assertTrue("secret incorrect", Arrays.equals(expectedSecret, secret));
}
public void testFindHtlcSecret() {}
@Test
@Ignore("Often fails due to unreliable BTC testnet ElectrumX servers")
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);
}
public void testBuildSpend() {}
@Test
@Ignore("Often fails due to unreliable BTC testnet ElectrumX servers")
public void testGetWalletBalance() throws ForeignBlockchainException {
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);
}
public void testGetWalletBalance() {}
@Test
@Ignore("Often fails due to unreliable BTC testnet ElectrumX servers")
public void testGetUnusedReceiveAddress() throws ForeignBlockchainException {
String xprv58 = "tprv8ZgxMBicQKsPdahhFSrCdvC1bsWyzHHZfTneTVqUXN6s1wEtZLwAkZXzFP6TYLg2aQMecZLXLre5bTVGajEB55L1HYJcawpdFG66STVAWPJ";
String address = bitcoin.getUnusedReceiveAddress(xprv58);
assertNotNull(address);
System.out.println(address);
}
public void testGetUnusedReceiveAddress() {}
}

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;
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.Test;
import org.qortal.crosschain.ForeignBlockchainException;
import org.qortal.crosschain.Bitcoiny;
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;
@Before
public void beforeTest() throws DataException {
Common.useDefaultSettings(); // TestNet3
digibyte = Digibyte.getInstance();
@Override
protected String getCoinName() {
return "Digibyte";
}
@After
public void afterTest() {
@Override
protected String getCoinSymbol() {
return "DGB";
}
@Override
protected Bitcoiny getCoin() {
return Digibyte.getInstance();
}
@Override
protected void resetCoinForTesting() {
Digibyte.resetForTesting();
digibyte = null;
}
@Test
public void testGetMedianBlockTime() throws BlockStoreException, ForeignBlockchainException {
long before = System.currentTimeMillis();
System.out.println(String.format("Digibyte median blocktime: %d", digibyte.getMedianBlockTime()));
long afterFirst = System.currentTimeMillis();
@Override
protected String getDeterministicKey58() {
return "xpub661MyMwAqRbcEnabTLX5uebYcsE3uG5y7ve9jn1VK8iY1MaU3YLoLJEe8sTu2YVav5Zka5qf2dmMssfxmXJTqZnazZL2kL7M2tNKwEoC34R";
}
System.out.println(String.format("Digibyte median blocktime: %d", digibyte.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);
@Override
protected String getRecipient() {
return "2N8WCg52ULCtDSMjkgVTm5mtPdCsUptkHWE";
}
@Test
@Ignore(value = "Doesn't work, to be fixed later")
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(digibyte, p2shAddress);
assertNotNull("secret not found", secret);
assertTrue("secret incorrect", Arrays.equals(expectedSecret, secret));
}
public void testFindHtlcSecret() {}
@Test
@Ignore(value = "No testnet nodes available, so we can't regularly test buildSpend yet")
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);
}
public void testBuildSpend() {}
}

View File

@ -1,115 +1,47 @@
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.Test;
import org.qortal.crosschain.BitcoinyHTLC;
import org.qortal.crosschain.ForeignBlockchainException;
import org.qortal.crosschain.Bitcoiny;
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.*;
public class DogecoinTests extends Common {
private Dogecoin dogecoin;
@Before
public void beforeTest() throws DataException {
Common.useDefaultSettings(); // TestNet3
dogecoin = Dogecoin.getInstance();
@Override
protected String getCoinName() {
return "Dogecoin";
}
@After
public void afterTest() {
@Override
protected String getCoinSymbol() {
return "DOGE";
}
@Override
protected Bitcoiny getCoin() {
return Dogecoin.getInstance();
}
@Override
protected void resetCoinForTesting() {
Dogecoin.resetForTesting();
dogecoin = null;
}
@Test
public void testGetMedianBlockTime() throws BlockStoreException, ForeignBlockchainException {
long before = System.currentTimeMillis();
System.out.println(String.format("Dogecoin median blocktime: %d", dogecoin.getMedianBlockTime()));
long afterFirst = System.currentTimeMillis();
@Override
protected String getDeterministicKey58() {
return "dgpv51eADS3spNJh9drNeW1Tc1P9z2LyaQRXPBortsq6yice1k47C2u2Prvgxycr2ihNBWzKZ2LthcBBGiYkWZ69KUTVkcLVbnjq7pD8mnApEru";
}
System.out.println(String.format("Dogecoin median blocktime: %d", dogecoin.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);
@Override
protected String getRecipient() {
return null;
}
@Test
@Ignore(value = "Doesn't work, to be fixed later")
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(dogecoin, p2shAddress);
assertNotNull("secret not found", secret);
assertTrue("secret incorrect", Arrays.equals(expectedSecret, secret));
}
public void testFindHtlcSecret() {}
@Test
@Ignore(value = "No testnet nodes available, so we can't regularly test buildSpend yet")
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);
}
public void testBuildSpend() {}
}

View File

@ -1,113 +1,43 @@
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.Test;
import org.qortal.crosschain.ForeignBlockchainException;
import org.qortal.crosschain.Bitcoiny;
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;
@Before
public void beforeTest() throws DataException {
Common.useDefaultSettings(); // TestNet3
litecoin = Litecoin.getInstance();
@Override
protected String getCoinName() {
return "Litecoin";
}
@After
public void afterTest() {
@Override
protected String getCoinSymbol() {
return "LTC";
}
@Override
protected Bitcoiny getCoin() {
return Litecoin.getInstance();
}
@Override
protected void resetCoinForTesting() {
Litecoin.resetForTesting();
litecoin = null;
}
@Test
public void testGetMedianBlockTime() throws ForeignBlockchainException {
long before = System.currentTimeMillis();
System.out.println(String.format("Litecoin median blocktime: %d", litecoin.getMedianBlockTime()));
long afterFirst = System.currentTimeMillis();
@Override
protected String getDeterministicKey58() {
return "tprv8ZgxMBicQKsPdahhFSrCdvC1bsWyzHHZfTneTVqUXN6s1wEtZLwAkZXzFP6TYLg2aQMecZLXLre5bTVGajEB55L1HYJcawpdFG66STVAWPJ";
}
System.out.println(String.format("Litecoin median blocktime: %d", litecoin.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);
@Override
protected String getRecipient() {
return "2N8WCg52ULCtDSMjkgVTm5mtPdCsUptkHWE";
}
@Test
@Ignore(value = "Doesn't work, to be fixed later")
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(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);
}
public void testFindHtlcSecret() {}
}

View File

@ -3,55 +3,51 @@ package org.qortal.test.crosschain;
import cash.z.wallet.sdk.rpc.CompactFormats.*;
import com.google.common.hash.HashCode;
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.Test;
import org.qortal.controller.tradebot.TradeBot;
import org.qortal.crosschain.*;
import org.qortal.crypto.Crypto;
import org.qortal.repository.DataException;
import org.qortal.test.common.Common;
import org.qortal.transform.TransformationException;
import java.util.Arrays;
import java.util.List;
import static org.junit.Assert.*;
import static org.qortal.crosschain.BitcoinyHTLC.Status.*;
public class PirateChainTests extends Common {
public class PirateChainTests extends BitcoinyTests {
private PirateChain pirateChain;
@Before
public void beforeTest() throws DataException {
Common.useDefaultSettings();
pirateChain = PirateChain.getInstance();
@Override
protected String getCoinName() {
return "PirateChain";
}
@After
public void afterTest() {
@Override
protected String getCoinSymbol() {
return "ARRR";
}
@Override
protected Bitcoiny getCoin() {
return PirateChain.getInstance();
}
@Override
protected void resetCoinForTesting() {
Litecoin.resetForTesting();
pirateChain = null;
}
@Test
public void testGetMedianBlockTime() throws BlockStoreException, ForeignBlockchainException {
long before = System.currentTimeMillis();
System.out.println(String.format("Pirate Chain median blocktime: %d", pirateChain.getMedianBlockTime()));
long afterFirst = System.currentTimeMillis();
@Override
protected String getDeterministicKey58() {
return null;
}
System.out.println(String.format("Pirate Chain median blocktime: %d", pirateChain.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));
@Override
protected String getRecipient() {
return null;
}
public void makeGetMedianBlockTimeAssertions(long firstPeriod, long secondPeriod) {
assertTrue("1st call should take less than 5 seconds", firstPeriod < 5000L);
assertTrue("2nd call should take less than 5 seconds", secondPeriod < 5000L);
}
@ -62,7 +58,7 @@ public class PirateChainTests extends Common {
int count = 20;
long before = System.currentTimeMillis();
List<CompactBlock> compactBlocks = pirateChain.getCompactBlocks(startHeight, count);
List<CompactBlock> compactBlocks = ((PirateChain) bitcoiny).getCompactBlocks(startHeight, count);
long after = System.currentTimeMillis();
System.out.println(String.format("Retrieval took: %d ms", after-before));
@ -82,7 +78,7 @@ public class PirateChainTests extends Common {
Bytes.reverse(txBytes);
String txHashBE = HashCode.fromBytes(txBytes).toString();
byte[] rawTransaction = pirateChain.getBlockchainProvider().getRawTransaction(txHashBE);
byte[] rawTransaction = bitcoiny.getBlockchainProvider().getRawTransaction(txHashBE);
assertNotNull(rawTransaction);
}
@ -121,7 +117,7 @@ public class PirateChainTests extends Common {
String p2shAddress = "ba6Q5HWrWtmfU2WZqQbrFdRYsafA45cUAt";
long p2shFee = 10000;
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);
}
@ -130,7 +126,7 @@ public class PirateChainTests extends Common {
String p2shAddress = "bYZrzSSgGp8aEGvihukoMGU8sXYrx19Wka";
long p2shFee = 10000;
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);
}
@ -139,14 +135,14 @@ public class PirateChainTests extends Common {
String p2shAddress = "bE49izfVxz8odhu8c2BcUaVFUnt7NLFRgv";
long p2shFee = 10000;
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);
}
@Test
public void testGetTxidForUnspentAddress() throws ForeignBlockchainException {
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
byte[] expectedTxidLE = HashCode.fromString("fea4b0c1abcf8f0f3ddc2fa2f9438501ee102aad62a9ff18a5ce7d08774755c0").asBytes();
@ -161,7 +157,7 @@ public class PirateChainTests extends Common {
String p2shAddress = "ba6Q5HWrWtmfU2WZqQbrFdRYsafA45cUAt";
long p2shFee = 10000;
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
byte[] expectedTxidLE = HashCode.fromString("fea4b0c1abcf8f0f3ddc2fa2f9438501ee102aad62a9ff18a5ce7d08774755c0").asBytes();
@ -174,7 +170,7 @@ public class PirateChainTests extends Common {
@Test
public void testGetTxidForSpentAddress() throws ForeignBlockchainException {
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
byte[] expectedTxidLE = HashCode.fromString("fb386fc8eea0fbf3ea37047726b92c39441652b32d8d62a274331687f7a1eca8").asBytes();
@ -187,7 +183,7 @@ public class PirateChainTests extends Common {
@Test
public void testGetTransactionsForAddress() throws ForeignBlockchainException {
String p2shAddress = "bE49izfVxz8odhu8c2BcUaVFUnt7NLFRgv"; //"t3KtVxeEb8srJofo6atMEpMpEP6TjEi8VqA";
List<BitcoinyTransaction> transactions = pirateChain.getBlockchainProvider()
List<BitcoinyTransaction> transactions = bitcoiny.getBlockchainProvider()
.getAddressBitcoinyTransactions(p2shAddress, false);
assertEquals(2, transactions.size());
@ -232,66 +228,17 @@ public class PirateChainTests extends Common {
@Test
@Ignore(value = "Doesn't work, to be fixed later")
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(pirateChain, p2shAddress);
assertNotNull("secret not found", secret);
assertTrue("secret incorrect", Arrays.equals(expectedSecret, secret));
}
public void testFindHtlcSecret() {}
@Test
@Ignore(value = "Needs adapting for Pirate Chain")
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);
}
public void testBuildSpend() {}
@Test
@Ignore(value = "Needs adapting for Pirate Chain")
public void testGetWalletBalance() throws ForeignBlockchainException {
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);
}
public void testGetWalletBalance() {}
@Test
@Ignore(value = "Needs adapting for Pirate Chain")
public void testGetUnusedReceiveAddress() throws ForeignBlockchainException {
String xprv58 = "tprv8ZgxMBicQKsPdahhFSrCdvC1bsWyzHHZfTneTVqUXN6s1wEtZLwAkZXzFP6TYLg2aQMecZLXLre5bTVGajEB55L1HYJcawpdFG66STVAWPJ";
String address = pirateChain.getUnusedReceiveAddress(xprv58);
assertNotNull(address);
System.out.println(address);
}
public void testGetUnusedReceiveAddress() {}
}

View File

@ -1,115 +1,47 @@
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.Test;
import org.qortal.crosschain.ForeignBlockchainException;
import org.qortal.crosschain.Bitcoiny;
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;
@Before
public void beforeTest() throws DataException {
Common.useDefaultSettings(); // TestNet3
ravencoin = Ravencoin.getInstance();
@Override
protected String getCoinName() {
return "Ravencoin";
}
@After
public void afterTest() {
@Override
protected String getCoinSymbol() {
return "RVN";
}
@Override
protected Bitcoiny getCoin() {
return Ravencoin.getInstance();
}
@Override
protected void resetCoinForTesting() {
Ravencoin.resetForTesting();
ravencoin = null;
}
@Test
public void testGetMedianBlockTime() throws BlockStoreException, ForeignBlockchainException {
long before = System.currentTimeMillis();
System.out.println(String.format("Ravencoin median blocktime: %d", ravencoin.getMedianBlockTime()));
long afterFirst = System.currentTimeMillis();
@Override
protected String getDeterministicKey58() {
return "xpub661MyMwAqRbcEt3Ge1wNmkagyb1J7yTQu4Kquvy77Ycg2iPoh7Urg8s9Jdwp7YmrqGkDKJpUVjsZXSSsQgmAVUC17ZVQQeoWMzm7vDTt1y7";
}
System.out.println(String.format("Ravencoin median blocktime: %d", ravencoin.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);
@Override
protected String getRecipient() {
return null;
}
@Test
@Ignore(value = "Doesn't work, to be fixed later")
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(ravencoin, p2shAddress);
assertNotNull("secret not found", secret);
assertTrue("secret incorrect", Arrays.equals(expectedSecret, secret));
}
public void testFindHtlcSecret() {}
@Test
@Ignore(value = "No testnet nodes available, so we can't regularly test buildSpend yet")
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);
}
public void testBuildSpend() {}
}

View File

@ -2,507 +2,89 @@ package org.qortal.test.crosschain.bitcoinv1;
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 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.BitcoinACCTv1;
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.test.crosschain.ACCTTests;
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.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[] hashOfSecretB = Crypto.hash160(secretB); // 31f0dd71decf59bbc8ef0661f4030479255cfa58
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
public void beforeTest() throws DataException {
Common.useDefaultSettings();
private static final String NAME = "Bitcoin";
@Override
protected byte[] getPublicKey() {
return bitcoinPublicKeyHash;
}
@Test
public void testCompile() {
PrivateKeyAccount tradeAccount = createTradeAccount(null);
byte[] creationBytes = BitcoinACCTv1.buildQortalAT(tradeAccount.getAddress(), bitcoinPublicKeyHash, hashOfSecretB, redeemAmount, bitcoinAmount, tradeTimeout);
assertNotNull(creationBytes);
System.out.println("AT creation bytes: " + HashCode.fromBytes(creationBytes).toString());
@Override
protected byte[] buildQortalAT(String address, byte[] publicKey, long redeemAmount, long foreignAmount, int tradeTimeout) {
return BitcoinACCTv1.buildQortalAT(address,publicKey, hashOfSecretB, redeemAmount, foreignAmount,tradeTimeout);
}
@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);
@Override
protected ACCT getInstance() {
return BitcoinACCTv1.getInstance();
}
@Override
protected int calcRefundTimeout(long partnersOfferMessageTransactionTimestamp, int lockTimeA) {
return BitcoinACCTv1.calcLockTimeB(partnersOfferMessageTransactionTimestamp, lockTimeA);
}
@Override
protected byte[] buildTradeMessage(String address, byte[] publicKey, byte[] hashOfSecretA, int lockTimeA, int refundTimeout) {
return BitcoinACCTv1.buildTradeMessage(address, publicKey, hashOfSecretA, lockTimeA, refundTimeout);
}
@Override
protected byte[] buildRedeemMessage(byte[] secretA, String address) {
return BitcoinACCTv1.buildRedeemMessage(secretA,secretB,address);
}
@Override
protected byte[] getCodeBytesHash() {
return BitcoinACCTv1.CODE_BYTES_HASH;
}
@Override
protected String getSymbol() {
return SYMBOL;
}
@Override
protected String getName() {
return NAME;
}
@SuppressWarnings("unused")
@Override
@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 = 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 {
public void testIncorrectSecretCorrectSender() throws DataException {
try (final Repository repository = RepositoryManager.getRepository()) {
PrivateKeyAccount deployer = Common.getTestAccount(repository, "chloe");
PrivateKeyAccount tradeAccount = createTradeAccount(repository);
@ -582,197 +164,8 @@ public class BitcoinACCTv1Tests extends Common {
}
}
@SuppressWarnings("unused")
@Test
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));
@Override
protected void describeRefundAt(CrossChainTradeData tradeData, Function<Long, String> epochMilliFormatter) {
if (tradeData.mode != AcctMode.OFFERING && tradeData.mode != AcctMode.CANCELLED) {
System.out.println(String.format("\trefund height: block %d,\n"
+ "\tHASH160 of secret-A: %s,\n"
@ -786,10 +179,4 @@ public class BitcoinACCTv1Tests extends Common {
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;
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.AcctMode;
import org.qortal.crosschain.ACCT;
import org.qortal.crosschain.BitcoinACCTv3;
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 org.qortal.test.crosschain.ACCTTests;
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;
public class BitcoinACCTv3Tests extends ACCTTests {
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 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 String SYMBOL = "BTC";
private static final String NAME = "Bitcoin";
private static final Random RANDOM = new Random();
@Before
public void beforeTest() throws DataException {
Common.useDefaultSettings();
@Override
protected byte[] getPublicKey() {
return bitcoinPublicKeyHash;
}
@Test
public void testCompile() {
PrivateKeyAccount tradeAccount = createTradeAccount(null);
byte[] creationBytes = BitcoinACCTv3.buildQortalAT(tradeAccount.getAddress(), bitcoinPublicKeyHash, redeemAmount, bitcoinAmount, tradeTimeout);
assertNotNull(creationBytes);
System.out.println("AT creation bytes: " + HashCode.fromBytes(creationBytes).toString());
@Override
protected byte[] buildQortalAT(String address, byte[] publicKey, long redeemAmount, long foreignAmount, int tradeTimeout) {
return BitcoinACCTv3.buildQortalAT(address, publicKey, redeemAmount, foreignAmount, tradeTimeout);
}
@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);
}
@Override
protected ACCT getInstance() {
return BitcoinACCTv3.getInstance();
}
@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 = 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);
}
@Override
protected int calcRefundTimeout(long partnersOfferMessageTransactionTimestamp, int lockTimeA) {
return BitcoinACCTv3.calcRefundTimeout(partnersOfferMessageTransactionTimestamp, lockTimeA);
}
@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 = BitcoinACCTv3.getInstance().populateTradeData(repository, atData);
assertEquals(AcctMode.CANCELLED, tradeData.mode);
}
@Override
protected byte[] buildTradeMessage(String address, byte[] publicKey, byte[] hashOfSecretA, int lockTimeA, int refundTimeout) {
return BitcoinACCTv3.buildTradeMessage(address, publicKey, hashOfSecretA, lockTimeA, refundTimeout);
}
@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 = 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);
}
@Override
protected byte[] buildRedeemMessage(byte[] secretA, String address) {
return BitcoinACCTv3.buildRedeemMessage(secretA, address);
}
// 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 = 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);
}
@Override
protected byte[] getCodeBytesHash() {
return BitcoinACCTv3.CODE_BYTES_HASH;
}
@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 = 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);
}
@Override
protected String getSymbol() {
return SYMBOL;
}
@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 = 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()));
@Override
protected String getName() {
return NAME;
}
}
@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;
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.AcctMode;
import org.qortal.crosschain.ACCT;
import org.qortal.crosschain.DigibyteACCTv3;
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 org.qortal.test.crosschain.ACCTTests;
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;
public class DigibyteACCTv3Tests extends ACCTTests {
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 int tradeTimeout = 20; // blocks
public static final long redeemAmount = 80_40200000L;
public static final long fundingAmount = 123_45600000L;
public static final long digibyteAmount = 864200L; // 0.00864200 DGB
private static final String SYMBOL = "DGB";
private static final String NAME = "DigiByte";
private static final Random RANDOM = new Random();
@Before
public void beforeTest() throws DataException {
Common.useDefaultSettings();
@Override
protected byte[] getPublicKey() {
return digibytePublicKeyHash;
}
@Test
public void testCompile() {
PrivateKeyAccount tradeAccount = createTradeAccount(null);
byte[] creationBytes = DigibyteACCTv3.buildQortalAT(tradeAccount.getAddress(), digibytePublicKeyHash, redeemAmount, digibyteAmount, tradeTimeout);
assertNotNull(creationBytes);
System.out.println("AT creation bytes: " + HashCode.fromBytes(creationBytes).toString());
@Override
protected byte[] buildQortalAT(String address, byte[] publicKey, long redeemAmount, long foreignAmount, int tradeTimeout) {
return DigibyteACCTv3.buildQortalAT(address, publicKey, redeemAmount, foreignAmount, tradeTimeout);
}
@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);
}
@Override
protected ACCT getInstance() {
return DigibyteACCTv3.getInstance();
}
@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 = 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);
}
@Override
protected int calcRefundTimeout(long partnersOfferMessageTransactionTimestamp, int lockTimeA) {
return DigibyteACCTv3.calcRefundTimeout(partnersOfferMessageTransactionTimestamp, lockTimeA);
}
@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 = DigibyteACCTv3.getInstance().populateTradeData(repository, atData);
assertEquals(AcctMode.CANCELLED, tradeData.mode);
}
@Override
protected byte[] buildTradeMessage(String address, byte[] publicKey, byte[] hashOfSecretA, int lockTimeA, int refundTimeout) {
return DigibyteACCTv3.buildTradeMessage(address, publicKey, hashOfSecretA, lockTimeA, refundTimeout);
}
@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 = 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);
}
@Override
protected byte[] buildRedeemMessage(byte[] secretA, String address) {
return DigibyteACCTv3.buildRedeemMessage(secretA, address);
}
// 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 = 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);
}
@Override
protected byte[] getCodeBytesHash() {
return DigibyteACCTv3.CODE_BYTES_HASH;
}
@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 = 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);
}
@Override
protected String getSymbol() {
return SYMBOL;
}
@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 = 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()));
@Override
protected String getName() {
return NAME;
}
}
@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;
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.AcctMode;
import org.qortal.crosschain.ACCT;
import org.qortal.crosschain.DogecoinACCTv3;
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 org.qortal.test.crosschain.ACCTTests;
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;
public class DogecoinACCTv3Tests extends ACCTTests {
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 int tradeTimeout = 20; // blocks
public static final long redeemAmount = 80_40200000L;
public static final long fundingAmount = 123_45600000L;
public static final long dogecoinAmount = 864200L; // 0.00864200 DOGE
private static final String SYMBOL = "DOGE";
private static final String NAME = "Dogecoin";
private static final Random RANDOM = new Random();
@Before
public void beforeTest() throws DataException {
Common.useDefaultSettings();
@Override
protected byte[] getPublicKey() {
return dogecoinPublicKeyHash;
}
@Test
public void testCompile() {
PrivateKeyAccount tradeAccount = createTradeAccount(null);
byte[] creationBytes = DogecoinACCTv3.buildQortalAT(tradeAccount.getAddress(), dogecoinPublicKeyHash, redeemAmount, dogecoinAmount, tradeTimeout);
assertNotNull(creationBytes);
System.out.println("AT creation bytes: " + HashCode.fromBytes(creationBytes).toString());
@Override
protected byte[] buildQortalAT(String address, byte[] publicKey, long redeemAmount, long foreignAmount, int tradeTimeout) {
return DogecoinACCTv3.buildQortalAT(address, publicKey, redeemAmount, foreignAmount, tradeTimeout);
}
@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);
}
@Override
protected ACCT getInstance() {
return DogecoinACCTv3.getInstance();
}
@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 = 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);
}
@Override
protected int calcRefundTimeout(long partnersOfferMessageTransactionTimestamp, int lockTimeA) {
return DogecoinACCTv3.calcRefundTimeout(partnersOfferMessageTransactionTimestamp, lockTimeA);
}
@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 = DogecoinACCTv3.getInstance().populateTradeData(repository, atData);
assertEquals(AcctMode.CANCELLED, tradeData.mode);
}
@Override
protected byte[] buildTradeMessage(String address, byte[] publicKey, byte[] hashOfSecretA, int lockTimeA, int refundTimeout) {
return DogecoinACCTv3.buildTradeMessage(address, publicKey, hashOfSecretA, lockTimeA, refundTimeout);
}
@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 = 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);
}
@Override
protected byte[] buildRedeemMessage(byte[] secretA, String address) {
return DogecoinACCTv3.buildRedeemMessage(secretA, address);
}
// 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 = 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);
}
@Override
protected byte[] getCodeBytesHash() {
return DogecoinACCTv3.CODE_BYTES_HASH;
}
@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 = 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);
}
@Override
protected String getSymbol() {
return SYMBOL;
}
@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 = 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()));
@Override
protected String getName() {
return NAME;
}
}
@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;
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 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.LitecoinACCTv1;
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 org.qortal.test.crosschain.ACCTTests;
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 int tradeTimeout = 20; // blocks
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 String SYMBOL = "LTC";
private static final Random RANDOM = new Random();
private static final String NAME = "Litecoin";
@Before
public void beforeTest() throws DataException {
Common.useDefaultSettings();
@Override
protected byte[] getPublicKey() {
return litecoinPublicKeyHash;
}
@Test
public void testCompile() {
PrivateKeyAccount tradeAccount = createTradeAccount(null);
byte[] creationBytes = LitecoinACCTv1.buildQortalAT(tradeAccount.getAddress(), litecoinPublicKeyHash, redeemAmount, litecoinAmount, tradeTimeout);
assertNotNull(creationBytes);
System.out.println("AT creation bytes: " + HashCode.fromBytes(creationBytes).toString());
@Override
protected byte[] buildQortalAT(String address, byte[] publicKey, long redeemAmount, long foreignAmount, int tradeTimeout) {
return LitecoinACCTv1.buildQortalAT(address, publicKey, redeemAmount, foreignAmount, tradeTimeout);
}
@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);
}
@Override
protected ACCT getInstance() {
return LitecoinACCTv1.getInstance();
}
@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 = 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);
}
@Override
protected int calcRefundTimeout(long partnersOfferMessageTransactionTimestamp, int lockTimeA) {
return LitecoinACCTv1.calcRefundTimeout(partnersOfferMessageTransactionTimestamp, lockTimeA);
}
@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 = LitecoinACCTv1.getInstance().populateTradeData(repository, atData);
assertEquals(AcctMode.CANCELLED, tradeData.mode);
}
@Override
protected byte[] buildTradeMessage(String address, byte[] publicKey, byte[] hashOfSecretA, int lockTimeA, int refundTimeout) {
return LitecoinACCTv1.buildTradeMessage(address, publicKey, hashOfSecretA, lockTimeA, refundTimeout);
}
@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 = 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);
}
@Override
protected byte[] buildRedeemMessage(byte[] secretA, String address) {
return LitecoinACCTv1.buildRedeemMessage(secretA, address);
}
// 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 = 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);
}
@Override
protected byte[] getCodeBytesHash() {
return LitecoinACCTv1.CODE_BYTES_HASH;
}
@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 = 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);
}
@Override
protected String getSymbol() {
return SYMBOL;
}
@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 = 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()));
@Override
protected String getName() {
return NAME;
}
}
@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;
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.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 org.qortal.crosschain.ACCT;
import org.qortal.crosschain.LitecoinACCTv1;
import org.qortal.test.crosschain.ACCTTests;
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;
public class LitecoinACCTv3Tests extends ACCTTests {
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 int tradeTimeout = 20; // blocks
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 String SYMBOL = "LTC";
private static final String NAME = "Litecoin";
private static final Random RANDOM = new Random();
@Before
public void beforeTest() throws DataException {
Common.useDefaultSettings();
@Override
protected byte[] getPublicKey() {
return litecoinPublicKeyHash;
}
@Test
public void testCompile() {
PrivateKeyAccount tradeAccount = createTradeAccount(null);
byte[] creationBytes = LitecoinACCTv3.buildQortalAT(tradeAccount.getAddress(), litecoinPublicKeyHash, redeemAmount, litecoinAmount, tradeTimeout);
assertNotNull(creationBytes);
System.out.println("AT creation bytes: " + HashCode.fromBytes(creationBytes).toString());
@Override
protected byte[] buildQortalAT(String address, byte[] publicKey, long redeemAmount, long foreignAmount, int tradeTimeout) {
return LitecoinACCTv1.buildQortalAT(address, publicKey, redeemAmount, foreignAmount, tradeTimeout);
}
@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);
}
@Override
protected ACCT getInstance() {
return LitecoinACCTv1.getInstance();
}
@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 = 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);
}
@Override
protected int calcRefundTimeout(long partnersOfferMessageTransactionTimestamp, int lockTimeA) {
return LitecoinACCTv1.calcRefundTimeout(partnersOfferMessageTransactionTimestamp, lockTimeA);
}
@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 = LitecoinACCTv3.getInstance().populateTradeData(repository, atData);
assertEquals(AcctMode.CANCELLED, tradeData.mode);
}
@Override
protected byte[] buildTradeMessage(String address, byte[] publicKey, byte[] hashOfSecretA, int lockTimeA, int refundTimeout) {
return LitecoinACCTv1.buildTradeMessage(address, publicKey, hashOfSecretA, lockTimeA, refundTimeout);
}
@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 = 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);
}
@Override
protected byte[] buildRedeemMessage(byte[] secretA, String address) {
return LitecoinACCTv1.buildRedeemMessage(secretA, address);
}
// 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 = 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);
}
@Override
protected byte[] getCodeBytesHash() {
return LitecoinACCTv1.CODE_BYTES_HASH;
}
@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 = 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);
}
@Override
protected String getSymbol() {
return SYMBOL;
}
@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 = 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()));
@Override
protected String getName() {
return NAME;
}
}
@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;
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.AcctMode;
import org.qortal.crosschain.ACCT;
import org.qortal.crosschain.PirateChainACCTv3;
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 org.qortal.test.crosschain.ACCTTests;
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;
public class PirateChainACCTv3Tests extends ACCTTests {
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 int tradeTimeout = 20; // blocks
public static final long redeemAmount = 80_40200000L;
public static final long fundingAmount = 123_45600000L;
public static final long arrrAmount = 864200L; // 0.00864200 ARRR
private static final String SYMBOL = "ARRR";
private static final String NAME = "Pirate Chain";
private static final Random RANDOM = new Random();
@Before
public void beforeTest() throws DataException {
Common.useDefaultSettings();
@Override
protected byte[] getPublicKey() {
return pirateChainPublicKey;
}
@Test
public void testCompile() {
PrivateKeyAccount tradeAccount = createTradeAccount(null);
byte[] creationBytes = PirateChainACCTv3.buildQortalAT(tradeAccount.getAddress(), pirateChainPublicKey, redeemAmount, arrrAmount, tradeTimeout);
assertNotNull(creationBytes);
System.out.println("AT creation bytes: " + HashCode.fromBytes(creationBytes).toString());
@Override
protected byte[] buildQortalAT(String address, byte[] publicKey, long redeemAmount, long foreignAmount, int tradeTimeout) {
return PirateChainACCTv3.buildQortalAT(address, publicKey, redeemAmount, foreignAmount, tradeTimeout);
}
@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);
}
@Override
protected ACCT getInstance() {
return PirateChainACCTv3.getInstance();
}
@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 = 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);
}
@Override
protected int calcRefundTimeout(long partnersOfferMessageTransactionTimestamp, int lockTimeA) {
return PirateChainACCTv3.calcRefundTimeout(partnersOfferMessageTransactionTimestamp, lockTimeA);
}
@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 = PirateChainACCTv3.getInstance().populateTradeData(repository, atData);
assertEquals(AcctMode.CANCELLED, tradeData.mode);
}
@Override
protected byte[] buildTradeMessage(String address, byte[] publicKey, byte[] hashOfSecretA, int lockTimeA, int refundTimeout) {
return PirateChainACCTv3.buildTradeMessage(address, publicKey, hashOfSecretA, lockTimeA, refundTimeout);
}
@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 = 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);
}
@Override
protected byte[] buildRedeemMessage(byte[] secretA, String address) {
return PirateChainACCTv3.buildRedeemMessage(secretA, address);
}
// 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 = 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);
}
@Override
protected byte[] getCodeBytesHash() {
return PirateChainACCTv3.CODE_BYTES_HASH;
}
@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 = 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);
}
@Override
protected String getSymbol() {
return SYMBOL;
}
@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 = 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()));
@Override
protected String getName() {
return NAME;
}
}
@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;
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.AcctMode;
import org.qortal.crosschain.ACCT;
import org.qortal.crosschain.RavencoinACCTv3;
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 org.qortal.test.crosschain.ACCTTests;
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;
public class RavencoinACCTv3Tests extends ACCTTests {
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 int tradeTimeout = 20; // blocks
public static final long redeemAmount = 80_40200000L;
public static final long fundingAmount = 123_45600000L;
public static final long ravencoinAmount = 864200L; // 0.00864200 RVN
private static final String SYMBOL = "RVN";
private static final String NAME = "Ravencoin";
private static final Random RANDOM = new Random();
@Before
public void beforeTest() throws DataException {
Common.useDefaultSettings();
@Override
protected byte[] getPublicKey() {
return ravencoinPublicKeyHash;
}
@Test
public void testCompile() {
PrivateKeyAccount tradeAccount = createTradeAccount(null);
byte[] creationBytes = RavencoinACCTv3.buildQortalAT(tradeAccount.getAddress(), ravencoinPublicKeyHash, redeemAmount, ravencoinAmount, tradeTimeout);
assertNotNull(creationBytes);
System.out.println("AT creation bytes: " + HashCode.fromBytes(creationBytes).toString());
@Override
protected byte[] buildQortalAT(String address, byte[] publicKey, long redeemAmount, long foreignAmount, int tradeTimeout) {
return RavencoinACCTv3.buildQortalAT(address, publicKey, redeemAmount, foreignAmount, tradeTimeout);
}
@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);
}
@Override
protected ACCT getInstance() {
return RavencoinACCTv3.getInstance();
}
@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 = 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);
}
@Override
protected int calcRefundTimeout(long partnersOfferMessageTransactionTimestamp, int lockTimeA) {
return RavencoinACCTv3.calcRefundTimeout(partnersOfferMessageTransactionTimestamp, lockTimeA);
}
@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 = RavencoinACCTv3.getInstance().populateTradeData(repository, atData);
assertEquals(AcctMode.CANCELLED, tradeData.mode);
}
@Override
protected byte[] buildTradeMessage(String address, byte[] publicKey, byte[] hashOfSecretA, int lockTimeA, int refundTimeout) {
return RavencoinACCTv3.buildTradeMessage(address, publicKey, hashOfSecretA, lockTimeA, refundTimeout);
}
@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 = 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);
}
@Override
protected byte[] buildRedeemMessage(byte[] secretA, String address) {
return RavencoinACCTv3.buildRedeemMessage(secretA, address);
}
// 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 = 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);
}
@Override
protected byte[] getCodeBytesHash() {
return RavencoinACCTv3.CODE_BYTES_HASH;
}
@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 = 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);
}
@Override
protected String getSymbol() {
return SYMBOL;
}
@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 = 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()));
@Override
protected String getName() {
return NAME;
}
}
@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");
name = "test name" + " " + random.nextInt(1_000_000);
price = random.nextInt(1000) * Amounts.MULTIPLIER;
price = (random.nextInt(1000) + 1) * Amounts.MULTIPLIER;
}
@After

View File

@ -20,6 +20,7 @@ import org.qortal.repository.Repository;
import org.qortal.repository.RepositoryManager;
import org.qortal.test.common.*;
import org.qortal.test.common.transaction.TestTransaction;
import org.qortal.transaction.PaymentTransaction;
import org.qortal.transaction.RegisterNameTransaction;
import org.qortal.transaction.Transaction;
import org.qortal.transaction.Transaction.ValidationResult;
@ -329,15 +330,19 @@ public class MiscTests extends Common {
public void testRegisterNameFeeIncrease() throws Exception {
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();
futureFeeIncrease.timestamp = 9999999999999L; // 20 Nov 2286
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));
// 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(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.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(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,
"maxBlockSize": 2097152,
"maxBytesPerUnitFee": 1024,
"unitFee": "0.1",
"unitFees": [
{ "timestamp": 0, "fee": "0.1" }
],
"nameRegistrationUnitFees": [
{ "timestamp": 0, "fee": "0.1" },
{ "timestamp": 1645372800000, "fee": "5" }
],
"requireGroupForApproval": false,
@ -15,6 +18,7 @@
"onlineAccountSignaturesMinLifetime": 3600000,
"onlineAccountSignaturesMaxLifetime": 86400000,
"selfSponsorshipAlgoV1SnapshotTimestamp": 9999999999999,
"mempowTransactionUpdatesTimestamp": 0,
"rewardsByHeight": [
{ "height": 1, "reward": 100 },
{ "height": 11, "reward": 10 },

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -50,7 +50,7 @@ tx_json=$( cat <<TX_END
{
"timestamp": ${timestamp},
"reference": "${lastref}",
"fee": 0.001,
"fee": 0.01,
"txGroupId": 0,
"adminPublicKey": "${pubkey}",
"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 POSIX;
use Getopt::Std;
use File::Slurp;
sub usage() {
die("usage: $0 [-p api-port] dev-private-key [short-commit-hash]\n");
@ -34,6 +35,8 @@ while (<POM>) {
}
close(POM);
my $apikey = read_file('apikey.txt');
# Do we need to determine commit hash?
unless ($commit_hash) {
# 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;
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 $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;
@ -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}$/;
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;
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;
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}`;
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;
# Check we can actually fetch update

120
tools/qdn
View File

@ -8,7 +8,7 @@ if [ -z "$*" ]; then
echo "Usage:"
echo
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
echo "Fetch data:"
@ -22,6 +22,21 @@ if [ -z "$*" ]; then
exit
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 )
if [ -f "apikey.txt" ]; then
@ -37,32 +52,46 @@ service=$2
name=$3
if [ -z "${method}" ]; then
echo "Error: missing method"; exit
echo "Error: missing method"
exit 1
fi
if [ -z "${service}" ]; then
echo "Error: missing service"; exit
echo "Error: missing service"
exit 1
fi
if [ -z "${name}" ]; then
echo "Error: missing name"; exit
echo "Error: missing name"
exit 1
fi
if [[ "${method}" == "POST" ]]; then
type=$4
data=$5
identifier=$6
title=$7
description=$8
tags=$9
category=${10}
fee=${11}
preview=${12}
if [ -z "${data}" ]; 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
echo "Error: missing data string"; exit
echo "Error: missing data string - please input the data string you wish to publish"
exit 1
else
echo "Error: unrecognized type"; exit
echo "Error: unrecognized type"
exit 1
fi
fi
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
if [ -z "${identifier}" ]; then
@ -76,29 +105,87 @@ if [[ "${method}" == "POST" ]]; then
type_component="/string"
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..."
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
echo "${tx_data}"; exit
echo "Error creating transaction: ${tx_data}"
exit 1
elif [ -z "${tx_data}" ]; then
echo "Error: no transaction data returned"; exit
echo "Error: no transaction data returned"
exit 1
fi
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}")
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
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}\"}")
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
echo "Broadcasting..."
success=$(curl --silent --insecure -X POST "http://${host}:${port}/transactions/process" -H "Content-Type: text/plain" -d "${signed_tx_data}")
if [[ "${success}" == "true" ]]; then
echo "Transaction broadcast successfully"
else
@ -131,9 +218,10 @@ elif [[ "${method}" == "GET" ]]; then
echo "Empty response from ${host}:${port}"
fi
if [[ "${response}" == *"error"* || "${response}" == *"ERROR"* ]]; then
echo "${response}"; exit
echo "${response}"
exit 1
fi
echo "${response}"
fi

View File

@ -40,7 +40,7 @@ our %b58 = map { $b58[$_] => $_ } 0 .. 57;
our %reverseb58 = reverse %b58;
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 = (
payment => {
@ -92,7 +92,7 @@ our %TRANSACTION_TYPES = (
},
remove_group_admin => {
url => 'groups/removeadmin',
required => [qw(groupId txGroupId member)],
required => [qw(groupId txGroupId admin)],
key_name => 'ownerPublicKey',
},
group_approval => {