forked from Qortal/qortal
Compare commits
91 Commits
Author | SHA1 | Date | |
---|---|---|---|
|
6c1b21da22 | ||
|
f6216b9745 | ||
|
91e82d1e3c | ||
|
50e2bda020 | ||
|
ab1de1aafa | ||
|
d4ac87f91d | ||
|
52f4008725 | ||
|
d8dd71ff50 | ||
|
02966bf39a | ||
|
a83d8bf1d5 | ||
|
1e4432b1f3 | ||
|
d50c979d9f | ||
|
4e60ec5192 | ||
|
31c4e3b1be | ||
|
b97fbd3171 | ||
|
43fb5d9332 | ||
|
ea3f1a8eff | ||
|
7bb060781e | ||
|
a1ab0b7c31 | ||
|
fae2afd010 | ||
|
76c0a5a4fa | ||
|
cdb65657b6 | ||
|
9007dfe779 | ||
|
99d09a9877 | ||
|
afcf51399e | ||
|
47679b7f6c | ||
|
8f2985862d | ||
|
23a524b464 | ||
|
ce8992867d | ||
|
c89de7adfb | ||
|
cac68ccc14 | ||
|
d507383487 | ||
|
ce5cf87094 | ||
|
ec2c9d2a44 | ||
|
36d0abe635 | ||
|
615381ca5a | ||
|
6b83499216 | ||
|
faa2e9502b | ||
|
cd07240ce7 | ||
|
91518464c2 | ||
|
25bf315e23 | ||
|
a8743b1bd3 | ||
|
f90bd6ee45 | ||
|
a351756883 | ||
|
ea9b0d4588 | ||
|
e9c85c946e | ||
|
876bfb525b | ||
|
6be67d0d92 | ||
|
16581766c6 | ||
|
7fd7104f46 | ||
|
d2cae7c8b5 | ||
|
83955acd22 | ||
|
d85b746021 | ||
|
e2dc91c1ea | ||
|
098e2623d6 | ||
|
2df045396d | ||
|
6c182a3567 | ||
|
340d6dfc8d | ||
|
eb27b0d3e2 | ||
|
7377893050 | ||
|
21d7a4eed1 | ||
|
fb2c2b1d09 | ||
|
6f2dd6c8d0 | ||
|
4cc0e7845f | ||
|
ca8eabc425 | ||
|
94c83d6a93 | ||
|
dea2f34c52 | ||
|
b294f5e333 | ||
|
f9b726a75d | ||
|
579645d6b7 | ||
|
e729571a21 | ||
|
f179139967 | ||
|
ee5119e4dd | ||
|
11bf5ac6fc | ||
|
c3eb385066 | ||
|
886c9156a5 | ||
|
23062c59cd | ||
|
da254058c5 | ||
|
a6fa4fc613 | ||
|
593b61ea4b | ||
|
04d691991a | ||
|
faa6e82bef | ||
|
65ccb80aa4 | ||
|
cc13d1d0f1 | ||
|
ead84d70d1 | ||
|
275146fb55 | ||
|
d81729d9f7 | ||
|
e74a249388 | ||
|
d8c5e557d8 | ||
|
984e8b5227 | ||
|
469bf2a63e |
3
WindowsInstaller/Install Files/AppData/settings.json
Executable file
3
WindowsInstaller/Install Files/AppData/settings.json
Executable file
@@ -0,0 +1,3 @@
|
||||
{
|
||||
"apiDocumentationEnabled": true
|
||||
}
|
70
WindowsInstaller/Install Files/log4j2.properties
Executable file
70
WindowsInstaller/Install Files/log4j2.properties
Executable file
@@ -0,0 +1,70 @@
|
||||
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}
|
||||
|
||||
rootLogger.appenderRef.console.ref = stdout
|
||||
rootLogger.appenderRef.rolling.ref = FILE
|
||||
|
||||
# Suppress extraneous bitcoinj library output
|
||||
logger.bitcoinj.name = org.bitcoinj
|
||||
logger.bitcoinj.level = error
|
||||
|
||||
# Override HSQLDB logging level to "warn" as too much is logged at "info"
|
||||
logger.hsqldb.name = hsqldb.db
|
||||
logger.hsqldb.level = warn
|
||||
|
||||
# Support optional, per-session HSQLDB debugging
|
||||
logger.hsqldbRepository.name = org.qortal.repository.hsqldb
|
||||
logger.hsqldbRepository.level = debug
|
||||
|
||||
# Suppress extraneous Jersey warning
|
||||
logger.jerseyInject.name = org.glassfish.jersey.internal.inject.Providers
|
||||
logger.jerseyInject.level = off
|
||||
|
||||
# Suppress extraneous Jersey EOF 'errors' (actually remote peers disconnecting early)
|
||||
logger.jerseyEOF.name = org.glassfish.jersey.server.internal
|
||||
logger.jerseyEOF.level = off
|
||||
|
||||
# Suppress extraneous Jetty entries
|
||||
# 2019-02-14 11:46:27 INFO ContextHandler:851 - Started o.e.j.s.ServletContextHandler@6949e948{/,null,AVAILABLE}
|
||||
# 2019-02-14 11:46:27 INFO AbstractConnector:289 - Started ServerConnector@50ad322b{HTTP/1.1,[http/1.1]}{0.0.0.0:9085}
|
||||
# 2019-02-14 11:46:27 INFO Server:374 - jetty-9.4.11.v20180605; built: 2018-06-05T18:24:03.829Z; git: d5fc0523cfa96bfebfbda19606cad384d772f04c; jvm 1.8.0_181-b13
|
||||
# 2019-02-14 11:46:27 INFO Server:411 - Started @2539ms
|
||||
logger.jetty.name = org.eclipse.jetty
|
||||
logger.jetty.level = warn
|
||||
# Even more extraneous Jetty output
|
||||
# 2019-01-26 02:18:10 WARN ResourceService:718 - java.util.concurrent.TimeoutException: Idle timeout expired: 30000/30000 ms
|
||||
logger.jettyRS.name = org.eclipse.jetty.server.ResourceService
|
||||
logger.jettyRS.level = error
|
||||
|
||||
# Suppress extraneous slf4j entries
|
||||
# 2019-02-14 11:46:27 INFO log:193 - Logging initialized @1636ms to org.eclipse.jetty.util.log.Slf4jLog
|
||||
logger.slf4j.name = org.slf4j
|
||||
logger.slf4j.level = warn
|
||||
|
||||
# Suppress extraneous Reflections entry
|
||||
# 2019-02-27 10:45:25 WARN Reflections:179 - given scan urls are empty. set urls in the configuration
|
||||
logger.orgReflections.name = org.reflections.Reflections
|
||||
logger.orgReflections.level = off
|
||||
logger.sunReflections.name = sun.reflect.Reflection
|
||||
logger.sunReflections.level = off
|
||||
|
||||
appender.console.type = Console
|
||||
appender.console.name = stdout
|
||||
appender.console.layout.type = PatternLayout
|
||||
appender.console.layout.pattern = %d{yyyy-MM-dd HH:mm:ss} %-5p %c{1}:%L - %m%n
|
||||
appender.console.filter.threshold.type = ThresholdFilter
|
||||
appender.console.filter.threshold.level = error
|
||||
|
||||
appender.rolling.type = RollingFile
|
||||
appender.rolling.name = FILE
|
||||
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 = ${dirname:-}${filename}.%i
|
||||
appender.rolling.policy.type = SizeBasedTriggeringPolicy
|
||||
appender.rolling.policy.size = 4MB
|
||||
# Set the immediate flush to true (default)
|
||||
# appender.rolling.immediateFlush = true
|
||||
# Set the append to true (default), should not overwrite
|
||||
# appender.rolling.append=true
|
33
WindowsInstaller/Install Files/ntpcfg.bat
Executable file
33
WindowsInstaller/Install Files/ntpcfg.bat
Executable file
@@ -0,0 +1,33 @@
|
||||
@echo off
|
||||
|
||||
:: BatchGotAdmin
|
||||
:-------------------------------------
|
||||
REM --> Check for permissions
|
||||
>nul 2>&1 "%SYSTEMROOT%\system32\cacls.exe" "%SYSTEMROOT%\system32\config\system"
|
||||
|
||||
REM --> If error flag set, we do not have admin.
|
||||
if '%errorlevel%' NEQ '0' (
|
||||
echo Requesting administrative privileges...
|
||||
goto UACPrompt
|
||||
) else ( goto gotAdmin )
|
||||
|
||||
:UACPrompt
|
||||
echo Set UAC = CreateObject^("Shell.Application"^) > "%temp%\getadmin.vbs"
|
||||
echo UAC.ShellExecute "%~s0", "", "", "runas", 1 >> "%temp%\getadmin.vbs"
|
||||
|
||||
"%temp%\getadmin.vbs"
|
||||
exit /B
|
||||
|
||||
:gotAdmin
|
||||
if exist "%temp%\getadmin.vbs" ( del "%temp%\getadmin.vbs" )
|
||||
pushd "%CD%"
|
||||
CD /D "%~dp0"
|
||||
:--------------------------------------
|
||||
|
||||
net stop "Windows Time"
|
||||
|
||||
w32tm /config "/manualpeerlist:pool.ntp.org 0.pool.ntp.org 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"
|
||||
|
||||
net start "Windows Time"
|
||||
|
||||
sc config w32time start= auto
|
BIN
WindowsInstaller/Install Files/qortal.jar
Executable file
BIN
WindowsInstaller/Install Files/qortal.jar
Executable file
Binary file not shown.
1268
WindowsInstaller/Qortal.aip
Executable file
1268
WindowsInstaller/Qortal.aip
Executable file
File diff suppressed because it is too large
Load Diff
26
WindowsInstaller/README.md
Normal file
26
WindowsInstaller/README.md
Normal file
@@ -0,0 +1,26 @@
|
||||
# Windows installer
|
||||
|
||||
## Prerequisites
|
||||
|
||||
* AdvancedInstaller v16 or better, and enterprise licence if translations are required
|
||||
* Installed AdoptOpenJDK v11 64bit, full JDK *not* JRE
|
||||
|
||||
## General build instructions
|
||||
|
||||
If this is your first time opening the `qortal.aip` file then you might need to adjust
|
||||
configured paths, or create a dummy `D:` drive with the expected layout.
|
||||
|
||||
Typical build procedure:
|
||||
|
||||
* Overwrite the `qortal.jar` file in `Install-Files\`
|
||||
* Open AdvancedInstaller with qortal.aip file
|
||||
* If releasing a new version, change version number in:
|
||||
+ "Product Information" side menu
|
||||
+ "Product Details" side menu entry
|
||||
+ "Product Details" tab in "Product Details" pane
|
||||
+ "Product Version" entry box
|
||||
* Click away to a different side menu entry, e.g. "Resources" -> "Files and Folders"
|
||||
* You should be prompted whether to generate a new product key, click "Generate New"
|
||||
* Click "Build" button
|
||||
* New EXE should be generated in `Qortal-SetupFiles\` folder with correct version number
|
||||
|
97
WindowsInstaller/dictionary.ail
Executable file
97
WindowsInstaller/dictionary.ail
Executable file
@@ -0,0 +1,97 @@
|
||||
<?xml version="1.0" encoding="UTF-8" standalone="yes"?>
|
||||
<DICTIONARY type="multilanguage">
|
||||
<!-- Control table -->
|
||||
<ENTRY id="Control.Text.CustomizeDataPathDlg#Description">
|
||||
<STRING lang="en" value="Do you want to store the blockchain, and other data, in a specific folder?"/>
|
||||
<STRING lang="zh" value="你想把区块链数据存放在一个特定的文件夹吗?"/>
|
||||
<STRING lang="zh_TW" value="你想把区块链数据存放在一个特定的文件夹吗?"/>
|
||||
</ENTRY>
|
||||
<ENTRY id="Control.Text.CustomizeDataPathDlg#Text">
|
||||
<STRING lang="en" value="Select one of the options below, then click "Next"."/>
|
||||
<STRING lang="zh" value="请选择,然后“下一步”"/>
|
||||
<STRING lang="zh_TW" value="请选择,然后“下一步”"/>
|
||||
</ENTRY>
|
||||
<ENTRY id="Control.Text.CustomizeDataPathDlg#Title">
|
||||
<STRING lang="en" value="Choose Custom Data Storage Folder?"/>
|
||||
<STRING lang="zh" value="选择数据保存的文件夹?"/>
|
||||
<STRING lang="zh_TW" value="选择数据保存的文件夹?"/>
|
||||
</ENTRY>
|
||||
<ENTRY id="Control.Text.CustomizeDbDlg#Description">
|
||||
<STRING lang="en" value="Do you want to store the blockchain, and other data, in a specific folder?"/>
|
||||
<STRING lang="zh" value="Do you want to store the blockchain, and other data, in a specific folder?"/>
|
||||
<STRING lang="zh_TW" value="Do you want to store the blockchain, and other data, in a specific folder?"/>
|
||||
</ENTRY>
|
||||
<ENTRY id="Control.Text.CustomizeDbDlg#Title">
|
||||
<STRING lang="en" value="Choose Custom Data Storage Folder?"/>
|
||||
<STRING lang="zh" value="Choose Custom Data Storage Folder?"/>
|
||||
<STRING lang="zh_TW" value="Choose Custom Data Storage Folder?"/>
|
||||
</ENTRY>
|
||||
<ENTRY id="Control.Text.DataFolderDlg#Description">
|
||||
<STRING lang="en" value="This is the folder where the blockchain, and other data, will be stored."/>
|
||||
<STRING lang="zh" value="这里是区块链及其它数据存放的文件夹"/>
|
||||
<STRING lang="zh_TW" value="这里是区块链及其它数据存放的文件夹"/>
|
||||
</ENTRY>
|
||||
<ENTRY id="Control.Text.DataFolderDlg#Text">
|
||||
<STRING lang="en" value="To store data in this folder, click "[Text_Next]". To store data in a different folder, enter it below or click "Browse"."/>
|
||||
<STRING lang="zh" value="如果存放在这个文件夹,点 “下一步”。如果存放在其它位置,请选择“浏览”。"/>
|
||||
<STRING lang="zh_TW" value="如果存放在这个文件夹,点 “下一步”。如果存放在其它位置,请选择“浏览”。"/>
|
||||
</ENTRY>
|
||||
<ENTRY id="Control.Text.DataFolderDlg#Title">
|
||||
<STRING lang="en" value="Select Data Storage Folder"/>
|
||||
<STRING lang="zh" value="请选择文件存储地方"/>
|
||||
<STRING lang="zh_TW" value="请选择文件存储地方"/>
|
||||
</ENTRY>
|
||||
<ENTRY id="Control.Text.DbFolderDlg#Description">
|
||||
<STRING lang="en" value="This is the folder where the blockchain, and other data, will be stored."/>
|
||||
<STRING lang="zh" value="This is the folder where the blockchain, and other data, will be stored."/>
|
||||
<STRING lang="zh_TW" value="This is the folder where the blockchain, and other data, will be stored."/>
|
||||
</ENTRY>
|
||||
<ENTRY id="Control.Text.DbFolderDlg#Title">
|
||||
<STRING lang="en" value="Select Data Storage Folder"/>
|
||||
<STRING lang="zh" value="请选择文件存储地方"/>
|
||||
<STRING lang="zh_TW" value="请选择文件存储地方"/>
|
||||
</ENTRY>
|
||||
<ENTRY id="Control.Text.NTPDialog#Description">
|
||||
<STRING lang="en" value="Reconfigure Windows for more accurate time?"/>
|
||||
<STRING lang="zh" value="重新配置Windows以获得更准确的时间?"/>
|
||||
</ENTRY>
|
||||
<ENTRY id="Control.Text.NTPDialog#Text_1">
|
||||
<STRING lang="en" value="An accurate Windows clock is required to connect to the [ProductName] network and make transactions."/>
|
||||
<STRING lang="zh" value="需要准确的Windows时钟才能连接到[ProductName]网络并进行交易。"/>
|
||||
</ENTRY>
|
||||
<ENTRY id="Control.Text.NTPDialog#Text_2">
|
||||
<STRING lang="en" value="Select one of the options below, then click "Next"."/>
|
||||
<STRING lang="zh" value="请选择,然后“下一步”"/>
|
||||
</ENTRY>
|
||||
<ENTRY id="Control.Text.NTPDialog#Text_3">
|
||||
<STRING lang="en" value="Your computer's clock needs to be accurate to within 0.5 seconds."/>
|
||||
<STRING lang="zh" value="您的计算机时钟需要准确到0.5秒内。"/>
|
||||
</ENTRY>
|
||||
<ENTRY id="Control.Text.NTPDialog#Title">
|
||||
<STRING lang="en" value="Windows clock accuracy"/>
|
||||
<STRING lang="zh" value="Windows 时钟精度"/>
|
||||
</ENTRY>
|
||||
<ENTRY id="Control.Text.VerifyRemoveDlg#RemoveBlockchainCheckbox">
|
||||
<STRING lang="en" value="Remove downloaded blockchain and other data"/>
|
||||
<STRING lang="zh" value="删除您下载的区块链"/>
|
||||
</ENTRY>
|
||||
<!-- RadioButton table -->
|
||||
<ENTRY id="RadioButton.Text.CUSTOM_DB_BOOL#choose">
|
||||
<STRING lang="en" value="Choose custom data storage folder..."/>
|
||||
<STRING lang="zh" value="选择特定的文件夹存储"/>
|
||||
<STRING lang="zh_TW" value="选择特定的文件夹存储"/>
|
||||
</ENTRY>
|
||||
<ENTRY id="RadioButton.Text.CUSTOM_DB_BOOL#default">
|
||||
<STRING lang="en" value="Use default location "/>
|
||||
<STRING lang="zh" value="使用默认存储地点"/>
|
||||
<STRING lang="zh_TW" value="使用默认存储地点"/>
|
||||
</ENTRY>
|
||||
<ENTRY id="RadioButton.Text.RECONFIG_NTP#1">
|
||||
<STRING lang="en" value="Yes, configure Windows to use internet time servers (Recommended)"/>
|
||||
<STRING lang="zh" value="是,将Windows配置为使用多个Internet时间服务器 (推荐的)"/>
|
||||
</ENTRY>
|
||||
<ENTRY id="RadioButton.Text.RECONFIG_NTP#2">
|
||||
<STRING lang="en" value="No, I will manage clock accuracy myself"/>
|
||||
<STRING lang="zh" value="不,我会自己管理时钟精度。"/>
|
||||
</ENTRY>
|
||||
</DICTIONARY>
|
BIN
WindowsInstaller/qortal.ico
Executable file
BIN
WindowsInstaller/qortal.ico
Executable file
Binary file not shown.
After Width: | Height: | Size: 250 KiB |
Binary file not shown.
@@ -4,6 +4,6 @@
|
||||
<modelVersion>4.0.0</modelVersion>
|
||||
<groupId>org.ciyam</groupId>
|
||||
<artifactId>AT</artifactId>
|
||||
<version>1.3.4</version>
|
||||
<version>1.3.7</version>
|
||||
<description>POM was created from install:install-file</description>
|
||||
</project>
|
@@ -3,10 +3,13 @@
|
||||
<groupId>org.ciyam</groupId>
|
||||
<artifactId>AT</artifactId>
|
||||
<versioning>
|
||||
<release>1.3.4</release>
|
||||
<release>1.3.7</release>
|
||||
<versions>
|
||||
<version>1.3.4</version>
|
||||
<version>1.3.5</version>
|
||||
<version>1.3.6</version>
|
||||
<version>1.3.7</version>
|
||||
</versions>
|
||||
<lastUpdated>20200414162728</lastUpdated>
|
||||
<lastUpdated>20200812131412</lastUpdated>
|
||||
</versioning>
|
||||
</metadata>
|
||||
|
6
pom.xml
6
pom.xml
@@ -3,13 +3,13 @@
|
||||
<modelVersion>4.0.0</modelVersion>
|
||||
<groupId>org.qortal</groupId>
|
||||
<artifactId>qortal</artifactId>
|
||||
<version>1.2.0</version>
|
||||
<version>1.3.2</version>
|
||||
<packaging>jar</packaging>
|
||||
<properties>
|
||||
<bitcoinj.version>0.15.5</bitcoinj.version>
|
||||
<bouncycastle.version>1.64</bouncycastle.version>
|
||||
<build.timestamp>${maven.build.timestamp}</build.timestamp>
|
||||
<ciyam-at.version>1.3.4</ciyam-at.version>
|
||||
<ciyam-at.version>1.3.7</ciyam-at.version>
|
||||
<commons-net.version>3.6</commons-net.version>
|
||||
<commons-text.version>1.8</commons-text.version>
|
||||
<dagger.version>1.2.2</dagger.version>
|
||||
@@ -17,7 +17,7 @@
|
||||
<hsqldb.version>2.5.0-fixed</hsqldb.version>
|
||||
<hsqldb-sqltool.version>2.5.0</hsqldb-sqltool.version>
|
||||
<jersey.version>2.29.1</jersey.version>
|
||||
<jetty.version>9.4.22.v20191022</jetty.version>
|
||||
<jetty.version>9.4.29.v20200521</jetty.version>
|
||||
<log4j.version>2.12.1</log4j.version>
|
||||
<project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
|
||||
<slf4j.version>1.7.12</slf4j.version>
|
||||
|
@@ -128,8 +128,8 @@ public enum ApiError {
|
||||
|
||||
// Bitcoin
|
||||
BTC_NETWORK_ISSUE(1201, 500),
|
||||
BTC_BALANCE_ISSUE(1202, 422),
|
||||
BTC_TOO_SOON(1203, 422);
|
||||
BTC_BALANCE_ISSUE(1202, 402),
|
||||
BTC_TOO_SOON(1203, 408);
|
||||
|
||||
private static final Map<Integer, ApiError> map = stream(ApiError.values()).collect(toMap(apiError -> apiError.code, apiError -> apiError));
|
||||
|
||||
|
@@ -3,6 +3,7 @@ package org.qortal.api;
|
||||
import java.io.IOException;
|
||||
|
||||
import javax.servlet.RequestDispatcher;
|
||||
import javax.servlet.ServletException;
|
||||
import javax.servlet.http.HttpServletRequest;
|
||||
import javax.servlet.http.HttpServletResponse;
|
||||
|
||||
@@ -17,7 +18,7 @@ public class ApiErrorHandler extends ErrorHandler {
|
||||
private static final Logger LOGGER = LogManager.getLogger(ApiErrorHandler.class);
|
||||
|
||||
@Override
|
||||
public void handle(String target, Request baseRequest, HttpServletRequest request, HttpServletResponse response) throws IOException {
|
||||
public void handle(String target, Request baseRequest, HttpServletRequest request, HttpServletResponse response) throws IOException, ServletException {
|
||||
if (Settings.getInstance().isApiLoggingEnabled()) {
|
||||
String requestURI = request.getRequestURI();
|
||||
|
||||
|
@@ -18,9 +18,9 @@ import org.eclipse.jetty.http.HttpVersion;
|
||||
import org.eclipse.jetty.rewrite.handler.RedirectPatternRule;
|
||||
import org.eclipse.jetty.rewrite.handler.RewriteHandler;
|
||||
import org.eclipse.jetty.server.CustomRequestLog;
|
||||
import org.eclipse.jetty.server.DetectorConnectionFactory;
|
||||
import org.eclipse.jetty.server.HttpConfiguration;
|
||||
import org.eclipse.jetty.server.HttpConnectionFactory;
|
||||
import org.eclipse.jetty.server.OptionalSslConnectionFactory;
|
||||
import org.eclipse.jetty.server.RequestLog;
|
||||
import org.eclipse.jetty.server.RequestLogWriter;
|
||||
import org.eclipse.jetty.server.SecureRequestCustomizer;
|
||||
@@ -43,6 +43,8 @@ import org.qortal.api.websocket.ActiveChatsWebSocket;
|
||||
import org.qortal.api.websocket.AdminStatusWebSocket;
|
||||
import org.qortal.api.websocket.BlocksWebSocket;
|
||||
import org.qortal.api.websocket.ChatMessagesWebSocket;
|
||||
import org.qortal.api.websocket.TradeBotWebSocket;
|
||||
import org.qortal.api.websocket.TradeOffersWebSocket;
|
||||
import org.qortal.settings.Settings;
|
||||
|
||||
public class ApiService {
|
||||
@@ -113,8 +115,7 @@ public class ApiService {
|
||||
SslConnectionFactory sslConnectionFactory = new SslConnectionFactory(sslContextFactory, HttpVersion.HTTP_1_1.asString());
|
||||
|
||||
ServerConnector portUnifiedConnector = new ServerConnector(this.server,
|
||||
new OptionalSslConnectionFactory(sslConnectionFactory, HttpVersion.HTTP_1_1.asString()),
|
||||
sslConnectionFactory,
|
||||
new DetectorConnectionFactory(sslConnectionFactory),
|
||||
httpConnectionFactory);
|
||||
portUnifiedConnector.setHost(Settings.getInstance().getBindAddress());
|
||||
portUnifiedConnector.setPort(Settings.getInstance().getApiPort());
|
||||
@@ -197,6 +198,8 @@ public class ApiService {
|
||||
context.addServlet(BlocksWebSocket.class, "/websockets/blocks");
|
||||
context.addServlet(ActiveChatsWebSocket.class, "/websockets/chat/active/*");
|
||||
context.addServlet(ChatMessagesWebSocket.class, "/websockets/chat/messages");
|
||||
context.addServlet(TradeOffersWebSocket.class, "/websockets/crosschain/tradeoffers");
|
||||
context.addServlet(TradeBotWebSocket.class, "/websockets/crosschain/tradebot");
|
||||
|
||||
// Start server
|
||||
this.server.start();
|
||||
|
25
src/main/java/org/qortal/api/model/BitcoinSendRequest.java
Normal file
25
src/main/java/org/qortal/api/model/BitcoinSendRequest.java
Normal file
@@ -0,0 +1,25 @@
|
||||
package org.qortal.api.model;
|
||||
|
||||
import javax.xml.bind.annotation.XmlAccessType;
|
||||
import javax.xml.bind.annotation.XmlAccessorType;
|
||||
import javax.xml.bind.annotation.adapters.XmlJavaTypeAdapter;
|
||||
|
||||
import io.swagger.v3.oas.annotations.media.Schema;
|
||||
|
||||
@XmlAccessorType(XmlAccessType.FIELD)
|
||||
public class BitcoinSendRequest {
|
||||
|
||||
@Schema(description = "Bitcoin BIP32 extended private key", example = "tprv8ZgxMBicQKsPdahhFSrCdvC1bsWyzHHZfTneTVqUXN6s1wEtZLwAkZXzFP6TbTVGajEB55L1HYLg2aQMecZLXLre5YJcawpdFG66STVAWPJ")
|
||||
public String xprv58;
|
||||
|
||||
@Schema(description = "Recipient's Bitcoin address ('legacy' P2PKH only)", example = "1BitcoinEaterAddressDontSendf59kuE")
|
||||
public String receivingAddress;
|
||||
|
||||
@Schema(description = "Amount of BTC to send")
|
||||
@XmlJavaTypeAdapter(value = org.qortal.api.AmountTypeAdapter.class)
|
||||
public long bitcoinAmount;
|
||||
|
||||
public BitcoinSendRequest() {
|
||||
}
|
||||
|
||||
}
|
71
src/main/java/org/qortal/api/model/BlockInfo.java
Normal file
71
src/main/java/org/qortal/api/model/BlockInfo.java
Normal file
@@ -0,0 +1,71 @@
|
||||
package org.qortal.api.model;
|
||||
|
||||
import javax.xml.bind.annotation.XmlAccessType;
|
||||
import javax.xml.bind.annotation.XmlAccessorType;
|
||||
|
||||
import org.qortal.data.account.RewardShareData;
|
||||
import org.qortal.data.block.BlockData;
|
||||
import org.qortal.repository.DataException;
|
||||
import org.qortal.repository.Repository;
|
||||
import org.qortal.repository.RepositoryManager;
|
||||
|
||||
@XmlAccessorType(XmlAccessType.FIELD)
|
||||
public class BlockInfo {
|
||||
|
||||
private byte[] signature;
|
||||
private int height;
|
||||
private long timestamp;
|
||||
private int transactionCount;
|
||||
private String minterAddress;
|
||||
|
||||
protected BlockInfo() {
|
||||
/* For JAXB */
|
||||
}
|
||||
|
||||
public BlockInfo(byte[] signature, int height, long timestamp, int transactionCount, String minterAddress) {
|
||||
this.signature = signature;
|
||||
this.height = height;
|
||||
this.timestamp = timestamp;
|
||||
this.transactionCount = transactionCount;
|
||||
this.minterAddress = minterAddress;
|
||||
}
|
||||
|
||||
public BlockInfo(BlockData blockData) {
|
||||
// Convert BlockData to BlockInfo, using additional data
|
||||
this.minterAddress = "unknown?";
|
||||
|
||||
try (final Repository repository = RepositoryManager.getRepository()) {
|
||||
RewardShareData rewardShareData = repository.getAccountRepository().getRewardShare(blockData.getMinterPublicKey());
|
||||
if (rewardShareData != null)
|
||||
this.minterAddress = rewardShareData.getMintingAccount();
|
||||
} catch (DataException e) {
|
||||
// We'll carry on with placeholder minterAddress then...
|
||||
}
|
||||
|
||||
this.signature = blockData.getSignature();
|
||||
this.height = blockData.getHeight();
|
||||
this.timestamp = blockData.getTimestamp();
|
||||
this.transactionCount = blockData.getTransactionCount();
|
||||
}
|
||||
|
||||
public byte[] getSignature() {
|
||||
return this.signature;
|
||||
}
|
||||
|
||||
public int getHeight() {
|
||||
return this.height;
|
||||
}
|
||||
|
||||
public long getTimestamp() {
|
||||
return this.timestamp;
|
||||
}
|
||||
|
||||
public int getTransactionCount() {
|
||||
return this.transactionCount;
|
||||
}
|
||||
|
||||
public String getMinterAddress() {
|
||||
return this.minterAddress;
|
||||
}
|
||||
|
||||
}
|
@@ -25,6 +25,9 @@ public class CrossChainBitcoinRedeemRequest {
|
||||
@Schema(description = "32-byte secret", example = "6gVbAXCVzJXAWwtAVGAfgAkkXpeXvPUwSciPmCfSfXJG")
|
||||
public byte[] secret;
|
||||
|
||||
@Schema(description = "Bitcoin HASH160(public key) for receiving funds, or omit to derive from private key", example = "u17kBVKkKSp12oUzaxFwNnq1JZf")
|
||||
public byte[] receivingAccountInfo;
|
||||
|
||||
public CrossChainBitcoinRedeemRequest() {
|
||||
}
|
||||
|
||||
|
@@ -12,20 +12,19 @@ public class CrossChainBuildRequest {
|
||||
@Schema(description = "AT creator's public key", example = "C6wuddsBV3HzRrXUtezE7P5MoRXp5m3mEDokRDGZB6ry")
|
||||
public byte[] creatorPublicKey;
|
||||
|
||||
@Schema(description = "Initial QORT amount paid when trade agreed", example = "0.00100000")
|
||||
@XmlJavaTypeAdapter(value = org.qortal.api.AmountTypeAdapter.class)
|
||||
public long initialQortAmount;
|
||||
|
||||
@Schema(description = "Final QORT amount paid out on successful trade", example = "80.40200000")
|
||||
@XmlJavaTypeAdapter(value = org.qortal.api.AmountTypeAdapter.class)
|
||||
public long finalQortAmount;
|
||||
public long qortAmount;
|
||||
|
||||
@Schema(description = "QORT amount funding AT, including covering AT execution fees", example = "123.45670000")
|
||||
@XmlJavaTypeAdapter(value = org.qortal.api.AmountTypeAdapter.class)
|
||||
public long fundingQortAmount;
|
||||
|
||||
@Schema(description = "HASH160 of creator's Bitcoin public key", example = "2daMveGc5pdjRyFacbxBzMksCbyC")
|
||||
public byte[] bitcoinPublicKeyHash;
|
||||
|
||||
@Schema(description = "HASH160 of secret", example = "43vnftqkjxrhb5kJdkU1ZFQLEnWV")
|
||||
public byte[] secretHash;
|
||||
public byte[] hashOfSecretB;
|
||||
|
||||
@Schema(description = "Bitcoin P2SH BTC balance for release of secret", example = "0.00864200")
|
||||
@XmlJavaTypeAdapter(value = org.qortal.api.AmountTypeAdapter.class)
|
||||
|
@@ -8,10 +8,10 @@ import io.swagger.v3.oas.annotations.media.Schema;
|
||||
@XmlAccessorType(XmlAccessType.FIELD)
|
||||
public class CrossChainCancelRequest {
|
||||
|
||||
@Schema(description = "AT creator's public key", example = "C6wuddsBV3HzRrXUtezE7P5MoRXp5m3mEDokRDGZB6ry")
|
||||
@Schema(description = "AT creator's public key", example = "K6wuddsBV3HzRrXFFezE7P5MoRXp5m3mEDokRDGZB6ry")
|
||||
public byte[] creatorPublicKey;
|
||||
|
||||
@Schema(description = "Qortal AT address")
|
||||
@Schema(description = "Qortal trade AT address")
|
||||
public String atAddress;
|
||||
|
||||
public CrossChainCancelRequest() {
|
||||
|
@@ -0,0 +1,86 @@
|
||||
package org.qortal.api.model;
|
||||
|
||||
import javax.xml.bind.annotation.XmlAccessType;
|
||||
import javax.xml.bind.annotation.XmlAccessorType;
|
||||
import javax.xml.bind.annotation.adapters.XmlJavaTypeAdapter;
|
||||
|
||||
import org.qortal.crosschain.BTCACCT;
|
||||
import org.qortal.data.crosschain.CrossChainTradeData;
|
||||
|
||||
import io.swagger.v3.oas.annotations.media.Schema;
|
||||
|
||||
// All properties to be converted to JSON via JAXB
|
||||
@XmlAccessorType(XmlAccessType.FIELD)
|
||||
public class CrossChainOfferSummary {
|
||||
|
||||
// Properties
|
||||
|
||||
@Schema(description = "AT's Qortal address")
|
||||
public String qortalAtAddress;
|
||||
|
||||
@Schema(description = "AT creator's Qortal address")
|
||||
public String qortalCreator;
|
||||
|
||||
@XmlJavaTypeAdapter(value = org.qortal.api.AmountTypeAdapter.class)
|
||||
private long qortAmount;
|
||||
|
||||
@XmlJavaTypeAdapter(value = org.qortal.api.AmountTypeAdapter.class)
|
||||
private long btcAmount;
|
||||
|
||||
@Schema(description = "Suggested trade timeout (minutes)", example = "10080")
|
||||
private int tradeTimeout;
|
||||
|
||||
private BTCACCT.Mode mode;
|
||||
|
||||
private long timestamp;
|
||||
|
||||
private String partnerQortalReceivingAddress;
|
||||
|
||||
protected CrossChainOfferSummary() {
|
||||
/* For JAXB */
|
||||
}
|
||||
|
||||
public CrossChainOfferSummary(CrossChainTradeData crossChainTradeData, long timestamp) {
|
||||
this.qortalAtAddress = crossChainTradeData.qortalAtAddress;
|
||||
this.qortalCreator = crossChainTradeData.qortalCreator;
|
||||
this.qortAmount = crossChainTradeData.qortAmount;
|
||||
this.btcAmount = crossChainTradeData.expectedBitcoin;
|
||||
this.tradeTimeout = crossChainTradeData.tradeTimeout;
|
||||
this.mode = crossChainTradeData.mode;
|
||||
this.timestamp = timestamp;
|
||||
this.partnerQortalReceivingAddress = crossChainTradeData.qortalPartnerReceivingAddress;
|
||||
}
|
||||
|
||||
public String getQortalAtAddress() {
|
||||
return this.qortalAtAddress;
|
||||
}
|
||||
|
||||
public String getQortalCreator() {
|
||||
return this.qortalCreator;
|
||||
}
|
||||
|
||||
public long getQortAmount() {
|
||||
return this.qortAmount;
|
||||
}
|
||||
|
||||
public long getBtcAmount() {
|
||||
return this.btcAmount;
|
||||
}
|
||||
|
||||
public int getTradeTimeout() {
|
||||
return this.tradeTimeout;
|
||||
}
|
||||
|
||||
public BTCACCT.Mode getMode() {
|
||||
return this.mode;
|
||||
}
|
||||
|
||||
public long getTimestamp() {
|
||||
return this.timestamp;
|
||||
}
|
||||
|
||||
public String getPartnerQortalReceivingAddress() {
|
||||
return this.partnerQortalReceivingAddress;
|
||||
}
|
||||
|
||||
}
|
@@ -8,14 +8,20 @@ import io.swagger.v3.oas.annotations.media.Schema;
|
||||
@XmlAccessorType(XmlAccessType.FIELD)
|
||||
public class CrossChainSecretRequest {
|
||||
|
||||
@Schema(description = "Public key to match AT's 'recipient'", example = "C6wuddsBV3HzRrXUtezE7P5MoRXp5m3mEDokRDGZB6ry")
|
||||
public byte[] recipientPublicKey;
|
||||
@Schema(description = "Public key to match AT's trade 'partner'", example = "C6wuddsBV3HzRrXUtezE7P5MoRXp5m3mEDokRDGZB6ry")
|
||||
public byte[] partnerPublicKey;
|
||||
|
||||
@Schema(description = "Qortal AT address")
|
||||
public String atAddress;
|
||||
|
||||
@Schema(description = "32-byte secret", example = "6gVbAXCVzJXAWwtAVGAfgAkkXpeXvPUwSciPmCfSfXJG")
|
||||
public byte[] secret;
|
||||
@Schema(description = "secret-A (32 bytes)", example = "FHMzten4he9jZ4HGb4297Utj6F5g2w7serjq2EnAg2s1")
|
||||
public byte[] secretA;
|
||||
|
||||
@Schema(description = "secret-B (32 bytes)", example = "EN2Bgx3BcEMtxFCewmCVSMkfZjVKYhx3KEXC5A21KBGx")
|
||||
public byte[] secretB;
|
||||
|
||||
@Schema(description = "Qortal address for receiving QORT from AT")
|
||||
public String receivingAddress;
|
||||
|
||||
public CrossChainSecretRequest() {
|
||||
}
|
||||
|
@@ -8,14 +8,14 @@ import io.swagger.v3.oas.annotations.media.Schema;
|
||||
@XmlAccessorType(XmlAccessType.FIELD)
|
||||
public class CrossChainTradeRequest {
|
||||
|
||||
@Schema(description = "AT creator's public key", example = "C6wuddsBV3HzRrXUtezE7P5MoRXp5m3mEDokRDGZB6ry")
|
||||
public byte[] creatorPublicKey;
|
||||
@Schema(description = "AT creator's 'trade' public key", example = "C6wuddsBV3HzRrXUtezE7P5MoRXp5m3mEDokRDGZB6ry")
|
||||
public byte[] tradePublicKey;
|
||||
|
||||
@Schema(description = "Qortal AT address")
|
||||
public String atAddress;
|
||||
|
||||
@Schema(description = "Qortal address for trade partner/recipient")
|
||||
public String recipient;
|
||||
@Schema(description = "Signature of trading partner's 'offer' MESSAGE transaction")
|
||||
public byte[] messageTransactionSignature;
|
||||
|
||||
public CrossChainTradeRequest() {
|
||||
}
|
||||
|
@@ -0,0 +1,43 @@
|
||||
package org.qortal.api.model;
|
||||
|
||||
import javax.xml.bind.annotation.XmlAccessType;
|
||||
import javax.xml.bind.annotation.XmlAccessorType;
|
||||
import javax.xml.bind.annotation.adapters.XmlJavaTypeAdapter;
|
||||
|
||||
import org.qortal.data.crosschain.CrossChainTradeData;
|
||||
|
||||
// All properties to be converted to JSON via JAXB
|
||||
@XmlAccessorType(XmlAccessType.FIELD)
|
||||
public class CrossChainTradeSummary {
|
||||
|
||||
private long tradeTimestamp;
|
||||
|
||||
@XmlJavaTypeAdapter(value = org.qortal.api.AmountTypeAdapter.class)
|
||||
private long qortAmount;
|
||||
|
||||
@XmlJavaTypeAdapter(value = org.qortal.api.AmountTypeAdapter.class)
|
||||
private long btcAmount;
|
||||
|
||||
protected CrossChainTradeSummary() {
|
||||
/* For JAXB */
|
||||
}
|
||||
|
||||
public CrossChainTradeSummary(CrossChainTradeData crossChainTradeData, long timestamp) {
|
||||
this.tradeTimestamp = timestamp;
|
||||
this.qortAmount = crossChainTradeData.qortAmount;
|
||||
this.btcAmount = crossChainTradeData.expectedBitcoin;
|
||||
}
|
||||
|
||||
public long getTradeTimestamp() {
|
||||
return this.tradeTimestamp;
|
||||
}
|
||||
|
||||
public long getQortAmount() {
|
||||
return this.qortAmount;
|
||||
}
|
||||
|
||||
public long getBtcAmount() {
|
||||
return this.btcAmount;
|
||||
}
|
||||
|
||||
}
|
@@ -0,0 +1,36 @@
|
||||
package org.qortal.api.model;
|
||||
|
||||
import javax.xml.bind.annotation.XmlAccessType;
|
||||
import javax.xml.bind.annotation.XmlAccessorType;
|
||||
import javax.xml.bind.annotation.adapters.XmlJavaTypeAdapter;
|
||||
|
||||
import io.swagger.v3.oas.annotations.media.Schema;
|
||||
|
||||
@XmlAccessorType(XmlAccessType.FIELD)
|
||||
public class TradeBotCreateRequest {
|
||||
|
||||
@Schema(description = "Trade creator's public key", example = "2zR1WFsbM7akHghqSCYKBPk6LDP8aKiQSRS1FrwoLvoB")
|
||||
public byte[] creatorPublicKey;
|
||||
|
||||
@Schema(description = "QORT amount paid out on successful trade", example = "80.40200000")
|
||||
@XmlJavaTypeAdapter(value = org.qortal.api.AmountTypeAdapter.class)
|
||||
public long qortAmount;
|
||||
|
||||
@Schema(description = "QORT amount funding AT, including covering AT execution fees", example = "81")
|
||||
@XmlJavaTypeAdapter(value = org.qortal.api.AmountTypeAdapter.class)
|
||||
public long fundingQortAmount;
|
||||
|
||||
@Schema(description = "Bitcoin amount wanted in return", example = "0.00864200")
|
||||
@XmlJavaTypeAdapter(value = org.qortal.api.AmountTypeAdapter.class)
|
||||
public long bitcoinAmount;
|
||||
|
||||
@Schema(description = "Suggested trade timeout (minutes)", example = "10080")
|
||||
public int tradeTimeout;
|
||||
|
||||
@Schema(description = "Bitcoin address for receiving", example = "1BitcoinEaterAddressDontSendf59kuE")
|
||||
public String receivingAddress;
|
||||
|
||||
public TradeBotCreateRequest() {
|
||||
}
|
||||
|
||||
}
|
@@ -0,0 +1,23 @@
|
||||
package org.qortal.api.model;
|
||||
|
||||
import javax.xml.bind.annotation.XmlAccessType;
|
||||
import javax.xml.bind.annotation.XmlAccessorType;
|
||||
|
||||
import io.swagger.v3.oas.annotations.media.Schema;
|
||||
|
||||
@XmlAccessorType(XmlAccessType.FIELD)
|
||||
public class TradeBotRespondRequest {
|
||||
|
||||
@Schema(description = "Qortal AT address", example = "AH3e3jHEsGHPVQPDiJx4pYqgVi72auxgVy")
|
||||
public String atAddress;
|
||||
|
||||
@Schema(description = "Bitcoin BIP32 extended private key", example = "tprv8ZgxMBicQKsPdahhFSrCdvC1bsWyzHHZfTneTVqUXN6s1wEtZLwAkZXzFP6TbTVGajEB55L1HYLg2aQMecZLXLre5YJcawpdFG66STVAWPJ")
|
||||
public String xprv58;
|
||||
|
||||
@Schema(description = "Qortal address for receiving QORT from AT")
|
||||
public String receivingAddress;
|
||||
|
||||
public TradeBotRespondRequest() {
|
||||
}
|
||||
|
||||
}
|
@@ -302,13 +302,13 @@ public class AdminResource {
|
||||
@DELETE
|
||||
@Path("/mintingaccounts")
|
||||
@Operation(
|
||||
summary = "Remove account/reward-share from use by BlockMinter, using private key",
|
||||
summary = "Remove account/reward-share from use by BlockMinter, using public or private key",
|
||||
requestBody = @RequestBody(
|
||||
required = true,
|
||||
content = @Content(
|
||||
mediaType = MediaType.TEXT_PLAIN,
|
||||
schema = @Schema(
|
||||
type = "string", example = "private key"
|
||||
type = "string", example = "public or private key"
|
||||
)
|
||||
)
|
||||
),
|
||||
@@ -319,13 +319,13 @@ public class AdminResource {
|
||||
}
|
||||
)
|
||||
@ApiErrors({ApiError.INVALID_PRIVATE_KEY, ApiError.REPOSITORY_ISSUE})
|
||||
public String deleteMintingAccount(String seed58) {
|
||||
public String deleteMintingAccount(String key58) {
|
||||
Security.checkApiCallAllowed(request);
|
||||
|
||||
try (final Repository repository = RepositoryManager.getRepository()) {
|
||||
byte[] seed = Base58.decode(seed58.trim());
|
||||
byte[] key = Base58.decode(key58.trim());
|
||||
|
||||
if (repository.getAccountRepository().delete(seed) == 0)
|
||||
if (repository.getAccountRepository().delete(key) == 0)
|
||||
return "false";
|
||||
|
||||
repository.saveChanges();
|
||||
|
@@ -25,7 +25,6 @@ import org.qortal.api.ApiError;
|
||||
import org.qortal.api.ApiErrors;
|
||||
import org.qortal.api.ApiException;
|
||||
import org.qortal.api.ApiExceptionFactory;
|
||||
import org.qortal.at.QortalAtLoggerFactory;
|
||||
import org.qortal.data.at.ATData;
|
||||
import org.qortal.data.at.ATStateData;
|
||||
import org.qortal.data.transaction.DeployAtTransactionData;
|
||||
@@ -147,8 +146,7 @@ public class AtResource {
|
||||
ATStateData atStateData = repository.getATRepository().getLatestATState(atAddress);
|
||||
byte[] stateData = atStateData.getStateData();
|
||||
|
||||
QortalAtLoggerFactory loggerFactory = QortalAtLoggerFactory.getInstance();
|
||||
byte[] dataBytes = MachineState.extractDataBytes(loggerFactory, stateData);
|
||||
byte[] dataBytes = MachineState.extractDataBytes(stateData);
|
||||
|
||||
return dataBytes;
|
||||
} catch (ApiException e) {
|
||||
|
@@ -23,6 +23,7 @@ import javax.ws.rs.core.MediaType;
|
||||
import org.qortal.api.ApiError;
|
||||
import org.qortal.api.ApiErrors;
|
||||
import org.qortal.api.ApiExceptionFactory;
|
||||
import org.qortal.api.model.BlockInfo;
|
||||
import org.qortal.api.model.BlockSignerSummary;
|
||||
import org.qortal.crypto.Crypto;
|
||||
import org.qortal.data.account.AccountData;
|
||||
@@ -480,4 +481,44 @@ public class BlocksResource {
|
||||
}
|
||||
}
|
||||
|
||||
@GET
|
||||
@Path("/summaries")
|
||||
@Operation(
|
||||
summary = "Fetch only summary info about a range of blocks",
|
||||
description = "Specify up to 2 out 3 of: start, end and count. If neither start nor end are specified, then end is assumed to be latest block. Where necessary, count is assumed to be 50.",
|
||||
responses = {
|
||||
@ApiResponse(
|
||||
description = "blocks",
|
||||
content = @Content(
|
||||
array = @ArraySchema(
|
||||
schema = @Schema(
|
||||
implementation = BlockInfo.class
|
||||
)
|
||||
)
|
||||
)
|
||||
)
|
||||
}
|
||||
)
|
||||
@ApiErrors({
|
||||
ApiError.REPOSITORY_ISSUE
|
||||
})
|
||||
public List<BlockInfo> getBlockRange(
|
||||
@QueryParam("start") Integer startHeight,
|
||||
@QueryParam("end") Integer endHeight,
|
||||
@Parameter(ref = "count") @QueryParam("count") Integer count) {
|
||||
// Check up to 2 out of 3 params
|
||||
if (startHeight != null && endHeight != null && count != null)
|
||||
throw ApiExceptionFactory.INSTANCE.createException(request, ApiError.INVALID_CRITERIA);
|
||||
|
||||
// Check values
|
||||
if ((startHeight != null && startHeight < 1) || (endHeight != null && endHeight < 1) || (count != null && count < 1))
|
||||
throw ApiExceptionFactory.INSTANCE.createException(request, ApiError.INVALID_CRITERIA);
|
||||
|
||||
try (final Repository repository = RepositoryManager.getRepository()) {
|
||||
return repository.getBlockRepository().getBlockInfos(startHeight, endHeight, count);
|
||||
} catch (DataException e) {
|
||||
throw ApiExceptionFactory.INSTANCE.createException(request, ApiError.REPOSITORY_ISSUE, e);
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
File diff suppressed because it is too large
Load Diff
@@ -6,11 +6,12 @@ import java.util.Map;
|
||||
import java.util.concurrent.atomic.AtomicReference;
|
||||
|
||||
import org.eclipse.jetty.websocket.api.Session;
|
||||
import org.eclipse.jetty.websocket.api.WebSocketException;
|
||||
import org.eclipse.jetty.websocket.api.annotations.OnWebSocketClose;
|
||||
import org.eclipse.jetty.websocket.api.annotations.OnWebSocketConnect;
|
||||
import org.eclipse.jetty.websocket.api.annotations.OnWebSocketError;
|
||||
import org.eclipse.jetty.websocket.api.annotations.OnWebSocketMessage;
|
||||
import org.eclipse.jetty.websocket.api.annotations.WebSocket;
|
||||
import org.eclipse.jetty.websocket.servlet.WebSocketServlet;
|
||||
import org.eclipse.jetty.websocket.servlet.WebSocketServletFactory;
|
||||
import org.qortal.controller.ChatNotifier;
|
||||
import org.qortal.crypto.Crypto;
|
||||
@@ -22,7 +23,7 @@ import org.qortal.repository.RepositoryManager;
|
||||
|
||||
@WebSocket
|
||||
@SuppressWarnings("serial")
|
||||
public class ActiveChatsWebSocket extends WebSocketServlet implements ApiWebSocket {
|
||||
public class ActiveChatsWebSocket extends ApiWebSocket {
|
||||
|
||||
@Override
|
||||
public void configure(WebSocketServletFactory factory) {
|
||||
@@ -31,7 +32,7 @@ public class ActiveChatsWebSocket extends WebSocketServlet implements ApiWebSock
|
||||
|
||||
@OnWebSocketConnect
|
||||
public void onWebSocketConnect(Session session) {
|
||||
Map<String, String> pathParams = this.getPathParams(session, "/{address}");
|
||||
Map<String, String> pathParams = getPathParams(session, "/{address}");
|
||||
|
||||
String address = pathParams.get("address");
|
||||
if (address == null || !Crypto.isValidAddress(address)) {
|
||||
@@ -52,6 +53,10 @@ public class ActiveChatsWebSocket extends WebSocketServlet implements ApiWebSock
|
||||
ChatNotifier.getInstance().deregister(session);
|
||||
}
|
||||
|
||||
@OnWebSocketError
|
||||
public void onWebSocketError(Session session, Throwable throwable) {
|
||||
}
|
||||
|
||||
@OnWebSocketMessage
|
||||
public void onWebSocketMessage(Session session, String message) {
|
||||
}
|
||||
@@ -70,7 +75,7 @@ public class ActiveChatsWebSocket extends WebSocketServlet implements ApiWebSock
|
||||
|
||||
StringWriter stringWriter = new StringWriter();
|
||||
|
||||
this.marshall(stringWriter, activeChats);
|
||||
marshall(stringWriter, activeChats);
|
||||
|
||||
// Only output if something has changed
|
||||
String output = stringWriter.toString();
|
||||
@@ -78,8 +83,8 @@ public class ActiveChatsWebSocket extends WebSocketServlet implements ApiWebSock
|
||||
return;
|
||||
|
||||
previousOutput.set(output);
|
||||
session.getRemote().sendString(output);
|
||||
} catch (DataException | IOException e) {
|
||||
session.getRemote().sendStringByFuture(output);
|
||||
} catch (DataException | IOException | WebSocketException e) {
|
||||
// No output this time?
|
||||
}
|
||||
}
|
||||
|
@@ -5,11 +5,12 @@ import java.io.StringWriter;
|
||||
import java.util.concurrent.atomic.AtomicReference;
|
||||
|
||||
import org.eclipse.jetty.websocket.api.Session;
|
||||
import org.eclipse.jetty.websocket.api.WebSocketException;
|
||||
import org.eclipse.jetty.websocket.api.annotations.OnWebSocketClose;
|
||||
import org.eclipse.jetty.websocket.api.annotations.OnWebSocketConnect;
|
||||
import org.eclipse.jetty.websocket.api.annotations.OnWebSocketError;
|
||||
import org.eclipse.jetty.websocket.api.annotations.OnWebSocketMessage;
|
||||
import org.eclipse.jetty.websocket.api.annotations.WebSocket;
|
||||
import org.eclipse.jetty.websocket.servlet.WebSocketServlet;
|
||||
import org.eclipse.jetty.websocket.servlet.WebSocketServletFactory;
|
||||
import org.qortal.api.model.NodeStatus;
|
||||
import org.qortal.controller.StatusNotifier;
|
||||
@@ -19,7 +20,7 @@ import org.qortal.repository.RepositoryManager;
|
||||
|
||||
@WebSocket
|
||||
@SuppressWarnings("serial")
|
||||
public class AdminStatusWebSocket extends WebSocketServlet implements ApiWebSocket {
|
||||
public class AdminStatusWebSocket extends ApiWebSocket {
|
||||
|
||||
@Override
|
||||
public void configure(WebSocketServletFactory factory) {
|
||||
@@ -41,6 +42,10 @@ public class AdminStatusWebSocket extends WebSocketServlet implements ApiWebSock
|
||||
StatusNotifier.getInstance().deregister(session);
|
||||
}
|
||||
|
||||
@OnWebSocketError
|
||||
public void onWebSocketError(Session session, Throwable throwable) {
|
||||
}
|
||||
|
||||
@OnWebSocketMessage
|
||||
public void onWebSocketMessage(Session session, String message) {
|
||||
}
|
||||
@@ -51,7 +56,7 @@ public class AdminStatusWebSocket extends WebSocketServlet implements ApiWebSock
|
||||
|
||||
StringWriter stringWriter = new StringWriter();
|
||||
|
||||
this.marshall(stringWriter, nodeStatus);
|
||||
marshall(stringWriter, nodeStatus);
|
||||
|
||||
// Only output if something has changed
|
||||
String output = stringWriter.toString();
|
||||
@@ -59,8 +64,8 @@ public class AdminStatusWebSocket extends WebSocketServlet implements ApiWebSock
|
||||
return;
|
||||
|
||||
previousOutput.set(output);
|
||||
session.getRemote().sendString(output);
|
||||
} catch (DataException | IOException e) {
|
||||
session.getRemote().sendStringByFuture(output);
|
||||
} catch (DataException | IOException | WebSocketException e) {
|
||||
// No output this time?
|
||||
}
|
||||
}
|
||||
|
@@ -3,7 +3,10 @@ package org.qortal.api.websocket;
|
||||
import java.io.IOException;
|
||||
import java.io.StringWriter;
|
||||
import java.io.Writer;
|
||||
import java.util.ArrayList;
|
||||
import java.util.Collection;
|
||||
import java.util.HashMap;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
|
||||
import javax.xml.bind.JAXBContext;
|
||||
@@ -13,24 +16,28 @@ import javax.xml.bind.Marshaller;
|
||||
import org.eclipse.jetty.http.pathmap.UriTemplatePathSpec;
|
||||
import org.eclipse.jetty.websocket.api.Session;
|
||||
import org.eclipse.jetty.websocket.servlet.ServletUpgradeRequest;
|
||||
import org.eclipse.jetty.websocket.servlet.WebSocketServlet;
|
||||
import org.eclipse.persistence.jaxb.JAXBContextFactory;
|
||||
import org.eclipse.persistence.jaxb.MarshallerProperties;
|
||||
import org.qortal.api.ApiError;
|
||||
import org.qortal.api.ApiErrorRoot;
|
||||
|
||||
interface ApiWebSocket {
|
||||
@SuppressWarnings("serial")
|
||||
abstract class ApiWebSocket extends WebSocketServlet {
|
||||
|
||||
default String getPathInfo(Session session) {
|
||||
private static final Map<Class<? extends ApiWebSocket>, List<Session>> SESSIONS_BY_CLASS = new HashMap<>();
|
||||
|
||||
protected static String getPathInfo(Session session) {
|
||||
ServletUpgradeRequest upgradeRequest = (ServletUpgradeRequest) session.getUpgradeRequest();
|
||||
return upgradeRequest.getHttpServletRequest().getPathInfo();
|
||||
}
|
||||
|
||||
default Map<String, String> getPathParams(Session session, String pathSpec) {
|
||||
protected static Map<String, String> getPathParams(Session session, String pathSpec) {
|
||||
UriTemplatePathSpec uriTemplatePathSpec = new UriTemplatePathSpec(pathSpec);
|
||||
return uriTemplatePathSpec.getPathParams(this.getPathInfo(session));
|
||||
return uriTemplatePathSpec.getPathParams(getPathInfo(session));
|
||||
}
|
||||
|
||||
default void sendError(Session session, ApiError apiError) {
|
||||
protected static void sendError(Session session, ApiError apiError) {
|
||||
ApiErrorRoot apiErrorRoot = new ApiErrorRoot();
|
||||
apiErrorRoot.setApiError(apiError);
|
||||
|
||||
@@ -43,7 +50,7 @@ interface ApiWebSocket {
|
||||
}
|
||||
}
|
||||
|
||||
default void marshall(Writer writer, Object object) throws IOException {
|
||||
protected static void marshall(Writer writer, Object object) throws IOException {
|
||||
Marshaller marshaller = createMarshaller(object.getClass());
|
||||
|
||||
try {
|
||||
@@ -53,7 +60,7 @@ interface ApiWebSocket {
|
||||
}
|
||||
}
|
||||
|
||||
default void marshall(Writer writer, Collection<?> collection) throws IOException {
|
||||
protected static void marshall(Writer writer, Collection<?> collection) throws IOException {
|
||||
// If collection is empty then we're returning "[]" anyway
|
||||
if (collection.isEmpty()) {
|
||||
writer.append("[]");
|
||||
@@ -92,4 +99,22 @@ interface ApiWebSocket {
|
||||
}
|
||||
}
|
||||
|
||||
public void onWebSocketConnect(Session session) {
|
||||
synchronized (SESSIONS_BY_CLASS) {
|
||||
SESSIONS_BY_CLASS.computeIfAbsent(this.getClass(), clazz -> new ArrayList<>()).add(session);
|
||||
}
|
||||
}
|
||||
|
||||
public void onWebSocketClose(Session session, int statusCode, String reason) {
|
||||
synchronized (SESSIONS_BY_CLASS) {
|
||||
SESSIONS_BY_CLASS.get(this.getClass()).remove(session);
|
||||
}
|
||||
}
|
||||
|
||||
protected List<Session> getSessions() {
|
||||
synchronized (SESSIONS_BY_CLASS) {
|
||||
return new ArrayList<>(SESSIONS_BY_CLASS.get(this.getClass()));
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
@@ -2,17 +2,19 @@ package org.qortal.api.websocket;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.io.StringWriter;
|
||||
import java.util.List;
|
||||
|
||||
import org.eclipse.jetty.websocket.api.Session;
|
||||
import org.eclipse.jetty.websocket.api.WebSocketException;
|
||||
import org.eclipse.jetty.websocket.api.annotations.OnWebSocketClose;
|
||||
import org.eclipse.jetty.websocket.api.annotations.OnWebSocketConnect;
|
||||
import org.eclipse.jetty.websocket.api.annotations.OnWebSocketError;
|
||||
import org.eclipse.jetty.websocket.api.annotations.OnWebSocketMessage;
|
||||
import org.eclipse.jetty.websocket.api.annotations.WebSocket;
|
||||
import org.eclipse.jetty.websocket.servlet.WebSocketServlet;
|
||||
import org.eclipse.jetty.websocket.servlet.WebSocketServletFactory;
|
||||
import org.qortal.api.ApiError;
|
||||
import org.qortal.api.model.BlockInfo;
|
||||
import org.qortal.controller.BlockNotifier;
|
||||
import org.qortal.data.block.BlockData;
|
||||
import org.qortal.repository.DataException;
|
||||
import org.qortal.repository.Repository;
|
||||
import org.qortal.repository.RepositoryManager;
|
||||
@@ -20,7 +22,7 @@ import org.qortal.utils.Base58;
|
||||
|
||||
@WebSocket
|
||||
@SuppressWarnings("serial")
|
||||
public class BlocksWebSocket extends WebSocketServlet implements ApiWebSocket {
|
||||
public class BlocksWebSocket extends ApiWebSocket {
|
||||
|
||||
@Override
|
||||
public void configure(WebSocketServletFactory factory) {
|
||||
@@ -29,7 +31,7 @@ public class BlocksWebSocket extends WebSocketServlet implements ApiWebSocket {
|
||||
|
||||
@OnWebSocketConnect
|
||||
public void onWebSocketConnect(Session session) {
|
||||
BlockNotifier.Listener listener = blockData -> onNotify(session, blockData);
|
||||
BlockNotifier.Listener listener = blockInfo -> onNotify(session, blockInfo);
|
||||
BlockNotifier.getInstance().register(session, listener);
|
||||
}
|
||||
|
||||
@@ -38,6 +40,10 @@ public class BlocksWebSocket extends WebSocketServlet implements ApiWebSocket {
|
||||
BlockNotifier.getInstance().deregister(session);
|
||||
}
|
||||
|
||||
@OnWebSocketError
|
||||
public void onWebSocketError(Session session, Throwable throwable) {
|
||||
}
|
||||
|
||||
@OnWebSocketMessage
|
||||
public void onWebSocketMessage(Session session, String message) {
|
||||
// We're expecting either a base58 block signature or an integer block height
|
||||
@@ -53,13 +59,19 @@ public class BlocksWebSocket extends WebSocketServlet implements ApiWebSocket {
|
||||
}
|
||||
|
||||
try (final Repository repository = RepositoryManager.getRepository()) {
|
||||
BlockData blockData = repository.getBlockRepository().fromSignature(signature);
|
||||
if (blockData == null) {
|
||||
int height = repository.getBlockRepository().getHeightFromSignature(signature);
|
||||
if (height == 0) {
|
||||
sendError(session, ApiError.BLOCK_UNKNOWN);
|
||||
return;
|
||||
}
|
||||
|
||||
onNotify(session, blockData);
|
||||
List<BlockInfo> blockInfos = repository.getBlockRepository().getBlockInfos(height, null, 1);
|
||||
if (blockInfos == null || blockInfos.isEmpty()) {
|
||||
sendError(session, ApiError.BLOCK_UNKNOWN);
|
||||
return;
|
||||
}
|
||||
|
||||
onNotify(session, blockInfos.get(0));
|
||||
} catch (DataException e) {
|
||||
sendError(session, ApiError.REPOSITORY_ISSUE);
|
||||
}
|
||||
@@ -82,26 +94,26 @@ public class BlocksWebSocket extends WebSocketServlet implements ApiWebSocket {
|
||||
}
|
||||
|
||||
try (final Repository repository = RepositoryManager.getRepository()) {
|
||||
BlockData blockData = repository.getBlockRepository().fromHeight(height);
|
||||
if (blockData == null) {
|
||||
List<BlockInfo> blockInfos = repository.getBlockRepository().getBlockInfos(height, null, 1);
|
||||
if (blockInfos == null || blockInfos.isEmpty()) {
|
||||
sendError(session, ApiError.BLOCK_UNKNOWN);
|
||||
return;
|
||||
}
|
||||
|
||||
onNotify(session, blockData);
|
||||
onNotify(session, blockInfos.get(0));
|
||||
} catch (DataException e) {
|
||||
sendError(session, ApiError.REPOSITORY_ISSUE);
|
||||
}
|
||||
}
|
||||
|
||||
private void onNotify(Session session, BlockData blockData) {
|
||||
private void onNotify(Session session, BlockInfo blockInfo) {
|
||||
StringWriter stringWriter = new StringWriter();
|
||||
|
||||
try {
|
||||
this.marshall(stringWriter, blockData);
|
||||
marshall(stringWriter, blockInfo);
|
||||
|
||||
session.getRemote().sendString(stringWriter.toString());
|
||||
} catch (IOException e) {
|
||||
session.getRemote().sendStringByFuture(stringWriter.toString());
|
||||
} catch (IOException | WebSocketException e) {
|
||||
// No output this time
|
||||
}
|
||||
}
|
||||
|
@@ -8,11 +8,12 @@ import java.util.List;
|
||||
import java.util.Map;
|
||||
|
||||
import org.eclipse.jetty.websocket.api.Session;
|
||||
import org.eclipse.jetty.websocket.api.WebSocketException;
|
||||
import org.eclipse.jetty.websocket.api.annotations.OnWebSocketClose;
|
||||
import org.eclipse.jetty.websocket.api.annotations.OnWebSocketConnect;
|
||||
import org.eclipse.jetty.websocket.api.annotations.OnWebSocketError;
|
||||
import org.eclipse.jetty.websocket.api.annotations.OnWebSocketMessage;
|
||||
import org.eclipse.jetty.websocket.api.annotations.WebSocket;
|
||||
import org.eclipse.jetty.websocket.servlet.WebSocketServlet;
|
||||
import org.eclipse.jetty.websocket.servlet.WebSocketServletFactory;
|
||||
import org.qortal.controller.ChatNotifier;
|
||||
import org.qortal.data.chat.ChatMessage;
|
||||
@@ -23,7 +24,7 @@ import org.qortal.repository.RepositoryManager;
|
||||
|
||||
@WebSocket
|
||||
@SuppressWarnings("serial")
|
||||
public class ChatMessagesWebSocket extends WebSocketServlet implements ApiWebSocket {
|
||||
public class ChatMessagesWebSocket extends ApiWebSocket {
|
||||
|
||||
@Override
|
||||
public void configure(WebSocketServletFactory factory) {
|
||||
@@ -89,6 +90,10 @@ public class ChatMessagesWebSocket extends WebSocketServlet implements ApiWebSoc
|
||||
ChatNotifier.getInstance().deregister(session);
|
||||
}
|
||||
|
||||
@OnWebSocketError
|
||||
public void onWebSocketError(Session session, Throwable throwable) {
|
||||
}
|
||||
|
||||
@OnWebSocketMessage
|
||||
public void onWebSocketMessage(Session session, String message) {
|
||||
}
|
||||
@@ -123,10 +128,10 @@ public class ChatMessagesWebSocket extends WebSocketServlet implements ApiWebSoc
|
||||
StringWriter stringWriter = new StringWriter();
|
||||
|
||||
try {
|
||||
this.marshall(stringWriter, chatMessages);
|
||||
marshall(stringWriter, chatMessages);
|
||||
|
||||
session.getRemote().sendString(stringWriter.toString());
|
||||
} catch (IOException e) {
|
||||
session.getRemote().sendStringByFuture(stringWriter.toString());
|
||||
} catch (IOException | WebSocketException e) {
|
||||
// No output this time?
|
||||
}
|
||||
}
|
||||
|
119
src/main/java/org/qortal/api/websocket/TradeBotWebSocket.java
Normal file
119
src/main/java/org/qortal/api/websocket/TradeBotWebSocket.java
Normal file
@@ -0,0 +1,119 @@
|
||||
package org.qortal.api.websocket;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.io.StringWriter;
|
||||
import java.util.Collections;
|
||||
import java.util.HashMap;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.stream.Collectors;
|
||||
|
||||
import org.eclipse.jetty.websocket.api.Session;
|
||||
import org.eclipse.jetty.websocket.api.annotations.OnWebSocketClose;
|
||||
import org.eclipse.jetty.websocket.api.annotations.OnWebSocketConnect;
|
||||
import org.eclipse.jetty.websocket.api.annotations.OnWebSocketMessage;
|
||||
import org.eclipse.jetty.websocket.api.annotations.WebSocket;
|
||||
import org.eclipse.jetty.websocket.servlet.WebSocketServletFactory;
|
||||
import org.qortal.controller.TradeBot;
|
||||
import org.qortal.data.crosschain.TradeBotData;
|
||||
import org.qortal.event.Event;
|
||||
import org.qortal.event.EventBus;
|
||||
import org.qortal.event.Listener;
|
||||
import org.qortal.repository.DataException;
|
||||
import org.qortal.repository.Repository;
|
||||
import org.qortal.repository.RepositoryManager;
|
||||
import org.qortal.utils.Base58;
|
||||
|
||||
@WebSocket
|
||||
@SuppressWarnings("serial")
|
||||
public class TradeBotWebSocket extends ApiWebSocket implements Listener {
|
||||
|
||||
/** Cache of trade-bot entry states, keyed by trade-bot entry's "trade private key" (base58) */
|
||||
private static final Map<String, TradeBotData.State> PREVIOUS_STATES = new HashMap<>();
|
||||
|
||||
@Override
|
||||
public void configure(WebSocketServletFactory factory) {
|
||||
factory.register(TradeBotWebSocket.class);
|
||||
|
||||
try (final Repository repository = RepositoryManager.getRepository()) {
|
||||
List<TradeBotData> tradeBotEntries = repository.getCrossChainRepository().getAllTradeBotData();
|
||||
if (tradeBotEntries == null)
|
||||
// How do we properly fail here?
|
||||
return;
|
||||
|
||||
PREVIOUS_STATES.putAll(tradeBotEntries.stream().collect(Collectors.toMap(entry -> Base58.encode(entry.getTradePrivateKey()), TradeBotData::getState)));
|
||||
} catch (DataException e) {
|
||||
// No output this time
|
||||
}
|
||||
|
||||
EventBus.INSTANCE.addListener(this::listen);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void listen(Event event) {
|
||||
if (!(event instanceof TradeBot.StateChangeEvent))
|
||||
return;
|
||||
|
||||
TradeBotData tradeBotData = ((TradeBot.StateChangeEvent) event).getTradeBotData();
|
||||
String tradePrivateKey58 = Base58.encode(tradeBotData.getTradePrivateKey());
|
||||
|
||||
synchronized (PREVIOUS_STATES) {
|
||||
if (PREVIOUS_STATES.get(tradePrivateKey58) == tradeBotData.getState())
|
||||
// Not changed
|
||||
return;
|
||||
|
||||
PREVIOUS_STATES.put(tradePrivateKey58, tradeBotData.getState());
|
||||
}
|
||||
|
||||
List<TradeBotData> tradeBotEntries = Collections.singletonList(tradeBotData);
|
||||
for (Session session : getSessions())
|
||||
sendEntries(session, tradeBotEntries);
|
||||
}
|
||||
|
||||
@OnWebSocketConnect
|
||||
public void onWebSocketConnect(Session session) {
|
||||
// Send all known trade-bot entries
|
||||
try (final Repository repository = RepositoryManager.getRepository()) {
|
||||
List<TradeBotData> tradeBotEntries = repository.getCrossChainRepository().getAllTradeBotData();
|
||||
if (tradeBotEntries == null) {
|
||||
session.close(4001, "repository issue fetching trade-bot entries");
|
||||
return;
|
||||
}
|
||||
|
||||
if (!sendEntries(session, tradeBotEntries)) {
|
||||
session.close(4002, "websocket issue");
|
||||
return;
|
||||
}
|
||||
} catch (DataException e) {
|
||||
// No output this time
|
||||
}
|
||||
|
||||
super.onWebSocketConnect(session);
|
||||
}
|
||||
|
||||
@OnWebSocketClose
|
||||
public void onWebSocketClose(Session session, int statusCode, String reason) {
|
||||
super.onWebSocketClose(session, statusCode, reason);
|
||||
}
|
||||
|
||||
@OnWebSocketMessage
|
||||
public void onWebSocketMessage(Session session, String message) {
|
||||
/* ignored */
|
||||
}
|
||||
|
||||
private boolean sendEntries(Session session, List<TradeBotData> tradeBotEntries) {
|
||||
try {
|
||||
StringWriter stringWriter = new StringWriter();
|
||||
marshall(stringWriter, tradeBotEntries);
|
||||
|
||||
String output = stringWriter.toString();
|
||||
session.getRemote().sendStringByFuture(output);
|
||||
} catch (IOException e) {
|
||||
// No output this time?
|
||||
return false;
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
}
|
212
src/main/java/org/qortal/api/websocket/TradeOffersWebSocket.java
Normal file
212
src/main/java/org/qortal/api/websocket/TradeOffersWebSocket.java
Normal file
@@ -0,0 +1,212 @@
|
||||
package org.qortal.api.websocket;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.io.StringWriter;
|
||||
import java.util.ArrayList;
|
||||
import java.util.HashMap;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.stream.Collectors;
|
||||
|
||||
import org.eclipse.jetty.websocket.api.Session;
|
||||
import org.eclipse.jetty.websocket.api.annotations.OnWebSocketClose;
|
||||
import org.eclipse.jetty.websocket.api.annotations.OnWebSocketConnect;
|
||||
import org.eclipse.jetty.websocket.api.annotations.OnWebSocketMessage;
|
||||
import org.eclipse.jetty.websocket.api.annotations.WebSocket;
|
||||
import org.eclipse.jetty.websocket.servlet.WebSocketServletFactory;
|
||||
import org.qortal.api.model.BlockInfo;
|
||||
import org.qortal.api.model.CrossChainOfferSummary;
|
||||
import org.qortal.controller.BlockNotifier;
|
||||
import org.qortal.crosschain.BTCACCT;
|
||||
import org.qortal.data.at.ATStateData;
|
||||
import org.qortal.data.crosschain.CrossChainTradeData;
|
||||
import org.qortal.repository.DataException;
|
||||
import org.qortal.repository.Repository;
|
||||
import org.qortal.repository.RepositoryManager;
|
||||
import org.qortal.utils.NTP;
|
||||
|
||||
@WebSocket
|
||||
@SuppressWarnings("serial")
|
||||
public class TradeOffersWebSocket extends ApiWebSocket {
|
||||
|
||||
@Override
|
||||
public void configure(WebSocketServletFactory factory) {
|
||||
factory.register(TradeOffersWebSocket.class);
|
||||
}
|
||||
|
||||
@OnWebSocketConnect
|
||||
public void onWebSocketConnect(Session session) {
|
||||
Map<String, List<String>> queryParams = session.getUpgradeRequest().getParameterMap();
|
||||
|
||||
final boolean includeHistoric = queryParams.get("includeHistoric") != null;
|
||||
final Map<String, BTCACCT.Mode> previousAtModes = new HashMap<>();
|
||||
List<CrossChainOfferSummary> crossChainOfferSummaries;
|
||||
|
||||
try (final Repository repository = RepositoryManager.getRepository()) {
|
||||
List<ATStateData> initialAtStates;
|
||||
|
||||
// We want ALL OFFERING trades
|
||||
Boolean isFinished = Boolean.FALSE;
|
||||
Integer dataByteOffset = BTCACCT.MODE_BYTE_OFFSET;
|
||||
Long expectedValue = (long) BTCACCT.Mode.OFFERING.value;
|
||||
Integer minimumFinalHeight = null;
|
||||
|
||||
initialAtStates = repository.getATRepository().getMatchingFinalATStates(BTCACCT.CODE_BYTES_HASH,
|
||||
isFinished, dataByteOffset, expectedValue, minimumFinalHeight,
|
||||
null, null, null);
|
||||
|
||||
if (initialAtStates == null) {
|
||||
session.close(4001, "repository issue fetching OFFERING trades");
|
||||
return;
|
||||
}
|
||||
|
||||
// Save initial AT modes
|
||||
previousAtModes.putAll(initialAtStates.stream().collect(Collectors.toMap(ATStateData::getATAddress, atState -> BTCACCT.Mode.OFFERING)));
|
||||
|
||||
// Convert to offer summaries
|
||||
crossChainOfferSummaries = produceSummaries(repository, initialAtStates, null);
|
||||
|
||||
if (includeHistoric) {
|
||||
// We also want REDEEMED/REFUNDED/CANCELLED trades over the last 24 hours
|
||||
long timestamp = NTP.getTime() - 24 * 60 * 60 * 1000L;
|
||||
minimumFinalHeight = repository.getBlockRepository().getHeightFromTimestamp(timestamp);
|
||||
|
||||
if (minimumFinalHeight != 0) {
|
||||
isFinished = Boolean.TRUE;
|
||||
dataByteOffset = null;
|
||||
expectedValue = null;
|
||||
++minimumFinalHeight; // because height is just *before* timestamp
|
||||
|
||||
List<ATStateData> historicAtStates = repository.getATRepository().getMatchingFinalATStates(BTCACCT.CODE_BYTES_HASH,
|
||||
isFinished, dataByteOffset, expectedValue, minimumFinalHeight,
|
||||
null, null, null);
|
||||
|
||||
if (historicAtStates == null) {
|
||||
session.close(4002, "repository issue fetching historic trades");
|
||||
return;
|
||||
}
|
||||
|
||||
for (ATStateData historicAtState : historicAtStates) {
|
||||
CrossChainOfferSummary historicOfferSummary = produceSummary(repository, historicAtState, null);
|
||||
|
||||
switch (historicOfferSummary.getMode()) {
|
||||
case REDEEMED:
|
||||
case REFUNDED:
|
||||
case CANCELLED:
|
||||
break;
|
||||
|
||||
default:
|
||||
continue;
|
||||
}
|
||||
|
||||
// Add summary to initial burst
|
||||
crossChainOfferSummaries.add(historicOfferSummary);
|
||||
|
||||
// Save initial AT mode
|
||||
previousAtModes.put(historicAtState.getATAddress(), historicOfferSummary.getMode());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
} catch (DataException e) {
|
||||
session.close(4003, "generic repository issue");
|
||||
return;
|
||||
}
|
||||
|
||||
if (!sendOfferSummaries(session, crossChainOfferSummaries)) {
|
||||
session.close(4004, "websocket issue");
|
||||
return;
|
||||
}
|
||||
|
||||
BlockNotifier.Listener listener = blockInfo -> onNotify(session, blockInfo, previousAtModes);
|
||||
BlockNotifier.getInstance().register(session, listener);
|
||||
}
|
||||
|
||||
@OnWebSocketClose
|
||||
public void onWebSocketClose(Session session, int statusCode, String reason) {
|
||||
BlockNotifier.getInstance().deregister(session);
|
||||
}
|
||||
|
||||
@OnWebSocketMessage
|
||||
public void onWebSocketMessage(Session session, String message) {
|
||||
/* ignored */
|
||||
}
|
||||
|
||||
private void onNotify(Session session, BlockInfo blockInfo, final Map<String, BTCACCT.Mode> previousAtModes) {
|
||||
List<CrossChainOfferSummary> crossChainOfferSummaries = null;
|
||||
|
||||
try (final Repository repository = RepositoryManager.getRepository()) {
|
||||
// Find any new trade ATs since this block
|
||||
final Boolean isFinished = null;
|
||||
final Integer dataByteOffset = null;
|
||||
final Long expectedValue = null;
|
||||
final Integer minimumFinalHeight = blockInfo.getHeight();
|
||||
|
||||
List<ATStateData> atStates = repository.getATRepository().getMatchingFinalATStates(BTCACCT.CODE_BYTES_HASH,
|
||||
isFinished, dataByteOffset, expectedValue, minimumFinalHeight,
|
||||
null, null, null);
|
||||
|
||||
if (atStates == null)
|
||||
return;
|
||||
|
||||
crossChainOfferSummaries = produceSummaries(repository, atStates, blockInfo.getTimestamp());
|
||||
} catch (DataException e) {
|
||||
// No output this time
|
||||
}
|
||||
|
||||
synchronized (previousAtModes) { //NOSONAR squid:S2445 suppressed because previousAtModes is final and curried in lambda
|
||||
// Remove any entries unchanged from last time
|
||||
crossChainOfferSummaries.removeIf(offerSummary -> previousAtModes.get(offerSummary.getQortalAtAddress()) == offerSummary.getMode());
|
||||
|
||||
// Don't send anything if no results
|
||||
if (crossChainOfferSummaries.isEmpty())
|
||||
return;
|
||||
|
||||
final boolean wasSent = sendOfferSummaries(session, crossChainOfferSummaries);
|
||||
|
||||
if (!wasSent)
|
||||
return;
|
||||
|
||||
previousAtModes.putAll(crossChainOfferSummaries.stream().collect(Collectors.toMap(CrossChainOfferSummary::getQortalAtAddress, CrossChainOfferSummary::getMode)));
|
||||
}
|
||||
}
|
||||
|
||||
private boolean sendOfferSummaries(Session session, List<CrossChainOfferSummary> crossChainOfferSummaries) {
|
||||
try {
|
||||
StringWriter stringWriter = new StringWriter();
|
||||
marshall(stringWriter, crossChainOfferSummaries);
|
||||
|
||||
String output = stringWriter.toString();
|
||||
session.getRemote().sendStringByFuture(output);
|
||||
} catch (IOException e) {
|
||||
// No output this time?
|
||||
return false;
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
private static CrossChainOfferSummary produceSummary(Repository repository, ATStateData atState, Long timestamp) throws DataException {
|
||||
CrossChainTradeData crossChainTradeData = BTCACCT.populateTradeData(repository, atState);
|
||||
|
||||
long atStateTimestamp;
|
||||
|
||||
if (crossChainTradeData.mode == BTCACCT.Mode.OFFERING)
|
||||
// We want when trade was created, not when it was last updated
|
||||
atStateTimestamp = atState.getCreation();
|
||||
else
|
||||
atStateTimestamp = timestamp != null ? timestamp : repository.getBlockRepository().getTimestampFromHeight(atState.getHeight());
|
||||
|
||||
return new CrossChainOfferSummary(crossChainTradeData, atStateTimestamp);
|
||||
}
|
||||
|
||||
private static List<CrossChainOfferSummary> produceSummaries(Repository repository, List<ATStateData> atStates, Long timestamp) throws DataException {
|
||||
List<CrossChainOfferSummary> offerSummaries = new ArrayList<>();
|
||||
|
||||
for (ATStateData atState : atStates)
|
||||
offerSummaries.add(produceSummary(repository, atState, timestamp));
|
||||
|
||||
return offerSummaries;
|
||||
}
|
||||
|
||||
}
|
@@ -117,12 +117,8 @@ public class AT {
|
||||
}
|
||||
|
||||
public void update(int blockHeight, long blockTimestamp) throws DataException {
|
||||
// [Re]create AT machine state using AT state data or from scratch as applicable
|
||||
QortalATAPI api = new QortalATAPI(repository, this.atData, blockTimestamp);
|
||||
QortalAtLoggerFactory loggerFactory = QortalAtLoggerFactory.getInstance();
|
||||
|
||||
byte[] codeBytes = this.atData.getCodeBytes();
|
||||
MachineState state = MachineState.fromBytes(api, loggerFactory, this.atStateData.getStateData(), codeBytes);
|
||||
// Extract minimal/flags-only AT machine state using AT state data
|
||||
MachineState state = MachineState.flagsOnlyfromBytes(this.atStateData.getStateData());
|
||||
|
||||
// Save latest AT state data
|
||||
this.repository.getATRepository().save(this.atStateData);
|
||||
@@ -151,12 +147,8 @@ public class AT {
|
||||
if (previousStateData == null)
|
||||
throw new DataException("Can't find previous AT state data for " + atAddress);
|
||||
|
||||
// [Re]create AT machine state using AT state data or from scratch as applicable
|
||||
QortalATAPI api = new QortalATAPI(repository, this.atData, blockTimestamp);
|
||||
QortalAtLoggerFactory loggerFactory = QortalAtLoggerFactory.getInstance();
|
||||
|
||||
byte[] codeBytes = this.atData.getCodeBytes();
|
||||
MachineState state = MachineState.fromBytes(api, loggerFactory, previousStateData.getStateData(), codeBytes);
|
||||
// Extract minimal/flags-only AT machine state using AT state data
|
||||
MachineState state = MachineState.flagsOnlyfromBytes(previousStateData.getStateData());
|
||||
|
||||
// Update AT info in repository
|
||||
this.atData.setIsSleeping(state.isSleeping());
|
||||
|
@@ -17,7 +17,6 @@ import org.qortal.account.Account;
|
||||
import org.qortal.account.NullAccount;
|
||||
import org.qortal.account.PublicKeyAccount;
|
||||
import org.qortal.asset.Asset;
|
||||
import org.qortal.block.Block;
|
||||
import org.qortal.block.BlockChain;
|
||||
import org.qortal.block.BlockChain.CiyamAtSettings;
|
||||
import org.qortal.crypto.Crypto;
|
||||
@@ -30,13 +29,13 @@ import org.qortal.data.transaction.MessageTransactionData;
|
||||
import org.qortal.data.transaction.PaymentTransactionData;
|
||||
import org.qortal.data.transaction.TransactionData;
|
||||
import org.qortal.group.Group;
|
||||
import org.qortal.repository.BlockRepository;
|
||||
import org.qortal.repository.ATRepository;
|
||||
import org.qortal.repository.DataException;
|
||||
import org.qortal.repository.Repository;
|
||||
import org.qortal.transaction.AtTransaction;
|
||||
import org.qortal.transaction.Transaction;
|
||||
import org.qortal.transaction.Transaction.TransactionType;
|
||||
import org.qortal.utils.Base58;
|
||||
import org.qortal.utils.BitTwiddling;
|
||||
|
||||
import com.google.common.primitives.Bytes;
|
||||
|
||||
@@ -133,9 +132,9 @@ public class QortalATAPI extends API {
|
||||
|
||||
byte[] signature = blockSummaries.get(0).getSignature();
|
||||
// Save some of minter's signature and transactions signature, so middle 24 bytes of the full 128 byte signature.
|
||||
this.setA2(state, fromBytes(signature, 52));
|
||||
this.setA3(state, fromBytes(signature, 60));
|
||||
this.setA4(state, fromBytes(signature, 68));
|
||||
this.setA2(state, BitTwiddling.longFromBEBytes(signature, 52));
|
||||
this.setA3(state, BitTwiddling.longFromBEBytes(signature, 60));
|
||||
this.setA4(state, BitTwiddling.longFromBEBytes(signature, 68));
|
||||
} catch (DataException e) {
|
||||
throw new RuntimeException("AT API unable to fetch previous block?", e);
|
||||
}
|
||||
@@ -149,59 +148,27 @@ public class QortalATAPI extends API {
|
||||
int height = timestamp.blockHeight;
|
||||
int sequence = timestamp.transactionSequence + 1;
|
||||
|
||||
BlockRepository blockRepository = this.getRepository().getBlockRepository();
|
||||
|
||||
ATRepository.NextTransactionInfo nextTransactionInfo;
|
||||
try {
|
||||
int currentHeight = blockRepository.getBlockchainHeight();
|
||||
List<Transaction> blockTransactions = null;
|
||||
|
||||
while (height <= currentHeight) {
|
||||
if (blockTransactions == null) {
|
||||
BlockData blockData = blockRepository.fromHeight(height);
|
||||
|
||||
if (blockData == null)
|
||||
throw new DataException("Unable to fetch block " + height + " from repository?");
|
||||
|
||||
Block block = new Block(this.getRepository(), blockData);
|
||||
|
||||
blockTransactions = block.getTransactions();
|
||||
}
|
||||
|
||||
// No more transactions in this block? Try next block
|
||||
if (sequence >= blockTransactions.size()) {
|
||||
++height;
|
||||
sequence = 0;
|
||||
blockTransactions = null;
|
||||
continue;
|
||||
}
|
||||
|
||||
Transaction transaction = blockTransactions.get(sequence);
|
||||
|
||||
// Transaction needs to be sent to specified recipient
|
||||
List<String> recipientAddresses = transaction.getRecipientAddresses();
|
||||
if (recipientAddresses.contains(atAddress)) {
|
||||
// Found a transaction
|
||||
|
||||
this.setA1(state, new Timestamp(height, timestamp.blockchainId, sequence).longValue());
|
||||
|
||||
// Copy transaction's partial signature into the other three A fields for future verification that it's the same transaction
|
||||
byte[] signature = transaction.getTransactionData().getSignature();
|
||||
this.setA2(state, fromBytes(signature, 8));
|
||||
this.setA3(state, fromBytes(signature, 16));
|
||||
this.setA4(state, fromBytes(signature, 24));
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
// Transaction wasn't for us - keep going
|
||||
++sequence;
|
||||
}
|
||||
|
||||
// No more transactions - zero A and exit
|
||||
this.zeroA(state);
|
||||
nextTransactionInfo = this.getRepository().getATRepository().findNextTransaction(atAddress, height, sequence);
|
||||
} catch (DataException e) {
|
||||
throw new RuntimeException("AT API unable to fetch next transaction?", e);
|
||||
}
|
||||
|
||||
if (nextTransactionInfo == null) {
|
||||
// No more transactions for AT at this time - zero A and exit
|
||||
this.zeroA(state);
|
||||
return;
|
||||
}
|
||||
|
||||
// Found a transaction
|
||||
|
||||
this.setA1(state, new Timestamp(nextTransactionInfo.height, timestamp.blockchainId, nextTransactionInfo.sequence).longValue());
|
||||
|
||||
// Copy transaction's partial signature into the other three A fields for future verification that it's the same transaction
|
||||
this.setA2(state, BitTwiddling.longFromBEBytes(nextTransactionInfo.signature, 8));
|
||||
this.setA3(state, BitTwiddling.longFromBEBytes(nextTransactionInfo.signature, 16));
|
||||
this.setA4(state, BitTwiddling.longFromBEBytes(nextTransactionInfo.signature, 24));
|
||||
}
|
||||
|
||||
@Override
|
||||
@@ -282,7 +249,7 @@ public class QortalATAPI extends API {
|
||||
|
||||
byte[] hash = Crypto.digest(input);
|
||||
|
||||
return fromBytes(hash, 0);
|
||||
return BitTwiddling.longFromBEBytes(hash, 0);
|
||||
} catch (DataException e) {
|
||||
throw new RuntimeException("AT API unable to fetch latest block from repository?", e);
|
||||
}
|
||||
@@ -296,30 +263,14 @@ public class QortalATAPI extends API {
|
||||
|
||||
TransactionData transactionData = this.getTransactionFromA(state);
|
||||
|
||||
byte[] messageData = null;
|
||||
|
||||
switch (transactionData.getType()) {
|
||||
case MESSAGE:
|
||||
messageData = ((MessageTransactionData) transactionData).getData();
|
||||
break;
|
||||
|
||||
case AT:
|
||||
messageData = ((ATTransactionData) transactionData).getMessage();
|
||||
break;
|
||||
|
||||
default:
|
||||
return;
|
||||
}
|
||||
|
||||
// Check data length is appropriate, i.e. not larger than B
|
||||
if (messageData.length > 4 * 8)
|
||||
return;
|
||||
byte[] messageData = this.getMessageFromTransaction(transactionData);
|
||||
|
||||
// Pad messageData to fit B
|
||||
byte[] paddedMessageData = Bytes.ensureCapacity(messageData, 4 * 8, 0);
|
||||
if (messageData.length < 4 * 8)
|
||||
messageData = Bytes.ensureCapacity(messageData, 4 * 8, 0);
|
||||
|
||||
// Endian must be correct here so that (for example) a SHA256 message can be compared to one generated locally
|
||||
this.setB(state, paddedMessageData);
|
||||
this.setB(state, messageData);
|
||||
}
|
||||
|
||||
@Override
|
||||
@@ -457,12 +408,6 @@ public class QortalATAPI extends API {
|
||||
|
||||
// Utility methods
|
||||
|
||||
/** Convert part of little-endian byte[] to long */
|
||||
/* package */ static long fromBytes(byte[] bytes, int start) {
|
||||
return (bytes[start] & 0xffL) | (bytes[start + 1] & 0xffL) << 8 | (bytes[start + 2] & 0xffL) << 16 | (bytes[start + 3] & 0xffL) << 24
|
||||
| (bytes[start + 4] & 0xffL) << 32 | (bytes[start + 5] & 0xffL) << 40 | (bytes[start + 6] & 0xffL) << 48 | (bytes[start + 7] & 0xffL) << 56;
|
||||
}
|
||||
|
||||
/** Returns partial transaction signature, used to verify we're operating on the same transaction and not naively using block height & sequence. */
|
||||
public static byte[] partialSignature(byte[] fullSignature) {
|
||||
return Arrays.copyOfRange(fullSignature, 8, 32);
|
||||
@@ -473,7 +418,7 @@ public class QortalATAPI extends API {
|
||||
// Compare end of transaction's signature against A2 thru A4
|
||||
byte[] sig = transactionData.getSignature();
|
||||
|
||||
if (this.getA2(state) != fromBytes(sig, 8) || this.getA3(state) != fromBytes(sig, 16) || this.getA4(state) != fromBytes(sig, 24))
|
||||
if (this.getA2(state) != BitTwiddling.longFromBEBytes(sig, 8) || this.getA3(state) != BitTwiddling.longFromBEBytes(sig, 16) || this.getA4(state) != BitTwiddling.longFromBEBytes(sig, 24))
|
||||
throw new IllegalStateException("Transaction signature in A no longer matches signature from repository");
|
||||
}
|
||||
|
||||
@@ -497,6 +442,20 @@ public class QortalATAPI extends API {
|
||||
}
|
||||
}
|
||||
|
||||
/** Returns message data from transaction. */
|
||||
/*package*/ byte[] getMessageFromTransaction(TransactionData transactionData) {
|
||||
switch (transactionData.getType()) {
|
||||
case MESSAGE:
|
||||
return ((MessageTransactionData) transactionData).getData();
|
||||
|
||||
case AT:
|
||||
return ((ATTransactionData) transactionData).getMessage();
|
||||
|
||||
default:
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
/** Returns AT's account */
|
||||
/* package */ Account getATAccount() {
|
||||
return new Account(this.repository, this.atData.getATAddress());
|
||||
@@ -563,4 +522,8 @@ public class QortalATAPI extends API {
|
||||
super.setB(state, bBytes);
|
||||
}
|
||||
|
||||
protected void zeroB(MachineState state) {
|
||||
super.zeroB(state);
|
||||
}
|
||||
|
||||
}
|
||||
|
@@ -691,6 +691,11 @@ public final class QortalAtLogger extends ExtendedLoggerWrapper implements org.c
|
||||
logger.logIfEnabled(FQCN, ERROR, null, msgSupplier, (Throwable) null);
|
||||
}
|
||||
|
||||
/** Java 8 version */
|
||||
public void error(final java.util.function.Supplier<String> msgSupplier) {
|
||||
logger.logIfEnabled(FQCN, ERROR, null, () -> msgSupplier.get(), (Throwable) null);
|
||||
}
|
||||
|
||||
/**
|
||||
* Logs a message (only to be constructed if the logging level is the {@code ERROR}
|
||||
* level) including the stack trace of the {@link Throwable} <code>t</code> passed as parameter.
|
||||
@@ -1375,6 +1380,11 @@ public final class QortalAtLogger extends ExtendedLoggerWrapper implements org.c
|
||||
logger.logIfEnabled(FQCN, DEBUG, null, msgSupplier, (Throwable) null);
|
||||
}
|
||||
|
||||
/** Java 8 version */
|
||||
public void debug(final java.util.function.Supplier<String> msgSupplier) {
|
||||
logger.logIfEnabled(FQCN, DEBUG, null, () -> msgSupplier.get(), (Throwable) null);
|
||||
}
|
||||
|
||||
/**
|
||||
* Logs a message (only to be constructed if the logging level is the {@code DEBUG}
|
||||
* level) including the stack trace of the {@link Throwable} <code>t</code> passed as parameter.
|
||||
@@ -2059,6 +2069,11 @@ public final class QortalAtLogger extends ExtendedLoggerWrapper implements org.c
|
||||
logger.logIfEnabled(FQCN, ECHO, null, msgSupplier, (Throwable) null);
|
||||
}
|
||||
|
||||
/** Java 8 version */
|
||||
public void echo(final java.util.function.Supplier<String> msgSupplier) {
|
||||
logger.logIfEnabled(FQCN, ECHO, null, () -> msgSupplier.get(), (Throwable) null);
|
||||
}
|
||||
|
||||
/**
|
||||
* Logs a message (only to be constructed if the logging level is the {@code ECHO}
|
||||
* level) including the stack trace of the {@link Throwable} <code>t</code> passed as parameter.
|
||||
|
@@ -12,6 +12,7 @@ import org.ciyam.at.IllegalFunctionCodeException;
|
||||
import org.ciyam.at.MachineState;
|
||||
import org.qortal.crosschain.BTC;
|
||||
import org.qortal.crypto.Crypto;
|
||||
import org.qortal.data.transaction.TransactionData;
|
||||
import org.qortal.settings.Settings;
|
||||
|
||||
/**
|
||||
@@ -22,8 +23,70 @@ import org.qortal.settings.Settings;
|
||||
*/
|
||||
public enum QortalFunctionCode {
|
||||
/**
|
||||
* <tt>0x0510</tt><br>
|
||||
* Convert address in B to 20-byte value in LSB of B1, and all of B2 & B3.
|
||||
* Returns length of message data from transaction in A.<br>
|
||||
* <tt>0x0501</tt><br>
|
||||
* If transaction has no 'message', returns -1.
|
||||
*/
|
||||
GET_MESSAGE_LENGTH_FROM_TX_IN_A(0x0501, 0, true) {
|
||||
@Override
|
||||
protected void postCheckExecute(FunctionData functionData, MachineState state, short rawFunctionCode) throws ExecutionException {
|
||||
QortalATAPI api = (QortalATAPI) state.getAPI();
|
||||
|
||||
TransactionData transactionData = api.getTransactionFromA(state);
|
||||
|
||||
byte[] messageData = api.getMessageFromTransaction(transactionData);
|
||||
|
||||
if (messageData == null)
|
||||
functionData.returnValue = -1L;
|
||||
else
|
||||
functionData.returnValue = (long) messageData.length;
|
||||
}
|
||||
},
|
||||
/**
|
||||
* Put offset 'message' from transaction in A into B<br>
|
||||
* <tt>0x0502 start-offset</tt><br>
|
||||
* Copies up to 32 bytes of message data, starting at <tt>start-offset</tt> into B.<br>
|
||||
* If transaction has no 'message', or <tt>start-offset</tt> out of bounds, then zero B<br>
|
||||
* Example 'message' could be 256-bit shared secret
|
||||
*/
|
||||
PUT_PARTIAL_MESSAGE_FROM_TX_IN_A_INTO_B(0x0502, 1, false) {
|
||||
@Override
|
||||
protected void postCheckExecute(FunctionData functionData, MachineState state, short rawFunctionCode) throws ExecutionException {
|
||||
QortalATAPI api = (QortalATAPI) state.getAPI();
|
||||
|
||||
// In case something goes wrong, or we don't have enough message data.
|
||||
api.zeroB(state);
|
||||
|
||||
if (functionData.value1 < 0 || functionData.value1 > Integer.MAX_VALUE)
|
||||
return;
|
||||
|
||||
int startOffset = functionData.value1.intValue();
|
||||
|
||||
TransactionData transactionData = api.getTransactionFromA(state);
|
||||
|
||||
byte[] messageData = api.getMessageFromTransaction(transactionData);
|
||||
|
||||
if (messageData == null || startOffset > messageData.length)
|
||||
return;
|
||||
|
||||
/*
|
||||
* Copy up to 32 bytes of message data into B,
|
||||
* retain order but pad with zeros in lower bytes.
|
||||
*
|
||||
* So a 4-byte message "a b c d" would copy thusly:
|
||||
* a b c d 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0
|
||||
*/
|
||||
int byteCount = Math.min(32, messageData.length - startOffset);
|
||||
byte[] bBytes = new byte[32];
|
||||
|
||||
System.arraycopy(messageData, startOffset, bBytes, 0, byteCount);
|
||||
|
||||
api.setB(state, bBytes);
|
||||
}
|
||||
},
|
||||
/**
|
||||
* Convert address in B to 20-byte value in LSB of B1, and all of B2 & B3.<br>
|
||||
* <tt>0x0510</tt>
|
||||
*/
|
||||
CONVERT_B_TO_PKH(0x0510, 0, false) {
|
||||
@Override
|
||||
@@ -38,8 +101,8 @@ public enum QortalFunctionCode {
|
||||
}
|
||||
},
|
||||
/**
|
||||
* <tt>0x0511</tt><br>
|
||||
* Convert 20-byte value in LSB of B1, and all of B2 & B3 to P2SH.<br>
|
||||
* <tt>0x0511</tt><br>
|
||||
* P2SH stored in lower 25 bytes of B.
|
||||
*/
|
||||
CONVERT_B_TO_P2SH(0x0511, 0, false) {
|
||||
@@ -51,8 +114,8 @@ public enum QortalFunctionCode {
|
||||
}
|
||||
},
|
||||
/**
|
||||
* <tt>0x0512</tt><br>
|
||||
* Convert 20-byte value in LSB of B1, and all of B2 & B3 to Qortal address.<br>
|
||||
* <tt>0x0512</tt><br>
|
||||
* Qortal address stored in lower 25 bytes of B.
|
||||
*/
|
||||
CONVERT_B_TO_QORTAL(0x0512, 0, false) {
|
||||
|
@@ -215,6 +215,9 @@ public class Block {
|
||||
/** Always use getExpandedAccounts() to access this, as it's lazy-instantiated. */
|
||||
private List<ExpandedAccount> cachedExpandedAccounts = null;
|
||||
|
||||
/** Opportunistic cache of this block's valid online accounts. Only created by call to isValid(). */
|
||||
private List<OnlineAccountData> cachedValidOnlineAccounts = null;
|
||||
|
||||
// Other useful constants
|
||||
|
||||
private static final BigInteger MAX_DISTANCE;
|
||||
@@ -940,24 +943,46 @@ public class Block {
|
||||
return ValidationResult.ONLINE_ACCOUNT_SIGNATURES_MALFORMED;
|
||||
|
||||
// Check signatures
|
||||
List<byte[]> onlineAccountsSignatures = BlockTransformer.decodeTimestampSignatures(this.blockData.getOnlineAccountsSignatures());
|
||||
long onlineTimestamp = this.blockData.getOnlineAccountsTimestamp();
|
||||
byte[] onlineTimestampBytes = Longs.toByteArray(onlineTimestamp);
|
||||
List<OnlineAccountData> onlineAccounts = Controller.getInstance().getOnlineAccounts();
|
||||
|
||||
// If this block is much older than current online timestamp, then there's no point checking current online accounts
|
||||
List<OnlineAccountData> currentOnlineAccounts = onlineTimestamp < NTP.getTime() - Controller.ONLINE_TIMESTAMP_MODULUS
|
||||
? null
|
||||
: Controller.getInstance().getOnlineAccounts();
|
||||
List<OnlineAccountData> latestBlocksOnlineAccounts = Controller.getInstance().getLatestBlocksOnlineAccounts();
|
||||
|
||||
// Extract online accounts' timestamp signatures from block data
|
||||
List<byte[]> onlineAccountsSignatures = BlockTransformer.decodeTimestampSignatures(this.blockData.getOnlineAccountsSignatures());
|
||||
|
||||
// We'll build up a list of online accounts to hand over to Controller if block is added to chain
|
||||
// and this will become latestBlocksOnlineAccounts (above) to reduce CPU load when we process next block...
|
||||
List<OnlineAccountData> ourOnlineAccounts = new ArrayList<>();
|
||||
|
||||
for (int i = 0; i < onlineAccountsSignatures.size(); ++i) {
|
||||
byte[] signature = onlineAccountsSignatures.get(i);
|
||||
byte[] publicKey = expandedAccounts.get(i).getRewardSharePublicKey();
|
||||
|
||||
// If signature is still current then no need to perform Ed25519 verify
|
||||
OnlineAccountData onlineAccountData = new OnlineAccountData(onlineTimestamp, signature, publicKey);
|
||||
if (onlineAccounts.remove(onlineAccountData)) // remove() is like contains() but also reduces the number to check next time
|
||||
ourOnlineAccounts.add(onlineAccountData);
|
||||
|
||||
// If signature is still current then no need to perform Ed25519 verify
|
||||
if (currentOnlineAccounts != null && currentOnlineAccounts.remove(onlineAccountData))
|
||||
// remove() returned true, so online account still current
|
||||
// and one less entry in currentOnlineAccounts to check next time
|
||||
continue;
|
||||
|
||||
// If signature was okay in latest block then no need to perform Ed25519 verify
|
||||
if (latestBlocksOnlineAccounts != null && latestBlocksOnlineAccounts.contains(onlineAccountData))
|
||||
continue;
|
||||
|
||||
if (!Crypto.verify(publicKey, signature, onlineTimestampBytes))
|
||||
return ValidationResult.ONLINE_ACCOUNT_SIGNATURE_INCORRECT;
|
||||
}
|
||||
|
||||
// All online accounts valid, so save our list of online accounts for potential later use
|
||||
this.cachedValidOnlineAccounts = ourOnlineAccounts;
|
||||
|
||||
return ValidationResult.OK;
|
||||
}
|
||||
|
||||
@@ -1271,6 +1296,9 @@ public class Block {
|
||||
linkTransactionsToBlock();
|
||||
|
||||
postBlockTidy();
|
||||
|
||||
// Give Controller our cached, valid online accounts data (if any) to help reduce CPU load for next block
|
||||
Controller.getInstance().pushLatestBlocksOnlineAccounts(this.cachedValidOnlineAccounts);
|
||||
}
|
||||
|
||||
protected void increaseAccountLevels() throws DataException {
|
||||
@@ -1474,6 +1502,9 @@ public class Block {
|
||||
this.blockData.setHeight(null);
|
||||
|
||||
postBlockTidy();
|
||||
|
||||
// Remove any cached, valid online accounts data from Controller
|
||||
Controller.getInstance().popLatestBlocksOnlineAccounts();
|
||||
}
|
||||
|
||||
protected void orphanTransactionsFromBlock() throws DataException {
|
||||
|
@@ -482,7 +482,7 @@ public class BlockChain {
|
||||
}
|
||||
|
||||
/**
|
||||
* Some sort start-up/initialization/checking method.
|
||||
* Some sort of start-up/initialization/checking method.
|
||||
*
|
||||
* @throws SQLException
|
||||
*/
|
||||
@@ -492,7 +492,9 @@ public class BlockChain {
|
||||
rebuildBlockchain();
|
||||
|
||||
try (final Repository repository = RepositoryManager.getRepository()) {
|
||||
BlockData detachedBlockData = repository.getBlockRepository().getDetachedBlockSignature();
|
||||
int startHeight = Math.max(repository.getBlockRepository().getBlockchainHeight() - 1440, 1);
|
||||
|
||||
BlockData detachedBlockData = repository.getBlockRepository().getDetachedBlockSignature(startHeight);
|
||||
|
||||
if (detachedBlockData != null) {
|
||||
LOGGER.error(String.format("Block %d's reference does not match any block's signature", detachedBlockData.getHeight()));
|
||||
|
@@ -214,8 +214,9 @@ public class AutoUpdate extends Thread {
|
||||
return false; // failed - try another repo
|
||||
}
|
||||
|
||||
// Give repository a chance to backup in case things go badly wrong
|
||||
RepositoryManager.backup(true);
|
||||
// Give repository a chance to backup in case things go badly wrong (if enabled)
|
||||
if (Settings.getInstance().getRepositoryBackupInterval() > 0)
|
||||
RepositoryManager.backup(true);
|
||||
|
||||
// Call ApplyUpdate to end this process (unlocking current JAR so it can be replaced)
|
||||
String javaHome = System.getProperty("java.home");
|
||||
|
@@ -1,9 +1,12 @@
|
||||
package org.qortal.controller;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.Collection;
|
||||
import java.util.HashMap;
|
||||
import java.util.Map;
|
||||
|
||||
import org.eclipse.jetty.websocket.api.Session;
|
||||
import org.qortal.api.model.BlockInfo;
|
||||
import org.qortal.data.block.BlockData;
|
||||
|
||||
public class BlockNotifier {
|
||||
@@ -12,7 +15,7 @@ public class BlockNotifier {
|
||||
|
||||
@FunctionalInterface
|
||||
public interface Listener {
|
||||
void notify(BlockData blockData);
|
||||
void notify(BlockInfo blockInfo);
|
||||
}
|
||||
|
||||
private Map<Session, Listener> listenersBySession = new HashMap<>();
|
||||
@@ -27,17 +30,32 @@ public class BlockNotifier {
|
||||
return instance;
|
||||
}
|
||||
|
||||
public synchronized void register(Session session, Listener listener) {
|
||||
this.listenersBySession.put(session, listener);
|
||||
public void register(Session session, Listener listener) {
|
||||
synchronized (this.listenersBySession) {
|
||||
this.listenersBySession.put(session, listener);
|
||||
}
|
||||
}
|
||||
|
||||
public synchronized void deregister(Session session) {
|
||||
this.listenersBySession.remove(session);
|
||||
public void deregister(Session session) {
|
||||
synchronized (this.listenersBySession) {
|
||||
this.listenersBySession.remove(session);
|
||||
}
|
||||
}
|
||||
|
||||
public synchronized void onNewBlock(BlockData blockData) {
|
||||
for (Listener listener : this.listenersBySession.values())
|
||||
listener.notify(blockData);
|
||||
public void onNewBlock(BlockData blockData) {
|
||||
// Convert BlockData to BlockInfo
|
||||
BlockInfo blockInfo = new BlockInfo(blockData);
|
||||
|
||||
for (Listener listener : getAllListeners())
|
||||
listener.notify(blockInfo);
|
||||
}
|
||||
|
||||
private Collection<Listener> getAllListeners() {
|
||||
// Make a copy of listeners to both avoid concurrent modification
|
||||
// and reduce synchronization time
|
||||
synchronized (this.listenersBySession) {
|
||||
return new ArrayList<>(this.listenersBySession.values());
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
@@ -1,5 +1,7 @@
|
||||
package org.qortal.controller;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.Collection;
|
||||
import java.util.HashMap;
|
||||
import java.util.Map;
|
||||
|
||||
@@ -27,22 +29,34 @@ public class ChatNotifier {
|
||||
return instance;
|
||||
}
|
||||
|
||||
public synchronized void register(Session session, Listener listener) {
|
||||
this.listenersBySession.put(session, listener);
|
||||
public void register(Session session, Listener listener) {
|
||||
synchronized (this.listenersBySession) {
|
||||
this.listenersBySession.put(session, listener);
|
||||
}
|
||||
}
|
||||
|
||||
public synchronized void deregister(Session session) {
|
||||
this.listenersBySession.remove(session);
|
||||
public void deregister(Session session) {
|
||||
synchronized (this.listenersBySession) {
|
||||
this.listenersBySession.remove(session);
|
||||
}
|
||||
}
|
||||
|
||||
public synchronized void onNewChatTransaction(ChatTransactionData chatTransactionData) {
|
||||
for (Listener listener : this.listenersBySession.values())
|
||||
public void onNewChatTransaction(ChatTransactionData chatTransactionData) {
|
||||
for (Listener listener : getAllListeners())
|
||||
listener.notify(chatTransactionData);
|
||||
}
|
||||
|
||||
public synchronized void onGroupMembershipChange() {
|
||||
for (Listener listener : this.listenersBySession.values())
|
||||
public void onGroupMembershipChange() {
|
||||
for (Listener listener : getAllListeners())
|
||||
listener.notify(null);
|
||||
}
|
||||
|
||||
private Collection<Listener> getAllListeners() {
|
||||
// Make a copy of listeners to both avoid concurrent modification
|
||||
// and reduce synchronization time
|
||||
synchronized (this.listenersBySession) {
|
||||
return new ArrayList<>(this.listenersBySession.values());
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
@@ -9,9 +9,11 @@ import java.security.Security;
|
||||
import java.time.LocalDateTime;
|
||||
import java.time.ZoneOffset;
|
||||
import java.time.format.DateTimeFormatter;
|
||||
import java.util.ArrayDeque;
|
||||
import java.util.ArrayList;
|
||||
import java.util.Arrays;
|
||||
import java.util.Collections;
|
||||
import java.util.Deque;
|
||||
import java.util.HashMap;
|
||||
import java.util.Iterator;
|
||||
import java.util.List;
|
||||
@@ -112,8 +114,10 @@ public class Controller extends Thread {
|
||||
// To do with online accounts list
|
||||
private static final long ONLINE_ACCOUNTS_TASKS_INTERVAL = 10 * 1000L; // ms
|
||||
private static final long ONLINE_ACCOUNTS_BROADCAST_INTERVAL = 1 * 60 * 1000L; // ms
|
||||
private static final long ONLINE_TIMESTAMP_MODULUS = 5 * 60 * 1000L;
|
||||
public static final long ONLINE_TIMESTAMP_MODULUS = 5 * 60 * 1000L;
|
||||
private static final long LAST_SEEN_EXPIRY_PERIOD = (ONLINE_TIMESTAMP_MODULUS * 2) + (1 * 60 * 1000L);
|
||||
/** How many (latest) blocks' worth of online accounts we cache */
|
||||
private static final int MAX_BLOCKS_CACHED_ONLINE_ACCOUNTS = 2;
|
||||
|
||||
private static volatile boolean isStopping = false;
|
||||
private static BlockMinter blockMinter = null;
|
||||
@@ -168,8 +172,10 @@ public class Controller extends Thread {
|
||||
/** Lock for only allowing one blockchain-modifying codepath at a time. e.g. synchronization or newly minted block. */
|
||||
private final ReentrantLock blockchainLock = new ReentrantLock();
|
||||
|
||||
/** Cache of 'online accounts' */
|
||||
/** Cache of current 'online accounts' */
|
||||
List<OnlineAccountData> onlineAccounts = new ArrayList<>();
|
||||
/** Cache of latest blocks' online accounts */
|
||||
Deque<List<OnlineAccountData>> latestBlocksOnlineAccounts = new ArrayDeque<>(MAX_BLOCKS_CACHED_ONLINE_ACCOUNTS);
|
||||
|
||||
// Constructors
|
||||
|
||||
@@ -537,8 +543,11 @@ public class Controller extends Thread {
|
||||
|
||||
public SynchronizationResult actuallySynchronize(Peer peer, boolean force) throws InterruptedException {
|
||||
syncPercent = (this.chainTip.getHeight() * 100) / peer.getChainTipData().getLastHeight();
|
||||
isSynchronizing = true;
|
||||
updateSysTray();
|
||||
// Only update SysTray if we're potentially changing height
|
||||
if (syncPercent < 100) {
|
||||
isSynchronizing = true;
|
||||
updateSysTray();
|
||||
}
|
||||
|
||||
BlockData priorChainTip = this.chainTip;
|
||||
|
||||
@@ -584,7 +593,6 @@ public class Controller extends Thread {
|
||||
break;
|
||||
|
||||
case OK:
|
||||
requestSysTrayUpdate = true;
|
||||
// fall-through...
|
||||
case NOTHING_TO_DO: {
|
||||
// Update our list of inferior chain tips
|
||||
@@ -611,14 +619,13 @@ public class Controller extends Thread {
|
||||
// Reset our cache of inferior chains
|
||||
inferiorChainSignatures.clear();
|
||||
|
||||
// Update chain-tip, notify peers, websockets, etc.
|
||||
// Update chain-tip, systray, notify peers, websockets, etc.
|
||||
this.onNewBlock(newChainTip);
|
||||
}
|
||||
|
||||
return syncResult;
|
||||
} finally {
|
||||
isSynchronizing = false;
|
||||
requestSysTrayUpdate = true;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -765,8 +772,10 @@ public class Controller extends Thread {
|
||||
BlockData latestBlockData = getChainTip();
|
||||
network.broadcast(peer -> network.buildHeightMessage(peer, latestBlockData));
|
||||
|
||||
// Send (if outbound) / Request unconfirmed transaction signatures
|
||||
network.broadcast(network::buildGetUnconfirmedTransactionsMessage);
|
||||
// Request unconfirmed transaction signatures, but only if we're up-to-date.
|
||||
// If we're NOT up-to-date then priority is synchronizing first
|
||||
if (isUpToDate())
|
||||
network.broadcast(network::buildGetUnconfirmedTransactionsMessage);
|
||||
}
|
||||
|
||||
public void onMintingPossibleChange(boolean isMintingPossible) {
|
||||
@@ -789,6 +798,9 @@ public class Controller extends Thread {
|
||||
this.notifyGroupMembershipChange = false;
|
||||
ChatNotifier.getInstance().onGroupMembershipChange();
|
||||
}
|
||||
|
||||
// Trade-bot might want to perform some actions too
|
||||
TradeBot.getInstance().onChainTipChange();
|
||||
});
|
||||
}
|
||||
|
||||
@@ -888,8 +900,16 @@ public class Controller extends Thread {
|
||||
try (final Repository repository = RepositoryManager.getRepository()) {
|
||||
BlockData blockData = repository.getBlockRepository().fromSignature(signature);
|
||||
if (blockData == null) {
|
||||
LOGGER.debug(() -> String.format("Ignoring GET_BLOCK request from peer %s for unknown block %s", peer, Base58.encode(signature)));
|
||||
// Send no response at all???
|
||||
// We don't have this block
|
||||
|
||||
// Send valid, yet unexpected message type in response, so peer's synchronizer doesn't have to wait for timeout
|
||||
LOGGER.debug(() -> String.format("Sending 'block unknown' response to peer %s for GET_BLOCK request for unknown block %s", peer, Base58.encode(signature)));
|
||||
|
||||
// We'll send empty block summaries message as it's very short
|
||||
Message blockUnknownMessage = new BlockSummariesMessage(Collections.emptyList());
|
||||
blockUnknownMessage.setId(message.getId());
|
||||
if (!peer.sendMessage(blockUnknownMessage))
|
||||
peer.disconnect("failed to send block-unknown response");
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -1040,7 +1060,12 @@ public class Controller extends Thread {
|
||||
|
||||
private void onNetworkGetUnconfirmedTransactionsMessage(Peer peer, Message message) {
|
||||
try (final Repository repository = RepositoryManager.getRepository()) {
|
||||
List<byte[]> signatures = repository.getTransactionRepository().getUnconfirmedTransactionSignatures();
|
||||
List<byte[]> signatures = Collections.emptyList();
|
||||
|
||||
// If we're NOT up-to-date then don't send out unconfirmed transactions
|
||||
// as it's possible they are already included in a later block that we don't have.
|
||||
if (isUpToDate())
|
||||
signatures = repository.getTransactionRepository().getUnconfirmedTransactionSignatures();
|
||||
|
||||
Message transactionSignaturesMessage = new TransactionSignaturesMessage(signatures);
|
||||
if (!peer.sendMessage(transactionSignaturesMessage))
|
||||
@@ -1446,6 +1471,32 @@ public class Controller extends Thread {
|
||||
}
|
||||
}
|
||||
|
||||
/** Returns cached, unmodifiable list of latest block's online accounts. */
|
||||
public List<OnlineAccountData> getLatestBlocksOnlineAccounts() {
|
||||
synchronized (this.latestBlocksOnlineAccounts) {
|
||||
return this.latestBlocksOnlineAccounts.peekFirst();
|
||||
}
|
||||
}
|
||||
|
||||
/** Caches list of latest block's online accounts. Typically called by Block.process() */
|
||||
public void pushLatestBlocksOnlineAccounts(List<OnlineAccountData> latestBlocksOnlineAccounts) {
|
||||
synchronized (this.latestBlocksOnlineAccounts) {
|
||||
if (this.latestBlocksOnlineAccounts.size() == MAX_BLOCKS_CACHED_ONLINE_ACCOUNTS)
|
||||
this.latestBlocksOnlineAccounts.pollLast();
|
||||
|
||||
this.latestBlocksOnlineAccounts.addFirst(latestBlocksOnlineAccounts == null
|
||||
? Collections.emptyList()
|
||||
: Collections.unmodifiableList(latestBlocksOnlineAccounts));
|
||||
}
|
||||
}
|
||||
|
||||
/** Reverts list of latest block's online accounts. Typically called by Block.orphan() */
|
||||
public void popLatestBlocksOnlineAccounts() {
|
||||
synchronized (this.latestBlocksOnlineAccounts) {
|
||||
this.latestBlocksOnlineAccounts.pollFirst();
|
||||
}
|
||||
}
|
||||
|
||||
public byte[] fetchArbitraryData(byte[] signature) throws InterruptedException {
|
||||
// Build request
|
||||
Message getArbitraryDataMessage = new GetArbitraryDataMessage(signature);
|
||||
|
@@ -1,5 +1,7 @@
|
||||
package org.qortal.controller;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.Collection;
|
||||
import java.util.HashMap;
|
||||
import java.util.Map;
|
||||
|
||||
@@ -26,17 +28,29 @@ public class StatusNotifier {
|
||||
return instance;
|
||||
}
|
||||
|
||||
public synchronized void register(Session session, Listener listener) {
|
||||
this.listenersBySession.put(session, listener);
|
||||
public void register(Session session, Listener listener) {
|
||||
synchronized (this.listenersBySession) {
|
||||
this.listenersBySession.put(session, listener);
|
||||
}
|
||||
}
|
||||
|
||||
public synchronized void deregister(Session session) {
|
||||
this.listenersBySession.remove(session);
|
||||
public void deregister(Session session) {
|
||||
synchronized (this.listenersBySession) {
|
||||
this.listenersBySession.remove(session);
|
||||
}
|
||||
}
|
||||
|
||||
public synchronized void onStatusChange(long now) {
|
||||
for (Listener listener : this.listenersBySession.values())
|
||||
public void onStatusChange(long now) {
|
||||
for (Listener listener : getAllListeners())
|
||||
listener.notify(now);
|
||||
}
|
||||
|
||||
private Collection<Listener> getAllListeners() {
|
||||
// Make a copy of listeners to both avoid concurrent modification
|
||||
// and reduce synchronization time
|
||||
synchronized (this.listenersBySession) {
|
||||
return new ArrayList<>(this.listenersBySession.values());
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
@@ -126,167 +126,22 @@ public class Synchronizer {
|
||||
|
||||
// Unless we're doing a forced sync, we might need to compare blocks after common block
|
||||
if (!force && ourInitialHeight > commonBlockHeight) {
|
||||
// If our latest block is very old, we're very behind and should ditch our fork.
|
||||
final Long minLatestBlockTimestamp = Controller.getMinimumLatestBlockTimestamp();
|
||||
if (minLatestBlockTimestamp == null)
|
||||
return SynchronizationResult.REPOSITORY_ISSUE;
|
||||
|
||||
if (ourLatestBlockData.getTimestamp() < minLatestBlockTimestamp) {
|
||||
LOGGER.info(String.format("Ditching our chain after height %d as our latest block is very old", commonBlockHeight));
|
||||
} else {
|
||||
// Compare chain weights
|
||||
|
||||
LOGGER.debug(String.format("Comparing chains from block %d with peer %s", commonBlockHeight + 1, peer));
|
||||
|
||||
// Fetch remaining peer's block summaries (which we also use to fill signatures list)
|
||||
int peerBlockCount = peerHeight - commonBlockHeight;
|
||||
|
||||
while (peerBlockSummaries.size() < peerBlockCount) {
|
||||
if (Controller.isStopping())
|
||||
return SynchronizationResult.SHUTTING_DOWN;
|
||||
|
||||
int lastSummaryHeight = commonBlockHeight + peerBlockSummaries.size();
|
||||
byte[] previousSignature;
|
||||
if (peerBlockSummaries.isEmpty())
|
||||
previousSignature = commonBlockSig;
|
||||
else
|
||||
previousSignature = peerBlockSummaries.get(peerBlockSummaries.size() - 1).getSignature();
|
||||
|
||||
List<BlockSummaryData> moreBlockSummaries = this.getBlockSummaries(peer, previousSignature, peerBlockCount - peerBlockSummaries.size());
|
||||
|
||||
if (moreBlockSummaries == null || moreBlockSummaries.isEmpty()) {
|
||||
LOGGER.info(String.format("Peer %s failed to respond with block summaries after height %d, sig %.8s", peer,
|
||||
lastSummaryHeight, Base58.encode(previousSignature)));
|
||||
return SynchronizationResult.NO_REPLY;
|
||||
}
|
||||
|
||||
// Check peer sent valid heights
|
||||
for (int i = 0; i < moreBlockSummaries.size(); ++i) {
|
||||
++lastSummaryHeight;
|
||||
|
||||
BlockSummaryData blockSummary = moreBlockSummaries.get(i);
|
||||
|
||||
if (blockSummary.getHeight() != lastSummaryHeight) {
|
||||
LOGGER.info(String.format("Peer %s responded with invalid block summary for height %d, sig %.8s", peer,
|
||||
lastSummaryHeight, Base58.encode(blockSummary.getSignature())));
|
||||
return SynchronizationResult.NO_REPLY;
|
||||
}
|
||||
}
|
||||
|
||||
peerBlockSummaries.addAll(moreBlockSummaries);
|
||||
}
|
||||
|
||||
// Fetch our corresponding block summaries
|
||||
List<BlockSummaryData> ourBlockSummaries = repository.getBlockRepository().getBlockSummaries(commonBlockHeight + 1, ourInitialHeight);
|
||||
|
||||
// Populate minter account levels for both lists of block summaries
|
||||
populateBlockSummariesMinterLevels(repository, peerBlockSummaries);
|
||||
populateBlockSummariesMinterLevels(repository, ourBlockSummaries);
|
||||
|
||||
// Calculate cumulative chain weights of both blockchain subsets, from common block to highest mutual block.
|
||||
BigInteger ourChainWeight = Block.calcChainWeight(commonBlockHeight, commonBlockSig, ourBlockSummaries);
|
||||
BigInteger peerChainWeight = Block.calcChainWeight(commonBlockHeight, commonBlockSig, peerBlockSummaries);
|
||||
|
||||
// If our blockchain has greater weight then don't synchronize with peer
|
||||
if (ourChainWeight.compareTo(peerChainWeight) >= 0) {
|
||||
LOGGER.debug(String.format("Not synchronizing with peer %s as we have better blockchain", peer));
|
||||
NumberFormat formatter = new DecimalFormat("0.###E0");
|
||||
LOGGER.debug(String.format("Our chain weight: %s, peer's chain weight: %s (higher is better)", formatter.format(ourChainWeight), formatter.format(peerChainWeight)));
|
||||
return SynchronizationResult.INFERIOR_CHAIN;
|
||||
}
|
||||
}
|
||||
SynchronizationResult chainCompareResult = compareChains(repository, commonBlockData, ourLatestBlockData, peer, peerHeight, peerBlockSummaries);
|
||||
if (chainCompareResult != SynchronizationResult.OK)
|
||||
return chainCompareResult;
|
||||
}
|
||||
|
||||
int ourHeight = ourInitialHeight;
|
||||
if (ourHeight > commonBlockHeight) {
|
||||
// Unwind to common block (unless common block is our latest block)
|
||||
LOGGER.debug(String.format("Orphaning blocks back to common block height %d, sig %.8s", commonBlockHeight, commonBlockSig58));
|
||||
|
||||
while (ourHeight > commonBlockHeight) {
|
||||
if (Controller.isStopping())
|
||||
return SynchronizationResult.SHUTTING_DOWN;
|
||||
|
||||
BlockData blockData = repository.getBlockRepository().fromHeight(ourHeight);
|
||||
Block block = new Block(repository, blockData);
|
||||
block.orphan();
|
||||
|
||||
--ourHeight;
|
||||
}
|
||||
|
||||
LOGGER.debug(String.format("Orphaned blocks back to height %d, sig %.8s - fetching blocks from peer %s", commonBlockHeight, commonBlockSig58, peer));
|
||||
SynchronizationResult syncResult = null;
|
||||
if (commonBlockHeight < ourInitialHeight) {
|
||||
// Peer's chain is better, sync to that one
|
||||
syncResult = syncToPeerChain(repository, commonBlockData, ourInitialHeight, peer, peerHeight, peerBlockSummaries);
|
||||
} else {
|
||||
LOGGER.debug(String.format("Fetching new blocks from peer %s", peer));
|
||||
// Simply fetch and apply blocks as they arrive
|
||||
syncResult = applyNewBlocks(repository, commonBlockData, ourInitialHeight, peer, peerHeight, peerBlockSummaries);
|
||||
}
|
||||
|
||||
// Fetch, and apply, blocks from peer
|
||||
byte[] latestPeerSignature = commonBlockSig;
|
||||
int maxBatchHeight = commonBlockHeight + SYNC_BATCH_SIZE;
|
||||
|
||||
// Convert any block summaries from above into signatures to request from peer
|
||||
List<byte[]> peerBlockSignatures = peerBlockSummaries.stream().map(BlockSummaryData::getSignature).collect(Collectors.toList());
|
||||
|
||||
while (ourHeight < peerHeight && ourHeight < maxBatchHeight) {
|
||||
if (Controller.isStopping())
|
||||
return SynchronizationResult.SHUTTING_DOWN;
|
||||
|
||||
// Do we need more signatures?
|
||||
if (peerBlockSignatures.isEmpty()) {
|
||||
int numberRequested = maxBatchHeight - ourHeight;
|
||||
LOGGER.trace(String.format("Requesting %d signature%s after height %d, sig %.8s",
|
||||
numberRequested, (numberRequested != 1 ? "s": ""), ourHeight, Base58.encode(latestPeerSignature)));
|
||||
|
||||
peerBlockSignatures = this.getBlockSignatures(peer, latestPeerSignature, numberRequested);
|
||||
|
||||
if (peerBlockSignatures == null || peerBlockSignatures.isEmpty()) {
|
||||
LOGGER.info(String.format("Peer %s failed to respond with more block signatures after height %d, sig %.8s", peer,
|
||||
ourHeight, Base58.encode(latestPeerSignature)));
|
||||
return SynchronizationResult.NO_REPLY;
|
||||
}
|
||||
|
||||
LOGGER.trace(String.format("Received %s signature%s", peerBlockSignatures.size(), (peerBlockSignatures.size() != 1 ? "s" : "")));
|
||||
}
|
||||
|
||||
latestPeerSignature = peerBlockSignatures.get(0);
|
||||
peerBlockSignatures.remove(0);
|
||||
++ourHeight;
|
||||
|
||||
Block newBlock = this.fetchBlock(repository, peer, latestPeerSignature);
|
||||
|
||||
if (newBlock == null) {
|
||||
LOGGER.info(String.format("Peer %s failed to respond with block for height %d, sig %.8s", peer,
|
||||
ourHeight, Base58.encode(latestPeerSignature)));
|
||||
return SynchronizationResult.NO_REPLY;
|
||||
}
|
||||
|
||||
if (!newBlock.isSignatureValid()) {
|
||||
LOGGER.info(String.format("Peer %s sent block with invalid signature for height %d, sig %.8s", peer,
|
||||
ourHeight, Base58.encode(latestPeerSignature)));
|
||||
return SynchronizationResult.INVALID_DATA;
|
||||
}
|
||||
|
||||
// Transactions are transmitted without approval status so determine that now
|
||||
for (Transaction transaction : newBlock.getTransactions())
|
||||
transaction.setInitialApprovalStatus();
|
||||
|
||||
ValidationResult blockResult = newBlock.isValid();
|
||||
if (blockResult != ValidationResult.OK) {
|
||||
LOGGER.info(String.format("Peer %s sent invalid block for height %d, sig %.8s: %s", peer,
|
||||
ourHeight, Base58.encode(latestPeerSignature), blockResult.name()));
|
||||
return SynchronizationResult.INVALID_DATA;
|
||||
}
|
||||
|
||||
// Save transactions attached to this block
|
||||
for (Transaction transaction : newBlock.getTransactions()) {
|
||||
TransactionData transactionData = transaction.getTransactionData();
|
||||
repository.getTransactionRepository().save(transactionData);
|
||||
}
|
||||
|
||||
newBlock.process();
|
||||
|
||||
// If we've grown our blockchain then at least save progress so far
|
||||
if (ourHeight > ourInitialHeight)
|
||||
repository.saveChanges();
|
||||
}
|
||||
if (syncResult != SynchronizationResult.OK)
|
||||
return syncResult;
|
||||
|
||||
// Commit
|
||||
repository.saveChanges();
|
||||
@@ -396,6 +251,268 @@ public class Synchronizer {
|
||||
return SynchronizationResult.OK;
|
||||
}
|
||||
|
||||
private SynchronizationResult compareChains(Repository repository, BlockData commonBlockData, BlockData ourLatestBlockData,
|
||||
Peer peer, int peerHeight, List<BlockSummaryData> peerBlockSummaries) throws DataException, InterruptedException {
|
||||
final int commonBlockHeight = commonBlockData.getHeight();
|
||||
final byte[] commonBlockSig = commonBlockData.getSignature();
|
||||
|
||||
// If our latest block is very old, we're very behind and should ditch our fork.
|
||||
final Long minLatestBlockTimestamp = Controller.getMinimumLatestBlockTimestamp();
|
||||
if (minLatestBlockTimestamp == null)
|
||||
return SynchronizationResult.REPOSITORY_ISSUE;
|
||||
|
||||
if (ourLatestBlockData.getTimestamp() < minLatestBlockTimestamp) {
|
||||
LOGGER.info(String.format("Ditching our chain after height %d as our latest block is very old", commonBlockHeight));
|
||||
} else {
|
||||
// Compare chain weights
|
||||
|
||||
LOGGER.debug(String.format("Comparing chains from block %d with peer %s", commonBlockHeight + 1, peer));
|
||||
|
||||
// Fetch remaining peer's block summaries (which we also use to fill signatures list)
|
||||
int peerBlockCount = peerHeight - commonBlockHeight;
|
||||
|
||||
while (peerBlockSummaries.size() < peerBlockCount) {
|
||||
if (Controller.isStopping())
|
||||
return SynchronizationResult.SHUTTING_DOWN;
|
||||
|
||||
int lastSummaryHeight = commonBlockHeight + peerBlockSummaries.size();
|
||||
byte[] previousSignature;
|
||||
if (peerBlockSummaries.isEmpty())
|
||||
previousSignature = commonBlockSig;
|
||||
else
|
||||
previousSignature = peerBlockSummaries.get(peerBlockSummaries.size() - 1).getSignature();
|
||||
|
||||
List<BlockSummaryData> moreBlockSummaries = this.getBlockSummaries(peer, previousSignature, peerBlockCount - peerBlockSummaries.size());
|
||||
|
||||
if (moreBlockSummaries == null || moreBlockSummaries.isEmpty()) {
|
||||
LOGGER.info(String.format("Peer %s failed to respond with block summaries after height %d, sig %.8s", peer,
|
||||
lastSummaryHeight, Base58.encode(previousSignature)));
|
||||
return SynchronizationResult.NO_REPLY;
|
||||
}
|
||||
|
||||
// Check peer sent valid heights
|
||||
for (int i = 0; i < moreBlockSummaries.size(); ++i) {
|
||||
++lastSummaryHeight;
|
||||
|
||||
BlockSummaryData blockSummary = moreBlockSummaries.get(i);
|
||||
|
||||
if (blockSummary.getHeight() != lastSummaryHeight) {
|
||||
LOGGER.info(String.format("Peer %s responded with invalid block summary for height %d, sig %.8s", peer,
|
||||
lastSummaryHeight, Base58.encode(blockSummary.getSignature())));
|
||||
return SynchronizationResult.NO_REPLY;
|
||||
}
|
||||
}
|
||||
|
||||
peerBlockSummaries.addAll(moreBlockSummaries);
|
||||
}
|
||||
|
||||
// Fetch our corresponding block summaries
|
||||
List<BlockSummaryData> ourBlockSummaries = repository.getBlockRepository().getBlockSummaries(commonBlockHeight + 1, ourLatestBlockData.getHeight());
|
||||
|
||||
// Populate minter account levels for both lists of block summaries
|
||||
populateBlockSummariesMinterLevels(repository, peerBlockSummaries);
|
||||
populateBlockSummariesMinterLevels(repository, ourBlockSummaries);
|
||||
|
||||
// Calculate cumulative chain weights of both blockchain subsets, from common block to highest mutual block.
|
||||
BigInteger ourChainWeight = Block.calcChainWeight(commonBlockHeight, commonBlockSig, ourBlockSummaries);
|
||||
BigInteger peerChainWeight = Block.calcChainWeight(commonBlockHeight, commonBlockSig, peerBlockSummaries);
|
||||
|
||||
// If our blockchain has greater weight then don't synchronize with peer
|
||||
if (ourChainWeight.compareTo(peerChainWeight) >= 0) {
|
||||
LOGGER.debug(String.format("Not synchronizing with peer %s as we have better blockchain", peer));
|
||||
NumberFormat formatter = new DecimalFormat("0.###E0");
|
||||
LOGGER.debug(String.format("Our chain weight: %s, peer's chain weight: %s (higher is better)", formatter.format(ourChainWeight), formatter.format(peerChainWeight)));
|
||||
return SynchronizationResult.INFERIOR_CHAIN;
|
||||
}
|
||||
}
|
||||
|
||||
return SynchronizationResult.OK;
|
||||
}
|
||||
|
||||
private SynchronizationResult syncToPeerChain(Repository repository, BlockData commonBlockData, int ourInitialHeight,
|
||||
Peer peer, final int peerHeight, List<BlockSummaryData> peerBlockSummaries) throws DataException, InterruptedException {
|
||||
final int commonBlockHeight = commonBlockData.getHeight();
|
||||
final byte[] commonBlockSig = commonBlockData.getSignature();
|
||||
String commonBlockSig58 = Base58.encode(commonBlockSig);
|
||||
|
||||
LOGGER.debug(() -> String.format("Fetching peer %s chain from height %d, sig %.8s", peer, commonBlockHeight, commonBlockSig58));
|
||||
|
||||
int ourHeight = ourInitialHeight;
|
||||
|
||||
// Overall plan: fetch peer's blocks first, then orphan, then apply
|
||||
|
||||
// Convert any leftover (post-common) block summaries into signatures to request from peer
|
||||
List<byte[]> peerBlockSignatures = peerBlockSummaries.stream().map(BlockSummaryData::getSignature).collect(Collectors.toList());
|
||||
|
||||
// Fetch remaining block signatures, if needed
|
||||
int numberSignaturesRequired = peerBlockSignatures.size() - (peerHeight - commonBlockHeight);
|
||||
if (numberSignaturesRequired > 0) {
|
||||
byte[] latestPeerSignature = peerBlockSignatures.isEmpty() ? commonBlockSig : peerBlockSignatures.get(peerBlockSignatures.size() - 1);
|
||||
|
||||
LOGGER.trace(String.format("Requesting %d signature%s after height %d, sig %.8s",
|
||||
numberSignaturesRequired, (numberSignaturesRequired != 1 ? "s": ""), ourHeight, Base58.encode(latestPeerSignature)));
|
||||
|
||||
List<byte[]> moreBlockSignatures = this.getBlockSignatures(peer, latestPeerSignature, numberSignaturesRequired);
|
||||
|
||||
if (moreBlockSignatures == null || moreBlockSignatures.isEmpty()) {
|
||||
LOGGER.info(String.format("Peer %s failed to respond with more block signatures after height %d, sig %.8s", peer,
|
||||
ourHeight, Base58.encode(latestPeerSignature)));
|
||||
return SynchronizationResult.NO_REPLY;
|
||||
}
|
||||
|
||||
LOGGER.trace(String.format("Received %s signature%s", peerBlockSignatures.size(), (peerBlockSignatures.size() != 1 ? "s" : "")));
|
||||
|
||||
peerBlockSignatures.addAll(moreBlockSignatures);
|
||||
}
|
||||
|
||||
// Fetch blocks using signatures
|
||||
LOGGER.debug(String.format("Fetching new blocks from peer %s", peer));
|
||||
List<Block> peerBlocks = new ArrayList<>();
|
||||
|
||||
for (byte[] blockSignature : peerBlockSignatures) {
|
||||
Block newBlock = this.fetchBlock(repository, peer, blockSignature);
|
||||
|
||||
if (newBlock == null) {
|
||||
LOGGER.info(String.format("Peer %s failed to respond with block for height %d, sig %.8s", peer,
|
||||
ourHeight, Base58.encode(blockSignature)));
|
||||
return SynchronizationResult.NO_REPLY;
|
||||
}
|
||||
|
||||
if (!newBlock.isSignatureValid()) {
|
||||
LOGGER.info(String.format("Peer %s sent block with invalid signature for height %d, sig %.8s", peer,
|
||||
ourHeight, Base58.encode(blockSignature)));
|
||||
return SynchronizationResult.INVALID_DATA;
|
||||
}
|
||||
|
||||
// Transactions are transmitted without approval status so determine that now
|
||||
for (Transaction transaction : newBlock.getTransactions())
|
||||
transaction.setInitialApprovalStatus();
|
||||
|
||||
peerBlocks.add(newBlock);
|
||||
}
|
||||
|
||||
// Unwind to common block (unless common block is our latest block)
|
||||
LOGGER.debug(String.format("Orphaning blocks back to common block height %d, sig %.8s", commonBlockHeight, commonBlockSig58));
|
||||
|
||||
while (ourHeight > commonBlockHeight) {
|
||||
if (Controller.isStopping())
|
||||
return SynchronizationResult.SHUTTING_DOWN;
|
||||
|
||||
BlockData blockData = repository.getBlockRepository().fromHeight(ourHeight);
|
||||
Block block = new Block(repository, blockData);
|
||||
block.orphan();
|
||||
|
||||
--ourHeight;
|
||||
}
|
||||
|
||||
LOGGER.debug(String.format("Orphaned blocks back to height %d, sig %.8s - applying new blocks from peer %s", commonBlockHeight, commonBlockSig58, peer));
|
||||
|
||||
for (Block newBlock : peerBlocks) {
|
||||
ValidationResult blockResult = newBlock.isValid();
|
||||
if (blockResult != ValidationResult.OK) {
|
||||
LOGGER.info(String.format("Peer %s sent invalid block for height %d, sig %.8s: %s", peer,
|
||||
ourHeight, Base58.encode(newBlock.getSignature()), blockResult.name()));
|
||||
return SynchronizationResult.INVALID_DATA;
|
||||
}
|
||||
|
||||
// Save transactions attached to this block
|
||||
for (Transaction transaction : newBlock.getTransactions()) {
|
||||
TransactionData transactionData = transaction.getTransactionData();
|
||||
repository.getTransactionRepository().save(transactionData);
|
||||
}
|
||||
|
||||
newBlock.process();
|
||||
|
||||
// If we've grown our blockchain then at least save progress so far
|
||||
if (ourHeight > ourInitialHeight)
|
||||
repository.saveChanges();
|
||||
}
|
||||
|
||||
return SynchronizationResult.OK;
|
||||
}
|
||||
|
||||
private SynchronizationResult applyNewBlocks(Repository repository, BlockData commonBlockData, int ourInitialHeight,
|
||||
Peer peer, int peerHeight, List<BlockSummaryData> peerBlockSummaries) throws InterruptedException, DataException {
|
||||
LOGGER.debug(String.format("Fetching new blocks from peer %s", peer));
|
||||
|
||||
final int commonBlockHeight = commonBlockData.getHeight();
|
||||
final byte[] commonBlockSig = commonBlockData.getSignature();
|
||||
|
||||
int ourHeight = ourInitialHeight;
|
||||
|
||||
// Fetch, and apply, blocks from peer
|
||||
byte[] latestPeerSignature = commonBlockSig;
|
||||
int maxBatchHeight = commonBlockHeight + SYNC_BATCH_SIZE;
|
||||
|
||||
// Convert any block summaries from above into signatures to request from peer
|
||||
List<byte[]> peerBlockSignatures = peerBlockSummaries.stream().map(BlockSummaryData::getSignature).collect(Collectors.toList());
|
||||
|
||||
while (ourHeight < peerHeight && ourHeight < maxBatchHeight) {
|
||||
if (Controller.isStopping())
|
||||
return SynchronizationResult.SHUTTING_DOWN;
|
||||
|
||||
// Do we need more signatures?
|
||||
if (peerBlockSignatures.isEmpty()) {
|
||||
int numberRequested = maxBatchHeight - ourHeight;
|
||||
LOGGER.trace(String.format("Requesting %d signature%s after height %d, sig %.8s",
|
||||
numberRequested, (numberRequested != 1 ? "s": ""), ourHeight, Base58.encode(latestPeerSignature)));
|
||||
|
||||
peerBlockSignatures = this.getBlockSignatures(peer, latestPeerSignature, numberRequested);
|
||||
|
||||
if (peerBlockSignatures == null || peerBlockSignatures.isEmpty()) {
|
||||
LOGGER.info(String.format("Peer %s failed to respond with more block signatures after height %d, sig %.8s", peer,
|
||||
ourHeight, Base58.encode(latestPeerSignature)));
|
||||
return SynchronizationResult.NO_REPLY;
|
||||
}
|
||||
|
||||
LOGGER.trace(String.format("Received %s signature%s", peerBlockSignatures.size(), (peerBlockSignatures.size() != 1 ? "s" : "")));
|
||||
}
|
||||
|
||||
latestPeerSignature = peerBlockSignatures.get(0);
|
||||
peerBlockSignatures.remove(0);
|
||||
++ourHeight;
|
||||
|
||||
Block newBlock = this.fetchBlock(repository, peer, latestPeerSignature);
|
||||
|
||||
if (newBlock == null) {
|
||||
LOGGER.info(String.format("Peer %s failed to respond with block for height %d, sig %.8s", peer,
|
||||
ourHeight, Base58.encode(latestPeerSignature)));
|
||||
return SynchronizationResult.NO_REPLY;
|
||||
}
|
||||
|
||||
if (!newBlock.isSignatureValid()) {
|
||||
LOGGER.info(String.format("Peer %s sent block with invalid signature for height %d, sig %.8s", peer,
|
||||
ourHeight, Base58.encode(latestPeerSignature)));
|
||||
return SynchronizationResult.INVALID_DATA;
|
||||
}
|
||||
|
||||
// Transactions are transmitted without approval status so determine that now
|
||||
for (Transaction transaction : newBlock.getTransactions())
|
||||
transaction.setInitialApprovalStatus();
|
||||
|
||||
ValidationResult blockResult = newBlock.isValid();
|
||||
if (blockResult != ValidationResult.OK) {
|
||||
LOGGER.info(String.format("Peer %s sent invalid block for height %d, sig %.8s: %s", peer,
|
||||
ourHeight, Base58.encode(latestPeerSignature), blockResult.name()));
|
||||
return SynchronizationResult.INVALID_DATA;
|
||||
}
|
||||
|
||||
// Save transactions attached to this block
|
||||
for (Transaction transaction : newBlock.getTransactions()) {
|
||||
TransactionData transactionData = transaction.getTransactionData();
|
||||
repository.getTransactionRepository().save(transactionData);
|
||||
}
|
||||
|
||||
newBlock.process();
|
||||
|
||||
// If we've grown our blockchain then at least save progress so far
|
||||
if (ourHeight > ourInitialHeight)
|
||||
repository.saveChanges();
|
||||
}
|
||||
|
||||
return SynchronizationResult.OK;
|
||||
}
|
||||
|
||||
private List<BlockSummaryData> getBlockSummaries(Peer peer, byte[] parentSignature, int numberRequested) throws InterruptedException {
|
||||
Message getBlockSummariesMessage = new GetBlockSummariesMessage(parentSignature, numberRequested);
|
||||
|
||||
|
1049
src/main/java/org/qortal/controller/TradeBot.java
Normal file
1049
src/main/java/org/qortal/controller/TradeBot.java
Normal file
File diff suppressed because it is too large
Load Diff
@@ -1,28 +1,45 @@
|
||||
package org.qortal.crosschain;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.HashSet;
|
||||
import java.util.List;
|
||||
import java.util.Set;
|
||||
import java.util.stream.Collectors;
|
||||
|
||||
import org.apache.logging.log4j.LogManager;
|
||||
import org.apache.logging.log4j.Logger;
|
||||
import org.bitcoinj.core.Address;
|
||||
import org.bitcoinj.core.Coin;
|
||||
import org.bitcoinj.core.ECKey;
|
||||
import org.bitcoinj.core.InsufficientMoneyException;
|
||||
import org.bitcoinj.core.LegacyAddress;
|
||||
import org.bitcoinj.core.NetworkParameters;
|
||||
import org.bitcoinj.core.Sha256Hash;
|
||||
import org.bitcoinj.core.Transaction;
|
||||
import org.bitcoinj.core.TransactionOutput;
|
||||
import org.bitcoinj.core.UTXO;
|
||||
import org.bitcoinj.core.UTXOProvider;
|
||||
import org.bitcoinj.core.UTXOProviderException;
|
||||
import org.bitcoinj.crypto.DeterministicHierarchy;
|
||||
import org.bitcoinj.crypto.DeterministicKey;
|
||||
import org.bitcoinj.params.MainNetParams;
|
||||
import org.bitcoinj.params.RegTestParams;
|
||||
import org.bitcoinj.params.TestNet3Params;
|
||||
import org.bitcoinj.script.Script.ScriptType;
|
||||
import org.bitcoinj.script.ScriptBuilder;
|
||||
import org.bitcoinj.utils.MonetaryFormat;
|
||||
import org.bitcoinj.wallet.DeterministicKeyChain;
|
||||
import org.bitcoinj.wallet.SendRequest;
|
||||
import org.bitcoinj.wallet.Wallet;
|
||||
import org.qortal.crosschain.ElectrumX.UnspentOutput;
|
||||
import org.qortal.crypto.Crypto;
|
||||
import org.qortal.settings.Settings;
|
||||
import org.qortal.utils.BitTwiddling;
|
||||
import org.qortal.utils.Pair;
|
||||
|
||||
import com.google.common.hash.HashCode;
|
||||
|
||||
public class BTC {
|
||||
|
||||
public static final MonetaryFormat FORMAT = new MonetaryFormat().minDecimals(8).postfixCode();
|
||||
public static final long NO_LOCKTIME_NO_RBF_SEQUENCE = 0xFFFFFFFFL;
|
||||
public static final long LOCKTIME_NO_RBF_SEQUENCE = NO_LOCKTIME_NO_RBF_SEQUENCE - 1;
|
||||
public static final int HASH160_LENGTH = 20;
|
||||
@@ -30,6 +47,7 @@ public class BTC {
|
||||
protected static final Logger LOGGER = LogManager.getLogger(BTC.class);
|
||||
|
||||
private static final int TIMESTAMP_OFFSET = 4 + 32 + 32;
|
||||
private static final MonetaryFormat FORMAT = new MonetaryFormat().minDecimals(8).postfixCode();
|
||||
|
||||
public enum BitcoinNet {
|
||||
MAIN {
|
||||
@@ -58,6 +76,9 @@ public class BTC {
|
||||
private final NetworkParameters params;
|
||||
private final ElectrumX electrumX;
|
||||
|
||||
// Let ECKey.equals() do the hard work
|
||||
private final Set<ECKey> spentKeys = new HashSet<>();
|
||||
|
||||
// Constructors and instance
|
||||
|
||||
private BTC() {
|
||||
@@ -88,6 +109,34 @@ public class BTC {
|
||||
|
||||
// Actual useful methods for use by other classes
|
||||
|
||||
public static String format(Coin amount) {
|
||||
return BTC.FORMAT.format(amount).toString();
|
||||
}
|
||||
|
||||
public static String format(long amount) {
|
||||
return format(Coin.valueOf(amount));
|
||||
}
|
||||
|
||||
public boolean isValidXprv(String xprv58) {
|
||||
try {
|
||||
DeterministicKey.deserializeB58(null, xprv58, this.params);
|
||||
return true;
|
||||
} catch (IllegalArgumentException e) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
/** Returns P2PKH Bitcoin address using passed public key hash. */
|
||||
public String pkhToAddress(byte[] publicKeyHash) {
|
||||
return LegacyAddress.fromPubKeyHash(this.params, publicKeyHash).toString();
|
||||
}
|
||||
|
||||
public String deriveP2shAddress(byte[] redeemScriptBytes) {
|
||||
byte[] redeemScriptHash = Crypto.hash160(redeemScriptBytes);
|
||||
Address p2shAddress = LegacyAddress.fromScriptHash(params, redeemScriptHash);
|
||||
return p2shAddress.toString();
|
||||
}
|
||||
|
||||
/** Returns median timestamp from latest 11 blocks, in seconds. */
|
||||
public Integer getMedianBlockTime() {
|
||||
Integer height = this.electrumX.getCurrentHeight();
|
||||
@@ -99,34 +148,31 @@ public class BTC {
|
||||
if (blockHeaders == null || blockHeaders.size() < 11)
|
||||
return null;
|
||||
|
||||
List<Integer> blockTimestamps = blockHeaders.stream().map(blockHeader -> BitTwiddling.fromLEBytes(blockHeader, TIMESTAMP_OFFSET)).collect(Collectors.toList());
|
||||
List<Integer> blockTimestamps = blockHeaders.stream().map(blockHeader -> BitTwiddling.intFromLEBytes(blockHeader, TIMESTAMP_OFFSET)).collect(Collectors.toList());
|
||||
|
||||
// Descending, but order shouldn't matter as we're picking median...
|
||||
// Descending order
|
||||
blockTimestamps.sort((a, b) -> Integer.compare(b, a));
|
||||
|
||||
// Pick median
|
||||
return blockTimestamps.get(5);
|
||||
}
|
||||
|
||||
public Coin getBalance(String base58Address) {
|
||||
Long balance = this.electrumX.getBalance(addressToScript(base58Address));
|
||||
if (balance == null)
|
||||
return null;
|
||||
|
||||
return Coin.valueOf(balance);
|
||||
public Long getBalance(String base58Address) {
|
||||
return this.electrumX.getBalance(addressToScript(base58Address));
|
||||
}
|
||||
|
||||
public List<TransactionOutput> getUnspentOutputs(String base58Address) {
|
||||
List<Pair<byte[], Integer>> unspentOutputs = this.electrumX.getUnspentOutputs(addressToScript(base58Address));
|
||||
List<UnspentOutput> unspentOutputs = this.electrumX.getUnspentOutputs(addressToScript(base58Address));
|
||||
if (unspentOutputs == null)
|
||||
return null;
|
||||
|
||||
List<TransactionOutput> unspentTransactionOutputs = new ArrayList<>();
|
||||
for (Pair<byte[], Integer> unspentOutput : unspentOutputs) {
|
||||
List<TransactionOutput> transactionOutputs = getOutputs(unspentOutput.getA());
|
||||
for (UnspentOutput unspentOutput : unspentOutputs) {
|
||||
List<TransactionOutput> transactionOutputs = getOutputs(unspentOutput.hash);
|
||||
if (transactionOutputs == null)
|
||||
return null;
|
||||
|
||||
unspentTransactionOutputs.add(transactionOutputs.get(unspentOutput.getB()));
|
||||
unspentTransactionOutputs.add(transactionOutputs.get(unspentOutput.index));
|
||||
}
|
||||
|
||||
return unspentTransactionOutputs;
|
||||
@@ -141,6 +187,7 @@ public class BTC {
|
||||
return transaction.getOutputs();
|
||||
}
|
||||
|
||||
/** Returns list of raw transactions spending passed address. */
|
||||
public List<byte[]> getAddressTransactions(String base58Address) {
|
||||
return this.electrumX.getAddressTransactions(addressToScript(base58Address));
|
||||
}
|
||||
@@ -149,6 +196,181 @@ public class BTC {
|
||||
return this.electrumX.broadcastTransaction(transaction.bitcoinSerialize());
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns bitcoinj transaction sending <tt>amount</tt> to <tt>recipient</tt>.
|
||||
*
|
||||
* @param xprv58 BIP32 extended Bitcoin private key
|
||||
* @param recipient P2PKH address
|
||||
* @param amount unscaled amount
|
||||
* @return transaction, or null if insufficient funds
|
||||
*/
|
||||
public Transaction buildSpend(String xprv58, String recipient, long amount) {
|
||||
Wallet wallet = Wallet.fromSpendingKeyB58(this.params, xprv58, DeterministicHierarchy.BIP32_STANDARDISATION_TIME_SECS);
|
||||
wallet.setUTXOProvider(new WalletAwareUTXOProvider(this, wallet, WalletAwareUTXOProvider.KeySearchMode.REQUEST_MORE_IF_ALL_SPENT));
|
||||
|
||||
Address destination = Address.fromString(this.params, recipient);
|
||||
SendRequest sendRequest = SendRequest.to(destination, Coin.valueOf(amount));
|
||||
|
||||
if (this.params == TestNet3Params.get())
|
||||
// Much smaller fee for TestNet3
|
||||
sendRequest.feePerKb = Coin.valueOf(2000L);
|
||||
|
||||
try {
|
||||
wallet.completeTx(sendRequest);
|
||||
return sendRequest.tx;
|
||||
} catch (InsufficientMoneyException e) {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns unspent Bitcoin balance given 'm' BIP32 key.
|
||||
*
|
||||
* @param xprv58 BIP32 extended Bitcoin private key
|
||||
* @return unspent BTC balance, or null if unable to determine balance
|
||||
*/
|
||||
public Long getWalletBalance(String xprv58) {
|
||||
Wallet wallet = Wallet.fromSpendingKeyB58(this.params, xprv58, DeterministicHierarchy.BIP32_STANDARDISATION_TIME_SECS);
|
||||
wallet.setUTXOProvider(new WalletAwareUTXOProvider(this, wallet, WalletAwareUTXOProvider.KeySearchMode.REQUEST_MORE_IF_ANY_SPENT));
|
||||
|
||||
Coin balance = wallet.getBalance();
|
||||
if (balance == null)
|
||||
return null;
|
||||
|
||||
return balance.value;
|
||||
}
|
||||
|
||||
// UTXOProvider support
|
||||
|
||||
static class WalletAwareUTXOProvider implements UTXOProvider {
|
||||
private static final int LOOKAHEAD_INCREMENT = 3;
|
||||
|
||||
private final BTC btc;
|
||||
private final Wallet wallet;
|
||||
|
||||
enum KeySearchMode {
|
||||
REQUEST_MORE_IF_ALL_SPENT, REQUEST_MORE_IF_ANY_SPENT;
|
||||
}
|
||||
private final KeySearchMode keySearchMode;
|
||||
private final DeterministicKeyChain keyChain;
|
||||
|
||||
public WalletAwareUTXOProvider(BTC btc, Wallet wallet, KeySearchMode keySearchMode) {
|
||||
this.btc = btc;
|
||||
this.wallet = wallet;
|
||||
this.keySearchMode = keySearchMode;
|
||||
this.keyChain = this.wallet.getActiveKeyChain();
|
||||
|
||||
// Set up wallet's key chain
|
||||
this.keyChain.setLookaheadSize(LOOKAHEAD_INCREMENT);
|
||||
this.keyChain.maybeLookAhead();
|
||||
}
|
||||
|
||||
public List<UTXO> getOpenTransactionOutputs(List<ECKey> keys) throws UTXOProviderException {
|
||||
List<UTXO> allUnspentOutputs = new ArrayList<>();
|
||||
final boolean coinbase = false;
|
||||
|
||||
int ki = 0;
|
||||
do {
|
||||
boolean areAllKeysUnspent = true;
|
||||
boolean areAllKeysSpent = true;
|
||||
|
||||
for (; ki < keys.size(); ++ki) {
|
||||
ECKey key = keys.get(ki);
|
||||
|
||||
Address address = Address.fromKey(btc.params, key, ScriptType.P2PKH);
|
||||
byte[] script = ScriptBuilder.createOutputScript(address).getProgram();
|
||||
|
||||
List<UnspentOutput> unspentOutputs = btc.electrumX.getUnspentOutputs(script);
|
||||
if (unspentOutputs == null)
|
||||
throw new UTXOProviderException(String.format("Unable to fetch unspent outputs for %s", address));
|
||||
|
||||
/*
|
||||
* If there are no unspent outputs then either:
|
||||
* a) all the outputs have been spent
|
||||
* b) address has never been used
|
||||
*
|
||||
* For case (a) we want to remember not to check this address (key) again.
|
||||
*/
|
||||
|
||||
if (unspentOutputs.isEmpty()) {
|
||||
// If this is a known key that has been spent before, then we can skip asking for transaction history
|
||||
if (btc.spentKeys.contains(key)) {
|
||||
wallet.getActiveKeyChain().markKeyAsUsed((DeterministicKey) key);
|
||||
areAllKeysUnspent = false;
|
||||
continue;
|
||||
}
|
||||
|
||||
// Ask for transaction history - if it's empty then key has never been used
|
||||
List<byte[]> historicTransactionHashes = btc.electrumX.getAddressTransactions(script);
|
||||
if (historicTransactionHashes == null)
|
||||
throw new UTXOProviderException(
|
||||
String.format("Unable to fetch transaction history for %s", address));
|
||||
|
||||
if (!historicTransactionHashes.isEmpty()) {
|
||||
// Fully spent key - case (a)
|
||||
btc.spentKeys.add(key);
|
||||
wallet.getActiveKeyChain().markKeyAsUsed((DeterministicKey) key);
|
||||
areAllKeysUnspent = false;
|
||||
} else {
|
||||
// Key never been used - case (b)
|
||||
areAllKeysSpent = false;
|
||||
}
|
||||
|
||||
continue;
|
||||
}
|
||||
|
||||
// If we reach here, then there's definitely at least one unspent key
|
||||
areAllKeysSpent = false;
|
||||
|
||||
for (UnspentOutput unspentOutput : unspentOutputs) {
|
||||
List<TransactionOutput> transactionOutputs = btc.getOutputs(unspentOutput.hash);
|
||||
if (transactionOutputs == null)
|
||||
throw new UTXOProviderException(String.format("Unable to fetch outputs for TX %s",
|
||||
HashCode.fromBytes(unspentOutput.hash)));
|
||||
|
||||
TransactionOutput transactionOutput = transactionOutputs.get(unspentOutput.index);
|
||||
|
||||
UTXO utxo = new UTXO(Sha256Hash.wrap(unspentOutput.hash), unspentOutput.index,
|
||||
Coin.valueOf(unspentOutput.value), unspentOutput.height, coinbase,
|
||||
transactionOutput.getScriptPubKey());
|
||||
|
||||
allUnspentOutputs.add(utxo);
|
||||
}
|
||||
}
|
||||
|
||||
if ((this.keySearchMode == KeySearchMode.REQUEST_MORE_IF_ALL_SPENT && areAllKeysSpent)
|
||||
|| (this.keySearchMode == KeySearchMode.REQUEST_MORE_IF_ANY_SPENT && !areAllKeysUnspent)) {
|
||||
// Generate some more keys
|
||||
this.keyChain.setLookaheadSize(this.keyChain.getLookaheadSize() + LOOKAHEAD_INCREMENT);
|
||||
this.keyChain.maybeLookAhead();
|
||||
|
||||
// This returns all keys, including those already in 'keys'
|
||||
List<DeterministicKey> allLeafKeys = this.keyChain.getLeafKeys();
|
||||
// Add only new keys onto our list of keys to search
|
||||
List<DeterministicKey> newKeys = allLeafKeys.subList(ki, allLeafKeys.size());
|
||||
keys.addAll(newKeys);
|
||||
// Fall-through to checking more keys as now 'ki' is smaller than 'keys.size()' again
|
||||
}
|
||||
|
||||
// If we have processed all keys, then we're done
|
||||
} while (ki < keys.size());
|
||||
|
||||
return allUnspentOutputs;
|
||||
}
|
||||
|
||||
public int getChainHeadHeight() throws UTXOProviderException {
|
||||
Integer height = btc.electrumX.getCurrentHeight();
|
||||
if (height == null)
|
||||
throw new UTXOProviderException("Unable to determine Bitcoin chain height");
|
||||
|
||||
return height.intValue();
|
||||
}
|
||||
|
||||
public NetworkParameters getParams() {
|
||||
return btc.params;
|
||||
}
|
||||
}
|
||||
|
||||
// Utility methods for us
|
||||
|
||||
private byte[] addressToScript(String base58Address) {
|
||||
|
File diff suppressed because it is too large
Load Diff
236
src/main/java/org/qortal/crosschain/BTCP2SH.java
Normal file
236
src/main/java/org/qortal/crosschain/BTCP2SH.java
Normal file
@@ -0,0 +1,236 @@
|
||||
package org.qortal.crosschain;
|
||||
|
||||
import java.util.List;
|
||||
import java.util.function.Function;
|
||||
|
||||
import org.bitcoinj.core.Address;
|
||||
import org.bitcoinj.core.Coin;
|
||||
import org.bitcoinj.core.ECKey;
|
||||
import org.bitcoinj.core.LegacyAddress;
|
||||
import org.bitcoinj.core.NetworkParameters;
|
||||
import org.bitcoinj.core.Transaction;
|
||||
import org.bitcoinj.core.Transaction.SigHash;
|
||||
import org.bitcoinj.core.TransactionInput;
|
||||
import org.bitcoinj.core.TransactionOutput;
|
||||
import org.bitcoinj.crypto.TransactionSignature;
|
||||
import org.bitcoinj.script.Script;
|
||||
import org.bitcoinj.script.ScriptBuilder;
|
||||
import org.bitcoinj.script.ScriptChunk;
|
||||
import org.bitcoinj.script.ScriptOpCodes;
|
||||
import org.qortal.crypto.Crypto;
|
||||
import org.qortal.utils.BitTwiddling;
|
||||
|
||||
import com.google.common.hash.HashCode;
|
||||
import com.google.common.primitives.Bytes;
|
||||
|
||||
public class BTCP2SH {
|
||||
|
||||
public static final int SECRET_LENGTH = 32;
|
||||
public static final int MIN_LOCKTIME = 1500000000;
|
||||
|
||||
/*
|
||||
* OP_TUCK (to copy public key to before signature)
|
||||
* OP_CHECKSIGVERIFY (sig & pubkey must verify or script fails)
|
||||
* OP_HASH160 (convert public key to PKH)
|
||||
* OP_DUP (duplicate PKH)
|
||||
* <push 20 bytes> <refund PKH> OP_EQUAL (does PKH match refund PKH?)
|
||||
* OP_IF
|
||||
* OP_DROP (no need for duplicate PKH)
|
||||
* <push 4 bytes> <locktime>
|
||||
* OP_CHECKLOCKTIMEVERIFY (if this passes, leftover stack is <locktime> so script passes)
|
||||
* OP_ELSE
|
||||
* <push 20 bytes> <redeem PKH> OP_EQUALVERIFY (duplicate PKH must match redeem PKH or script fails)
|
||||
* OP_HASH160 (hash secret)
|
||||
* <push 20 bytes> <hash of secret> OP_EQUAL (do hashes of secrets match? if true, script passes else script fails)
|
||||
* OP_ENDIF
|
||||
*/
|
||||
|
||||
private static final byte[] redeemScript1 = HashCode.fromString("7dada97614").asBytes(); // OP_TUCK OP_CHECKSIGVERIFY OP_HASH160 OP_DUP push(0x14 bytes)
|
||||
private static final byte[] redeemScript2 = HashCode.fromString("87637504").asBytes(); // OP_EQUAL OP_IF OP_DROP push(0x4 bytes)
|
||||
private static final byte[] redeemScript3 = HashCode.fromString("b16714").asBytes(); // OP_CHECKLOCKTIMEVERIFY OP_ELSE push(0x14 bytes)
|
||||
private static final byte[] redeemScript4 = HashCode.fromString("88a914").asBytes(); // OP_EQUALVERIFY OP_HASH160 push(0x14 bytes)
|
||||
private static final byte[] redeemScript5 = HashCode.fromString("8768").asBytes(); // OP_EQUAL OP_ENDIF
|
||||
|
||||
/**
|
||||
* Returns Bitcoin redeemScript used for cross-chain trading.
|
||||
* <p>
|
||||
* See comments in {@link BTCP2SH} for more details.
|
||||
*
|
||||
* @param refunderPubKeyHash 20-byte HASH160 of P2SH funder's public key, for refunding purposes
|
||||
* @param lockTime seconds-since-epoch threshold, after which P2SH funder can claim refund
|
||||
* @param redeemerPubKeyHash 20-byte HASH160 of P2SH redeemer's public key
|
||||
* @param secretHash 20-byte HASH160 of secret, used by P2SH redeemer to claim funds
|
||||
* @return
|
||||
*/
|
||||
public static byte[] buildScript(byte[] refunderPubKeyHash, int lockTime, byte[] redeemerPubKeyHash, byte[] secretHash) {
|
||||
return Bytes.concat(redeemScript1, refunderPubKeyHash, redeemScript2, BitTwiddling.toLEByteArray((int) (lockTime & 0xffffffffL)),
|
||||
redeemScript3, redeemerPubKeyHash, redeemScript4, secretHash, redeemScript5);
|
||||
}
|
||||
|
||||
/**
|
||||
* Builds a custom transaction to spend P2SH.
|
||||
*
|
||||
* @param amount output amount, should be total of input amounts, less miner fees
|
||||
* @param spendKey key for signing transaction, and also where funds are 'sent' (output)
|
||||
* @param fundingOutput output from transaction that funded P2SH address
|
||||
* @param redeemScriptBytes the redeemScript itself, in byte[] form
|
||||
* @param lockTime (optional) transaction nLockTime, used in refund scenario
|
||||
* @param scriptSigBuilder function for building scriptSig using transaction input signature
|
||||
* @param outputPublicKeyHash PKH used to create P2PKH output
|
||||
* @return Signed Bitcoin transaction for spending P2SH
|
||||
*/
|
||||
public static Transaction buildP2shTransaction(Coin amount, ECKey spendKey, List<TransactionOutput> fundingOutputs, byte[] redeemScriptBytes,
|
||||
Long lockTime, Function<byte[], Script> scriptSigBuilder, byte[] outputPublicKeyHash) {
|
||||
NetworkParameters params = BTC.getInstance().getNetworkParameters();
|
||||
|
||||
Transaction transaction = new Transaction(params);
|
||||
transaction.setVersion(2);
|
||||
|
||||
// Output is back to P2SH funder
|
||||
transaction.addOutput(amount, ScriptBuilder.createP2PKHOutputScript(outputPublicKeyHash));
|
||||
|
||||
for (int inputIndex = 0; inputIndex < fundingOutputs.size(); ++inputIndex) {
|
||||
TransactionOutput fundingOutput = fundingOutputs.get(inputIndex);
|
||||
|
||||
// Input (without scriptSig prior to signing)
|
||||
TransactionInput input = new TransactionInput(params, null, redeemScriptBytes, fundingOutput.getOutPointFor());
|
||||
if (lockTime != null)
|
||||
input.setSequenceNumber(BTC.LOCKTIME_NO_RBF_SEQUENCE); // Use max-value - 1, so lockTime can be used but not RBF
|
||||
else
|
||||
input.setSequenceNumber(BTC.NO_LOCKTIME_NO_RBF_SEQUENCE); // Use max-value, so no lockTime and no RBF
|
||||
transaction.addInput(input);
|
||||
}
|
||||
|
||||
// Set locktime after inputs added but before input signatures are generated
|
||||
if (lockTime != null)
|
||||
transaction.setLockTime(lockTime);
|
||||
|
||||
for (int inputIndex = 0; inputIndex < fundingOutputs.size(); ++inputIndex) {
|
||||
// Generate transaction signature for input
|
||||
final boolean anyoneCanPay = false;
|
||||
TransactionSignature txSig = transaction.calculateSignature(inputIndex, spendKey, redeemScriptBytes, SigHash.ALL, anyoneCanPay);
|
||||
|
||||
// Calculate transaction signature
|
||||
byte[] txSigBytes = txSig.encodeToBitcoin();
|
||||
|
||||
// Build scriptSig using lambda and tx signature
|
||||
Script scriptSig = scriptSigBuilder.apply(txSigBytes);
|
||||
|
||||
// Set input scriptSig
|
||||
transaction.getInput(inputIndex).setScriptSig(scriptSig);
|
||||
}
|
||||
|
||||
return transaction;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns signed Bitcoin transaction claiming refund from P2SH address.
|
||||
*
|
||||
* @param refundAmount refund amount, should be total of input amounts, less miner fees
|
||||
* @param refundKey key for signing transaction, and also where refund is 'sent' (output)
|
||||
* @param fundingOutput output from transaction that funded P2SH address
|
||||
* @param redeemScriptBytes the redeemScript itself, in byte[] form
|
||||
* @param lockTime transaction nLockTime - must be at least locktime used in redeemScript
|
||||
* @return Signed Bitcoin transaction for refunding P2SH
|
||||
*/
|
||||
public static Transaction buildRefundTransaction(Coin refundAmount, ECKey refundKey, List<TransactionOutput> fundingOutputs, byte[] redeemScriptBytes, long lockTime) {
|
||||
Function<byte[], Script> refundSigScriptBuilder = (txSigBytes) -> {
|
||||
// Build scriptSig with...
|
||||
ScriptBuilder scriptBuilder = new ScriptBuilder();
|
||||
|
||||
// transaction signature
|
||||
scriptBuilder.addChunk(new ScriptChunk(txSigBytes.length, txSigBytes));
|
||||
|
||||
// redeem public key
|
||||
byte[] refundPubKey = refundKey.getPubKey();
|
||||
scriptBuilder.addChunk(new ScriptChunk(refundPubKey.length, refundPubKey));
|
||||
|
||||
// redeem script
|
||||
scriptBuilder.addChunk(new ScriptChunk(ScriptOpCodes.OP_PUSHDATA1, redeemScriptBytes));
|
||||
|
||||
return scriptBuilder.build();
|
||||
};
|
||||
|
||||
// Send funds back to funding address
|
||||
return buildP2shTransaction(refundAmount, refundKey, fundingOutputs, redeemScriptBytes, lockTime, refundSigScriptBuilder, refundKey.getPubKeyHash());
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns signed Bitcoin transaction redeeming funds from P2SH address.
|
||||
*
|
||||
* @param redeemAmount redeem amount, should be total of input amounts, less miner fees
|
||||
* @param redeemKey key for signing transaction, and also where funds are 'sent' (output)
|
||||
* @param fundingOutput output from transaction that funded P2SH address
|
||||
* @param redeemScriptBytes the redeemScript itself, in byte[] form
|
||||
* @param secret actual 32-byte secret used when building redeemScript
|
||||
* @param receivingAccountInfo Bitcoin PKH used for output
|
||||
* @return Signed Bitcoin transaction for redeeming P2SH
|
||||
*/
|
||||
public static Transaction buildRedeemTransaction(Coin redeemAmount, ECKey redeemKey, List<TransactionOutput> fundingOutputs, byte[] redeemScriptBytes, byte[] secret, byte[] receivingAccountInfo) {
|
||||
Function<byte[], Script> redeemSigScriptBuilder = (txSigBytes) -> {
|
||||
// Build scriptSig with...
|
||||
ScriptBuilder scriptBuilder = new ScriptBuilder();
|
||||
|
||||
// secret
|
||||
scriptBuilder.addChunk(new ScriptChunk(secret.length, secret));
|
||||
|
||||
// transaction signature
|
||||
scriptBuilder.addChunk(new ScriptChunk(txSigBytes.length, txSigBytes));
|
||||
|
||||
// redeem public key
|
||||
byte[] redeemPubKey = redeemKey.getPubKey();
|
||||
scriptBuilder.addChunk(new ScriptChunk(redeemPubKey.length, redeemPubKey));
|
||||
|
||||
// redeem script
|
||||
scriptBuilder.addChunk(new ScriptChunk(ScriptOpCodes.OP_PUSHDATA1, redeemScriptBytes));
|
||||
|
||||
return scriptBuilder.build();
|
||||
};
|
||||
|
||||
return buildP2shTransaction(redeemAmount, redeemKey, fundingOutputs, redeemScriptBytes, null, redeemSigScriptBuilder, receivingAccountInfo);
|
||||
}
|
||||
|
||||
/** Returns 'secret', if any, given list of raw bitcoin transactions. */
|
||||
public static byte[] findP2shSecret(String p2shAddress, List<byte[]> rawTransactions) {
|
||||
NetworkParameters params = BTC.getInstance().getNetworkParameters();
|
||||
|
||||
for (byte[] rawTransaction : rawTransactions) {
|
||||
Transaction transaction = new Transaction(params, rawTransaction);
|
||||
|
||||
// Cycle through inputs, looking for one that spends our P2SH
|
||||
for (TransactionInput input : transaction.getInputs()) {
|
||||
Script scriptSig = input.getScriptSig();
|
||||
List<ScriptChunk> scriptChunks = scriptSig.getChunks();
|
||||
|
||||
// Expected number of script chunks for redeem. Refund might not have the same number.
|
||||
int expectedChunkCount = 1 /*secret*/ + 1 /*sig*/ + 1 /*pubkey*/ + 1 /*redeemScript*/;
|
||||
if (scriptChunks.size() != expectedChunkCount)
|
||||
continue;
|
||||
|
||||
// We're expecting last chunk to contain the actual redeemScript
|
||||
ScriptChunk lastChunk = scriptChunks.get(scriptChunks.size() - 1);
|
||||
byte[] redeemScriptBytes = lastChunk.data;
|
||||
|
||||
// If non-push scripts, redeemScript will be null
|
||||
if (redeemScriptBytes == null)
|
||||
continue;
|
||||
|
||||
byte[] redeemScriptHash = Crypto.hash160(redeemScriptBytes);
|
||||
Address inputAddress = LegacyAddress.fromScriptHash(params, redeemScriptHash);
|
||||
|
||||
if (!inputAddress.toString().equals(p2shAddress))
|
||||
// Input isn't spending our P2SH
|
||||
continue;
|
||||
|
||||
byte[] secret = scriptChunks.get(0).data;
|
||||
if (secret.length != BTCP2SH.SECRET_LENGTH)
|
||||
continue;
|
||||
|
||||
return secret;
|
||||
}
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
}
|
@@ -25,11 +25,11 @@ import org.json.simple.JSONObject;
|
||||
import org.json.simple.JSONValue;
|
||||
import org.qortal.crypto.Crypto;
|
||||
import org.qortal.crypto.TrustlessSSLSocketFactory;
|
||||
import org.qortal.utils.Pair;
|
||||
|
||||
import com.google.common.hash.HashCode;
|
||||
import com.google.common.primitives.Bytes;
|
||||
|
||||
/** ElectrumX network support for querying Bitcoin-related info like block headers, transaction outputs, etc. */
|
||||
public class ElectrumX {
|
||||
|
||||
private static final Logger LOGGER = LogManager.getLogger(ElectrumX.class);
|
||||
@@ -93,7 +93,21 @@ public class ElectrumX {
|
||||
private ElectrumX(String bitcoinNetwork) {
|
||||
switch (bitcoinNetwork) {
|
||||
case "MAIN":
|
||||
servers.addAll(Arrays.asList());
|
||||
servers.addAll(Arrays.asList(
|
||||
// Servers chosen on NO BASIS WHATSOEVER from various sources!
|
||||
new Server("tardis.bauerj.eu", Server.ConnectionType.SSL, 50002),
|
||||
new Server("rbx.curalle.ovh", Server.ConnectionType.SSL, 50002),
|
||||
new Server("quick.electumx.live", Server.ConnectionType.SSL, 50002),
|
||||
new Server("enode.duckdns.org", Server.ConnectionType.SSL, 50002),
|
||||
new Server("electrumx.ddns.net", Server.ConnectionType.SSL, 50002),
|
||||
new Server("electrumx.ml", Server.ConnectionType.SSL, 50002),
|
||||
new Server("electrum.eff.ro", Server.ConnectionType.SSL, 50002),
|
||||
new Server("electrum.bitkoins.nl", Server.ConnectionType.SSL, 50512),
|
||||
new Server("E-X.not.fyi", Server.ConnectionType.SSL, 50002),
|
||||
new Server("btc.electroncash.dk", Server.ConnectionType.SSL, 60002),
|
||||
new Server("electrum.blockstream.info", Server.ConnectionType.TCP, 50001),
|
||||
new Server("electrum.blockstream.info", Server.ConnectionType.SSL, 50002),
|
||||
new Server("bitcoin.aranguren.org", Server.ConnectionType.TCP, 50001)));
|
||||
break;
|
||||
|
||||
case "TEST3":
|
||||
@@ -119,6 +133,7 @@ public class ElectrumX {
|
||||
rpc("server.banner");
|
||||
}
|
||||
|
||||
/** Returns ElectrumX instance linked to passed Bitcoin network, one of "MAIN", "TEST3" or "REGTEST". */
|
||||
public static synchronized ElectrumX getInstance(String bitcoinNetwork) {
|
||||
if (!instances.containsKey(bitcoinNetwork))
|
||||
instances.put(bitcoinNetwork, new ElectrumX(bitcoinNetwork));
|
||||
@@ -129,16 +144,26 @@ public class ElectrumX {
|
||||
// Methods for use by other classes
|
||||
|
||||
public Integer getCurrentHeight() {
|
||||
JSONObject blockJson = (JSONObject) this.rpc("blockchain.headers.subscribe");
|
||||
if (blockJson == null || !blockJson.containsKey("height"))
|
||||
Object blockObj = this.rpc("blockchain.headers.subscribe");
|
||||
if (!(blockObj instanceof JSONObject))
|
||||
return null;
|
||||
|
||||
JSONObject blockJson = (JSONObject) blockObj;
|
||||
|
||||
if (!blockJson.containsKey("height"))
|
||||
return null;
|
||||
|
||||
return ((Long) blockJson.get("height")).intValue();
|
||||
}
|
||||
|
||||
public List<byte[]> getBlockHeaders(int startHeight, long count) {
|
||||
JSONObject blockJson = (JSONObject) this.rpc("blockchain.block.headers", startHeight, count);
|
||||
if (blockJson == null || !blockJson.containsKey("count") || !blockJson.containsKey("hex"))
|
||||
Object blockObj = this.rpc("blockchain.block.headers", startHeight, count);
|
||||
if (!(blockObj instanceof JSONObject))
|
||||
return null;
|
||||
|
||||
JSONObject blockJson = (JSONObject) blockObj;
|
||||
|
||||
if (!blockJson.containsKey("count") || !blockJson.containsKey("hex"))
|
||||
return null;
|
||||
|
||||
Long returnedCount = (Long) blockJson.get("count");
|
||||
@@ -155,57 +180,87 @@ public class ElectrumX {
|
||||
return rawBlockHeaders;
|
||||
}
|
||||
|
||||
/** Returns confirmed balance, based on passed payment script, or null if there was an error or no known balance. */
|
||||
public Long getBalance(byte[] script) {
|
||||
byte[] scriptHash = Crypto.digest(script);
|
||||
Bytes.reverse(scriptHash);
|
||||
|
||||
JSONObject balanceJson = (JSONObject) this.rpc("blockchain.scripthash.get_balance", HashCode.fromBytes(scriptHash).toString());
|
||||
if (balanceJson == null || !balanceJson.containsKey("confirmed"))
|
||||
Object balanceObj = this.rpc("blockchain.scripthash.get_balance", HashCode.fromBytes(scriptHash).toString());
|
||||
if (!(balanceObj instanceof JSONObject))
|
||||
return null;
|
||||
|
||||
JSONObject balanceJson = (JSONObject) balanceObj;
|
||||
|
||||
if (!balanceJson.containsKey("confirmed"))
|
||||
return null;
|
||||
|
||||
return (Long) balanceJson.get("confirmed");
|
||||
}
|
||||
|
||||
public List<Pair<byte[], Integer>> getUnspentOutputs(byte[] script) {
|
||||
/** Unspent output info as returned by ElectrumX network. */
|
||||
public static class UnspentOutput {
|
||||
public final byte[] hash;
|
||||
public final int index;
|
||||
public final int height;
|
||||
public final long value;
|
||||
|
||||
public UnspentOutput(byte[] hash, int index, int height, long value) {
|
||||
this.hash = hash;
|
||||
this.index = index;
|
||||
this.height = height;
|
||||
this.value = value;
|
||||
}
|
||||
}
|
||||
|
||||
/** Returns list of unspent outputs pertaining to passed payment script, or null if there was an error. */
|
||||
public List<UnspentOutput> getUnspentOutputs(byte[] script) {
|
||||
byte[] scriptHash = Crypto.digest(script);
|
||||
Bytes.reverse(scriptHash);
|
||||
|
||||
JSONArray unspentJson = (JSONArray) this.rpc("blockchain.scripthash.listunspent", HashCode.fromBytes(scriptHash).toString());
|
||||
if (unspentJson == null)
|
||||
Object unspentJson = this.rpc("blockchain.scripthash.listunspent", HashCode.fromBytes(scriptHash).toString());
|
||||
if (!(unspentJson instanceof JSONArray))
|
||||
return null;
|
||||
|
||||
List<Pair<byte[], Integer>> unspentOutputs = new ArrayList<>();
|
||||
for (Object rawUnspent : unspentJson) {
|
||||
List<UnspentOutput> unspentOutputs = new ArrayList<>();
|
||||
for (Object rawUnspent : (JSONArray) unspentJson) {
|
||||
JSONObject unspent = (JSONObject) rawUnspent;
|
||||
|
||||
int height = ((Long) unspent.get("height")).intValue();
|
||||
// We only want unspent outputs from confirmed transactions (and definitely not mempool duplicates with height 0)
|
||||
if (height <= 0)
|
||||
continue;
|
||||
|
||||
byte[] txHash = HashCode.fromString((String) unspent.get("tx_hash")).asBytes();
|
||||
int outputIndex = ((Long) unspent.get("tx_pos")).intValue();
|
||||
long value = (Long) unspent.get("value");
|
||||
|
||||
unspentOutputs.add(new Pair<>(txHash, outputIndex));
|
||||
unspentOutputs.add(new UnspentOutput(txHash, outputIndex, height, value));
|
||||
}
|
||||
|
||||
return unspentOutputs;
|
||||
}
|
||||
|
||||
/** Returns raw transaction for passed transaction hash, or null if not found. */
|
||||
public byte[] getRawTransaction(byte[] txHash) {
|
||||
String rawTransactionHex = (String) this.rpc("blockchain.transaction.get", HashCode.fromBytes(txHash).toString());
|
||||
if (rawTransactionHex == null)
|
||||
Object rawTransactionHex = this.rpc("blockchain.transaction.get", HashCode.fromBytes(txHash).toString());
|
||||
if (!(rawTransactionHex instanceof String))
|
||||
return null;
|
||||
|
||||
return HashCode.fromString(rawTransactionHex).asBytes();
|
||||
return HashCode.fromString((String) rawTransactionHex).asBytes();
|
||||
}
|
||||
|
||||
/** Returns list of raw transactions, relating to passed payment script, if null if there's an error. */
|
||||
public List<byte[]> getAddressTransactions(byte[] script) {
|
||||
byte[] scriptHash = Crypto.digest(script);
|
||||
Bytes.reverse(scriptHash);
|
||||
|
||||
JSONArray transactionsJson = (JSONArray) this.rpc("blockchain.scripthash.get_history", HashCode.fromBytes(scriptHash).toString());
|
||||
if (transactionsJson == null)
|
||||
Object transactionsJson = this.rpc("blockchain.scripthash.get_history", HashCode.fromBytes(scriptHash).toString());
|
||||
if (!(transactionsJson instanceof JSONArray))
|
||||
return null;
|
||||
|
||||
List<byte[]> rawTransactions = new ArrayList<>();
|
||||
|
||||
for (Object rawTransactionInfo : transactionsJson) {
|
||||
for (Object rawTransactionInfo : (JSONArray) transactionsJson) {
|
||||
JSONObject transactionInfo = (JSONObject) rawTransactionInfo;
|
||||
|
||||
// We only want confirmed transactions
|
||||
@@ -223,6 +278,7 @@ public class ElectrumX {
|
||||
return rawTransactions;
|
||||
}
|
||||
|
||||
/** Returns true if raw transaction successfully broadcast. */
|
||||
public boolean broadcastTransaction(byte[] transactionBytes) {
|
||||
Object rawBroadcastResult = this.rpc("blockchain.transaction.broadcast", HashCode.fromBytes(transactionBytes).toString());
|
||||
if (rawBroadcastResult == null)
|
||||
@@ -235,14 +291,15 @@ public class ElectrumX {
|
||||
|
||||
// Class-private utility methods
|
||||
|
||||
/** Query current server for its list of peer servers, and return those we can parse. */
|
||||
private Set<Server> serverPeersSubscribe() {
|
||||
Set<Server> newServers = new HashSet<>();
|
||||
|
||||
JSONArray peers = (JSONArray) this.connectedRpc("server.peers.subscribe");
|
||||
if (peers == null)
|
||||
Object peers = this.connectedRpc("server.peers.subscribe");
|
||||
if (!(peers instanceof JSONArray))
|
||||
return newServers;
|
||||
|
||||
for (Object rawPeer : peers) {
|
||||
for (Object rawPeer : (JSONArray) peers) {
|
||||
JSONArray peer = (JSONArray) rawPeer;
|
||||
if (peer.size() < 3)
|
||||
continue;
|
||||
@@ -287,6 +344,7 @@ public class ElectrumX {
|
||||
return newServers;
|
||||
}
|
||||
|
||||
/** Return output from RPC call, with automatic reconnection to different server if needed. */
|
||||
private synchronized Object rpc(String method, Object...params) {
|
||||
while (haveConnection()) {
|
||||
Object response = connectedRpc(method, params);
|
||||
@@ -305,6 +363,7 @@ public class ElectrumX {
|
||||
return null;
|
||||
}
|
||||
|
||||
/** Returns true if we have, or create, a connection to an ElectrumX server. */
|
||||
private boolean haveConnection() {
|
||||
if (this.currentServer != null)
|
||||
return true;
|
||||
@@ -377,10 +436,12 @@ public class ElectrumX {
|
||||
if (response.isEmpty())
|
||||
return null;
|
||||
|
||||
JSONObject responseJson = (JSONObject) JSONValue.parse(response);
|
||||
if (responseJson == null)
|
||||
Object responseObj = JSONValue.parse(response);
|
||||
if (!(responseObj instanceof JSONObject))
|
||||
return null;
|
||||
|
||||
JSONObject responseJson = (JSONObject) responseObj;
|
||||
|
||||
return responseJson.get("result");
|
||||
}
|
||||
|
||||
|
@@ -1,11 +1,15 @@
|
||||
package org.qortal.data.account;
|
||||
|
||||
import java.math.BigDecimal;
|
||||
|
||||
import javax.xml.bind.annotation.XmlAccessType;
|
||||
import javax.xml.bind.annotation.XmlAccessorType;
|
||||
import javax.xml.bind.annotation.XmlElement;
|
||||
import javax.xml.bind.annotation.XmlTransient;
|
||||
import javax.xml.bind.annotation.adapters.XmlJavaTypeAdapter;
|
||||
|
||||
import org.qortal.utils.Base58;
|
||||
|
||||
import io.swagger.v3.oas.annotations.media.Schema;
|
||||
|
||||
// All properties to be converted to JSON via JAXB
|
||||
@@ -71,4 +75,13 @@ public class RewardShareData {
|
||||
return this.minter;
|
||||
}
|
||||
|
||||
// For debugging
|
||||
|
||||
public String toString() {
|
||||
if (this.minter.equals(this.recipient))
|
||||
return String.format("Minter/recipient: %s, reward-share public key: %s", this.minter, Base58.encode(this.rewardSharePublicKey));
|
||||
else
|
||||
return String.format("Minter: %s, recipient: %s (%s %%), reward-share public key: %s", this.minter, this.recipient, BigDecimal.valueOf(this.sharePercent, 2), Base58.encode(this.rewardSharePublicKey));
|
||||
}
|
||||
|
||||
}
|
||||
|
@@ -4,14 +4,14 @@ import javax.xml.bind.annotation.XmlAccessType;
|
||||
import javax.xml.bind.annotation.XmlAccessorType;
|
||||
import javax.xml.bind.annotation.adapters.XmlJavaTypeAdapter;
|
||||
|
||||
import org.qortal.crosschain.BTCACCT;
|
||||
|
||||
import io.swagger.v3.oas.annotations.media.Schema;
|
||||
|
||||
// All properties to be converted to JSON via JAXB
|
||||
@XmlAccessorType(XmlAccessType.FIELD)
|
||||
public class CrossChainTradeData {
|
||||
|
||||
public enum Mode { OFFER, TRADE };
|
||||
|
||||
// Properties
|
||||
|
||||
@Schema(description = "AT's Qortal address")
|
||||
@@ -20,32 +20,40 @@ public class CrossChainTradeData {
|
||||
@Schema(description = "AT creator's Qortal address")
|
||||
public String qortalCreator;
|
||||
|
||||
@Schema(description = "AT creator's Qortal trade address")
|
||||
public String qortalCreatorTradeAddress;
|
||||
|
||||
@Schema(description = "AT creator's Bitcoin trade public-key-hash (PKH)")
|
||||
public byte[] creatorBitcoinPKH;
|
||||
|
||||
@Schema(description = "Timestamp when AT was created (milliseconds since epoch)")
|
||||
public long creationTimestamp;
|
||||
|
||||
@Schema(description = "Suggested trade timeout (minutes)", example = "10080")
|
||||
public int tradeTimeout;
|
||||
|
||||
@Schema(description = "AT's current QORT balance")
|
||||
@XmlJavaTypeAdapter(value = org.qortal.api.AmountTypeAdapter.class)
|
||||
public long qortBalance;
|
||||
|
||||
@Schema(description = "HASH160 of 32-byte secret")
|
||||
public byte[] secretHash;
|
||||
@Schema(description = "HASH160 of 32-byte secret-A")
|
||||
public byte[] hashOfSecretA;
|
||||
|
||||
@Schema(description = "Initial QORT payment that will be sent to Qortal trade partner")
|
||||
@XmlJavaTypeAdapter(value = org.qortal.api.AmountTypeAdapter.class)
|
||||
public long initialPayout;
|
||||
@Schema(description = "HASH160 of 32-byte secret-B")
|
||||
public byte[] hashOfSecretB;
|
||||
|
||||
@Schema(description = "Final QORT payment that will be sent to Qortal trade partner")
|
||||
@XmlJavaTypeAdapter(value = org.qortal.api.AmountTypeAdapter.class)
|
||||
public long redeemPayout;
|
||||
public long qortAmount;
|
||||
|
||||
@Schema(description = "Trade partner's Qortal address (trade begins when this is set)")
|
||||
public String qortalRecipient;
|
||||
public String qortalPartnerAddress;
|
||||
|
||||
@Schema(description = "Timestamp when AT switched to trade mode")
|
||||
public Long tradeModeTimestamp;
|
||||
|
||||
@Schema(description = "How long from beginning trade until AT triggers automatic refund to AT creator (minutes)")
|
||||
public long tradeRefundTimeout;
|
||||
@Schema(description = "How long from AT creation until AT triggers automatic refund to AT creator (minutes)")
|
||||
public Integer refundTimeout;
|
||||
|
||||
@Schema(description = "Actual Qortal block height when AT will automatically refund to AT creator (after trade begins)")
|
||||
public Integer tradeRefundHeight;
|
||||
@@ -54,10 +62,19 @@ public class CrossChainTradeData {
|
||||
@XmlJavaTypeAdapter(value = org.qortal.api.AmountTypeAdapter.class)
|
||||
public long expectedBitcoin;
|
||||
|
||||
public Mode mode;
|
||||
public BTCACCT.Mode mode;
|
||||
|
||||
@Schema(description = "Suggested Bitcoin P2SH nLockTime based on trade timeout")
|
||||
public Integer lockTime;
|
||||
@Schema(description = "Suggested Bitcoin P2SH-A nLockTime based on trade timeout")
|
||||
public Integer lockTimeA;
|
||||
|
||||
@Schema(description = "Suggested Bitcoin P2SH-B nLockTime based on trade timeout")
|
||||
public Integer lockTimeB;
|
||||
|
||||
@Schema(description = "Trade partner's Bitcoin public-key-hash (PKH)")
|
||||
public byte[] partnerBitcoinPKH;
|
||||
|
||||
@Schema(description = "Trade partner's Qortal receiving address")
|
||||
public String qortalPartnerReceivingAddress;
|
||||
|
||||
// Constructors
|
||||
|
||||
|
193
src/main/java/org/qortal/data/crosschain/TradeBotData.java
Normal file
193
src/main/java/org/qortal/data/crosschain/TradeBotData.java
Normal file
@@ -0,0 +1,193 @@
|
||||
package org.qortal.data.crosschain;
|
||||
|
||||
import static java.util.Arrays.stream;
|
||||
import static java.util.stream.Collectors.toMap;
|
||||
|
||||
import java.util.Map;
|
||||
|
||||
import javax.xml.bind.annotation.XmlAccessType;
|
||||
import javax.xml.bind.annotation.XmlAccessorType;
|
||||
import javax.xml.bind.annotation.XmlTransient;
|
||||
import javax.xml.bind.annotation.adapters.XmlJavaTypeAdapter;
|
||||
|
||||
import io.swagger.v3.oas.annotations.media.Schema;
|
||||
|
||||
// All properties to be converted to JSON via JAXB
|
||||
@XmlAccessorType(XmlAccessType.FIELD)
|
||||
public class TradeBotData {
|
||||
|
||||
private byte[] tradePrivateKey;
|
||||
|
||||
public enum State {
|
||||
BOB_WAITING_FOR_AT_CONFIRM(10), BOB_WAITING_FOR_MESSAGE(15), BOB_WAITING_FOR_P2SH_B(20), BOB_WAITING_FOR_AT_REDEEM(25), BOB_DONE(30), BOB_REFUNDED(35),
|
||||
ALICE_WAITING_FOR_P2SH_A(80), ALICE_WAITING_FOR_AT_LOCK(85), ALICE_WATCH_P2SH_B(90), ALICE_DONE(95), ALICE_REFUNDING_B(100), ALICE_REFUNDING_A(105), ALICE_REFUNDED(110);
|
||||
|
||||
public final int value;
|
||||
private static final Map<Integer, State> map = stream(State.values()).collect(toMap(state -> state.value, state -> state));
|
||||
|
||||
State(int value) {
|
||||
this.value = value;
|
||||
}
|
||||
|
||||
public static State valueOf(int value) {
|
||||
return map.get(value);
|
||||
}
|
||||
}
|
||||
private State tradeState;
|
||||
|
||||
private String creatorAddress;
|
||||
private String atAddress;
|
||||
|
||||
private long timestamp;
|
||||
|
||||
@XmlJavaTypeAdapter(value = org.qortal.api.AmountTypeAdapter.class)
|
||||
private long qortAmount;
|
||||
|
||||
private byte[] tradeNativePublicKey;
|
||||
private byte[] tradeNativePublicKeyHash;
|
||||
String tradeNativeAddress;
|
||||
|
||||
private byte[] secret;
|
||||
private byte[] hashOfSecret;
|
||||
|
||||
private byte[] tradeForeignPublicKey;
|
||||
private byte[] tradeForeignPublicKeyHash;
|
||||
|
||||
@XmlJavaTypeAdapter(value = org.qortal.api.AmountTypeAdapter.class)
|
||||
private long bitcoinAmount;
|
||||
|
||||
// Never expose this via API
|
||||
@XmlTransient
|
||||
@Schema(hidden = true)
|
||||
private String xprv58;
|
||||
|
||||
private byte[] lastTransactionSignature;
|
||||
|
||||
private Integer lockTimeA;
|
||||
|
||||
// Could be Bitcoin or Qortal...
|
||||
private byte[] receivingAccountInfo;
|
||||
|
||||
protected TradeBotData() {
|
||||
/* JAXB */
|
||||
}
|
||||
|
||||
public TradeBotData(byte[] tradePrivateKey, State tradeState, String creatorAddress, String atAddress,
|
||||
long timestamp, long qortAmount,
|
||||
byte[] tradeNativePublicKey, byte[] tradeNativePublicKeyHash, String tradeNativeAddress,
|
||||
byte[] secret, byte[] hashOfSecret,
|
||||
byte[] tradeForeignPublicKey, byte[] tradeForeignPublicKeyHash,
|
||||
long bitcoinAmount, String xprv58, byte[] lastTransactionSignature, Integer lockTimeA, byte[] receivingAccountInfo) {
|
||||
this.tradePrivateKey = tradePrivateKey;
|
||||
this.tradeState = tradeState;
|
||||
this.creatorAddress = creatorAddress;
|
||||
this.atAddress = atAddress;
|
||||
this.timestamp = timestamp;
|
||||
this.qortAmount = qortAmount;
|
||||
this.tradeNativePublicKey = tradeNativePublicKey;
|
||||
this.tradeNativePublicKeyHash = tradeNativePublicKeyHash;
|
||||
this.tradeNativeAddress = tradeNativeAddress;
|
||||
this.secret = secret;
|
||||
this.hashOfSecret = hashOfSecret;
|
||||
this.tradeForeignPublicKey = tradeForeignPublicKey;
|
||||
this.tradeForeignPublicKeyHash = tradeForeignPublicKeyHash;
|
||||
this.bitcoinAmount = bitcoinAmount;
|
||||
this.xprv58 = xprv58;
|
||||
this.lastTransactionSignature = lastTransactionSignature;
|
||||
this.lockTimeA = lockTimeA;
|
||||
this.receivingAccountInfo = receivingAccountInfo;
|
||||
}
|
||||
|
||||
public byte[] getTradePrivateKey() {
|
||||
return this.tradePrivateKey;
|
||||
}
|
||||
|
||||
public State getState() {
|
||||
return this.tradeState;
|
||||
}
|
||||
|
||||
public void setState(State state) {
|
||||
this.tradeState = state;
|
||||
}
|
||||
|
||||
public String getCreatorAddress() {
|
||||
return this.creatorAddress;
|
||||
}
|
||||
|
||||
public String getAtAddress() {
|
||||
return this.atAddress;
|
||||
}
|
||||
|
||||
public void setAtAddress(String atAddress) {
|
||||
this.atAddress = atAddress;
|
||||
}
|
||||
|
||||
public long getTimestamp() {
|
||||
return this.timestamp;
|
||||
}
|
||||
|
||||
public void setTimestamp(long timestamp) {
|
||||
this.timestamp = timestamp;
|
||||
}
|
||||
|
||||
public long getQortAmount() {
|
||||
return this.qortAmount;
|
||||
}
|
||||
|
||||
public byte[] getTradeNativePublicKey() {
|
||||
return this.tradeNativePublicKey;
|
||||
}
|
||||
|
||||
public byte[] getTradeNativePublicKeyHash() {
|
||||
return this.tradeNativePublicKeyHash;
|
||||
}
|
||||
|
||||
public String getTradeNativeAddress() {
|
||||
return this.tradeNativeAddress;
|
||||
}
|
||||
|
||||
public byte[] getSecret() {
|
||||
return this.secret;
|
||||
}
|
||||
|
||||
public byte[] getHashOfSecret() {
|
||||
return this.hashOfSecret;
|
||||
}
|
||||
|
||||
public byte[] getTradeForeignPublicKey() {
|
||||
return this.tradeForeignPublicKey;
|
||||
}
|
||||
|
||||
public byte[] getTradeForeignPublicKeyHash() {
|
||||
return this.tradeForeignPublicKeyHash;
|
||||
}
|
||||
|
||||
public long getBitcoinAmount() {
|
||||
return this.bitcoinAmount;
|
||||
}
|
||||
|
||||
public String getXprv58() {
|
||||
return this.xprv58;
|
||||
}
|
||||
|
||||
public byte[] getLastTransactionSignature() {
|
||||
return this.lastTransactionSignature;
|
||||
}
|
||||
|
||||
public void setLastTransactionSignature(byte[] lastTransactionSignature) {
|
||||
this.lastTransactionSignature = lastTransactionSignature;
|
||||
}
|
||||
|
||||
public Integer getLockTimeA() {
|
||||
return this.lockTimeA;
|
||||
}
|
||||
|
||||
public void setLockTimeA(Integer lockTimeA) {
|
||||
this.lockTimeA = lockTimeA;
|
||||
}
|
||||
|
||||
public byte[] getReceivingAccountInfo() {
|
||||
return this.receivingAccountInfo;
|
||||
}
|
||||
|
||||
}
|
5
src/main/java/org/qortal/event/Event.java
Normal file
5
src/main/java/org/qortal/event/Event.java
Normal file
@@ -0,0 +1,5 @@
|
||||
package org.qortal.event;
|
||||
|
||||
public interface Event {
|
||||
|
||||
}
|
33
src/main/java/org/qortal/event/EventBus.java
Normal file
33
src/main/java/org/qortal/event/EventBus.java
Normal file
@@ -0,0 +1,33 @@
|
||||
package org.qortal.event;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
|
||||
public enum EventBus {
|
||||
INSTANCE;
|
||||
|
||||
private static final List<Listener> LISTENERS = new ArrayList<>();
|
||||
|
||||
public void addListener(Listener newListener) {
|
||||
synchronized (LISTENERS) {
|
||||
LISTENERS.add(newListener);
|
||||
}
|
||||
}
|
||||
|
||||
public void removeListener(Listener listener) {
|
||||
synchronized (LISTENERS) {
|
||||
LISTENERS.remove(listener);
|
||||
}
|
||||
}
|
||||
|
||||
public void notify(Event event) {
|
||||
List<Listener> clonedListeners;
|
||||
|
||||
synchronized (LISTENERS) {
|
||||
clonedListeners = new ArrayList<>(LISTENERS);
|
||||
}
|
||||
|
||||
for (Listener listener : clonedListeners)
|
||||
listener.listen(event);
|
||||
}
|
||||
}
|
6
src/main/java/org/qortal/event/Listener.java
Normal file
6
src/main/java/org/qortal/event/Listener.java
Normal file
@@ -0,0 +1,6 @@
|
||||
package org.qortal.event;
|
||||
|
||||
@FunctionalInterface
|
||||
public interface Listener {
|
||||
void listen(Event event);
|
||||
}
|
@@ -1,6 +1,8 @@
|
||||
package org.qortal.network;
|
||||
|
||||
import java.util.Arrays;
|
||||
import java.util.concurrent.ExecutorService;
|
||||
import java.util.concurrent.Executors;
|
||||
import java.util.regex.Matcher;
|
||||
import java.util.regex.Pattern;
|
||||
|
||||
@@ -13,7 +15,9 @@ import org.qortal.network.message.ChallengeMessage;
|
||||
import org.qortal.network.message.HelloMessage;
|
||||
import org.qortal.network.message.Message;
|
||||
import org.qortal.network.message.Message.MessageType;
|
||||
import org.qortal.settings.Settings;
|
||||
import org.qortal.network.message.ResponseMessage;
|
||||
import org.qortal.utils.DaemonThreadFactory;
|
||||
import org.qortal.utils.NTP;
|
||||
|
||||
import com.google.common.primitives.Bytes;
|
||||
@@ -27,6 +31,7 @@ public enum Handshake {
|
||||
|
||||
@Override
|
||||
public void action(Peer peer) {
|
||||
/* Never called */
|
||||
}
|
||||
},
|
||||
HELLO(MessageType.HELLO) {
|
||||
@@ -157,13 +162,20 @@ public enum Handshake {
|
||||
}
|
||||
|
||||
int nonce = responseMessage.getNonce();
|
||||
if (!MemoryPoW.verify2(data, POW_BUFFER_SIZE, POW_DIFFICULTY, nonce)) {
|
||||
int powBufferSize = peer.getPeersVersion() < PEER_VERSION_131 ? POW_BUFFER_SIZE_PRE_131 : POW_BUFFER_SIZE_POST_131;
|
||||
int powDifficulty = peer.getPeersVersion() < PEER_VERSION_131 ? POW_DIFFICULTY_PRE_131 : POW_DIFFICULTY_POST_131;
|
||||
if (!MemoryPoW.verify2(data, powBufferSize, powDifficulty, nonce)) {
|
||||
LOGGER.debug(() -> String.format("Peer %s sent incorrect RESPONSE nonce", peer));
|
||||
return null;
|
||||
}
|
||||
|
||||
peer.setPeersNodeId(Crypto.toNodeAddress(peersPublicKey));
|
||||
|
||||
// For inbound peers, we need to go into interim holding state while we compute RESPONSE
|
||||
if (!peer.isOutbound())
|
||||
return RESPONDING;
|
||||
|
||||
// Handshake completed!
|
||||
return COMPLETED;
|
||||
}
|
||||
|
||||
@@ -178,28 +190,52 @@ public enum Handshake {
|
||||
final byte[] data = Crypto.digest(Bytes.concat(sharedSecret, peersChallenge));
|
||||
|
||||
// We do this in a new thread as it can take a while...
|
||||
Thread responseThread = new Thread(() -> {
|
||||
Integer nonce = MemoryPoW.compute2(data, POW_BUFFER_SIZE, POW_DIFFICULTY);
|
||||
responseExecutor.execute(() -> {
|
||||
// Are we still connected?
|
||||
if (peer.isStopping())
|
||||
// No point computing for dead peer
|
||||
return;
|
||||
|
||||
int powBufferSize = peer.getPeersVersion() < PEER_VERSION_131 ? POW_BUFFER_SIZE_PRE_131 : POW_BUFFER_SIZE_POST_131;
|
||||
int powDifficulty = peer.getPeersVersion() < PEER_VERSION_131 ? POW_DIFFICULTY_PRE_131 : POW_DIFFICULTY_POST_131;
|
||||
Integer nonce = MemoryPoW.compute2(data, powBufferSize, powDifficulty);
|
||||
|
||||
Message responseMessage = new ResponseMessage(nonce, data);
|
||||
if (!peer.sendMessage(responseMessage))
|
||||
peer.disconnect("failed to send RESPONSE");
|
||||
});
|
||||
|
||||
responseThread.setDaemon(true);
|
||||
responseThread.start();
|
||||
// For inbound peers, we should actually be in RESPONDING state.
|
||||
// So we need to do the extra work to move to COMPLETED state.
|
||||
if (!peer.isOutbound()) {
|
||||
peer.setHandshakeStatus(COMPLETED);
|
||||
Network.getInstance().onHandshakeCompleted(peer);
|
||||
}
|
||||
});
|
||||
}
|
||||
},
|
||||
COMPLETED(null) {
|
||||
// Interim holding state while we compute RESPONSE to send to inbound peer
|
||||
RESPONDING(null) {
|
||||
@Override
|
||||
public Handshake onMessage(Peer peer, Message message) {
|
||||
// Handshake completed
|
||||
// Should never be called
|
||||
return null;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void action(Peer peer) {
|
||||
// Note: this is only called when we've made outbound connection
|
||||
// Should never be called
|
||||
}
|
||||
},
|
||||
COMPLETED(null) {
|
||||
@Override
|
||||
public Handshake onMessage(Peer peer, Message message) {
|
||||
// Should never be called
|
||||
return null;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void action(Peer peer) {
|
||||
// Note: this is only called if we've made outbound connection
|
||||
}
|
||||
};
|
||||
|
||||
@@ -210,8 +246,16 @@ public enum Handshake {
|
||||
|
||||
private static final Pattern VERSION_PATTERN = Pattern.compile(Controller.VERSION_PREFIX + "(\\d{1,3})\\.(\\d{1,5})\\.(\\d{1,5})");
|
||||
|
||||
private static final int POW_BUFFER_SIZE = 8 * 1024 * 1024; // bytes
|
||||
private static final int POW_DIFFICULTY = 8; // leading zero bits
|
||||
private static final long PEER_VERSION_131 = 0x0100030001L;
|
||||
|
||||
private static final int POW_BUFFER_SIZE_PRE_131 = 8 * 1024 * 1024; // bytes
|
||||
private static final int POW_DIFFICULTY_PRE_131 = 8; // leading zero bits
|
||||
// Can always be made harder in the future...
|
||||
private static final int POW_BUFFER_SIZE_POST_131 = 2 * 1024 * 1024; // bytes
|
||||
private static final int POW_DIFFICULTY_POST_131 = 2; // leading zero bits
|
||||
|
||||
|
||||
private static final ExecutorService responseExecutor = Executors.newFixedThreadPool(Settings.getInstance().getNetworkPoWComputePoolSize(), new DaemonThreadFactory("Network-PoW"));
|
||||
|
||||
private static final byte[] ZERO_CHALLENGE = new byte[ChallengeMessage.CHALLENGE_LENGTH];
|
||||
|
||||
|
@@ -55,6 +55,7 @@ import org.qortal.utils.ExecuteProduceConsume;
|
||||
// import org.qortal.utils.ExecutorDumper;
|
||||
import org.qortal.utils.ExecuteProduceConsume.StatsSnapshot;
|
||||
import org.qortal.utils.NTP;
|
||||
import org.qortal.utils.NamedThreadFactory;
|
||||
|
||||
// For managing peers
|
||||
public class Network {
|
||||
@@ -80,16 +81,9 @@ public class Network {
|
||||
private static final byte[] TESTNET_MESSAGE_MAGIC = new byte[] { 0x71, 0x6f, 0x72, 0x54 }; // qorT
|
||||
|
||||
private static final String[] INITIAL_PEERS = new String[] {
|
||||
"node1.qortal.org",
|
||||
"node2.qortal.org",
|
||||
"node3.qortal.org",
|
||||
"node4.qortal.org",
|
||||
"node5.qortal.org",
|
||||
"node6.qortal.org",
|
||||
"node7.qortal.org",
|
||||
"node8.qortal.org",
|
||||
"node9.qortal.org",
|
||||
"node10.qortal.org"
|
||||
"node1.qortal.org", "node2.qortal.org", "node3.qortal.org", "node4.qortal.org", "node5.qortal.org",
|
||||
"node6.qortal.org", "node7.qortal.org", "node8.qortal.org", "node9.qortal.org", "node10.qortal.org",
|
||||
"node.qortal.ru", "node2.qortal.ru", "node3.qortal.ru", "node.qortal.uk"
|
||||
};
|
||||
|
||||
private static final long NETWORK_EPC_KEEPALIVE = 10L; // seconds
|
||||
@@ -151,7 +145,8 @@ public class Network {
|
||||
ExecutorService networkExecutor = new ThreadPoolExecutor(1,
|
||||
Settings.getInstance().getMaxNetworkThreadPoolSize(),
|
||||
NETWORK_EPC_KEEPALIVE, TimeUnit.SECONDS,
|
||||
new SynchronousQueue<Runnable>());
|
||||
new SynchronousQueue<Runnable>(),
|
||||
new NamedThreadFactory("Network-EPC"));
|
||||
networkEPC = new NetworkProcessor(networkExecutor);
|
||||
}
|
||||
|
||||
@@ -355,7 +350,7 @@ public class Network {
|
||||
|
||||
private Task maybeProducePeerPingTask(Long now) {
|
||||
// Ask connected peers whether they need a ping
|
||||
for (Peer peer : getConnectedPeers()) {
|
||||
for (Peer peer : getHandshakedPeers()) {
|
||||
Task peerTask = peer.getPingTask(now);
|
||||
if (peerTask != null)
|
||||
return peerTask;
|
||||
@@ -635,7 +630,7 @@ public class Network {
|
||||
|
||||
/** Called when Peer's thread has setup and is ready to process messages */
|
||||
public void onPeerReady(Peer peer) {
|
||||
this.onMessage(peer, null);
|
||||
onHandshakingMessage(peer, null, Handshake.STARTED);
|
||||
}
|
||||
|
||||
public void onDisconnect(Peer peer) {
|
||||
@@ -777,7 +772,7 @@ public class Network {
|
||||
opportunisticMergePeers(peer.toString(), peerV2Addresses);
|
||||
}
|
||||
|
||||
private void onHandshakeCompleted(Peer peer) {
|
||||
/*pacakge*/ void onHandshakeCompleted(Peer peer) {
|
||||
LOGGER.debug(String.format("Handshake completed with peer %s", peer));
|
||||
|
||||
// Are we already connected to this peer?
|
||||
|
@@ -372,9 +372,11 @@ public class Peer {
|
||||
if (message == null && bytesRead == 0 && !wasByteBufferFull) {
|
||||
// No complete message in buffer, no more bytes to read from socket even though there was room to read bytes
|
||||
|
||||
/* DISABLED
|
||||
// If byteBuffer is empty then we can deallocate it, to save memory, albeit costing GC
|
||||
if (this.byteBuffer.remaining() == this.byteBuffer.capacity())
|
||||
this.byteBuffer = null;
|
||||
*/
|
||||
|
||||
return;
|
||||
}
|
||||
|
@@ -15,6 +15,9 @@ public interface ATRepository {
|
||||
/** Returns where AT with passed address exists in repository */
|
||||
public boolean exists(String atAddress) throws DataException;
|
||||
|
||||
/** Returns AT creator's public key, or null if not found */
|
||||
public byte[] getCreatorPublicKey(String atAddress) throws DataException;
|
||||
|
||||
/** Returns list of executable ATs, empty if none found */
|
||||
public List<ATData> getAllExecutableATs() throws DataException;
|
||||
|
||||
@@ -54,6 +57,24 @@ public interface ATRepository {
|
||||
*/
|
||||
public ATStateData getLatestATState(String atAddress) throws DataException;
|
||||
|
||||
/**
|
||||
* Returns final ATStateData for ATs matching codeHash (required)
|
||||
* and specific data segment value (optional).
|
||||
* <p>
|
||||
* If searching for specific data segment value, both <tt>dataByteOffset</tt>
|
||||
* and <tt>expectedValue</tt> need to be non-null.
|
||||
* <p>
|
||||
* Note that <tt>dataByteOffset</tt> starts from 0 and will typically be
|
||||
* a multiple of <tt>MachineState.VALUE_SIZE</tt>, which is usually 8:
|
||||
* width of a long.
|
||||
* <p>
|
||||
* Although <tt>expectedValue</tt>, if provided, is natively an unsigned long,
|
||||
* the data segment comparison is done via unsigned hex string.
|
||||
*/
|
||||
public List<ATStateData> getMatchingFinalATStates(byte[] codeHash, Boolean isFinished,
|
||||
Integer dataByteOffset, Long expectedValue, Integer minimumFinalHeight,
|
||||
Integer limit, Integer offset, Boolean reverse) throws DataException;
|
||||
|
||||
/**
|
||||
* Returns all ATStateData for a given block height.
|
||||
* <p>
|
||||
@@ -88,4 +109,28 @@ public interface ATRepository {
|
||||
/** Delete state data for all ATs at this height */
|
||||
public void deleteATStates(int height) throws DataException;
|
||||
|
||||
// Finding transactions for ATs to process
|
||||
|
||||
static class NextTransactionInfo {
|
||||
public final int height;
|
||||
public final int sequence;
|
||||
public final byte[] signature;
|
||||
|
||||
public NextTransactionInfo(int height, int sequence, byte[] signature) {
|
||||
this.height = height;
|
||||
this.sequence = sequence;
|
||||
this.signature = signature;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Find next transaction for AT to process.
|
||||
* <p>
|
||||
* @param recipient AT address
|
||||
* @param height starting height
|
||||
* @param sequence starting sequence
|
||||
* @return next transaction info, or null if none found
|
||||
*/
|
||||
public NextTransactionInfo findNextTransaction(String recipient, int height, int sequence) throws DataException;
|
||||
|
||||
}
|
||||
|
@@ -169,8 +169,8 @@ public interface AccountRepository {
|
||||
|
||||
public void save(MintingAccountData mintingAccountData) throws DataException;
|
||||
|
||||
/** Delete minting account info, used by BlockMinter, from repository using passed private key. */
|
||||
public int delete(byte[] mintingAccountPrivateKey) throws DataException;
|
||||
/** Delete minting account info, used by BlockMinter, from repository using passed public or private key. */
|
||||
public int delete(byte[] mintingAccountKey) throws DataException;
|
||||
|
||||
// Managing QORT from legacy QORA
|
||||
|
||||
|
@@ -2,6 +2,7 @@ package org.qortal.repository;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
import org.qortal.api.model.BlockInfo;
|
||||
import org.qortal.api.model.BlockSignerSummary;
|
||||
import org.qortal.data.block.BlockData;
|
||||
import org.qortal.data.block.BlockSummaryData;
|
||||
@@ -60,6 +61,15 @@ public interface BlockRepository {
|
||||
*/
|
||||
public int getHeightFromTimestamp(long timestamp) throws DataException;
|
||||
|
||||
/**
|
||||
* Returns block timestamp for a given height.
|
||||
*
|
||||
* @param height
|
||||
* @return timestamp, or 0 if height is out of bounds.
|
||||
* @throws DataException
|
||||
*/
|
||||
public long getTimestampFromHeight(int height) throws DataException;
|
||||
|
||||
/**
|
||||
* Return highest block height from repository.
|
||||
*
|
||||
@@ -128,6 +138,11 @@ public interface BlockRepository {
|
||||
*/
|
||||
public List<BlockSummaryData> getBlockSummaries(int firstBlockHeight, int lastBlockHeight) throws DataException;
|
||||
|
||||
/**
|
||||
* Returns block infos for the passed height range, for API use.
|
||||
*/
|
||||
public List<BlockInfo> getBlockInfos(Integer startHeight, Integer endHeight, Integer count) throws DataException;
|
||||
|
||||
/**
|
||||
* Trim online accounts signatures from blocks older than passed timestamp.
|
||||
*
|
||||
@@ -137,11 +152,12 @@ public interface BlockRepository {
|
||||
public int trimOldOnlineAccountsSignatures(long timestamp) throws DataException;
|
||||
|
||||
/**
|
||||
* Returns first (lowest height) block that doesn't link back to genesis block.
|
||||
* Returns first (lowest height) block that doesn't link back to specified block.
|
||||
*
|
||||
* @param startHeight height of specified block
|
||||
* @throws DataException
|
||||
*/
|
||||
public BlockData getDetachedBlockSignature() throws DataException;
|
||||
public BlockData getDetachedBlockSignature(int startHeight) throws DataException;
|
||||
|
||||
/**
|
||||
* Saves block into repository.
|
||||
|
@@ -0,0 +1,18 @@
|
||||
package org.qortal.repository;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
import org.qortal.data.crosschain.TradeBotData;
|
||||
|
||||
public interface CrossChainRepository {
|
||||
|
||||
public TradeBotData getTradeBotData(byte[] tradePrivateKey) throws DataException;
|
||||
|
||||
public List<TradeBotData> getAllTradeBotData() throws DataException;
|
||||
|
||||
public void save(TradeBotData tradeBotData) throws DataException;
|
||||
|
||||
/** Delete trade-bot states using passed private key. */
|
||||
public int delete(byte[] tradePrivateKey) throws DataException;
|
||||
|
||||
}
|
@@ -14,6 +14,8 @@ public interface Repository extends AutoCloseable {
|
||||
|
||||
public ChatRepository getChatRepository();
|
||||
|
||||
public CrossChainRepository getCrossChainRepository();
|
||||
|
||||
public GroupRepository getGroupRepository();
|
||||
|
||||
public NameRepository getNameRepository();
|
||||
|
@@ -6,6 +6,7 @@ import java.util.Map;
|
||||
import org.qortal.api.resource.TransactionsResource.ConfirmationStatus;
|
||||
import org.qortal.data.group.GroupApprovalData;
|
||||
import org.qortal.data.transaction.GroupApprovalTransactionData;
|
||||
import org.qortal.data.transaction.MessageTransactionData;
|
||||
import org.qortal.data.transaction.TransactionData;
|
||||
import org.qortal.data.transaction.TransferAssetTransactionData;
|
||||
import org.qortal.transaction.Transaction.TransactionType;
|
||||
@@ -107,6 +108,18 @@ public interface TransactionRepository {
|
||||
*/
|
||||
public byte[] getLatestAutoUpdateTransaction(TransactionType txType, int txGroupId, Integer service) throws DataException;
|
||||
|
||||
/**
|
||||
* Returns list of MESSAGE transaction data matching recipient.
|
||||
* @param recipient
|
||||
* @param limit
|
||||
* @param offset
|
||||
* @param reverse
|
||||
* @return
|
||||
* @throws DataException
|
||||
*/
|
||||
public List<MessageTransactionData> getMessagesByRecipient(String recipient,
|
||||
Integer limit, Integer offset, Boolean reverse) throws DataException;
|
||||
|
||||
/**
|
||||
* Returns list of transactions relating to specific asset ID.
|
||||
*
|
||||
|
@@ -68,6 +68,20 @@ public class HSQLDBATRepository implements ATRepository {
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public byte[] getCreatorPublicKey(String atAddress) throws DataException {
|
||||
String sql = "SELECT creator FROM ATs WHERE AT_address = ?";
|
||||
|
||||
try (ResultSet resultSet = this.repository.checkedExecute(sql, atAddress)) {
|
||||
if (resultSet == null)
|
||||
return null;
|
||||
|
||||
return resultSet.getBytes(1);
|
||||
} catch (SQLException e) {
|
||||
throw new DataException("Unable to fetch AT creator's public key from repository", e);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public List<ATData> getAllExecutableATs() throws DataException {
|
||||
String sql = "SELECT AT_address, creator, created_when, version, asset_id, code_bytes, code_hash, "
|
||||
@@ -254,7 +268,8 @@ public class HSQLDBATRepository implements ATRepository {
|
||||
+ "FROM ATStates "
|
||||
+ "WHERE AT_address = ? "
|
||||
+ "ORDER BY height DESC "
|
||||
+ "LIMIT 1";
|
||||
+ "LIMIT 1 "
|
||||
+ "USING INDEX";
|
||||
|
||||
try (ResultSet resultSet = this.repository.checkedExecute(sql, atAddress)) {
|
||||
if (resultSet == null)
|
||||
@@ -273,6 +288,79 @@ public class HSQLDBATRepository implements ATRepository {
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public List<ATStateData> getMatchingFinalATStates(byte[] codeHash, Boolean isFinished,
|
||||
Integer dataByteOffset, Long expectedValue, Integer minimumFinalHeight,
|
||||
Integer limit, Integer offset, Boolean reverse) throws DataException {
|
||||
StringBuilder sql = new StringBuilder(1024);
|
||||
sql.append("SELECT AT_address, height, created_when, state_data, state_hash, fees, is_initial "
|
||||
+ "FROM ATs "
|
||||
+ "CROSS JOIN LATERAL("
|
||||
+ "SELECT height, created_when, state_data, state_hash, fees, is_initial "
|
||||
+ "FROM ATStates "
|
||||
+ "WHERE ATStates.AT_address = ATs.AT_address "
|
||||
+ "ORDER BY height DESC "
|
||||
+ "LIMIT 1 "
|
||||
+ "USING INDEX"
|
||||
+ ") AS FinalATStates "
|
||||
+ "WHERE code_hash = ? ");
|
||||
|
||||
List<Object> bindParams = new ArrayList<>();
|
||||
bindParams.add(codeHash);
|
||||
|
||||
if (isFinished != null) {
|
||||
sql.append("AND is_finished = ?");
|
||||
bindParams.add(isFinished);
|
||||
}
|
||||
|
||||
if (dataByteOffset != null && expectedValue != null) {
|
||||
sql.append("AND RAWTOHEX(SUBSTRING(state_data FROM ? FOR 8)) = ? ");
|
||||
|
||||
// We convert our long to hex Java-side to control endian
|
||||
String expectedHexValue = String.format("%016x", expectedValue); // left-zero-padding and conversion
|
||||
|
||||
// SQL binary data offsets start at 1
|
||||
bindParams.add(dataByteOffset + 1);
|
||||
bindParams.add(expectedHexValue);
|
||||
}
|
||||
|
||||
if (minimumFinalHeight != null) {
|
||||
sql.append("AND height >= ");
|
||||
sql.append(minimumFinalHeight);
|
||||
}
|
||||
|
||||
sql.append(" ORDER BY height ");
|
||||
if (reverse != null && reverse)
|
||||
sql.append("DESC");
|
||||
|
||||
HSQLDBRepository.limitOffsetSql(sql, limit, offset);
|
||||
|
||||
List<ATStateData> atStates = new ArrayList<>();
|
||||
|
||||
try (ResultSet resultSet = this.repository.checkedExecute(sql.toString(), bindParams.toArray())) {
|
||||
if (resultSet == null)
|
||||
return atStates;
|
||||
|
||||
do {
|
||||
String atAddress = resultSet.getString(1);
|
||||
int height = resultSet.getInt(2);
|
||||
long created = resultSet.getLong(3);
|
||||
byte[] stateData = resultSet.getBytes(4); // Actually BLOB
|
||||
byte[] stateHash = resultSet.getBytes(5);
|
||||
long fees = resultSet.getLong(6);
|
||||
boolean isInitial = resultSet.getBoolean(7);
|
||||
|
||||
ATStateData atStateData = new ATStateData(atAddress, height, created, stateData, stateHash, fees, isInitial);
|
||||
|
||||
atStates.add(atStateData);
|
||||
} while (resultSet.next());
|
||||
|
||||
return atStates;
|
||||
} catch (SQLException e) {
|
||||
throw new DataException("Unable to fetch matching AT states from repository", e);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public List<ATStateData> getBlockATStatesAtHeight(int height) throws DataException {
|
||||
String sql = "SELECT AT_address, state_hash, fees, is_initial "
|
||||
@@ -341,4 +429,40 @@ public class HSQLDBATRepository implements ATRepository {
|
||||
}
|
||||
}
|
||||
|
||||
// Finding transactions for ATs to process
|
||||
|
||||
public NextTransactionInfo findNextTransaction(String recipient, int height, int sequence) throws DataException {
|
||||
// We only need to search for a subset of transaction types: MESSAGE, PAYMENT or AT
|
||||
|
||||
String sql = "SELECT height, sequence, Transactions.signature "
|
||||
+ "FROM ("
|
||||
+ "SELECT signature FROM PaymentTransactions WHERE recipient = ? "
|
||||
+ "UNION "
|
||||
+ "SELECT signature FROM MessageTransactions WHERE recipient = ? "
|
||||
+ "UNION "
|
||||
+ "SELECT signature FROM ATTransactions WHERE recipient = ?"
|
||||
+ ") AS Transactions "
|
||||
+ "JOIN BlockTransactions ON BlockTransactions.transaction_signature = Transactions.signature "
|
||||
+ "JOIN Blocks ON Blocks.signature = BlockTransactions.block_signature "
|
||||
+ "WHERE (height > ? OR (height = ? AND sequence > ?)) "
|
||||
+ "ORDER BY height ASC, sequence ASC "
|
||||
+ "LIMIT 1";
|
||||
|
||||
Object[] bindParams = new Object[] { recipient, recipient, recipient, height, height, sequence };
|
||||
|
||||
try (ResultSet resultSet = this.repository.checkedExecute(sql, bindParams)) {
|
||||
if (resultSet == null)
|
||||
return null;
|
||||
|
||||
int nextHeight = resultSet.getInt(1);
|
||||
int nextSequence = resultSet.getInt(2);
|
||||
byte[] nextSignature = resultSet.getBytes(3);
|
||||
|
||||
return new NextTransactionInfo(nextHeight, nextSequence, nextSignature);
|
||||
} catch (SQLException e) {
|
||||
throw new DataException("Unable to find next transaction to AT from repository", e);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
|
@@ -774,9 +774,9 @@ public class HSQLDBAccountRepository implements AccountRepository {
|
||||
}
|
||||
|
||||
@Override
|
||||
public int delete(byte[] minterPrivateKey) throws DataException {
|
||||
public int delete(byte[] minterKey) throws DataException {
|
||||
try {
|
||||
return this.repository.delete("MintingAccounts", "minter_private_key = ?", minterPrivateKey);
|
||||
return this.repository.delete("MintingAccounts", "minter_private_key = ? OR minter_public_key = ?", minterKey, minterKey);
|
||||
} catch (SQLException e) {
|
||||
throw new DataException("Unable to delete minting account from repository", e);
|
||||
}
|
||||
|
@@ -6,6 +6,7 @@ import java.util.ArrayList;
|
||||
import java.util.Collections;
|
||||
import java.util.List;
|
||||
|
||||
import org.qortal.api.model.BlockInfo;
|
||||
import org.qortal.api.model.BlockSignerSummary;
|
||||
import org.qortal.data.block.BlockData;
|
||||
import org.qortal.data.block.BlockSummaryData;
|
||||
@@ -131,6 +132,20 @@ public class HSQLDBBlockRepository implements BlockRepository {
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public long getTimestampFromHeight(int height) throws DataException {
|
||||
String sql = "SELECT minted_when FROM Blocks WHERE height = ?";
|
||||
|
||||
try (ResultSet resultSet = this.repository.checkedExecute(sql, height)) {
|
||||
if (resultSet == null)
|
||||
return 0;
|
||||
|
||||
return resultSet.getLong(1);
|
||||
} catch (SQLException e) {
|
||||
throw new DataException("Error obtaining block timestamp by height from repository", e);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public int getBlockchainHeight() throws DataException {
|
||||
String sql = "SELECT height FROM Blocks ORDER BY height DESC LIMIT 1";
|
||||
@@ -360,6 +375,90 @@ public class HSQLDBBlockRepository implements BlockRepository {
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public List<BlockInfo> getBlockInfos(Integer startHeight, Integer endHeight, Integer count) throws DataException {
|
||||
StringBuilder sql = new StringBuilder(512);
|
||||
sql.append("SELECT signature, height, minted_when, transaction_count, RewardShares.minter ");
|
||||
|
||||
/*
|
||||
* start end count result
|
||||
* 10 40 null blocks 10 to 39 (excludes end block, ignore count)
|
||||
*
|
||||
* null null null blocks 1 to 50 (assume count=50, maybe start=1)
|
||||
* 30 null null blocks 30 to 79 (assume count=50)
|
||||
* 30 null 10 blocks 30 to 39
|
||||
*
|
||||
* null null 50 last 50 blocks? so if max(blocks.height) is 200, then blocks 151 to 200
|
||||
* null 200 null blocks 150 to 199 (excludes end block, assume count=50)
|
||||
* null 200 10 blocks 190 to 199 (excludes end block)
|
||||
*/
|
||||
|
||||
if (startHeight != null && endHeight != null) {
|
||||
sql.append("FROM Blocks ");
|
||||
sql.append("JOIN RewardShares ON RewardShares.reward_share_public_key = Blocks.minter ");
|
||||
sql.append("WHERE height BETWEEN ");
|
||||
sql.append(startHeight);
|
||||
sql.append(" AND ");
|
||||
sql.append(endHeight - 1);
|
||||
} else if (endHeight != null || (startHeight == null && count != null)) {
|
||||
// we are going to return blocks from the end of the chain
|
||||
if (count == null)
|
||||
count = 50;
|
||||
|
||||
if (endHeight == null) {
|
||||
sql.append("FROM (SELECT height FROM Blocks ORDER BY height DESC LIMIT 1) AS MaxHeights (max_height) ");
|
||||
sql.append("JOIN Blocks ON height BETWEEN (max_height - ");
|
||||
sql.append(count);
|
||||
sql.append(" + 1) AND max_height ");
|
||||
sql.append("JOIN RewardShares ON RewardShares.reward_share_public_key = Blocks.minter");
|
||||
} else {
|
||||
sql.append("FROM Blocks ");
|
||||
sql.append("JOIN RewardShares ON RewardShares.reward_share_public_key = Blocks.minter ");
|
||||
sql.append("WHERE height BETWEEN ");
|
||||
sql.append(endHeight - count);
|
||||
sql.append(" AND ");
|
||||
sql.append(endHeight - 1);
|
||||
}
|
||||
|
||||
} else {
|
||||
// we are going to return blocks from the start of the chain
|
||||
if (startHeight == null)
|
||||
startHeight = 1;
|
||||
|
||||
if (count == null)
|
||||
count = 50;
|
||||
|
||||
sql.append("FROM Blocks ");
|
||||
sql.append("JOIN RewardShares ON RewardShares.reward_share_public_key = Blocks.minter ");
|
||||
sql.append("WHERE height BETWEEN ");
|
||||
sql.append(startHeight);
|
||||
sql.append(" AND ");
|
||||
sql.append(startHeight + count - 1);
|
||||
}
|
||||
|
||||
List<BlockInfo> blockInfos = new ArrayList<>();
|
||||
|
||||
try (ResultSet resultSet = this.repository.checkedExecute(sql.toString())) {
|
||||
if (resultSet == null)
|
||||
return blockInfos;
|
||||
|
||||
do {
|
||||
byte[] signature = resultSet.getBytes(1);
|
||||
int height = resultSet.getInt(2);
|
||||
long timestamp = resultSet.getLong(3);
|
||||
int transactionCount = resultSet.getInt(4);
|
||||
String minterAddress = resultSet.getString(5);
|
||||
|
||||
BlockInfo blockInfo = new BlockInfo(signature, height, timestamp, transactionCount, minterAddress);
|
||||
blockInfos.add(blockInfo);
|
||||
} while (resultSet.next());
|
||||
|
||||
return blockInfos;
|
||||
} catch (SQLException e) {
|
||||
throw new DataException("Unable to fetch height-ranged block infos from repository", e);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public int trimOldOnlineAccountsSignatures(long timestamp) throws DataException {
|
||||
String sql = "UPDATE Blocks set online_accounts_signatures = NULL WHERE minted_when < ? AND online_accounts_signatures IS NOT NULL";
|
||||
@@ -372,14 +471,14 @@ public class HSQLDBBlockRepository implements BlockRepository {
|
||||
}
|
||||
|
||||
@Override
|
||||
public BlockData getDetachedBlockSignature() throws DataException {
|
||||
public BlockData getDetachedBlockSignature(int startHeight) throws DataException {
|
||||
String sql = "SELECT " + BLOCK_DB_COLUMNS + " FROM Blocks "
|
||||
+ "LEFT OUTER JOIN Blocks AS ParentBlocks "
|
||||
+ "ON ParentBlocks.signature = Blocks.reference "
|
||||
+ "WHERE ParentBlocks.signature IS NULL AND Blocks.height > 1 "
|
||||
+ "WHERE ParentBlocks.signature IS NULL AND Blocks.height > ? "
|
||||
+ "ORDER BY Blocks.height ASC LIMIT 1";
|
||||
|
||||
try (ResultSet resultSet = this.repository.checkedExecute(sql)) {
|
||||
try (ResultSet resultSet = this.repository.checkedExecute(sql, startHeight)) {
|
||||
return getBlockFromResultSet(resultSet);
|
||||
} catch (SQLException e) {
|
||||
throw new DataException("Error fetching block by signature from repository", e);
|
||||
|
@@ -0,0 +1,165 @@
|
||||
package org.qortal.repository.hsqldb;
|
||||
|
||||
import java.sql.ResultSet;
|
||||
import java.sql.SQLException;
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
|
||||
import org.qortal.data.crosschain.TradeBotData;
|
||||
import org.qortal.repository.CrossChainRepository;
|
||||
import org.qortal.repository.DataException;
|
||||
|
||||
public class HSQLDBCrossChainRepository implements CrossChainRepository {
|
||||
|
||||
protected HSQLDBRepository repository;
|
||||
|
||||
public HSQLDBCrossChainRepository(HSQLDBRepository repository) {
|
||||
this.repository = repository;
|
||||
}
|
||||
|
||||
@Override
|
||||
public TradeBotData getTradeBotData(byte[] tradePrivateKey) throws DataException {
|
||||
String sql = "SELECT trade_state, creator_address, at_address, "
|
||||
+ "updated_when, qort_amount, "
|
||||
+ "trade_native_public_key, trade_native_public_key_hash, "
|
||||
+ "trade_native_address, secret, hash_of_secret, "
|
||||
+ "trade_foreign_public_key, trade_foreign_public_key_hash, "
|
||||
+ "bitcoin_amount, xprv58, last_transaction_signature, locktime_a, receiving_account_info "
|
||||
+ "FROM TradeBotStates "
|
||||
+ "WHERE trade_private_key = ?";
|
||||
|
||||
try (ResultSet resultSet = this.repository.checkedExecute(sql, tradePrivateKey)) {
|
||||
if (resultSet == null)
|
||||
return null;
|
||||
|
||||
int tradeStateValue = resultSet.getInt(1);
|
||||
TradeBotData.State tradeState = TradeBotData.State.valueOf(tradeStateValue);
|
||||
if (tradeState == null)
|
||||
throw new DataException("Illegal trade-bot trade-state fetched from repository");
|
||||
|
||||
String creatorAddress = resultSet.getString(2);
|
||||
String atAddress = resultSet.getString(3);
|
||||
long timestamp = resultSet.getLong(4);
|
||||
long qortAmount = resultSet.getLong(5);
|
||||
byte[] tradeNativePublicKey = resultSet.getBytes(6);
|
||||
byte[] tradeNativePublicKeyHash = resultSet.getBytes(7);
|
||||
String tradeNativeAddress = resultSet.getString(8);
|
||||
byte[] secret = resultSet.getBytes(9);
|
||||
byte[] hashOfSecret = resultSet.getBytes(10);
|
||||
byte[] tradeForeignPublicKey = resultSet.getBytes(11);
|
||||
byte[] tradeForeignPublicKeyHash = resultSet.getBytes(12);
|
||||
long bitcoinAmount = resultSet.getLong(13);
|
||||
String xprv58 = resultSet.getString(14);
|
||||
byte[] lastTransactionSignature = resultSet.getBytes(15);
|
||||
Integer lockTimeA = resultSet.getInt(16);
|
||||
if (lockTimeA == 0 && resultSet.wasNull())
|
||||
lockTimeA = null;
|
||||
byte[] receivingAccountInfo = resultSet.getBytes(17);
|
||||
|
||||
return new TradeBotData(tradePrivateKey, tradeState,
|
||||
creatorAddress, atAddress, timestamp, qortAmount,
|
||||
tradeNativePublicKey, tradeNativePublicKeyHash, tradeNativeAddress,
|
||||
secret, hashOfSecret,
|
||||
tradeForeignPublicKey, tradeForeignPublicKeyHash,
|
||||
bitcoinAmount, xprv58, lastTransactionSignature, lockTimeA, receivingAccountInfo);
|
||||
} catch (SQLException e) {
|
||||
throw new DataException("Unable to fetch trade-bot trading state from repository", e);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public List<TradeBotData> getAllTradeBotData() throws DataException {
|
||||
String sql = "SELECT trade_private_key, trade_state, creator_address, at_address, "
|
||||
+ "updated_when, qort_amount, "
|
||||
+ "trade_native_public_key, trade_native_public_key_hash, "
|
||||
+ "trade_native_address, secret, hash_of_secret, "
|
||||
+ "trade_foreign_public_key, trade_foreign_public_key_hash, "
|
||||
+ "bitcoin_amount, xprv58, last_transaction_signature, locktime_a, receiving_account_info "
|
||||
+ "FROM TradeBotStates";
|
||||
|
||||
List<TradeBotData> allTradeBotData = new ArrayList<>();
|
||||
|
||||
try (ResultSet resultSet = this.repository.checkedExecute(sql)) {
|
||||
if (resultSet == null)
|
||||
return allTradeBotData;
|
||||
|
||||
do {
|
||||
byte[] tradePrivateKey = resultSet.getBytes(1);
|
||||
int tradeStateValue = resultSet.getInt(2);
|
||||
TradeBotData.State tradeState = TradeBotData.State.valueOf(tradeStateValue);
|
||||
if (tradeState == null)
|
||||
throw new DataException("Illegal trade-bot trade-state fetched from repository");
|
||||
|
||||
String creatorAddress = resultSet.getString(3);
|
||||
String atAddress = resultSet.getString(4);
|
||||
long timestamp = resultSet.getLong(5);
|
||||
long qortAmount = resultSet.getLong(6);
|
||||
byte[] tradeNativePublicKey = resultSet.getBytes(7);
|
||||
byte[] tradeNativePublicKeyHash = resultSet.getBytes(8);
|
||||
String tradeNativeAddress = resultSet.getString(9);
|
||||
byte[] secret = resultSet.getBytes(10);
|
||||
byte[] hashOfSecret = resultSet.getBytes(11);
|
||||
byte[] tradeForeignPublicKey = resultSet.getBytes(12);
|
||||
byte[] tradeForeignPublicKeyHash = resultSet.getBytes(13);
|
||||
long bitcoinAmount = resultSet.getLong(14);
|
||||
String xprv58 = resultSet.getString(15);
|
||||
byte[] lastTransactionSignature = resultSet.getBytes(16);
|
||||
Integer lockTimeA = resultSet.getInt(17);
|
||||
if (lockTimeA == 0 && resultSet.wasNull())
|
||||
lockTimeA = null;
|
||||
byte[] receivingAccountInfo = resultSet.getBytes(18);
|
||||
|
||||
TradeBotData tradeBotData = new TradeBotData(tradePrivateKey, tradeState,
|
||||
creatorAddress, atAddress, timestamp, qortAmount,
|
||||
tradeNativePublicKey, tradeNativePublicKeyHash, tradeNativeAddress,
|
||||
secret, hashOfSecret,
|
||||
tradeForeignPublicKey, tradeForeignPublicKeyHash,
|
||||
bitcoinAmount, xprv58, lastTransactionSignature, lockTimeA, receivingAccountInfo);
|
||||
allTradeBotData.add(tradeBotData);
|
||||
} while (resultSet.next());
|
||||
|
||||
return allTradeBotData;
|
||||
} catch (SQLException e) {
|
||||
throw new DataException("Unable to fetch trade-bot trading states from repository", e);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void save(TradeBotData tradeBotData) throws DataException {
|
||||
HSQLDBSaver saveHelper = new HSQLDBSaver("TradeBotStates");
|
||||
|
||||
saveHelper.bind("trade_private_key", tradeBotData.getTradePrivateKey())
|
||||
.bind("trade_state", tradeBotData.getState().value)
|
||||
.bind("creator_address", tradeBotData.getCreatorAddress())
|
||||
.bind("at_address", tradeBotData.getAtAddress())
|
||||
.bind("updated_when", tradeBotData.getTimestamp())
|
||||
.bind("qort_amount", tradeBotData.getQortAmount())
|
||||
.bind("trade_native_public_key", tradeBotData.getTradeNativePublicKey())
|
||||
.bind("trade_native_public_key_hash", tradeBotData.getTradeNativePublicKeyHash())
|
||||
.bind("trade_native_address", tradeBotData.getTradeNativeAddress())
|
||||
.bind("secret", tradeBotData.getSecret()).bind("hash_of_secret", tradeBotData.getHashOfSecret())
|
||||
.bind("trade_foreign_public_key", tradeBotData.getTradeForeignPublicKey())
|
||||
.bind("trade_foreign_public_key_hash", tradeBotData.getTradeForeignPublicKeyHash())
|
||||
.bind("bitcoin_amount", tradeBotData.getBitcoinAmount())
|
||||
.bind("xprv58", tradeBotData.getXprv58())
|
||||
.bind("last_transaction_signature", tradeBotData.getLastTransactionSignature())
|
||||
.bind("locktime_a", tradeBotData.getLockTimeA())
|
||||
.bind("receiving_account_info", tradeBotData.getReceivingAccountInfo());
|
||||
|
||||
try {
|
||||
saveHelper.execute(this.repository);
|
||||
} catch (SQLException e) {
|
||||
throw new DataException("Unable to save trade bot data into repository", e);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public int delete(byte[] tradePrivateKey) throws DataException {
|
||||
try {
|
||||
return this.repository.delete("TradeBotStates", "trade_private_key = ?", tradePrivateKey);
|
||||
} catch (SQLException e) {
|
||||
throw new DataException("Unable to delete trade-bot states from repository", e);
|
||||
}
|
||||
}
|
||||
|
||||
}
|
@@ -95,7 +95,6 @@ public class HSQLDBDatabaseUpdates {
|
||||
stmt.execute("CREATE COLLATION SQL_TEXT_NO_PAD FOR SQL_TEXT FROM SQL_TEXT NO PAD");
|
||||
|
||||
stmt.execute("SET FILES SPACE TRUE"); // Enable per-table block space within .data file, useful for CACHED table types
|
||||
stmt.execute("SET FILES LOB SCALE 1"); // LOB granularity is 1KB
|
||||
// Slow down log fsync() calls from every 500ms to reduce I/O load
|
||||
stmt.execute("SET FILES WRITE DELAY 5"); // only fsync() every 5 seconds
|
||||
|
||||
@@ -103,15 +102,15 @@ public class HSQLDBDatabaseUpdates {
|
||||
stmt.execute("INSERT INTO DatabaseInfo VALUES ( 0 )");
|
||||
|
||||
stmt.execute("CREATE TYPE ArbitraryData AS VARBINARY(256)");
|
||||
stmt.execute("CREATE TYPE AssetData AS CLOB(400K)");
|
||||
stmt.execute("CREATE TYPE AssetData AS VARCHAR(400K)");
|
||||
stmt.execute("CREATE TYPE AssetID AS BIGINT");
|
||||
stmt.execute("CREATE TYPE AssetName AS VARCHAR(34) COLLATE SQL_TEXT_NO_PAD");
|
||||
stmt.execute("CREATE TYPE AssetOrderID AS VARBINARY(64)");
|
||||
stmt.execute("CREATE TYPE ATCode AS BLOB(64K)"); // 16bit * 1
|
||||
stmt.execute("CREATE TYPE ATCreationBytes AS BLOB(576K)"); // 16bit * 1 + 16bit * 8
|
||||
stmt.execute("CREATE TYPE ATCode AS VARBINARY(1024)"); // was: 16bit * 1
|
||||
stmt.execute("CREATE TYPE ATCreationBytes AS VARBINARY(4096)"); // was: 16bit * 1 + 16bit * 8
|
||||
stmt.execute("CREATE TYPE ATMessage AS VARBINARY(32)");
|
||||
stmt.execute("CREATE TYPE ATName AS VARCHAR(32) COLLATE SQL_TEXT_UCC_NO_PAD");
|
||||
stmt.execute("CREATE TYPE ATState AS BLOB(1M)"); // 16bit * 8 + 16bit * 4 + 16bit * 4
|
||||
stmt.execute("CREATE TYPE ATState AS VARBINARY(1024)"); // was: 16bit * 8 + 16bit * 4 + 16bit * 4
|
||||
stmt.execute("CREATE TYPE ATTags AS VARCHAR(80) COLLATE SQL_TEXT_UCC_NO_PAD");
|
||||
stmt.execute("CREATE TYPE ATType AS VARCHAR(32) COLLATE SQL_TEXT_UCC_NO_PAD");
|
||||
stmt.execute("CREATE TYPE ATStateHash as VARBINARY(32)");
|
||||
@@ -142,7 +141,7 @@ public class HSQLDBDatabaseUpdates {
|
||||
+ "transaction_count INTEGER NOT NULL, total_fees QortalAmount NOT NULL, transactions_signature Signature NOT NULL, "
|
||||
+ "height INTEGER NOT NULL, minted_when EpochMillis NOT NULL, "
|
||||
+ "minter QortalPublicKey NOT NULL, minter_signature Signature NOT NULL, AT_count INTEGER NOT NULL, AT_fees QortalAmount NOT NULL, "
|
||||
+ "online_accounts BLOB(1M), online_accounts_count INTEGER NOT NULL, online_accounts_timestamp EpochMillis, online_accounts_signatures BLOB(1M), "
|
||||
+ "online_accounts VARBINARY(1204), online_accounts_count INTEGER NOT NULL, online_accounts_timestamp EpochMillis, online_accounts_signatures VARBINARY(1M), "
|
||||
+ "PRIMARY KEY (signature))");
|
||||
// For finding blocks by height.
|
||||
stmt.execute("CREATE INDEX BlockHeightIndex ON Blocks (height)");
|
||||
@@ -618,6 +617,39 @@ public class HSQLDBDatabaseUpdates {
|
||||
stmt.execute("CREATE TABLE PublicizeTransactions (signature Signature, nonce INT NOT NULL, " + TRANSACTION_KEYS + ")");
|
||||
break;
|
||||
|
||||
case 20:
|
||||
// Trade bot
|
||||
stmt.execute("CREATE TABLE TradeBotStates (trade_private_key QortalKeySeed NOT NULL, trade_state TINYINT NOT NULL, "
|
||||
+ "creator_address QortalAddress NOT NULL, at_address QortalAddress, updated_when BIGINT NOT NULL, qort_amount QortalAmount NOT NULL, "
|
||||
+ "trade_native_public_key QortalPublicKey NOT NULL, trade_native_public_key_hash VARBINARY(32) NOT NULL, "
|
||||
+ "trade_native_address QortalAddress NOT NULL, secret VARBINARY(32) NOT NULL, hash_of_secret VARBINARY(32) NOT NULL, "
|
||||
+ "trade_foreign_public_key VARBINARY(33) NOT NULL, trade_foreign_public_key_hash VARBINARY(32) NOT NULL, "
|
||||
+ "bitcoin_amount BIGINT NOT NULL, xprv58 VARCHAR(200), last_transaction_signature Signature, locktime_a BIGINT, "
|
||||
+ "receiving_account_info VARBINARY(32) NOT NULL, PRIMARY KEY (trade_private_key))");
|
||||
break;
|
||||
|
||||
case 21:
|
||||
// AT functionality index
|
||||
stmt.execute("CREATE INDEX IF NOT EXISTS ATCodeHashIndex ON ATs (code_hash, is_finished)");
|
||||
break;
|
||||
|
||||
case 22:
|
||||
// LOB downsizing
|
||||
stmt.execute("ALTER TABLE Blocks ALTER COLUMN online_accounts VARBINARY(1024)");
|
||||
stmt.execute("CHECKPOINT");
|
||||
stmt.execute("ALTER TABLE Blocks ALTER COLUMN online_accounts_signatures VARBINARY(1048576)");
|
||||
stmt.execute("CHECKPOINT");
|
||||
|
||||
stmt.execute("ALTER TABLE DeployATTransactions ALTER COLUMN creation_bytes VARBINARY(4096)");
|
||||
stmt.execute("CHECKPOINT");
|
||||
|
||||
stmt.execute("ALTER TABLE ATs ALTER COLUMN code_bytes VARBINARY(1024)");
|
||||
stmt.execute("CHECKPOINT");
|
||||
|
||||
stmt.execute("ALTER TABLE ATStates ALTER COLUMN state_data VARBINARY(1024)");
|
||||
stmt.execute("CHECKPOINT");
|
||||
break;
|
||||
|
||||
default:
|
||||
// nothing to do
|
||||
return false;
|
||||
|
@@ -32,6 +32,7 @@ import org.qortal.repository.ArbitraryRepository;
|
||||
import org.qortal.repository.AssetRepository;
|
||||
import org.qortal.repository.BlockRepository;
|
||||
import org.qortal.repository.ChatRepository;
|
||||
import org.qortal.repository.CrossChainRepository;
|
||||
import org.qortal.repository.DataException;
|
||||
import org.qortal.repository.GroupRepository;
|
||||
import org.qortal.repository.NameRepository;
|
||||
@@ -115,6 +116,11 @@ public class HSQLDBRepository implements Repository {
|
||||
return new HSQLDBChatRepository(this);
|
||||
}
|
||||
|
||||
@Override
|
||||
public CrossChainRepository getCrossChainRepository() {
|
||||
return new HSQLDBCrossChainRepository(this);
|
||||
}
|
||||
|
||||
@Override
|
||||
public GroupRepository getGroupRepository() {
|
||||
return new HSQLDBGroupRepository(this);
|
||||
|
@@ -19,6 +19,7 @@ import org.qortal.data.PaymentData;
|
||||
import org.qortal.data.group.GroupApprovalData;
|
||||
import org.qortal.data.transaction.BaseTransactionData;
|
||||
import org.qortal.data.transaction.GroupApprovalTransactionData;
|
||||
import org.qortal.data.transaction.MessageTransactionData;
|
||||
import org.qortal.data.transaction.TransactionData;
|
||||
import org.qortal.data.transaction.TransferAssetTransactionData;
|
||||
import org.qortal.repository.DataException;
|
||||
@@ -630,6 +631,43 @@ public class HSQLDBTransactionRepository implements TransactionRepository {
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public List<MessageTransactionData> getMessagesByRecipient(String recipient,
|
||||
Integer limit, Integer offset, Boolean reverse) throws DataException {
|
||||
StringBuilder sql = new StringBuilder(1024);
|
||||
sql.append("SELECT signature from MessageTransactions "
|
||||
+ "JOIN Transactions USING (signature) "
|
||||
+ "JOIN BlockTransactions ON transaction_signature = signature "
|
||||
+ "WHERE recipient = ?");
|
||||
|
||||
sql.append("ORDER BY Transactions.created_when");
|
||||
sql.append((reverse == null || !reverse) ? " ASC" : " DESC");
|
||||
|
||||
HSQLDBRepository.limitOffsetSql(sql, limit, offset);
|
||||
|
||||
List<MessageTransactionData> messageTransactionsData = new ArrayList<>();
|
||||
|
||||
try (ResultSet resultSet = this.repository.checkedExecute(sql.toString(), recipient)) {
|
||||
if (resultSet == null)
|
||||
return messageTransactionsData;
|
||||
|
||||
do {
|
||||
byte[] signature = resultSet.getBytes(1);
|
||||
|
||||
TransactionData transactionData = this.fromSignature(signature);
|
||||
if (transactionData == null || transactionData.getType() != TransactionType.MESSAGE)
|
||||
return null;
|
||||
|
||||
messageTransactionsData.add((MessageTransactionData) transactionData);
|
||||
} while (resultSet.next());
|
||||
|
||||
return messageTransactionsData;
|
||||
} catch (SQLException e) {
|
||||
throw new DataException("Unable to fetch trade-bot messages from repository", e);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@Override
|
||||
public List<TransactionData> getAssetTransactions(long assetId, ConfirmationStatus confirmationStatus, Integer limit, Integer offset, Boolean reverse)
|
||||
throws DataException {
|
||||
|
@@ -47,7 +47,7 @@ public class Settings {
|
||||
// UI servers
|
||||
private int uiPort = 12388;
|
||||
private String[] uiLocalServers = new String[] {
|
||||
"localhost", "172.24.1.1", "qor.tal"
|
||||
"localhost", "127.0.0.1", "172.24.1.1", "qor.tal"
|
||||
};
|
||||
private String[] uiRemoteServers = new String[] {
|
||||
"node1.qortal.org", "node2.qortal.org", "node3.qortal.org", "node4.qortal.org", "node5.qortal.org",
|
||||
@@ -92,6 +92,8 @@ public class Settings {
|
||||
private int maxPeers = 32;
|
||||
/** Maximum number of threads for network engine. */
|
||||
private int maxNetworkThreadPoolSize = 20;
|
||||
/** Maximum number of threads for network proof-of-work compute, used during handshaking. */
|
||||
private int networkPoWComputePoolSize = 2;
|
||||
|
||||
// Which blockchains this node is running
|
||||
private String blockchainConfig = null; // use default from resources
|
||||
@@ -105,8 +107,8 @@ public class Settings {
|
||||
|
||||
// Auto-update sources
|
||||
private String[] autoUpdateRepos = new String[] {
|
||||
"https://github.com/QORT/qortal/raw/%s/qortal.update",
|
||||
"https://raw.githubusercontent.com@151.101.16.133/QORT/qortal/%s/qortal.update"
|
||||
"https://github.com/Qortal/qortal/raw/%s/qortal.update",
|
||||
"https://raw.githubusercontent.com@151.101.16.133/Qortal/qortal/%s/qortal.update"
|
||||
};
|
||||
|
||||
/** Array of NTP server hostnames. */
|
||||
@@ -353,6 +355,10 @@ public class Settings {
|
||||
return this.maxNetworkThreadPoolSize;
|
||||
}
|
||||
|
||||
public int getNetworkPoWComputePoolSize() {
|
||||
return this.networkPoWComputePoolSize;
|
||||
}
|
||||
|
||||
public String getBlockchainConfig() {
|
||||
return this.blockchainConfig;
|
||||
}
|
||||
|
@@ -26,45 +26,47 @@ import com.google.common.base.Utf8;
|
||||
public class DeployAtTransaction extends Transaction {
|
||||
|
||||
// Properties
|
||||
private DeployAtTransactionData deployATTransactionData;
|
||||
private DeployAtTransactionData deployAtTransactionData;
|
||||
|
||||
// Other useful constants
|
||||
public static final int MAX_NAME_SIZE = 200;
|
||||
public static final int MAX_DESCRIPTION_SIZE = 2000;
|
||||
public static final int MAX_AT_TYPE_SIZE = 200;
|
||||
public static final int MAX_TAGS_SIZE = 200;
|
||||
public static final int MAX_CREATION_BYTES_SIZE = 100_000;
|
||||
public static final int MAX_CREATION_BYTES_SIZE = 4096;
|
||||
public static final int MAX_CODE_BYTES_LENGTH = 1024;
|
||||
public static final int MAX_AT_STATE_LENGTH = 1024;
|
||||
|
||||
// Constructors
|
||||
|
||||
public DeployAtTransaction(Repository repository, TransactionData transactionData) {
|
||||
super(repository, transactionData);
|
||||
|
||||
this.deployATTransactionData = (DeployAtTransactionData) this.transactionData;
|
||||
this.deployAtTransactionData = (DeployAtTransactionData) this.transactionData;
|
||||
}
|
||||
|
||||
// More information
|
||||
|
||||
@Override
|
||||
public List<String> getRecipientAddresses() throws DataException {
|
||||
return Collections.singletonList(this.deployATTransactionData.getAtAddress());
|
||||
return Collections.singletonList(this.deployAtTransactionData.getAtAddress());
|
||||
}
|
||||
|
||||
/** Returns AT version from the header bytes */
|
||||
private short getVersion() {
|
||||
byte[] creationBytes = deployATTransactionData.getCreationBytes();
|
||||
byte[] creationBytes = deployAtTransactionData.getCreationBytes();
|
||||
return (short) ((creationBytes[0] << 8) | (creationBytes[1] & 0xff)); // Big-endian
|
||||
}
|
||||
|
||||
/** Make sure deployATTransactionData has an ATAddress */
|
||||
private void ensureATAddress() throws DataException {
|
||||
if (this.deployATTransactionData.getAtAddress() != null)
|
||||
public static void ensureATAddress(DeployAtTransactionData deployAtTransactionData) throws DataException {
|
||||
if (deployAtTransactionData.getAtAddress() != null)
|
||||
return;
|
||||
|
||||
// Use transaction transformer
|
||||
try {
|
||||
String atAddress = Crypto.toATAddress(TransactionTransformer.toBytesForSigning(this.deployATTransactionData));
|
||||
this.deployATTransactionData.setAtAddress(atAddress);
|
||||
String atAddress = Crypto.toATAddress(TransactionTransformer.toBytesForSigning(deployAtTransactionData));
|
||||
deployAtTransactionData.setAtAddress(atAddress);
|
||||
} catch (TransformationException e) {
|
||||
throw new DataException("Unable to generate AT address");
|
||||
}
|
||||
@@ -73,9 +75,9 @@ public class DeployAtTransaction extends Transaction {
|
||||
// Navigation
|
||||
|
||||
public Account getATAccount() throws DataException {
|
||||
ensureATAddress();
|
||||
ensureATAddress(this.deployAtTransactionData);
|
||||
|
||||
return new Account(this.repository, this.deployATTransactionData.getAtAddress());
|
||||
return new Account(this.repository, this.deployAtTransactionData.getAtAddress());
|
||||
}
|
||||
|
||||
// Processing
|
||||
@@ -83,30 +85,30 @@ public class DeployAtTransaction extends Transaction {
|
||||
@Override
|
||||
public ValidationResult isValid() throws DataException {
|
||||
// Check name size bounds
|
||||
int nameLength = Utf8.encodedLength(this.deployATTransactionData.getName());
|
||||
int nameLength = Utf8.encodedLength(this.deployAtTransactionData.getName());
|
||||
if (nameLength < 1 || nameLength > MAX_NAME_SIZE)
|
||||
return ValidationResult.INVALID_NAME_LENGTH;
|
||||
|
||||
// Check description size bounds
|
||||
int descriptionlength = Utf8.encodedLength(this.deployATTransactionData.getDescription());
|
||||
int descriptionlength = Utf8.encodedLength(this.deployAtTransactionData.getDescription());
|
||||
if (descriptionlength < 1 || descriptionlength > MAX_DESCRIPTION_SIZE)
|
||||
return ValidationResult.INVALID_DESCRIPTION_LENGTH;
|
||||
|
||||
// Check AT-type size bounds
|
||||
int atTypeLength = Utf8.encodedLength(this.deployATTransactionData.getAtType());
|
||||
int atTypeLength = Utf8.encodedLength(this.deployAtTransactionData.getAtType());
|
||||
if (atTypeLength < 1 || atTypeLength > MAX_AT_TYPE_SIZE)
|
||||
return ValidationResult.INVALID_AT_TYPE_LENGTH;
|
||||
|
||||
// Check tags size bounds
|
||||
int tagsLength = Utf8.encodedLength(this.deployATTransactionData.getTags());
|
||||
int tagsLength = Utf8.encodedLength(this.deployAtTransactionData.getTags());
|
||||
if (tagsLength < 1 || tagsLength > MAX_TAGS_SIZE)
|
||||
return ValidationResult.INVALID_TAGS_LENGTH;
|
||||
|
||||
// Check amount is positive
|
||||
if (this.deployATTransactionData.getAmount() <= 0)
|
||||
if (this.deployAtTransactionData.getAmount() <= 0)
|
||||
return ValidationResult.NEGATIVE_AMOUNT;
|
||||
|
||||
long assetId = this.deployATTransactionData.getAssetId();
|
||||
long assetId = this.deployAtTransactionData.getAssetId();
|
||||
AssetData assetData = this.repository.getAssetRepository().fromAssetId(assetId);
|
||||
// Check asset even exists
|
||||
if (assetData == null)
|
||||
@@ -117,7 +119,7 @@ public class DeployAtTransaction extends Transaction {
|
||||
return ValidationResult.ASSET_NOT_SPENDABLE;
|
||||
|
||||
// Check asset amount is integer if asset is not divisible
|
||||
if (!assetData.isDivisible() && this.deployATTransactionData.getAmount() % Amounts.MULTIPLIER != 0)
|
||||
if (!assetData.isDivisible() && this.deployAtTransactionData.getAmount() % Amounts.MULTIPLIER != 0)
|
||||
return ValidationResult.INVALID_AMOUNT;
|
||||
|
||||
Account creator = this.getCreator();
|
||||
@@ -125,15 +127,15 @@ public class DeployAtTransaction extends Transaction {
|
||||
// Check creator has enough funds
|
||||
if (assetId == Asset.QORT) {
|
||||
// Simple case: amount and fee both in QORT
|
||||
long minimumBalance = this.deployATTransactionData.getFee() + this.deployATTransactionData.getAmount();
|
||||
long minimumBalance = this.deployAtTransactionData.getFee() + this.deployAtTransactionData.getAmount();
|
||||
|
||||
if (creator.getConfirmedBalance(Asset.QORT) < minimumBalance)
|
||||
return ValidationResult.NO_BALANCE;
|
||||
} else {
|
||||
if (creator.getConfirmedBalance(Asset.QORT) < this.deployATTransactionData.getFee())
|
||||
if (creator.getConfirmedBalance(Asset.QORT) < this.deployAtTransactionData.getFee())
|
||||
return ValidationResult.NO_BALANCE;
|
||||
|
||||
if (creator.getConfirmedBalance(assetId) < this.deployATTransactionData.getAmount())
|
||||
if (creator.getConfirmedBalance(assetId) < this.deployAtTransactionData.getAmount())
|
||||
return ValidationResult.NO_BALANCE;
|
||||
}
|
||||
|
||||
@@ -142,12 +144,12 @@ public class DeployAtTransaction extends Transaction {
|
||||
return ValidationResult.INVALID_CREATION_BYTES;
|
||||
|
||||
// Check creation bytes are valid (for v2+)
|
||||
this.ensureATAddress();
|
||||
ensureATAddress(this.deployAtTransactionData);
|
||||
|
||||
// Just enough AT data to allow API to query initial balances, etc.
|
||||
String atAddress = this.deployATTransactionData.getAtAddress();
|
||||
byte[] creatorPublicKey = this.deployATTransactionData.getCreatorPublicKey();
|
||||
long creation = this.deployATTransactionData.getTimestamp();
|
||||
String atAddress = this.deployAtTransactionData.getAtAddress();
|
||||
byte[] creatorPublicKey = this.deployAtTransactionData.getCreatorPublicKey();
|
||||
long creation = this.deployAtTransactionData.getTimestamp();
|
||||
ATData skeletonAtData = new ATData(atAddress, creatorPublicKey, creation, assetId);
|
||||
|
||||
int height = this.repository.getBlockRepository().getBlockchainHeight() + 1;
|
||||
@@ -157,7 +159,15 @@ public class DeployAtTransaction extends Transaction {
|
||||
QortalAtLoggerFactory loggerFactory = QortalAtLoggerFactory.getInstance();
|
||||
|
||||
try {
|
||||
new MachineState(api, loggerFactory, this.deployATTransactionData.getCreationBytes());
|
||||
MachineState state = new MachineState(api, loggerFactory, this.deployAtTransactionData.getCreationBytes());
|
||||
|
||||
byte[] codeBytes = state.getCodeBytes();
|
||||
if (codeBytes == null || codeBytes.length > MAX_CODE_BYTES_LENGTH)
|
||||
return ValidationResult.INVALID_CREATION_BYTES;
|
||||
|
||||
byte[] atStateBytes = state.toBytes();
|
||||
if (atStateBytes == null || atStateBytes.length > MAX_AT_STATE_LENGTH)
|
||||
return ValidationResult.INVALID_CREATION_BYTES;
|
||||
} catch (IllegalArgumentException e) {
|
||||
// Not valid
|
||||
return ValidationResult.INVALID_CREATION_BYTES;
|
||||
@@ -169,25 +179,25 @@ public class DeployAtTransaction extends Transaction {
|
||||
@Override
|
||||
public ValidationResult isProcessable() throws DataException {
|
||||
Account creator = getCreator();
|
||||
long assetId = this.deployATTransactionData.getAssetId();
|
||||
long assetId = this.deployAtTransactionData.getAssetId();
|
||||
|
||||
// Check creator has enough funds
|
||||
if (assetId == Asset.QORT) {
|
||||
// Simple case: amount and fee both in QORT
|
||||
long minimumBalance = this.deployATTransactionData.getFee() + this.deployATTransactionData.getAmount();
|
||||
long minimumBalance = this.deployAtTransactionData.getFee() + this.deployAtTransactionData.getAmount();
|
||||
|
||||
if (creator.getConfirmedBalance(Asset.QORT) < minimumBalance)
|
||||
return ValidationResult.NO_BALANCE;
|
||||
} else {
|
||||
if (creator.getConfirmedBalance(Asset.QORT) < this.deployATTransactionData.getFee())
|
||||
if (creator.getConfirmedBalance(Asset.QORT) < this.deployAtTransactionData.getFee())
|
||||
return ValidationResult.NO_BALANCE;
|
||||
|
||||
if (creator.getConfirmedBalance(assetId) < this.deployATTransactionData.getAmount())
|
||||
if (creator.getConfirmedBalance(assetId) < this.deployAtTransactionData.getAmount())
|
||||
return ValidationResult.NO_BALANCE;
|
||||
}
|
||||
|
||||
// Check AT doesn't already exist
|
||||
if (this.repository.getATRepository().exists(this.deployATTransactionData.getAtAddress()))
|
||||
if (this.repository.getATRepository().exists(this.deployAtTransactionData.getAtAddress()))
|
||||
return ValidationResult.AT_ALREADY_EXISTS;
|
||||
|
||||
return ValidationResult.OK;
|
||||
@@ -195,40 +205,40 @@ public class DeployAtTransaction extends Transaction {
|
||||
|
||||
@Override
|
||||
public void process() throws DataException {
|
||||
this.ensureATAddress();
|
||||
ensureATAddress(this.deployAtTransactionData);
|
||||
|
||||
// Deploy AT, saving into repository
|
||||
AT at = new AT(this.repository, this.deployATTransactionData);
|
||||
AT at = new AT(this.repository, this.deployAtTransactionData);
|
||||
at.deploy();
|
||||
|
||||
long assetId = this.deployATTransactionData.getAssetId();
|
||||
long assetId = this.deployAtTransactionData.getAssetId();
|
||||
|
||||
// Update creator's balance regarding initial payment to AT
|
||||
Account creator = getCreator();
|
||||
creator.modifyAssetBalance(assetId, - this.deployATTransactionData.getAmount());
|
||||
creator.modifyAssetBalance(assetId, - this.deployAtTransactionData.getAmount());
|
||||
|
||||
// Update AT's reference, which also creates AT account
|
||||
Account atAccount = this.getATAccount();
|
||||
atAccount.setLastReference(this.deployATTransactionData.getSignature());
|
||||
atAccount.setLastReference(this.deployAtTransactionData.getSignature());
|
||||
|
||||
// Update AT's balance
|
||||
atAccount.setConfirmedBalance(assetId, this.deployATTransactionData.getAmount());
|
||||
atAccount.setConfirmedBalance(assetId, this.deployAtTransactionData.getAmount());
|
||||
}
|
||||
|
||||
@Override
|
||||
public void orphan() throws DataException {
|
||||
// Delete AT from repository
|
||||
AT at = new AT(this.repository, this.deployATTransactionData);
|
||||
AT at = new AT(this.repository, this.deployAtTransactionData);
|
||||
at.undeploy();
|
||||
|
||||
long assetId = this.deployATTransactionData.getAssetId();
|
||||
long assetId = this.deployAtTransactionData.getAssetId();
|
||||
|
||||
// Update creator's balance regarding initial payment to AT
|
||||
Account creator = getCreator();
|
||||
creator.modifyAssetBalance(assetId, this.deployATTransactionData.getAmount());
|
||||
creator.modifyAssetBalance(assetId, this.deployAtTransactionData.getAmount());
|
||||
|
||||
// Delete AT's account (and hence its balance)
|
||||
this.repository.getAccountRepository().delete(this.deployATTransactionData.getAtAddress());
|
||||
this.repository.getAccountRepository().delete(this.deployAtTransactionData.getAtAddress());
|
||||
}
|
||||
|
||||
}
|
||||
|
@@ -136,7 +136,7 @@ public class RewardShareTransaction extends Transaction {
|
||||
|
||||
// Deleting a non-existent reward-share makes no sense
|
||||
if (isCancellingSharePercent)
|
||||
return ValidationResult.INVALID_REWARD_SHARE_PERCENT;
|
||||
return ValidationResult.REWARD_SHARE_UNKNOWN;
|
||||
|
||||
// Check the minting account hasn't reach maximum number of reward-shares
|
||||
int rewardShareCount = this.repository.getAccountRepository().countRewardShares(creator.getPublicKey());
|
||||
|
@@ -225,6 +225,7 @@ public abstract class Transaction {
|
||||
AT_IS_FINISHED(71),
|
||||
NO_FLAG_PERMISSION(72),
|
||||
NOT_MINTING_ACCOUNT(73),
|
||||
REWARD_SHARE_UNKNOWN(76),
|
||||
INVALID_REWARD_SHARE_PERCENT(77),
|
||||
PUBLIC_KEY_UNKNOWN(78),
|
||||
INVALID_PUBLIC_KEY(79),
|
||||
|
@@ -26,9 +26,26 @@ public class BitTwiddling {
|
||||
return new byte[] { (byte) (value), (byte) (value >> 8), (byte) (value >> 16), (byte) (value >> 24) };
|
||||
}
|
||||
|
||||
/** Convert int to big-endian byte array */
|
||||
public static byte[] toBEByteArray(int value) {
|
||||
return new byte[] { (byte) (value >> 24), (byte) (value >> 16), (byte) (value >> 8), (byte) (value) };
|
||||
}
|
||||
|
||||
/** Convert long to big-endian byte array */
|
||||
public static byte[] toBEByteArray(long value) {
|
||||
return new byte[] { (byte) (value >> 56), (byte) (value >> 48), (byte) (value >> 40), (byte) (value >> 32),
|
||||
(byte) (value >> 24), (byte) (value >> 16), (byte) (value >> 8), (byte) (value) };
|
||||
}
|
||||
|
||||
/** Convert little-endian bytes to int */
|
||||
public static int fromLEBytes(byte[] bytes, int offset) {
|
||||
public static int intFromLEBytes(byte[] bytes, int offset) {
|
||||
return (bytes[offset] & 0xff) | (bytes[offset + 1] & 0xff) << 8 | (bytes[offset + 2] & 0xff) << 16 | (bytes[offset + 3] & 0xff) << 24;
|
||||
}
|
||||
|
||||
/** Convert big-endian bytes to long */
|
||||
public static long longFromBEBytes(byte[] bytes, int start) {
|
||||
return (bytes[start] & 0xffL) << 56 | (bytes[start + 1] & 0xffL) << 48 | (bytes[start + 2] & 0xffL) << 40 | (bytes[start + 3] & 0xffL) << 32
|
||||
| (bytes[start + 4] & 0xffL) << 24 | (bytes[start + 5] & 0xffL) << 16 | (bytes[start + 6] & 0xffL) << 8 | (bytes[start + 7] & 0xffL);
|
||||
}
|
||||
|
||||
}
|
||||
|
31
src/main/java/org/qortal/utils/DaemonThreadFactory.java
Normal file
31
src/main/java/org/qortal/utils/DaemonThreadFactory.java
Normal file
@@ -0,0 +1,31 @@
|
||||
package org.qortal.utils;
|
||||
|
||||
import java.util.concurrent.Executors;
|
||||
import java.util.concurrent.ThreadFactory;
|
||||
import java.util.concurrent.atomic.AtomicInteger;
|
||||
|
||||
public class DaemonThreadFactory implements ThreadFactory {
|
||||
|
||||
private final String name;
|
||||
private final AtomicInteger threadNumber = new AtomicInteger(1);
|
||||
|
||||
public DaemonThreadFactory(String name) {
|
||||
this.name = name;
|
||||
}
|
||||
|
||||
public DaemonThreadFactory() {
|
||||
this(null);
|
||||
}
|
||||
|
||||
@Override
|
||||
public Thread newThread(Runnable runnable) {
|
||||
Thread thread = Executors.defaultThreadFactory().newThread(runnable);
|
||||
thread.setDaemon(true);
|
||||
|
||||
if (this.name != null)
|
||||
thread.setName(this.name + "-" + this.threadNumber.getAndIncrement());
|
||||
|
||||
return thread;
|
||||
}
|
||||
|
||||
}
|
24
src/main/java/org/qortal/utils/NamedThreadFactory.java
Normal file
24
src/main/java/org/qortal/utils/NamedThreadFactory.java
Normal file
@@ -0,0 +1,24 @@
|
||||
package org.qortal.utils;
|
||||
|
||||
import java.util.concurrent.Executors;
|
||||
import java.util.concurrent.ThreadFactory;
|
||||
import java.util.concurrent.atomic.AtomicInteger;
|
||||
|
||||
public class NamedThreadFactory implements ThreadFactory {
|
||||
|
||||
private final String name;
|
||||
private final AtomicInteger threadNumber = new AtomicInteger(1);
|
||||
|
||||
public NamedThreadFactory(String name) {
|
||||
this.name = name;
|
||||
}
|
||||
|
||||
@Override
|
||||
public Thread newThread(Runnable runnable) {
|
||||
Thread thread = Executors.defaultThreadFactory().newThread(runnable);
|
||||
thread.setName(this.name + "-" + this.threadNumber.getAndIncrement());
|
||||
|
||||
return thread;
|
||||
}
|
||||
|
||||
}
|
220
src/test/java/org/qortal/test/at/GetMessageLengthTests.java
Normal file
220
src/test/java/org/qortal/test/at/GetMessageLengthTests.java
Normal file
@@ -0,0 +1,220 @@
|
||||
package org.qortal.test.at;
|
||||
|
||||
import static org.junit.Assert.*;
|
||||
|
||||
import java.nio.ByteBuffer;
|
||||
import java.util.Random;
|
||||
|
||||
import org.ciyam.at.CompilationException;
|
||||
import org.ciyam.at.FunctionCode;
|
||||
import org.ciyam.at.MachineState;
|
||||
import org.ciyam.at.OpCode;
|
||||
import org.junit.Before;
|
||||
import org.junit.Test;
|
||||
import org.qortal.account.PrivateKeyAccount;
|
||||
import org.qortal.asset.Asset;
|
||||
import org.qortal.at.QortalFunctionCode;
|
||||
import org.qortal.data.at.ATStateData;
|
||||
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.AccountUtils;
|
||||
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.BitTwiddling;
|
||||
|
||||
public class GetMessageLengthTests extends Common {
|
||||
|
||||
private static final Random RANDOM = new Random();
|
||||
|
||||
@Before
|
||||
public void before() throws DataException {
|
||||
Common.useDefaultSettings();
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testGetMessageLength() throws DataException {
|
||||
try (final Repository repository = RepositoryManager.getRepository()) {
|
||||
PrivateKeyAccount deployer = Common.getTestAccount(repository, "alice");
|
||||
|
||||
byte[] creationBytes = buildMessageLengthAT();
|
||||
|
||||
long fundingAmount = 1_00000000L;
|
||||
DeployAtTransaction deployAtTransaction = doDeploy(repository, deployer, creationBytes, fundingAmount);
|
||||
String atAddress = deployAtTransaction.getATAccount().getAddress();
|
||||
|
||||
// Send messages with known length
|
||||
checkMessageLength(repository, deployer, atAddress, 1);
|
||||
checkMessageLength(repository, deployer, atAddress, 10);
|
||||
checkMessageLength(repository, deployer, atAddress, 32);
|
||||
checkMessageLength(repository, deployer, atAddress, 99);
|
||||
|
||||
// Finally, send a payment instead and check returned length is -1
|
||||
AccountUtils.pay(repository, deployer, atAddress, 123L);
|
||||
// Mint another block so AT can process payment
|
||||
BlockUtils.mintBlock(repository);
|
||||
|
||||
// Check AT result
|
||||
ATStateData atStateData = repository.getATRepository().getLatestATState(atAddress);
|
||||
byte[] stateData = atStateData.getStateData();
|
||||
|
||||
byte[] dataBytes = MachineState.extractDataBytes(stateData);
|
||||
|
||||
long extractedLength = BitTwiddling.longFromBEBytes(dataBytes, 0);
|
||||
|
||||
assertEquals(-1L, extractedLength);
|
||||
}
|
||||
}
|
||||
|
||||
private void checkMessageLength(Repository repository, PrivateKeyAccount sender, String atAddress, int messageLength) throws DataException {
|
||||
byte[] testMessage = new byte[messageLength];
|
||||
RANDOM.nextBytes(testMessage);
|
||||
|
||||
sendMessage(repository, sender, testMessage, atAddress);
|
||||
// Mint another block so AT can process message
|
||||
BlockUtils.mintBlock(repository);
|
||||
|
||||
// Check AT result
|
||||
ATStateData atStateData = repository.getATRepository().getLatestATState(atAddress);
|
||||
byte[] stateData = atStateData.getStateData();
|
||||
|
||||
byte[] dataBytes = MachineState.extractDataBytes(stateData);
|
||||
|
||||
long extractedLength = BitTwiddling.longFromBEBytes(dataBytes, 0);
|
||||
|
||||
assertEquals(messageLength, extractedLength);
|
||||
}
|
||||
|
||||
private byte[] buildMessageLengthAT() {
|
||||
// Labels for data segment addresses
|
||||
int addrCounter = 0;
|
||||
|
||||
// Make result first for easier extraction
|
||||
final int addrResult = addrCounter++;
|
||||
final int addrLastTxTimestamp = addrCounter++;
|
||||
|
||||
// Data segment
|
||||
ByteBuffer dataByteBuffer = ByteBuffer.allocate(addrCounter * MachineState.VALUE_SIZE);
|
||||
|
||||
// Code labels
|
||||
Integer labelCheckTx = null;
|
||||
|
||||
ByteBuffer codeByteBuffer = ByteBuffer.allocate(512);
|
||||
|
||||
// Two-pass version
|
||||
for (int pass = 0; pass < 2; ++pass) {
|
||||
codeByteBuffer.clear();
|
||||
|
||||
try {
|
||||
/* Initialization */
|
||||
|
||||
// Use AT creation 'timestamp' as starting point for finding transactions sent to AT
|
||||
codeByteBuffer.put(OpCode.EXT_FUN_RET.compile(FunctionCode.GET_CREATION_TIMESTAMP, addrLastTxTimestamp));
|
||||
|
||||
// Set restart position to after this opcode
|
||||
codeByteBuffer.put(OpCode.SET_PCS.compile());
|
||||
|
||||
/* Loop, waiting for message to AT */
|
||||
|
||||
// Find next transaction to this AT since the last one (if any)
|
||||
codeByteBuffer.put(OpCode.EXT_FUN_DAT.compile(FunctionCode.PUT_TX_AFTER_TIMESTAMP_INTO_A, addrLastTxTimestamp));
|
||||
// If no transaction found, A will be zero. If A is zero, set addrComparator to 1, otherwise 0.
|
||||
codeByteBuffer.put(OpCode.EXT_FUN_RET.compile(FunctionCode.CHECK_A_IS_ZERO, addrResult));
|
||||
// If addrResult is zero (i.e. A is non-zero, transaction was found) then go check transaction
|
||||
codeByteBuffer.put(OpCode.BZR_DAT.compile(addrResult, OpCode.calcOffset(codeByteBuffer, labelCheckTx)));
|
||||
// Stop and wait for next block
|
||||
codeByteBuffer.put(OpCode.STP_IMD.compile());
|
||||
|
||||
/* Check transaction */
|
||||
labelCheckTx = codeByteBuffer.position();
|
||||
|
||||
// Update our 'last found transaction's timestamp' using 'timestamp' from transaction
|
||||
codeByteBuffer.put(OpCode.EXT_FUN_RET.compile(FunctionCode.GET_TIMESTAMP_FROM_TX_IN_A, addrLastTxTimestamp));
|
||||
// Save message length
|
||||
codeByteBuffer.put(OpCode.EXT_FUN_RET.compile(QortalFunctionCode.GET_MESSAGE_LENGTH_FROM_TX_IN_A.value, addrResult));
|
||||
|
||||
// Stop and wait for next block (and hence more transactions)
|
||||
codeByteBuffer.put(OpCode.STP_IMD.compile());
|
||||
} catch (CompilationException e) {
|
||||
throw new IllegalStateException("Unable to compile AT?", e);
|
||||
}
|
||||
}
|
||||
|
||||
codeByteBuffer.flip();
|
||||
|
||||
byte[] codeBytes = new byte[codeByteBuffer.limit()];
|
||||
codeByteBuffer.get(codeBytes);
|
||||
|
||||
final short ciyamAtVersion = 2;
|
||||
final short numCallStackPages = 0;
|
||||
final short numUserStackPages = 0;
|
||||
final long minActivationAmount = 0L;
|
||||
|
||||
return MachineState.toCreationBytes(ciyamAtVersion, codeBytes, dataByteBuffer.array(), numCallStackPages, numUserStackPages, minActivationAmount);
|
||||
}
|
||||
|
||||
private DeployAtTransaction doDeploy(Repository repository, PrivateKeyAccount deployer, byte[] creationBytes, long fundingAmount) throws DataException {
|
||||
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 = "Test AT";
|
||||
String description = "Test AT";
|
||||
String atType = "Test";
|
||||
String tags = "TEST";
|
||||
|
||||
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;
|
||||
}
|
||||
|
||||
}
|
266
src/test/java/org/qortal/test/at/GetNextTransactionTests.java
Normal file
266
src/test/java/org/qortal/test/at/GetNextTransactionTests.java
Normal file
@@ -0,0 +1,266 @@
|
||||
package org.qortal.test.at;
|
||||
|
||||
import static org.junit.Assert.*;
|
||||
|
||||
import java.nio.ByteBuffer;
|
||||
import java.util.Arrays;
|
||||
import java.util.List;
|
||||
|
||||
import org.ciyam.at.CompilationException;
|
||||
import org.ciyam.at.FunctionCode;
|
||||
import org.ciyam.at.MachineState;
|
||||
import org.ciyam.at.OpCode;
|
||||
import org.ciyam.at.Timestamp;
|
||||
import org.junit.Before;
|
||||
import org.junit.Test;
|
||||
import org.qortal.account.PrivateKeyAccount;
|
||||
import org.qortal.asset.Asset;
|
||||
import org.qortal.block.Block;
|
||||
import org.qortal.data.at.ATStateData;
|
||||
import org.qortal.data.block.BlockData;
|
||||
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.transaction.Transaction;
|
||||
import org.qortal.utils.BitTwiddling;
|
||||
|
||||
public class GetNextTransactionTests extends Common {
|
||||
|
||||
@Before
|
||||
public void before() throws DataException {
|
||||
Common.useDefaultSettings();
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testGetNextTransaction() throws DataException {
|
||||
byte[] data = new byte[] { 0x44 };
|
||||
|
||||
try (final Repository repository = RepositoryManager.getRepository()) {
|
||||
PrivateKeyAccount deployer = Common.getTestAccount(repository, "alice");
|
||||
|
||||
byte[] creationBytes = buildGetNextTransactionAT();
|
||||
|
||||
long fundingAmount = 1_00000000L;
|
||||
DeployAtTransaction deployAtTransaction = doDeploy(repository, deployer, creationBytes, fundingAmount);
|
||||
String atAddress = deployAtTransaction.getATAccount().getAddress();
|
||||
|
||||
byte[] rawNextTimestamp = new byte[32];
|
||||
Transaction transaction;
|
||||
|
||||
// Confirm initial value is zero
|
||||
extractNextTxTimestamp(repository, atAddress, rawNextTimestamp);
|
||||
assertArrayEquals(new byte[32], rawNextTimestamp);
|
||||
|
||||
// Send message to someone other than AT
|
||||
sendMessage(repository, deployer, data, deployer.getAddress());
|
||||
BlockUtils.mintBlock(repository);
|
||||
|
||||
// Confirm AT does not find message
|
||||
extractNextTxTimestamp(repository, atAddress, rawNextTimestamp);
|
||||
assertArrayEquals(new byte[32], rawNextTimestamp);
|
||||
|
||||
// Send message to AT
|
||||
transaction = sendMessage(repository, deployer, data, atAddress);
|
||||
BlockUtils.mintBlock(repository);
|
||||
|
||||
// Confirm AT finds message
|
||||
BlockUtils.mintBlock(repository);
|
||||
assertTimestamp(repository, atAddress, transaction);
|
||||
|
||||
// Mint a few blocks, then send non-AT message, followed by AT message
|
||||
for (int i = 0; i < 5; ++i)
|
||||
BlockUtils.mintBlock(repository);
|
||||
sendMessage(repository, deployer, data, deployer.getAddress());
|
||||
transaction = sendMessage(repository, deployer, data, atAddress);
|
||||
BlockUtils.mintBlock(repository);
|
||||
|
||||
// Confirm AT finds message
|
||||
BlockUtils.mintBlock(repository);
|
||||
assertTimestamp(repository, atAddress, transaction);
|
||||
}
|
||||
}
|
||||
|
||||
private byte[] buildGetNextTransactionAT() {
|
||||
// Labels for data segment addresses
|
||||
int addrCounter = 0;
|
||||
|
||||
// Beginning of data segment for easy extraction
|
||||
final int addrNextTx = addrCounter;
|
||||
addrCounter += 4;
|
||||
|
||||
final int addrNextTxIndex = addrCounter++;
|
||||
|
||||
final int addrLastTxTimestamp = addrCounter++;
|
||||
|
||||
// Data segment
|
||||
ByteBuffer dataByteBuffer = ByteBuffer.allocate(addrCounter * MachineState.VALUE_SIZE);
|
||||
|
||||
// skip addrNextTx
|
||||
dataByteBuffer.position(dataByteBuffer.position() + 4 * MachineState.VALUE_SIZE);
|
||||
|
||||
// Store pointer to addrNextTx at addrNextTxIndex
|
||||
dataByteBuffer.putLong(addrNextTx);
|
||||
|
||||
ByteBuffer codeByteBuffer = ByteBuffer.allocate(512);
|
||||
|
||||
// Two-pass version
|
||||
for (int pass = 0; pass < 2; ++pass) {
|
||||
codeByteBuffer.clear();
|
||||
|
||||
try {
|
||||
/* Initialization */
|
||||
|
||||
// Use AT creation 'timestamp' as starting point for finding transactions sent to AT
|
||||
codeByteBuffer.put(OpCode.EXT_FUN_RET.compile(FunctionCode.GET_CREATION_TIMESTAMP, addrLastTxTimestamp));
|
||||
|
||||
// Set restart position to after this opcode
|
||||
codeByteBuffer.put(OpCode.SET_PCS.compile());
|
||||
|
||||
/* Loop, waiting for message to AT */
|
||||
|
||||
// Find next transaction to this AT since the last one (if any)
|
||||
codeByteBuffer.put(OpCode.EXT_FUN_DAT.compile(FunctionCode.PUT_TX_AFTER_TIMESTAMP_INTO_A, addrLastTxTimestamp));
|
||||
// Copy A to data segment, starting at addrNextTx (as pointed to by addrNextTxIndex)
|
||||
codeByteBuffer.put(OpCode.EXT_FUN_DAT.compile(FunctionCode.GET_A_IND, addrNextTxIndex));
|
||||
// Stop if timestamp part of A is zero
|
||||
codeByteBuffer.put(OpCode.STZ_DAT.compile(addrNextTx));
|
||||
|
||||
// Update our 'last found transaction's timestamp' using 'timestamp' from transaction
|
||||
codeByteBuffer.put(OpCode.EXT_FUN_RET.compile(FunctionCode.GET_TIMESTAMP_FROM_TX_IN_A, addrLastTxTimestamp));
|
||||
// Stop and wait for next block
|
||||
codeByteBuffer.put(OpCode.STP_IMD.compile());
|
||||
|
||||
} catch (CompilationException e) {
|
||||
throw new IllegalStateException("Unable to compile AT?", e);
|
||||
}
|
||||
}
|
||||
|
||||
codeByteBuffer.flip();
|
||||
|
||||
byte[] codeBytes = new byte[codeByteBuffer.limit()];
|
||||
codeByteBuffer.get(codeBytes);
|
||||
|
||||
final short ciyamAtVersion = 2;
|
||||
final short numCallStackPages = 0;
|
||||
final short numUserStackPages = 0;
|
||||
final long minActivationAmount = 0L;
|
||||
|
||||
return MachineState.toCreationBytes(ciyamAtVersion, codeBytes, dataByteBuffer.array(), numCallStackPages, numUserStackPages, minActivationAmount);
|
||||
}
|
||||
|
||||
private DeployAtTransaction doDeploy(Repository repository, PrivateKeyAccount deployer, byte[] creationBytes, long fundingAmount) throws DataException {
|
||||
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 = "Test AT";
|
||||
String description = "Test AT";
|
||||
String atType = "Test";
|
||||
String tags = "TEST";
|
||||
|
||||
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 void extractNextTxTimestamp(Repository repository, String atAddress, byte[] rawNextTimestamp) throws DataException {
|
||||
// Check AT result
|
||||
ATStateData atStateData = repository.getATRepository().getLatestATState(atAddress);
|
||||
byte[] stateData = atStateData.getStateData();
|
||||
|
||||
byte[] dataBytes = MachineState.extractDataBytes(stateData);
|
||||
|
||||
System.arraycopy(dataBytes, 0, rawNextTimestamp, 0, rawNextTimestamp.length);
|
||||
}
|
||||
|
||||
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.signAndImportValid(repository, messageTransactionData, sender);
|
||||
|
||||
return messageTransaction;
|
||||
}
|
||||
|
||||
private void assertTimestamp(Repository repository, String atAddress, Transaction transaction) throws DataException {
|
||||
int height = transaction.getHeight();
|
||||
byte[] transactionSignature = transaction.getTransactionData().getSignature();
|
||||
|
||||
BlockData blockData = repository.getBlockRepository().fromHeight(height);
|
||||
assertNotNull(blockData);
|
||||
|
||||
Block block = new Block(repository, blockData);
|
||||
|
||||
List<Transaction> blockTransactions = block.getTransactions();
|
||||
int sequence;
|
||||
for (sequence = blockTransactions.size() - 1; sequence >= 0; --sequence)
|
||||
if (Arrays.equals(blockTransactions.get(sequence).getTransactionData().getSignature(), transactionSignature))
|
||||
break;
|
||||
|
||||
assertNotSame(-1, sequence);
|
||||
|
||||
byte[] rawNextTimestamp = new byte[32];
|
||||
extractNextTxTimestamp(repository, atAddress, rawNextTimestamp);
|
||||
|
||||
Timestamp expectedTimestamp = new Timestamp(height, sequence);
|
||||
Timestamp actualTimestamp = new Timestamp(BitTwiddling.longFromBEBytes(rawNextTimestamp, 0));
|
||||
|
||||
assertEquals(String.format("Expected height %d, seq %d but was height %d, seq %d",
|
||||
height, sequence,
|
||||
actualTimestamp.blockHeight, actualTimestamp.transactionSequence
|
||||
),
|
||||
expectedTimestamp.longValue(),
|
||||
actualTimestamp.longValue());
|
||||
|
||||
byte[] expectedPartialSignature = new byte[24];
|
||||
System.arraycopy(transactionSignature, 8, expectedPartialSignature, 0, expectedPartialSignature.length);
|
||||
|
||||
byte[] actualPartialSignature = new byte[24];
|
||||
System.arraycopy(rawNextTimestamp, 8, actualPartialSignature, 0, actualPartialSignature.length);
|
||||
|
||||
assertArrayEquals(expectedPartialSignature, actualPartialSignature);
|
||||
}
|
||||
|
||||
}
|
219
src/test/java/org/qortal/test/at/GetPartialMessageTests.java
Normal file
219
src/test/java/org/qortal/test/at/GetPartialMessageTests.java
Normal file
@@ -0,0 +1,219 @@
|
||||
package org.qortal.test.at;
|
||||
|
||||
import static org.junit.Assert.*;
|
||||
|
||||
import java.nio.ByteBuffer;
|
||||
|
||||
import org.ciyam.at.CompilationException;
|
||||
import org.ciyam.at.FunctionCode;
|
||||
import org.ciyam.at.MachineState;
|
||||
import org.ciyam.at.OpCode;
|
||||
import org.junit.Before;
|
||||
import org.junit.Test;
|
||||
import org.qortal.account.PrivateKeyAccount;
|
||||
import org.qortal.asset.Asset;
|
||||
import org.qortal.at.QortalFunctionCode;
|
||||
import org.qortal.data.at.ATStateData;
|
||||
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;
|
||||
|
||||
public class GetPartialMessageTests extends Common {
|
||||
|
||||
@Before
|
||||
public void before() throws DataException {
|
||||
Common.useDefaultSettings();
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testGetPartialMessage() throws DataException {
|
||||
try (final Repository repository = RepositoryManager.getRepository()) {
|
||||
PrivateKeyAccount deployer = Common.getTestAccount(repository, "alice");
|
||||
|
||||
byte[] messageData = "The quick brown fox jumped over the lazy dog.".getBytes();
|
||||
int[] offsets = new int[] { 0, 7, 32, 44, messageData.length };
|
||||
|
||||
byte[] creationBytes = buildGetPartialMessageAT(offsets);
|
||||
|
||||
long fundingAmount = 1_00000000L;
|
||||
DeployAtTransaction deployAtTransaction = doDeploy(repository, deployer, creationBytes, fundingAmount);
|
||||
String atAddress = deployAtTransaction.getATAccount().getAddress();
|
||||
|
||||
sendMessage(repository, deployer, messageData, atAddress);
|
||||
|
||||
for (int offset : offsets) {
|
||||
// Mint another block so AT can process message
|
||||
BlockUtils.mintBlock(repository);
|
||||
|
||||
byte[] expectedData = new byte[32];
|
||||
int byteCount = Math.min(32, messageData.length - offset);
|
||||
System.arraycopy(messageData, offset, expectedData, 0, byteCount);
|
||||
|
||||
// Check AT result
|
||||
ATStateData atStateData = repository.getATRepository().getLatestATState(atAddress);
|
||||
byte[] stateData = atStateData.getStateData();
|
||||
|
||||
byte[] dataBytes = MachineState.extractDataBytes(stateData);
|
||||
|
||||
byte[] actualData = new byte[32];
|
||||
System.arraycopy(dataBytes, MachineState.VALUE_SIZE, actualData, 0, 32);
|
||||
|
||||
assertArrayEquals(expectedData, actualData);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private byte[] buildGetPartialMessageAT(int... offsets) {
|
||||
// Labels for data segment addresses
|
||||
int addrCounter = 0;
|
||||
|
||||
final int addrCopyOfBIndex = addrCounter++;
|
||||
|
||||
// 2nd position for easy extraction
|
||||
final int addrCopyOfB = addrCounter;
|
||||
addrCounter += 4;
|
||||
|
||||
final int addrResult = addrCounter++;
|
||||
final int addrLastTxTimestamp = addrCounter++;
|
||||
final int addrOffset = addrCounter++;
|
||||
|
||||
// Data segment
|
||||
ByteBuffer dataByteBuffer = ByteBuffer.allocate(addrCounter * MachineState.VALUE_SIZE);
|
||||
|
||||
dataByteBuffer.putLong(addrCopyOfB);
|
||||
|
||||
// Code labels
|
||||
Integer labelCheckTx = null;
|
||||
|
||||
ByteBuffer codeByteBuffer = ByteBuffer.allocate(512);
|
||||
|
||||
// Two-pass version
|
||||
for (int pass = 0; pass < 2; ++pass) {
|
||||
codeByteBuffer.clear();
|
||||
|
||||
try {
|
||||
/* Initialization */
|
||||
|
||||
// Use AT creation 'timestamp' as starting point for finding transactions sent to AT
|
||||
codeByteBuffer.put(OpCode.EXT_FUN_RET.compile(FunctionCode.GET_CREATION_TIMESTAMP, addrLastTxTimestamp));
|
||||
|
||||
// Set restart position to after this opcode
|
||||
codeByteBuffer.put(OpCode.SET_PCS.compile());
|
||||
|
||||
/* Loop, waiting for message to AT */
|
||||
|
||||
// Find next transaction to this AT since the last one (if any)
|
||||
codeByteBuffer.put(OpCode.EXT_FUN_DAT.compile(FunctionCode.PUT_TX_AFTER_TIMESTAMP_INTO_A, addrLastTxTimestamp));
|
||||
// If no transaction found, A will be zero. If A is zero, set addrComparator to 1, otherwise 0.
|
||||
codeByteBuffer.put(OpCode.EXT_FUN_RET.compile(FunctionCode.CHECK_A_IS_ZERO, addrResult));
|
||||
// If addrResult is zero (i.e. A is non-zero, transaction was found) then go check transaction
|
||||
codeByteBuffer.put(OpCode.BZR_DAT.compile(addrResult, OpCode.calcOffset(codeByteBuffer, labelCheckTx)));
|
||||
// Stop and wait for next block
|
||||
codeByteBuffer.put(OpCode.STP_IMD.compile());
|
||||
|
||||
/* Check transaction */
|
||||
labelCheckTx = codeByteBuffer.position();
|
||||
|
||||
// Generate code per offset
|
||||
for (int i = 0; i < offsets.length; ++i) {
|
||||
if (i > 0)
|
||||
// Wait for next block
|
||||
codeByteBuffer.put(OpCode.SLP_IMD.compile());
|
||||
|
||||
// Set offset
|
||||
codeByteBuffer.put(OpCode.SET_VAL.compile(addrOffset, offsets[i]));
|
||||
|
||||
// Extract partial message
|
||||
codeByteBuffer.put(OpCode.EXT_FUN_DAT.compile(QortalFunctionCode.PUT_PARTIAL_MESSAGE_FROM_TX_IN_A_INTO_B.value, addrOffset));
|
||||
|
||||
// Copy B to data segment
|
||||
codeByteBuffer.put(OpCode.EXT_FUN_DAT.compile(FunctionCode.GET_B_IND, addrCopyOfBIndex));
|
||||
}
|
||||
|
||||
// We're done
|
||||
codeByteBuffer.put(OpCode.FIN_IMD.compile());
|
||||
} catch (CompilationException e) {
|
||||
throw new IllegalStateException("Unable to compile AT?", e);
|
||||
}
|
||||
}
|
||||
|
||||
codeByteBuffer.flip();
|
||||
|
||||
byte[] codeBytes = new byte[codeByteBuffer.limit()];
|
||||
codeByteBuffer.get(codeBytes);
|
||||
|
||||
final short ciyamAtVersion = 2;
|
||||
final short numCallStackPages = 0;
|
||||
final short numUserStackPages = 0;
|
||||
final long minActivationAmount = 0L;
|
||||
|
||||
return MachineState.toCreationBytes(ciyamAtVersion, codeBytes, dataByteBuffer.array(), numCallStackPages, numUserStackPages, minActivationAmount);
|
||||
}
|
||||
|
||||
private DeployAtTransaction doDeploy(Repository repository, PrivateKeyAccount deployer, byte[] creationBytes, long fundingAmount) throws DataException {
|
||||
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 = "Test AT";
|
||||
String description = "Test AT";
|
||||
String atType = "Test";
|
||||
String tags = "TEST";
|
||||
|
||||
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;
|
||||
}
|
||||
|
||||
}
|
@@ -1,7 +1,6 @@
|
||||
package org.qortal.test.btcacct;
|
||||
|
||||
import static org.junit.Assert.assertEquals;
|
||||
import static org.junit.Assert.assertTrue;
|
||||
import static org.junit.Assert.*;
|
||||
|
||||
import java.time.Instant;
|
||||
import java.time.LocalDateTime;
|
||||
@@ -10,14 +9,15 @@ 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.bitcoinj.core.Base58;
|
||||
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.BTCACCT;
|
||||
import org.qortal.crypto.Crypto;
|
||||
import org.qortal.data.at.ATData;
|
||||
@@ -43,14 +43,18 @@ import com.google.common.primitives.Bytes;
|
||||
|
||||
public class AtTests extends Common {
|
||||
|
||||
public static final byte[] secret = "This string is exactly 32 bytes!".getBytes();
|
||||
public static final byte[] secretHash = Crypto.hash160(secret); // daf59884b4d1aec8c1b17102530909ee43c0151a
|
||||
public static final int refundTimeout = 10; // blocks
|
||||
public static final long initialPayout = 100000L;
|
||||
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;
|
||||
|
||||
private static final Random RANDOM = new Random();
|
||||
|
||||
@Before
|
||||
public void beforeTest() throws DataException {
|
||||
Common.useDefaultSettings();
|
||||
@@ -58,9 +62,9 @@ public class AtTests extends Common {
|
||||
|
||||
@Test
|
||||
public void testCompile() {
|
||||
Account deployer = Common.getTestAccount(null, "chloe");
|
||||
PrivateKeyAccount tradeAccount = createTradeAccount(null);
|
||||
|
||||
byte[] creationBytes = BTCACCT.buildQortalAT(deployer.getAddress(), secretHash, refundTimeout, initialPayout, redeemAmount, bitcoinAmount);
|
||||
byte[] creationBytes = BTCACCT.buildQortalAT(tradeAccount.getAddress(), bitcoinPublicKeyHash, hashOfSecretB, redeemAmount, bitcoinAmount, tradeTimeout);
|
||||
System.out.println("CIYAM AT creation bytes: " + HashCode.fromBytes(creationBytes).toString());
|
||||
}
|
||||
|
||||
@@ -68,12 +72,14 @@ public class AtTests extends Common {
|
||||
public void testDeploy() throws DataException {
|
||||
try (final Repository repository = RepositoryManager.getRepository()) {
|
||||
PrivateKeyAccount deployer = Common.getTestAccount(repository, "chloe");
|
||||
PrivateKeyAccount recipient = Common.getTestAccount(repository, "dilbert");
|
||||
PrivateKeyAccount tradeAccount = createTradeAccount(repository);
|
||||
|
||||
PrivateKeyAccount partner = Common.getTestAccount(repository, "dilbert");
|
||||
|
||||
long deployersInitialBalance = deployer.getConfirmedBalance(Asset.QORT);
|
||||
long recipientsInitialBalance = recipient.getConfirmedBalance(Asset.QORT);
|
||||
long partnersInitialBalance = partner.getConfirmedBalance(Asset.QORT);
|
||||
|
||||
DeployAtTransaction deployAtTransaction = doDeploy(repository, deployer);
|
||||
DeployAtTransaction deployAtTransaction = doDeploy(repository, deployer, tradeAccount.getAddress());
|
||||
|
||||
long expectedBalance = deployersInitialBalance - fundingAmount - deployAtTransaction.getTransactionData().getFee();
|
||||
long actualBalance = deployer.getConfirmedBalance(Asset.QORT);
|
||||
@@ -85,10 +91,10 @@ public class AtTests extends Common {
|
||||
|
||||
assertEquals("AT's post-deployment balance incorrect", expectedBalance, actualBalance);
|
||||
|
||||
expectedBalance = recipientsInitialBalance;
|
||||
actualBalance = recipient.getConfirmedBalance(Asset.QORT);
|
||||
expectedBalance = partnersInitialBalance;
|
||||
actualBalance = partner.getConfirmedBalance(Asset.QORT);
|
||||
|
||||
assertEquals("Recipient's post-deployment balance incorrect", expectedBalance, actualBalance);
|
||||
assertEquals("Partner's post-deployment balance incorrect", expectedBalance, actualBalance);
|
||||
|
||||
// Test orphaning
|
||||
BlockUtils.orphanLastBlock(repository);
|
||||
@@ -103,10 +109,10 @@ public class AtTests extends Common {
|
||||
|
||||
assertEquals("AT's post-orphan/pre-deployment balance incorrect", expectedBalance, actualBalance);
|
||||
|
||||
expectedBalance = recipientsInitialBalance;
|
||||
actualBalance = recipient.getConfirmedBalance(Asset.QORT);
|
||||
expectedBalance = partnersInitialBalance;
|
||||
actualBalance = partner.getConfirmedBalance(Asset.QORT);
|
||||
|
||||
assertEquals("Recipient's post-orphan/pre-deployment balance incorrect", expectedBalance, actualBalance);
|
||||
assertEquals("Partner's post-orphan/pre-deployment balance incorrect", expectedBalance, actualBalance);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -115,26 +121,39 @@ public class AtTests extends Common {
|
||||
public void testOfferCancel() throws DataException {
|
||||
try (final Repository repository = RepositoryManager.getRepository()) {
|
||||
PrivateKeyAccount deployer = Common.getTestAccount(repository, "chloe");
|
||||
PrivateKeyAccount recipient = Common.getTestAccount(repository, "dilbert");
|
||||
PrivateKeyAccount tradeAccount = createTradeAccount(repository);
|
||||
|
||||
PrivateKeyAccount partner = Common.getTestAccount(repository, "dilbert");
|
||||
|
||||
long deployersInitialBalance = deployer.getConfirmedBalance(Asset.QORT);
|
||||
long recipientsInitialBalance = recipient.getConfirmedBalance(Asset.QORT);
|
||||
long partnersInitialBalance = partner.getConfirmedBalance(Asset.QORT);
|
||||
|
||||
DeployAtTransaction deployAtTransaction = doDeploy(repository, deployer);
|
||||
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
|
||||
byte[] recipientAddressBytes = Bytes.ensureCapacity(Base58.decode(deployer.getAddress()), 32, 0);
|
||||
MessageTransaction messageTransaction = sendMessage(repository, deployer, recipientAddressBytes, atAddress);
|
||||
// Send creator's address to AT, instead of typical partner's address
|
||||
byte[] messageData = BTCACCT.buildCancelMessage(deployer.getAddress());
|
||||
MessageTransaction messageTransaction = sendMessage(repository, deployer, messageData, atAddress);
|
||||
long messageFee = messageTransaction.getTransactionData().getFee();
|
||||
|
||||
// Refund should happen 1st block after receiving recipient address
|
||||
// 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 = BTCACCT.populateTradeData(repository, atData);
|
||||
assertEquals(BTCACCT.Mode.CANCELLED, tradeData.mode);
|
||||
|
||||
// Check balances
|
||||
long expectedMinimumBalance = deployersPostDeploymentBalance;
|
||||
long expectedMaximumBalance = deployersInitialBalance - deployAtFee - messageFee;
|
||||
|
||||
@@ -143,11 +162,10 @@ public class AtTests extends Common {
|
||||
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);
|
||||
|
||||
describeAt(repository, atAddress);
|
||||
|
||||
// Test orphaning
|
||||
BlockUtils.orphanLastBlock(repository);
|
||||
|
||||
// Check balances
|
||||
long expectedBalance = deployersPostDeploymentBalance - messageFee;
|
||||
actualBalance = deployer.getConfirmedBalance(Asset.QORT);
|
||||
|
||||
@@ -157,71 +175,144 @@ public class AtTests extends Common {
|
||||
|
||||
@SuppressWarnings("unused")
|
||||
@Test
|
||||
public void testInitialPayment() throws DataException {
|
||||
public void testOfferCancelInvalidLength() throws DataException {
|
||||
try (final Repository repository = RepositoryManager.getRepository()) {
|
||||
PrivateKeyAccount deployer = Common.getTestAccount(repository, "chloe");
|
||||
PrivateKeyAccount recipient = Common.getTestAccount(repository, "dilbert");
|
||||
PrivateKeyAccount tradeAccount = createTradeAccount(repository);
|
||||
|
||||
PrivateKeyAccount partner = Common.getTestAccount(repository, "dilbert");
|
||||
|
||||
long deployersInitialBalance = deployer.getConfirmedBalance(Asset.QORT);
|
||||
long recipientsInitialBalance = recipient.getConfirmedBalance(Asset.QORT);
|
||||
long partnersInitialBalance = partner.getConfirmedBalance(Asset.QORT);
|
||||
|
||||
DeployAtTransaction deployAtTransaction = doDeploy(repository, deployer);
|
||||
DeployAtTransaction deployAtTransaction = doDeploy(repository, deployer, tradeAccount.getAddress());
|
||||
Account at = deployAtTransaction.getATAccount();
|
||||
String atAddress = at.getAddress();
|
||||
|
||||
// Send recipient's address to AT
|
||||
byte[] recipientAddressBytes = Bytes.ensureCapacity(Base58.decode(recipient.getAddress()), 32, 0);
|
||||
MessageTransaction messageTransaction = sendMessage(repository, deployer, recipientAddressBytes, atAddress);
|
||||
long deployAtFee = deployAtTransaction.getTransactionData().getFee();
|
||||
long deployersPostDeploymentBalance = deployersInitialBalance - fundingAmount - deployAtFee;
|
||||
|
||||
// Initial payment should happen 1st block after receiving recipient address
|
||||
// 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);
|
||||
|
||||
long expectedBalance = recipientsInitialBalance + initialPayout;
|
||||
long actualBalance = recipient.getConfirmedBalance(Asset.QORT);
|
||||
|
||||
assertEquals("Recipient's post-initial-payout balance incorrect", expectedBalance, actualBalance);
|
||||
|
||||
describeAt(repository, atAddress);
|
||||
|
||||
// Test orphaning
|
||||
BlockUtils.orphanLastBlock(repository);
|
||||
// Check AT is finished
|
||||
ATData atData = repository.getATRepository().fromATAddress(atAddress);
|
||||
assertTrue(atData.getIsFinished());
|
||||
|
||||
expectedBalance = recipientsInitialBalance;
|
||||
actualBalance = recipient.getConfirmedBalance(Asset.QORT);
|
||||
|
||||
assertEquals("Recipient's pre-initial-payout balance incorrect", expectedBalance, actualBalance);
|
||||
// AT should be in CANCELLED mode
|
||||
CrossChainTradeData tradeData = BTCACCT.populateTradeData(repository, atData);
|
||||
assertEquals(BTCACCT.Mode.CANCELLED, tradeData.mode);
|
||||
}
|
||||
}
|
||||
|
||||
// TEST SENDING RECIPIENT ADDRESS BUT NOT FROM AT CREATOR (SHOULD BE IGNORED)
|
||||
@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 = BTCACCT.calcLockTimeB(partnersOfferMessageTransactionTimestamp, lockTimeA);
|
||||
|
||||
// Send trade info to AT
|
||||
byte[] messageData = BTCACCT.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 = BTCACCT.populateTradeData(repository, atData);
|
||||
|
||||
// AT should be in TRADE mode
|
||||
assertEquals(BTCACCT.Mode.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.partnerBitcoinPKH));
|
||||
|
||||
// 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 recipient = Common.getTestAccount(repository, "dilbert");
|
||||
PrivateKeyAccount tradeAccount = createTradeAccount(repository);
|
||||
|
||||
PrivateKeyAccount partner = Common.getTestAccount(repository, "dilbert");
|
||||
|
||||
PrivateKeyAccount bystander = Common.getTestAccount(repository, "bob");
|
||||
|
||||
long deployersInitialBalance = deployer.getConfirmedBalance(Asset.QORT);
|
||||
long recipientsInitialBalance = recipient.getConfirmedBalance(Asset.QORT);
|
||||
long partnersInitialBalance = partner.getConfirmedBalance(Asset.QORT);
|
||||
|
||||
DeployAtTransaction deployAtTransaction = doDeploy(repository, deployer);
|
||||
DeployAtTransaction deployAtTransaction = doDeploy(repository, deployer, tradeAccount.getAddress());
|
||||
Account at = deployAtTransaction.getATAccount();
|
||||
String atAddress = at.getAddress();
|
||||
|
||||
// Send recipient's address to AT BUT NOT FROM AT CREATOR
|
||||
byte[] recipientAddressBytes = Bytes.ensureCapacity(Base58.decode(recipient.getAddress()), 32, 0);
|
||||
MessageTransaction messageTransaction = sendMessage(repository, bystander, recipientAddressBytes, atAddress);
|
||||
long partnersOfferMessageTransactionTimestamp = System.currentTimeMillis();
|
||||
int lockTimeA = calcTestLockTimeA(partnersOfferMessageTransactionTimestamp);
|
||||
int lockTimeB = BTCACCT.calcLockTimeB(partnersOfferMessageTransactionTimestamp, lockTimeA);
|
||||
|
||||
// Send trade info to AT BUT NOT FROM AT CREATOR
|
||||
byte[] messageData = BTCACCT.buildTradeMessage(partner.getAddress(), bitcoinPublicKeyHash, hashOfSecretA, lockTimeA, lockTimeB);
|
||||
MessageTransaction messageTransaction = sendMessage(repository, bystander, messageData, atAddress);
|
||||
|
||||
// Initial payment should NOT happen
|
||||
BlockUtils.mintBlock(repository);
|
||||
|
||||
long expectedBalance = recipientsInitialBalance;
|
||||
long actualBalance = recipient.getConfirmedBalance(Asset.QORT);
|
||||
long expectedBalance = partnersInitialBalance;
|
||||
long actualBalance = partner.getConfirmedBalance(Asset.QORT);
|
||||
|
||||
assertEquals("Recipient's post-initial-payout balance incorrect", expectedBalance, actualBalance);
|
||||
assertEquals("Partner's post-initial-payout balance incorrect", expectedBalance, actualBalance);
|
||||
|
||||
describeAt(repository, atAddress);
|
||||
|
||||
ATData atData = repository.getATRepository().fromATAddress(atAddress);
|
||||
CrossChainTradeData tradeData = BTCACCT.populateTradeData(repository, atData);
|
||||
|
||||
// AT should still be in OFFER mode
|
||||
assertEquals(BTCACCT.Mode.OFFERING, tradeData.mode);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -230,34 +321,48 @@ public class AtTests extends Common {
|
||||
public void testAutomaticTradeRefund() throws DataException {
|
||||
try (final Repository repository = RepositoryManager.getRepository()) {
|
||||
PrivateKeyAccount deployer = Common.getTestAccount(repository, "chloe");
|
||||
PrivateKeyAccount recipient = Common.getTestAccount(repository, "dilbert");
|
||||
PrivateKeyAccount tradeAccount = createTradeAccount(repository);
|
||||
|
||||
PrivateKeyAccount partner = Common.getTestAccount(repository, "dilbert");
|
||||
|
||||
long deployersInitialBalance = deployer.getConfirmedBalance(Asset.QORT);
|
||||
long recipientsInitialBalance = recipient.getConfirmedBalance(Asset.QORT);
|
||||
long partnersInitialBalance = partner.getConfirmedBalance(Asset.QORT);
|
||||
|
||||
DeployAtTransaction deployAtTransaction = doDeploy(repository, deployer);
|
||||
DeployAtTransaction deployAtTransaction = doDeploy(repository, deployer, tradeAccount.getAddress());
|
||||
Account at = deployAtTransaction.getATAccount();
|
||||
String atAddress = at.getAddress();
|
||||
|
||||
// Send recipient's address to AT
|
||||
byte[] recipientAddressBytes = Bytes.ensureCapacity(Base58.decode(recipient.getAddress()), 32, 0);
|
||||
MessageTransaction messageTransaction = sendMessage(repository, deployer, recipientAddressBytes, atAddress);
|
||||
long partnersOfferMessageTransactionTimestamp = System.currentTimeMillis();
|
||||
int lockTimeA = calcTestLockTimeA(partnersOfferMessageTransactionTimestamp);
|
||||
int lockTimeB = BTCACCT.calcLockTimeB(partnersOfferMessageTransactionTimestamp, lockTimeA);
|
||||
|
||||
// Initial payment should happen 1st block after receiving recipient address
|
||||
BlockUtils.mintBlock(repository);
|
||||
// Send trade info to AT
|
||||
byte[] messageData = BTCACCT.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 messageFee = messageTransaction.getTransactionData().getFee();
|
||||
long deployersPostDeploymentBalance = deployersInitialBalance - fundingAmount - deployAtFee - messageFee;
|
||||
long deployersPostDeploymentBalance = deployersInitialBalance - fundingAmount - deployAtFee;
|
||||
|
||||
checkTradeRefund(repository, deployer, deployersInitialBalance, deployAtFee);
|
||||
|
||||
describeAt(repository, atAddress);
|
||||
|
||||
// Test orphaning
|
||||
BlockUtils.orphanLastBlock(repository);
|
||||
BlockUtils.orphanLastBlock(repository);
|
||||
// Check AT is finished
|
||||
ATData atData = repository.getATRepository().fromATAddress(atAddress);
|
||||
assertTrue(atData.getIsFinished());
|
||||
|
||||
// AT should be in REFUNDED mode
|
||||
CrossChainTradeData tradeData = BTCACCT.populateTradeData(repository, atData);
|
||||
assertEquals(BTCACCT.Mode.REFUNDED, tradeData.mode);
|
||||
|
||||
// Test orphaning
|
||||
BlockUtils.orphanToBlock(repository, postDeploymentBlockHeight);
|
||||
|
||||
// Check balances
|
||||
long expectedBalance = deployersPostDeploymentBalance;
|
||||
long actualBalance = deployer.getConfirmedBalance(Asset.QORT);
|
||||
|
||||
@@ -267,46 +372,63 @@ public class AtTests extends Common {
|
||||
|
||||
@SuppressWarnings("unused")
|
||||
@Test
|
||||
public void testCorrectSecretCorrectSender() throws DataException {
|
||||
public void testCorrectSecretsCorrectSender() throws DataException {
|
||||
try (final Repository repository = RepositoryManager.getRepository()) {
|
||||
PrivateKeyAccount deployer = Common.getTestAccount(repository, "chloe");
|
||||
PrivateKeyAccount recipient = Common.getTestAccount(repository, "dilbert");
|
||||
PrivateKeyAccount tradeAccount = createTradeAccount(repository);
|
||||
|
||||
PrivateKeyAccount partner = Common.getTestAccount(repository, "dilbert");
|
||||
|
||||
long deployersInitialBalance = deployer.getConfirmedBalance(Asset.QORT);
|
||||
long recipientsInitialBalance = recipient.getConfirmedBalance(Asset.QORT);
|
||||
long partnersInitialBalance = partner.getConfirmedBalance(Asset.QORT);
|
||||
|
||||
DeployAtTransaction deployAtTransaction = doDeploy(repository, deployer);
|
||||
DeployAtTransaction deployAtTransaction = doDeploy(repository, deployer, tradeAccount.getAddress());
|
||||
Account at = deployAtTransaction.getATAccount();
|
||||
String atAddress = at.getAddress();
|
||||
|
||||
// Send recipient's address to AT
|
||||
byte[] recipientAddressBytes = Bytes.ensureCapacity(Base58.decode(recipient.getAddress()), 32, 0);
|
||||
MessageTransaction messageTransaction = sendMessage(repository, deployer, recipientAddressBytes, atAddress);
|
||||
long partnersOfferMessageTransactionTimestamp = System.currentTimeMillis();
|
||||
int lockTimeA = calcTestLockTimeA(partnersOfferMessageTransactionTimestamp);
|
||||
int lockTimeB = BTCACCT.calcLockTimeB(partnersOfferMessageTransactionTimestamp, lockTimeA);
|
||||
|
||||
// Initial payment should happen 1st block after receiving recipient address
|
||||
// Send trade info to AT
|
||||
byte[] messageData = BTCACCT.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 secret to AT
|
||||
messageTransaction = sendMessage(repository, recipient, secret, atAddress);
|
||||
// Send correct secrets to AT, from correct account
|
||||
messageData = BTCACCT.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);
|
||||
|
||||
long expectedBalance = recipientsInitialBalance + initialPayout - messageTransaction.getTransactionData().getFee() + redeemAmount;
|
||||
long actualBalance = recipient.getConfirmedBalance(Asset.QORT);
|
||||
|
||||
assertEquals("Recipent's post-redeem balance incorrect", expectedBalance, actualBalance);
|
||||
|
||||
describeAt(repository, atAddress);
|
||||
|
||||
// Check AT is finished
|
||||
ATData atData = repository.getATRepository().fromATAddress(atAddress);
|
||||
assertTrue(atData.getIsFinished());
|
||||
|
||||
// AT should be in REDEEMED mode
|
||||
CrossChainTradeData tradeData = BTCACCT.populateTradeData(repository, atData);
|
||||
assertEquals(BTCACCT.Mode.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);
|
||||
|
||||
expectedBalance = recipientsInitialBalance + initialPayout - messageTransaction.getTransactionData().getFee();
|
||||
actualBalance = recipient.getConfirmedBalance(Asset.QORT);
|
||||
// Check balances
|
||||
expectedBalance = partnersInitialBalance - messageTransaction.getTransactionData().getFee();
|
||||
actualBalance = partner.getConfirmedBalance(Asset.QORT);
|
||||
|
||||
assertEquals("Recipent's post-orphan/pre-redeem balance incorrect", expectedBalance, actualBalance);
|
||||
assertEquals("Partner's post-orphan/pre-redeem balance incorrect", expectedBalance, actualBalance);
|
||||
|
||||
// Check AT state
|
||||
ATStateData postOrphanAtStateData = repository.getATRepository().getLatestATState(atAddress);
|
||||
@@ -317,99 +439,206 @@ public class AtTests extends Common {
|
||||
|
||||
@SuppressWarnings("unused")
|
||||
@Test
|
||||
public void testCorrectSecretIncorrectSender() throws DataException {
|
||||
public void testCorrectSecretsIncorrectSender() throws DataException {
|
||||
try (final Repository repository = RepositoryManager.getRepository()) {
|
||||
PrivateKeyAccount deployer = Common.getTestAccount(repository, "chloe");
|
||||
PrivateKeyAccount recipient = Common.getTestAccount(repository, "dilbert");
|
||||
PrivateKeyAccount tradeAccount = createTradeAccount(repository);
|
||||
|
||||
PrivateKeyAccount partner = Common.getTestAccount(repository, "dilbert");
|
||||
|
||||
PrivateKeyAccount bystander = Common.getTestAccount(repository, "bob");
|
||||
|
||||
long deployersInitialBalance = deployer.getConfirmedBalance(Asset.QORT);
|
||||
long recipientsInitialBalance = recipient.getConfirmedBalance(Asset.QORT);
|
||||
long partnersInitialBalance = partner.getConfirmedBalance(Asset.QORT);
|
||||
|
||||
DeployAtTransaction deployAtTransaction = doDeploy(repository, deployer);
|
||||
DeployAtTransaction deployAtTransaction = doDeploy(repository, deployer, tradeAccount.getAddress());
|
||||
long deployAtFee = deployAtTransaction.getTransactionData().getFee();
|
||||
|
||||
Account at = deployAtTransaction.getATAccount();
|
||||
String atAddress = at.getAddress();
|
||||
|
||||
// Send recipient's address to AT
|
||||
byte[] recipientAddressBytes = Bytes.ensureCapacity(Base58.decode(recipient.getAddress()), 32, 0);
|
||||
MessageTransaction messageTransaction = sendMessage(repository, deployer, recipientAddressBytes, atAddress);
|
||||
long partnersOfferMessageTransactionTimestamp = System.currentTimeMillis();
|
||||
int lockTimeA = calcTestLockTimeA(partnersOfferMessageTransactionTimestamp);
|
||||
int lockTimeB = BTCACCT.calcLockTimeB(partnersOfferMessageTransactionTimestamp, lockTimeA);
|
||||
|
||||
// Initial payment should happen 1st block after receiving recipient address
|
||||
// Send trade info to AT
|
||||
byte[] messageData = BTCACCT.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 secret to AT, but from wrong account
|
||||
messageTransaction = sendMessage(repository, bystander, secret, atAddress);
|
||||
// Send correct secrets to AT, but from wrong account
|
||||
messageData = BTCACCT.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);
|
||||
|
||||
long expectedBalance = recipientsInitialBalance + initialPayout;
|
||||
long actualBalance = recipient.getConfirmedBalance(Asset.QORT);
|
||||
|
||||
assertEquals("Recipent's balance incorrect", expectedBalance, actualBalance);
|
||||
|
||||
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 = BTCACCT.populateTradeData(repository, atData);
|
||||
assertEquals(BTCACCT.Mode.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 {
|
||||
public void testIncorrectSecretsCorrectSender() throws DataException {
|
||||
try (final Repository repository = RepositoryManager.getRepository()) {
|
||||
PrivateKeyAccount deployer = Common.getTestAccount(repository, "chloe");
|
||||
PrivateKeyAccount recipient = Common.getTestAccount(repository, "dilbert");
|
||||
PrivateKeyAccount tradeAccount = createTradeAccount(repository);
|
||||
|
||||
PrivateKeyAccount partner = Common.getTestAccount(repository, "dilbert");
|
||||
|
||||
long deployersInitialBalance = deployer.getConfirmedBalance(Asset.QORT);
|
||||
long recipientsInitialBalance = recipient.getConfirmedBalance(Asset.QORT);
|
||||
long partnersInitialBalance = partner.getConfirmedBalance(Asset.QORT);
|
||||
|
||||
DeployAtTransaction deployAtTransaction = doDeploy(repository, deployer);
|
||||
DeployAtTransaction deployAtTransaction = doDeploy(repository, deployer, tradeAccount.getAddress());
|
||||
long deployAtFee = deployAtTransaction.getTransactionData().getFee();
|
||||
|
||||
Account at = deployAtTransaction.getATAccount();
|
||||
String atAddress = at.getAddress();
|
||||
|
||||
// Send recipient's address to AT
|
||||
byte[] recipientAddressBytes = Bytes.ensureCapacity(Base58.decode(recipient.getAddress()), 32, 0);
|
||||
MessageTransaction messageTransaction = sendMessage(repository, deployer, recipientAddressBytes, atAddress);
|
||||
long partnersOfferMessageTransactionTimestamp = System.currentTimeMillis();
|
||||
int lockTimeA = calcTestLockTimeA(partnersOfferMessageTransactionTimestamp);
|
||||
int lockTimeB = BTCACCT.calcLockTimeB(partnersOfferMessageTransactionTimestamp, lockTimeA);
|
||||
|
||||
// Initial payment should happen 1st block after receiving recipient address
|
||||
// Send trade info to AT
|
||||
byte[] messageData = BTCACCT.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 secret to AT, but from wrong account
|
||||
byte[] wrongSecret = Crypto.digest(secret);
|
||||
messageTransaction = sendMessage(repository, recipient, wrongSecret, atAddress);
|
||||
// Send incorrect secrets to AT, from correct account
|
||||
byte[] wrongSecret = new byte[32];
|
||||
RANDOM.nextBytes(wrongSecret);
|
||||
messageData = BTCACCT.buildRedeemMessage(wrongSecret, secretB, 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);
|
||||
|
||||
long expectedBalance = recipientsInitialBalance + initialPayout - messageTransaction.getTransactionData().getFee();
|
||||
long actualBalance = recipient.getConfirmedBalance(Asset.QORT);
|
||||
describeAt(repository, atAddress);
|
||||
|
||||
assertEquals("Recipent's balance incorrect", expectedBalance, actualBalance);
|
||||
// Check AT is NOT finished
|
||||
ATData atData = repository.getATRepository().fromATAddress(atAddress);
|
||||
assertFalse(atData.getIsFinished());
|
||||
|
||||
// AT should still be in TRADE mode
|
||||
CrossChainTradeData tradeData = BTCACCT.populateTradeData(repository, atData);
|
||||
assertEquals(BTCACCT.Mode.TRADING, tradeData.mode);
|
||||
|
||||
long expectedBalance = partnersInitialBalance - messageTransaction.getTransactionData().getFee();
|
||||
long actualBalance = partner.getConfirmedBalance(Asset.QORT);
|
||||
|
||||
assertEquals("Partner's balance incorrect", expectedBalance, actualBalance);
|
||||
|
||||
// Send incorrect secrets to AT, from correct account
|
||||
messageData = BTCACCT.buildRedeemMessage(secretA, wrongSecret, partner.getAddress());
|
||||
messageTransaction = sendMessage(repository, partner, messageData, atAddress);
|
||||
|
||||
// AT should NOT send funds in the next block
|
||||
BlockUtils.mintBlock(repository);
|
||||
|
||||
describeAt(repository, atAddress);
|
||||
|
||||
// Check AT is NOT finished
|
||||
atData = repository.getATRepository().fromATAddress(atAddress);
|
||||
assertFalse(atData.getIsFinished());
|
||||
|
||||
// AT should still be in TRADE mode
|
||||
tradeData = BTCACCT.populateTradeData(repository, atData);
|
||||
assertEquals(BTCACCT.Mode.TRADING, tradeData.mode);
|
||||
|
||||
// Check balances
|
||||
expectedBalance = partnersInitialBalance - messageTransaction.getTransactionData().getFee() * 2;
|
||||
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 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 = BTCACCT.calcLockTimeB(partnersOfferMessageTransactionTimestamp, lockTimeA);
|
||||
|
||||
// Send trade info to AT
|
||||
byte[] messageData = BTCACCT.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 = BTCACCT.populateTradeData(repository, atData);
|
||||
assertEquals(BTCACCT.Mode.TRADING, tradeData.mode);
|
||||
}
|
||||
}
|
||||
|
||||
@SuppressWarnings("unused")
|
||||
@Test
|
||||
public void testDescribeDeployed() throws DataException {
|
||||
try (final Repository repository = RepositoryManager.getRepository()) {
|
||||
PrivateKeyAccount deployer = Common.getTestAccount(repository, "chloe");
|
||||
PrivateKeyAccount recipient = Common.getTestAccount(repository, "dilbert");
|
||||
PrivateKeyAccount tradeAccount = createTradeAccount(repository);
|
||||
|
||||
PrivateKeyAccount partner = Common.getTestAccount(repository, "dilbert");
|
||||
|
||||
long deployersInitialBalance = deployer.getConfirmedBalance(Asset.QORT);
|
||||
long recipientsInitialBalance = recipient.getConfirmedBalance(Asset.QORT);
|
||||
long partnersInitialBalance = partner.getConfirmedBalance(Asset.QORT);
|
||||
|
||||
DeployAtTransaction deployAtTransaction = doDeploy(repository, deployer);
|
||||
DeployAtTransaction deployAtTransaction = doDeploy(repository, deployer, tradeAccount.getAddress());
|
||||
|
||||
List<ATData> executableAts = repository.getATRepository().getAllExecutableATs();
|
||||
|
||||
@@ -433,8 +662,12 @@ public class AtTests extends Common {
|
||||
}
|
||||
}
|
||||
|
||||
private DeployAtTransaction doDeploy(Repository repository, PrivateKeyAccount deployer) throws DataException {
|
||||
byte[] creationBytes = BTCACCT.buildQortalAT(deployer.getAddress(), secretHash, refundTimeout, initialPayout, redeemAmount, bitcoinAmount);
|
||||
private int calcTestLockTimeA(long messageTimestamp) {
|
||||
return (int) (messageTimestamp / 1000L + tradeTimeout * 60);
|
||||
}
|
||||
|
||||
private DeployAtTransaction doDeploy(Repository repository, PrivateKeyAccount deployer, String tradeAddress) throws DataException {
|
||||
byte[] creationBytes = BTCACCT.buildQortalAT(tradeAddress, bitcoinPublicKeyHash, hashOfSecretB, redeemAmount, bitcoinAmount, tradeTimeout);
|
||||
|
||||
long txTimestamp = System.currentTimeMillis();
|
||||
byte[] lastReference = deployer.getLastReference();
|
||||
@@ -493,6 +726,7 @@ public class AtTests extends Common {
|
||||
|
||||
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)
|
||||
@@ -500,7 +734,7 @@ public class AtTests extends Common {
|
||||
|
||||
// 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 - initialPayout;
|
||||
long expectedMaximumBalance = deployersInitialBalance - deployAtFee;
|
||||
|
||||
long actualBalance = deployer.getConfirmedBalance(Asset.QORT);
|
||||
|
||||
@@ -516,40 +750,43 @@ public class AtTests extends Common {
|
||||
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"
|
||||
+ "\tHASH160 of secret: %s,\n"
|
||||
+ "\tinitial payout: %s QORT,\n"
|
||||
+ "\tis finished: %b,\n"
|
||||
+ "\tHASH160 of secret-B: %s,\n"
|
||||
+ "\tredeem payout: %s QORT,\n"
|
||||
+ "\texpected bitcoin: %s BTC,\n"
|
||||
+ "\ttrade timeout: %d minutes (from trade start),\n"
|
||||
+ "\tcurrent block height: %d,\n",
|
||||
tradeData.qortalAtAddress,
|
||||
tradeData.mode.name(),
|
||||
tradeData.qortalCreator,
|
||||
epochMilliFormatter.apply(tradeData.creationTimestamp),
|
||||
Amounts.prettyAmount(tradeData.qortBalance),
|
||||
HashCode.fromBytes(tradeData.secretHash).toString().substring(0, 40),
|
||||
Amounts.prettyAmount(tradeData.initialPayout),
|
||||
Amounts.prettyAmount(tradeData.redeemPayout),
|
||||
atData.getIsFinished(),
|
||||
HashCode.fromBytes(tradeData.hashOfSecretB).toString().substring(0, 40),
|
||||
Amounts.prettyAmount(tradeData.qortAmount),
|
||||
Amounts.prettyAmount(tradeData.expectedBitcoin),
|
||||
tradeData.tradeRefundTimeout,
|
||||
currentBlockHeight));
|
||||
|
||||
// Are we in 'offer' or 'trade' stage?
|
||||
if (tradeData.tradeRefundHeight == null) {
|
||||
// Offer
|
||||
System.out.println(String.format("\tstatus: 'offer mode'"));
|
||||
} else {
|
||||
// Trade
|
||||
System.out.println(String.format("\tstatus: 'trade mode',\n"
|
||||
+ "\ttrade timeout: block %d,\n"
|
||||
+ "\tBitcoin P2SH nLockTime: %d (%s),\n"
|
||||
+ "\ttrade recipient: %s",
|
||||
if (tradeData.mode != BTCACCT.Mode.OFFERING && tradeData.mode != BTCACCT.Mode.CANCELLED) {
|
||||
System.out.println(String.format("\trefund height: block %d,\n"
|
||||
+ "\tHASH160 of secret-A: %s,\n"
|
||||
+ "\tBitcoin P2SH-A nLockTime: %d (%s),\n"
|
||||
+ "\tBitcoin P2SH-B nLockTime: %d (%s),\n"
|
||||
+ "\ttrade partner: %s",
|
||||
tradeData.tradeRefundHeight,
|
||||
tradeData.lockTime, epochMilliFormatter.apply(tradeData.lockTime * 1000L),
|
||||
tradeData.qortalRecipient));
|
||||
HashCode.fromBytes(tradeData.hashOfSecretA).toString().substring(0, 40),
|
||||
tradeData.lockTimeA, epochMilliFormatter.apply(tradeData.lockTimeA * 1000L),
|
||||
tradeData.lockTimeB, epochMilliFormatter.apply(tradeData.lockTimeB * 1000L),
|
||||
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");
|
||||
}
|
||||
|
||||
}
|
||||
|
@@ -5,12 +5,13 @@ import static org.junit.Assert.*;
|
||||
import java.util.Arrays;
|
||||
import java.util.List;
|
||||
|
||||
import org.bitcoinj.core.Transaction;
|
||||
import org.bitcoinj.store.BlockStoreException;
|
||||
import org.junit.After;
|
||||
import org.junit.Before;
|
||||
import org.junit.Test;
|
||||
import org.qortal.crosschain.BTC;
|
||||
import org.qortal.crosschain.BTCACCT;
|
||||
import org.qortal.crosschain.BTCP2SH;
|
||||
import org.qortal.repository.DataException;
|
||||
import org.qortal.test.common.Common;
|
||||
|
||||
@@ -55,11 +56,52 @@ public class BtcTests extends Common {
|
||||
|
||||
List<byte[]> rawTransactions = BTC.getInstance().getAddressTransactions(p2shAddress);
|
||||
|
||||
byte[] expectedSecret = AtTests.secret;
|
||||
byte[] secret = BTCACCT.findP2shSecret(p2shAddress, rawTransactions);
|
||||
byte[] expectedSecret = "This string is exactly 32 bytes!".getBytes();
|
||||
byte[] secret = BTCP2SH.findP2shSecret(p2shAddress, rawTransactions);
|
||||
|
||||
assertNotNull(secret);
|
||||
assertTrue("secret incorrect", Arrays.equals(expectedSecret, secret));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testBuildSpend() {
|
||||
BTC btc = BTC.getInstance();
|
||||
|
||||
String xprv58 = "tprv8ZgxMBicQKsPdahhFSrCdvC1bsWyzHHZfTneTVqUXN6s1wEtZLwAkZXzFP6TYLg2aQMecZLXLre5bTVGajEB55L1HYJcawpdFG66STVAWPJ";
|
||||
|
||||
String recipient = "2N8WCg52ULCtDSMjkgVTm5mtPdCsUptkHWE";
|
||||
long amount = 1000L;
|
||||
|
||||
Transaction transaction = btc.buildSpend(xprv58, recipient, amount);
|
||||
assertNotNull(transaction);
|
||||
|
||||
// Check spent key caching doesn't affect outcome
|
||||
|
||||
transaction = btc.buildSpend(xprv58, recipient, amount);
|
||||
assertNotNull(transaction);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testGetWalletBalance() {
|
||||
BTC btc = BTC.getInstance();
|
||||
|
||||
String xprv58 = "tprv8ZgxMBicQKsPdahhFSrCdvC1bsWyzHHZfTneTVqUXN6s1wEtZLwAkZXzFP6TYLg2aQMecZLXLre5bTVGajEB55L1HYJcawpdFG66STVAWPJ";
|
||||
|
||||
Long balance = btc.getWalletBalance(xprv58);
|
||||
|
||||
assertNotNull(balance);
|
||||
|
||||
System.out.println(BTC.format(balance));
|
||||
|
||||
// Check spent key caching doesn't affect outcome
|
||||
|
||||
Long repeatBalance = btc.getWalletBalance(xprv58);
|
||||
|
||||
assertNotNull(repeatBalance);
|
||||
|
||||
System.out.println(BTC.format(repeatBalance));
|
||||
|
||||
assertEquals(balance, repeatBalance);
|
||||
}
|
||||
|
||||
}
|
||||
|
@@ -13,7 +13,7 @@ import org.bitcoinj.script.Script.ScriptType;
|
||||
import org.bouncycastle.jce.provider.BouncyCastleProvider;
|
||||
import org.qortal.controller.Controller;
|
||||
import org.qortal.crosschain.BTC;
|
||||
import org.qortal.crosschain.BTCACCT;
|
||||
import org.qortal.crosschain.BTCP2SH;
|
||||
import org.qortal.crypto.Crypto;
|
||||
import org.qortal.repository.DataException;
|
||||
import org.qortal.repository.Repository;
|
||||
@@ -98,12 +98,12 @@ public class BuildP2SH {
|
||||
System.out.println(String.format("Bitcoin redeem amount: %s", bitcoinAmount.toPlainString()));
|
||||
|
||||
System.out.println(String.format("Redeem Bitcoin address: %s", redeemBitcoinAddress));
|
||||
System.out.println(String.format("Redeem miner's fee: %s", BTC.FORMAT.format(bitcoinFee)));
|
||||
System.out.println(String.format("Redeem miner's fee: %s", BTC.format(bitcoinFee)));
|
||||
|
||||
System.out.println(String.format("Redeem script lockTime: %s (%d)", LocalDateTime.ofInstant(Instant.ofEpochSecond(lockTime), ZoneOffset.UTC), lockTime));
|
||||
System.out.println(String.format("Hash of secret: %s", HashCode.fromBytes(secretHash)));
|
||||
|
||||
byte[] redeemScriptBytes = BTCACCT.buildScript(refundBitcoinAddress.getHash(), lockTime, redeemBitcoinAddress.getHash(), secretHash);
|
||||
byte[] redeemScriptBytes = BTCP2SH.buildScript(refundBitcoinAddress.getHash(), lockTime, redeemBitcoinAddress.getHash(), secretHash);
|
||||
System.out.println(String.format("Redeem script: %s", HashCode.fromBytes(redeemScriptBytes)));
|
||||
|
||||
byte[] redeemScriptHash = Crypto.hash160(redeemScriptBytes);
|
||||
@@ -115,7 +115,7 @@ public class BuildP2SH {
|
||||
|
||||
// Fund P2SH
|
||||
System.out.println(String.format("\nYou need to fund %s with %s (includes redeem/refund fee of %s)",
|
||||
p2shAddress.toString(), BTC.FORMAT.format(bitcoinAmount), BTC.FORMAT.format(bitcoinFee)));
|
||||
p2shAddress.toString(), BTC.format(bitcoinAmount), BTC.format(bitcoinFee)));
|
||||
|
||||
System.out.println("Once this is done, responder should run Respond to check P2SH funding and create AT");
|
||||
} catch (DataException e) {
|
||||
|
@@ -15,7 +15,7 @@ import org.bitcoinj.script.Script.ScriptType;
|
||||
import org.bouncycastle.jce.provider.BouncyCastleProvider;
|
||||
import org.qortal.controller.Controller;
|
||||
import org.qortal.crosschain.BTC;
|
||||
import org.qortal.crosschain.BTCACCT;
|
||||
import org.qortal.crosschain.BTCP2SH;
|
||||
import org.qortal.crypto.Crypto;
|
||||
import org.qortal.repository.DataException;
|
||||
import org.qortal.repository.Repository;
|
||||
@@ -106,14 +106,14 @@ public class CheckP2SH {
|
||||
System.out.println(String.format("Bitcoin redeem amount: %s", bitcoinAmount.toPlainString()));
|
||||
|
||||
System.out.println(String.format("Redeem Bitcoin address: %s", refundBitcoinAddress));
|
||||
System.out.println(String.format("Redeem miner's fee: %s", BTC.FORMAT.format(bitcoinFee)));
|
||||
System.out.println(String.format("Redeem miner's fee: %s", BTC.format(bitcoinFee)));
|
||||
|
||||
System.out.println(String.format("Redeem script lockTime: %s (%d)", LocalDateTime.ofInstant(Instant.ofEpochSecond(lockTime), ZoneOffset.UTC), lockTime));
|
||||
System.out.println(String.format("Hash of secret: %s", HashCode.fromBytes(secretHash)));
|
||||
|
||||
System.out.println(String.format("P2SH address: %s", p2shAddress));
|
||||
|
||||
byte[] redeemScriptBytes = BTCACCT.buildScript(refundBitcoinAddress.getHash(), lockTime, redeemBitcoinAddress.getHash(), secretHash);
|
||||
byte[] redeemScriptBytes = BTCP2SH.buildScript(refundBitcoinAddress.getHash(), lockTime, redeemBitcoinAddress.getHash(), secretHash);
|
||||
System.out.println(String.format("Redeem script: %s", HashCode.fromBytes(redeemScriptBytes)));
|
||||
|
||||
byte[] redeemScriptHash = Crypto.hash160(redeemScriptBytes);
|
||||
@@ -135,12 +135,12 @@ public class CheckP2SH {
|
||||
System.out.println(String.format("Too soon (%s) to redeem based on median block time %s", LocalDateTime.ofInstant(Instant.ofEpochMilli(now), ZoneOffset.UTC), LocalDateTime.ofInstant(Instant.ofEpochSecond(medianBlockTime), ZoneOffset.UTC)));
|
||||
|
||||
// Check P2SH is funded
|
||||
Coin p2shBalance = BTC.getInstance().getBalance(p2shAddress.toString());
|
||||
Long p2shBalance = BTC.getInstance().getBalance(p2shAddress.toString());
|
||||
if (p2shBalance == null) {
|
||||
System.err.println(String.format("Unable to check P2SH address %s balance", p2shAddress));
|
||||
System.exit(2);
|
||||
}
|
||||
System.out.println(String.format("P2SH address %s balance: %s", p2shAddress, BTC.FORMAT.format(p2shBalance)));
|
||||
System.out.println(String.format("P2SH address %s balance: %s", p2shAddress, BTC.format(p2shBalance)));
|
||||
|
||||
// Grab all P2SH funding transactions (just in case there are more than one)
|
||||
List<TransactionOutput> fundingOutputs = BTC.getInstance().getUnspentOutputs(p2shAddress.toString());
|
||||
@@ -152,7 +152,7 @@ public class CheckP2SH {
|
||||
System.out.println(String.format("Found %d output%s for P2SH", fundingOutputs.size(), (fundingOutputs.size() != 1 ? "s" : "")));
|
||||
|
||||
for (TransactionOutput fundingOutput : fundingOutputs)
|
||||
System.out.println(String.format("Output %s:%d amount %s", HashCode.fromBytes(fundingOutput.getParentTransactionHash().getBytes()), fundingOutput.getIndex(), BTC.FORMAT.format(fundingOutput.getValue())));
|
||||
System.out.println(String.format("Output %s:%d amount %s", HashCode.fromBytes(fundingOutput.getParentTransactionHash().getBytes()), fundingOutput.getIndex(), BTC.format(fundingOutput.getValue())));
|
||||
|
||||
if (fundingOutputs.isEmpty()) {
|
||||
System.err.println(String.format("Can't redeem spent/unfunded P2SH"));
|
||||
|
@@ -34,20 +34,20 @@ public class DeployAT {
|
||||
if (error != null)
|
||||
System.err.println(error);
|
||||
|
||||
System.err.println(String.format("usage: DeployAT <your Qortal PRIVATE key> <QORT amount> <BTC amount> <HASH160-of-secret> <initial QORT payout> <AT funding amount> <AT trade timeout>"));
|
||||
System.err.println(String.format("usage: DeployAT <your Qortal PRIVATE key> <QORT amount> <BTC amount> <your Bitcoin PKH> <HASH160-of-secret> <AT funding amount> <trade-timeout>"));
|
||||
System.err.println(String.format("example: DeployAT "
|
||||
+ "AdTd9SUEYSdTW8mgK3Gu72K97bCHGdUwi2VvLNjUohot \\\n"
|
||||
+ "\t80.4020 \\\n"
|
||||
+ "\t0.00864200 \\\n"
|
||||
+ "\t750b06757a2448b8a4abebaa6e4662833fd5ddbb \\\n"
|
||||
+ "\tdaf59884b4d1aec8c1b17102530909ee43c0151a \\\n"
|
||||
+ "\t0.0001 \\\n"
|
||||
+ "\t123.456 \\\n"
|
||||
+ "\t10"));
|
||||
+ "\t10080"));
|
||||
System.exit(1);
|
||||
}
|
||||
|
||||
public static void main(String[] args) {
|
||||
if (args.length != 8)
|
||||
if (args.length != 7)
|
||||
usage(null);
|
||||
|
||||
Security.insertProviderAt(new BouncyCastleProvider(), 0);
|
||||
@@ -56,8 +56,8 @@ public class DeployAT {
|
||||
byte[] refundPrivateKey = null;
|
||||
long redeemAmount = 0;
|
||||
long expectedBitcoin = 0;
|
||||
byte[] bitcoinPublicKeyHash = null;
|
||||
byte[] secretHash = null;
|
||||
long initialPayout = 0;
|
||||
long fundingAmount = 0;
|
||||
int tradeTimeout = 0;
|
||||
|
||||
@@ -75,21 +75,21 @@ public class DeployAT {
|
||||
if (expectedBitcoin <= 0)
|
||||
usage("Expected BTC amount must be positive");
|
||||
|
||||
bitcoinPublicKeyHash = HashCode.fromString(args[argIndex++]).asBytes();
|
||||
if (bitcoinPublicKeyHash.length != 20)
|
||||
usage("Bitcoin PKH must be 20 bytes");
|
||||
|
||||
secretHash = HashCode.fromString(args[argIndex++]).asBytes();
|
||||
if (secretHash.length != 20)
|
||||
usage("Hash of secret must be 20 bytes");
|
||||
|
||||
initialPayout = Long.parseLong(args[argIndex++]);
|
||||
if (initialPayout < 0)
|
||||
usage("Initial QORT payout must be positive");
|
||||
|
||||
fundingAmount = Long.parseLong(args[argIndex++]);
|
||||
if (fundingAmount <= redeemAmount)
|
||||
usage("AT funding amount must be greater than QORT redeem amount");
|
||||
|
||||
tradeTimeout = Integer.parseInt(args[argIndex++]);
|
||||
if (tradeTimeout < 10 || tradeTimeout > 50000)
|
||||
usage("AT trade timeout should be between 10 and 50,000 minutes");
|
||||
if (tradeTimeout < 60 || tradeTimeout > 50000)
|
||||
usage("Trade timeout (minutes) must be between 60 and 50000");
|
||||
} catch (IllegalArgumentException e) {
|
||||
usage(String.format("Invalid argument %d: %s", argIndex, e.getMessage()));
|
||||
}
|
||||
@@ -114,7 +114,7 @@ public class DeployAT {
|
||||
System.out.println(String.format("HASH160 of secret: %s", HashCode.fromBytes(secretHash)));
|
||||
|
||||
// Deploy AT
|
||||
byte[] creationBytes = BTCACCT.buildQortalAT(refundAccount.getAddress(), secretHash, tradeTimeout, initialPayout, redeemAmount, expectedBitcoin);
|
||||
byte[] creationBytes = BTCACCT.buildQortalAT(refundAccount.getAddress(), bitcoinPublicKeyHash, secretHash, redeemAmount, expectedBitcoin, tradeTimeout);
|
||||
System.out.println("CIYAM AT creation bytes: " + HashCode.fromBytes(creationBytes).toString());
|
||||
|
||||
long txTimestamp = System.currentTimeMillis();
|
||||
|
@@ -12,8 +12,8 @@ import org.bouncycastle.jce.provider.BouncyCastleProvider;
|
||||
import org.bouncycastle.jsse.provider.BouncyCastleJsseProvider;
|
||||
import org.junit.Test;
|
||||
import org.qortal.crosschain.ElectrumX;
|
||||
import org.qortal.crosschain.ElectrumX.UnspentOutput;
|
||||
import org.qortal.utils.BitTwiddling;
|
||||
import org.qortal.utils.Pair;
|
||||
|
||||
import com.google.common.hash.HashCode;
|
||||
|
||||
@@ -61,7 +61,7 @@ public class ElectrumXTests {
|
||||
|
||||
// Timestamp(int) is at 4 + 32 + 32 = 68 bytes offset
|
||||
int offset = 4 + 32 + 32;
|
||||
int timestamp = BitTwiddling.fromLEBytes(blockHeader, offset);
|
||||
int timestamp = BitTwiddling.intFromLEBytes(blockHeader, offset);
|
||||
System.out.println(String.format("Block %d timestamp: %d", height + i, timestamp));
|
||||
}
|
||||
}
|
||||
@@ -100,13 +100,13 @@ public class ElectrumXTests {
|
||||
|
||||
Address address = Address.fromString(TestNet3Params.get(), "2N4szZUfigj7fSBCEX4PaC8TVbC5EvidaVF");
|
||||
byte[] script = ScriptBuilder.createOutputScript(address).getProgram();
|
||||
List<Pair<byte[], Integer>> unspentOutputs = electrumX.getUnspentOutputs(script);
|
||||
List<UnspentOutput> unspentOutputs = electrumX.getUnspentOutputs(script);
|
||||
|
||||
assertNotNull(unspentOutputs);
|
||||
assertFalse(unspentOutputs.isEmpty());
|
||||
|
||||
for (Pair<byte[], Integer> unspentOutput : unspentOutputs)
|
||||
System.out.println(String.format("TestNet address %s has unspent output at tx %s, output index %d", address, HashCode.fromBytes(unspentOutput.getA()).toString(), unspentOutput.getB()));
|
||||
for (UnspentOutput unspentOutput : unspentOutputs)
|
||||
System.out.println(String.format("TestNet address %s has unspent output at tx %s, output index %d", address, HashCode.fromBytes(unspentOutput.hash), unspentOutput.index));
|
||||
}
|
||||
|
||||
@Test
|
||||
|
@@ -18,7 +18,7 @@ import org.bitcoinj.script.Script.ScriptType;
|
||||
import org.bouncycastle.jce.provider.BouncyCastleProvider;
|
||||
import org.qortal.controller.Controller;
|
||||
import org.qortal.crosschain.BTC;
|
||||
import org.qortal.crosschain.BTCACCT;
|
||||
import org.qortal.crosschain.BTCP2SH;
|
||||
import org.qortal.crypto.Crypto;
|
||||
import org.qortal.repository.DataException;
|
||||
import org.qortal.repository.Repository;
|
||||
@@ -107,7 +107,7 @@ public class Redeem {
|
||||
System.out.println("Confirm the following is correct based on the info you've given:");
|
||||
|
||||
System.out.println(String.format("Redeem PRIVATE key: %s", HashCode.fromBytes(redeemPrivateKey)));
|
||||
System.out.println(String.format("Redeem miner's fee: %s", BTC.FORMAT.format(bitcoinFee)));
|
||||
System.out.println(String.format("Redeem miner's fee: %s", BTC.format(bitcoinFee)));
|
||||
System.out.println(String.format("Redeem script lockTime: %s (%d)", LocalDateTime.ofInstant(Instant.ofEpochSecond(lockTime), ZoneOffset.UTC), lockTime));
|
||||
|
||||
// New/derived info
|
||||
@@ -121,7 +121,7 @@ public class Redeem {
|
||||
|
||||
System.out.println(String.format("P2SH address: %s", p2shAddress));
|
||||
|
||||
byte[] redeemScriptBytes = BTCACCT.buildScript(refundBitcoinAddress.getHash(), lockTime, redeemAddress.getHash(), secretHash);
|
||||
byte[] redeemScriptBytes = BTCP2SH.buildScript(refundBitcoinAddress.getHash(), lockTime, redeemAddress.getHash(), secretHash);
|
||||
System.out.println(String.format("Redeem script: %s", HashCode.fromBytes(redeemScriptBytes)));
|
||||
|
||||
byte[] redeemScriptHash = Crypto.hash160(redeemScriptBytes);
|
||||
@@ -147,12 +147,12 @@ public class Redeem {
|
||||
}
|
||||
|
||||
// Check P2SH is funded
|
||||
Coin p2shBalance = BTC.getInstance().getBalance(p2shAddress.toString());
|
||||
Long p2shBalance = BTC.getInstance().getBalance(p2shAddress.toString());
|
||||
if (p2shBalance == null) {
|
||||
System.err.println(String.format("Unable to check P2SH address %s balance", p2shAddress));
|
||||
System.exit(2);
|
||||
}
|
||||
System.out.println(String.format("P2SH address %s balance: %s BTC", p2shAddress, p2shBalance.toPlainString()));
|
||||
System.out.println(String.format("P2SH address %s balance: %s", p2shAddress, BTC.format(p2shBalance)));
|
||||
|
||||
// Grab all P2SH funding transactions (just in case there are more than one)
|
||||
List<TransactionOutput> fundingOutputs = BTC.getInstance().getUnspentOutputs(p2shAddress.toString());
|
||||
@@ -164,7 +164,7 @@ public class Redeem {
|
||||
System.out.println(String.format("Found %d output%s for P2SH", fundingOutputs.size(), (fundingOutputs.size() != 1 ? "s" : "")));
|
||||
|
||||
for (TransactionOutput fundingOutput : fundingOutputs)
|
||||
System.out.println(String.format("Output %s:%d amount %s", HashCode.fromBytes(fundingOutput.getParentTransactionHash().getBytes()), fundingOutput.getIndex(), BTC.FORMAT.format(fundingOutput.getValue())));
|
||||
System.out.println(String.format("Output %s:%d amount %s", HashCode.fromBytes(fundingOutput.getParentTransactionHash().getBytes()), fundingOutput.getIndex(), BTC.format(fundingOutput.getValue())));
|
||||
|
||||
if (fundingOutputs.isEmpty()) {
|
||||
System.err.println(String.format("Can't redeem spent/unfunded P2SH"));
|
||||
@@ -179,10 +179,10 @@ public class Redeem {
|
||||
for (TransactionOutput fundingOutput : fundingOutputs)
|
||||
System.out.println(String.format("Using output %s:%d for redeem", HashCode.fromBytes(fundingOutput.getParentTransactionHash().getBytes()), fundingOutput.getIndex()));
|
||||
|
||||
Coin redeemAmount = p2shBalance.subtract(bitcoinFee);
|
||||
System.out.println(String.format("Spending %s of output, with %s as mining fee", BTC.FORMAT.format(redeemAmount), BTC.FORMAT.format(bitcoinFee)));
|
||||
Coin redeemAmount = Coin.valueOf(p2shBalance).subtract(bitcoinFee);
|
||||
System.out.println(String.format("Spending %s of output, with %s as mining fee", BTC.format(redeemAmount), BTC.format(bitcoinFee)));
|
||||
|
||||
Transaction redeemTransaction = BTCACCT.buildRedeemTransaction(redeemAmount, redeemKey, fundingOutputs, redeemScriptBytes, secret);
|
||||
Transaction redeemTransaction = BTCP2SH.buildRedeemTransaction(redeemAmount, redeemKey, fundingOutputs, redeemScriptBytes, secret, redeemAddress.getHash());
|
||||
|
||||
byte[] redeemBytes = redeemTransaction.bitcoinSerialize();
|
||||
|
||||
|
@@ -18,7 +18,7 @@ import org.bitcoinj.script.Script.ScriptType;
|
||||
import org.bouncycastle.jce.provider.BouncyCastleProvider;
|
||||
import org.qortal.controller.Controller;
|
||||
import org.qortal.crosschain.BTC;
|
||||
import org.qortal.crosschain.BTCACCT;
|
||||
import org.qortal.crosschain.BTCP2SH;
|
||||
import org.qortal.crypto.Crypto;
|
||||
import org.qortal.repository.DataException;
|
||||
import org.qortal.repository.Repository;
|
||||
@@ -110,7 +110,7 @@ public class Refund {
|
||||
System.out.println(String.format("Redeem Bitcoin address: %s", redeemBitcoinAddress));
|
||||
System.out.println(String.format("Redeem script lockTime: %s (%d)", LocalDateTime.ofInstant(Instant.ofEpochSecond(lockTime), ZoneOffset.UTC), lockTime));
|
||||
System.out.println(String.format("P2SH address: %s", p2shAddress));
|
||||
System.out.println(String.format("Refund miner's fee: %s", BTC.FORMAT.format(bitcoinFee)));
|
||||
System.out.println(String.format("Refund miner's fee: %s", BTC.format(bitcoinFee)));
|
||||
|
||||
// New/derived info
|
||||
|
||||
@@ -120,7 +120,7 @@ public class Refund {
|
||||
Address refundAddress = Address.fromKey(params, refundKey, ScriptType.P2PKH);
|
||||
System.out.println(String.format("Refund recipient (PKH): %s (%s)", refundAddress, HashCode.fromBytes(refundAddress.getHash())));
|
||||
|
||||
byte[] redeemScriptBytes = BTCACCT.buildScript(refundAddress.getHash(), lockTime, redeemBitcoinAddress.getHash(), secretHash);
|
||||
byte[] redeemScriptBytes = BTCP2SH.buildScript(refundAddress.getHash(), lockTime, redeemBitcoinAddress.getHash(), secretHash);
|
||||
System.out.println(String.format("Redeem script: %s", HashCode.fromBytes(redeemScriptBytes)));
|
||||
|
||||
byte[] redeemScriptHash = Crypto.hash160(redeemScriptBytes);
|
||||
@@ -151,12 +151,12 @@ public class Refund {
|
||||
}
|
||||
|
||||
// Check P2SH is funded
|
||||
Coin p2shBalance = BTC.getInstance().getBalance(p2shAddress.toString());
|
||||
Long p2shBalance = BTC.getInstance().getBalance(p2shAddress.toString());
|
||||
if (p2shBalance == null) {
|
||||
System.err.println(String.format("Unable to check P2SH address %s balance", p2shAddress));
|
||||
System.exit(2);
|
||||
}
|
||||
System.out.println(String.format("P2SH address %s balance: %s BTC", p2shAddress, p2shBalance.toPlainString()));
|
||||
System.out.println(String.format("P2SH address %s balance: %s", p2shAddress, BTC.format(p2shBalance)));
|
||||
|
||||
// Grab all P2SH funding transactions (just in case there are more than one)
|
||||
List<TransactionOutput> fundingOutputs = BTC.getInstance().getUnspentOutputs(p2shAddress.toString());
|
||||
@@ -168,7 +168,7 @@ public class Refund {
|
||||
System.out.println(String.format("Found %d output%s for P2SH", fundingOutputs.size(), (fundingOutputs.size() != 1 ? "s" : "")));
|
||||
|
||||
for (TransactionOutput fundingOutput : fundingOutputs)
|
||||
System.out.println(String.format("Output %s:%d amount %s", HashCode.fromBytes(fundingOutput.getParentTransactionHash().getBytes()), fundingOutput.getIndex(), BTC.FORMAT.format(fundingOutput.getValue())));
|
||||
System.out.println(String.format("Output %s:%d amount %s", HashCode.fromBytes(fundingOutput.getParentTransactionHash().getBytes()), fundingOutput.getIndex(), BTC.format(fundingOutput.getValue())));
|
||||
|
||||
if (fundingOutputs.isEmpty()) {
|
||||
System.err.println(String.format("Can't refund spent/unfunded P2SH"));
|
||||
@@ -183,10 +183,10 @@ public class Refund {
|
||||
for (TransactionOutput fundingOutput : fundingOutputs)
|
||||
System.out.println(String.format("Using output %s:%d for redeem", HashCode.fromBytes(fundingOutput.getParentTransactionHash().getBytes()), fundingOutput.getIndex()));
|
||||
|
||||
Coin refundAmount = p2shBalance.subtract(bitcoinFee);
|
||||
System.out.println(String.format("Spending %s of output, with %s as mining fee", BTC.FORMAT.format(refundAmount), BTC.FORMAT.format(bitcoinFee)));
|
||||
Coin refundAmount = Coin.valueOf(p2shBalance).subtract(bitcoinFee);
|
||||
System.out.println(String.format("Spending %s of output, with %s as mining fee", BTC.format(refundAmount), BTC.format(bitcoinFee)));
|
||||
|
||||
Transaction redeemTransaction = BTCACCT.buildRefundTransaction(refundAmount, refundKey, fundingOutputs, redeemScriptBytes, lockTime);
|
||||
Transaction redeemTransaction = BTCP2SH.buildRefundTransaction(refundAmount, refundKey, fundingOutputs, redeemScriptBytes, lockTime);
|
||||
|
||||
byte[] redeemBytes = redeemTransaction.bitcoinSerialize();
|
||||
|
||||
|
@@ -20,15 +20,19 @@ public class AccountUtils {
|
||||
public static final int txGroupId = Group.NO_GROUP;
|
||||
public static final long fee = 1L * Amounts.MULTIPLIER;
|
||||
|
||||
public static void pay(Repository repository, String sender, String recipient, long amount) throws DataException {
|
||||
PrivateKeyAccount sendingAccount = Common.getTestAccount(repository, sender);
|
||||
PrivateKeyAccount recipientAccount = Common.getTestAccount(repository, recipient);
|
||||
public static void pay(Repository repository, String testSenderName, String testRecipientName, long amount) throws DataException {
|
||||
PrivateKeyAccount sendingAccount = Common.getTestAccount(repository, testSenderName);
|
||||
PrivateKeyAccount recipientAccount = Common.getTestAccount(repository, testRecipientName);
|
||||
|
||||
pay(repository, sendingAccount, recipientAccount.getAddress(), amount);
|
||||
}
|
||||
|
||||
public static void pay(Repository repository, PrivateKeyAccount sendingAccount, String recipientAddress, long amount) throws DataException {
|
||||
byte[] reference = sendingAccount.getLastReference();
|
||||
long timestamp = repository.getTransactionRepository().fromSignature(reference).getTimestamp() + 1;
|
||||
|
||||
BaseTransactionData baseTransactionData = new BaseTransactionData(timestamp, txGroupId, reference, sendingAccount.getPublicKey(), fee, null);
|
||||
TransactionData transactionData = new PaymentTransactionData(baseTransactionData, recipientAccount.getAddress(), amount);
|
||||
TransactionData transactionData = new PaymentTransactionData(baseTransactionData, recipientAddress, amount);
|
||||
|
||||
TransactionUtils.signAndMint(repository, transactionData, sendingAccount);
|
||||
}
|
||||
|
13
tools/peer-heights
Executable file
13
tools/peer-heights
Executable file
@@ -0,0 +1,13 @@
|
||||
#!/usr/bin/env bash
|
||||
|
||||
# Requires: 'qort' script in PATH, and 'jq' utility installed
|
||||
|
||||
set -e
|
||||
|
||||
# Any extra args passed to us are also passed to 'qort', just prior to '-p peers'
|
||||
qort $@ -p peers | \
|
||||
jq -r 'def lpad($len):
|
||||
tostring | ($len - length) as $l | (" " * $l)[:$l] + .;
|
||||
.[] |
|
||||
select(has("lastHeight")) |
|
||||
"\(.address | lpad(22)) (\(.version)), height \(.lastHeight), sig: \(.lastBlockSignature[0:8]), ts \(.lastBlockTimestamp / 1e3 | strftime("%Y-%m-%d %H:%M:%S"))"'
|
87
tools/qort
Executable file
87
tools/qort
Executable file
@@ -0,0 +1,87 @@
|
||||
#!/usr/bin/env bash
|
||||
|
||||
# default output post-processor
|
||||
postproc=cat
|
||||
|
||||
# Qortal defaults
|
||||
port=12391
|
||||
example_host=node10.qortal.org
|
||||
|
||||
# called-as name
|
||||
name="${0##*/}"
|
||||
|
||||
while [ -n "$*" ]; do
|
||||
case $1 in
|
||||
-p)
|
||||
shift
|
||||
postproc="json_pp -f json -t json -json_opt utf8,pretty"
|
||||
;;
|
||||
|
||||
[Dd][Ee][Ll][Ee][Tt][Ee])
|
||||
shift
|
||||
method="-X DELETE"
|
||||
;;
|
||||
|
||||
-l)
|
||||
shift
|
||||
src="--interface localhost"
|
||||
;;
|
||||
|
||||
-t)
|
||||
shift
|
||||
testnet=true
|
||||
;;
|
||||
|
||||
-j)
|
||||
shift
|
||||
content_type="Content-Type: application/json"
|
||||
;;
|
||||
|
||||
*)
|
||||
break
|
||||
;;
|
||||
esac
|
||||
done
|
||||
|
||||
if [ "${name}" = "qort" ]; then
|
||||
port=${testnet:+62391}
|
||||
port=${port:-12391}
|
||||
example_host=node10.qortal.org
|
||||
fi
|
||||
|
||||
if [ -z "$*" ]; then
|
||||
echo "usage: $name [-l] [-p] [-t] [DELETE] <url> [<post-data>]"
|
||||
echo "-l: use localhost as source address"
|
||||
echo "-p: pretty-print JSON output"
|
||||
echo "-t: use testnet port"
|
||||
echo "example (using localhost:${port}): $name -p blocks/last"
|
||||
echo "example: $name -p http://${example_host}:${port}/blocks/last"
|
||||
echo "example: BASE_URL=http://${example_host}:${port} $name -p blocks/last"
|
||||
echo "example: BASE_URL=${example_host} $name -p blocks/last"
|
||||
echo "example: $name -l DELETE peers/known"
|
||||
exit
|
||||
fi
|
||||
|
||||
url=$1
|
||||
shift
|
||||
|
||||
if [ "${url:0:4}" != "http" ]; then
|
||||
base_url=${BASE_URL-localhost:${port}}
|
||||
|
||||
if [ "${base_url:0:4}" != "http" ]; then
|
||||
base_url="http://${base_url}"
|
||||
fi
|
||||
|
||||
if [ -n "${base_url/#*:[0-9[0-9]*}" ]; then
|
||||
base_url="${base_url%%/}:${port}"
|
||||
fi
|
||||
|
||||
url="${base_url%%/}/${url#/}"
|
||||
fi
|
||||
|
||||
if [ "$#" != 0 ]; then
|
||||
data="--data"
|
||||
fi
|
||||
|
||||
curl --silent --insecure --connect-timeout 5 ${content_type:+--header} "${content_type}" ${method} ${src} --url ${url} ${data} "$@" | ${postproc}
|
||||
echo
|
353
tools/tx.pl
Executable file
353
tools/tx.pl
Executable file
@@ -0,0 +1,353 @@
|
||||
#!/usr/bin/env perl
|
||||
|
||||
use JSON;
|
||||
use warnings;
|
||||
use strict;
|
||||
|
||||
use Getopt::Std;
|
||||
use File::Basename;
|
||||
|
||||
our %opt;
|
||||
getopts('dpst', \%opt);
|
||||
|
||||
my $proc = basename($0);
|
||||
|
||||
if (@ARGV < 1) {
|
||||
print STDERR "usage: $proc [-d] [-p] [-s] [-t] <tx-type> <privkey> <values> [<key-value pairs>]\n";
|
||||
print STDERR "-d: debug, -p: process (broadcast) transaction, -s: sign, -t: testnet\n";
|
||||
print STDERR "example: $proc PAYMENT P22kW91AJfDNBj32nVii292hhfo5AgvUYPz5W12ExsjE QxxQZiK7LZBjmpGjRz1FAZSx9MJDCoaHqz 0.1\n";
|
||||
print STDERR "example: $proc JOIN_GROUP X92h3hf9k20kBj32nVnoh3XT14o5AgvUYPz5W12ExsjE 3\n";
|
||||
print STDERR "example: BASE_URL=node10.qortal.org $proc JOIN_GROUP CB2DW91AJfd47432nVnoh3XT14o5AgvUYPz5W12ExsjE 3\n";
|
||||
print STDERR "example: $proc -p sign C4ifh827ffDNBj32nVnoh3XT14o5AgvUYPz5W12ExsjE 111jivxUwerRw...Fjtu\n";
|
||||
print STDERR "for help: $proc all\n";
|
||||
print STDERR "for help: $proc REGISTER_NAME\n";
|
||||
exit 2;
|
||||
}
|
||||
|
||||
our $BASE_URL = $ENV{BASE_URL} || $opt{t} ? 'http://localhost:62391' : 'http://localhost:12391';
|
||||
our $DEFAULT_FEE = 0.001;
|
||||
|
||||
our %TRANSACTION_TYPES = (
|
||||
payment => {
|
||||
url => 'payments/pay',
|
||||
required => [qw(recipient amount)],
|
||||
key_name => 'senderPublicKey',
|
||||
},
|
||||
# groups
|
||||
set_group => {
|
||||
url => 'groups/setdefault',
|
||||
required => [qw(defaultGroupId)],
|
||||
key_name => 'creatorPublicKey',
|
||||
},
|
||||
create_group => {
|
||||
url => 'groups/create',
|
||||
required => [qw(groupName description isOpen approvalThreshold)],
|
||||
key_name => 'creatorPublicKey',
|
||||
},
|
||||
update_group => {
|
||||
url => 'groups/update',
|
||||
required => [qw(groupId newOwner newDescription newIsOpen newApprovalThreshold)],
|
||||
key_name => 'ownerPublicKey',
|
||||
},
|
||||
join_group => {
|
||||
url => 'groups/join',
|
||||
required => [qw(groupId)],
|
||||
key_name => 'joinerPublicKey',
|
||||
},
|
||||
leave_group => {
|
||||
url => 'groups/leave',
|
||||
required => [qw(groupId)],
|
||||
key_name => 'leaverPublicKey',
|
||||
},
|
||||
group_invite => {
|
||||
url => 'groups/invite',
|
||||
required => [qw(groupId invitee)],
|
||||
key_name => 'adminPublicKey',
|
||||
},
|
||||
group_kick => {
|
||||
url => 'groups/kick',
|
||||
required => [qw(groupId member reason)],
|
||||
key_name => 'adminPublicKey',
|
||||
},
|
||||
add_group_admin => {
|
||||
url => 'groups/addadmin',
|
||||
required => [qw(groupId member)],
|
||||
key_name => 'ownerPublicKey',
|
||||
},
|
||||
group_approval => {
|
||||
url => 'groups/approval',
|
||||
required => [qw(pendingSignature approval)],
|
||||
key_name => 'adminPublicKey',
|
||||
},
|
||||
# assets
|
||||
issue_asset => {
|
||||
url => 'assets/issue',
|
||||
required => [qw(assetName description quantity isDivisible)],
|
||||
key_name => 'issuerPublicKey',
|
||||
},
|
||||
update_asset => {
|
||||
url => 'assets/update',
|
||||
required => [qw(assetId newOwner)],
|
||||
key_name => 'ownerPublicKey',
|
||||
},
|
||||
transfer_asset => {
|
||||
url => 'assets/transfer',
|
||||
required => [qw(recipient amount assetId)],
|
||||
key_name => 'senderPublicKey',
|
||||
},
|
||||
create_order => {
|
||||
url => 'assets/order',
|
||||
required => [qw(haveAssetId wantAssetId amount price)],
|
||||
key_name => 'creatorPublicKey',
|
||||
},
|
||||
# names
|
||||
register_name => {
|
||||
url => 'names/register',
|
||||
required => [qw(name data)],
|
||||
key_name => 'registrantPublicKey',
|
||||
},
|
||||
update_name => {
|
||||
url => 'names/update',
|
||||
required => [qw(newName newData)],
|
||||
key_name => 'ownerPublicKey',
|
||||
},
|
||||
# reward-shares
|
||||
reward_share => {
|
||||
url => 'addresses/rewardshare',
|
||||
required => [qw(recipient rewardSharePublicKey sharePercent)],
|
||||
key_name => 'minterPublicKey',
|
||||
},
|
||||
# arbitrary
|
||||
arbitrary => {
|
||||
url => 'arbitrary',
|
||||
required => [qw(service dataType data)],
|
||||
key_name => 'senderPublicKey',
|
||||
},
|
||||
# chat
|
||||
chat => {
|
||||
url => 'chat',
|
||||
required => [qw(data)],
|
||||
optional => [qw(recipient isText isEncrypted)],
|
||||
key_name => 'senderPublicKey',
|
||||
defaults => { isText => 'true' },
|
||||
pow_url => 'chat/compute',
|
||||
},
|
||||
# misc
|
||||
publicize => {
|
||||
url => 'addresses/publicize',
|
||||
required => [],
|
||||
key_name => 'senderPublicKey',
|
||||
pow_url => 'addresses/publicize/compute',
|
||||
},
|
||||
# Cross-chain trading
|
||||
build_trade => {
|
||||
url => 'crosschain/build',
|
||||
required => [qw(initialQortAmount finalQortAmount fundingQortAmount secretHash bitcoinAmount)],
|
||||
optional => [qw(tradeTimeout)],
|
||||
key_name => 'creatorPublicKey',
|
||||
defaults => { tradeTimeout => 10800 },
|
||||
},
|
||||
trade_recipient => {
|
||||
url => 'crosschain/tradeoffer/recipient',
|
||||
required => [qw(atAddress recipient)],
|
||||
key_name => 'creatorPublicKey',
|
||||
remove => [qw(timestamp reference fee)],
|
||||
},
|
||||
trade_secret => {
|
||||
url => 'crosschain/tradeoffer/secret',
|
||||
required => [qw(atAddress secret)],
|
||||
key_name => 'recipientPublicKey',
|
||||
remove => [qw(timestamp reference fee)],
|
||||
},
|
||||
# These are fake transaction types to provide utility functions:
|
||||
sign => {
|
||||
url => 'transactions/sign',
|
||||
required => [qw{transactionBytes}],
|
||||
},
|
||||
);
|
||||
|
||||
my $tx_type = lc(shift(@ARGV));
|
||||
|
||||
if ($tx_type eq 'all') {
|
||||
printf STDERR "Transaction types: %s\n", join(', ', sort { $a cmp $b } keys %TRANSACTION_TYPES);
|
||||
exit 2;
|
||||
}
|
||||
|
||||
my $tx_info = $TRANSACTION_TYPES{$tx_type};
|
||||
|
||||
if (!$tx_info) {
|
||||
printf STDERR "Transaction type '%s' unknown\n", uc($tx_type);
|
||||
exit 1;
|
||||
}
|
||||
|
||||
my @required = @{$tx_info->{required}};
|
||||
|
||||
if (@ARGV < @required + 1) {
|
||||
printf STDERR "usage: %s %s <privkey> %s", $proc, uc($tx_type), join(' ', map { "<$_>"} @required);
|
||||
printf STDERR " %s", join(' ', map { "[$_ <$_>]" } @{$tx_info->{optional}}) if exists $tx_info->{optional};
|
||||
print "\n";
|
||||
exit 2;
|
||||
}
|
||||
|
||||
my $priv_key = shift @ARGV;
|
||||
|
||||
my $account = account($priv_key);
|
||||
my $raw;
|
||||
|
||||
if ($tx_type ne 'sign') {
|
||||
my %extras;
|
||||
|
||||
foreach my $required_arg (@required) {
|
||||
$extras{$required_arg} = shift @ARGV;
|
||||
}
|
||||
|
||||
# For CHAT we use a random reference
|
||||
if ($tx_type eq 'chat') {
|
||||
$extras{reference} = api('utils/random?length=64');
|
||||
}
|
||||
|
||||
%extras = (%extras, %{$tx_info->{defaults}}) if exists $tx_info->{defaults};
|
||||
|
||||
%extras = (%extras, @ARGV);
|
||||
|
||||
$raw = build_raw($tx_type, $account, %extras);
|
||||
printf "Raw: %s\n", $raw if $opt{d} || (!$opt{s} && !$opt{p});
|
||||
|
||||
# Some transaction types require proof-of-work, e.g. CHAT
|
||||
if (exists $tx_info->{pow_url}) {
|
||||
$raw = api($tx_info->{pow_url}, $raw);
|
||||
printf "Raw with PoW: %s\n", $raw if $opt{d};
|
||||
}
|
||||
} else {
|
||||
$raw = shift @ARGV;
|
||||
$opt{s}++;
|
||||
}
|
||||
|
||||
if ($opt{s}) {
|
||||
my $signed = sign($account->{private}, $raw);
|
||||
printf "Signed: %s\n", $signed if $opt{d} || $tx_type eq 'sign';
|
||||
|
||||
if ($opt{p}) {
|
||||
my $processed = process($signed);
|
||||
printf "Processed: %s\n", $processed if $opt{d};
|
||||
}
|
||||
|
||||
my $hex = api('utils/frombase58', $signed);
|
||||
# sig is last 64 bytes / 128 chars
|
||||
my $sighex = substr($hex, -128);
|
||||
|
||||
my $sig58 = api('utils/tobase58/{hex}', '', '{hex}', $sighex);
|
||||
printf "Signature: %s\n", $sig58;
|
||||
}
|
||||
|
||||
sub account {
|
||||
my ($creator) = @_;
|
||||
|
||||
my $account = { private => $creator };
|
||||
$account->{public} = api('utils/publickey', $creator);
|
||||
$account->{address} = api('addresses/convert/{publickey}', '', '{publickey}', $account->{public});
|
||||
|
||||
return $account;
|
||||
}
|
||||
|
||||
sub build_raw {
|
||||
my ($type, $account, %extras) = @_;
|
||||
|
||||
my $tx_info = $TRANSACTION_TYPES{$type};
|
||||
die("unknown tx type: $type\n") unless defined $tx_info;
|
||||
|
||||
my $ref = exists $extras{reference} ? $extras{reference} : lastref($account->{address});
|
||||
|
||||
my %json = (
|
||||
timestamp => time * 1000,
|
||||
reference => $ref,
|
||||
fee => $DEFAULT_FEE,
|
||||
);
|
||||
|
||||
$json{$tx_info->{key_name}} = $account->{public} if exists $tx_info->{key_name};
|
||||
|
||||
foreach my $required (@{$tx_info->{required}}) {
|
||||
die("missing tx field: $required\n") unless exists $extras{$required};
|
||||
}
|
||||
|
||||
while (my ($key, $value) = each %extras) {
|
||||
$json{$key} = $value;
|
||||
}
|
||||
|
||||
if (exists $tx_info->{remove}) {
|
||||
foreach my $key (@{$tx_info->{remove}}) {
|
||||
delete $json{$key};
|
||||
}
|
||||
}
|
||||
|
||||
my $json = "{\n";
|
||||
while (my ($key, $value) = each %json) {
|
||||
if (ref($value) eq 'ARRAY') {
|
||||
$json .= "\t\"$key\": [],\n";
|
||||
} else {
|
||||
$json .= "\t\"$key\": \"$value\",\n";
|
||||
}
|
||||
}
|
||||
# remove final comma
|
||||
substr($json, -2, 1) = '';
|
||||
$json .= "}\n";
|
||||
|
||||
printf "%s:\n%s\n", $type, $json if $opt{d};
|
||||
|
||||
my $raw = api($tx_info->{url}, $json);
|
||||
return $raw;
|
||||
}
|
||||
|
||||
sub sign {
|
||||
my ($private, $raw) = @_;
|
||||
|
||||
my $json = <<" __JSON__";
|
||||
{
|
||||
"privateKey": "$private",
|
||||
"transactionBytes": "$raw"
|
||||
}
|
||||
__JSON__
|
||||
|
||||
return api('transactions/sign', $json);
|
||||
}
|
||||
|
||||
sub process {
|
||||
my ($signed) = @_;
|
||||
|
||||
return api('transactions/process', $signed);
|
||||
}
|
||||
|
||||
sub lastref {
|
||||
my ($address) = @_;
|
||||
|
||||
return api('addresses/lastreference/{address}', '', '{address}', $address)
|
||||
}
|
||||
|
||||
sub api {
|
||||
my ($endpoint, $postdata, @args) = @_;
|
||||
|
||||
my $url = $endpoint;
|
||||
my $method = 'GET';
|
||||
|
||||
for (my $i = 0; $i < @args; $i += 2) {
|
||||
my $placemarker = $args[$i];
|
||||
my $value = $args[$i + 1];
|
||||
|
||||
$url =~ s/$placemarker/$value/g;
|
||||
}
|
||||
|
||||
my $curl = "curl --silent --output - --url '$BASE_URL/$url'";
|
||||
if (defined $postdata && $postdata ne '') {
|
||||
$postdata =~ tr|\n| |s;
|
||||
$curl .= " --header 'Content-Type: application/json' --data-binary '$postdata'";
|
||||
$method = 'POST';
|
||||
}
|
||||
my $response = `$curl 2>/dev/null`;
|
||||
chomp $response;
|
||||
|
||||
if ($response eq '' || substr($response, 0, 6) eq '<html>' || $response =~ m/(^\{|,)"error":(\d+)[,}]/) {
|
||||
die("API call '$method $BASE_URL/$endpoint' failed:\n$response\n");
|
||||
}
|
||||
|
||||
return $response;
|
||||
}
|
Reference in New Issue
Block a user