forked from Qortal/qortal
Compare commits
184 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
2df045396d | ||
|
|
6c182a3567 | ||
|
|
340d6dfc8d | ||
|
|
eb27b0d3e2 | ||
|
|
7377893050 | ||
|
|
fb2c2b1d09 | ||
|
|
6f2dd6c8d0 | ||
|
|
4cc0e7845f | ||
|
|
275146fb55 | ||
|
|
d81729d9f7 | ||
|
|
e74a249388 | ||
|
|
d8c5e557d8 | ||
|
|
984e8b5227 | ||
|
|
469bf2a63e | ||
|
|
e05fcd6655 | ||
|
|
3a7751910e | ||
|
|
3c139f3e53 | ||
|
|
2c14a12464 | ||
|
|
faa6405d5f | ||
|
|
e2e4555009 | ||
|
|
448e984995 | ||
|
|
ec1954bae1 | ||
|
|
66276a6f65 | ||
|
|
c00ab2f87c | ||
|
|
99f3ab9921 | ||
|
|
75b15c6639 | ||
|
|
e5e60a5032 | ||
|
|
b9d2bbb78b | ||
|
|
3d79408574 | ||
|
|
67b184acc9 | ||
|
|
11040ae60a | ||
|
|
a338202ded | ||
|
|
e0398490ae | ||
|
|
847093edac | ||
|
|
758a42db36 | ||
|
|
c2b253df55 | ||
|
|
cc3adc6720 | ||
|
|
d77acd9eb9 | ||
|
|
5ad2bc1940 | ||
|
|
ea4c51026b | ||
|
|
d0b4a1f12f | ||
|
|
50b912e229 | ||
|
|
5ffddd0169 | ||
|
|
b5512dfa91 | ||
|
|
2493d5f7a8 | ||
|
|
bef1828404 | ||
|
|
0ae232b8ba | ||
|
|
cdf0795881 | ||
|
|
31e85226f4 | ||
|
|
8df3c68df9 | ||
|
|
ca0deb2bf6 | ||
|
|
6eea7c2aa1 | ||
|
|
9aabf93523 | ||
|
|
322e2cdc41 | ||
|
|
df395c77db | ||
|
|
274002c473 | ||
|
|
3d4fc38fcb | ||
|
|
d50f16b8a9 | ||
|
|
59de22883b | ||
|
|
db73afaf88 | ||
|
|
3afbd7aa51 | ||
|
|
0c32afa07f | ||
|
|
bd543a526b | ||
|
|
b262044a52 | ||
|
|
200a97184c | ||
|
|
0164bca2d7 | ||
|
|
5f4b66e5b0 | ||
|
|
9c48343581 | ||
|
|
219f82f562 | ||
|
|
51bfd49e25 | ||
|
|
7102f4a727 | ||
|
|
28991a926f | ||
|
|
74f89af841 | ||
|
|
b4284515e7 | ||
|
|
032c5d0d07 | ||
|
|
72100fe1d8 | ||
|
|
ed178e744d | ||
|
|
94f7079c2e | ||
|
|
f1638aa9d9 | ||
|
|
a7b9215ace | ||
|
|
956ad7bfa8 | ||
|
|
4baf442cb8 | ||
|
|
24eb7c6933 | ||
|
|
38a2af8cd5 | ||
|
|
7447ab20a9 | ||
|
|
197c742ce7 | ||
|
|
f6ed3388a4 | ||
|
|
c61690f3e6 | ||
|
|
9a94873d0e | ||
|
|
5c8bda37d1 | ||
|
|
fbb73ee88e | ||
|
|
fa08041696 | ||
|
|
f01a34a461 | ||
|
|
c0242fe78b | ||
|
|
ef790a8cb1 | ||
|
|
cea0cee9a8 | ||
|
|
d9f784ed2b | ||
|
|
f29ae656b9 | ||
|
|
a9852e5305 | ||
|
|
32470fa641 | ||
|
|
0d1c08bf96 | ||
|
|
026c904ce4 | ||
|
|
59ae070c83 | ||
|
|
2ab695f308 | ||
|
|
f0ff77cd31 | ||
|
|
e241d9fa67 | ||
|
|
5e9b0cd03c | ||
|
|
a5c437913f | ||
|
|
3fa7da5115 | ||
|
|
6d8f41ab05 | ||
|
|
c7c419a3cd | ||
|
|
3094ec3c26 | ||
|
|
359a35931e | ||
|
|
9e0001c4f6 | ||
|
|
53112709fe | ||
|
|
d1bc500ab9 | ||
|
|
74b5401e84 | ||
|
|
d2559f36ce | ||
|
|
0cc9cd728e | ||
|
|
e5cf76f3e0 | ||
|
|
44e8b3e6e7 | ||
|
|
1bca152d9c | ||
|
|
4edc3ee121 | ||
|
|
e9f29767c8 | ||
|
|
e2916b130b | ||
|
|
538e117abd | ||
|
|
71e80bd02f | ||
|
|
800103225b | ||
|
|
cfb7a3cc4c | ||
|
|
3185cf23df | ||
|
|
3ac1b36549 | ||
|
|
55e99062ca | ||
|
|
edb56b74da | ||
|
|
233ace23de | ||
|
|
e1f3b9a7a3 | ||
|
|
6be88ac86e | ||
|
|
d03cca2e76 | ||
|
|
e86143426b | ||
|
|
a309f8de9e | ||
|
|
df15f81b9f | ||
|
|
6ab50e4dff | ||
|
|
476d9e4c95 | ||
|
|
9eaf31707a | ||
|
|
e0007269b9 | ||
|
|
0006911e0a | ||
|
|
e141e98ecc | ||
|
|
40531284dd | ||
|
|
9e2663b11b | ||
|
|
2602bb01e1 | ||
|
|
ac15dfe789 | ||
|
|
cd066cf357 | ||
|
|
bd521baade | ||
|
|
136188339d | ||
|
|
48de33fe24 | ||
|
|
df4798e2a1 | ||
|
|
edb842f0d1 | ||
|
|
b563fe567d | ||
|
|
b3dd0d89df | ||
|
|
360f6cd4f1 | ||
|
|
f1e4528581 | ||
|
|
1375372380 | ||
|
|
a7d0ad27b1 | ||
|
|
833a785996 | ||
|
|
94d18538d8 | ||
|
|
8baf42765e | ||
|
|
b93dca1818 | ||
|
|
98506a038b | ||
|
|
3eaeb927ec | ||
|
|
7ded8954c6 | ||
|
|
d2eb8b0c2b | ||
|
|
2ed2cc0fab | ||
|
|
87bb9090f5 | ||
|
|
8844cc0076 | ||
|
|
2c4bad6455 | ||
|
|
5c0134c16a | ||
|
|
369a45f5c0 | ||
|
|
d58b7c1f53 | ||
|
|
5011a2be22 | ||
|
|
33010f82d8 | ||
|
|
8dbd8c4e65 | ||
|
|
c2a3c1271c | ||
|
|
1e9a7ac87d | ||
|
|
e25d24964c | ||
|
|
d90d84ab06 |
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 |
BIN
lib/org/ciyam/AT/1.3.4/AT-1.3.4.jar
Normal file
BIN
lib/org/ciyam/AT/1.3.4/AT-1.3.4.jar
Normal file
Binary file not shown.
@@ -3,7 +3,7 @@
|
||||
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance">
|
||||
<modelVersion>4.0.0</modelVersion>
|
||||
<groupId>org.ciyam</groupId>
|
||||
<artifactId>at</artifactId>
|
||||
<version>1.0</version>
|
||||
<artifactId>AT</artifactId>
|
||||
<version>1.3.4</version>
|
||||
<description>POM was created from install:install-file</description>
|
||||
</project>
|
||||
@@ -1,12 +1,12 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<metadata>
|
||||
<groupId>org.ciyam</groupId>
|
||||
<artifactId>at</artifactId>
|
||||
<artifactId>AT</artifactId>
|
||||
<versioning>
|
||||
<release>1.0</release>
|
||||
<release>1.3.4</release>
|
||||
<versions>
|
||||
<version>1.0</version>
|
||||
<version>1.3.4</version>
|
||||
</versions>
|
||||
<lastUpdated>20181105100741</lastUpdated>
|
||||
<lastUpdated>20200414162728</lastUpdated>
|
||||
</versioning>
|
||||
</metadata>
|
||||
Binary file not shown.
@@ -1,11 +1,15 @@
|
||||
rootLogger.level = info
|
||||
# On Windows, uncomment this:
|
||||
# property.dirname = ${sys:user.home}\\AppData\\Roaming\\qortal\\
|
||||
# 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
|
||||
|
||||
29
pom.xml
29
pom.xml
@@ -3,12 +3,13 @@
|
||||
<modelVersion>4.0.0</modelVersion>
|
||||
<groupId>org.qortal</groupId>
|
||||
<artifactId>qortal</artifactId>
|
||||
<version>1.0.6</version>
|
||||
<version>1.2.3</version>
|
||||
<packaging>jar</packaging>
|
||||
<properties>
|
||||
<bitcoin.version>0.15.4</bitcoin.version>
|
||||
<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>
|
||||
<commons-net.version>3.6</commons-net.version>
|
||||
<commons-text.version>1.8</commons-text.version>
|
||||
<dagger.version>1.2.2</dagger.version>
|
||||
@@ -16,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>
|
||||
@@ -405,14 +406,14 @@
|
||||
<!-- CIYAM AT (automated transactions) -->
|
||||
<dependency>
|
||||
<groupId>org.ciyam</groupId>
|
||||
<artifactId>at</artifactId>
|
||||
<version>1.0</version>
|
||||
<artifactId>AT</artifactId>
|
||||
<version>${ciyam-at.version}</version>
|
||||
</dependency>
|
||||
<!-- Bitcoin support -->
|
||||
<dependency>
|
||||
<groupId>org.bitcoinj</groupId>
|
||||
<artifactId>bitcoinj-core</artifactId>
|
||||
<version>${bitcoin.version}</version>
|
||||
<version>${bitcoinj.version}</version>
|
||||
</dependency>
|
||||
<!-- Utilities -->
|
||||
<dependency>
|
||||
@@ -450,6 +451,10 @@
|
||||
<groupId>org.asynchttpclient</groupId>
|
||||
<artifactId>async-http-client</artifactId>
|
||||
</exclusion>
|
||||
<exclusion>
|
||||
<groupId>io.druid</groupId>
|
||||
<artifactId>java-util</artifactId>
|
||||
</exclusion>
|
||||
</exclusions>
|
||||
</dependency>
|
||||
<!-- For NTP -->
|
||||
@@ -503,6 +508,12 @@
|
||||
<artifactId>mail</artifactId>
|
||||
<version>1.5.0-b01</version>
|
||||
</dependency>
|
||||
<!-- Unicode homoglyph utilities -->
|
||||
<dependency>
|
||||
<groupId>net.codebox</groupId>
|
||||
<artifactId>homoglyph</artifactId>
|
||||
<version>1.2.0</version>
|
||||
</dependency>
|
||||
<!-- Jetty -->
|
||||
<dependency>
|
||||
<groupId>org.eclipse.jetty</groupId>
|
||||
@@ -531,6 +542,12 @@
|
||||
<artifactId>jetty-client</artifactId>
|
||||
<version>${jetty.version}</version>
|
||||
</dependency>
|
||||
<!-- Websocket support -->
|
||||
<dependency>
|
||||
<groupId>org.eclipse.jetty.websocket</groupId>
|
||||
<artifactId>javax-websocket-server-impl</artifactId>
|
||||
<version>${jetty.version}</version>
|
||||
</dependency>
|
||||
<!-- Jersey -->
|
||||
<dependency>
|
||||
<groupId>org.glassfish.jersey.core</groupId>
|
||||
|
||||
@@ -1,7 +1,9 @@
|
||||
package org.qortal.account;
|
||||
|
||||
import java.math.BigDecimal;
|
||||
import java.util.List;
|
||||
import static org.qortal.utils.Amounts.prettyAmount;
|
||||
|
||||
import javax.xml.bind.annotation.XmlAccessType;
|
||||
import javax.xml.bind.annotation.XmlAccessorType;
|
||||
|
||||
import org.apache.logging.log4j.LogManager;
|
||||
import org.apache.logging.log4j.Logger;
|
||||
@@ -9,12 +11,11 @@ import org.qortal.block.BlockChain;
|
||||
import org.qortal.data.account.AccountBalanceData;
|
||||
import org.qortal.data.account.AccountData;
|
||||
import org.qortal.data.account.RewardShareData;
|
||||
import org.qortal.data.transaction.TransactionData;
|
||||
import org.qortal.repository.DataException;
|
||||
import org.qortal.repository.Repository;
|
||||
import org.qortal.transaction.Transaction;
|
||||
import org.qortal.utils.Base58;
|
||||
|
||||
@XmlAccessorType(XmlAccessType.NONE) // Stops JAX-RS errors when unmarshalling blockchain config
|
||||
public class Account {
|
||||
|
||||
private static final Logger LOGGER = LogManager.getLogger(Account.class);
|
||||
@@ -51,39 +52,52 @@ public class Account {
|
||||
return new AccountData(this.address);
|
||||
}
|
||||
|
||||
public void ensureAccount() throws DataException {
|
||||
this.repository.getAccountRepository().ensureAccount(this.buildAccountData());
|
||||
}
|
||||
|
||||
// Balance manipulations - assetId is 0 for QORT
|
||||
|
||||
public BigDecimal getBalance(long assetId) throws DataException {
|
||||
public long getConfirmedBalance(long assetId) throws DataException {
|
||||
AccountBalanceData accountBalanceData = this.repository.getAccountRepository().getBalance(this.address, assetId);
|
||||
if (accountBalanceData == null)
|
||||
return BigDecimal.ZERO.setScale(8);
|
||||
return 0;
|
||||
|
||||
return accountBalanceData.getBalance();
|
||||
}
|
||||
|
||||
public BigDecimal getConfirmedBalance(long assetId) throws DataException {
|
||||
AccountBalanceData accountBalanceData = this.repository.getAccountRepository().getBalance(this.address, assetId);
|
||||
if (accountBalanceData == null)
|
||||
return BigDecimal.ZERO.setScale(8);
|
||||
|
||||
return accountBalanceData.getBalance();
|
||||
}
|
||||
|
||||
public void setConfirmedBalance(long assetId, BigDecimal balance) throws DataException {
|
||||
public void setConfirmedBalance(long assetId, long balance) throws DataException {
|
||||
// Safety feature!
|
||||
if (balance.compareTo(BigDecimal.ZERO) < 0) {
|
||||
String message = String.format("Refusing to set negative balance %s [assetId %d] for %s", balance.toPlainString(), assetId, this.address);
|
||||
if (balance < 0) {
|
||||
String message = String.format("Refusing to set negative balance %s [assetId %d] for %s", prettyAmount(balance), assetId, this.address);
|
||||
LOGGER.error(message);
|
||||
throw new DataException(message);
|
||||
}
|
||||
|
||||
// Delete account balance record instead of setting balance to zero
|
||||
if (balance == 0) {
|
||||
this.repository.getAccountRepository().delete(this.address, assetId);
|
||||
return;
|
||||
}
|
||||
|
||||
// Can't have a balance without an account - make sure it exists!
|
||||
this.repository.getAccountRepository().ensureAccount(this.buildAccountData());
|
||||
this.ensureAccount();
|
||||
|
||||
AccountBalanceData accountBalanceData = new AccountBalanceData(this.address, assetId, balance);
|
||||
this.repository.getAccountRepository().save(accountBalanceData);
|
||||
|
||||
LOGGER.trace(() -> String.format("%s balance now %s [assetId %s]", this.address, balance.toPlainString(), assetId));
|
||||
LOGGER.trace(() -> String.format("%s balance now %s [assetId %s]", this.address, prettyAmount(balance), assetId));
|
||||
}
|
||||
|
||||
// Convenience method
|
||||
public void modifyAssetBalance(long assetId, long deltaBalance) throws DataException {
|
||||
this.repository.getAccountRepository().modifyAssetBalance(this.getAddress(), assetId, deltaBalance);
|
||||
|
||||
LOGGER.trace(() -> String.format("%s balance %s by %s [assetId %s]",
|
||||
this.address,
|
||||
(deltaBalance >= 0 ? "increased" : "decreased"),
|
||||
prettyAmount(Math.abs(deltaBalance)),
|
||||
assetId));
|
||||
}
|
||||
|
||||
public void deleteBalance(long assetId) throws DataException {
|
||||
@@ -99,38 +113,11 @@ public class Account {
|
||||
* @throws DataException
|
||||
*/
|
||||
public byte[] getLastReference() throws DataException {
|
||||
byte[] reference = this.repository.getAccountRepository().getLastReference(this.address);
|
||||
byte[] reference = AccountRefCache.getLastReference(this.repository, this.address);
|
||||
LOGGER.trace(() -> String.format("Last reference for %s is %s", this.address, reference == null ? "null" : Base58.encode(reference)));
|
||||
return reference;
|
||||
}
|
||||
|
||||
/**
|
||||
* Fetch last reference for account, considering unconfirmed transactions only, or return null.
|
||||
* <p>
|
||||
* NOTE: calls Transaction.getUnconfirmedTransactions which discards uncommitted
|
||||
* repository changes.
|
||||
*
|
||||
* @return byte[] reference, or null if no unconfirmed transactions for this account.
|
||||
* @throws DataException
|
||||
*/
|
||||
public byte[] getUnconfirmedLastReference() throws DataException {
|
||||
// Newest unconfirmed transaction takes priority
|
||||
List<TransactionData> unconfirmedTransactions = Transaction.getUnconfirmedTransactions(repository);
|
||||
|
||||
byte[] reference = null;
|
||||
|
||||
for (TransactionData transactionData : unconfirmedTransactions) {
|
||||
String unconfirmedTransactionAddress = PublicKeyAccount.getAddress(transactionData.getCreatorPublicKey());
|
||||
|
||||
if (unconfirmedTransactionAddress.equals(this.address))
|
||||
reference = transactionData.getSignature();
|
||||
}
|
||||
|
||||
final byte[] loggingReference = reference;
|
||||
LOGGER.trace(() -> String.format("Last unconfirmed reference for %s is %s", this.address, loggingReference == null ? "null" : Base58.encode(loggingReference)));
|
||||
return reference;
|
||||
}
|
||||
|
||||
/**
|
||||
* Set last reference for account.
|
||||
*
|
||||
@@ -143,7 +130,7 @@ public class Account {
|
||||
|
||||
AccountData accountData = this.buildAccountData();
|
||||
accountData.setReference(reference);
|
||||
this.repository.getAccountRepository().setLastReference(accountData);
|
||||
AccountRefCache.setLastReference(this.repository, accountData);
|
||||
}
|
||||
|
||||
// Default groupID manipulations
|
||||
@@ -279,11 +266,7 @@ public class Account {
|
||||
if (Account.isFounder(accountData.getFlags()))
|
||||
return BlockChain.getInstance().getFounderEffectiveMintingLevel();
|
||||
|
||||
Integer level = accountData.getLevel();
|
||||
if (level == null)
|
||||
return 0;
|
||||
|
||||
return level;
|
||||
return accountData.getLevel();
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
217
src/main/java/org/qortal/account/AccountRefCache.java
Normal file
217
src/main/java/org/qortal/account/AccountRefCache.java
Normal file
@@ -0,0 +1,217 @@
|
||||
package org.qortal.account;
|
||||
|
||||
import java.util.HashMap;
|
||||
import java.util.Map;
|
||||
import java.util.Map.Entry;
|
||||
import java.util.function.BinaryOperator;
|
||||
|
||||
import org.qortal.data.account.AccountData;
|
||||
import org.qortal.repository.DataException;
|
||||
import org.qortal.repository.Repository;
|
||||
import org.qortal.utils.Pair;
|
||||
|
||||
/**
|
||||
* Account lastReference caching
|
||||
* <p>
|
||||
* When checking an account's lastReference, the value returned should be the
|
||||
* most recent value set after processing the most recent block.
|
||||
* <p>
|
||||
* However, when processing a batch of transactions, e.g. during block processing or validation,
|
||||
* each transaction needs to check, and maybe update, multiple accounts' lastReference values.
|
||||
* <p>
|
||||
* Because the intermediate updates would affect future checks, we set up a cache of that
|
||||
* maintains a consistent value for fetching lastReference, but also tracks the latest new
|
||||
* value, without the overhead of repository calls.
|
||||
* <p>
|
||||
* Thus, when batch transaction processing is finished, only the latest new lastReference values
|
||||
* can be committed to the repository, via {@link AccountRefCache#commit()}.
|
||||
* <p>
|
||||
* Getting and setting lastReferences values are done the usual way via
|
||||
* {@link Account#getLastReference()} and {@link Account#setLastReference(byte[])} which call
|
||||
* package-visibility methods in <tt>AccountRefCache</tt>.
|
||||
* <p>
|
||||
* If {@link Account#getLastReference()} or {@link Account#setLastReference(byte[])} are called
|
||||
* outside of caching then lastReference values are fetched/set directly from/to the repository.
|
||||
* <p>
|
||||
* <tt>AccountRefCache</tt> implements <tt>AutoCloseable</tt> for (typical) use in a try-with-resources block.
|
||||
*
|
||||
* @see Account#getLastReference()
|
||||
* @see Account#setLastReference(byte[])
|
||||
* @see org.qortal.block.Block#process()
|
||||
*/
|
||||
public class AccountRefCache implements AutoCloseable {
|
||||
|
||||
private static final Map<Repository, RefCache> CACHE = new HashMap<>();
|
||||
|
||||
private static class RefCache {
|
||||
private final Map<String, byte[]> getLastReferenceValues = new HashMap<>();
|
||||
private final Map<String, Pair<byte[], byte[]>> setLastReferenceValues = new HashMap<>();
|
||||
|
||||
/**
|
||||
* Function for merging publicKey from new data with old publicKey from map.
|
||||
* <p>
|
||||
* Last reference is <tt>A</tt> element in pair.<br>
|
||||
* Public key is <tt>B</tt> element in pair.
|
||||
*/
|
||||
private static final BinaryOperator<Pair<byte[], byte[]>> mergePublicKey = (oldPair, newPair) -> {
|
||||
// If passed new pair contains non-null publicKey, then we use that one in preference.
|
||||
if (newPair.getB() == null)
|
||||
// Otherwise, inherit publicKey from old map value.
|
||||
newPair.setB(oldPair.getB());
|
||||
|
||||
// We always use new lastReference from new pair.
|
||||
return newPair;
|
||||
};
|
||||
|
||||
|
||||
public byte[] getLastReference(Repository repository, String address) throws DataException {
|
||||
synchronized (this.getLastReferenceValues) {
|
||||
byte[] lastReference = getLastReferenceValues.get(address);
|
||||
if (lastReference != null)
|
||||
// address is present in map, lastReference not null
|
||||
return lastReference;
|
||||
|
||||
// address is present in map, just lastReference is null
|
||||
if (getLastReferenceValues.containsKey(address))
|
||||
return null;
|
||||
|
||||
lastReference = repository.getAccountRepository().getLastReference(address);
|
||||
this.getLastReferenceValues.put(address, lastReference);
|
||||
return lastReference;
|
||||
}
|
||||
}
|
||||
|
||||
public void setLastReference(AccountData accountData) {
|
||||
// We're only interested in lastReference and publicKey
|
||||
Pair<byte[], byte[]> newPair = new Pair<>(accountData.getReference(), accountData.getPublicKey());
|
||||
|
||||
synchronized (this.setLastReferenceValues) {
|
||||
setLastReferenceValues.merge(accountData.getAddress(), newPair, mergePublicKey);
|
||||
}
|
||||
}
|
||||
|
||||
Map<String, Pair<byte[], byte[]>> getNewLastReferences() {
|
||||
return setLastReferenceValues;
|
||||
}
|
||||
}
|
||||
|
||||
private Repository repository;
|
||||
|
||||
/**
|
||||
* Constructs a new account reference cache, unique to passed <tt>repository</tt> handle.
|
||||
*
|
||||
* @param repository
|
||||
* @throws IllegalStateException if a cache already exists for <tt>repository</tt>
|
||||
*/
|
||||
public AccountRefCache(Repository repository) {
|
||||
RefCache refCache = new RefCache();
|
||||
|
||||
synchronized (CACHE) {
|
||||
if (CACHE.putIfAbsent(repository, refCache) != null)
|
||||
throw new IllegalStateException("Account reference cache entry already exists");
|
||||
}
|
||||
|
||||
this.repository = repository;
|
||||
}
|
||||
|
||||
/**
|
||||
* Save all cached setLastReference account-reference values into repository.
|
||||
* <p>
|
||||
* Closes cache to prevent any future setLastReference() attempts post-commit.
|
||||
*
|
||||
* @throws DataException
|
||||
*/
|
||||
public void commit() throws DataException {
|
||||
RefCache refCache;
|
||||
|
||||
// Also duplicated in close(), this prevents future setLastReference() attempts post-commit.
|
||||
synchronized (CACHE) {
|
||||
refCache = CACHE.remove(this.repository);
|
||||
}
|
||||
|
||||
if (refCache == null)
|
||||
throw new IllegalStateException("Tried to commit non-existent account reference cache");
|
||||
|
||||
Map<String, Pair<byte[], byte[]>> newLastReferenceValues = refCache.getNewLastReferences();
|
||||
|
||||
for (Entry<String, Pair<byte[], byte[]>> entry : newLastReferenceValues.entrySet()) {
|
||||
AccountData accountData = new AccountData(entry.getKey());
|
||||
|
||||
accountData.setReference(entry.getValue().getA());
|
||||
|
||||
if (entry.getValue().getB() != null)
|
||||
accountData.setPublicKey(entry.getValue().getB());
|
||||
|
||||
this.repository.getAccountRepository().setLastReference(accountData);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void close() {
|
||||
synchronized (CACHE) {
|
||||
CACHE.remove(this.repository);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns lastReference value for account.
|
||||
* <p>
|
||||
* If cache is not in effect for passed <tt>repository</tt> handle,
|
||||
* then this method fetches lastReference directly from repository.
|
||||
* <p>
|
||||
* If cache <i>is</i> in effect, then this method returns cached
|
||||
* lastReference, which is <b>not</b> affected by calls to
|
||||
* <tt>setLastReference</tt>.
|
||||
* <p>
|
||||
* Typically called by corresponding method in Account class.
|
||||
*
|
||||
* @param repository
|
||||
* @param address account's address
|
||||
* @return account's lastReference, or null if account unknown, or lastReference not set
|
||||
* @throws DataException
|
||||
*/
|
||||
/*package*/ static byte[] getLastReference(Repository repository, String address) throws DataException {
|
||||
RefCache refCache;
|
||||
|
||||
synchronized (CACHE) {
|
||||
refCache = CACHE.get(repository);
|
||||
}
|
||||
|
||||
if (refCache == null)
|
||||
return repository.getAccountRepository().getLastReference(address);
|
||||
|
||||
return refCache.getLastReference(repository, address);
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets lastReference value for account.
|
||||
* <p>
|
||||
* If cache is not in effect for passed <tt>repository</tt> handle,
|
||||
* then this method sets lastReference directly in repository.
|
||||
* <p>
|
||||
* If cache <i>is</i> in effect, then this method caches the new
|
||||
* lastReference, which is <b>not</b> returned by calls to
|
||||
* <tt>getLastReference</tt>.
|
||||
* <p>
|
||||
* Typically called by corresponding method in Account class.
|
||||
*
|
||||
* @param repository
|
||||
* @param accountData
|
||||
* @throws DataException
|
||||
*/
|
||||
/*package*/ static void setLastReference(Repository repository, AccountData accountData) throws DataException {
|
||||
RefCache refCache;
|
||||
|
||||
synchronized (CACHE) {
|
||||
refCache = CACHE.get(repository);
|
||||
}
|
||||
|
||||
if (refCache == null) {
|
||||
repository.getAccountRepository().setLastReference(accountData);
|
||||
return;
|
||||
}
|
||||
|
||||
refCache.setLastReference(accountData);
|
||||
}
|
||||
|
||||
}
|
||||
@@ -1,13 +0,0 @@
|
||||
package org.qortal.account;
|
||||
|
||||
import org.qortal.repository.Repository;
|
||||
|
||||
public final class GenesisAccount extends PublicKeyAccount {
|
||||
|
||||
public static final byte[] PUBLIC_KEY = new byte[] { 1, 1, 1, 1, 1, 1, 1, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0 };
|
||||
|
||||
public GenesisAccount(Repository repository) {
|
||||
super(repository, PUBLIC_KEY);
|
||||
}
|
||||
|
||||
}
|
||||
24
src/main/java/org/qortal/account/NullAccount.java
Normal file
24
src/main/java/org/qortal/account/NullAccount.java
Normal file
@@ -0,0 +1,24 @@
|
||||
package org.qortal.account;
|
||||
|
||||
import org.qortal.crypto.Crypto;
|
||||
import org.qortal.repository.Repository;
|
||||
|
||||
public final class NullAccount extends PublicKeyAccount {
|
||||
|
||||
public static final byte[] PUBLIC_KEY = new byte[32];
|
||||
public static final String ADDRESS = Crypto.toAddress(PUBLIC_KEY);
|
||||
|
||||
public NullAccount(Repository repository) {
|
||||
super(repository, PUBLIC_KEY, ADDRESS);
|
||||
}
|
||||
|
||||
protected NullAccount() {
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean verify(byte[] signature, byte[] message) {
|
||||
// Can't sign, hence can't verify.
|
||||
return false;
|
||||
}
|
||||
|
||||
}
|
||||
@@ -2,18 +2,11 @@ package org.qortal.account;
|
||||
|
||||
import org.bouncycastle.crypto.params.Ed25519PrivateKeyParameters;
|
||||
import org.bouncycastle.crypto.params.Ed25519PublicKeyParameters;
|
||||
import org.bouncycastle.crypto.params.X25519PrivateKeyParameters;
|
||||
import org.bouncycastle.crypto.params.X25519PublicKeyParameters;
|
||||
import org.bouncycastle.math.ec.rfc8032.Ed25519;
|
||||
import org.qortal.crypto.BouncyCastle25519;
|
||||
import org.qortal.crypto.Crypto;
|
||||
import org.qortal.repository.Repository;
|
||||
|
||||
public class PrivateKeyAccount extends PublicKeyAccount {
|
||||
|
||||
private static final int SIGNATURE_LENGTH = 64;
|
||||
private static final int SHARED_SECRET_LENGTH = 32;
|
||||
|
||||
private final byte[] privateKey;
|
||||
private final Ed25519PrivateKeyParameters edPrivateKeyParams;
|
||||
|
||||
@@ -49,24 +42,11 @@ public class PrivateKeyAccount extends PublicKeyAccount {
|
||||
}
|
||||
|
||||
public byte[] sign(byte[] message) {
|
||||
byte[] signature = new byte[SIGNATURE_LENGTH];
|
||||
|
||||
this.edPrivateKeyParams.sign(Ed25519.Algorithm.Ed25519, edPublicKeyParams, null, message, 0, message.length, signature, 0);
|
||||
|
||||
return signature;
|
||||
return Crypto.sign(this.edPrivateKeyParams, message);
|
||||
}
|
||||
|
||||
public byte[] getSharedSecret(byte[] publicKey) {
|
||||
byte[] x25519PrivateKey = BouncyCastle25519.toX25519PrivateKey(this.privateKey);
|
||||
X25519PrivateKeyParameters xPrivateKeyParams = new X25519PrivateKeyParameters(x25519PrivateKey, 0);
|
||||
|
||||
byte[] x25519PublicKey = BouncyCastle25519.toX25519PublicKey(publicKey);
|
||||
X25519PublicKeyParameters xPublicKeyParams = new X25519PublicKeyParameters(x25519PublicKey, 0);
|
||||
|
||||
byte[] sharedSecret = new byte[SHARED_SECRET_LENGTH];
|
||||
xPrivateKeyParams.generateSecret(xPublicKeyParams, sharedSecret, 0);
|
||||
|
||||
return sharedSecret;
|
||||
return Crypto.getSharedSecret(this.privateKey, publicKey);
|
||||
}
|
||||
|
||||
public byte[] getRewardSharePrivateKey(byte[] publicKey) {
|
||||
|
||||
@@ -1,7 +1,6 @@
|
||||
package org.qortal.account;
|
||||
|
||||
import org.bouncycastle.crypto.params.Ed25519PublicKeyParameters;
|
||||
import org.bouncycastle.math.ec.rfc8032.Ed25519;
|
||||
import org.qortal.crypto.Crypto;
|
||||
import org.qortal.data.account.AccountData;
|
||||
import org.qortal.repository.Repository;
|
||||
@@ -22,6 +21,18 @@ public class PublicKeyAccount extends Account {
|
||||
this.publicKey = edPublicKeyParams.getEncoded();
|
||||
}
|
||||
|
||||
protected PublicKeyAccount(Repository repository, byte[] publicKey, String address) {
|
||||
super(repository, address);
|
||||
|
||||
this.publicKey = publicKey;
|
||||
this.edPublicKeyParams = null;
|
||||
}
|
||||
|
||||
protected PublicKeyAccount() {
|
||||
this.publicKey = null;
|
||||
this.edPublicKeyParams = null;
|
||||
}
|
||||
|
||||
public byte[] getPublicKey() {
|
||||
return this.publicKey;
|
||||
}
|
||||
@@ -34,15 +45,7 @@ public class PublicKeyAccount extends Account {
|
||||
}
|
||||
|
||||
public boolean verify(byte[] signature, byte[] message) {
|
||||
return PublicKeyAccount.verify(this.publicKey, signature, message);
|
||||
}
|
||||
|
||||
public static boolean verify(byte[] publicKey, byte[] signature, byte[] message) {
|
||||
try {
|
||||
return Ed25519.verify(signature, 0, publicKey, 0, message, 0, message.length);
|
||||
} catch (Exception e) {
|
||||
return false;
|
||||
}
|
||||
return Crypto.verify(this.publicKey, signature, message);
|
||||
}
|
||||
|
||||
public static String getAddress(byte[] publicKey) {
|
||||
|
||||
27
src/main/java/org/qortal/api/AmountTypeAdapter.java
Normal file
27
src/main/java/org/qortal/api/AmountTypeAdapter.java
Normal file
@@ -0,0 +1,27 @@
|
||||
package org.qortal.api;
|
||||
|
||||
import java.math.BigDecimal;
|
||||
|
||||
import javax.xml.bind.annotation.adapters.XmlAdapter;
|
||||
|
||||
import org.qortal.utils.Amounts;
|
||||
|
||||
public class AmountTypeAdapter extends XmlAdapter<String, Long> {
|
||||
|
||||
@Override
|
||||
public Long unmarshal(String input) throws Exception {
|
||||
if (input == null)
|
||||
return null;
|
||||
|
||||
return new BigDecimal(input).setScale(8).unscaledValue().longValue();
|
||||
}
|
||||
|
||||
@Override
|
||||
public String marshal(Long output) throws Exception {
|
||||
if (output == null)
|
||||
return null;
|
||||
|
||||
return Amounts.prettyAmount(output);
|
||||
}
|
||||
|
||||
}
|
||||
@@ -5,6 +5,12 @@ 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.XmlRootElement;
|
||||
|
||||
@XmlAccessorType(XmlAccessType.NONE)
|
||||
@XmlRootElement
|
||||
public enum ApiError {
|
||||
// COMMON
|
||||
// UNKNOWN(0, 500),
|
||||
@@ -15,6 +21,7 @@ public enum ApiError {
|
||||
REPOSITORY_ISSUE(5, 500),
|
||||
NON_PRODUCTION(6, 403),
|
||||
BLOCKCHAIN_NEEDS_SYNC(7, 503),
|
||||
NO_TIME_SYNC(8, 503),
|
||||
|
||||
// VALIDATION
|
||||
INVALID_SIGNATURE(101, 400),
|
||||
@@ -117,7 +124,12 @@ public enum ApiError {
|
||||
// MESSAGESIZE_EXCEEDED(1004, 400),
|
||||
|
||||
// Groups
|
||||
GROUP_UNKNOWN(1101, 404);
|
||||
GROUP_UNKNOWN(1101, 404),
|
||||
|
||||
// Bitcoin
|
||||
BTC_NETWORK_ISSUE(1201, 500),
|
||||
BTC_BALANCE_ISSUE(1202, 422),
|
||||
BTC_TOO_SOON(1203, 422);
|
||||
|
||||
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();
|
||||
|
||||
|
||||
20
src/main/java/org/qortal/api/ApiErrorRoot.java
Normal file
20
src/main/java/org/qortal/api/ApiErrorRoot.java
Normal file
@@ -0,0 +1,20 @@
|
||||
package org.qortal.api;
|
||||
|
||||
import javax.xml.bind.annotation.XmlElement;
|
||||
import javax.xml.bind.annotation.adapters.XmlJavaTypeAdapter;
|
||||
|
||||
public class ApiErrorRoot {
|
||||
|
||||
private ApiError apiError;
|
||||
|
||||
@XmlJavaTypeAdapter(ApiErrorTypeAdapter.class)
|
||||
@XmlElement(name = "error")
|
||||
public ApiError getApiError() {
|
||||
return this.apiError;
|
||||
}
|
||||
|
||||
public void setApiError(ApiError apiError) {
|
||||
this.apiError = apiError;
|
||||
}
|
||||
|
||||
}
|
||||
32
src/main/java/org/qortal/api/ApiErrorTypeAdapter.java
Normal file
32
src/main/java/org/qortal/api/ApiErrorTypeAdapter.java
Normal file
@@ -0,0 +1,32 @@
|
||||
package org.qortal.api;
|
||||
|
||||
import javax.xml.bind.annotation.adapters.XmlAdapter;
|
||||
|
||||
public class ApiErrorTypeAdapter extends XmlAdapter<ApiErrorTypeAdapter.AdaptedApiError, ApiError> {
|
||||
|
||||
public static class AdaptedApiError {
|
||||
public int code;
|
||||
public String description;
|
||||
}
|
||||
|
||||
@Override
|
||||
public ApiError unmarshal(AdaptedApiError adaptedInput) throws Exception {
|
||||
if (adaptedInput == null)
|
||||
return null;
|
||||
|
||||
return ApiError.fromCode(adaptedInput.code);
|
||||
}
|
||||
|
||||
@Override
|
||||
public AdaptedApiError marshal(ApiError output) throws Exception {
|
||||
if (output == null)
|
||||
return null;
|
||||
|
||||
AdaptedApiError adaptedOutput = new AdaptedApiError();
|
||||
adaptedOutput.code = output.getCode();
|
||||
adaptedOutput.description = output.name();
|
||||
|
||||
return adaptedOutput;
|
||||
}
|
||||
|
||||
}
|
||||
@@ -2,15 +2,31 @@ package org.qortal.api;
|
||||
|
||||
import io.swagger.v3.jaxrs2.integration.resources.OpenApiResource;
|
||||
|
||||
import java.io.InputStream;
|
||||
import java.net.InetAddress;
|
||||
import java.net.InetSocketAddress;
|
||||
import java.nio.file.Files;
|
||||
import java.nio.file.Path;
|
||||
import java.nio.file.Paths;
|
||||
import java.security.KeyStore;
|
||||
import java.security.SecureRandom;
|
||||
|
||||
import javax.net.ssl.KeyManagerFactory;
|
||||
import javax.net.ssl.SSLContext;
|
||||
|
||||
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.RequestLog;
|
||||
import org.eclipse.jetty.server.RequestLogWriter;
|
||||
import org.eclipse.jetty.server.SecureRequestCustomizer;
|
||||
import org.eclipse.jetty.server.Server;
|
||||
import org.eclipse.jetty.server.ServerConnector;
|
||||
import org.eclipse.jetty.server.SslConnectionFactory;
|
||||
import org.eclipse.jetty.server.handler.ErrorHandler;
|
||||
import org.eclipse.jetty.server.handler.InetAccessHandler;
|
||||
import org.eclipse.jetty.servlet.DefaultServlet;
|
||||
@@ -18,10 +34,15 @@ import org.eclipse.jetty.servlet.FilterHolder;
|
||||
import org.eclipse.jetty.servlet.ServletContextHandler;
|
||||
import org.eclipse.jetty.servlet.ServletHolder;
|
||||
import org.eclipse.jetty.servlets.CrossOriginFilter;
|
||||
import org.eclipse.jetty.util.ssl.SslContextFactory;
|
||||
import org.glassfish.jersey.server.ResourceConfig;
|
||||
import org.glassfish.jersey.servlet.ServletContainer;
|
||||
import org.qortal.api.resource.AnnotationPostProcessor;
|
||||
import org.qortal.api.resource.ApiDefinition;
|
||||
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.settings.Settings;
|
||||
|
||||
public class ApiService {
|
||||
@@ -53,9 +74,57 @@ public class ApiService {
|
||||
public void start() {
|
||||
try {
|
||||
// Create API server
|
||||
InetAddress bindAddr = InetAddress.getByName(Settings.getInstance().getBindAddress());
|
||||
InetSocketAddress endpoint = new InetSocketAddress(bindAddr, Settings.getInstance().getApiPort());
|
||||
this.server = new Server(endpoint);
|
||||
|
||||
// SSL support if requested
|
||||
String keystorePathname = Settings.getInstance().getSslKeystorePathname();
|
||||
String keystorePassword = Settings.getInstance().getSslKeystorePassword();
|
||||
|
||||
if (keystorePathname != null && keystorePassword != null) {
|
||||
// SSL version
|
||||
if (!Files.isReadable(Path.of(keystorePathname)))
|
||||
throw new RuntimeException("Failed to start SSL API due to broken keystore");
|
||||
|
||||
// BouncyCastle-specific SSLContext build
|
||||
SSLContext sslContext = SSLContext.getInstance("TLS", "BCJSSE");
|
||||
KeyManagerFactory keyManagerFactory = KeyManagerFactory.getInstance("PKIX", "BCJSSE");
|
||||
|
||||
KeyStore keyStore = KeyStore.getInstance(KeyStore.getDefaultType(), "BC");
|
||||
|
||||
try (InputStream keystoreStream = Files.newInputStream(Paths.get(keystorePathname))) {
|
||||
keyStore.load(keystoreStream, keystorePassword.toCharArray());
|
||||
}
|
||||
|
||||
keyManagerFactory.init(keyStore, keystorePassword.toCharArray());
|
||||
sslContext.init(keyManagerFactory.getKeyManagers(), null, new SecureRandom());
|
||||
|
||||
SslContextFactory.Server sslContextFactory = new SslContextFactory.Server();
|
||||
sslContextFactory.setSslContext(sslContext);
|
||||
|
||||
this.server = new Server();
|
||||
|
||||
HttpConfiguration httpConfig = new HttpConfiguration();
|
||||
httpConfig.setSecureScheme("https");
|
||||
httpConfig.setSecurePort(Settings.getInstance().getApiPort());
|
||||
|
||||
SecureRequestCustomizer src = new SecureRequestCustomizer();
|
||||
httpConfig.addCustomizer(src);
|
||||
|
||||
HttpConnectionFactory httpConnectionFactory = new HttpConnectionFactory(httpConfig);
|
||||
SslConnectionFactory sslConnectionFactory = new SslConnectionFactory(sslContextFactory, HttpVersion.HTTP_1_1.asString());
|
||||
|
||||
ServerConnector portUnifiedConnector = new ServerConnector(this.server,
|
||||
new DetectorConnectionFactory(sslConnectionFactory),
|
||||
httpConnectionFactory);
|
||||
portUnifiedConnector.setHost(Settings.getInstance().getBindAddress());
|
||||
portUnifiedConnector.setPort(Settings.getInstance().getApiPort());
|
||||
|
||||
this.server.addConnector(portUnifiedConnector);
|
||||
} else {
|
||||
// Non-SSL
|
||||
InetAddress bindAddr = InetAddress.getByName(Settings.getInstance().getBindAddress());
|
||||
InetSocketAddress endpoint = new InetSocketAddress(bindAddr, Settings.getInstance().getApiPort());
|
||||
this.server = new Server(endpoint);
|
||||
}
|
||||
|
||||
// Error handler
|
||||
ErrorHandler errorHandler = new ApiErrorHandler();
|
||||
@@ -123,6 +192,11 @@ public class ApiService {
|
||||
rewriteHandler.addRule(new RedirectPatternRule("/api-documentation", "/api-documentation/")); // redirect to add trailing slash if missing
|
||||
}
|
||||
|
||||
context.addServlet(AdminStatusWebSocket.class, "/websockets/admin/status");
|
||||
context.addServlet(BlocksWebSocket.class, "/websockets/blocks");
|
||||
context.addServlet(ActiveChatsWebSocket.class, "/websockets/chat/active/*");
|
||||
context.addServlet(ChatMessagesWebSocket.class, "/websockets/chat/messages");
|
||||
|
||||
// Start server
|
||||
this.server.start();
|
||||
} catch (Exception e) {
|
||||
|
||||
@@ -0,0 +1,25 @@
|
||||
package org.qortal.api;
|
||||
|
||||
import java.math.BigDecimal;
|
||||
|
||||
import javax.xml.bind.annotation.adapters.XmlAdapter;
|
||||
|
||||
public class RewardSharePercentTypeAdapter extends XmlAdapter<String, Integer> {
|
||||
|
||||
@Override
|
||||
public Integer unmarshal(String input) throws Exception {
|
||||
if (input == null)
|
||||
return null;
|
||||
|
||||
return new BigDecimal(input).setScale(2).unscaledValue().intValue();
|
||||
}
|
||||
|
||||
@Override
|
||||
public String marshal(Integer output) throws Exception {
|
||||
if (output == null)
|
||||
return null;
|
||||
|
||||
return String.format("%d.%02d", output / 100, Math.abs(output % 100));
|
||||
}
|
||||
|
||||
}
|
||||
@@ -1,11 +1,10 @@
|
||||
package org.qortal.api.model;
|
||||
|
||||
import java.math.BigDecimal;
|
||||
|
||||
import javax.xml.bind.Marshaller;
|
||||
import javax.xml.bind.annotation.XmlAccessType;
|
||||
import javax.xml.bind.annotation.XmlAccessorType;
|
||||
import javax.xml.bind.annotation.XmlElement;
|
||||
import javax.xml.bind.annotation.adapters.XmlJavaTypeAdapter;
|
||||
|
||||
import org.qortal.data.asset.OrderData;
|
||||
|
||||
@@ -29,12 +28,14 @@ public class AggregatedOrder {
|
||||
}
|
||||
|
||||
@XmlElement(name = "price")
|
||||
public BigDecimal getPrice() {
|
||||
@XmlJavaTypeAdapter(value = org.qortal.api.AmountTypeAdapter.class)
|
||||
public long getPrice() {
|
||||
return this.orderData.getPrice();
|
||||
}
|
||||
|
||||
@XmlElement(name = "unfulfilled")
|
||||
public BigDecimal getUnfulfilled() {
|
||||
@XmlJavaTypeAdapter(value = org.qortal.api.AmountTypeAdapter.class)
|
||||
public long getUnfulfilled() {
|
||||
return this.orderData.getAmount();
|
||||
}
|
||||
|
||||
|
||||
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;
|
||||
}
|
||||
|
||||
}
|
||||
@@ -6,7 +6,7 @@ import javax.xml.bind.annotation.XmlAccessorType;
|
||||
import org.qortal.crypto.Crypto;
|
||||
|
||||
@XmlAccessorType(XmlAccessType.FIELD)
|
||||
public class BlockMinterSummary {
|
||||
public class BlockSignerSummary {
|
||||
|
||||
// Properties
|
||||
|
||||
@@ -20,19 +20,19 @@ public class BlockMinterSummary {
|
||||
|
||||
// Constructors
|
||||
|
||||
protected BlockMinterSummary() {
|
||||
protected BlockSignerSummary() {
|
||||
}
|
||||
|
||||
/** Constructs BlockMinterSummary in non-reward-share context. */
|
||||
public BlockMinterSummary(byte[] blockMinterPublicKey, int blockCount) {
|
||||
/** Constructs BlockSignerSummary in non-reward-share context. */
|
||||
public BlockSignerSummary(byte[] blockMinterPublicKey, int blockCount) {
|
||||
this.blockCount = blockCount;
|
||||
|
||||
this.mintingAccountPublicKey = blockMinterPublicKey;
|
||||
this.mintingAccount = Crypto.toAddress(this.mintingAccountPublicKey);
|
||||
}
|
||||
|
||||
/** Constructs BlockMinterSummary in reward-share context. */
|
||||
public BlockMinterSummary(byte[] rewardSharePublicKey, int blockCount, byte[] mintingAccountPublicKey, String minterAccount, String recipientAccount) {
|
||||
/** Constructs BlockSignerSummary in reward-share context. */
|
||||
public BlockSignerSummary(byte[] rewardSharePublicKey, int blockCount, byte[] mintingAccountPublicKey, String minterAccount, String recipientAccount) {
|
||||
this.rewardSharePublicKey = rewardSharePublicKey;
|
||||
this.blockCount = blockCount;
|
||||
|
||||
@@ -25,7 +25,8 @@ public class ConnectedPeer {
|
||||
|
||||
public String address;
|
||||
public String version;
|
||||
public Long buildTimestamp;
|
||||
|
||||
public String nodeId;
|
||||
|
||||
public Integer lastHeight;
|
||||
@Schema(example = "base58")
|
||||
@@ -45,10 +46,9 @@ public class ConnectedPeer {
|
||||
this.peersConnectedWhen = peer.getPeersConnectionTimestamp();
|
||||
|
||||
this.address = peerData.getAddress().toString();
|
||||
if (peer.getVersionMessage() != null) {
|
||||
this.version = peer.getVersionMessage().getVersionString();
|
||||
this.buildTimestamp = peer.getVersionMessage().getBuildTimestamp();
|
||||
}
|
||||
|
||||
this.version = peer.getPeersVersionString();
|
||||
this.nodeId = peer.getPeersNodeId();
|
||||
|
||||
PeerChainTipData peerChainTipData = peer.getChainTipData();
|
||||
if (peerChainTipData != null) {
|
||||
|
||||
@@ -0,0 +1,31 @@
|
||||
package org.qortal.api.model;
|
||||
|
||||
import java.math.BigDecimal;
|
||||
|
||||
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 CrossChainBitcoinP2SHStatus {
|
||||
|
||||
@Schema(description = "Bitcoin P2SH address", example = "3CdH27kTpV8dcFHVRYjQ8EEV5FJg9X8pSJ (mainnet), 2fMiRRXVsxhZeyfum9ifybZvaMHbQTmwdZw (testnet)")
|
||||
public String bitcoinP2shAddress;
|
||||
|
||||
@Schema(description = "Bitcoin P2SH balance")
|
||||
public BigDecimal bitcoinP2shBalance;
|
||||
|
||||
@Schema(description = "Can P2SH redeem yet?")
|
||||
public boolean canRedeem;
|
||||
|
||||
@Schema(description = "Can P2SH refund yet?")
|
||||
public boolean canRefund;
|
||||
|
||||
@Schema(description = "Secret extracted by P2SH redeemer")
|
||||
public byte[] secret;
|
||||
|
||||
public CrossChainBitcoinP2SHStatus() {
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,31 @@
|
||||
package org.qortal.api.model;
|
||||
|
||||
import java.math.BigDecimal;
|
||||
|
||||
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 CrossChainBitcoinRedeemRequest {
|
||||
|
||||
@Schema(description = "Bitcoin HASH160(public key) for refund", example = "2nGDBPPPFS1c9w1h33YwFk4KUJU2")
|
||||
public byte[] refundPublicKeyHash;
|
||||
|
||||
@Schema(description = "Bitcoin PRIVATE KEY for redeem", example = "cUvGNSnu14q6Hr1X7TESjYVTqBpFjj8GGLGjGdpJwD9NhSQKeYUk")
|
||||
public byte[] redeemPrivateKey;
|
||||
|
||||
@Schema(description = "Qortal AT address")
|
||||
public String atAddress;
|
||||
|
||||
@Schema(description = "Bitcoin miner fee", example = "0.00001000")
|
||||
public BigDecimal bitcoinMinerFee;
|
||||
|
||||
@Schema(description = "32-byte secret", example = "6gVbAXCVzJXAWwtAVGAfgAkkXpeXvPUwSciPmCfSfXJG")
|
||||
public byte[] secret;
|
||||
|
||||
public CrossChainBitcoinRedeemRequest() {
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,28 @@
|
||||
package org.qortal.api.model;
|
||||
|
||||
import java.math.BigDecimal;
|
||||
|
||||
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 CrossChainBitcoinRefundRequest {
|
||||
|
||||
@Schema(description = "Bitcoin PRIVATE KEY for refund", example = "cSP3zTb6bfm8GATtAcEJ8LqYtNQmzZ9jE2wQUVnZGiBzojDdrwKV")
|
||||
public byte[] refundPrivateKey;
|
||||
|
||||
@Schema(description = "Bitcoin HASH160(public key) for redeem", example = "2daMveGc5pdjRyFacbxBzMksCbyC")
|
||||
public byte[] redeemPublicKeyHash;
|
||||
|
||||
@Schema(description = "Qortal AT address")
|
||||
public String atAddress;
|
||||
|
||||
@Schema(description = "Bitcoin miner fee", example = "0.00001000")
|
||||
public BigDecimal bitcoinMinerFee;
|
||||
|
||||
public CrossChainBitcoinRefundRequest() {
|
||||
}
|
||||
|
||||
}
|
||||
@@ -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 CrossChainBitcoinTemplateRequest {
|
||||
|
||||
@Schema(description = "Bitcoin HASH160(public key) for refund", example = "2nGDBPPPFS1c9w1h33YwFk4KUJU2")
|
||||
public byte[] refundPublicKeyHash;
|
||||
|
||||
@Schema(description = "Bitcoin HASH160(public key) for redeem", example = "2daMveGc5pdjRyFacbxBzMksCbyC")
|
||||
public byte[] redeemPublicKeyHash;
|
||||
|
||||
@Schema(description = "Qortal AT address")
|
||||
public String atAddress;
|
||||
|
||||
public CrossChainBitcoinTemplateRequest() {
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,40 @@
|
||||
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 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;
|
||||
|
||||
@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 secret", example = "43vnftqkjxrhb5kJdkU1ZFQLEnWV")
|
||||
public byte[] secretHash;
|
||||
|
||||
@Schema(description = "Bitcoin P2SH BTC balance for release of secret", example = "0.00864200")
|
||||
@XmlJavaTypeAdapter(value = org.qortal.api.AmountTypeAdapter.class)
|
||||
public long bitcoinAmount;
|
||||
|
||||
@Schema(description = "Trade time window (minutes) from trade agreement to automatic refund", example = "10080")
|
||||
public Integer tradeTimeout;
|
||||
|
||||
public CrossChainBuildRequest() {
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,20 @@
|
||||
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 CrossChainCancelRequest {
|
||||
|
||||
@Schema(description = "AT creator's public key", example = "C6wuddsBV3HzRrXUtezE7P5MoRXp5m3mEDokRDGZB6ry")
|
||||
public byte[] creatorPublicKey;
|
||||
|
||||
@Schema(description = "Qortal AT address")
|
||||
public String atAddress;
|
||||
|
||||
public CrossChainCancelRequest() {
|
||||
}
|
||||
|
||||
}
|
||||
@@ -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 CrossChainSecretRequest {
|
||||
|
||||
@Schema(description = "Public key to match AT's 'recipient'", example = "C6wuddsBV3HzRrXUtezE7P5MoRXp5m3mEDokRDGZB6ry")
|
||||
public byte[] recipientPublicKey;
|
||||
|
||||
@Schema(description = "Qortal AT address")
|
||||
public String atAddress;
|
||||
|
||||
@Schema(description = "32-byte secret", example = "6gVbAXCVzJXAWwtAVGAfgAkkXpeXvPUwSciPmCfSfXJG")
|
||||
public byte[] secret;
|
||||
|
||||
public CrossChainSecretRequest() {
|
||||
}
|
||||
|
||||
}
|
||||
@@ -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 CrossChainTradeRequest {
|
||||
|
||||
@Schema(description = "AT creator's public key", example = "C6wuddsBV3HzRrXUtezE7P5MoRXp5m3mEDokRDGZB6ry")
|
||||
public byte[] creatorPublicKey;
|
||||
|
||||
@Schema(description = "Qortal AT address")
|
||||
public String atAddress;
|
||||
|
||||
@Schema(description = "Qortal address for trade partner/recipient")
|
||||
public String recipient;
|
||||
|
||||
public CrossChainTradeRequest() {
|
||||
}
|
||||
|
||||
}
|
||||
@@ -10,6 +10,7 @@ public class NodeInfo {
|
||||
public long uptime;
|
||||
public String buildVersion;
|
||||
public long buildTimestamp;
|
||||
public String nodeId;
|
||||
|
||||
public NodeInfo() {
|
||||
}
|
||||
|
||||
@@ -3,13 +3,34 @@ package org.qortal.api.model;
|
||||
import javax.xml.bind.annotation.XmlAccessType;
|
||||
import javax.xml.bind.annotation.XmlAccessorType;
|
||||
|
||||
import org.qortal.controller.Controller;
|
||||
import org.qortal.network.Network;
|
||||
|
||||
@XmlAccessorType(XmlAccessType.FIELD)
|
||||
public class NodeStatus {
|
||||
|
||||
public boolean isMintingPossible;
|
||||
public boolean isSynchronizing;
|
||||
public final boolean isMintingPossible;
|
||||
public final boolean isSynchronizing;
|
||||
|
||||
// Not always present
|
||||
public final Integer syncPercent;
|
||||
|
||||
public final int numberOfConnections;
|
||||
|
||||
public final int height;
|
||||
|
||||
public NodeStatus() {
|
||||
isMintingPossible = Controller.getInstance().isMintingPossible();
|
||||
isSynchronizing = Controller.getInstance().isSynchronizing();
|
||||
|
||||
if (isSynchronizing)
|
||||
syncPercent = Controller.getInstance().getSyncPercent();
|
||||
else
|
||||
syncPercent = null;
|
||||
|
||||
numberOfConnections = Network.getInstance().getHandshakedPeers().size();
|
||||
|
||||
height = Controller.getInstance().getChainHeight();
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -28,6 +28,7 @@ import org.qortal.api.ApiError;
|
||||
import org.qortal.api.ApiErrors;
|
||||
import org.qortal.api.ApiException;
|
||||
import org.qortal.api.ApiExceptionFactory;
|
||||
import org.qortal.api.Security;
|
||||
import org.qortal.api.model.ApiOnlineAccount;
|
||||
import org.qortal.api.model.RewardShareKeyRequest;
|
||||
import org.qortal.asset.Asset;
|
||||
@@ -36,18 +37,27 @@ import org.qortal.crypto.Crypto;
|
||||
import org.qortal.data.account.AccountData;
|
||||
import org.qortal.data.account.RewardShareData;
|
||||
import org.qortal.data.network.OnlineAccountData;
|
||||
import org.qortal.data.transaction.PublicizeTransactionData;
|
||||
import org.qortal.data.transaction.RewardShareTransactionData;
|
||||
import org.qortal.data.transaction.TransactionData;
|
||||
import org.qortal.repository.DataException;
|
||||
import org.qortal.repository.Repository;
|
||||
import org.qortal.repository.RepositoryManager;
|
||||
import org.qortal.settings.Settings;
|
||||
import org.qortal.transaction.PublicizeTransaction;
|
||||
import org.qortal.transaction.Transaction;
|
||||
import org.qortal.transaction.Transaction.TransactionType;
|
||||
import org.qortal.transaction.Transaction.ValidationResult;
|
||||
import org.qortal.transform.TransformationException;
|
||||
import org.qortal.transform.Transformer;
|
||||
import org.qortal.transform.transaction.PublicizeTransactionTransformer;
|
||||
import org.qortal.transform.transaction.RewardShareTransactionTransformer;
|
||||
import org.qortal.transform.transaction.TransactionTransformer;
|
||||
import org.qortal.utils.Amounts;
|
||||
import org.qortal.utils.Base58;
|
||||
|
||||
import com.google.common.primitives.Bytes;
|
||||
|
||||
@Path("/addresses")
|
||||
@Tag(name = "Addresses")
|
||||
public class AddressesResource {
|
||||
@@ -66,32 +76,18 @@ public class AddressesResource {
|
||||
)
|
||||
}
|
||||
)
|
||||
@ApiErrors({ApiError.INVALID_ADDRESS, ApiError.REPOSITORY_ISSUE})
|
||||
@ApiErrors({ApiError.INVALID_ADDRESS, ApiError.ADDRESS_UNKNOWN, ApiError.REPOSITORY_ISSUE})
|
||||
public AccountData getAccountInfo(@PathParam("address") String address) {
|
||||
if (!Crypto.isValidAddress(address))
|
||||
throw ApiExceptionFactory.INSTANCE.createException(request, ApiError.INVALID_ADDRESS);
|
||||
|
||||
try (final Repository repository = RepositoryManager.getRepository()) {
|
||||
AccountData accountData = repository.getAccountRepository().getAccount(address);
|
||||
|
||||
// Not found?
|
||||
if (accountData == null)
|
||||
accountData = new AccountData(address);
|
||||
else {
|
||||
// Unconfirmed transactions could update lastReference
|
||||
Account account = new Account(repository, address);
|
||||
|
||||
// Use last reference based on unconfirmed transactions if possible
|
||||
byte[] unconfirmedLastReference = account.getUnconfirmedLastReference();
|
||||
|
||||
if (unconfirmedLastReference != null)
|
||||
// There are unconfirmed transactions so modify returned data
|
||||
accountData.setReference(unconfirmedLastReference);
|
||||
}
|
||||
throw ApiExceptionFactory.INSTANCE.createException(request, ApiError.ADDRESS_UNKNOWN);
|
||||
|
||||
return accountData;
|
||||
} catch (ApiException e) {
|
||||
throw e;
|
||||
} catch (DataException e) {
|
||||
throw ApiExceptionFactory.INSTANCE.createException(request, ApiError.REPOSITORY_ISSUE, e);
|
||||
}
|
||||
@@ -100,42 +96,37 @@ public class AddressesResource {
|
||||
@GET
|
||||
@Path("/lastreference/{address}")
|
||||
@Operation(
|
||||
summary = "Fetch reference for next transaction to be created by address, considering unconfirmed transactions",
|
||||
description = "Returns the base58-encoded signature of the last confirmed/unconfirmed transaction created by address, failing that: the first incoming transaction. Returns \"false\" if there is no transactions.",
|
||||
summary = "Fetch reference for next transaction to be created by address",
|
||||
description = "Returns the base58-encoded signature of the last confirmed transaction created by address, failing that: the first incoming transaction. Returns \"false\" if there is no last-reference.",
|
||||
responses = {
|
||||
@ApiResponse(
|
||||
description = "the base58-encoded transaction signature",
|
||||
description = "the base58-encoded last-reference",
|
||||
content = @Content(mediaType = MediaType.TEXT_PLAIN, schema = @Schema(type = "string"))
|
||||
)
|
||||
}
|
||||
)
|
||||
@ApiErrors({ApiError.INVALID_ADDRESS, ApiError.REPOSITORY_ISSUE})
|
||||
public String getLastReferenceUnconfirmed(@PathParam("address") String address) {
|
||||
@ApiErrors({ApiError.INVALID_ADDRESS, ApiError.ADDRESS_UNKNOWN, ApiError.REPOSITORY_ISSUE})
|
||||
public String getLastReference(@PathParam("address") String address) {
|
||||
if (!Crypto.isValidAddress(address))
|
||||
throw ApiExceptionFactory.INSTANCE.createException(request, ApiError.INVALID_ADDRESS);
|
||||
|
||||
byte[] lastReference = null;
|
||||
|
||||
try (final Repository repository = RepositoryManager.getRepository()) {
|
||||
Account account = new Account(repository, address);
|
||||
AccountData accountData = repository.getAccountRepository().getAccount(address);
|
||||
// Not found?
|
||||
if (accountData == null)
|
||||
throw ApiExceptionFactory.INSTANCE.createException(request, ApiError.ADDRESS_UNKNOWN);
|
||||
|
||||
// Use last reference based on unconfirmed transactions if possible
|
||||
lastReference = account.getUnconfirmedLastReference();
|
||||
|
||||
if (lastReference == null)
|
||||
// No unconfirmed transactions so fallback to using one save in account data
|
||||
lastReference = account.getLastReference();
|
||||
} catch (ApiException e) {
|
||||
throw e;
|
||||
lastReference = accountData.getReference();
|
||||
} catch (DataException e) {
|
||||
throw ApiExceptionFactory.INSTANCE.createException(request, ApiError.REPOSITORY_ISSUE, e);
|
||||
}
|
||||
|
||||
if(lastReference == null || lastReference.length == 0) {
|
||||
if (lastReference == null || lastReference.length == 0)
|
||||
return "false";
|
||||
} else {
|
||||
return Base58.encode(lastReference);
|
||||
}
|
||||
|
||||
return Base58.encode(lastReference);
|
||||
}
|
||||
|
||||
@GET
|
||||
@@ -214,7 +205,7 @@ public class AddressesResource {
|
||||
else if (!repository.getAssetRepository().assetExists(assetId))
|
||||
throw ApiExceptionFactory.INSTANCE.createException(request, ApiError.INVALID_ASSET_ID);
|
||||
|
||||
return account.getBalance(assetId);
|
||||
return Amounts.toBigDecimal(account.getConfirmedBalance(assetId));
|
||||
} catch (ApiException e) {
|
||||
throw e;
|
||||
} catch (DataException e) {
|
||||
@@ -408,4 +399,119 @@ public class AddressesResource {
|
||||
}
|
||||
}
|
||||
|
||||
@POST
|
||||
@Path("/publicize")
|
||||
@Operation(
|
||||
summary = "Build raw, unsigned, PUBLICIZE transaction",
|
||||
requestBody = @RequestBody(
|
||||
required = true,
|
||||
content = @Content(
|
||||
mediaType = MediaType.APPLICATION_JSON,
|
||||
schema = @Schema(
|
||||
implementation = PublicizeTransactionData.class
|
||||
)
|
||||
)
|
||||
),
|
||||
responses = {
|
||||
@ApiResponse(
|
||||
description = "raw, unsigned, PUBLICIZE transaction encoded in Base58",
|
||||
content = @Content(
|
||||
mediaType = MediaType.TEXT_PLAIN,
|
||||
schema = @Schema(
|
||||
type = "string"
|
||||
)
|
||||
)
|
||||
)
|
||||
}
|
||||
)
|
||||
@ApiErrors({ApiError.NON_PRODUCTION, ApiError.TRANSACTION_INVALID, ApiError.TRANSFORMATION_ERROR, ApiError.REPOSITORY_ISSUE})
|
||||
public String publicize(PublicizeTransactionData transactionData) {
|
||||
if (Settings.getInstance().isApiRestricted())
|
||||
throw ApiExceptionFactory.INSTANCE.createException(request, ApiError.NON_PRODUCTION);
|
||||
|
||||
try (final Repository repository = RepositoryManager.getRepository()) {
|
||||
Transaction transaction = Transaction.fromData(repository, transactionData);
|
||||
|
||||
ValidationResult result = transaction.isValidUnconfirmed();
|
||||
if (result != ValidationResult.OK && result != ValidationResult.INCORRECT_NONCE)
|
||||
throw TransactionsResource.createTransactionInvalidException(request, result);
|
||||
|
||||
byte[] bytes = PublicizeTransactionTransformer.toBytes(transactionData);
|
||||
return Base58.encode(bytes);
|
||||
} catch (TransformationException e) {
|
||||
throw ApiExceptionFactory.INSTANCE.createException(request, ApiError.TRANSFORMATION_ERROR, e);
|
||||
} catch (DataException e) {
|
||||
throw ApiExceptionFactory.INSTANCE.createException(request, ApiError.REPOSITORY_ISSUE, e);
|
||||
}
|
||||
}
|
||||
|
||||
@POST
|
||||
@Path("/publicize/compute")
|
||||
@Operation(
|
||||
summary = "Compute nonce for raw, unsigned PUBLICIZE transaction",
|
||||
requestBody = @RequestBody(
|
||||
required = true,
|
||||
content = @Content(
|
||||
mediaType = MediaType.TEXT_PLAIN,
|
||||
schema = @Schema(
|
||||
type = "string",
|
||||
description = "raw, unsigned PUBLICIZE transaction in base58 encoding",
|
||||
example = "raw transaction base58"
|
||||
)
|
||||
)
|
||||
),
|
||||
responses = {
|
||||
@ApiResponse(
|
||||
description = "raw, unsigned, PUBLICIZE transaction encoded in Base58",
|
||||
content = @Content(
|
||||
mediaType = MediaType.TEXT_PLAIN,
|
||||
schema = @Schema(
|
||||
type = "string"
|
||||
)
|
||||
)
|
||||
)
|
||||
}
|
||||
)
|
||||
@ApiErrors({ApiError.TRANSACTION_INVALID, ApiError.INVALID_DATA, ApiError.TRANSFORMATION_ERROR, ApiError.REPOSITORY_ISSUE})
|
||||
public String computePublicize(String rawBytes58) {
|
||||
Security.checkApiCallAllowed(request);
|
||||
|
||||
try (final Repository repository = RepositoryManager.getRepository()) {
|
||||
byte[] rawBytes = Base58.decode(rawBytes58);
|
||||
// We're expecting unsigned transaction, so append empty signature prior to decoding
|
||||
rawBytes = Bytes.concat(rawBytes, new byte[TransactionTransformer.SIGNATURE_LENGTH]);
|
||||
|
||||
TransactionData transactionData = TransactionTransformer.fromBytes(rawBytes);
|
||||
if (transactionData == null)
|
||||
throw ApiExceptionFactory.INSTANCE.createException(request, ApiError.INVALID_DATA);
|
||||
|
||||
if (transactionData.getType() != TransactionType.PUBLICIZE)
|
||||
throw ApiExceptionFactory.INSTANCE.createException(request, ApiError.INVALID_DATA);
|
||||
|
||||
PublicizeTransaction publicizeTransaction = (PublicizeTransaction) Transaction.fromData(repository, transactionData);
|
||||
|
||||
// Quicker validity check first before we compute nonce
|
||||
ValidationResult result = publicizeTransaction.isValid();
|
||||
if (result != ValidationResult.OK && result != ValidationResult.INCORRECT_NONCE)
|
||||
throw TransactionsResource.createTransactionInvalidException(request, result);
|
||||
|
||||
publicizeTransaction.computeNonce();
|
||||
|
||||
// Re-check, but ignores signature
|
||||
result = publicizeTransaction.isValidUnconfirmed();
|
||||
if (result != ValidationResult.OK)
|
||||
throw TransactionsResource.createTransactionInvalidException(request, result);
|
||||
|
||||
// Strip zeroed signature
|
||||
transactionData.setSignature(null);
|
||||
|
||||
byte[] bytes = PublicizeTransactionTransformer.toBytes(transactionData);
|
||||
return Base58.encode(bytes);
|
||||
} catch (TransformationException e) {
|
||||
throw ApiExceptionFactory.INSTANCE.createException(request, ApiError.TRANSFORMATION_ERROR, e);
|
||||
} catch (DataException e) {
|
||||
throw ApiExceptionFactory.INSTANCE.createException(request, ApiError.REPOSITORY_ISSUE, e);
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -117,6 +117,7 @@ public class AdminResource {
|
||||
nodeInfo.uptime = System.currentTimeMillis() - Controller.startTime;
|
||||
nodeInfo.buildVersion = Controller.getInstance().getVersionString();
|
||||
nodeInfo.buildTimestamp = Controller.getInstance().getBuildTimestamp();
|
||||
nodeInfo.nodeId = Network.getInstance().getOurNodeId();
|
||||
|
||||
return nodeInfo;
|
||||
}
|
||||
@@ -136,9 +137,6 @@ public class AdminResource {
|
||||
|
||||
NodeStatus nodeStatus = new NodeStatus();
|
||||
|
||||
nodeStatus.isMintingPossible = Controller.getInstance().isMintingPossible();
|
||||
nodeStatus.isSynchronizing = Controller.getInstance().isSynchronizing();
|
||||
|
||||
return nodeStatus;
|
||||
}
|
||||
|
||||
@@ -304,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"
|
||||
)
|
||||
)
|
||||
),
|
||||
@@ -321,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();
|
||||
|
||||
@@ -13,7 +13,10 @@ import io.swagger.v3.oas.annotations.tags.Tag;
|
||||
@Tag(name = "Admin"),
|
||||
@Tag(name = "Arbitrary"),
|
||||
@Tag(name = "Assets"),
|
||||
@Tag(name = "Automated Transactions"),
|
||||
@Tag(name = "Blocks"),
|
||||
@Tag(name = "Chat"),
|
||||
@Tag(name = "Cross-Chain"),
|
||||
@Tag(name = "Groups"),
|
||||
@Tag(name = "Names"),
|
||||
@Tag(name = "Payments"),
|
||||
|
||||
206
src/main/java/org/qortal/api/resource/AtResource.java
Normal file
206
src/main/java/org/qortal/api/resource/AtResource.java
Normal file
@@ -0,0 +1,206 @@
|
||||
package org.qortal.api.resource;
|
||||
|
||||
import io.swagger.v3.oas.annotations.Operation;
|
||||
import io.swagger.v3.oas.annotations.Parameter;
|
||||
import io.swagger.v3.oas.annotations.media.ArraySchema;
|
||||
import io.swagger.v3.oas.annotations.media.Content;
|
||||
import io.swagger.v3.oas.annotations.media.Schema;
|
||||
import io.swagger.v3.oas.annotations.parameters.RequestBody;
|
||||
import io.swagger.v3.oas.annotations.responses.ApiResponse;
|
||||
import io.swagger.v3.oas.annotations.tags.Tag;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
import javax.servlet.http.HttpServletRequest;
|
||||
import javax.ws.rs.GET;
|
||||
import javax.ws.rs.POST;
|
||||
import javax.ws.rs.Path;
|
||||
import javax.ws.rs.PathParam;
|
||||
import javax.ws.rs.QueryParam;
|
||||
import javax.ws.rs.core.Context;
|
||||
import javax.ws.rs.core.MediaType;
|
||||
|
||||
import org.ciyam.at.MachineState;
|
||||
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;
|
||||
import org.qortal.repository.DataException;
|
||||
import org.qortal.repository.Repository;
|
||||
import org.qortal.repository.RepositoryManager;
|
||||
import org.qortal.settings.Settings;
|
||||
import org.qortal.transaction.Transaction;
|
||||
import org.qortal.transaction.Transaction.ValidationResult;
|
||||
import org.qortal.transform.TransformationException;
|
||||
import org.qortal.transform.transaction.DeployAtTransactionTransformer;
|
||||
import org.qortal.utils.Base58;
|
||||
|
||||
@Path("/at")
|
||||
@Tag(name = "Automated Transactions")
|
||||
public class AtResource {
|
||||
|
||||
@Context
|
||||
HttpServletRequest request;
|
||||
|
||||
@GET
|
||||
@Path("/byfunction/{codehash}")
|
||||
@Operation(
|
||||
summary = "Find automated transactions with matching functionality (code hash)",
|
||||
responses = {
|
||||
@ApiResponse(
|
||||
description = "automated transactions",
|
||||
content = @Content(
|
||||
array = @ArraySchema(
|
||||
schema = @Schema(
|
||||
implementation = ATData.class
|
||||
)
|
||||
)
|
||||
)
|
||||
)
|
||||
}
|
||||
)
|
||||
@ApiErrors({
|
||||
ApiError.INVALID_CRITERIA, ApiError.REPOSITORY_ISSUE
|
||||
})
|
||||
public List<ATData> getByFunctionality(
|
||||
@PathParam("codehash")
|
||||
String codeHash58,
|
||||
@Parameter(description = "whether to include ATs that can run, or not, or both (if omitted)")
|
||||
@QueryParam("isExecutable")
|
||||
Boolean isExecutable,
|
||||
@Parameter( ref = "limit") @QueryParam("limit") Integer limit,
|
||||
@Parameter( ref = "offset" ) @QueryParam("offset") Integer offset,
|
||||
@Parameter( ref = "reverse" ) @QueryParam("reverse") Boolean reverse) {
|
||||
// Decode codeHash
|
||||
byte[] codeHash;
|
||||
try {
|
||||
codeHash = Base58.decode(codeHash58);
|
||||
} catch (NumberFormatException e) {
|
||||
throw ApiExceptionFactory.INSTANCE.createException(request, ApiError.INVALID_CRITERIA, e);
|
||||
}
|
||||
|
||||
// codeHash must be present and have correct length
|
||||
if (codeHash == null || codeHash.length != 32)
|
||||
throw ApiExceptionFactory.INSTANCE.createException(request, ApiError.INVALID_CRITERIA);
|
||||
|
||||
// Impose a limit on 'limit'
|
||||
if (limit != null && limit > 100)
|
||||
throw ApiExceptionFactory.INSTANCE.createException(request, ApiError.INVALID_CRITERIA);
|
||||
|
||||
try (final Repository repository = RepositoryManager.getRepository()) {
|
||||
return repository.getATRepository().getATsByFunctionality(codeHash, isExecutable, limit, offset, reverse);
|
||||
} catch (ApiException e) {
|
||||
throw e;
|
||||
} catch (DataException e) {
|
||||
throw ApiExceptionFactory.INSTANCE.createException(request, ApiError.REPOSITORY_ISSUE, e);
|
||||
}
|
||||
}
|
||||
|
||||
@GET
|
||||
@Path("/{ataddress}")
|
||||
@Operation(
|
||||
summary = "Fetch info associated with AT address",
|
||||
responses = {
|
||||
@ApiResponse(
|
||||
description = "automated transaction",
|
||||
content = @Content(
|
||||
schema = @Schema(implementation = ATData.class)
|
||||
)
|
||||
)
|
||||
}
|
||||
)
|
||||
@ApiErrors({
|
||||
ApiError.REPOSITORY_ISSUE
|
||||
})
|
||||
public ATData getByAddress(@PathParam("ataddress") String atAddress) {
|
||||
try (final Repository repository = RepositoryManager.getRepository()) {
|
||||
return repository.getATRepository().fromATAddress(atAddress);
|
||||
} catch (ApiException e) {
|
||||
throw e;
|
||||
} catch (DataException e) {
|
||||
throw ApiExceptionFactory.INSTANCE.createException(request, ApiError.REPOSITORY_ISSUE, e);
|
||||
}
|
||||
}
|
||||
|
||||
@GET
|
||||
@Path("/{ataddress}/data")
|
||||
@Operation(
|
||||
summary = "Fetch data segment associated with AT address",
|
||||
responses = {
|
||||
@ApiResponse(
|
||||
description = "automated transaction",
|
||||
content = @Content(
|
||||
schema = @Schema(implementation = byte[].class)
|
||||
)
|
||||
)
|
||||
}
|
||||
)
|
||||
@ApiErrors({
|
||||
ApiError.REPOSITORY_ISSUE
|
||||
})
|
||||
public byte[] getDataByAddress(@PathParam("ataddress") String atAddress) {
|
||||
try (final Repository repository = RepositoryManager.getRepository()) {
|
||||
ATStateData atStateData = repository.getATRepository().getLatestATState(atAddress);
|
||||
byte[] stateData = atStateData.getStateData();
|
||||
|
||||
QortalAtLoggerFactory loggerFactory = QortalAtLoggerFactory.getInstance();
|
||||
byte[] dataBytes = MachineState.extractDataBytes(loggerFactory, stateData);
|
||||
|
||||
return dataBytes;
|
||||
} catch (ApiException e) {
|
||||
throw e;
|
||||
} catch (DataException e) {
|
||||
throw ApiExceptionFactory.INSTANCE.createException(request, ApiError.REPOSITORY_ISSUE, e);
|
||||
}
|
||||
}
|
||||
|
||||
@POST
|
||||
@Operation(
|
||||
summary = "Build raw, unsigned, DEPLOY_AT transaction",
|
||||
requestBody = @RequestBody(
|
||||
required = true,
|
||||
content = @Content(
|
||||
mediaType = MediaType.APPLICATION_JSON,
|
||||
schema = @Schema(
|
||||
implementation = DeployAtTransactionData.class
|
||||
)
|
||||
)
|
||||
),
|
||||
responses = {
|
||||
@ApiResponse(
|
||||
description = "raw, unsigned, DEPLOY_AT transaction encoded in Base58",
|
||||
content = @Content(
|
||||
mediaType = MediaType.TEXT_PLAIN,
|
||||
schema = @Schema(
|
||||
type = "string"
|
||||
)
|
||||
)
|
||||
)
|
||||
}
|
||||
)
|
||||
@ApiErrors({ApiError.NON_PRODUCTION, ApiError.TRANSACTION_INVALID, ApiError.TRANSFORMATION_ERROR, ApiError.REPOSITORY_ISSUE})
|
||||
public String createDeployAt(DeployAtTransactionData transactionData) {
|
||||
if (Settings.getInstance().isApiRestricted())
|
||||
throw ApiExceptionFactory.INSTANCE.createException(request, ApiError.NON_PRODUCTION);
|
||||
|
||||
try (final Repository repository = RepositoryManager.getRepository()) {
|
||||
Transaction transaction = Transaction.fromData(repository, transactionData);
|
||||
|
||||
ValidationResult result = transaction.isValidUnconfirmed();
|
||||
if (result != ValidationResult.OK)
|
||||
throw TransactionsResource.createTransactionInvalidException(request, result);
|
||||
|
||||
byte[] bytes = DeployAtTransactionTransformer.toBytes(transactionData);
|
||||
return Base58.encode(bytes);
|
||||
} catch (TransformationException e) {
|
||||
throw ApiExceptionFactory.INSTANCE.createException(request, ApiError.TRANSFORMATION_ERROR, e);
|
||||
} catch (DataException e) {
|
||||
throw ApiExceptionFactory.INSTANCE.createException(request, ApiError.REPOSITORY_ISSUE, e);
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
@@ -22,9 +22,9 @@ import javax.ws.rs.core.MediaType;
|
||||
|
||||
import org.qortal.api.ApiError;
|
||||
import org.qortal.api.ApiErrors;
|
||||
import org.qortal.api.ApiException;
|
||||
import org.qortal.api.ApiExceptionFactory;
|
||||
import org.qortal.api.model.BlockMinterSummary;
|
||||
import org.qortal.api.model.BlockInfo;
|
||||
import org.qortal.api.model.BlockSignerSummary;
|
||||
import org.qortal.crypto.Crypto;
|
||||
import org.qortal.data.account.AccountData;
|
||||
import org.qortal.data.block.BlockData;
|
||||
@@ -71,9 +71,11 @@ public class BlocksResource {
|
||||
}
|
||||
|
||||
try (final Repository repository = RepositoryManager.getRepository()) {
|
||||
return repository.getBlockRepository().fromSignature(signature);
|
||||
} catch (ApiException e) {
|
||||
throw e;
|
||||
BlockData blockData = repository.getBlockRepository().fromSignature(signature);
|
||||
if (blockData == null)
|
||||
throw ApiExceptionFactory.INSTANCE.createException(request, ApiError.BLOCK_UNKNOWN);
|
||||
|
||||
return blockData;
|
||||
} catch (DataException e) {
|
||||
throw ApiExceptionFactory.INSTANCE.createException(request, ApiError.REPOSITORY_ISSUE, e);
|
||||
}
|
||||
@@ -120,8 +122,6 @@ public class BlocksResource {
|
||||
throw ApiExceptionFactory.INSTANCE.createException(request, ApiError.BLOCK_UNKNOWN);
|
||||
|
||||
return repository.getBlockRepository().getTransactionsFromSignature(signature, limit, offset, reverse);
|
||||
} catch (ApiException e) {
|
||||
throw e;
|
||||
} catch (DataException e) {
|
||||
throw ApiExceptionFactory.INSTANCE.createException(request, ApiError.REPOSITORY_ISSUE, e);
|
||||
}
|
||||
@@ -223,8 +223,6 @@ public class BlocksResource {
|
||||
throw ApiExceptionFactory.INSTANCE.createException(request, ApiError.BLOCK_UNKNOWN);
|
||||
|
||||
return childBlockData;
|
||||
} catch (ApiException e) {
|
||||
throw e;
|
||||
} catch (DataException e) {
|
||||
throw ApiExceptionFactory.INSTANCE.createException(request, ApiError.REPOSITORY_ISSUE, e);
|
||||
}
|
||||
@@ -253,8 +251,6 @@ public class BlocksResource {
|
||||
public int getHeight() {
|
||||
try (final Repository repository = RepositoryManager.getRepository()) {
|
||||
return repository.getBlockRepository().getBlockchainHeight();
|
||||
} catch (ApiException e) {
|
||||
throw e;
|
||||
} catch (DataException e) {
|
||||
throw ApiExceptionFactory.INSTANCE.createException(request, ApiError.REPOSITORY_ISSUE, e);
|
||||
}
|
||||
@@ -297,8 +293,6 @@ public class BlocksResource {
|
||||
throw ApiExceptionFactory.INSTANCE.createException(request, ApiError.BLOCK_UNKNOWN);
|
||||
|
||||
return blockData.getHeight();
|
||||
} catch (ApiException e) {
|
||||
throw e;
|
||||
} catch (DataException e) {
|
||||
throw ApiExceptionFactory.INSTANCE.createException(request, ApiError.REPOSITORY_ISSUE, e);
|
||||
}
|
||||
@@ -330,8 +324,6 @@ public class BlocksResource {
|
||||
throw ApiExceptionFactory.INSTANCE.createException(request, ApiError.BLOCK_UNKNOWN);
|
||||
|
||||
return blockData;
|
||||
} catch (ApiException e) {
|
||||
throw e;
|
||||
} catch (DataException e) {
|
||||
throw ApiExceptionFactory.INSTANCE.createException(request, ApiError.REPOSITORY_ISSUE, e);
|
||||
}
|
||||
@@ -366,8 +358,6 @@ public class BlocksResource {
|
||||
throw ApiExceptionFactory.INSTANCE.createException(request, ApiError.BLOCK_UNKNOWN);
|
||||
|
||||
return blockData;
|
||||
} catch (ApiException e) {
|
||||
throw e;
|
||||
} catch (DataException e) {
|
||||
throw ApiExceptionFactory.INSTANCE.createException(request, ApiError.REPOSITORY_ISSUE, e);
|
||||
}
|
||||
@@ -416,9 +406,9 @@ public class BlocksResource {
|
||||
}
|
||||
|
||||
@GET
|
||||
@Path("/minter/{address}")
|
||||
@Path("/signer/{address}")
|
||||
@Operation(
|
||||
summary = "Fetch block summaries for blocks minted by address",
|
||||
summary = "Fetch block summaries for blocks signed by address",
|
||||
responses = {
|
||||
@ApiResponse(
|
||||
description = "block summaries",
|
||||
@@ -433,7 +423,7 @@ public class BlocksResource {
|
||||
}
|
||||
)
|
||||
@ApiErrors({ApiError.INVALID_ADDRESS, ApiError.PUBLIC_KEY_NOT_FOUND, ApiError.REPOSITORY_ISSUE})
|
||||
public List<BlockSummaryData> getBlockSummariesByMinter(@PathParam("address") String address, @Parameter(
|
||||
public List<BlockSummaryData> getBlockSummariesBySigner(@PathParam("address") String address, @Parameter(
|
||||
ref = "limit"
|
||||
) @QueryParam("limit") Integer limit, @Parameter(
|
||||
ref = "offset"
|
||||
@@ -449,32 +439,30 @@ public class BlocksResource {
|
||||
if (accountData == null || accountData.getPublicKey() == null)
|
||||
throw ApiExceptionFactory.INSTANCE.createException(request, ApiError.PUBLIC_KEY_NOT_FOUND);
|
||||
|
||||
return repository.getBlockRepository().getBlockSummariesByMinter(accountData.getPublicKey(), limit, offset, reverse);
|
||||
} catch (ApiException e) {
|
||||
throw e;
|
||||
return repository.getBlockRepository().getBlockSummariesBySigner(accountData.getPublicKey(), limit, offset, reverse);
|
||||
} catch (DataException e) {
|
||||
throw ApiExceptionFactory.INSTANCE.createException(request, ApiError.REPOSITORY_ISSUE, e);
|
||||
}
|
||||
}
|
||||
|
||||
@GET
|
||||
@Path("/minters")
|
||||
@Path("/signers")
|
||||
@Operation(
|
||||
summary = "Show summary of block minters",
|
||||
description = "Returns count of blocks minted, optionally limited to minters/recipients in passed address(es).",
|
||||
summary = "Show summary of block signers",
|
||||
description = "Returns count of blocks signed, optionally limited to minters/recipients in passed address(es).",
|
||||
responses = {
|
||||
@ApiResponse(
|
||||
content = @Content(
|
||||
array = @ArraySchema(
|
||||
schema = @Schema(
|
||||
implementation = BlockMinterSummary.class
|
||||
implementation = BlockSignerSummary.class
|
||||
)
|
||||
)
|
||||
)
|
||||
)
|
||||
}
|
||||
)
|
||||
public List<BlockMinterSummary> getBlockMinters(@QueryParam("address") List<String> addresses,
|
||||
public List<BlockSignerSummary> getBlockSigners(@QueryParam("address") List<String> addresses,
|
||||
@Parameter(
|
||||
ref = "limit"
|
||||
) @QueryParam("limit") Integer limit, @Parameter(
|
||||
@@ -487,7 +475,47 @@ public class BlocksResource {
|
||||
if (!Crypto.isValidAddress(address))
|
||||
throw ApiExceptionFactory.INSTANCE.createException(request, ApiError.INVALID_ADDRESS);
|
||||
|
||||
return repository.getBlockRepository().getBlockMinters(addresses, limit, offset, reverse);
|
||||
return repository.getBlockRepository().getBlockSigners(addresses, limit, offset, reverse);
|
||||
} catch (DataException e) {
|
||||
throw ApiExceptionFactory.INSTANCE.createException(request, ApiError.REPOSITORY_ISSUE, e);
|
||||
}
|
||||
}
|
||||
|
||||
@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);
|
||||
}
|
||||
|
||||
247
src/main/java/org/qortal/api/resource/ChatResource.java
Normal file
247
src/main/java/org/qortal/api/resource/ChatResource.java
Normal file
@@ -0,0 +1,247 @@
|
||||
package org.qortal.api.resource;
|
||||
|
||||
import io.swagger.v3.oas.annotations.Operation;
|
||||
import io.swagger.v3.oas.annotations.Parameter;
|
||||
import io.swagger.v3.oas.annotations.media.ArraySchema;
|
||||
import io.swagger.v3.oas.annotations.media.Content;
|
||||
import io.swagger.v3.oas.annotations.media.Schema;
|
||||
import io.swagger.v3.oas.annotations.parameters.RequestBody;
|
||||
import io.swagger.v3.oas.annotations.responses.ApiResponse;
|
||||
import io.swagger.v3.oas.annotations.tags.Tag;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
import javax.servlet.http.HttpServletRequest;
|
||||
import javax.ws.rs.GET;
|
||||
import javax.ws.rs.POST;
|
||||
import javax.ws.rs.Path;
|
||||
import javax.ws.rs.PathParam;
|
||||
import javax.ws.rs.QueryParam;
|
||||
import javax.ws.rs.core.Context;
|
||||
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.Security;
|
||||
import org.qortal.crypto.Crypto;
|
||||
import org.qortal.data.chat.ActiveChats;
|
||||
import org.qortal.data.chat.ChatMessage;
|
||||
import org.qortal.data.transaction.ChatTransactionData;
|
||||
import org.qortal.data.transaction.TransactionData;
|
||||
import org.qortal.repository.DataException;
|
||||
import org.qortal.repository.Repository;
|
||||
import org.qortal.repository.RepositoryManager;
|
||||
import org.qortal.transaction.ChatTransaction;
|
||||
import org.qortal.transaction.Transaction;
|
||||
import org.qortal.transaction.Transaction.TransactionType;
|
||||
import org.qortal.transaction.Transaction.ValidationResult;
|
||||
import org.qortal.transform.TransformationException;
|
||||
import org.qortal.transform.transaction.ChatTransactionTransformer;
|
||||
import org.qortal.transform.transaction.TransactionTransformer;
|
||||
import org.qortal.utils.Base58;
|
||||
|
||||
import com.google.common.primitives.Bytes;
|
||||
|
||||
@Path("/chat")
|
||||
@Tag(name = "Chat")
|
||||
public class ChatResource {
|
||||
|
||||
@Context
|
||||
HttpServletRequest request;
|
||||
|
||||
@GET
|
||||
@Path("/messages")
|
||||
@Operation(
|
||||
summary = "Find chat messages",
|
||||
description = "Returns CHAT messages that match criteria. Must provide EITHER 'txGroupId' OR two 'involving' addresses.",
|
||||
responses = {
|
||||
@ApiResponse(
|
||||
description = "CHAT messages",
|
||||
content = @Content(
|
||||
array = @ArraySchema(
|
||||
schema = @Schema(
|
||||
implementation = ChatMessage.class
|
||||
)
|
||||
)
|
||||
)
|
||||
)
|
||||
}
|
||||
)
|
||||
@ApiErrors({ApiError.INVALID_CRITERIA, ApiError.INVALID_ADDRESS, ApiError.REPOSITORY_ISSUE})
|
||||
public List<ChatMessage> searchChat(@QueryParam("before") Long before, @QueryParam("after") Long after,
|
||||
@QueryParam("txGroupId") Integer txGroupId,
|
||||
@QueryParam("involving") List<String> involvingAddresses,
|
||||
@Parameter(ref = "limit") @QueryParam("limit") Integer limit,
|
||||
@Parameter(ref = "offset") @QueryParam("offset") Integer offset,
|
||||
@Parameter(ref = "reverse") @QueryParam("reverse") Boolean reverse) {
|
||||
// Check args meet expectations
|
||||
if ((txGroupId == null && involvingAddresses.size() != 2)
|
||||
|| (txGroupId != null && !involvingAddresses.isEmpty()))
|
||||
throw ApiExceptionFactory.INSTANCE.createException(request, ApiError.INVALID_CRITERIA);
|
||||
|
||||
// Check any provided addresses are valid
|
||||
if (involvingAddresses.stream().anyMatch(address -> !Crypto.isValidAddress(address)))
|
||||
throw ApiExceptionFactory.INSTANCE.createException(request, ApiError.INVALID_ADDRESS);
|
||||
|
||||
if (before != null && before < 1500000000000L)
|
||||
throw ApiExceptionFactory.INSTANCE.createException(request, ApiError.INVALID_CRITERIA);
|
||||
|
||||
if (after != null && after < 1500000000000L)
|
||||
throw ApiExceptionFactory.INSTANCE.createException(request, ApiError.INVALID_CRITERIA);
|
||||
|
||||
try (final Repository repository = RepositoryManager.getRepository()) {
|
||||
return repository.getChatRepository().getMessagesMatchingCriteria(
|
||||
before,
|
||||
after,
|
||||
txGroupId,
|
||||
involvingAddresses,
|
||||
limit, offset, reverse);
|
||||
} catch (DataException e) {
|
||||
throw ApiExceptionFactory.INSTANCE.createException(request, ApiError.REPOSITORY_ISSUE, e);
|
||||
}
|
||||
}
|
||||
|
||||
@GET
|
||||
@Path("/active/{address}")
|
||||
@Operation(
|
||||
summary = "Find active chats (group/direct) involving address",
|
||||
responses = {
|
||||
@ApiResponse(
|
||||
content = @Content(
|
||||
array = @ArraySchema(
|
||||
schema = @Schema(
|
||||
implementation = ActiveChats.class
|
||||
)
|
||||
)
|
||||
)
|
||||
)
|
||||
}
|
||||
)
|
||||
@ApiErrors({ApiError.INVALID_CRITERIA, ApiError.INVALID_ADDRESS, ApiError.REPOSITORY_ISSUE})
|
||||
public ActiveChats getActiveChats(@PathParam("address") String address) {
|
||||
if (address == null || !Crypto.isValidAddress(address))
|
||||
throw ApiExceptionFactory.INSTANCE.createException(request, ApiError.INVALID_ADDRESS);
|
||||
|
||||
try (final Repository repository = RepositoryManager.getRepository()) {
|
||||
return repository.getChatRepository().getActiveChats(address);
|
||||
} catch (DataException e) {
|
||||
throw ApiExceptionFactory.INSTANCE.createException(request, ApiError.REPOSITORY_ISSUE, e);
|
||||
}
|
||||
}
|
||||
|
||||
@POST
|
||||
@Operation(
|
||||
summary = "Build raw, unsigned, CHAT transaction",
|
||||
description = "Builds a raw, unsigned CHAT transaction but does NOT compute proof-of-work nonce. See POST /chat/compute.",
|
||||
requestBody = @RequestBody(
|
||||
required = true,
|
||||
content = @Content(
|
||||
mediaType = MediaType.APPLICATION_JSON,
|
||||
schema = @Schema(
|
||||
implementation = ChatTransactionData.class
|
||||
)
|
||||
)
|
||||
),
|
||||
responses = {
|
||||
@ApiResponse(
|
||||
description = "raw, unsigned, CHAT transaction encoded in Base58",
|
||||
content = @Content(
|
||||
mediaType = MediaType.TEXT_PLAIN,
|
||||
schema = @Schema(
|
||||
type = "string"
|
||||
)
|
||||
)
|
||||
)
|
||||
}
|
||||
)
|
||||
@ApiErrors({ApiError.TRANSACTION_INVALID, ApiError.TRANSFORMATION_ERROR, ApiError.REPOSITORY_ISSUE})
|
||||
public String buildChat(ChatTransactionData transactionData) {
|
||||
Security.checkApiCallAllowed(request);
|
||||
|
||||
try (final Repository repository = RepositoryManager.getRepository()) {
|
||||
ChatTransaction chatTransaction = (ChatTransaction) Transaction.fromData(repository, transactionData);
|
||||
|
||||
ValidationResult result = chatTransaction.isValidUnconfirmed();
|
||||
if (result != ValidationResult.OK)
|
||||
throw TransactionsResource.createTransactionInvalidException(request, result);
|
||||
|
||||
byte[] bytes = ChatTransactionTransformer.toBytes(transactionData);
|
||||
return Base58.encode(bytes);
|
||||
} catch (TransformationException e) {
|
||||
throw ApiExceptionFactory.INSTANCE.createException(request, ApiError.TRANSFORMATION_ERROR, e);
|
||||
} catch (DataException e) {
|
||||
throw ApiExceptionFactory.INSTANCE.createException(request, ApiError.REPOSITORY_ISSUE, e);
|
||||
}
|
||||
}
|
||||
|
||||
@POST
|
||||
@Path("/compute")
|
||||
@Operation(
|
||||
summary = "Compute nonce for raw, unsigned CHAT transaction",
|
||||
requestBody = @RequestBody(
|
||||
required = true,
|
||||
content = @Content(
|
||||
mediaType = MediaType.TEXT_PLAIN,
|
||||
schema = @Schema(
|
||||
type = "string",
|
||||
description = "raw, unsigned CHAT transaction in base58 encoding",
|
||||
example = "raw transaction base58"
|
||||
)
|
||||
)
|
||||
),
|
||||
responses = {
|
||||
@ApiResponse(
|
||||
description = "raw, unsigned, CHAT transaction encoded in Base58",
|
||||
content = @Content(
|
||||
mediaType = MediaType.TEXT_PLAIN,
|
||||
schema = @Schema(
|
||||
type = "string"
|
||||
)
|
||||
)
|
||||
)
|
||||
}
|
||||
)
|
||||
@ApiErrors({ApiError.TRANSACTION_INVALID, ApiError.INVALID_DATA, ApiError.TRANSFORMATION_ERROR, ApiError.REPOSITORY_ISSUE})
|
||||
public String buildChat(String rawBytes58) {
|
||||
Security.checkApiCallAllowed(request);
|
||||
|
||||
try (final Repository repository = RepositoryManager.getRepository()) {
|
||||
byte[] rawBytes = Base58.decode(rawBytes58);
|
||||
// We're expecting unsigned transaction, so append empty signature prior to decoding
|
||||
rawBytes = Bytes.concat(rawBytes, new byte[TransactionTransformer.SIGNATURE_LENGTH]);
|
||||
|
||||
TransactionData transactionData = TransactionTransformer.fromBytes(rawBytes);
|
||||
if (transactionData == null)
|
||||
throw ApiExceptionFactory.INSTANCE.createException(request, ApiError.INVALID_DATA);
|
||||
|
||||
if (transactionData.getType() != TransactionType.CHAT)
|
||||
throw ApiExceptionFactory.INSTANCE.createException(request, ApiError.INVALID_DATA);
|
||||
|
||||
ChatTransaction chatTransaction = (ChatTransaction) Transaction.fromData(repository, transactionData);
|
||||
|
||||
// Quicker validity check first before we compute nonce
|
||||
ValidationResult result = chatTransaction.isValid();
|
||||
if (result != ValidationResult.OK)
|
||||
throw TransactionsResource.createTransactionInvalidException(request, result);
|
||||
|
||||
chatTransaction.computeNonce();
|
||||
|
||||
// Re-check, but ignores signature
|
||||
result = chatTransaction.isValidUnconfirmed();
|
||||
if (result != ValidationResult.OK)
|
||||
throw TransactionsResource.createTransactionInvalidException(request, result);
|
||||
|
||||
// Strip zeroed signature
|
||||
transactionData.setSignature(null);
|
||||
|
||||
byte[] bytes = ChatTransactionTransformer.toBytes(transactionData);
|
||||
return Base58.encode(bytes);
|
||||
} catch (TransformationException e) {
|
||||
throw ApiExceptionFactory.INSTANCE.createException(request, ApiError.TRANSFORMATION_ERROR, e);
|
||||
} catch (DataException e) {
|
||||
throw ApiExceptionFactory.INSTANCE.createException(request, ApiError.REPOSITORY_ISSUE, e);
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
774
src/main/java/org/qortal/api/resource/CrossChainResource.java
Normal file
774
src/main/java/org/qortal/api/resource/CrossChainResource.java
Normal file
@@ -0,0 +1,774 @@
|
||||
package org.qortal.api.resource;
|
||||
|
||||
import io.swagger.v3.oas.annotations.Operation;
|
||||
import io.swagger.v3.oas.annotations.Parameter;
|
||||
import io.swagger.v3.oas.annotations.media.ArraySchema;
|
||||
import io.swagger.v3.oas.annotations.media.Content;
|
||||
import io.swagger.v3.oas.annotations.media.Schema;
|
||||
import io.swagger.v3.oas.annotations.parameters.RequestBody;
|
||||
import io.swagger.v3.oas.annotations.responses.ApiResponse;
|
||||
import io.swagger.v3.oas.annotations.tags.Tag;
|
||||
|
||||
import java.math.BigDecimal;
|
||||
import java.util.ArrayList;
|
||||
import java.util.Arrays;
|
||||
import java.util.List;
|
||||
|
||||
import javax.servlet.http.HttpServletRequest;
|
||||
import javax.ws.rs.DELETE;
|
||||
import javax.ws.rs.GET;
|
||||
import javax.ws.rs.POST;
|
||||
import javax.ws.rs.Path;
|
||||
import javax.ws.rs.QueryParam;
|
||||
import javax.ws.rs.core.Context;
|
||||
import javax.ws.rs.core.MediaType;
|
||||
|
||||
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.TransactionOutput;
|
||||
import org.qortal.account.PublicKeyAccount;
|
||||
import org.qortal.api.ApiError;
|
||||
import org.qortal.api.ApiErrors;
|
||||
import org.qortal.api.ApiException;
|
||||
import org.qortal.api.ApiExceptionFactory;
|
||||
import org.qortal.api.model.CrossChainCancelRequest;
|
||||
import org.qortal.api.model.CrossChainSecretRequest;
|
||||
import org.qortal.api.model.CrossChainTradeRequest;
|
||||
import org.qortal.api.model.CrossChainBitcoinP2SHStatus;
|
||||
import org.qortal.api.model.CrossChainBitcoinRedeemRequest;
|
||||
import org.qortal.api.model.CrossChainBitcoinRefundRequest;
|
||||
import org.qortal.api.model.CrossChainBitcoinTemplateRequest;
|
||||
import org.qortal.api.model.CrossChainBuildRequest;
|
||||
import org.qortal.asset.Asset;
|
||||
import org.qortal.crosschain.BTC;
|
||||
import org.qortal.crosschain.BTCACCT;
|
||||
import org.qortal.crypto.Crypto;
|
||||
import org.qortal.data.at.ATData;
|
||||
import org.qortal.data.crosschain.CrossChainTradeData;
|
||||
import org.qortal.data.crosschain.CrossChainTradeData.Mode;
|
||||
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.transaction.DeployAtTransaction;
|
||||
import org.qortal.transaction.MessageTransaction;
|
||||
import org.qortal.transaction.Transaction;
|
||||
import org.qortal.transaction.Transaction.ValidationResult;
|
||||
import org.qortal.transform.TransformationException;
|
||||
import org.qortal.transform.Transformer;
|
||||
import org.qortal.transform.transaction.DeployAtTransactionTransformer;
|
||||
import org.qortal.transform.transaction.MessageTransactionTransformer;
|
||||
import org.qortal.utils.Base58;
|
||||
import org.qortal.utils.NTP;
|
||||
|
||||
import com.google.common.primitives.Bytes;
|
||||
|
||||
@Path("/crosschain")
|
||||
@Tag(name = "Cross-Chain")
|
||||
public class CrossChainResource {
|
||||
|
||||
@Context
|
||||
HttpServletRequest request;
|
||||
|
||||
@GET
|
||||
@Path("/tradeoffers")
|
||||
@Operation(
|
||||
summary = "Find cross-chain trade offers",
|
||||
responses = {
|
||||
@ApiResponse(
|
||||
description = "automated transactions",
|
||||
content = @Content(
|
||||
array = @ArraySchema(
|
||||
schema = @Schema(
|
||||
implementation = CrossChainTradeData.class
|
||||
)
|
||||
)
|
||||
)
|
||||
)
|
||||
}
|
||||
)
|
||||
@ApiErrors({ApiError.INVALID_CRITERIA, ApiError.REPOSITORY_ISSUE})
|
||||
public List<CrossChainTradeData> getTradeOffers(
|
||||
@Parameter( ref = "limit") @QueryParam("limit") Integer limit,
|
||||
@Parameter( ref = "offset" ) @QueryParam("offset") Integer offset,
|
||||
@Parameter( ref = "reverse" ) @QueryParam("reverse") Boolean reverse) {
|
||||
// Impose a limit on 'limit'
|
||||
if (limit != null && limit > 100)
|
||||
throw ApiExceptionFactory.INSTANCE.createException(request, ApiError.INVALID_CRITERIA);
|
||||
|
||||
byte[] codeHash = BTCACCT.CODE_BYTES_HASH;
|
||||
boolean isExecutable = true;
|
||||
|
||||
try (final Repository repository = RepositoryManager.getRepository()) {
|
||||
List<ATData> atsData = repository.getATRepository().getATsByFunctionality(codeHash, isExecutable, limit, offset, reverse);
|
||||
|
||||
List<CrossChainTradeData> crossChainTradesData = new ArrayList<>();
|
||||
for (ATData atData : atsData) {
|
||||
CrossChainTradeData crossChainTradeData = BTCACCT.populateTradeData(repository, atData);
|
||||
crossChainTradesData.add(crossChainTradeData);
|
||||
}
|
||||
|
||||
return crossChainTradesData;
|
||||
} catch (ApiException e) {
|
||||
throw e;
|
||||
} catch (DataException e) {
|
||||
throw ApiExceptionFactory.INSTANCE.createException(request, ApiError.REPOSITORY_ISSUE, e);
|
||||
}
|
||||
}
|
||||
|
||||
@POST
|
||||
@Path("/build")
|
||||
@Operation(
|
||||
summary = "Build cross-chain trading AT",
|
||||
description = "Returns raw, unsigned DEPLOY_AT transaction",
|
||||
requestBody = @RequestBody(
|
||||
required = true,
|
||||
content = @Content(
|
||||
mediaType = MediaType.APPLICATION_JSON,
|
||||
schema = @Schema(
|
||||
implementation = CrossChainBuildRequest.class
|
||||
)
|
||||
)
|
||||
),
|
||||
responses = {
|
||||
@ApiResponse(
|
||||
content = @Content(mediaType = MediaType.TEXT_PLAIN, schema = @Schema(type = "string"))
|
||||
)
|
||||
}
|
||||
)
|
||||
@ApiErrors({ApiError.INVALID_PUBLIC_KEY, ApiError.INVALID_DATA, ApiError.INVALID_REFERENCE, ApiError.TRANSFORMATION_ERROR, ApiError.REPOSITORY_ISSUE})
|
||||
public String buildTrade(CrossChainBuildRequest tradeRequest) {
|
||||
byte[] creatorPublicKey = tradeRequest.creatorPublicKey;
|
||||
|
||||
if (creatorPublicKey == null || creatorPublicKey.length != Transformer.PUBLIC_KEY_LENGTH)
|
||||
throw ApiExceptionFactory.INSTANCE.createException(request, ApiError.INVALID_PUBLIC_KEY);
|
||||
|
||||
if (tradeRequest.secretHash == null || tradeRequest.secretHash.length != BTC.HASH160_LENGTH)
|
||||
throw ApiExceptionFactory.INSTANCE.createException(request, ApiError.INVALID_DATA);
|
||||
|
||||
if (tradeRequest.tradeTimeout == null)
|
||||
tradeRequest.tradeTimeout = 7 * 24 * 60; // 7 days
|
||||
else
|
||||
if (tradeRequest.tradeTimeout < 10 || tradeRequest.tradeTimeout > 50000)
|
||||
throw ApiExceptionFactory.INSTANCE.createException(request, ApiError.INVALID_DATA);
|
||||
|
||||
if (tradeRequest.initialQortAmount < 0)
|
||||
throw ApiExceptionFactory.INSTANCE.createException(request, ApiError.INVALID_DATA);
|
||||
|
||||
if (tradeRequest.finalQortAmount <= 0)
|
||||
throw ApiExceptionFactory.INSTANCE.createException(request, ApiError.INVALID_DATA);
|
||||
|
||||
if (tradeRequest.fundingQortAmount <= 0)
|
||||
throw ApiExceptionFactory.INSTANCE.createException(request, ApiError.INVALID_DATA);
|
||||
|
||||
// funding amount must exceed initial + final
|
||||
if (tradeRequest.fundingQortAmount <= tradeRequest.initialQortAmount + tradeRequest.finalQortAmount)
|
||||
throw ApiExceptionFactory.INSTANCE.createException(request, ApiError.INVALID_DATA);
|
||||
|
||||
if (tradeRequest.bitcoinAmount <= 0)
|
||||
throw ApiExceptionFactory.INSTANCE.createException(request, ApiError.INVALID_DATA);
|
||||
|
||||
try (final Repository repository = RepositoryManager.getRepository()) {
|
||||
PublicKeyAccount creatorAccount = new PublicKeyAccount(repository, creatorPublicKey);
|
||||
|
||||
byte[] creationBytes = BTCACCT.buildQortalAT(creatorAccount.getAddress(), tradeRequest.secretHash, tradeRequest.tradeTimeout, tradeRequest.initialQortAmount, tradeRequest.finalQortAmount, tradeRequest.bitcoinAmount);
|
||||
|
||||
long txTimestamp = NTP.getTime();
|
||||
byte[] lastReference = creatorAccount.getLastReference();
|
||||
if (lastReference == null)
|
||||
throw ApiExceptionFactory.INSTANCE.createException(request, ApiError.INVALID_REFERENCE);
|
||||
|
||||
long fee = 0;
|
||||
String name = "QORT-BTC cross-chain trade";
|
||||
String description = "Qortal-Bitcoin cross-chain trade";
|
||||
String atType = "ACCT";
|
||||
String tags = "QORT-BTC ACCT";
|
||||
|
||||
BaseTransactionData baseTransactionData = new BaseTransactionData(txTimestamp, Group.NO_GROUP, lastReference, creatorAccount.getPublicKey(), fee, null);
|
||||
TransactionData deployAtTransactionData = new DeployAtTransactionData(baseTransactionData, name, description, atType, tags, creationBytes, tradeRequest.fundingQortAmount, Asset.QORT);
|
||||
|
||||
Transaction deployAtTransaction = new DeployAtTransaction(repository, deployAtTransactionData);
|
||||
|
||||
fee = deployAtTransaction.calcRecommendedFee();
|
||||
deployAtTransactionData.setFee(fee);
|
||||
|
||||
ValidationResult result = deployAtTransaction.isValidUnconfirmed();
|
||||
if (result != ValidationResult.OK)
|
||||
throw TransactionsResource.createTransactionInvalidException(request, result);
|
||||
|
||||
byte[] bytes = DeployAtTransactionTransformer.toBytes(deployAtTransactionData);
|
||||
return Base58.encode(bytes);
|
||||
} catch (TransformationException e) {
|
||||
throw ApiExceptionFactory.INSTANCE.createException(request, ApiError.TRANSFORMATION_ERROR, e);
|
||||
} catch (DataException e) {
|
||||
throw ApiExceptionFactory.INSTANCE.createException(request, ApiError.REPOSITORY_ISSUE, e);
|
||||
}
|
||||
}
|
||||
|
||||
@POST
|
||||
@Path("/tradeoffer/recipient")
|
||||
@Operation(
|
||||
summary = "Builds raw, unsigned MESSAGE transaction that sends cross-chain trade recipient address, triggering 'trade' mode",
|
||||
description = "Specify address of cross-chain AT that needs to be messaged, and address of Qortal recipient.<br>"
|
||||
+ "AT needs to be in 'offer' mode. Messages sent to an AT in 'trade' mode will be ignored, but still cost fees to send!<br>"
|
||||
+ "You need to sign output with same account as the AT creator otherwise the MESSAGE transaction will be invalid.",
|
||||
requestBody = @RequestBody(
|
||||
required = true,
|
||||
content = @Content(
|
||||
mediaType = MediaType.APPLICATION_JSON,
|
||||
schema = @Schema(
|
||||
implementation = CrossChainTradeRequest.class
|
||||
)
|
||||
)
|
||||
),
|
||||
responses = {
|
||||
@ApiResponse(
|
||||
content = @Content(
|
||||
schema = @Schema(
|
||||
type = "string"
|
||||
)
|
||||
)
|
||||
)
|
||||
}
|
||||
)
|
||||
@ApiErrors({ApiError.INVALID_PUBLIC_KEY, ApiError.INVALID_ADDRESS, ApiError.INVALID_CRITERIA, ApiError.REPOSITORY_ISSUE})
|
||||
public String sendTradeRecipient(CrossChainTradeRequest tradeRequest) {
|
||||
byte[] creatorPublicKey = tradeRequest.creatorPublicKey;
|
||||
|
||||
if (creatorPublicKey == null || creatorPublicKey.length != Transformer.PUBLIC_KEY_LENGTH)
|
||||
throw ApiExceptionFactory.INSTANCE.createException(request, ApiError.INVALID_PUBLIC_KEY);
|
||||
|
||||
if (tradeRequest.atAddress == null || !Crypto.isValidAtAddress(tradeRequest.atAddress))
|
||||
throw ApiExceptionFactory.INSTANCE.createException(request, ApiError.INVALID_ADDRESS);
|
||||
|
||||
if (tradeRequest.recipient == null || !Crypto.isValidAddress(tradeRequest.recipient))
|
||||
throw ApiExceptionFactory.INSTANCE.createException(request, ApiError.INVALID_ADDRESS);
|
||||
|
||||
try (final Repository repository = RepositoryManager.getRepository()) {
|
||||
ATData atData = fetchAtDataWithChecking(repository, creatorPublicKey, tradeRequest.atAddress);
|
||||
CrossChainTradeData crossChainTradeData = BTCACCT.populateTradeData(repository, atData);
|
||||
|
||||
if (crossChainTradeData.mode == CrossChainTradeData.Mode.TRADE)
|
||||
throw ApiExceptionFactory.INSTANCE.createException(request, ApiError.INVALID_CRITERIA);
|
||||
|
||||
// Good to make MESSAGE
|
||||
|
||||
byte[] recipientAddressBytes = Bytes.ensureCapacity(Base58.decode(tradeRequest.recipient), 32, 0);
|
||||
byte[] messageTransactionBytes = buildAtMessage(repository, creatorPublicKey, tradeRequest.atAddress, recipientAddressBytes);
|
||||
|
||||
return Base58.encode(messageTransactionBytes);
|
||||
} catch (DataException e) {
|
||||
throw ApiExceptionFactory.INSTANCE.createException(request, ApiError.REPOSITORY_ISSUE, e);
|
||||
}
|
||||
}
|
||||
|
||||
@POST
|
||||
@Path("/tradeoffer/secret")
|
||||
@Operation(
|
||||
summary = "Builds raw, unsigned MESSAGE transaction that sends secret to AT, releasing funds to recipient",
|
||||
description = "Specify address of cross-chain AT that needs to be messaged, and 32-byte secret.<br>"
|
||||
+ "AT needs to be in 'trade' mode. Messages sent to an AT in 'trade' mode will be ignored, but still cost fees to send!<br>"
|
||||
+ "You need to sign output with account the AT considers the 'recipient' otherwise the MESSAGE transaction will be invalid.",
|
||||
requestBody = @RequestBody(
|
||||
required = true,
|
||||
content = @Content(
|
||||
mediaType = MediaType.APPLICATION_JSON,
|
||||
schema = @Schema(
|
||||
implementation = CrossChainSecretRequest.class
|
||||
)
|
||||
)
|
||||
),
|
||||
responses = {
|
||||
@ApiResponse(
|
||||
content = @Content(
|
||||
schema = @Schema(
|
||||
type = "string"
|
||||
)
|
||||
)
|
||||
)
|
||||
}
|
||||
)
|
||||
@ApiErrors({ApiError.INVALID_PUBLIC_KEY, ApiError.INVALID_ADDRESS, ApiError.INVALID_DATA, ApiError.INVALID_CRITERIA, ApiError.REPOSITORY_ISSUE})
|
||||
public String sendSecret(CrossChainSecretRequest secretRequest) {
|
||||
byte[] recipientPublicKey = secretRequest.recipientPublicKey;
|
||||
|
||||
if (recipientPublicKey == null || recipientPublicKey.length != Transformer.PUBLIC_KEY_LENGTH)
|
||||
throw ApiExceptionFactory.INSTANCE.createException(request, ApiError.INVALID_PUBLIC_KEY);
|
||||
|
||||
if (secretRequest.atAddress == null || !Crypto.isValidAtAddress(secretRequest.atAddress))
|
||||
throw ApiExceptionFactory.INSTANCE.createException(request, ApiError.INVALID_ADDRESS);
|
||||
|
||||
if (secretRequest.secret == null || secretRequest.secret.length != BTCACCT.SECRET_LENGTH)
|
||||
throw ApiExceptionFactory.INSTANCE.createException(request, ApiError.INVALID_DATA);
|
||||
|
||||
try (final Repository repository = RepositoryManager.getRepository()) {
|
||||
ATData atData = fetchAtDataWithChecking(repository, null, secretRequest.atAddress); // null to skip creator check
|
||||
CrossChainTradeData crossChainTradeData = BTCACCT.populateTradeData(repository, atData);
|
||||
|
||||
if (crossChainTradeData.mode == CrossChainTradeData.Mode.OFFER)
|
||||
throw ApiExceptionFactory.INSTANCE.createException(request, ApiError.INVALID_CRITERIA);
|
||||
|
||||
PublicKeyAccount recipientAccount = new PublicKeyAccount(repository, recipientPublicKey);
|
||||
String recipientAddress = recipientAccount.getAddress();
|
||||
|
||||
// MESSAGE must come from address that AT considers trade partner / 'recipient'
|
||||
if (!crossChainTradeData.qortalRecipient.equals(recipientAddress))
|
||||
throw ApiExceptionFactory.INSTANCE.createException(request, ApiError.INVALID_ADDRESS);
|
||||
|
||||
// Good to make MESSAGE
|
||||
|
||||
byte[] messageTransactionBytes = buildAtMessage(repository, recipientPublicKey, secretRequest.atAddress, secretRequest.secret);
|
||||
|
||||
return Base58.encode(messageTransactionBytes);
|
||||
} catch (DataException e) {
|
||||
throw ApiExceptionFactory.INSTANCE.createException(request, ApiError.REPOSITORY_ISSUE, e);
|
||||
}
|
||||
}
|
||||
|
||||
@DELETE
|
||||
@Path("/tradeoffer")
|
||||
@Operation(
|
||||
summary = "Builds raw, unsigned MESSAGE transaction that cancels cross-chain trade offer",
|
||||
description = "Specify address of cross-chain AT that needs to be cancelled.<br>"
|
||||
+ "AT needs to be in 'offer' mode. Messages sent to an AT in 'trade' mode will be ignored, but still cost fees to send!<br>"
|
||||
+ "You need to sign output with same account as the AT creator otherwise the MESSAGE transaction will be invalid.",
|
||||
requestBody = @RequestBody(
|
||||
required = true,
|
||||
content = @Content(
|
||||
mediaType = MediaType.APPLICATION_JSON,
|
||||
schema = @Schema(
|
||||
implementation = CrossChainCancelRequest.class
|
||||
)
|
||||
)
|
||||
),
|
||||
responses = {
|
||||
@ApiResponse(
|
||||
content = @Content(
|
||||
schema = @Schema(
|
||||
type = "string"
|
||||
)
|
||||
)
|
||||
)
|
||||
}
|
||||
)
|
||||
@ApiErrors({ApiError.INVALID_PUBLIC_KEY, ApiError.INVALID_ADDRESS, ApiError.INVALID_CRITERIA, ApiError.REPOSITORY_ISSUE})
|
||||
public String cancelTradeOffer(CrossChainCancelRequest cancelRequest) {
|
||||
byte[] creatorPublicKey = cancelRequest.creatorPublicKey;
|
||||
|
||||
if (creatorPublicKey == null || creatorPublicKey.length != Transformer.PUBLIC_KEY_LENGTH)
|
||||
throw ApiExceptionFactory.INSTANCE.createException(request, ApiError.INVALID_PUBLIC_KEY);
|
||||
|
||||
if (cancelRequest.atAddress == null || !Crypto.isValidAtAddress(cancelRequest.atAddress))
|
||||
throw ApiExceptionFactory.INSTANCE.createException(request, ApiError.INVALID_ADDRESS);
|
||||
|
||||
try (final Repository repository = RepositoryManager.getRepository()) {
|
||||
ATData atData = fetchAtDataWithChecking(repository, creatorPublicKey, cancelRequest.atAddress);
|
||||
CrossChainTradeData crossChainTradeData = BTCACCT.populateTradeData(repository, atData);
|
||||
|
||||
if (crossChainTradeData.mode == CrossChainTradeData.Mode.TRADE)
|
||||
throw ApiExceptionFactory.INSTANCE.createException(request, ApiError.INVALID_CRITERIA);
|
||||
|
||||
// Good to make MESSAGE
|
||||
|
||||
PublicKeyAccount creatorAccount = new PublicKeyAccount(repository, creatorPublicKey);
|
||||
String creatorAddress = creatorAccount.getAddress();
|
||||
byte[] recipientAddressBytes = Bytes.ensureCapacity(Base58.decode(creatorAddress), 32, 0);
|
||||
|
||||
byte[] messageTransactionBytes = buildAtMessage(repository, creatorPublicKey, cancelRequest.atAddress, recipientAddressBytes);
|
||||
|
||||
return Base58.encode(messageTransactionBytes);
|
||||
} catch (DataException e) {
|
||||
throw ApiExceptionFactory.INSTANCE.createException(request, ApiError.REPOSITORY_ISSUE, e);
|
||||
}
|
||||
}
|
||||
|
||||
@POST
|
||||
@Path("/p2sh")
|
||||
@Operation(
|
||||
summary = "Returns Bitcoin P2SH address based on trade info",
|
||||
requestBody = @RequestBody(
|
||||
required = true,
|
||||
content = @Content(
|
||||
mediaType = MediaType.APPLICATION_JSON,
|
||||
schema = @Schema(
|
||||
implementation = CrossChainBitcoinTemplateRequest.class
|
||||
)
|
||||
)
|
||||
),
|
||||
responses = {
|
||||
@ApiResponse(
|
||||
content = @Content(mediaType = MediaType.TEXT_PLAIN, schema = @Schema(type = "string"))
|
||||
)
|
||||
}
|
||||
)
|
||||
@ApiErrors({ApiError.INVALID_PUBLIC_KEY, ApiError.INVALID_ADDRESS, ApiError.REPOSITORY_ISSUE})
|
||||
public String deriveP2sh(CrossChainBitcoinTemplateRequest templateRequest) {
|
||||
BTC btc = BTC.getInstance();
|
||||
NetworkParameters params = btc.getNetworkParameters();
|
||||
|
||||
if (templateRequest.refundPublicKeyHash == null || templateRequest.refundPublicKeyHash.length != 20)
|
||||
throw ApiExceptionFactory.INSTANCE.createException(request, ApiError.INVALID_PUBLIC_KEY);
|
||||
|
||||
if (templateRequest.redeemPublicKeyHash == null || templateRequest.redeemPublicKeyHash.length != 20)
|
||||
throw ApiExceptionFactory.INSTANCE.createException(request, ApiError.INVALID_PUBLIC_KEY);
|
||||
|
||||
if (templateRequest.atAddress == null || !Crypto.isValidAtAddress(templateRequest.atAddress))
|
||||
throw ApiExceptionFactory.INSTANCE.createException(request, ApiError.INVALID_ADDRESS);
|
||||
|
||||
// Extract data from cross-chain trading AT
|
||||
try (final Repository repository = RepositoryManager.getRepository()) {
|
||||
ATData atData = fetchAtDataWithChecking(repository, null, templateRequest.atAddress); // null to skip creator check
|
||||
CrossChainTradeData crossChainTradeData = BTCACCT.populateTradeData(repository, atData);
|
||||
|
||||
if (crossChainTradeData.mode == Mode.OFFER)
|
||||
throw ApiExceptionFactory.INSTANCE.createException(request, ApiError.INVALID_CRITERIA);
|
||||
|
||||
byte[] redeemScriptBytes = BTCACCT.buildScript(templateRequest.refundPublicKeyHash, crossChainTradeData.lockTime, templateRequest.redeemPublicKeyHash, crossChainTradeData.secretHash);
|
||||
byte[] redeemScriptHash = Crypto.hash160(redeemScriptBytes);
|
||||
|
||||
Address p2shAddress = LegacyAddress.fromScriptHash(params, redeemScriptHash);
|
||||
return p2shAddress.toString();
|
||||
} catch (DataException e) {
|
||||
throw ApiExceptionFactory.INSTANCE.createException(request, ApiError.REPOSITORY_ISSUE, e);
|
||||
}
|
||||
}
|
||||
|
||||
@POST
|
||||
@Path("/p2sh/check")
|
||||
@Operation(
|
||||
summary = "Checks Bitcoin P2SH address based on trade info",
|
||||
requestBody = @RequestBody(
|
||||
required = true,
|
||||
content = @Content(
|
||||
mediaType = MediaType.APPLICATION_JSON,
|
||||
schema = @Schema(
|
||||
implementation = CrossChainBitcoinTemplateRequest.class
|
||||
)
|
||||
)
|
||||
),
|
||||
responses = {
|
||||
@ApiResponse(
|
||||
content = @Content(mediaType = MediaType.APPLICATION_JSON, schema = @Schema(implementation = CrossChainBitcoinP2SHStatus.class))
|
||||
)
|
||||
}
|
||||
)
|
||||
@ApiErrors({ApiError.INVALID_PUBLIC_KEY, ApiError.INVALID_ADDRESS, ApiError.ADDRESS_UNKNOWN, ApiError.REPOSITORY_ISSUE})
|
||||
public CrossChainBitcoinP2SHStatus checkP2sh(CrossChainBitcoinTemplateRequest templateRequest) {
|
||||
BTC btc = BTC.getInstance();
|
||||
NetworkParameters params = btc.getNetworkParameters();
|
||||
|
||||
if (templateRequest.refundPublicKeyHash == null || templateRequest.refundPublicKeyHash.length != 20)
|
||||
throw ApiExceptionFactory.INSTANCE.createException(request, ApiError.INVALID_PUBLIC_KEY);
|
||||
|
||||
if (templateRequest.redeemPublicKeyHash == null || templateRequest.redeemPublicKeyHash.length != 20)
|
||||
throw ApiExceptionFactory.INSTANCE.createException(request, ApiError.INVALID_PUBLIC_KEY);
|
||||
|
||||
if (templateRequest.atAddress == null || !Crypto.isValidAtAddress(templateRequest.atAddress))
|
||||
throw ApiExceptionFactory.INSTANCE.createException(request, ApiError.INVALID_ADDRESS);
|
||||
|
||||
// Extract data from cross-chain trading AT
|
||||
try (final Repository repository = RepositoryManager.getRepository()) {
|
||||
ATData atData = fetchAtDataWithChecking(repository, null, templateRequest.atAddress); // null to skip creator check
|
||||
CrossChainTradeData crossChainTradeData = BTCACCT.populateTradeData(repository, atData);
|
||||
|
||||
if (crossChainTradeData.mode == Mode.OFFER)
|
||||
throw ApiExceptionFactory.INSTANCE.createException(request, ApiError.INVALID_CRITERIA);
|
||||
|
||||
byte[] redeemScriptBytes = BTCACCT.buildScript(templateRequest.refundPublicKeyHash, crossChainTradeData.lockTime, templateRequest.redeemPublicKeyHash, crossChainTradeData.secretHash);
|
||||
byte[] redeemScriptHash = Crypto.hash160(redeemScriptBytes);
|
||||
|
||||
Address p2shAddress = LegacyAddress.fromScriptHash(params, redeemScriptHash);
|
||||
|
||||
Integer medianBlockTime = BTC.getInstance().getMedianBlockTime();
|
||||
if (medianBlockTime == null)
|
||||
throw ApiExceptionFactory.INSTANCE.createException(request, ApiError.BTC_NETWORK_ISSUE);
|
||||
|
||||
long now = NTP.getTime();
|
||||
|
||||
// Check P2SH is funded
|
||||
|
||||
Coin p2shBalance = BTC.getInstance().getBalance(p2shAddress.toString());
|
||||
if (p2shBalance == null)
|
||||
throw ApiExceptionFactory.INSTANCE.createException(request, ApiError.ADDRESS_UNKNOWN);
|
||||
|
||||
CrossChainBitcoinP2SHStatus p2shStatus = new CrossChainBitcoinP2SHStatus();
|
||||
p2shStatus.bitcoinP2shAddress = p2shAddress.toString();
|
||||
p2shStatus.bitcoinP2shBalance = BigDecimal.valueOf(p2shBalance.value, 8);
|
||||
|
||||
List<TransactionOutput> fundingOutputs = BTC.getInstance().getUnspentOutputs(p2shAddress.toString());
|
||||
|
||||
if (p2shBalance.value >= crossChainTradeData.expectedBitcoin && !fundingOutputs.isEmpty()) {
|
||||
p2shStatus.canRedeem = now >= medianBlockTime * 1000L;
|
||||
p2shStatus.canRefund = now >= crossChainTradeData.lockTime * 1000L;
|
||||
}
|
||||
|
||||
if (now >= medianBlockTime * 1000L) {
|
||||
// See if we can extract secret
|
||||
List<byte[]> rawTransactions = BTC.getInstance().getAddressTransactions(p2shStatus.bitcoinP2shAddress);
|
||||
p2shStatus.secret = BTCACCT.findP2shSecret(p2shStatus.bitcoinP2shAddress, rawTransactions);
|
||||
}
|
||||
|
||||
return p2shStatus;
|
||||
} catch (DataException e) {
|
||||
throw ApiExceptionFactory.INSTANCE.createException(request, ApiError.REPOSITORY_ISSUE, e);
|
||||
}
|
||||
}
|
||||
|
||||
@POST
|
||||
@Path("/p2sh/refund")
|
||||
@Operation(
|
||||
summary = "Returns serialized Bitcoin transaction attempting refund from P2SH address",
|
||||
requestBody = @RequestBody(
|
||||
required = true,
|
||||
content = @Content(
|
||||
mediaType = MediaType.APPLICATION_JSON,
|
||||
schema = @Schema(
|
||||
implementation = CrossChainBitcoinRefundRequest.class
|
||||
)
|
||||
)
|
||||
),
|
||||
responses = {
|
||||
@ApiResponse(
|
||||
content = @Content(mediaType = MediaType.TEXT_PLAIN, schema = @Schema(type = "string"))
|
||||
)
|
||||
}
|
||||
)
|
||||
@ApiErrors({ApiError.INVALID_PUBLIC_KEY, ApiError.INVALID_ADDRESS, ApiError.ADDRESS_UNKNOWN,
|
||||
ApiError.BTC_TOO_SOON, ApiError.BTC_BALANCE_ISSUE, ApiError.BTC_NETWORK_ISSUE, ApiError.REPOSITORY_ISSUE})
|
||||
public String refundP2sh(CrossChainBitcoinRefundRequest refundRequest) {
|
||||
BTC btc = BTC.getInstance();
|
||||
NetworkParameters params = btc.getNetworkParameters();
|
||||
|
||||
byte[] refundPrivateKey = refundRequest.refundPrivateKey;
|
||||
if (refundPrivateKey == null)
|
||||
throw ApiExceptionFactory.INSTANCE.createException(request, ApiError.INVALID_PRIVATE_KEY);
|
||||
|
||||
ECKey refundKey = null;
|
||||
|
||||
try {
|
||||
// Auto-trim
|
||||
if (refundPrivateKey.length >= 37 && refundPrivateKey.length <= 38)
|
||||
refundPrivateKey = Arrays.copyOfRange(refundPrivateKey, 1, 33);
|
||||
if (refundPrivateKey.length != 32)
|
||||
throw ApiExceptionFactory.INSTANCE.createException(request, ApiError.INVALID_PRIVATE_KEY);
|
||||
|
||||
refundKey = ECKey.fromPrivate(refundPrivateKey);
|
||||
} catch (IllegalArgumentException e) {
|
||||
throw ApiExceptionFactory.INSTANCE.createException(request, ApiError.INVALID_PRIVATE_KEY);
|
||||
}
|
||||
|
||||
if (refundRequest.redeemPublicKeyHash == null || refundRequest.redeemPublicKeyHash.length != 20)
|
||||
throw ApiExceptionFactory.INSTANCE.createException(request, ApiError.INVALID_PUBLIC_KEY);
|
||||
|
||||
if (refundRequest.atAddress == null || !Crypto.isValidAtAddress(refundRequest.atAddress))
|
||||
throw ApiExceptionFactory.INSTANCE.createException(request, ApiError.INVALID_ADDRESS);
|
||||
|
||||
// Extract data from cross-chain trading AT
|
||||
try (final Repository repository = RepositoryManager.getRepository()) {
|
||||
ATData atData = fetchAtDataWithChecking(repository, null, refundRequest.atAddress); // null to skip creator check
|
||||
CrossChainTradeData crossChainTradeData = BTCACCT.populateTradeData(repository, atData);
|
||||
|
||||
if (crossChainTradeData.mode == Mode.OFFER)
|
||||
throw ApiExceptionFactory.INSTANCE.createException(request, ApiError.INVALID_CRITERIA);
|
||||
|
||||
byte[] redeemScriptBytes = BTCACCT.buildScript(refundKey.getPubKeyHash(), crossChainTradeData.lockTime, refundRequest.redeemPublicKeyHash, crossChainTradeData.secretHash);
|
||||
byte[] redeemScriptHash = Crypto.hash160(redeemScriptBytes);
|
||||
|
||||
Address p2shAddress = LegacyAddress.fromScriptHash(params, redeemScriptHash);
|
||||
|
||||
long now = NTP.getTime();
|
||||
|
||||
// Check P2SH is funded
|
||||
|
||||
Coin p2shBalance = BTC.getInstance().getBalance(p2shAddress.toString());
|
||||
if (p2shBalance == null)
|
||||
throw ApiExceptionFactory.INSTANCE.createException(request, ApiError.ADDRESS_UNKNOWN);
|
||||
|
||||
List<TransactionOutput> fundingOutputs = BTC.getInstance().getUnspentOutputs(p2shAddress.toString());
|
||||
if (fundingOutputs.isEmpty())
|
||||
throw ApiExceptionFactory.INSTANCE.createException(request, ApiError.INVALID_ADDRESS);
|
||||
|
||||
boolean canRefund = now >= crossChainTradeData.lockTime * 1000L;
|
||||
if (!canRefund)
|
||||
throw ApiExceptionFactory.INSTANCE.createException(request, ApiError.BTC_TOO_SOON);
|
||||
|
||||
if (p2shBalance.value < crossChainTradeData.expectedBitcoin)
|
||||
throw ApiExceptionFactory.INSTANCE.createException(request, ApiError.BTC_BALANCE_ISSUE);
|
||||
|
||||
Coin refundAmount = p2shBalance.subtract(Coin.valueOf(refundRequest.bitcoinMinerFee.unscaledValue().longValue()));
|
||||
|
||||
org.bitcoinj.core.Transaction refundTransaction = BTCACCT.buildRefundTransaction(refundAmount, refundKey, fundingOutputs, redeemScriptBytes, crossChainTradeData.lockTime);
|
||||
boolean wasBroadcast = BTC.getInstance().broadcastTransaction(refundTransaction);
|
||||
|
||||
if (!wasBroadcast)
|
||||
throw ApiExceptionFactory.INSTANCE.createException(request, ApiError.BTC_NETWORK_ISSUE);
|
||||
|
||||
return refundTransaction.getTxId().toString();
|
||||
} catch (DataException e) {
|
||||
throw ApiExceptionFactory.INSTANCE.createException(request, ApiError.REPOSITORY_ISSUE, e);
|
||||
}
|
||||
}
|
||||
|
||||
@POST
|
||||
@Path("/p2sh/redeem")
|
||||
@Operation(
|
||||
summary = "Returns serialized Bitcoin transaction attempting redeem from P2SH address",
|
||||
requestBody = @RequestBody(
|
||||
required = true,
|
||||
content = @Content(
|
||||
mediaType = MediaType.APPLICATION_JSON,
|
||||
schema = @Schema(
|
||||
implementation = CrossChainBitcoinRedeemRequest.class
|
||||
)
|
||||
)
|
||||
),
|
||||
responses = {
|
||||
@ApiResponse(
|
||||
content = @Content(mediaType = MediaType.TEXT_PLAIN, schema = @Schema(type = "string"))
|
||||
)
|
||||
}
|
||||
)
|
||||
@ApiErrors({ApiError.INVALID_PUBLIC_KEY, ApiError.INVALID_ADDRESS, ApiError.ADDRESS_UNKNOWN,
|
||||
ApiError.BTC_TOO_SOON, ApiError.BTC_BALANCE_ISSUE, ApiError.BTC_NETWORK_ISSUE, ApiError.REPOSITORY_ISSUE})
|
||||
public String redeemP2sh(CrossChainBitcoinRedeemRequest redeemRequest) {
|
||||
BTC btc = BTC.getInstance();
|
||||
NetworkParameters params = btc.getNetworkParameters();
|
||||
|
||||
byte[] redeemPrivateKey = redeemRequest.redeemPrivateKey;
|
||||
if (redeemPrivateKey == null)
|
||||
throw ApiExceptionFactory.INSTANCE.createException(request, ApiError.INVALID_PRIVATE_KEY);
|
||||
|
||||
ECKey redeemKey = null;
|
||||
|
||||
try {
|
||||
// Auto-trim
|
||||
if (redeemPrivateKey.length >= 37 && redeemPrivateKey.length <= 38)
|
||||
redeemPrivateKey = Arrays.copyOfRange(redeemPrivateKey, 1, 33);
|
||||
if (redeemPrivateKey.length != 32)
|
||||
throw ApiExceptionFactory.INSTANCE.createException(request, ApiError.INVALID_PRIVATE_KEY);
|
||||
|
||||
redeemKey = ECKey.fromPrivate(redeemPrivateKey);
|
||||
} catch (IllegalArgumentException e) {
|
||||
throw ApiExceptionFactory.INSTANCE.createException(request, ApiError.INVALID_PRIVATE_KEY);
|
||||
}
|
||||
|
||||
if (redeemRequest.refundPublicKeyHash == null || redeemRequest.refundPublicKeyHash.length != 20)
|
||||
throw ApiExceptionFactory.INSTANCE.createException(request, ApiError.INVALID_PUBLIC_KEY);
|
||||
|
||||
if (redeemRequest.atAddress == null || !Crypto.isValidAtAddress(redeemRequest.atAddress))
|
||||
throw ApiExceptionFactory.INSTANCE.createException(request, ApiError.INVALID_ADDRESS);
|
||||
|
||||
if (redeemRequest.secret == null || redeemRequest.secret.length != BTCACCT.SECRET_LENGTH)
|
||||
throw ApiExceptionFactory.INSTANCE.createException(request, ApiError.INVALID_DATA);
|
||||
|
||||
// Extract data from cross-chain trading AT
|
||||
try (final Repository repository = RepositoryManager.getRepository()) {
|
||||
ATData atData = fetchAtDataWithChecking(repository, null, redeemRequest.atAddress); // null to skip creator check
|
||||
CrossChainTradeData crossChainTradeData = BTCACCT.populateTradeData(repository, atData);
|
||||
|
||||
if (crossChainTradeData.mode == Mode.OFFER)
|
||||
throw ApiExceptionFactory.INSTANCE.createException(request, ApiError.INVALID_CRITERIA);
|
||||
|
||||
byte[] redeemScriptBytes = BTCACCT.buildScript(redeemRequest.refundPublicKeyHash, crossChainTradeData.lockTime, redeemKey.getPubKeyHash(), crossChainTradeData.secretHash);
|
||||
byte[] redeemScriptHash = Crypto.hash160(redeemScriptBytes);
|
||||
|
||||
Address p2shAddress = LegacyAddress.fromScriptHash(params, redeemScriptHash);
|
||||
|
||||
Integer medianBlockTime = BTC.getInstance().getMedianBlockTime();
|
||||
if (medianBlockTime == null)
|
||||
throw ApiExceptionFactory.INSTANCE.createException(request, ApiError.BTC_NETWORK_ISSUE);
|
||||
|
||||
long now = NTP.getTime();
|
||||
|
||||
// Check P2SH is funded
|
||||
Coin p2shBalance = BTC.getInstance().getBalance(p2shAddress.toString());
|
||||
if (p2shBalance == null)
|
||||
throw ApiExceptionFactory.INSTANCE.createException(request, ApiError.ADDRESS_UNKNOWN);
|
||||
|
||||
if (p2shBalance.value < crossChainTradeData.expectedBitcoin)
|
||||
throw ApiExceptionFactory.INSTANCE.createException(request, ApiError.BTC_BALANCE_ISSUE);
|
||||
|
||||
List<TransactionOutput> fundingOutputs = BTC.getInstance().getUnspentOutputs(p2shAddress.toString());
|
||||
if (fundingOutputs.isEmpty())
|
||||
throw ApiExceptionFactory.INSTANCE.createException(request, ApiError.INVALID_ADDRESS);
|
||||
|
||||
boolean canRedeem = now >= medianBlockTime * 1000L;
|
||||
if (!canRedeem)
|
||||
throw ApiExceptionFactory.INSTANCE.createException(request, ApiError.BTC_TOO_SOON);
|
||||
|
||||
Coin redeemAmount = p2shBalance.subtract(Coin.valueOf(redeemRequest.bitcoinMinerFee.unscaledValue().longValue()));
|
||||
|
||||
org.bitcoinj.core.Transaction redeemTransaction = BTCACCT.buildRedeemTransaction(redeemAmount, redeemKey, fundingOutputs, redeemScriptBytes, redeemRequest.secret);
|
||||
boolean wasBroadcast = BTC.getInstance().broadcastTransaction(redeemTransaction);
|
||||
|
||||
if (!wasBroadcast)
|
||||
throw ApiExceptionFactory.INSTANCE.createException(request, ApiError.BTC_NETWORK_ISSUE);
|
||||
|
||||
return redeemTransaction.getTxId().toString();
|
||||
} catch (DataException e) {
|
||||
throw ApiExceptionFactory.INSTANCE.createException(request, ApiError.REPOSITORY_ISSUE, e);
|
||||
}
|
||||
}
|
||||
|
||||
private ATData fetchAtDataWithChecking(Repository repository, byte[] creatorPublicKey, String atAddress) throws DataException {
|
||||
ATData atData = repository.getATRepository().fromATAddress(atAddress);
|
||||
if (atData == null)
|
||||
throw ApiExceptionFactory.INSTANCE.createException(request, ApiError.ADDRESS_UNKNOWN);
|
||||
|
||||
// Does supplied public key match that of AT?
|
||||
if (creatorPublicKey != null && !Arrays.equals(creatorPublicKey, atData.getCreatorPublicKey()))
|
||||
throw ApiExceptionFactory.INSTANCE.createException(request, ApiError.INVALID_PUBLIC_KEY);
|
||||
|
||||
// Must be correct AT - check functionality using code hash
|
||||
if (!Arrays.equals(atData.getCodeHash(), BTCACCT.CODE_BYTES_HASH))
|
||||
throw ApiExceptionFactory.INSTANCE.createException(request, ApiError.INVALID_CRITERIA);
|
||||
|
||||
// No point sending message to AT that's finished
|
||||
if (atData.getIsFinished())
|
||||
throw ApiExceptionFactory.INSTANCE.createException(request, ApiError.INVALID_CRITERIA);
|
||||
|
||||
return atData;
|
||||
}
|
||||
|
||||
private byte[] buildAtMessage(Repository repository, byte[] senderPublicKey, String atAddress, byte[] messageData) throws DataException {
|
||||
PublicKeyAccount creatorAccount = new PublicKeyAccount(repository, senderPublicKey);
|
||||
|
||||
long txTimestamp = NTP.getTime();
|
||||
byte[] lastReference = creatorAccount.getLastReference();
|
||||
|
||||
if (lastReference == null)
|
||||
throw ApiExceptionFactory.INSTANCE.createException(request, ApiError.INVALID_REFERENCE);
|
||||
|
||||
int version = 4;
|
||||
int nonce = 0;
|
||||
long amount = 0L;
|
||||
Long assetId = null; // no assetId as amount is zero
|
||||
Long fee = null;
|
||||
|
||||
BaseTransactionData baseTransactionData = new BaseTransactionData(txTimestamp, Group.NO_GROUP, lastReference, senderPublicKey, fee, null);
|
||||
TransactionData messageTransactionData = new MessageTransactionData(baseTransactionData, version, nonce, atAddress, amount, assetId, messageData, false, false);
|
||||
|
||||
MessageTransaction messageTransaction = new MessageTransaction(repository, messageTransactionData);
|
||||
|
||||
fee = messageTransaction.calcRecommendedFee();
|
||||
messageTransactionData.setFee(fee);
|
||||
|
||||
ValidationResult result = messageTransaction.isValidUnconfirmed();
|
||||
if (result != ValidationResult.OK)
|
||||
throw TransactionsResource.createTransactionInvalidException(request, result);
|
||||
|
||||
try {
|
||||
return MessageTransactionTransformer.toBytes(messageTransactionData);
|
||||
} catch (TransformationException e) {
|
||||
throw ApiExceptionFactory.INSTANCE.createException(request, ApiError.TRANSFORMATION_ERROR, e);
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
@@ -8,6 +8,7 @@ import io.swagger.v3.oas.annotations.parameters.RequestBody;
|
||||
import io.swagger.v3.oas.annotations.responses.ApiResponse;
|
||||
import io.swagger.v3.oas.annotations.tags.Tag;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
import java.util.stream.Collectors;
|
||||
|
||||
@@ -29,9 +30,8 @@ import org.qortal.data.network.PeerData;
|
||||
import org.qortal.network.Network;
|
||||
import org.qortal.network.PeerAddress;
|
||||
import org.qortal.repository.DataException;
|
||||
import org.qortal.repository.Repository;
|
||||
import org.qortal.repository.RepositoryManager;
|
||||
import org.qortal.utils.ExecuteProduceConsume;
|
||||
import org.qortal.utils.NTP;
|
||||
|
||||
@Path("/peers")
|
||||
@Tag(name = "Peers")
|
||||
@@ -81,11 +81,7 @@ public class PeersResource {
|
||||
ApiError.REPOSITORY_ISSUE
|
||||
})
|
||||
public List<PeerData> getKnownPeers() {
|
||||
try (final Repository repository = RepositoryManager.getRepository()) {
|
||||
return repository.getNetworkRepository().getAllPeers();
|
||||
} catch (DataException e) {
|
||||
throw ApiExceptionFactory.INSTANCE.createException(request, ApiError.REPOSITORY_ISSUE, e);
|
||||
}
|
||||
return Network.getInstance().getAllKnownPeers();
|
||||
}
|
||||
|
||||
@GET
|
||||
@@ -166,18 +162,21 @@ public class PeersResource {
|
||||
public String addPeer(String address) {
|
||||
Security.checkApiCallAllowed(request);
|
||||
|
||||
try (final Repository repository = RepositoryManager.getRepository()) {
|
||||
final Long addedWhen = NTP.getTime();
|
||||
if (addedWhen == null)
|
||||
throw ApiExceptionFactory.INSTANCE.createException(request, ApiError.NO_TIME_SYNC);
|
||||
|
||||
try {
|
||||
PeerAddress peerAddress = PeerAddress.fromString(address);
|
||||
|
||||
PeerData peerData = new PeerData(peerAddress, System.currentTimeMillis(), "API");
|
||||
repository.getNetworkRepository().save(peerData);
|
||||
repository.saveChanges();
|
||||
List<PeerAddress> newPeerAddresses = new ArrayList<>(1);
|
||||
newPeerAddresses.add(peerAddress);
|
||||
|
||||
return "true";
|
||||
boolean addResult = Network.getInstance().mergePeers("API", addedWhen, newPeerAddresses);
|
||||
|
||||
return addResult ? "true" : "false";
|
||||
} catch (IllegalArgumentException e) {
|
||||
throw ApiExceptionFactory.INSTANCE.createException(request, ApiError.INVALID_NETWORK_ADDRESS);
|
||||
} catch (ApiException e) {
|
||||
throw e;
|
||||
} catch (DataException e) {
|
||||
throw ApiExceptionFactory.INSTANCE.createException(request, ApiError.REPOSITORY_ISSUE, e);
|
||||
}
|
||||
|
||||
@@ -363,6 +363,60 @@ public class TransactionsResource {
|
||||
}
|
||||
}
|
||||
|
||||
@GET
|
||||
@Path("/creator/{publickey}")
|
||||
@Operation(
|
||||
summary = "Find matching transactions created by account with given public key",
|
||||
responses = {
|
||||
@ApiResponse(
|
||||
description = "transactions",
|
||||
content = @Content(
|
||||
array = @ArraySchema(
|
||||
schema = @Schema(
|
||||
implementation = TransactionData.class
|
||||
)
|
||||
)
|
||||
)
|
||||
)
|
||||
}
|
||||
)
|
||||
@ApiErrors({
|
||||
ApiError.INVALID_CRITERIA, ApiError.REPOSITORY_ISSUE
|
||||
})
|
||||
public List<TransactionData> findCreatorsTransactions(@PathParam("publickey") String publicKey58,
|
||||
@Parameter(
|
||||
description = "whether to include confirmed, unconfirmed or both",
|
||||
required = true
|
||||
) @QueryParam("confirmationStatus") ConfirmationStatus confirmationStatus, @Parameter(
|
||||
ref = "limit"
|
||||
) @QueryParam("limit") Integer limit, @Parameter(
|
||||
ref = "offset"
|
||||
) @QueryParam("offset") Integer offset, @Parameter(
|
||||
ref = "reverse"
|
||||
) @QueryParam("reverse") Boolean reverse) {
|
||||
// Decode public key
|
||||
byte[] publicKey;
|
||||
try {
|
||||
publicKey = Base58.decode(publicKey58);
|
||||
} catch (NumberFormatException e) {
|
||||
throw ApiExceptionFactory.INSTANCE.createException(request, ApiError.INVALID_PUBLIC_KEY, e);
|
||||
}
|
||||
|
||||
try (final Repository repository = RepositoryManager.getRepository()) {
|
||||
List<byte[]> signatures = repository.getTransactionRepository().getSignaturesMatchingCriteria(null,
|
||||
publicKey, confirmationStatus, limit, offset, reverse);
|
||||
|
||||
// Expand signatures to transactions
|
||||
List<TransactionData> transactions = new ArrayList<>(signatures.size());
|
||||
for (byte[] signature : signatures)
|
||||
transactions.add(repository.getTransactionRepository().fromSignature(signature));
|
||||
|
||||
return transactions;
|
||||
} catch (DataException e) {
|
||||
throw ApiExceptionFactory.INSTANCE.createException(request, ApiError.REPOSITORY_ISSUE, e);
|
||||
}
|
||||
}
|
||||
|
||||
@POST
|
||||
@Path("/sign")
|
||||
@Operation(
|
||||
@@ -477,14 +531,14 @@ public class TransactionsResource {
|
||||
ValidationResult result = transaction.importAsUnconfirmed();
|
||||
if (result != ValidationResult.OK)
|
||||
throw createTransactionInvalidException(request, result);
|
||||
|
||||
// Notify controller of new transaction
|
||||
Controller.getInstance().onNewTransaction(transactionData);
|
||||
|
||||
return "true";
|
||||
} finally {
|
||||
blockchainLock.unlock();
|
||||
}
|
||||
|
||||
// Notify controller of new transaction
|
||||
Controller.getInstance().onNewTransaction(transactionData, null);
|
||||
|
||||
return "true";
|
||||
} catch (NumberFormatException e) {
|
||||
throw ApiExceptionFactory.INSTANCE.createException(request, ApiError.INVALID_DATA, e);
|
||||
} catch (TransformationException e) {
|
||||
|
||||
@@ -0,0 +1,93 @@
|
||||
package org.qortal.api.websocket;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.io.StringWriter;
|
||||
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;
|
||||
import org.qortal.data.chat.ActiveChats;
|
||||
import org.qortal.data.transaction.ChatTransactionData;
|
||||
import org.qortal.repository.DataException;
|
||||
import org.qortal.repository.Repository;
|
||||
import org.qortal.repository.RepositoryManager;
|
||||
|
||||
@WebSocket
|
||||
@SuppressWarnings("serial")
|
||||
public class ActiveChatsWebSocket extends WebSocketServlet implements ApiWebSocket {
|
||||
|
||||
@Override
|
||||
public void configure(WebSocketServletFactory factory) {
|
||||
factory.register(ActiveChatsWebSocket.class);
|
||||
}
|
||||
|
||||
@OnWebSocketConnect
|
||||
public void onWebSocketConnect(Session session) {
|
||||
Map<String, String> pathParams = this.getPathParams(session, "/{address}");
|
||||
|
||||
String address = pathParams.get("address");
|
||||
if (address == null || !Crypto.isValidAddress(address)) {
|
||||
session.close(4001, "invalid address");
|
||||
return;
|
||||
}
|
||||
|
||||
AtomicReference<String> previousOutput = new AtomicReference<>(null);
|
||||
|
||||
ChatNotifier.Listener listener = chatTransactionData -> onNotify(session, chatTransactionData, address, previousOutput);
|
||||
ChatNotifier.getInstance().register(session, listener);
|
||||
|
||||
this.onNotify(session, null, address, previousOutput);
|
||||
}
|
||||
|
||||
@OnWebSocketClose
|
||||
public void onWebSocketClose(Session session, int statusCode, String reason) {
|
||||
ChatNotifier.getInstance().deregister(session);
|
||||
}
|
||||
|
||||
@OnWebSocketError
|
||||
public void onWebSocketError(Session session, Throwable throwable) {
|
||||
}
|
||||
|
||||
@OnWebSocketMessage
|
||||
public void onWebSocketMessage(Session session, String message) {
|
||||
}
|
||||
|
||||
private void onNotify(Session session, ChatTransactionData chatTransactionData, String ourAddress, AtomicReference<String> previousOutput) {
|
||||
// If CHAT has a recipient (i.e. direct message, not group-based) and we're neither sender nor recipient, then it's of no interest
|
||||
if (chatTransactionData != null) {
|
||||
String recipient = chatTransactionData.getRecipient();
|
||||
|
||||
if (recipient != null && (!recipient.equals(ourAddress) && !chatTransactionData.getSender().equals(ourAddress)))
|
||||
return;
|
||||
}
|
||||
|
||||
try (final Repository repository = RepositoryManager.getRepository()) {
|
||||
ActiveChats activeChats = repository.getChatRepository().getActiveChats(ourAddress);
|
||||
|
||||
StringWriter stringWriter = new StringWriter();
|
||||
|
||||
this.marshall(stringWriter, activeChats);
|
||||
|
||||
// Only output if something has changed
|
||||
String output = stringWriter.toString();
|
||||
if (output.equals(previousOutput.get()))
|
||||
return;
|
||||
|
||||
previousOutput.set(output);
|
||||
session.getRemote().sendStringByFuture(output);
|
||||
} catch (DataException | IOException | WebSocketException e) {
|
||||
// No output this time?
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,74 @@
|
||||
package org.qortal.api.websocket;
|
||||
|
||||
import java.io.IOException;
|
||||
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;
|
||||
import org.qortal.repository.DataException;
|
||||
import org.qortal.repository.Repository;
|
||||
import org.qortal.repository.RepositoryManager;
|
||||
|
||||
@WebSocket
|
||||
@SuppressWarnings("serial")
|
||||
public class AdminStatusWebSocket extends WebSocketServlet implements ApiWebSocket {
|
||||
|
||||
@Override
|
||||
public void configure(WebSocketServletFactory factory) {
|
||||
factory.register(AdminStatusWebSocket.class);
|
||||
}
|
||||
|
||||
@OnWebSocketConnect
|
||||
public void onWebSocketConnect(Session session) {
|
||||
AtomicReference<String> previousOutput = new AtomicReference<>(null);
|
||||
|
||||
StatusNotifier.Listener listener = timestamp -> onNotify(session, previousOutput);
|
||||
StatusNotifier.getInstance().register(session, listener);
|
||||
|
||||
this.onNotify(session, previousOutput);
|
||||
}
|
||||
|
||||
@OnWebSocketClose
|
||||
public void onWebSocketClose(Session session, int statusCode, String reason) {
|
||||
StatusNotifier.getInstance().deregister(session);
|
||||
}
|
||||
|
||||
@OnWebSocketError
|
||||
public void onWebSocketError(Session session, Throwable throwable) {
|
||||
}
|
||||
|
||||
@OnWebSocketMessage
|
||||
public void onWebSocketMessage(Session session, String message) {
|
||||
}
|
||||
|
||||
private void onNotify(Session session,AtomicReference<String> previousOutput) {
|
||||
try (final Repository repository = RepositoryManager.getRepository()) {
|
||||
NodeStatus nodeStatus = new NodeStatus();
|
||||
|
||||
StringWriter stringWriter = new StringWriter();
|
||||
|
||||
this.marshall(stringWriter, nodeStatus);
|
||||
|
||||
// Only output if something has changed
|
||||
String output = stringWriter.toString();
|
||||
if (output.equals(previousOutput.get()))
|
||||
return;
|
||||
|
||||
previousOutput.set(output);
|
||||
session.getRemote().sendStringByFuture(output);
|
||||
} catch (DataException | IOException | WebSocketException e) {
|
||||
// No output this time?
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
95
src/main/java/org/qortal/api/websocket/ApiWebSocket.java
Normal file
95
src/main/java/org/qortal/api/websocket/ApiWebSocket.java
Normal file
@@ -0,0 +1,95 @@
|
||||
package org.qortal.api.websocket;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.io.StringWriter;
|
||||
import java.io.Writer;
|
||||
import java.util.Collection;
|
||||
import java.util.Map;
|
||||
|
||||
import javax.xml.bind.JAXBContext;
|
||||
import javax.xml.bind.JAXBException;
|
||||
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.persistence.jaxb.JAXBContextFactory;
|
||||
import org.eclipse.persistence.jaxb.MarshallerProperties;
|
||||
import org.qortal.api.ApiError;
|
||||
import org.qortal.api.ApiErrorRoot;
|
||||
|
||||
interface ApiWebSocket {
|
||||
|
||||
default String getPathInfo(Session session) {
|
||||
ServletUpgradeRequest upgradeRequest = (ServletUpgradeRequest) session.getUpgradeRequest();
|
||||
return upgradeRequest.getHttpServletRequest().getPathInfo();
|
||||
}
|
||||
|
||||
default Map<String, String> getPathParams(Session session, String pathSpec) {
|
||||
UriTemplatePathSpec uriTemplatePathSpec = new UriTemplatePathSpec(pathSpec);
|
||||
return uriTemplatePathSpec.getPathParams(this.getPathInfo(session));
|
||||
}
|
||||
|
||||
default void sendError(Session session, ApiError apiError) {
|
||||
ApiErrorRoot apiErrorRoot = new ApiErrorRoot();
|
||||
apiErrorRoot.setApiError(apiError);
|
||||
|
||||
StringWriter stringWriter = new StringWriter();
|
||||
try {
|
||||
marshall(stringWriter, apiErrorRoot);
|
||||
session.getRemote().sendString(stringWriter.toString());
|
||||
} catch (IOException e) {
|
||||
// Remote end probably closed
|
||||
}
|
||||
}
|
||||
|
||||
default void marshall(Writer writer, Object object) throws IOException {
|
||||
Marshaller marshaller = createMarshaller(object.getClass());
|
||||
|
||||
try {
|
||||
marshaller.marshal(object, writer);
|
||||
} catch (JAXBException e) {
|
||||
throw new IOException("Unable to create marshall object for websocket", e);
|
||||
}
|
||||
}
|
||||
|
||||
default void marshall(Writer writer, Collection<?> collection) throws IOException {
|
||||
// If collection is empty then we're returning "[]" anyway
|
||||
if (collection.isEmpty()) {
|
||||
writer.append("[]");
|
||||
return;
|
||||
}
|
||||
|
||||
// Grab an entry from collection so we can determine type
|
||||
Object entry = collection.iterator().next();
|
||||
|
||||
Marshaller marshaller = createMarshaller(entry.getClass());
|
||||
|
||||
try {
|
||||
marshaller.marshal(collection, writer);
|
||||
} catch (JAXBException e) {
|
||||
throw new IOException("Unable to create marshall object for websocket", e);
|
||||
}
|
||||
}
|
||||
|
||||
private static Marshaller createMarshaller(Class<?> objectClass) {
|
||||
try {
|
||||
// Create JAXB context aware of object's class
|
||||
JAXBContext jc = JAXBContextFactory.createContext(new Class[] { objectClass }, null);
|
||||
|
||||
// Create marshaller
|
||||
Marshaller marshaller = jc.createMarshaller();
|
||||
|
||||
// Set the marshaller media type to JSON
|
||||
marshaller.setProperty(MarshallerProperties.MEDIA_TYPE, "application/json");
|
||||
|
||||
// Tell marshaller not to include JSON root element in the output
|
||||
marshaller.setProperty(MarshallerProperties.JSON_INCLUDE_ROOT, false);
|
||||
|
||||
return marshaller;
|
||||
} catch (JAXBException e) {
|
||||
throw new RuntimeException("Unable to create websocket marshaller", e);
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
122
src/main/java/org/qortal/api/websocket/BlocksWebSocket.java
Normal file
122
src/main/java/org/qortal/api/websocket/BlocksWebSocket.java
Normal file
@@ -0,0 +1,122 @@
|
||||
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.repository.DataException;
|
||||
import org.qortal.repository.Repository;
|
||||
import org.qortal.repository.RepositoryManager;
|
||||
import org.qortal.utils.Base58;
|
||||
|
||||
@WebSocket
|
||||
@SuppressWarnings("serial")
|
||||
public class BlocksWebSocket extends WebSocketServlet implements ApiWebSocket {
|
||||
|
||||
@Override
|
||||
public void configure(WebSocketServletFactory factory) {
|
||||
factory.register(BlocksWebSocket.class);
|
||||
}
|
||||
|
||||
@OnWebSocketConnect
|
||||
public void onWebSocketConnect(Session session) {
|
||||
BlockNotifier.Listener listener = blockInfo -> onNotify(session, blockInfo);
|
||||
BlockNotifier.getInstance().register(session, listener);
|
||||
}
|
||||
|
||||
@OnWebSocketClose
|
||||
public void onWebSocketClose(Session session, int statusCode, String reason) {
|
||||
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
|
||||
if (message.length() > 128) {
|
||||
// Try base58 block signature
|
||||
byte[] signature;
|
||||
|
||||
try {
|
||||
signature = Base58.decode(message);
|
||||
} catch (NumberFormatException e) {
|
||||
sendError(session, ApiError.INVALID_SIGNATURE);
|
||||
return;
|
||||
}
|
||||
|
||||
try (final Repository repository = RepositoryManager.getRepository()) {
|
||||
int height = repository.getBlockRepository().getHeightFromSignature(signature);
|
||||
if (height == 0) {
|
||||
sendError(session, ApiError.BLOCK_UNKNOWN);
|
||||
return;
|
||||
}
|
||||
|
||||
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);
|
||||
}
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
if (message.length() > 10)
|
||||
// Bigger than max integer value, so probably a ping - silently ignore
|
||||
return;
|
||||
|
||||
// Try integer
|
||||
int height;
|
||||
|
||||
try {
|
||||
height = Integer.parseInt(message);
|
||||
} catch (NumberFormatException e) {
|
||||
sendError(session, ApiError.INVALID_HEIGHT);
|
||||
return;
|
||||
}
|
||||
|
||||
try (final Repository repository = RepositoryManager.getRepository()) {
|
||||
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);
|
||||
}
|
||||
}
|
||||
|
||||
private void onNotify(Session session, BlockInfo blockInfo) {
|
||||
StringWriter stringWriter = new StringWriter();
|
||||
|
||||
try {
|
||||
this.marshall(stringWriter, blockInfo);
|
||||
|
||||
session.getRemote().sendStringByFuture(stringWriter.toString());
|
||||
} catch (IOException | WebSocketException e) {
|
||||
// No output this time
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,153 @@
|
||||
package org.qortal.api.websocket;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.io.StringWriter;
|
||||
import java.util.Arrays;
|
||||
import java.util.Collections;
|
||||
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;
|
||||
import org.qortal.data.transaction.ChatTransactionData;
|
||||
import org.qortal.repository.DataException;
|
||||
import org.qortal.repository.Repository;
|
||||
import org.qortal.repository.RepositoryManager;
|
||||
|
||||
@WebSocket
|
||||
@SuppressWarnings("serial")
|
||||
public class ChatMessagesWebSocket extends WebSocketServlet implements ApiWebSocket {
|
||||
|
||||
@Override
|
||||
public void configure(WebSocketServletFactory factory) {
|
||||
factory.register(ChatMessagesWebSocket.class);
|
||||
}
|
||||
|
||||
@OnWebSocketConnect
|
||||
public void onWebSocketConnect(Session session) {
|
||||
Map<String, List<String>> queryParams = session.getUpgradeRequest().getParameterMap();
|
||||
|
||||
List<String> txGroupIds = queryParams.get("txGroupId");
|
||||
if (txGroupIds != null && txGroupIds.size() == 1) {
|
||||
int txGroupId = Integer.parseInt(txGroupIds.get(0));
|
||||
|
||||
try (final Repository repository = RepositoryManager.getRepository()) {
|
||||
List<ChatMessage> chatMessages = repository.getChatRepository().getMessagesMatchingCriteria(
|
||||
null,
|
||||
null,
|
||||
txGroupId,
|
||||
null,
|
||||
null, null, null);
|
||||
|
||||
sendMessages(session, chatMessages);
|
||||
} catch (DataException e) {
|
||||
// Not a good start
|
||||
session.close(4001, "Couldn't fetch initial messages from repository");
|
||||
return;
|
||||
}
|
||||
|
||||
ChatNotifier.Listener listener = chatTransactionData -> onNotify(session, chatTransactionData, txGroupId);
|
||||
ChatNotifier.getInstance().register(session, listener);
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
List<String> involvingAddresses = queryParams.get("involving");
|
||||
if (involvingAddresses == null || involvingAddresses.size() != 2) {
|
||||
session.close(4001, "invalid criteria");
|
||||
return;
|
||||
}
|
||||
|
||||
try (final Repository repository = RepositoryManager.getRepository()) {
|
||||
List<ChatMessage> chatMessages = repository.getChatRepository().getMessagesMatchingCriteria(
|
||||
null,
|
||||
null,
|
||||
null,
|
||||
involvingAddresses,
|
||||
null, null, null);
|
||||
|
||||
sendMessages(session, chatMessages);
|
||||
} catch (DataException e) {
|
||||
// Not a good start
|
||||
session.close(4001, "Couldn't fetch initial messages from repository");
|
||||
return;
|
||||
}
|
||||
|
||||
ChatNotifier.Listener listener = chatTransactionData -> onNotify(session, chatTransactionData, involvingAddresses);
|
||||
ChatNotifier.getInstance().register(session, listener);
|
||||
}
|
||||
|
||||
@OnWebSocketClose
|
||||
public void onWebSocketClose(Session session, int statusCode, String reason) {
|
||||
ChatNotifier.getInstance().deregister(session);
|
||||
}
|
||||
|
||||
@OnWebSocketError
|
||||
public void onWebSocketError(Session session, Throwable throwable) {
|
||||
}
|
||||
|
||||
@OnWebSocketMessage
|
||||
public void onWebSocketMessage(Session session, String message) {
|
||||
}
|
||||
|
||||
private void onNotify(Session session, ChatTransactionData chatTransactionData, int txGroupId) {
|
||||
if (chatTransactionData == null)
|
||||
// There has been a group-membership change, but we're not interested
|
||||
return;
|
||||
|
||||
// We only want group-based messages with our txGroupId
|
||||
if (chatTransactionData.getRecipient() != null || chatTransactionData.getTxGroupId() != txGroupId)
|
||||
return;
|
||||
|
||||
sendChat(session, chatTransactionData);
|
||||
}
|
||||
|
||||
private void onNotify(Session session, ChatTransactionData chatTransactionData, List<String> involvingAddresses) {
|
||||
// We only want direct/non-group messages where sender/recipient match our addresses
|
||||
String recipient = chatTransactionData.getRecipient();
|
||||
if (recipient == null)
|
||||
return;
|
||||
|
||||
List<String> transactionAddresses = Arrays.asList(recipient, chatTransactionData.getSender());
|
||||
|
||||
if (!transactionAddresses.containsAll(involvingAddresses))
|
||||
return;
|
||||
|
||||
sendChat(session, chatTransactionData);
|
||||
}
|
||||
|
||||
private void sendMessages(Session session, List<ChatMessage> chatMessages) {
|
||||
StringWriter stringWriter = new StringWriter();
|
||||
|
||||
try {
|
||||
this.marshall(stringWriter, chatMessages);
|
||||
|
||||
session.getRemote().sendStringByFuture(stringWriter.toString());
|
||||
} catch (IOException | WebSocketException e) {
|
||||
// No output this time?
|
||||
}
|
||||
}
|
||||
|
||||
private void sendChat(Session session, ChatTransactionData chatTransactionData) {
|
||||
// Convert ChatTransactionData to ChatMessage
|
||||
ChatMessage chatMessage;
|
||||
try (final Repository repository = RepositoryManager.getRepository()) {
|
||||
chatMessage = repository.getChatRepository().toChatMessage(chatTransactionData);
|
||||
} catch (DataException e) {
|
||||
// No output this time?
|
||||
return;
|
||||
}
|
||||
|
||||
sendMessages(session, Collections.singletonList(chatMessage));
|
||||
}
|
||||
|
||||
}
|
||||
@@ -1,11 +1,13 @@
|
||||
package org.qortal.asset;
|
||||
|
||||
import org.qortal.crypto.Crypto;
|
||||
import org.qortal.data.asset.AssetData;
|
||||
import org.qortal.data.transaction.IssueAssetTransactionData;
|
||||
import org.qortal.data.transaction.TransactionData;
|
||||
import org.qortal.data.transaction.UpdateAssetTransactionData;
|
||||
import org.qortal.repository.DataException;
|
||||
import org.qortal.repository.Repository;
|
||||
import org.qortal.utils.Amounts;
|
||||
|
||||
public class Asset {
|
||||
|
||||
@@ -21,12 +23,12 @@ public class Asset {
|
||||
|
||||
// Other useful constants
|
||||
|
||||
public static final int MAX_NAME_SIZE = 400;
|
||||
public static final int MIN_NAME_SIZE = 3;
|
||||
public static final int MAX_NAME_SIZE = 40;
|
||||
public static final int MAX_DESCRIPTION_SIZE = 4000;
|
||||
public static final int MAX_DATA_SIZE = 400000;
|
||||
|
||||
public static final long MAX_DIVISIBLE_QUANTITY = 10_000_000_000L; // but also to 8 decimal places
|
||||
public static final long MAX_INDIVISIBLE_QUANTITY = 1_000_000_000_000_000_000L;
|
||||
public static final long MAX_QUANTITY = 10_000_000_000L * Amounts.MULTIPLIER; // but also to 8 decimal places
|
||||
|
||||
// Properties
|
||||
private Repository repository;
|
||||
@@ -42,12 +44,14 @@ public class Asset {
|
||||
public Asset(Repository repository, IssueAssetTransactionData issueAssetTransactionData) {
|
||||
this.repository = repository;
|
||||
|
||||
String ownerAddress = Crypto.toAddress(issueAssetTransactionData.getCreatorPublicKey());
|
||||
|
||||
// NOTE: transaction's reference is used to look up newly assigned assetID on creation!
|
||||
this.assetData = new AssetData(issueAssetTransactionData.getOwner(), issueAssetTransactionData.getAssetName(),
|
||||
this.assetData = new AssetData(ownerAddress, issueAssetTransactionData.getAssetName(),
|
||||
issueAssetTransactionData.getDescription(), issueAssetTransactionData.getQuantity(),
|
||||
issueAssetTransactionData.getIsDivisible(), issueAssetTransactionData.getData(),
|
||||
issueAssetTransactionData.getIsUnspendable(),
|
||||
issueAssetTransactionData.getTxGroupId(), issueAssetTransactionData.getSignature());
|
||||
issueAssetTransactionData.isDivisible(), issueAssetTransactionData.getData(),
|
||||
issueAssetTransactionData.isUnspendable(), issueAssetTransactionData.getTxGroupId(),
|
||||
issueAssetTransactionData.getSignature(), issueAssetTransactionData.getReducedAssetName());
|
||||
}
|
||||
|
||||
public Asset(Repository repository, long assetId) throws DataException {
|
||||
@@ -118,10 +122,11 @@ public class Asset {
|
||||
throw new IllegalStateException("Missing referenced transaction when orphaning UPDATE_ASSET");
|
||||
|
||||
switch (previousTransactionData.getType()) {
|
||||
case ISSUE_ASSET:
|
||||
case ISSUE_ASSET: {
|
||||
IssueAssetTransactionData previousIssueAssetTransactionData = (IssueAssetTransactionData) previousTransactionData;
|
||||
|
||||
this.assetData.setOwner(previousIssueAssetTransactionData.getOwner());
|
||||
String ownerAddress = Crypto.toAddress(previousIssueAssetTransactionData.getCreatorPublicKey());
|
||||
this.assetData.setOwner(ownerAddress);
|
||||
|
||||
if (needDescription) {
|
||||
this.assetData.setDescription(previousIssueAssetTransactionData.getDescription());
|
||||
@@ -133,8 +138,9 @@ public class Asset {
|
||||
needData = false;
|
||||
}
|
||||
break;
|
||||
}
|
||||
|
||||
case UPDATE_ASSET:
|
||||
case UPDATE_ASSET: {
|
||||
UpdateAssetTransactionData previousUpdateAssetTransactionData = (UpdateAssetTransactionData) previousTransactionData;
|
||||
|
||||
this.assetData.setOwner(previousUpdateAssetTransactionData.getNewOwner());
|
||||
@@ -152,7 +158,9 @@ public class Asset {
|
||||
// Get signature for previous transaction in chain, just in case we need it
|
||||
if (needDescription || needData)
|
||||
previousTransactionSignature = previousUpdateAssetTransactionData.getOrphanReference();
|
||||
|
||||
break;
|
||||
}
|
||||
|
||||
default:
|
||||
throw new IllegalStateException("Invalid referenced transaction when orphaning UPDATE_ASSET");
|
||||
|
||||
@@ -1,8 +1,8 @@
|
||||
package org.qortal.asset;
|
||||
|
||||
import java.math.BigDecimal;
|
||||
import static org.qortal.utils.Amounts.prettyAmount;
|
||||
|
||||
import java.math.BigInteger;
|
||||
import java.math.RoundingMode;
|
||||
import java.util.Arrays;
|
||||
import java.util.List;
|
||||
|
||||
@@ -11,13 +11,12 @@ import org.apache.logging.log4j.LogManager;
|
||||
import org.apache.logging.log4j.Logger;
|
||||
import org.qortal.account.Account;
|
||||
import org.qortal.account.PublicKeyAccount;
|
||||
import org.qortal.block.BlockChain;
|
||||
import org.qortal.data.asset.AssetData;
|
||||
import org.qortal.data.asset.OrderData;
|
||||
import org.qortal.data.asset.TradeData;
|
||||
import org.qortal.repository.AssetRepository;
|
||||
import org.qortal.repository.DataException;
|
||||
import org.qortal.repository.Repository;
|
||||
import org.qortal.utils.Amounts;
|
||||
import org.qortal.utils.Base58;
|
||||
|
||||
public class Order {
|
||||
@@ -29,9 +28,11 @@ public class Order {
|
||||
private OrderData orderData;
|
||||
|
||||
// Used quite a bit
|
||||
private final boolean isOurOrderNewPricing;
|
||||
private final long haveAssetId;
|
||||
private final long wantAssetId;
|
||||
private final boolean isAmountInWantAsset;
|
||||
private final BigInteger orderAmount;
|
||||
private final BigInteger orderPrice;
|
||||
|
||||
/** Cache of price-pair units e.g. QORT/GOLD, but use getPricePair() instead! */
|
||||
private String cachedPricePair;
|
||||
@@ -47,9 +48,12 @@ public class Order {
|
||||
this.repository = repository;
|
||||
this.orderData = orderData;
|
||||
|
||||
this.isOurOrderNewPricing = this.orderData.getTimestamp() >= BlockChain.getInstance().getNewAssetPricingTimestamp();
|
||||
this.haveAssetId = this.orderData.getHaveAssetId();
|
||||
this.wantAssetId = this.orderData.getWantAssetId();
|
||||
this.isAmountInWantAsset = haveAssetId < wantAssetId;
|
||||
|
||||
this.orderAmount = BigInteger.valueOf(this.orderData.getAmount());
|
||||
this.orderPrice = BigInteger.valueOf(this.orderData.getPrice());
|
||||
}
|
||||
|
||||
// Getters/Setters
|
||||
@@ -60,16 +64,16 @@ public class Order {
|
||||
|
||||
// More information
|
||||
|
||||
public static BigDecimal getAmountLeft(OrderData orderData) {
|
||||
return orderData.getAmount().subtract(orderData.getFulfilled());
|
||||
public static long getAmountLeft(OrderData orderData) {
|
||||
return orderData.getAmount() - orderData.getFulfilled();
|
||||
}
|
||||
|
||||
public BigDecimal getAmountLeft() {
|
||||
public long getAmountLeft() {
|
||||
return Order.getAmountLeft(this.orderData);
|
||||
}
|
||||
|
||||
public static boolean isFulfilled(OrderData orderData) {
|
||||
return orderData.getFulfilled().compareTo(orderData.getAmount()) == 0;
|
||||
return orderData.getFulfilled() == orderData.getAmount();
|
||||
}
|
||||
|
||||
public boolean isFulfilled() {
|
||||
@@ -86,13 +90,10 @@ public class Order {
|
||||
* <p>
|
||||
* @return granularity of matched-amount
|
||||
*/
|
||||
public static BigDecimal calculateAmountGranularity(boolean isAmountAssetDivisible, boolean isReturnAssetDivisible, BigDecimal price) {
|
||||
// Multiplier to scale BigDecimal fractional amounts into integer domain
|
||||
BigInteger multiplier = BigInteger.valueOf(1_0000_0000L);
|
||||
|
||||
public static long calculateAmountGranularity(boolean isAmountAssetDivisible, boolean isReturnAssetDivisible, long price) {
|
||||
// Calculate the minimum increment for matched-amount using greatest-common-divisor
|
||||
BigInteger returnAmount = multiplier; // 1 unit (* multiplier)
|
||||
BigInteger matchedAmount = price.movePointRight(8).toBigInteger();
|
||||
BigInteger returnAmount = Amounts.MULTIPLIER_BI; // 1 unit * multiplier
|
||||
BigInteger matchedAmount = BigInteger.valueOf(price);
|
||||
|
||||
BigInteger gcd = returnAmount.gcd(matchedAmount);
|
||||
returnAmount = returnAmount.divide(gcd);
|
||||
@@ -100,20 +101,20 @@ public class Order {
|
||||
|
||||
// Calculate GCD in combination with divisibility
|
||||
if (isAmountAssetDivisible)
|
||||
returnAmount = returnAmount.multiply(multiplier);
|
||||
returnAmount = returnAmount.multiply(Amounts.MULTIPLIER_BI);
|
||||
|
||||
if (isReturnAssetDivisible)
|
||||
matchedAmount = matchedAmount.multiply(multiplier);
|
||||
matchedAmount = matchedAmount.multiply(Amounts.MULTIPLIER_BI);
|
||||
|
||||
gcd = returnAmount.gcd(matchedAmount);
|
||||
|
||||
// Calculate the granularity at which we have to buy
|
||||
BigDecimal granularity = new BigDecimal(returnAmount.divide(gcd));
|
||||
BigInteger granularity = returnAmount.multiply(Amounts.MULTIPLIER_BI).divide(gcd);
|
||||
if (isAmountAssetDivisible)
|
||||
granularity = granularity.movePointLeft(8);
|
||||
granularity = granularity.divide(Amounts.MULTIPLIER_BI);
|
||||
|
||||
// Return
|
||||
return granularity;
|
||||
return granularity.longValue();
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -130,7 +131,7 @@ public class Order {
|
||||
|
||||
/** Calculate price pair. (e.g. QORT/GOLD)
|
||||
* <p>
|
||||
* Under 'new' pricing scheme, lowest-assetID asset is first,
|
||||
* Lowest-assetID asset is first,
|
||||
* so if QORT has assetID 0 and GOLD has assetID 10, then
|
||||
* the pricing pair is QORT/GOLD.
|
||||
* <p>
|
||||
@@ -141,32 +142,32 @@ public class Order {
|
||||
AssetData haveAssetData = getHaveAsset();
|
||||
AssetData wantAssetData = getWantAsset();
|
||||
|
||||
if (isOurOrderNewPricing && haveAssetId > wantAssetId)
|
||||
if (haveAssetId > wantAssetId)
|
||||
cachedPricePair = wantAssetData.getName() + "/" + haveAssetData.getName();
|
||||
else
|
||||
cachedPricePair = haveAssetData.getName() + "/" + wantAssetData.getName();
|
||||
}
|
||||
|
||||
/** Returns amount of have-asset to remove from order's creator's balance on placing this order. */
|
||||
private BigDecimal calcHaveAssetCommittment() {
|
||||
BigDecimal committedCost = this.orderData.getAmount();
|
||||
private long calcHaveAssetCommittment() {
|
||||
// Simple case: amount is in have asset
|
||||
if (!this.isAmountInWantAsset)
|
||||
return this.orderData.getAmount();
|
||||
|
||||
// If 'new' pricing and "amount" is in want-asset then we need to convert
|
||||
if (isOurOrderNewPricing && haveAssetId < wantAssetId)
|
||||
committedCost = committedCost.multiply(this.orderData.getPrice()).setScale(8, RoundingMode.HALF_UP);
|
||||
return Amounts.roundUpScaledMultiply(this.orderAmount, this.orderPrice);
|
||||
}
|
||||
|
||||
return committedCost;
|
||||
private long calcHaveAssetRefund(long amount) {
|
||||
// Simple case: amount is in have asset
|
||||
if (!this.isAmountInWantAsset)
|
||||
return amount;
|
||||
|
||||
return Amounts.roundUpScaledMultiply(BigInteger.valueOf(amount), this.orderPrice);
|
||||
}
|
||||
|
||||
/** Returns amount of remaining have-asset to refund to order's creator's balance on cancelling this order. */
|
||||
private BigDecimal calcHaveAssetRefund() {
|
||||
BigDecimal refund = getAmountLeft();
|
||||
|
||||
// If 'new' pricing and "amount" is in want-asset then we need to convert
|
||||
if (isOurOrderNewPricing && haveAssetId < wantAssetId)
|
||||
refund = refund.multiply(this.orderData.getPrice()).setScale(8, RoundingMode.HALF_UP);
|
||||
|
||||
return refund;
|
||||
private long calcHaveAssetRefund() {
|
||||
return calcHaveAssetRefund(getAmountLeft());
|
||||
}
|
||||
|
||||
// Navigation
|
||||
@@ -192,27 +193,19 @@ public class Order {
|
||||
/**
|
||||
* Returns AssetData for asset in effect for "amount" field.
|
||||
* <p>
|
||||
* For 'old' pricing, this is the have-asset.<br>
|
||||
* For 'new' pricing, this is the asset with highest assetID.
|
||||
* This is the asset with highest assetID.
|
||||
*/
|
||||
public AssetData getAmountAsset() throws DataException {
|
||||
if (isOurOrderNewPricing && wantAssetId > haveAssetId)
|
||||
return getWantAsset();
|
||||
else
|
||||
return getHaveAsset();
|
||||
return (wantAssetId > haveAssetId) ? getWantAsset() : getHaveAsset();
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns AssetData for other (return) asset traded.
|
||||
* <p>
|
||||
* For 'old' pricing, this is the want-asset.<br>
|
||||
* For 'new' pricing, this is the asset with lowest assetID.
|
||||
* This is the asset with lowest assetID.
|
||||
*/
|
||||
public AssetData getReturnAsset() throws DataException {
|
||||
if (isOurOrderNewPricing && haveAssetId < wantAssetId)
|
||||
return getHaveAsset();
|
||||
else
|
||||
return getWantAsset();
|
||||
return (haveAssetId < wantAssetId) ? getHaveAsset() : getWantAsset();
|
||||
}
|
||||
|
||||
// Processing
|
||||
@@ -227,8 +220,6 @@ public class Order {
|
||||
|
||||
// NOTE: the following values are specific to passed orderData, not the same as class instance values!
|
||||
|
||||
final boolean isOrderNewAssetPricing = orderData.getTimestamp() >= BlockChain.getInstance().getNewAssetPricingTimestamp();
|
||||
|
||||
// Cached for readability
|
||||
final long _haveAssetId = orderData.getHaveAssetId();
|
||||
final long _wantAssetId = orderData.getWantAssetId();
|
||||
@@ -236,43 +227,36 @@ public class Order {
|
||||
final AssetData haveAssetData = this.repository.getAssetRepository().fromAssetId(_haveAssetId);
|
||||
final AssetData wantAssetData = this.repository.getAssetRepository().fromAssetId(_wantAssetId);
|
||||
|
||||
final long amountAssetId = (isOurOrderNewPricing && _wantAssetId > _haveAssetId) ? _wantAssetId : _haveAssetId;
|
||||
final long returnAssetId = (isOurOrderNewPricing && _haveAssetId < _wantAssetId) ? _haveAssetId : _wantAssetId;
|
||||
final long amountAssetId = (_wantAssetId > _haveAssetId) ? _wantAssetId : _haveAssetId;
|
||||
final long returnAssetId = (_haveAssetId < _wantAssetId) ? _haveAssetId : _wantAssetId;
|
||||
|
||||
final AssetData amountAssetData = this.repository.getAssetRepository().fromAssetId(amountAssetId);
|
||||
final AssetData returnAssetData = this.repository.getAssetRepository().fromAssetId(returnAssetId);
|
||||
|
||||
LOGGER.debug(String.format("%s %s", orderPrefix, Base58.encode(orderData.getOrderId())));
|
||||
LOGGER.debug(() -> String.format("%s %s", orderPrefix, Base58.encode(orderData.getOrderId())));
|
||||
|
||||
LOGGER.trace(String.format("%s have %s, want %s. '%s' pricing scheme.", weThey, haveAssetData.getName(), wantAssetData.getName(), isOrderNewAssetPricing ? "new" : "old"));
|
||||
LOGGER.trace(() -> String.format("%s have %s, want %s.", weThey, haveAssetData.getName(), wantAssetData.getName()));
|
||||
|
||||
LOGGER.trace(String.format("%s amount: %s (ordered) - %s (fulfilled) = %s %s left", ourTheir,
|
||||
orderData.getAmount().stripTrailingZeros().toPlainString(),
|
||||
orderData.getFulfilled().stripTrailingZeros().toPlainString(),
|
||||
Order.getAmountLeft(orderData).stripTrailingZeros().toPlainString(),
|
||||
LOGGER.trace(() -> String.format("%s amount: %s (ordered) - %s (fulfilled) = %s %s left", ourTheir,
|
||||
prettyAmount(orderData.getAmount()),
|
||||
prettyAmount(orderData.getFulfilled()),
|
||||
prettyAmount(Order.getAmountLeft(orderData)),
|
||||
amountAssetData.getName()));
|
||||
|
||||
BigDecimal maxReturnAmount = Order.getAmountLeft(orderData).multiply(orderData.getPrice()).setScale(8, RoundingMode.HALF_UP);
|
||||
long maxReturnAmount = Amounts.roundUpScaledMultiply(Order.getAmountLeft(orderData), orderData.getPrice());
|
||||
String pricePair = getPricePair();
|
||||
|
||||
LOGGER.trace(String.format("%s price: %s %s (%s %s tradable)", ourTheir,
|
||||
orderData.getPrice().toPlainString(), getPricePair(),
|
||||
maxReturnAmount.stripTrailingZeros().toPlainString(), returnAssetData.getName()));
|
||||
LOGGER.trace(() -> String.format("%s price: %s %s (%s %s tradable)", ourTheir,
|
||||
prettyAmount(orderData.getPrice()),
|
||||
pricePair,
|
||||
prettyAmount(maxReturnAmount),
|
||||
returnAssetData.getName()));
|
||||
}
|
||||
|
||||
public void process() throws DataException {
|
||||
AssetRepository assetRepository = this.repository.getAssetRepository();
|
||||
|
||||
AssetData haveAssetData = getHaveAsset();
|
||||
AssetData wantAssetData = getWantAsset();
|
||||
|
||||
/** The asset while working out amount that matches. */
|
||||
AssetData matchingAssetData = isOurOrderNewPricing ? getAmountAsset() : wantAssetData;
|
||||
/** The return asset traded if trade completes. */
|
||||
AssetData returnAssetData = isOurOrderNewPricing ? getReturnAsset() : haveAssetData;
|
||||
|
||||
// Subtract have-asset from creator
|
||||
Account creator = new PublicKeyAccount(this.repository, this.orderData.getCreatorPublicKey());
|
||||
creator.setConfirmedBalance(haveAssetId, creator.getConfirmedBalance(haveAssetId).subtract(this.calcHaveAssetCommittment()));
|
||||
creator.modifyAssetBalance(haveAssetId, - this.calcHaveAssetCommittment());
|
||||
|
||||
// Save this order into repository so it's available for matching, possibly by itself
|
||||
this.repository.getAssetRepository().save(this.orderData);
|
||||
@@ -281,36 +265,28 @@ public class Order {
|
||||
|
||||
// Fetch corresponding open orders that might potentially match, hence reversed want/have assetIDs.
|
||||
// Returned orders are sorted with lowest "price" first.
|
||||
List<OrderData> orders = assetRepository.getOpenOrdersForTrading(wantAssetId, haveAssetId, isOurOrderNewPricing ? this.orderData.getPrice() : null);
|
||||
LOGGER.trace("Open orders fetched from repository: " + orders.size());
|
||||
List<OrderData> orders = this.repository.getAssetRepository().getOpenOrdersForTrading(wantAssetId, haveAssetId, this.orderData.getPrice());
|
||||
LOGGER.trace(() -> String.format("Open orders fetched from repository: %d", orders.size()));
|
||||
|
||||
if (orders.isEmpty())
|
||||
return;
|
||||
|
||||
matchOrders(orders);
|
||||
}
|
||||
|
||||
private void matchOrders(List<OrderData> orders) throws DataException {
|
||||
AssetData haveAssetData = getHaveAsset();
|
||||
AssetData wantAssetData = getWantAsset();
|
||||
|
||||
/** The asset while working out amount that matches. */
|
||||
AssetData matchingAssetData = getAmountAsset();
|
||||
/** The return asset traded if trade completes. */
|
||||
AssetData returnAssetData = getReturnAsset();
|
||||
|
||||
// Attempt to match orders
|
||||
|
||||
/*
|
||||
* Potential matching order example ("old"):
|
||||
*
|
||||
* Our order:
|
||||
* haveAssetId=[GOLD], wantAssetId=0 (QORT), amount=40 (GOLD), price=486 (QORT/GOLD)
|
||||
* This translates to "we have 40 GOLD and want QORT at a price of 486 QORT per GOLD"
|
||||
* If our order matched, we'd end up with 40 * 486 = 19,440 QORT.
|
||||
*
|
||||
* Their order:
|
||||
* haveAssetId=0 (QORT), wantAssetId=[GOLD], amount=20,000 (QORT), price=0.00205761 (GOLD/QORT)
|
||||
* This translates to "they have 20,000 QORT and want GOLD at a price of 0.00205761 GOLD per QORT"
|
||||
*
|
||||
* Their price, converted into 'our' units of QORT/GOLD, is: 1 / 0.00205761 = 486.00074844 QORT/GOLD.
|
||||
* This is better than our requested 486 QORT/GOLD so this order matches.
|
||||
*
|
||||
* Using their price, we end up with 40 * 486.00074844 = 19440.02993760 QORT. They end up with 40 GOLD.
|
||||
*
|
||||
* If their order had 19,440 QORT left, only 19,440 * 0.00205761 = 39.99993840 GOLD would be traded.
|
||||
*/
|
||||
|
||||
/*
|
||||
* Potential matching order example ("new"):
|
||||
* Potential matching order example:
|
||||
*
|
||||
* Our order:
|
||||
* haveAssetId=[GOLD], wantAssetId=0 (QORT), amount=40 (GOLD), price=486 (QORT/GOLD)
|
||||
@@ -328,129 +304,107 @@ public class Order {
|
||||
* If their order only had 36 GOLD left, only 36 * 486.00074844 = 17496.02694384 QORT would be traded.
|
||||
*/
|
||||
|
||||
BigDecimal ourPrice = this.orderData.getPrice();
|
||||
long ourPrice = this.orderData.getPrice();
|
||||
String pricePair = getPricePair();
|
||||
|
||||
for (OrderData theirOrderData : orders) {
|
||||
logOrder("Considering order", false, theirOrderData);
|
||||
|
||||
// Not used:
|
||||
// boolean isTheirOrderNewAssetPricing = theirOrderData.getTimestamp() >= BlockChain.getInstance().getNewAssetPricingTimestamp();
|
||||
|
||||
// Determine their order price
|
||||
BigDecimal theirPrice;
|
||||
|
||||
if (isOurOrderNewPricing) {
|
||||
// Pricing units are the same way round for both orders, so no conversion needed.
|
||||
// Orders under 'old' pricing have been converted during repository update.
|
||||
theirPrice = theirOrderData.getPrice();
|
||||
LOGGER.trace(String.format("Their price: %s %s", theirPrice.toPlainString(), getPricePair()));
|
||||
} else {
|
||||
// If our order is 'old' pricing then all other existing orders must be 'old' pricing too
|
||||
// Their order pricing will be inverted, so convert
|
||||
theirPrice = BigDecimal.ONE.setScale(8).divide(theirOrderData.getPrice(), RoundingMode.DOWN);
|
||||
LOGGER.trace(String.format("Their price: %s %s per %s", theirPrice.toPlainString(), wantAssetData.getName(), haveAssetData.getName()));
|
||||
}
|
||||
long theirPrice = theirOrderData.getPrice();
|
||||
LOGGER.trace(() -> String.format("Their price: %s %s", prettyAmount(theirPrice), pricePair));
|
||||
|
||||
// If their price is worse than what we're willing to accept then we're done as prices only get worse as we iterate through list of orders
|
||||
if (isOurOrderNewPricing) {
|
||||
if (haveAssetId < wantAssetId && theirPrice.compareTo(ourPrice) > 0)
|
||||
break;
|
||||
if (haveAssetId > wantAssetId && theirPrice.compareTo(ourPrice) < 0)
|
||||
break;
|
||||
} else {
|
||||
// 'old' pricing scheme
|
||||
if (theirPrice.compareTo(ourPrice) < 0)
|
||||
break;
|
||||
}
|
||||
if ((haveAssetId < wantAssetId && theirPrice > ourPrice) || (haveAssetId > wantAssetId && theirPrice < ourPrice))
|
||||
break;
|
||||
|
||||
// Calculate how much we could buy at their price.
|
||||
BigDecimal ourMaxAmount;
|
||||
if (isOurOrderNewPricing)
|
||||
// In 'new' pricing scheme, "amount" is expressed in terms of asset with highest assetID
|
||||
ourMaxAmount = this.getAmountLeft();
|
||||
else
|
||||
// In 'old' pricing scheme, "amount" is expressed in terms of our want-asset.
|
||||
ourMaxAmount = this.getAmountLeft().multiply(theirPrice).setScale(8, RoundingMode.DOWN);
|
||||
LOGGER.trace("ourMaxAmount (max we could trade at their price): " + ourMaxAmount.stripTrailingZeros().toPlainString() + " " + matchingAssetData.getName());
|
||||
// Calculate how much we could buy at their price, "amount" is expressed in terms of asset with highest assetID.
|
||||
long ourMaxAmount = this.getAmountLeft();
|
||||
LOGGER.trace(() -> String.format("ourMaxAmount (max we could trade at their price): %s %s", prettyAmount(ourMaxAmount), matchingAssetData.getName()));
|
||||
|
||||
// How much is remaining available in their order.
|
||||
BigDecimal theirAmountLeft = Order.getAmountLeft(theirOrderData);
|
||||
LOGGER.trace("theirAmountLeft (max amount remaining in their order): " + theirAmountLeft.stripTrailingZeros().toPlainString() + " " + matchingAssetData.getName());
|
||||
long theirAmountLeft = Order.getAmountLeft(theirOrderData);
|
||||
LOGGER.trace(() -> String.format("theirAmountLeft (max amount remaining in their order): %s %s", prettyAmount(theirAmountLeft), matchingAssetData.getName()));
|
||||
|
||||
// So matchable want-asset amount is the minimum of above two values
|
||||
BigDecimal matchedAmount = ourMaxAmount.min(theirAmountLeft);
|
||||
LOGGER.trace("matchedAmount: " + matchedAmount.stripTrailingZeros().toPlainString() + " " + matchingAssetData.getName());
|
||||
long interimMatchedAmount = Math.min(ourMaxAmount, theirAmountLeft);
|
||||
LOGGER.trace(() -> String.format("matchedAmount: %s %s", prettyAmount(interimMatchedAmount), matchingAssetData.getName()));
|
||||
|
||||
// If we can't buy anything then try another order
|
||||
if (matchedAmount.compareTo(BigDecimal.ZERO) <= 0)
|
||||
if (interimMatchedAmount <= 0)
|
||||
continue;
|
||||
|
||||
// Calculate amount granularity, based on price and both assets' divisibility, so that return-amount traded is a valid value (integer or to 8 d.p.)
|
||||
BigDecimal granularity = calculateAmountGranularity(matchingAssetData.getIsDivisible(), returnAssetData.getIsDivisible(), theirOrderData.getPrice());
|
||||
LOGGER.trace("granularity (amount granularity): " + granularity.stripTrailingZeros().toPlainString() + " " + matchingAssetData.getName());
|
||||
long granularity = calculateAmountGranularity(matchingAssetData.isDivisible(), returnAssetData.isDivisible(), theirOrderData.getPrice());
|
||||
LOGGER.trace(() -> String.format("granularity (amount granularity): %s %s", prettyAmount(granularity), matchingAssetData.getName()));
|
||||
|
||||
// Reduce matched amount (if need be) to fit granularity
|
||||
matchedAmount = matchedAmount.subtract(matchedAmount.remainder(granularity));
|
||||
LOGGER.trace("matchedAmount adjusted for granularity: " + matchedAmount.stripTrailingZeros().toPlainString() + " " + matchingAssetData.getName());
|
||||
long matchedAmount = interimMatchedAmount - interimMatchedAmount % granularity;
|
||||
LOGGER.trace(() -> String.format("matchedAmount adjusted for granularity: %s %s", prettyAmount(matchedAmount), matchingAssetData.getName()));
|
||||
|
||||
// If we can't buy anything then try another order
|
||||
if (matchedAmount.compareTo(BigDecimal.ZERO) <= 0)
|
||||
if (matchedAmount <= 0)
|
||||
continue;
|
||||
|
||||
// Safety check
|
||||
if (!matchingAssetData.getIsDivisible() && matchedAmount.stripTrailingZeros().scale() > 0) {
|
||||
Account participant = new PublicKeyAccount(this.repository, theirOrderData.getCreatorPublicKey());
|
||||
|
||||
String message = String.format("Refusing to trade fractional %s [indivisible assetID %d] for %s",
|
||||
matchedAmount.toPlainString(), matchingAssetData.getAssetId(), participant.getAddress());
|
||||
LOGGER.error(message);
|
||||
throw new DataException(message);
|
||||
}
|
||||
checkDivisibility(matchingAssetData, matchedAmount, theirOrderData);
|
||||
|
||||
// Trade can go ahead!
|
||||
|
||||
// Calculate the total cost to us, in return-asset, based on their price
|
||||
BigDecimal returnAmountTraded = matchedAmount.multiply(theirOrderData.getPrice()).setScale(8, RoundingMode.DOWN);
|
||||
LOGGER.trace("returnAmountTraded: " + returnAmountTraded.stripTrailingZeros().toPlainString() + " " + returnAssetData.getName());
|
||||
long returnAmountTraded = Amounts.roundDownScaledMultiply(matchedAmount, theirOrderData.getPrice());
|
||||
LOGGER.trace(() -> String.format("returnAmountTraded: %s %s", prettyAmount(returnAmountTraded), returnAssetData.getName()));
|
||||
|
||||
// Safety check
|
||||
if (!returnAssetData.getIsDivisible() && returnAmountTraded.stripTrailingZeros().scale() > 0) {
|
||||
String message = String.format("Refusing to trade fractional %s [indivisible assetID %d] for %s",
|
||||
returnAmountTraded.toPlainString(), returnAssetData.getAssetId(), creator.getAddress());
|
||||
LOGGER.error(message);
|
||||
throw new DataException(message);
|
||||
}
|
||||
checkDivisibility(returnAssetData, returnAmountTraded, this.orderData);
|
||||
|
||||
BigDecimal tradedWantAmount = (isOurOrderNewPricing && haveAssetId > wantAssetId) ? returnAmountTraded : matchedAmount;
|
||||
BigDecimal tradedHaveAmount = (isOurOrderNewPricing && haveAssetId > wantAssetId) ? matchedAmount : returnAmountTraded;
|
||||
long tradedWantAmount = this.isAmountInWantAsset ? matchedAmount : returnAmountTraded;
|
||||
long tradedHaveAmount = this.isAmountInWantAsset ? returnAmountTraded : matchedAmount;
|
||||
|
||||
// We also need to know how much have-asset to refund based on price improvement ('new' pricing only and only one direction applies)
|
||||
BigDecimal haveAssetRefund = isOurOrderNewPricing && haveAssetId < wantAssetId ? ourPrice.subtract(theirPrice).abs().multiply(matchedAmount).setScale(8, RoundingMode.DOWN) : BigDecimal.ZERO;
|
||||
// We also need to know how much have-asset to refund based on price improvement (only one direction applies)
|
||||
long haveAssetRefund = this.isAmountInWantAsset ? Amounts.roundDownScaledMultiply(matchedAmount, Math.abs(ourPrice - theirPrice)) : 0;
|
||||
|
||||
LOGGER.trace(String.format("We traded %s %s (have-asset) for %s %s (want-asset), saving %s %s (have-asset)",
|
||||
tradedHaveAmount.toPlainString(), haveAssetData.getName(),
|
||||
tradedWantAmount.toPlainString(), wantAssetData.getName(),
|
||||
haveAssetRefund.toPlainString(), haveAssetData.getName()));
|
||||
LOGGER.trace(() -> String.format("We traded %s %s (have-asset) for %s %s (want-asset), saving %s %s (have-asset)",
|
||||
prettyAmount(tradedHaveAmount), haveAssetData.getName(),
|
||||
prettyAmount(tradedWantAmount), wantAssetData.getName(),
|
||||
prettyAmount(haveAssetRefund), haveAssetData.getName()));
|
||||
|
||||
// Construct trade
|
||||
TradeData tradeData = new TradeData(this.orderData.getOrderId(), theirOrderData.getOrderId(),
|
||||
tradedWantAmount, tradedHaveAmount, haveAssetRefund, this.orderData.getTimestamp());
|
||||
|
||||
// Process trade, updating corresponding orders in repository
|
||||
Trade trade = new Trade(this.repository, tradeData);
|
||||
trade.process();
|
||||
|
||||
// Update our order in terms of fulfilment, etc. but do not save into repository as that's handled by Trade above
|
||||
BigDecimal amountFulfilled = isOurOrderNewPricing ? matchedAmount : returnAmountTraded;
|
||||
this.orderData.setFulfilled(this.orderData.getFulfilled().add(amountFulfilled));
|
||||
LOGGER.trace("Updated our order's fulfilled amount to: " + this.orderData.getFulfilled().stripTrailingZeros().toPlainString() + " " + matchingAssetData.getName());
|
||||
LOGGER.trace("Our order's amount remaining: " + this.getAmountLeft().stripTrailingZeros().toPlainString() + " " + matchingAssetData.getName());
|
||||
long amountFulfilled = matchedAmount;
|
||||
this.orderData.setFulfilled(this.orderData.getFulfilled() + amountFulfilled);
|
||||
LOGGER.trace(() -> String.format("Updated our order's fulfilled amount to: %s %s", prettyAmount(this.orderData.getFulfilled()), matchingAssetData.getName()));
|
||||
LOGGER.trace(() -> String.format("Our order's amount remaining: %s %s", prettyAmount(this.getAmountLeft()), matchingAssetData.getName()));
|
||||
|
||||
// Continue on to process other open orders if we still have amount left to match
|
||||
if (this.getAmountLeft().compareTo(BigDecimal.ZERO) <= 0)
|
||||
if (this.getAmountLeft() <= 0)
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Check amount has no fractional part if asset is indivisible.
|
||||
*
|
||||
* @throws DataException if divisibility check fails
|
||||
*/
|
||||
private void checkDivisibility(AssetData assetData, long amount, OrderData orderData) throws DataException {
|
||||
if (assetData.isDivisible() || amount % Amounts.MULTIPLIER == 0)
|
||||
// Asset is divisible or amount has no fractional part
|
||||
return;
|
||||
|
||||
String message = String.format("Refusing to trade fractional %s [indivisible assetID %d] for order %s",
|
||||
prettyAmount(amount), assetData.getAssetId(), Base58.encode(orderData.getOrderId()));
|
||||
LOGGER.error(message);
|
||||
throw new DataException(message);
|
||||
}
|
||||
|
||||
public void orphan() throws DataException {
|
||||
// Orphan trades that occurred as a result of this order
|
||||
for (TradeData tradeData : getTrades())
|
||||
@@ -464,7 +418,7 @@ public class Order {
|
||||
|
||||
// Return asset to creator
|
||||
Account creator = new PublicKeyAccount(this.repository, this.orderData.getCreatorPublicKey());
|
||||
creator.setConfirmedBalance(haveAssetId, creator.getConfirmedBalance(haveAssetId).add(this.calcHaveAssetCommittment()));
|
||||
creator.modifyAssetBalance(haveAssetId, this.calcHaveAssetCommittment());
|
||||
}
|
||||
|
||||
// This is called by CancelOrderTransaction so that an Order can no longer trade
|
||||
@@ -474,14 +428,14 @@ public class Order {
|
||||
|
||||
// Update creator's balance with unfulfilled amount
|
||||
Account creator = new PublicKeyAccount(this.repository, this.orderData.getCreatorPublicKey());
|
||||
creator.setConfirmedBalance(haveAssetId, creator.getConfirmedBalance(haveAssetId).add(calcHaveAssetRefund()));
|
||||
creator.modifyAssetBalance(haveAssetId, calcHaveAssetRefund());
|
||||
}
|
||||
|
||||
// Opposite of cancel() above for use during orphaning
|
||||
public void reopen() throws DataException {
|
||||
// Update creator's balance with unfulfilled amount
|
||||
Account creator = new PublicKeyAccount(this.repository, this.orderData.getCreatorPublicKey());
|
||||
creator.setConfirmedBalance(haveAssetId, creator.getConfirmedBalance(haveAssetId).subtract(calcHaveAssetRefund()));
|
||||
creator.modifyAssetBalance(haveAssetId, - calcHaveAssetRefund());
|
||||
|
||||
this.orderData.setIsClosed(false);
|
||||
this.repository.getAssetRepository().save(this.orderData);
|
||||
|
||||
@@ -1,10 +1,7 @@
|
||||
package org.qortal.asset;
|
||||
|
||||
import java.math.BigDecimal;
|
||||
|
||||
import org.qortal.account.Account;
|
||||
import org.qortal.account.PublicKeyAccount;
|
||||
import org.qortal.block.BlockChain;
|
||||
import org.qortal.data.asset.OrderData;
|
||||
import org.qortal.data.asset.TradeData;
|
||||
import org.qortal.repository.AssetRepository;
|
||||
@@ -17,12 +14,11 @@ public class Trade {
|
||||
private Repository repository;
|
||||
private TradeData tradeData;
|
||||
|
||||
private boolean isNewPricing;
|
||||
private AssetRepository assetRepository;
|
||||
|
||||
private OrderData initiatingOrder;
|
||||
private OrderData targetOrder;
|
||||
private BigDecimal newPricingFulfilled;
|
||||
private long fulfilled;
|
||||
|
||||
// Constructors
|
||||
|
||||
@@ -30,7 +26,6 @@ public class Trade {
|
||||
this.repository = repository;
|
||||
this.tradeData = tradeData;
|
||||
|
||||
this.isNewPricing = this.tradeData.getTimestamp() > BlockChain.getInstance().getNewAssetPricingTimestamp();
|
||||
this.assetRepository = this.repository.getAssetRepository();
|
||||
}
|
||||
|
||||
@@ -43,9 +38,9 @@ public class Trade {
|
||||
// Note: targetAmount is amount traded FROM target order
|
||||
// Note: initiatorAmount is amount traded FROM initiating order
|
||||
|
||||
// Under 'new' pricing scheme, "amount" and "fulfilled" are the same asset for both orders
|
||||
// "amount" and "fulfilled" are the same asset for both orders
|
||||
// which is the matchedAmount in asset with highest assetID
|
||||
this.newPricingFulfilled = (initiatingOrder.getHaveAssetId() < initiatingOrder.getWantAssetId()) ? this.tradeData.getTargetAmount() : this.tradeData.getInitiatorAmount();
|
||||
this.fulfilled = initiatingOrder.getHaveAssetId() < initiatingOrder.getWantAssetId() ? this.tradeData.getTargetAmount() : this.tradeData.getInitiatorAmount();
|
||||
}
|
||||
|
||||
public void process() throws DataException {
|
||||
@@ -55,16 +50,16 @@ public class Trade {
|
||||
// Note: targetAmount is amount traded FROM target order
|
||||
// Note: initiatorAmount is amount traded FROM initiating order
|
||||
|
||||
// Update corresponding Orders on both sides of trade
|
||||
commonPrep();
|
||||
|
||||
initiatingOrder.setFulfilled(initiatingOrder.getFulfilled().add(isNewPricing ? newPricingFulfilled : tradeData.getInitiatorAmount()));
|
||||
// Update corresponding Orders on both sides of trade
|
||||
initiatingOrder.setFulfilled(initiatingOrder.getFulfilled() + fulfilled);
|
||||
initiatingOrder.setIsFulfilled(Order.isFulfilled(initiatingOrder));
|
||||
// Set isClosed to true if isFulfilled now true
|
||||
initiatingOrder.setIsClosed(initiatingOrder.getIsFulfilled());
|
||||
assetRepository.save(initiatingOrder);
|
||||
|
||||
targetOrder.setFulfilled(targetOrder.getFulfilled().add(isNewPricing ? newPricingFulfilled : tradeData.getTargetAmount()));
|
||||
targetOrder.setFulfilled(targetOrder.getFulfilled() + fulfilled);
|
||||
targetOrder.setIsFulfilled(Order.isFulfilled(targetOrder));
|
||||
// Set isClosed to true if isFulfilled now true
|
||||
targetOrder.setIsClosed(targetOrder.getIsFulfilled());
|
||||
@@ -72,33 +67,31 @@ public class Trade {
|
||||
|
||||
// Actually transfer asset balances
|
||||
Account initiatingCreator = new PublicKeyAccount(this.repository, initiatingOrder.getCreatorPublicKey());
|
||||
initiatingCreator.setConfirmedBalance(initiatingOrder.getWantAssetId(), initiatingCreator.getConfirmedBalance(initiatingOrder.getWantAssetId()).add(tradeData.getTargetAmount()));
|
||||
initiatingCreator.modifyAssetBalance(initiatingOrder.getWantAssetId(), tradeData.getTargetAmount());
|
||||
|
||||
Account targetCreator = new PublicKeyAccount(this.repository, targetOrder.getCreatorPublicKey());
|
||||
targetCreator.setConfirmedBalance(targetOrder.getWantAssetId(), targetCreator.getConfirmedBalance(targetOrder.getWantAssetId()).add(tradeData.getInitiatorAmount()));
|
||||
targetCreator.modifyAssetBalance(targetOrder.getWantAssetId(), tradeData.getInitiatorAmount());
|
||||
|
||||
// Possible partial saving to refund to initiator
|
||||
BigDecimal initiatorSaving = this.tradeData.getInitiatorSaving();
|
||||
if (initiatorSaving.compareTo(BigDecimal.ZERO) > 0)
|
||||
initiatingCreator.setConfirmedBalance(initiatingOrder.getHaveAssetId(), initiatingCreator.getConfirmedBalance(initiatingOrder.getHaveAssetId()).add(initiatorSaving));
|
||||
long initiatorSaving = this.tradeData.getInitiatorSaving();
|
||||
if (initiatorSaving > 0)
|
||||
initiatingCreator.modifyAssetBalance(initiatingOrder.getHaveAssetId(), initiatorSaving);
|
||||
}
|
||||
|
||||
public void orphan() throws DataException {
|
||||
AssetRepository assetRepository = this.repository.getAssetRepository();
|
||||
|
||||
// Note: targetAmount is amount traded FROM target order
|
||||
// Note: initiatorAmount is amount traded FROM initiating order
|
||||
|
||||
// Revert corresponding Orders on both sides of trade
|
||||
commonPrep();
|
||||
|
||||
initiatingOrder.setFulfilled(initiatingOrder.getFulfilled().subtract(isNewPricing ? newPricingFulfilled : tradeData.getInitiatorAmount()));
|
||||
// Revert corresponding Orders on both sides of trade
|
||||
initiatingOrder.setFulfilled(initiatingOrder.getFulfilled() - fulfilled);
|
||||
initiatingOrder.setIsFulfilled(Order.isFulfilled(initiatingOrder));
|
||||
// Set isClosed to false if isFulfilled now false
|
||||
initiatingOrder.setIsClosed(initiatingOrder.getIsFulfilled());
|
||||
assetRepository.save(initiatingOrder);
|
||||
|
||||
targetOrder.setFulfilled(targetOrder.getFulfilled().subtract(isNewPricing ? newPricingFulfilled : tradeData.getTargetAmount()));
|
||||
targetOrder.setFulfilled(targetOrder.getFulfilled() - fulfilled);
|
||||
targetOrder.setIsFulfilled(Order.isFulfilled(targetOrder));
|
||||
// Set isClosed to false if isFulfilled now false
|
||||
targetOrder.setIsClosed(targetOrder.getIsFulfilled());
|
||||
@@ -106,15 +99,15 @@ public class Trade {
|
||||
|
||||
// Reverse asset transfers
|
||||
Account initiatingCreator = new PublicKeyAccount(this.repository, initiatingOrder.getCreatorPublicKey());
|
||||
initiatingCreator.setConfirmedBalance(initiatingOrder.getWantAssetId(), initiatingCreator.getConfirmedBalance(initiatingOrder.getWantAssetId()).subtract(tradeData.getTargetAmount()));
|
||||
initiatingCreator.modifyAssetBalance(initiatingOrder.getWantAssetId(), - tradeData.getTargetAmount());
|
||||
|
||||
Account targetCreator = new PublicKeyAccount(this.repository, targetOrder.getCreatorPublicKey());
|
||||
targetCreator.setConfirmedBalance(targetOrder.getWantAssetId(), targetCreator.getConfirmedBalance(targetOrder.getWantAssetId()).subtract(tradeData.getInitiatorAmount()));
|
||||
targetCreator.modifyAssetBalance(targetOrder.getWantAssetId(), - tradeData.getInitiatorAmount());
|
||||
|
||||
// Possible partial saving to claw back from initiator
|
||||
BigDecimal initiatorSaving = this.tradeData.getInitiatorSaving();
|
||||
if (initiatorSaving.compareTo(BigDecimal.ZERO) > 0)
|
||||
initiatingCreator.setConfirmedBalance(initiatingOrder.getHaveAssetId(), initiatingCreator.getConfirmedBalance(initiatingOrder.getHaveAssetId()).subtract(initiatorSaving));
|
||||
long initiatorSaving = this.tradeData.getInitiatorSaving();
|
||||
if (initiatorSaving > 0)
|
||||
initiatingCreator.modifyAssetBalance(initiatingOrder.getHaveAssetId(), - initiatorSaving);
|
||||
|
||||
// Remove trade from repository
|
||||
assetRepository.delete(tradeData);
|
||||
|
||||
@@ -1,11 +1,9 @@
|
||||
package org.qortal.at;
|
||||
|
||||
import java.math.BigDecimal;
|
||||
import java.nio.ByteBuffer;
|
||||
import java.util.List;
|
||||
|
||||
import org.ciyam.at.MachineState;
|
||||
import org.qortal.asset.Asset;
|
||||
import org.ciyam.at.Timestamp;
|
||||
import org.qortal.crypto.Crypto;
|
||||
import org.qortal.data.at.ATData;
|
||||
import org.qortal.data.at.ATStateData;
|
||||
@@ -42,66 +40,27 @@ public class AT {
|
||||
int height = this.repository.getBlockRepository().getBlockchainHeight() + 1;
|
||||
byte[] creatorPublicKey = deployATTransactionData.getCreatorPublicKey();
|
||||
long creation = deployATTransactionData.getTimestamp();
|
||||
|
||||
byte[] creationBytes = deployATTransactionData.getCreationBytes();
|
||||
long assetId = deployATTransactionData.getAssetId();
|
||||
short version = (short) ((creationBytes[0] & 0xff) | (creationBytes[1] << 8)); // Little-endian
|
||||
|
||||
if (version >= 2) {
|
||||
MachineState machineState = new MachineState(deployATTransactionData.getCreationBytes());
|
||||
// Just enough AT data to allow API to query initial balances, etc.
|
||||
ATData skeletonAtData = new ATData(atAddress, creatorPublicKey, creation, assetId);
|
||||
|
||||
this.atData = new ATData(atAddress, creatorPublicKey, creation, machineState.version, assetId, machineState.getCodeBytes(),
|
||||
machineState.getIsSleeping(), machineState.getSleepUntilHeight(), machineState.getIsFinished(), machineState.getHadFatalError(),
|
||||
machineState.getIsFrozen(), machineState.getFrozenBalance());
|
||||
long blockTimestamp = Timestamp.toLong(height, 0);
|
||||
QortalATAPI api = new QortalATAPI(repository, skeletonAtData, blockTimestamp);
|
||||
QortalAtLoggerFactory loggerFactory = QortalAtLoggerFactory.getInstance();
|
||||
|
||||
byte[] stateData = machineState.toBytes();
|
||||
byte[] stateHash = Crypto.digest(stateData);
|
||||
MachineState machineState = new MachineState(api, loggerFactory, deployATTransactionData.getCreationBytes());
|
||||
|
||||
this.atStateData = new ATStateData(atAddress, height, creation, stateData, stateHash, BigDecimal.ZERO.setScale(8));
|
||||
} else {
|
||||
// Legacy v1 AT
|
||||
// We would deploy these in 'dead' state as they will never be run on Qortal
|
||||
// but this breaks import from Qora1 so something else will have to mark them dead at hard-fork
|
||||
byte[] codeHash = Crypto.digest(machineState.getCodeBytes());
|
||||
|
||||
// Extract code bytes length
|
||||
ByteBuffer byteBuffer = ByteBuffer.wrap(deployATTransactionData.getCreationBytes());
|
||||
this.atData = new ATData(atAddress, creatorPublicKey, creation, machineState.version, assetId, machineState.getCodeBytes(), codeHash,
|
||||
machineState.isSleeping(), machineState.getSleepUntilHeight(), machineState.isFinished(), machineState.hadFatalError(),
|
||||
machineState.isFrozen(), machineState.getFrozenBalance());
|
||||
|
||||
// v1 AT header is: version, reserved, code-pages, data-pages, call-stack-pages, user-stack-pages (all shorts)
|
||||
byte[] stateData = machineState.toBytes();
|
||||
byte[] stateHash = Crypto.digest(stateData);
|
||||
|
||||
// Number of code pages
|
||||
short numCodePages = byteBuffer.get(2 + 2);
|
||||
|
||||
// Skip header and also "minimum activation amount" (long)
|
||||
byteBuffer.position(6 * 2 + 8);
|
||||
|
||||
int codeLen = 0;
|
||||
|
||||
// Extract actual code length, stored in minimal-size form (byte, short or int)
|
||||
if (numCodePages * 256 < 257) {
|
||||
codeLen = byteBuffer.get() & 0xff;
|
||||
} else if (numCodePages * 256 < Short.MAX_VALUE + 1) {
|
||||
codeLen = byteBuffer.getShort() & 0xffff;
|
||||
} else if (numCodePages * 256 <= Integer.MAX_VALUE) {
|
||||
codeLen = byteBuffer.getInt();
|
||||
}
|
||||
|
||||
// Extract code bytes
|
||||
byte[] codeBytes = new byte[codeLen];
|
||||
byteBuffer.get(codeBytes);
|
||||
|
||||
// Create AT
|
||||
boolean isSleeping = false;
|
||||
Integer sleepUntilHeight = null;
|
||||
boolean isFinished = false;
|
||||
boolean hadFatalError = false;
|
||||
boolean isFrozen = false;
|
||||
Long frozenBalance = null;
|
||||
|
||||
this.atData = new ATData(atAddress, creatorPublicKey, creation, version, Asset.QORT, codeBytes, isSleeping, sleepUntilHeight, isFinished,
|
||||
hadFatalError, isFrozen, frozenBalance);
|
||||
|
||||
this.atStateData = new ATStateData(atAddress, height, creation, null, null, BigDecimal.ZERO.setScale(8));
|
||||
}
|
||||
this.atStateData = new ATStateData(atAddress, height, creation, stateData, stateHash, 0L, true);
|
||||
}
|
||||
|
||||
// Getters / setters
|
||||
@@ -116,9 +75,7 @@ public class AT {
|
||||
ATRepository atRepository = this.repository.getATRepository();
|
||||
atRepository.save(this.atData);
|
||||
|
||||
// For version 2+ we also store initial AT state data
|
||||
if (this.atData.getVersion() >= 2)
|
||||
atRepository.save(this.atStateData);
|
||||
atRepository.save(this.atStateData);
|
||||
}
|
||||
|
||||
public void undeploy() throws DataException {
|
||||
@@ -126,34 +83,89 @@ public class AT {
|
||||
this.repository.getATRepository().delete(this.atData.getATAddress());
|
||||
}
|
||||
|
||||
public List<AtTransaction> run(long blockTimestamp) throws DataException {
|
||||
public List<AtTransaction> run(int blockHeight, long blockTimestamp) throws DataException {
|
||||
String atAddress = this.atData.getATAddress();
|
||||
|
||||
QortalATAPI api = new QortalATAPI(repository, this.atData, blockTimestamp);
|
||||
QortalATLogger logger = new QortalATLogger();
|
||||
QortalAtLoggerFactory loggerFactory = QortalAtLoggerFactory.getInstance();
|
||||
|
||||
byte[] codeBytes = this.atData.getCodeBytes();
|
||||
|
||||
// Fetch latest ATStateData for this AT (if any)
|
||||
// Fetch latest ATStateData for this AT
|
||||
ATStateData latestAtStateData = this.repository.getATRepository().getLatestATState(atAddress);
|
||||
|
||||
// There should be at least initial AT state data
|
||||
// There should be at least initial deployment AT state data
|
||||
if (latestAtStateData == null)
|
||||
throw new IllegalStateException("No initial AT state data found");
|
||||
throw new IllegalStateException("No previous AT state data found");
|
||||
|
||||
// [Re]create AT machine state using AT state data or from scratch as applicable
|
||||
MachineState state = MachineState.fromBytes(api, logger, latestAtStateData.getStateData(), codeBytes);
|
||||
state.execute();
|
||||
MachineState state = MachineState.fromBytes(api, loggerFactory, latestAtStateData.getStateData(), codeBytes);
|
||||
try {
|
||||
state.execute();
|
||||
} catch (Exception e) {
|
||||
throw new DataException(String.format("Uncaught exception while running AT '%s'", atAddress), e);
|
||||
}
|
||||
|
||||
int height = this.repository.getBlockRepository().getBlockchainHeight() + 1;
|
||||
long creation = this.atData.getCreation();
|
||||
byte[] stateData = state.toBytes();
|
||||
byte[] stateHash = Crypto.digest(stateData);
|
||||
BigDecimal atFees = api.calcFinalFees(state);
|
||||
long atFees = api.calcFinalFees(state);
|
||||
|
||||
this.atStateData = new ATStateData(atAddress, height, creation, stateData, stateHash, atFees);
|
||||
this.atStateData = new ATStateData(atAddress, blockHeight, creation, stateData, stateHash, atFees, false);
|
||||
|
||||
return api.getTransactions();
|
||||
}
|
||||
|
||||
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);
|
||||
|
||||
// Save latest AT state data
|
||||
this.repository.getATRepository().save(this.atStateData);
|
||||
|
||||
// Update AT info in repository too
|
||||
this.atData.setIsSleeping(state.isSleeping());
|
||||
this.atData.setSleepUntilHeight(state.getSleepUntilHeight());
|
||||
this.atData.setIsFinished(state.isFinished());
|
||||
this.atData.setHadFatalError(state.hadFatalError());
|
||||
this.atData.setIsFrozen(state.isFrozen());
|
||||
this.atData.setFrozenBalance(state.getFrozenBalance());
|
||||
this.repository.getATRepository().save(this.atData);
|
||||
}
|
||||
|
||||
public void revert(int blockHeight, long blockTimestamp) throws DataException {
|
||||
String atAddress = this.atData.getATAddress();
|
||||
|
||||
// Delete old AT state data from repository
|
||||
this.repository.getATRepository().delete(atAddress, blockHeight);
|
||||
|
||||
if (this.atStateData.isInitial())
|
||||
return;
|
||||
|
||||
// Load previous state data
|
||||
ATStateData previousStateData = this.repository.getATRepository().getLatestATState(atAddress);
|
||||
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);
|
||||
|
||||
// Update AT info in repository
|
||||
this.atData.setIsSleeping(state.isSleeping());
|
||||
this.atData.setSleepUntilHeight(state.getSleepUntilHeight());
|
||||
this.atData.setIsFinished(state.isFinished());
|
||||
this.atData.setHadFatalError(state.hadFatalError());
|
||||
this.atData.setIsFrozen(state.isFrozen());
|
||||
this.atData.setFrozenBalance(state.getFrozenBalance());
|
||||
this.repository.getATRepository().save(this.atData);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -1,134 +0,0 @@
|
||||
package org.qortal.at;
|
||||
|
||||
import static java.util.Arrays.stream;
|
||||
import static java.util.stream.Collectors.toMap;
|
||||
|
||||
import java.math.BigDecimal;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
|
||||
import org.ciyam.at.MachineState;
|
||||
import org.ciyam.at.Timestamp;
|
||||
import org.qortal.account.Account;
|
||||
import org.qortal.block.Block;
|
||||
import org.qortal.data.block.BlockData;
|
||||
import org.qortal.data.transaction.ATTransactionData;
|
||||
import org.qortal.data.transaction.PaymentTransactionData;
|
||||
import org.qortal.data.transaction.TransactionData;
|
||||
import org.qortal.repository.BlockRepository;
|
||||
import org.qortal.repository.DataException;
|
||||
import org.qortal.transaction.Transaction;
|
||||
|
||||
public enum BlockchainAPI {
|
||||
|
||||
QORTAL(0) {
|
||||
@Override
|
||||
public void putTransactionFromRecipientAfterTimestampInA(String recipient, Timestamp timestamp, MachineState state) {
|
||||
int height = timestamp.blockHeight;
|
||||
int sequence = timestamp.transactionSequence + 1;
|
||||
|
||||
QortalATAPI api = (QortalATAPI) state.getAPI();
|
||||
BlockRepository blockRepository = api.repository.getBlockRepository();
|
||||
|
||||
try {
|
||||
Account recipientAccount = new Account(api.repository, recipient);
|
||||
|
||||
while (height <= blockRepository.getBlockchainHeight()) {
|
||||
BlockData blockData = blockRepository.fromHeight(height);
|
||||
|
||||
if (blockData == null)
|
||||
throw new DataException("Unable to fetch block " + height + " from repository?");
|
||||
|
||||
Block block = new Block(api.repository, blockData);
|
||||
|
||||
List<Transaction> transactions = block.getTransactions();
|
||||
|
||||
// No more transactions in this block? Try next block
|
||||
if (sequence >= transactions.size()) {
|
||||
++height;
|
||||
sequence = 0;
|
||||
continue;
|
||||
}
|
||||
|
||||
Transaction transaction = transactions.get(sequence);
|
||||
|
||||
// Transaction needs to be sent to specified recipient
|
||||
if (transaction.getRecipientAccounts().contains(recipientAccount)) {
|
||||
// Found a transaction
|
||||
|
||||
api.setA1(state, new Timestamp(height, timestamp.blockchainId, sequence).longValue());
|
||||
|
||||
// Hash transaction's signature into other three A fields for future verification that it's the same transaction
|
||||
byte[] hash = QortalATAPI.sha192(transaction.getTransactionData().getSignature());
|
||||
|
||||
api.setA2(state, QortalATAPI.fromBytes(hash, 0));
|
||||
api.setA3(state, QortalATAPI.fromBytes(hash, 8));
|
||||
api.setA4(state, QortalATAPI.fromBytes(hash, 16));
|
||||
return;
|
||||
}
|
||||
|
||||
// Transaction wasn't for us - keep going
|
||||
++sequence;
|
||||
}
|
||||
|
||||
// No more transactions - zero A and exit
|
||||
api.zeroA(state);
|
||||
} catch (DataException e) {
|
||||
throw new RuntimeException("AT API unable to fetch next transaction?", e);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public long getAmountFromTransactionInA(Timestamp timestamp, MachineState state) {
|
||||
QortalATAPI api = (QortalATAPI) state.getAPI();
|
||||
TransactionData transactionData = api.fetchTransaction(state);
|
||||
|
||||
switch (transactionData.getType()) {
|
||||
case PAYMENT:
|
||||
return ((PaymentTransactionData) transactionData).getAmount().unscaledValue().longValue();
|
||||
|
||||
case AT:
|
||||
BigDecimal amount = ((ATTransactionData) transactionData).getAmount();
|
||||
|
||||
if (amount != null)
|
||||
return amount.unscaledValue().longValue();
|
||||
else
|
||||
return 0xffffffffffffffffL;
|
||||
|
||||
default:
|
||||
return 0xffffffffffffffffL;
|
||||
}
|
||||
}
|
||||
},
|
||||
BTC(1) {
|
||||
@Override
|
||||
public void putTransactionFromRecipientAfterTimestampInA(String recipient, Timestamp timestamp, MachineState state) {
|
||||
// TODO BTC transaction support for ATv2
|
||||
}
|
||||
|
||||
@Override
|
||||
public long getAmountFromTransactionInA(Timestamp timestamp, MachineState state) {
|
||||
// TODO BTC transaction support for ATv2
|
||||
return 0;
|
||||
}
|
||||
};
|
||||
|
||||
public final int value;
|
||||
|
||||
private static final Map<Integer, BlockchainAPI> map = stream(BlockchainAPI.values()).collect(toMap(type -> type.value, type -> type));
|
||||
|
||||
BlockchainAPI(int value) {
|
||||
this.value = value;
|
||||
}
|
||||
|
||||
public static BlockchainAPI valueOf(int value) {
|
||||
return map.get(value);
|
||||
}
|
||||
|
||||
// Blockchain-specific API methods
|
||||
|
||||
public abstract void putTransactionFromRecipientAfterTimestampInA(String recipient, Timestamp timestamp, MachineState state);
|
||||
|
||||
public abstract long getAmountFromTransactionInA(Timestamp timestamp, MachineState state);
|
||||
|
||||
}
|
||||
@@ -1,11 +1,11 @@
|
||||
package org.qortal.at;
|
||||
|
||||
import java.math.BigDecimal;
|
||||
import java.security.MessageDigest;
|
||||
import java.security.NoSuchAlgorithmException;
|
||||
import java.util.ArrayList;
|
||||
import java.util.Arrays;
|
||||
import java.util.List;
|
||||
|
||||
import org.apache.logging.log4j.LogManager;
|
||||
import org.apache.logging.log4j.Logger;
|
||||
import org.ciyam.at.API;
|
||||
import org.ciyam.at.ExecutionException;
|
||||
import org.ciyam.at.FunctionData;
|
||||
@@ -14,35 +14,42 @@ import org.ciyam.at.MachineState;
|
||||
import org.ciyam.at.OpCode;
|
||||
import org.ciyam.at.Timestamp;
|
||||
import org.qortal.account.Account;
|
||||
import org.qortal.account.GenesisAccount;
|
||||
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;
|
||||
import org.qortal.data.at.ATData;
|
||||
import org.qortal.data.block.BlockData;
|
||||
import org.qortal.data.block.BlockSummaryData;
|
||||
import org.qortal.data.transaction.ATTransactionData;
|
||||
import org.qortal.data.transaction.BaseTransactionData;
|
||||
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.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 com.google.common.primitives.Bytes;
|
||||
|
||||
public class QortalATAPI extends API {
|
||||
|
||||
// Useful constants
|
||||
private static final BigDecimal FEE_PER_STEP = BigDecimal.valueOf(1.0).setScale(8); // 1 QORT per "step"
|
||||
private static final int MAX_STEPS_PER_ROUND = 500;
|
||||
private static final int STEPS_PER_FUNCTION_CALL = 10;
|
||||
private static final int MINUTES_PER_BLOCK = 10;
|
||||
private static final byte[] ADDRESS_PADDING = new byte[32 - Account.ADDRESS_LENGTH];
|
||||
private static final Logger LOGGER = LogManager.getLogger(QortalATAPI.class);
|
||||
|
||||
// Properties
|
||||
Repository repository;
|
||||
ATData atData;
|
||||
long blockTimestamp;
|
||||
private Repository repository;
|
||||
private ATData atData;
|
||||
private long blockTimestamp;
|
||||
private final CiyamAtSettings ciyamAtSettings;
|
||||
|
||||
/** List of generated AT transactions */
|
||||
List<AtTransaction> transactions;
|
||||
@@ -54,36 +61,42 @@ public class QortalATAPI extends API {
|
||||
this.atData = atData;
|
||||
this.transactions = new ArrayList<>();
|
||||
this.blockTimestamp = blockTimestamp;
|
||||
|
||||
this.ciyamAtSettings = BlockChain.getInstance().getCiyamAtSettings();
|
||||
}
|
||||
|
||||
// Methods specific to Qortal AT processing, not inherited
|
||||
|
||||
public Repository getRepository() {
|
||||
return this.repository;
|
||||
}
|
||||
|
||||
public List<AtTransaction> getTransactions() {
|
||||
return this.transactions;
|
||||
}
|
||||
|
||||
public BigDecimal calcFinalFees(MachineState state) {
|
||||
return FEE_PER_STEP.multiply(BigDecimal.valueOf(state.getSteps()));
|
||||
public long calcFinalFees(MachineState state) {
|
||||
return state.getSteps() * this.ciyamAtSettings.feePerStep;
|
||||
}
|
||||
|
||||
// Inherited methods from CIYAM AT API
|
||||
|
||||
@Override
|
||||
public int getMaxStepsPerRound() {
|
||||
return MAX_STEPS_PER_ROUND;
|
||||
return this.ciyamAtSettings.maxStepsPerRound;
|
||||
}
|
||||
|
||||
@Override
|
||||
public int getOpCodeSteps(OpCode opcode) {
|
||||
if (opcode.value >= OpCode.EXT_FUN.value && opcode.value <= OpCode.EXT_FUN_RET_DAT_2.value)
|
||||
return STEPS_PER_FUNCTION_CALL;
|
||||
return this.ciyamAtSettings.stepsPerFunctionCall;
|
||||
|
||||
return 1;
|
||||
}
|
||||
|
||||
@Override
|
||||
public long getFeePerStep() {
|
||||
return FEE_PER_STEP.unscaledValue().longValue();
|
||||
return this.ciyamAtSettings.feePerStep;
|
||||
}
|
||||
|
||||
@Override
|
||||
@@ -105,31 +118,95 @@ public class QortalATAPI extends API {
|
||||
}
|
||||
|
||||
@Override
|
||||
public void putPreviousBlockHashInA(MachineState state) {
|
||||
public void putPreviousBlockHashIntoA(MachineState state) {
|
||||
try {
|
||||
BlockData blockData = this.repository.getBlockRepository().fromHeight(this.getPreviousBlockHeight());
|
||||
int previousBlockHeight = this.repository.getBlockRepository().getBlockchainHeight() - 1;
|
||||
|
||||
// We only need signature, so only request a block summary
|
||||
List<BlockSummaryData> blockSummaries = this.repository.getBlockRepository().getBlockSummaries(previousBlockHeight, previousBlockHeight);
|
||||
if (blockSummaries == null || blockSummaries.size() != 1)
|
||||
throw new RuntimeException("AT API unable to fetch previous block hash?");
|
||||
|
||||
// Block's signature is 128 bytes so we need to reduce this to 4 longs (32 bytes)
|
||||
byte[] blockHash = Crypto.digest(blockData.getSignature());
|
||||
// To be able to use hash to look up block, save height (8 bytes) and partial signature (24 bytes)
|
||||
this.setA1(state, previousBlockHeight);
|
||||
|
||||
this.setA(state, blockHash);
|
||||
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));
|
||||
} catch (DataException e) {
|
||||
throw new RuntimeException("AT API unable to fetch previous block?", e);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void putTransactionAfterTimestampInA(Timestamp timestamp, MachineState state) {
|
||||
public void putTransactionAfterTimestampIntoA(Timestamp timestamp, MachineState state) {
|
||||
// Recipient is this AT
|
||||
String recipient = this.atData.getATAddress();
|
||||
String atAddress = this.atData.getATAddress();
|
||||
|
||||
BlockchainAPI blockchainAPI = BlockchainAPI.valueOf(timestamp.blockchainId);
|
||||
blockchainAPI.putTransactionFromRecipientAfterTimestampInA(recipient, timestamp, state);
|
||||
int height = timestamp.blockHeight;
|
||||
int sequence = timestamp.transactionSequence + 1;
|
||||
|
||||
BlockRepository blockRepository = this.getRepository().getBlockRepository();
|
||||
|
||||
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);
|
||||
} catch (DataException e) {
|
||||
throw new RuntimeException("AT API unable to fetch next transaction?", e);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public long getTypeFromTransactionInA(MachineState state) {
|
||||
TransactionData transactionData = this.fetchTransaction(state);
|
||||
TransactionData transactionData = this.getTransactionFromA(state);
|
||||
|
||||
switch (transactionData.getType()) {
|
||||
case PAYMENT:
|
||||
@@ -151,22 +228,36 @@ public class QortalATAPI extends API {
|
||||
|
||||
@Override
|
||||
public long getAmountFromTransactionInA(MachineState state) {
|
||||
Timestamp timestamp = new Timestamp(state.getA1());
|
||||
BlockchainAPI blockchainAPI = BlockchainAPI.valueOf(timestamp.blockchainId);
|
||||
return blockchainAPI.getAmountFromTransactionInA(timestamp, state);
|
||||
TransactionData transactionData = this.getTransactionFromA(state);
|
||||
|
||||
switch (transactionData.getType()) {
|
||||
case PAYMENT:
|
||||
return ((PaymentTransactionData) transactionData).getAmount();
|
||||
|
||||
case AT:
|
||||
Long amount = ((ATTransactionData) transactionData).getAmount();
|
||||
|
||||
if (amount != null)
|
||||
return amount;
|
||||
|
||||
// fall-through to default
|
||||
|
||||
default:
|
||||
return 0xffffffffffffffffL;
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public long getTimestampFromTransactionInA(MachineState state) {
|
||||
// Transaction's "timestamp" already stored in A1
|
||||
Timestamp timestamp = new Timestamp(state.getA1());
|
||||
Timestamp timestamp = new Timestamp(this.getA1(state));
|
||||
return timestamp.longValue();
|
||||
}
|
||||
|
||||
@Override
|
||||
public long generateRandomUsingTransactionInA(MachineState state) {
|
||||
// The plan here is to sleep for a block then use next block's signature and this transaction's signature to generate pseudo-random, but deterministic,
|
||||
// value.
|
||||
// The plan here is to sleep for a block then use next block's signature
|
||||
// and this transaction's signature to generate pseudo-random, but deterministic, value.
|
||||
|
||||
if (!isFirstOpCodeAfterSleeping(state)) {
|
||||
// First call
|
||||
@@ -179,7 +270,7 @@ public class QortalATAPI extends API {
|
||||
// Second call
|
||||
|
||||
// HASH(A and new block hash)
|
||||
TransactionData transactionData = this.fetchTransaction(state);
|
||||
TransactionData transactionData = this.getTransactionFromA(state);
|
||||
|
||||
try {
|
||||
BlockData blockData = this.repository.getBlockRepository().getLastBlock();
|
||||
@@ -203,7 +294,7 @@ public class QortalATAPI extends API {
|
||||
// Zero B in case of issues or shorter-than-B message
|
||||
this.zeroB(state);
|
||||
|
||||
TransactionData transactionData = this.fetchTransaction(state);
|
||||
TransactionData transactionData = this.getTransactionFromA(state);
|
||||
|
||||
byte[] messageData = null;
|
||||
|
||||
@@ -233,20 +324,36 @@ public class QortalATAPI extends API {
|
||||
|
||||
@Override
|
||||
public void putAddressFromTransactionInAIntoB(MachineState state) {
|
||||
TransactionData transactionData = this.fetchTransaction(state);
|
||||
TransactionData transactionData = this.getTransactionFromA(state);
|
||||
|
||||
// We actually use public key as it has more potential utility (e.g. message verification) than an address
|
||||
byte[] bytes = transactionData.getCreatorPublicKey();
|
||||
String address;
|
||||
if (transactionData.getType() == TransactionType.AT) {
|
||||
// Use AT address from transaction data, as transaction's public key will always be fake
|
||||
address = ((ATTransactionData) transactionData).getATAddress();
|
||||
} else {
|
||||
byte[] publicKey = transactionData.getCreatorPublicKey();
|
||||
address = Crypto.toAddress(publicKey);
|
||||
}
|
||||
|
||||
this.setB(state, bytes);
|
||||
// Convert to byte form as this only takes 25 bytes,
|
||||
// compared to string-form's 34 bytes,
|
||||
// and we only have 32 bytes available.
|
||||
byte[] addressBytes = Bytes.ensureCapacity(Base58.decode(address), 32, 0); // pad to 32 bytes
|
||||
|
||||
this.setB(state, addressBytes);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void putCreatorAddressIntoB(MachineState state) {
|
||||
// We actually use public key as it has more potential utility (e.g. message verification) than an address
|
||||
byte[] bytes = atData.getCreatorPublicKey();
|
||||
byte[] publicKey = atData.getCreatorPublicKey();
|
||||
String address = Crypto.toAddress(publicKey);
|
||||
|
||||
this.setB(state, bytes);
|
||||
// Convert to byte form as this only takes 25 bytes,
|
||||
// compared to string-form's 34 bytes,
|
||||
// and we only have 32 bytes available.
|
||||
byte[] addressBytes = Bytes.ensureCapacity(Base58.decode(address), 32, 0); // pad to 32 bytes
|
||||
|
||||
this.setB(state, addressBytes);
|
||||
}
|
||||
|
||||
@Override
|
||||
@@ -254,25 +361,22 @@ public class QortalATAPI extends API {
|
||||
try {
|
||||
Account atAccount = this.getATAccount();
|
||||
|
||||
return atAccount.getConfirmedBalance(Asset.QORT).unscaledValue().longValue();
|
||||
return atAccount.getConfirmedBalance(Asset.QORT);
|
||||
} catch (DataException e) {
|
||||
throw new RuntimeException("AT API unable to fetch AT's current balance?", e);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void payAmountToB(long unscaledAmount, MachineState state) {
|
||||
byte[] publicKey = state.getB();
|
||||
|
||||
PublicKeyAccount recipient = new PublicKeyAccount(this.repository, publicKey);
|
||||
public void payAmountToB(long amount, MachineState state) {
|
||||
Account recipient = getAccountFromB(state);
|
||||
|
||||
long timestamp = this.getNextTransactionTimestamp();
|
||||
byte[] reference = this.getLastReference();
|
||||
BigDecimal amount = BigDecimal.valueOf(unscaledAmount, 8);
|
||||
|
||||
BaseTransactionData baseTransactionData = new BaseTransactionData(timestamp, Group.NO_GROUP, reference, GenesisAccount.PUBLIC_KEY, BigDecimal.ZERO, null);
|
||||
BaseTransactionData baseTransactionData = new BaseTransactionData(timestamp, Group.NO_GROUP, reference, NullAccount.PUBLIC_KEY, 0L, null);
|
||||
ATTransactionData atTransactionData = new ATTransactionData(baseTransactionData, this.atData.getATAddress(),
|
||||
recipient.getAddress(), amount, this.atData.getAssetId(), new byte[0]);
|
||||
recipient.getAddress(), amount, this.atData.getAssetId());
|
||||
AtTransaction atTransaction = new AtTransaction(this.repository, atTransactionData);
|
||||
|
||||
// Add to our transactions
|
||||
@@ -281,17 +385,15 @@ public class QortalATAPI extends API {
|
||||
|
||||
@Override
|
||||
public void messageAToB(MachineState state) {
|
||||
byte[] message = state.getA();
|
||||
byte[] publicKey = state.getB();
|
||||
|
||||
PublicKeyAccount recipient = new PublicKeyAccount(this.repository, publicKey);
|
||||
byte[] message = this.getA(state);
|
||||
Account recipient = getAccountFromB(state);
|
||||
|
||||
long timestamp = this.getNextTransactionTimestamp();
|
||||
byte[] reference = this.getLastReference();
|
||||
|
||||
BaseTransactionData baseTransactionData = new BaseTransactionData(timestamp, Group.NO_GROUP, reference, GenesisAccount.PUBLIC_KEY, BigDecimal.ZERO, null);
|
||||
BaseTransactionData baseTransactionData = new BaseTransactionData(timestamp, Group.NO_GROUP, reference, NullAccount.PUBLIC_KEY, 0L, null);
|
||||
ATTransactionData atTransactionData = new ATTransactionData(baseTransactionData, this.atData.getATAddress(),
|
||||
recipient.getAddress(), BigDecimal.ZERO, this.atData.getAssetId(), message);
|
||||
recipient.getAddress(), message);
|
||||
AtTransaction atTransaction = new AtTransaction(this.repository, atTransactionData);
|
||||
|
||||
// Add to our transactions
|
||||
@@ -303,22 +405,24 @@ public class QortalATAPI extends API {
|
||||
int blockHeight = timestamp.blockHeight;
|
||||
|
||||
// At least one block in the future
|
||||
blockHeight += (minutes / MINUTES_PER_BLOCK) + 1;
|
||||
blockHeight += Math.max(minutes / this.ciyamAtSettings.minutesPerBlock, 1);
|
||||
|
||||
return new Timestamp(blockHeight, 0).longValue();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onFinished(long finalBalance, MachineState state) {
|
||||
if (finalBalance <= 0)
|
||||
return;
|
||||
|
||||
// Refund remaining balance (if any) to AT's creator
|
||||
Account creator = this.getCreator();
|
||||
long timestamp = this.getNextTransactionTimestamp();
|
||||
byte[] reference = this.getLastReference();
|
||||
BigDecimal amount = BigDecimal.valueOf(finalBalance, 8);
|
||||
|
||||
BaseTransactionData baseTransactionData = new BaseTransactionData(timestamp, Group.NO_GROUP, reference, GenesisAccount.PUBLIC_KEY, BigDecimal.ZERO, null);
|
||||
BaseTransactionData baseTransactionData = new BaseTransactionData(timestamp, Group.NO_GROUP, reference, NullAccount.PUBLIC_KEY, 0L, null);
|
||||
ATTransactionData atTransactionData = new ATTransactionData(baseTransactionData, this.atData.getATAddress(),
|
||||
creator.getAddress(), amount, this.atData.getAssetId(), new byte[0]);
|
||||
creator.getAddress(), finalBalance, this.atData.getAssetId());
|
||||
AtTransaction atTransaction = new AtTransaction(this.repository, atTransactionData);
|
||||
|
||||
// Add to our transactions
|
||||
@@ -327,7 +431,7 @@ public class QortalATAPI extends API {
|
||||
|
||||
@Override
|
||||
public void onFatalError(MachineState state, ExecutionException e) {
|
||||
state.getLogger().error("AT " + this.atData.getATAddress() + " suffered fatal error: " + e.getMessage());
|
||||
LOGGER.error("AT " + this.atData.getATAddress() + " suffered fatal error: " + e.getMessage());
|
||||
}
|
||||
|
||||
@Override
|
||||
@@ -338,13 +442,16 @@ public class QortalATAPI extends API {
|
||||
if (qortalFunctionCode == null)
|
||||
throw new IllegalFunctionCodeException("Unknown Qortal function code 0x" + String.format("%04x", rawFunctionCode) + " encountered");
|
||||
|
||||
qortalFunctionCode.preExecuteCheck(2, true, state, rawFunctionCode);
|
||||
qortalFunctionCode.preExecuteCheck(paramCount, returnValueExpected, rawFunctionCode);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void platformSpecificPostCheckExecute(FunctionData functionData, MachineState state, short rawFunctionCode) throws ExecutionException {
|
||||
QortalFunctionCode qortalFunctionCode = QortalFunctionCode.valueOf(rawFunctionCode);
|
||||
|
||||
if (qortalFunctionCode == null)
|
||||
throw new IllegalFunctionCodeException("Unknown Qortal function code 0x" + String.format("%04x", rawFunctionCode) + " encountered");
|
||||
|
||||
qortalFunctionCode.execute(functionData, state, rawFunctionCode);
|
||||
}
|
||||
|
||||
@@ -356,29 +463,23 @@ public class QortalATAPI extends API {
|
||||
| (bytes[start + 4] & 0xffL) << 32 | (bytes[start + 5] & 0xffL) << 40 | (bytes[start + 6] & 0xffL) << 48 | (bytes[start + 7] & 0xffL) << 56;
|
||||
}
|
||||
|
||||
/** Returns SHA2-192 digest of input - used to verify transaction signatures */
|
||||
public static byte[] sha192(byte[] input) {
|
||||
try {
|
||||
// SHA2-192
|
||||
MessageDigest sha192 = MessageDigest.getInstance("SHA-192");
|
||||
return sha192.digest(input);
|
||||
} catch (NoSuchAlgorithmException e) {
|
||||
throw new RuntimeException("SHA-192 not available");
|
||||
}
|
||||
/** 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);
|
||||
}
|
||||
|
||||
/** Verify transaction's SHA2-192 hashed signature matches A2 thru A4 */
|
||||
private static void verifyTransaction(TransactionData transactionData, MachineState state) {
|
||||
// Compare SHA2-192 of transaction's signature against A2 thru A4
|
||||
byte[] hash = sha192(transactionData.getSignature());
|
||||
/** Verify transaction's partial signature matches A2 thru A4 */
|
||||
private void verifyTransaction(TransactionData transactionData, MachineState state) {
|
||||
// Compare end of transaction's signature against A2 thru A4
|
||||
byte[] sig = transactionData.getSignature();
|
||||
|
||||
if (state.getA2() != fromBytes(hash, 0) || state.getA3() != fromBytes(hash, 8) || state.getA4() != fromBytes(hash, 16))
|
||||
if (this.getA2(state) != fromBytes(sig, 8) || this.getA3(state) != fromBytes(sig, 16) || this.getA4(state) != fromBytes(sig, 24))
|
||||
throw new IllegalStateException("Transaction signature in A no longer matches signature from repository");
|
||||
}
|
||||
|
||||
/** Returns transaction data from repository using block height & sequence from A1, checking the transaction signatures match too */
|
||||
/* package */ TransactionData fetchTransaction(MachineState state) {
|
||||
Timestamp timestamp = new Timestamp(state.getA1());
|
||||
/* package */ TransactionData getTransactionFromA(MachineState state) {
|
||||
Timestamp timestamp = new Timestamp(this.getA1(state));
|
||||
|
||||
try {
|
||||
TransactionData transactionData = this.repository.getTransactionRepository().fromHeightAndSequence(timestamp.blockHeight,
|
||||
@@ -409,29 +510,17 @@ public class QortalATAPI extends API {
|
||||
/** Returns the timestamp to use for next AT Transaction */
|
||||
private long getNextTransactionTimestamp() {
|
||||
/*
|
||||
* Timestamp is block's timestamp + position in AT-Transactions list.
|
||||
* Use block's timestamp.
|
||||
*
|
||||
* We need increasing timestamps to preserve transaction order and hence a correct signature-reference chain when the block is processed.
|
||||
*
|
||||
* As Qora blocks must share the same milliseconds component in their timestamps, this allows us to generate up to 1,000 AT-Transactions per AT without
|
||||
* issue.
|
||||
*
|
||||
* As long as ATs are not allowed to generate that many per block, e.g. by limiting maximum steps per execution round, then we should be fine.
|
||||
* This is OK because AT transactions are always generated locally and order is preserved in Transaction.getDataComparator().
|
||||
*/
|
||||
|
||||
// XXX THE ABOVE IS NO LONGER TRUE IN QORTAL!
|
||||
// return this.blockTimestamp + this.transactions.size();
|
||||
throw new RuntimeException("AT timestamp code not fixed!");
|
||||
return this.blockTimestamp;
|
||||
}
|
||||
|
||||
/** Returns AT account's lastReference, taking newly generated ATTransactions into account */
|
||||
/** Returns AT account's lastReference */
|
||||
private byte[] getLastReference() {
|
||||
// Use signature from last AT Transaction we generated
|
||||
if (!this.transactions.isEmpty())
|
||||
return this.transactions.get(this.transactions.size() - 1).getTransactionData().getSignature();
|
||||
|
||||
try {
|
||||
// No transactions yet, so look up AT's account's last reference from repository
|
||||
// Look up AT's account's last reference from repository
|
||||
Account atAccount = this.getATAccount();
|
||||
|
||||
return atAccount.getLastReference();
|
||||
@@ -440,4 +529,38 @@ public class QortalATAPI extends API {
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns Account (possibly PublicKeyAccount) based on value in B.
|
||||
* <p>
|
||||
* If first byte in B starts with either address version bytes,<br>
|
||||
* and bytes 26 to 32 are zero, then use as an address, but only if valid.
|
||||
* <p>
|
||||
* Otherwise, assume B is a public key.
|
||||
*/
|
||||
private Account getAccountFromB(MachineState state) {
|
||||
byte[] bBytes = this.getB(state);
|
||||
|
||||
if ((bBytes[0] == Crypto.ADDRESS_VERSION || bBytes[0] == Crypto.AT_ADDRESS_VERSION)
|
||||
&& Arrays.mismatch(bBytes, Account.ADDRESS_LENGTH, 32, ADDRESS_PADDING, 0, ADDRESS_PADDING.length) == -1) {
|
||||
// Extract only the bytes containing address
|
||||
byte[] addressBytes = Arrays.copyOf(bBytes, Account.ADDRESS_LENGTH);
|
||||
// If address (in byte form) is valid...
|
||||
if (Crypto.isValidAddress(addressBytes))
|
||||
// ...then return an Account using address (converted to Base58
|
||||
return new Account(this.repository, Base58.encode(addressBytes));
|
||||
}
|
||||
|
||||
return new PublicKeyAccount(this.repository, bBytes);
|
||||
}
|
||||
|
||||
/* Convenience methods to allow QortalFunctionCode package-visibility access to A/B-get/set methods. */
|
||||
|
||||
protected byte[] getB(MachineState state) {
|
||||
return super.getB(state);
|
||||
}
|
||||
|
||||
protected void setB(MachineState state, byte[] bBytes) {
|
||||
super.setB(state, bBytes);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -1,26 +0,0 @@
|
||||
package org.qortal.at;
|
||||
|
||||
import org.apache.logging.log4j.LogManager;
|
||||
import org.apache.logging.log4j.Logger;
|
||||
|
||||
public class QortalATLogger implements org.ciyam.at.LoggerInterface {
|
||||
|
||||
// NOTE: We're logging on behalf of org.qortal.at.AT, not ourselves!
|
||||
private static final Logger LOGGER = LogManager.getLogger(AT.class);
|
||||
|
||||
@Override
|
||||
public void error(String message) {
|
||||
LOGGER.error(message);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void debug(String message) {
|
||||
LOGGER.debug(message);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void echo(String message) {
|
||||
LOGGER.info(message);
|
||||
}
|
||||
|
||||
}
|
||||
2182
src/main/java/org/qortal/at/QortalAtLogger.java
Normal file
2182
src/main/java/org/qortal/at/QortalAtLogger.java
Normal file
File diff suppressed because it is too large
Load Diff
24
src/main/java/org/qortal/at/QortalAtLoggerFactory.java
Normal file
24
src/main/java/org/qortal/at/QortalAtLoggerFactory.java
Normal file
@@ -0,0 +1,24 @@
|
||||
package org.qortal.at;
|
||||
|
||||
import org.ciyam.at.AtLogger;
|
||||
|
||||
public class QortalAtLoggerFactory implements org.ciyam.at.AtLoggerFactory {
|
||||
|
||||
private static QortalAtLoggerFactory instance;
|
||||
|
||||
private QortalAtLoggerFactory() {
|
||||
}
|
||||
|
||||
public static synchronized QortalAtLoggerFactory getInstance() {
|
||||
if (instance == null)
|
||||
instance = new QortalAtLoggerFactory();
|
||||
|
||||
return instance;
|
||||
}
|
||||
|
||||
@Override
|
||||
public AtLogger create(final Class<?> loggerName) {
|
||||
return QortalAtLogger.create(loggerName);
|
||||
}
|
||||
|
||||
}
|
||||
@@ -1,15 +1,18 @@
|
||||
package org.qortal.at;
|
||||
|
||||
import java.nio.charset.StandardCharsets;
|
||||
import java.util.Arrays;
|
||||
import java.util.Map;
|
||||
import java.util.stream.Collectors;
|
||||
|
||||
import org.apache.logging.log4j.LogManager;
|
||||
import org.apache.logging.log4j.Logger;
|
||||
import org.ciyam.at.ExecutionException;
|
||||
import org.ciyam.at.FunctionData;
|
||||
import org.ciyam.at.IllegalFunctionCodeException;
|
||||
import org.ciyam.at.MachineState;
|
||||
import org.ciyam.at.Timestamp;
|
||||
import org.qortal.crosschain.BTC;
|
||||
import org.qortal.crypto.Crypto;
|
||||
import org.qortal.settings.Settings;
|
||||
|
||||
/**
|
||||
* Qortal-specific CIYAM-AT Functions.
|
||||
@@ -19,28 +22,43 @@ import org.ciyam.at.Timestamp;
|
||||
*/
|
||||
public enum QortalFunctionCode {
|
||||
/**
|
||||
* <tt>0x0500</tt><br>
|
||||
* Returns current BTC block's "timestamp"
|
||||
* <tt>0x0510</tt><br>
|
||||
* Convert address in B to 20-byte value in LSB of B1, and all of B2 & B3.
|
||||
*/
|
||||
GET_BTC_BLOCK_TIMESTAMP(0x0500, 0, true) {
|
||||
CONVERT_B_TO_PKH(0x0510, 0, false) {
|
||||
@Override
|
||||
protected void postCheckExecute(FunctionData functionData, MachineState state, short rawFunctionCode) throws ExecutionException {
|
||||
functionData.returnValue = Timestamp.toLong(state.getAPI().getCurrentBlockHeight(), BlockchainAPI.BTC.value, 0);
|
||||
// Needs to be 'B' sized
|
||||
byte[] pkh = new byte[32];
|
||||
|
||||
// Copy PKH part of B to last 20 bytes
|
||||
System.arraycopy(getB(state), 32 - 20 - 4, pkh, 32 - 20, 20);
|
||||
|
||||
setB(state, pkh);
|
||||
}
|
||||
},
|
||||
/**
|
||||
* <tt>0x0501</tt><br>
|
||||
* Put transaction from specific recipient after timestamp in A, or zero if none<br>
|
||||
* <tt>0x0511</tt><br>
|
||||
* Convert 20-byte value in LSB of B1, and all of B2 & B3 to P2SH.<br>
|
||||
* P2SH stored in lower 25 bytes of B.
|
||||
*/
|
||||
PUT_TX_FROM_B_RECIPIENT_AFTER_TIMESTAMP_IN_A(0x0501, 1, false) {
|
||||
CONVERT_B_TO_P2SH(0x0511, 0, false) {
|
||||
@Override
|
||||
protected void postCheckExecute(FunctionData functionData, MachineState state, short rawFunctionCode) throws ExecutionException {
|
||||
Timestamp timestamp = new Timestamp(functionData.value2);
|
||||
byte addressPrefix = Settings.getInstance().getBitcoinNet() == BTC.BitcoinNet.MAIN ? 0x05 : (byte) 0xc4;
|
||||
|
||||
String recipient = new String(state.getB(), StandardCharsets.UTF_8);
|
||||
|
||||
BlockchainAPI blockchainAPI = BlockchainAPI.valueOf(timestamp.blockchainId);
|
||||
blockchainAPI.putTransactionFromRecipientAfterTimestampInA(recipient, timestamp, state);
|
||||
convertAddressInB(addressPrefix, state);
|
||||
}
|
||||
},
|
||||
/**
|
||||
* <tt>0x0512</tt><br>
|
||||
* Convert 20-byte value in LSB of B1, and all of B2 & B3 to Qortal address.<br>
|
||||
* Qortal address stored in lower 25 bytes of B.
|
||||
*/
|
||||
CONVERT_B_TO_QORTAL(0x0512, 0, false) {
|
||||
@Override
|
||||
protected void postCheckExecute(FunctionData functionData, MachineState state, short rawFunctionCode) throws ExecutionException {
|
||||
convertAddressInB(Crypto.ADDRESS_VERSION, state);
|
||||
}
|
||||
};
|
||||
|
||||
@@ -48,7 +66,9 @@ public enum QortalFunctionCode {
|
||||
public final int paramCount;
|
||||
public final boolean returnsValue;
|
||||
|
||||
private final static Map<Short, QortalFunctionCode> map = Arrays.stream(QortalFunctionCode.values())
|
||||
private static final Logger LOGGER = LogManager.getLogger(QortalFunctionCode.class);
|
||||
|
||||
private static final Map<Short, QortalFunctionCode> map = Arrays.stream(QortalFunctionCode.values())
|
||||
.collect(Collectors.toMap(functionCode -> functionCode.value, functionCode -> functionCode));
|
||||
|
||||
private QortalFunctionCode(int value, int paramCount, boolean returnsValue) {
|
||||
@@ -61,7 +81,7 @@ public enum QortalFunctionCode {
|
||||
return map.get((short) value);
|
||||
}
|
||||
|
||||
public void preExecuteCheck(int paramCount, boolean returnValueExpected, MachineState state, short rawFunctionCode) throws IllegalFunctionCodeException {
|
||||
public void preExecuteCheck(int paramCount, boolean returnValueExpected, short rawFunctionCode) throws IllegalFunctionCodeException {
|
||||
if (paramCount != this.paramCount)
|
||||
throw new IllegalFunctionCodeException(
|
||||
"Passed paramCount (" + paramCount + ") does not match function's required paramCount (" + this.paramCount + ")");
|
||||
@@ -84,7 +104,7 @@ public enum QortalFunctionCode {
|
||||
*/
|
||||
public void execute(FunctionData functionData, MachineState state, short rawFunctionCode) throws ExecutionException {
|
||||
// Check passed functionData against requirements of this function
|
||||
preExecuteCheck(functionData.paramCount, functionData.returnValueExpected, state, rawFunctionCode);
|
||||
preExecuteCheck(functionData.paramCount, functionData.returnValueExpected, rawFunctionCode);
|
||||
|
||||
if (functionData.paramCount >= 1 && functionData.value1 == null)
|
||||
throw new IllegalFunctionCodeException("Passed value1 is null but function has paramCount of (" + this.paramCount + ")");
|
||||
@@ -92,7 +112,7 @@ public enum QortalFunctionCode {
|
||||
if (functionData.paramCount == 2 && functionData.value2 == null)
|
||||
throw new IllegalFunctionCodeException("Passed value2 is null but function has paramCount of (" + this.paramCount + ")");
|
||||
|
||||
state.getLogger().debug("Function \"" + this.name() + "\"");
|
||||
LOGGER.debug(() -> String.format("Function \"%s\"", this.name()));
|
||||
|
||||
postCheckExecute(functionData, state, rawFunctionCode);
|
||||
}
|
||||
@@ -100,4 +120,29 @@ public enum QortalFunctionCode {
|
||||
/** Actually execute function */
|
||||
protected abstract void postCheckExecute(FunctionData functionData, MachineState state, short rawFunctionCode) throws ExecutionException;
|
||||
|
||||
private static void convertAddressInB(byte addressPrefix, MachineState state) {
|
||||
byte[] addressNoChecksum = new byte[1 + 20];
|
||||
addressNoChecksum[0] = addressPrefix;
|
||||
System.arraycopy(getB(state), 0, addressNoChecksum, 1, 20);
|
||||
|
||||
byte[] checksum = Crypto.doubleDigest(addressNoChecksum);
|
||||
|
||||
// Needs to be 'B' sized
|
||||
byte[] address = new byte[32];
|
||||
System.arraycopy(addressNoChecksum, 0, address, 32 - 1 - 20 - 4, addressNoChecksum.length);
|
||||
System.arraycopy(checksum, 0, address, 32 - 4, 4);
|
||||
|
||||
setB(state, address);
|
||||
}
|
||||
|
||||
private static byte[] getB(MachineState state) {
|
||||
QortalATAPI api = (QortalATAPI) state.getAPI();
|
||||
return api.getB(state);
|
||||
}
|
||||
|
||||
private static void setB(MachineState state, byte[] bBytes) {
|
||||
QortalATAPI api = (QortalATAPI) state.getAPI();
|
||||
api.setB(state, bBytes);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -3,8 +3,6 @@ package org.qortal.block;
|
||||
import java.io.File;
|
||||
import java.io.FileNotFoundException;
|
||||
import java.io.InputStream;
|
||||
import java.math.BigDecimal;
|
||||
import java.math.MathContext;
|
||||
import java.sql.SQLException;
|
||||
import java.util.ArrayList;
|
||||
import java.util.Collections;
|
||||
@@ -56,12 +54,14 @@ public class BlockChain {
|
||||
/** Transaction expiry period, starting from transaction's timestamp, in milliseconds. */
|
||||
private long transactionExpiryPeriod;
|
||||
|
||||
private BigDecimal unitFee;
|
||||
private BigDecimal maxBytesPerUnitFee;
|
||||
private BigDecimal minFeePerByte;
|
||||
@XmlJavaTypeAdapter(value = org.qortal.api.AmountTypeAdapter.class)
|
||||
private long unitFee;
|
||||
|
||||
private int maxBytesPerUnitFee;
|
||||
|
||||
/** Maximum acceptable timestamp disagreement offset in milliseconds. */
|
||||
private long blockTimestampMargin;
|
||||
|
||||
/** Maximum block size, in bytes. */
|
||||
private int maxBlockSize;
|
||||
|
||||
@@ -71,15 +71,6 @@ public class BlockChain {
|
||||
private GenesisBlock.GenesisInfo genesisInfo;
|
||||
|
||||
public enum FeatureTrigger {
|
||||
messageHeight, // block height when MESSAGE transactions are enabled
|
||||
atHeight, // block height when CIYAM automated transactions are enabled
|
||||
assetsTimestamp, // timestamp when assets (issue/transfer/payments) are enabled
|
||||
votingTimestamp, // timestamp when voting is enabled
|
||||
arbitraryTimestamp, // timestamp when arbitrary transactions are enabled
|
||||
powfixTimestamp, // timestamp when various legacy fixes come into effect
|
||||
qortalTimestamp, // timestamp when Qortal changes come into effect
|
||||
newAssetPricingTimestamp, // timestamp when new asset pricing comes into effect
|
||||
groupApprovalTimestamp; // timestamp when transaction approval comes into effect
|
||||
}
|
||||
|
||||
/** Map of which blockchain features are enabled when (height/timestamp) */
|
||||
@@ -95,21 +86,28 @@ public class BlockChain {
|
||||
/** Block rewards by block height */
|
||||
public static class RewardByHeight {
|
||||
public int height;
|
||||
public BigDecimal reward;
|
||||
@XmlJavaTypeAdapter(value = org.qortal.api.AmountTypeAdapter.class)
|
||||
public long reward;
|
||||
}
|
||||
List<RewardByHeight> rewardsByHeight;
|
||||
private List<RewardByHeight> rewardsByHeight;
|
||||
|
||||
/** Share of block reward/fees by account level */
|
||||
public static class ShareByLevel {
|
||||
public static class AccountLevelShareBin {
|
||||
public List<Integer> levels;
|
||||
public BigDecimal share;
|
||||
@XmlJavaTypeAdapter(value = org.qortal.api.AmountTypeAdapter.class)
|
||||
public long share;
|
||||
}
|
||||
List<ShareByLevel> sharesByLevel;
|
||||
private List<AccountLevelShareBin> sharesByLevel;
|
||||
/** Generated lookup of share-bin by account level */
|
||||
private AccountLevelShareBin[] shareBinsByLevel;
|
||||
|
||||
/** Share of block reward/fees to legacy QORA coin holders */
|
||||
BigDecimal qoraHoldersShare;
|
||||
@XmlJavaTypeAdapter(value = org.qortal.api.AmountTypeAdapter.class)
|
||||
private Long qoraHoldersShare;
|
||||
|
||||
/** How many legacy QORA per 1 QORT of block reward. */
|
||||
BigDecimal qoraPerQortReward;
|
||||
@XmlJavaTypeAdapter(value = org.qortal.api.AmountTypeAdapter.class)
|
||||
private Long qoraPerQortReward;
|
||||
|
||||
/**
|
||||
* Number of minted blocks required to reach next level from previous.
|
||||
@@ -120,7 +118,7 @@ public class BlockChain {
|
||||
* Example: if <tt>blocksNeededByLevel[3]</tt> is 200,<br>
|
||||
* then level 3 accounts need to mint 200 blocks to reach level 4.
|
||||
*/
|
||||
List<Integer> blocksNeededByLevel;
|
||||
private List<Integer> blocksNeededByLevel;
|
||||
|
||||
/**
|
||||
* Cumulative number of minted blocks required to reach next level from scratch.
|
||||
@@ -134,7 +132,7 @@ public class BlockChain {
|
||||
* <p>
|
||||
* Should NOT be present in blockchain config file!
|
||||
*/
|
||||
List<Integer> cumulativeBlocksByLevel;
|
||||
private List<Integer> cumulativeBlocksByLevel;
|
||||
|
||||
/** Block times by block height */
|
||||
public static class BlockTimingByHeight {
|
||||
@@ -143,7 +141,7 @@ public class BlockChain {
|
||||
public long deviation; // ms
|
||||
public double power;
|
||||
}
|
||||
List<BlockTimingByHeight> blockTimingsByHeight;
|
||||
private List<BlockTimingByHeight> blockTimingsByHeight;
|
||||
|
||||
private int minAccountLevelToMint = 1;
|
||||
private int minAccountLevelToRewardShare;
|
||||
@@ -155,6 +153,19 @@ public class BlockChain {
|
||||
/** Maximum time to retain online account signatures (ms) for block validity checks, to allow for clock variance. */
|
||||
private long onlineAccountSignaturesMaxLifetime;
|
||||
|
||||
/** Settings relating to CIYAM AT feature. */
|
||||
public static class CiyamAtSettings {
|
||||
/** Fee per step/op-code executed. */
|
||||
@XmlJavaTypeAdapter(value = org.qortal.api.AmountTypeAdapter.class)
|
||||
public long feePerStep;
|
||||
/** Maximum number of steps per execution round, before AT is forced to sleep until next block. */
|
||||
public int maxStepsPerRound;
|
||||
/** How many steps for calling a function. */
|
||||
public int stepsPerFunctionCall;
|
||||
/** Roughly how many minutes per block. */
|
||||
public int minutesPerBlock;
|
||||
}
|
||||
private CiyamAtSettings ciyamAtSettings;
|
||||
|
||||
// Constructors, etc.
|
||||
|
||||
@@ -225,6 +236,19 @@ public class BlockChain {
|
||||
Throwable linkedException = e.getLinkedException();
|
||||
if (linkedException instanceof XMLMarshalException) {
|
||||
String message = ((XMLMarshalException) linkedException).getInternalException().getLocalizedMessage();
|
||||
|
||||
if (message == null && linkedException.getCause() != null && linkedException.getCause().getCause() != null )
|
||||
message = linkedException.getCause().getCause().getLocalizedMessage();
|
||||
|
||||
if (message == null && linkedException.getCause() != null)
|
||||
message = linkedException.getCause().getLocalizedMessage();
|
||||
|
||||
if (message == null)
|
||||
message = linkedException.getLocalizedMessage();
|
||||
|
||||
if (message == null)
|
||||
message = e.getLocalizedMessage();
|
||||
|
||||
LOGGER.error(message);
|
||||
throw new RuntimeException(message, e);
|
||||
}
|
||||
@@ -257,18 +281,14 @@ public class BlockChain {
|
||||
return this.isTestChain;
|
||||
}
|
||||
|
||||
public BigDecimal getUnitFee() {
|
||||
public long getUnitFee() {
|
||||
return this.unitFee;
|
||||
}
|
||||
|
||||
public BigDecimal getMaxBytesPerUnitFee() {
|
||||
public int getMaxBytesPerUnitFee() {
|
||||
return this.maxBytesPerUnitFee;
|
||||
}
|
||||
|
||||
public BigDecimal getMinFeePerByte() {
|
||||
return this.minFeePerByte;
|
||||
}
|
||||
|
||||
public long getTransactionExpiryPeriod() {
|
||||
return this.transactionExpiryPeriod;
|
||||
}
|
||||
@@ -298,10 +318,14 @@ public class BlockChain {
|
||||
return this.rewardsByHeight;
|
||||
}
|
||||
|
||||
public List<ShareByLevel> getBlockSharesByLevel() {
|
||||
public List<AccountLevelShareBin> getAccountLevelShareBins() {
|
||||
return this.sharesByLevel;
|
||||
}
|
||||
|
||||
public AccountLevelShareBin[] getShareBinsByAccountLevel() {
|
||||
return this.shareBinsByLevel;
|
||||
}
|
||||
|
||||
public List<Integer> getBlocksNeededByLevel() {
|
||||
return this.blocksNeededByLevel;
|
||||
}
|
||||
@@ -310,11 +334,11 @@ public class BlockChain {
|
||||
return this.cumulativeBlocksByLevel;
|
||||
}
|
||||
|
||||
public BigDecimal getQoraHoldersShare() {
|
||||
public long getQoraHoldersShare() {
|
||||
return this.qoraHoldersShare;
|
||||
}
|
||||
|
||||
public BigDecimal getQoraPerQortReward() {
|
||||
public long getQoraPerQortReward() {
|
||||
return this.qoraPerQortReward;
|
||||
}
|
||||
|
||||
@@ -342,53 +366,21 @@ public class BlockChain {
|
||||
return this.onlineAccountSignaturesMaxLifetime;
|
||||
}
|
||||
|
||||
public CiyamAtSettings getCiyamAtSettings() {
|
||||
return this.ciyamAtSettings;
|
||||
}
|
||||
|
||||
// Convenience methods for specific blockchain feature triggers
|
||||
|
||||
public long getMessageReleaseHeight() {
|
||||
return featureTriggers.get("messageHeight");
|
||||
}
|
||||
|
||||
public long getATReleaseHeight() {
|
||||
return featureTriggers.get("atHeight");
|
||||
}
|
||||
|
||||
public long getPowFixReleaseTimestamp() {
|
||||
return featureTriggers.get("powfixTimestamp");
|
||||
}
|
||||
|
||||
public long getAssetsReleaseTimestamp() {
|
||||
return featureTriggers.get("assetsTimestamp");
|
||||
}
|
||||
|
||||
public long getVotingReleaseTimestamp() {
|
||||
return featureTriggers.get("votingTimestamp");
|
||||
}
|
||||
|
||||
public long getArbitraryReleaseTimestamp() {
|
||||
return featureTriggers.get("arbitraryTimestamp");
|
||||
}
|
||||
|
||||
public long getQortalTimestamp() {
|
||||
return featureTriggers.get("qortalTimestamp");
|
||||
}
|
||||
|
||||
public long getNewAssetPricingTimestamp() {
|
||||
return featureTriggers.get("newAssetPricingTimestamp");
|
||||
}
|
||||
|
||||
public long getGroupApprovalTimestamp() {
|
||||
return featureTriggers.get("groupApprovalTimestamp");
|
||||
}
|
||||
|
||||
// More complex getters for aspects that change by height or timestamp
|
||||
|
||||
public BigDecimal getRewardAtHeight(int ourHeight) {
|
||||
public long getRewardAtHeight(int ourHeight) {
|
||||
// Scan through for reward at our height
|
||||
for (int i = rewardsByHeight.size() - 1; i >= 0; --i)
|
||||
if (rewardsByHeight.get(i).height <= ourHeight)
|
||||
return rewardsByHeight.get(i).reward;
|
||||
|
||||
return null;
|
||||
return 0;
|
||||
}
|
||||
|
||||
public BlockTimingByHeight getBlockTimingByHeight(int ourHeight) {
|
||||
@@ -437,6 +429,9 @@ public class BlockChain {
|
||||
if (this.founderEffectiveMintingLevel <= 0)
|
||||
Settings.throwValidationError("Invalid/missing \"founderEffectiveMintingLevel\" in blockchain config");
|
||||
|
||||
if (this.ciyamAtSettings == null)
|
||||
Settings.throwValidationError("No \"ciyamAtSettings\" entry found in blockchain config");
|
||||
|
||||
if (this.featureTriggers == null)
|
||||
Settings.throwValidationError("No \"featureTriggers\" entry found in blockchain config");
|
||||
|
||||
@@ -444,15 +439,20 @@ public class BlockChain {
|
||||
for (FeatureTrigger featureTrigger : FeatureTrigger.values())
|
||||
if (!this.featureTriggers.containsKey(featureTrigger.name()))
|
||||
Settings.throwValidationError(String.format("Missing feature trigger \"%s\" in blockchain config", featureTrigger.name()));
|
||||
|
||||
// Check block reward share bounds
|
||||
long totalShare = this.qoraHoldersShare;
|
||||
// Add share percents for account-level-based rewards
|
||||
for (AccountLevelShareBin accountLevelShareBin : this.sharesByLevel)
|
||||
totalShare += accountLevelShareBin.share;
|
||||
|
||||
if (totalShare < 0 || totalShare > 1_00000000L)
|
||||
Settings.throwValidationError("Total non-founder share out of bounds (0<x<1e8)");
|
||||
}
|
||||
|
||||
/** Minor normalization, cached value generation, etc. */
|
||||
private void fixUp() {
|
||||
this.maxBytesPerUnitFee = this.maxBytesPerUnitFee.setScale(8);
|
||||
this.unitFee = this.unitFee.setScale(8);
|
||||
this.minFeePerByte = this.unitFee.divide(this.maxBytesPerUnitFee, MathContext.DECIMAL32);
|
||||
|
||||
// Pre-calculate cumulative blocks required for each level
|
||||
// Calculate cumulative blocks required for each level
|
||||
int cumulativeBlocks = 0;
|
||||
this.cumulativeBlocksByLevel = new ArrayList<>(this.blocksNeededByLevel.size() + 1);
|
||||
for (int level = 0; level <= this.blocksNeededByLevel.size(); ++level) {
|
||||
@@ -462,6 +462,17 @@ public class BlockChain {
|
||||
cumulativeBlocks += this.blocksNeededByLevel.get(level);
|
||||
}
|
||||
|
||||
// Generate lookup-array for account-level share bins
|
||||
AccountLevelShareBin lastAccountLevelShareBin = this.sharesByLevel.get(this.sharesByLevel.size() - 1);
|
||||
final int lastLevel = lastAccountLevelShareBin.levels.get(lastAccountLevelShareBin.levels.size() - 1);
|
||||
this.shareBinsByLevel = new AccountLevelShareBin[lastLevel];
|
||||
|
||||
for (AccountLevelShareBin accountLevelShareBin : this.sharesByLevel)
|
||||
for (int level : accountLevelShareBin.levels)
|
||||
// level 1 stored at index 0, level 2 stored at index 1, etc.
|
||||
// level 0 not allowed
|
||||
this.shareBinsByLevel[level - 1] = accountLevelShareBin;
|
||||
|
||||
// Convert collections to unmodifiable form
|
||||
this.rewardsByHeight = Collections.unmodifiableList(this.rewardsByHeight);
|
||||
this.sharesByLevel = Collections.unmodifiableList(this.sharesByLevel);
|
||||
@@ -499,7 +510,7 @@ public class BlockChain {
|
||||
}
|
||||
}
|
||||
|
||||
private static boolean isGenesisBlockValid() throws DataException {
|
||||
private static boolean isGenesisBlockValid() {
|
||||
try (final Repository repository = RepositoryManager.getRepository()) {
|
||||
BlockRepository blockRepository = repository.getBlockRepository();
|
||||
|
||||
@@ -512,6 +523,8 @@ public class BlockChain {
|
||||
return false;
|
||||
|
||||
return GenesisBlock.isGenesisBlock(blockData);
|
||||
} catch (DataException e) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -5,6 +5,7 @@ import java.util.ArrayList;
|
||||
import java.util.Arrays;
|
||||
import java.util.Iterator;
|
||||
import java.util.List;
|
||||
import java.util.concurrent.TimeUnit;
|
||||
import java.util.concurrent.locks.ReentrantLock;
|
||||
import java.util.stream.Collectors;
|
||||
|
||||
@@ -39,6 +40,8 @@ public class BlockMinter extends Thread {
|
||||
|
||||
// Other properties
|
||||
private static final Logger LOGGER = LogManager.getLogger(BlockMinter.class);
|
||||
private static Long lastLogTimestamp;
|
||||
private static Long logTimeout;
|
||||
|
||||
// Constructors
|
||||
|
||||
@@ -75,20 +78,15 @@ public class BlockMinter extends Thread {
|
||||
boolean isMintingPossible = false;
|
||||
boolean wasMintingPossible = isMintingPossible;
|
||||
while (running) {
|
||||
repository.discardChanges(); // Free repository locks, if any
|
||||
|
||||
if (isMintingPossible != wasMintingPossible)
|
||||
Controller.getInstance().onMintingPossibleChange(isMintingPossible);
|
||||
|
||||
wasMintingPossible = isMintingPossible;
|
||||
|
||||
// Sleep for a while
|
||||
try {
|
||||
repository.discardChanges(); // Free repository locks, if any
|
||||
|
||||
if (isMintingPossible != wasMintingPossible)
|
||||
Controller.getInstance().onMintingPossibleChange(isMintingPossible);
|
||||
|
||||
wasMintingPossible = isMintingPossible;
|
||||
|
||||
Thread.sleep(1000);
|
||||
} catch (InterruptedException e) {
|
||||
// We've been interrupted - time to exit
|
||||
return;
|
||||
}
|
||||
Thread.sleep(1000);
|
||||
|
||||
isMintingPossible = false;
|
||||
|
||||
@@ -130,7 +128,7 @@ public class BlockMinter extends Thread {
|
||||
}
|
||||
}
|
||||
|
||||
List<Peer> peers = Network.getInstance().getUniqueHandshakedPeers();
|
||||
List<Peer> peers = Network.getInstance().getHandshakedPeers();
|
||||
BlockData lastBlockData = blockRepository.getLastBlock();
|
||||
|
||||
// Disregard peers that have "misbehaved" recently
|
||||
@@ -155,6 +153,9 @@ public class BlockMinter extends Thread {
|
||||
if (previousBlock == null || !Arrays.equals(previousBlock.getSignature(), lastBlockData.getSignature())) {
|
||||
previousBlock = new Block(repository, lastBlockData);
|
||||
newBlocks.clear();
|
||||
|
||||
// Reduce log timeout
|
||||
logTimeout = 10 * 1000L;
|
||||
}
|
||||
|
||||
// Discard accounts we have already built blocks with
|
||||
@@ -167,11 +168,23 @@ public class BlockMinter extends Thread {
|
||||
// First block does the AT heavy-lifting
|
||||
if (newBlocks.isEmpty()) {
|
||||
Block newBlock = Block.mint(repository, previousBlock.getBlockData(), mintingAccount);
|
||||
if (newBlock == null) {
|
||||
// For some reason we can't mint right now
|
||||
moderatedLog(() -> LOGGER.error("Couldn't build a to-be-minted block"));
|
||||
continue;
|
||||
}
|
||||
|
||||
newBlocks.add(newBlock);
|
||||
} else {
|
||||
// The blocks for other minters require less effort...
|
||||
Block newBlock = newBlocks.get(0);
|
||||
newBlocks.add(newBlock.remint(mintingAccount));
|
||||
Block newBlock = newBlocks.get(0).remint(mintingAccount);
|
||||
if (newBlock == null) {
|
||||
// For some reason we can't mint right now
|
||||
moderatedLog(() -> LOGGER.error("Couldn't rebuild a to-be-minted block"));
|
||||
continue;
|
||||
}
|
||||
|
||||
newBlocks.add(newBlock);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -181,15 +194,23 @@ public class BlockMinter extends Thread {
|
||||
|
||||
// Make sure we're the only thread modifying the blockchain
|
||||
ReentrantLock blockchainLock = Controller.getInstance().getBlockchainLock();
|
||||
if (!blockchainLock.tryLock())
|
||||
if (!blockchainLock.tryLock(30, TimeUnit.SECONDS)) {
|
||||
LOGGER.warn("Couldn't acquire blockchain lock even after waiting 30 seconds");
|
||||
continue;
|
||||
}
|
||||
|
||||
boolean newBlockMinted = false;
|
||||
Block newBlock = null;
|
||||
|
||||
try {
|
||||
// Clear repository's "in transaction" state so we don't cause a repository deadlock
|
||||
// Clear repository session state so we have latest view of data
|
||||
repository.discardChanges();
|
||||
|
||||
// Now that we have blockchain lock, do final check that chain hasn't changed
|
||||
BlockData latestBlockData = blockRepository.getLastBlock();
|
||||
if (!Arrays.equals(lastBlockData.getSignature(), latestBlockData.getSignature()))
|
||||
continue;
|
||||
|
||||
List<Block> goodBlocks = new ArrayList<>();
|
||||
for (Block testBlock : newBlocks) {
|
||||
// Is new block's timestamp valid yet?
|
||||
@@ -198,8 +219,12 @@ public class BlockMinter extends Thread {
|
||||
continue;
|
||||
|
||||
// Is new block valid yet? (Before adding unconfirmed transactions)
|
||||
if (testBlock.isValid() != ValidationResult.OK)
|
||||
ValidationResult result = testBlock.isValid();
|
||||
if (result != ValidationResult.OK) {
|
||||
moderatedLog(() -> LOGGER.error(String.format("To-be-minted block invalid '%s' before adding transactions?", result.name())));
|
||||
|
||||
continue;
|
||||
}
|
||||
|
||||
goodBlocks.add(testBlock);
|
||||
}
|
||||
@@ -211,7 +236,6 @@ public class BlockMinter extends Thread {
|
||||
final int parentHeight = previousBlock.getBlockData().getHeight();
|
||||
final byte[] parentBlockSignature = previousBlock.getSignature();
|
||||
|
||||
Block newBlock = null;
|
||||
BigInteger bestWeight = null;
|
||||
|
||||
for (int bi = 0; bi < goodBlocks.size(); ++bi) {
|
||||
@@ -282,10 +306,13 @@ public class BlockMinter extends Thread {
|
||||
}
|
||||
|
||||
if (newBlockMinted)
|
||||
Controller.getInstance().onBlockMinted();
|
||||
Controller.getInstance().onNewBlock(newBlock.getBlockData());
|
||||
}
|
||||
} catch (DataException e) {
|
||||
LOGGER.warn("Repository issue while running block minter", e);
|
||||
} catch (InterruptedException e) {
|
||||
// We've been interrupted - time to exit
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -338,19 +365,21 @@ public class BlockMinter extends Thread {
|
||||
this.interrupt();
|
||||
}
|
||||
|
||||
public static void mintTestingBlock(Repository repository, PrivateKeyAccount... mintingAndOnlineAccounts) throws DataException {
|
||||
if (!BlockChain.getInstance().isTestChain()) {
|
||||
LOGGER.warn("Ignoring attempt to mint testing block for non-test chain!");
|
||||
return;
|
||||
}
|
||||
public static Block mintTestingBlock(Repository repository, PrivateKeyAccount... mintingAndOnlineAccounts) throws DataException {
|
||||
if (!BlockChain.getInstance().isTestChain())
|
||||
throw new DataException("Ignoring attempt to mint testing block for non-test chain!");
|
||||
|
||||
// Ensure mintingAccount is 'online' so blocks can be minted
|
||||
Controller.getInstance().ensureTestingAccountsOnline(mintingAndOnlineAccounts);
|
||||
|
||||
BlockData previousBlockData = repository.getBlockRepository().getLastBlock();
|
||||
|
||||
PrivateKeyAccount mintingAccount = mintingAndOnlineAccounts[0];
|
||||
|
||||
return mintTestingBlockRetainingTimestamps(repository, mintingAccount);
|
||||
}
|
||||
|
||||
public static Block mintTestingBlockRetainingTimestamps(Repository repository, PrivateKeyAccount mintingAccount) throws DataException {
|
||||
BlockData previousBlockData = repository.getBlockRepository().getLastBlock();
|
||||
|
||||
Block newBlock = Block.mint(repository, previousBlockData, mintingAccount);
|
||||
|
||||
// Make sure we're the only thread modifying the blockchain
|
||||
@@ -373,9 +402,22 @@ public class BlockMinter extends Thread {
|
||||
LOGGER.info(String.format("Minted new test block: %d", newBlock.getBlockData().getHeight()));
|
||||
|
||||
repository.saveChanges();
|
||||
|
||||
return newBlock;
|
||||
} finally {
|
||||
blockchainLock.unlock();
|
||||
}
|
||||
}
|
||||
|
||||
private static void moderatedLog(Runnable logFunction) {
|
||||
// We only log if logging at TRACE or previous log timeout has expired
|
||||
if (!LOGGER.isTraceEnabled() && lastLogTimestamp != null && lastLogTimestamp + logTimeout > System.currentTimeMillis())
|
||||
return;
|
||||
|
||||
lastLogTimestamp = System.currentTimeMillis();
|
||||
logTimeout = 2 * 60 * 1000L; // initial timeout, can be reduced if new block appears
|
||||
|
||||
logFunction.run();
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -2,32 +2,25 @@ package org.qortal.block;
|
||||
|
||||
import java.io.ByteArrayOutputStream;
|
||||
import java.io.IOException;
|
||||
import java.math.BigDecimal;
|
||||
import java.util.ArrayList;
|
||||
import java.util.Arrays;
|
||||
import java.util.Collections;
|
||||
import java.util.List;
|
||||
import java.util.stream.Collectors;
|
||||
|
||||
import javax.xml.bind.annotation.XmlAccessType;
|
||||
import javax.xml.bind.annotation.XmlAccessorType;
|
||||
|
||||
import org.apache.logging.log4j.LogManager;
|
||||
import org.apache.logging.log4j.Logger;
|
||||
import org.qortal.account.Account;
|
||||
import org.qortal.account.GenesisAccount;
|
||||
import org.qortal.account.PublicKeyAccount;
|
||||
import org.qortal.account.NullAccount;
|
||||
import org.qortal.crypto.Crypto;
|
||||
import org.qortal.data.asset.AssetData;
|
||||
import org.qortal.data.block.BlockData;
|
||||
import org.qortal.data.transaction.IssueAssetTransactionData;
|
||||
import org.qortal.data.transaction.TransactionData;
|
||||
import org.qortal.group.Group;
|
||||
import org.qortal.repository.DataException;
|
||||
import org.qortal.repository.Repository;
|
||||
import org.qortal.transaction.Transaction;
|
||||
import org.qortal.transaction.Transaction.ApprovalStatus;
|
||||
import org.qortal.transaction.Transaction.TransactionType;
|
||||
import org.qortal.transform.TransformationException;
|
||||
import org.qortal.transform.transaction.TransactionTransformer;
|
||||
|
||||
@@ -39,9 +32,8 @@ public class GenesisBlock extends Block {
|
||||
|
||||
private static final Logger LOGGER = LogManager.getLogger(GenesisBlock.class);
|
||||
|
||||
private static final byte[] GENESIS_REFERENCE = new byte[] {
|
||||
1, 1, 1, 1, 1, 1, 1, 1
|
||||
}; // NOTE: Neither 64 nor 128 bytes!
|
||||
private static final byte[] GENESIS_BLOCK_REFERENCE = new byte[128];
|
||||
private static final byte[] GENESIS_TRANSACTION_REFERENCE = new byte[64];
|
||||
|
||||
@XmlAccessorType(XmlAccessType.FIELD)
|
||||
public static class GenesisInfo {
|
||||
@@ -93,46 +85,25 @@ public class GenesisBlock extends Block {
|
||||
// Add default values to transactions
|
||||
transactionsData.stream().forEach(transactionData -> {
|
||||
if (transactionData.getFee() == null)
|
||||
transactionData.setFee(BigDecimal.ZERO.setScale(8));
|
||||
transactionData.setFee(0L);
|
||||
|
||||
if (transactionData.getCreatorPublicKey() == null)
|
||||
transactionData.setCreatorPublicKey(GenesisAccount.PUBLIC_KEY);
|
||||
transactionData.setCreatorPublicKey(NullAccount.PUBLIC_KEY);
|
||||
|
||||
if (transactionData.getTimestamp() == 0)
|
||||
transactionData.setTimestamp(info.timestamp);
|
||||
});
|
||||
|
||||
// For version 1, extract any ISSUE_ASSET transactions into initialAssets and only allow GENESIS transactions
|
||||
if (info.version == 1) {
|
||||
List<TransactionData> issueAssetTransactions = transactionsData.stream()
|
||||
.filter(transactionData -> transactionData.getType() == TransactionType.ISSUE_ASSET).collect(Collectors.toList());
|
||||
transactionsData.removeAll(issueAssetTransactions);
|
||||
|
||||
// There should be only GENESIS transactions left;
|
||||
if (transactionsData.stream().anyMatch(transactionData -> transactionData.getType() != TransactionType.GENESIS)) {
|
||||
LOGGER.error("Version 1 genesis block only allowed to contain GENESIS transctions (after issue-asset processing)");
|
||||
throw new RuntimeException("Version 1 genesis block only allowed to contain GENESIS transctions (after issue-asset processing)");
|
||||
}
|
||||
|
||||
// Convert ISSUE_ASSET transactions into initial assets
|
||||
initialAssets = issueAssetTransactions.stream().map(transactionData -> {
|
||||
IssueAssetTransactionData issueAssetTransactionData = (IssueAssetTransactionData) transactionData;
|
||||
|
||||
return new AssetData(issueAssetTransactionData.getOwner(), issueAssetTransactionData.getAssetName(), issueAssetTransactionData.getDescription(),
|
||||
issueAssetTransactionData.getQuantity(), issueAssetTransactionData.getIsDivisible(), "", false, Group.NO_GROUP, issueAssetTransactionData.getReference());
|
||||
}).collect(Collectors.toList());
|
||||
}
|
||||
|
||||
byte[] reference = GENESIS_REFERENCE;
|
||||
byte[] reference = GENESIS_BLOCK_REFERENCE;
|
||||
int transactionCount = transactionsData.size();
|
||||
BigDecimal totalFees = BigDecimal.ZERO.setScale(8);
|
||||
byte[] minterPublicKey = GenesisAccount.PUBLIC_KEY;
|
||||
long totalFees = 0;
|
||||
byte[] minterPublicKey = NullAccount.PUBLIC_KEY;
|
||||
byte[] bytesForSignature = getBytesForMinterSignature(info.timestamp, reference, minterPublicKey);
|
||||
byte[] minterSignature = calcGenesisMinterSignature(bytesForSignature);
|
||||
byte[] transactionsSignature = calcGenesisTransactionsSignature();
|
||||
int height = 1;
|
||||
int atCount = 0;
|
||||
BigDecimal atFees = BigDecimal.ZERO.setScale(8);
|
||||
long atFees = 0;
|
||||
|
||||
genesisBlockData = new BlockData(info.version, reference, transactionCount, totalFees, transactionsSignature, height, info.timestamp,
|
||||
minterPublicKey, minterSignature, atCount, atFees);
|
||||
@@ -172,7 +143,7 @@ public class GenesisBlock extends Block {
|
||||
/**
|
||||
* Refuse to calculate genesis block's minter signature!
|
||||
* <p>
|
||||
* This is not possible as there is no private key for the genesis account and so no way to sign data.
|
||||
* This is not possible as there is no private key for the null account and so no way to sign data.
|
||||
* <p>
|
||||
* <b>Always throws IllegalStateException.</b>
|
||||
*
|
||||
@@ -180,13 +151,13 @@ public class GenesisBlock extends Block {
|
||||
*/
|
||||
@Override
|
||||
public void calcMinterSignature() {
|
||||
throw new IllegalStateException("There is no private key for genesis account");
|
||||
throw new IllegalStateException("There is no private key for null account");
|
||||
}
|
||||
|
||||
/**
|
||||
* Refuse to calculate genesis block's transactions signature!
|
||||
* <p>
|
||||
* This is not possible as there is no private key for the genesis account and so no way to sign data.
|
||||
* This is not possible as there is no private key for the null account and so no way to sign data.
|
||||
* <p>
|
||||
* <b>Always throws IllegalStateException.</b>
|
||||
*
|
||||
@@ -194,13 +165,13 @@ public class GenesisBlock extends Block {
|
||||
*/
|
||||
@Override
|
||||
public void calcTransactionsSignature() {
|
||||
throw new IllegalStateException("There is no private key for genesis account");
|
||||
throw new IllegalStateException("There is no private key for null account");
|
||||
}
|
||||
|
||||
/**
|
||||
* Generate genesis block minter signature.
|
||||
* <p>
|
||||
* This is handled differently as there is no private key for the genesis account and so no way to sign data.
|
||||
* This is handled differently as there is no private key for the null account and so no way to sign data.
|
||||
*
|
||||
* @return byte[]
|
||||
*/
|
||||
@@ -212,24 +183,16 @@ public class GenesisBlock extends Block {
|
||||
try {
|
||||
// Passing expected size to ByteArrayOutputStream avoids reallocation when adding more bytes than default 32.
|
||||
// See below for explanation of some of the values used to calculated expected size.
|
||||
ByteArrayOutputStream bytes = new ByteArrayOutputStream(8 + 64 + 8 + 32);
|
||||
ByteArrayOutputStream bytes = new ByteArrayOutputStream(8 + 128 + 32);
|
||||
|
||||
/*
|
||||
* NOTE: Historic code had genesis block using Longs.toByteArray(version) compared to standard block's Ints.toByteArray. The subsequent
|
||||
* Bytes.ensureCapacity(versionBytes, 0, 4) did not truncate versionBytes back to 4 bytes either. This means 8 bytes were used even though
|
||||
* VERSION_LENGTH is set to 4. Correcting this historic bug will break genesis block signatures!
|
||||
*/
|
||||
// For Qortal, we use genesis timestamp instead
|
||||
// Genesis block timestamp
|
||||
bytes.write(Longs.toByteArray(timestamp));
|
||||
|
||||
/*
|
||||
* NOTE: Historic code had the reference expanded to only 64 bytes whereas standard block references are 128 bytes. Correcting this historic bug
|
||||
* will break genesis block signatures!
|
||||
*/
|
||||
bytes.write(Bytes.ensureCapacity(reference, 64, 0));
|
||||
// Block's reference
|
||||
bytes.write(reference);
|
||||
|
||||
// NOTE: Genesis account's public key is only 8 bytes, not the usual 32, so we have to pad.
|
||||
bytes.write(Bytes.ensureCapacity(minterPublicKey, 32, 0));
|
||||
// Minting account's public key (typically NullAccount)
|
||||
bytes.write(minterPublicKey);
|
||||
|
||||
return bytes.toByteArray();
|
||||
} catch (IOException e) {
|
||||
@@ -295,26 +258,18 @@ public class GenesisBlock extends Block {
|
||||
public void process() throws DataException {
|
||||
LOGGER.info(String.format("Using genesis block timestamp of %d", this.blockData.getTimestamp()));
|
||||
|
||||
// If we're a version 1 genesis block, create assets now
|
||||
if (this.blockData.getVersion() == 1)
|
||||
for (AssetData assetData : initialAssets)
|
||||
repository.getAssetRepository().save(assetData);
|
||||
|
||||
/*
|
||||
* Some transactions will be missing references and signatures,
|
||||
* so we generate them by trial-processing transactions and using
|
||||
* account's last-reference to fill in the gaps for reference,
|
||||
* so we generate them by using <tt>GENESIS_TRANSACTION_REFERENCE</tt>
|
||||
* and a duplicated SHA256 digest for signature
|
||||
*/
|
||||
this.repository.setSavepoint();
|
||||
try {
|
||||
for (Transaction transaction : this.getTransactions()) {
|
||||
TransactionData transactionData = transaction.getTransactionData();
|
||||
Account creator = new PublicKeyAccount(this.repository, transactionData.getCreatorPublicKey());
|
||||
|
||||
// Missing reference?
|
||||
if (transactionData.getReference() == null)
|
||||
transactionData.setReference(creator.getLastReference());
|
||||
transactionData.setReference(GENESIS_TRANSACTION_REFERENCE);
|
||||
|
||||
// Missing signature?
|
||||
if (transactionData.getSignature() == null) {
|
||||
@@ -324,24 +279,21 @@ public class GenesisBlock extends Block {
|
||||
transactionData.setSignature(signature);
|
||||
}
|
||||
|
||||
// Missing approval status (not used in V1)
|
||||
// Approval status
|
||||
transactionData.setApprovalStatus(ApprovalStatus.NOT_REQUIRED);
|
||||
|
||||
// Ask transaction to update references, etc.
|
||||
transaction.processReferencesAndFees();
|
||||
|
||||
creator.setLastReference(transactionData.getSignature());
|
||||
}
|
||||
} catch (TransformationException e) {
|
||||
throw new RuntimeException("Can't process genesis block transaction", e);
|
||||
} finally {
|
||||
this.repository.rollbackToSavepoint();
|
||||
}
|
||||
|
||||
// Save transactions into repository ready for processing
|
||||
for (Transaction transaction : this.getTransactions())
|
||||
this.repository.getTransactionRepository().save(transaction.getTransactionData());
|
||||
|
||||
// No ATs in genesis block
|
||||
this.ourAtStates = Collections.emptyList();
|
||||
this.ourAtFees = 0;
|
||||
|
||||
super.process();
|
||||
}
|
||||
|
||||
|
||||
61
src/main/java/org/qortal/controller/BlockNotifier.java
Normal file
61
src/main/java/org/qortal/controller/BlockNotifier.java
Normal file
@@ -0,0 +1,61 @@
|
||||
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 {
|
||||
|
||||
private static BlockNotifier instance;
|
||||
|
||||
@FunctionalInterface
|
||||
public interface Listener {
|
||||
void notify(BlockInfo blockInfo);
|
||||
}
|
||||
|
||||
private Map<Session, Listener> listenersBySession = new HashMap<>();
|
||||
|
||||
private BlockNotifier() {
|
||||
}
|
||||
|
||||
public static synchronized BlockNotifier getInstance() {
|
||||
if (instance == null)
|
||||
instance = new BlockNotifier();
|
||||
|
||||
return instance;
|
||||
}
|
||||
|
||||
public void register(Session session, Listener listener) {
|
||||
synchronized (this.listenersBySession) {
|
||||
this.listenersBySession.put(session, listener);
|
||||
}
|
||||
}
|
||||
|
||||
public void deregister(Session session) {
|
||||
synchronized (this.listenersBySession) {
|
||||
this.listenersBySession.remove(session);
|
||||
}
|
||||
}
|
||||
|
||||
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());
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
62
src/main/java/org/qortal/controller/ChatNotifier.java
Normal file
62
src/main/java/org/qortal/controller/ChatNotifier.java
Normal file
@@ -0,0 +1,62 @@
|
||||
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.data.transaction.ChatTransactionData;
|
||||
|
||||
public class ChatNotifier {
|
||||
|
||||
private static ChatNotifier instance;
|
||||
|
||||
@FunctionalInterface
|
||||
public interface Listener {
|
||||
void notify(ChatTransactionData chatTransactionData);
|
||||
}
|
||||
|
||||
private Map<Session, Listener> listenersBySession = new HashMap<>();
|
||||
|
||||
private ChatNotifier() {
|
||||
}
|
||||
|
||||
public static synchronized ChatNotifier getInstance() {
|
||||
if (instance == null)
|
||||
instance = new ChatNotifier();
|
||||
|
||||
return instance;
|
||||
}
|
||||
|
||||
public void register(Session session, Listener listener) {
|
||||
synchronized (this.listenersBySession) {
|
||||
this.listenersBySession.put(session, listener);
|
||||
}
|
||||
}
|
||||
|
||||
public void deregister(Session session) {
|
||||
synchronized (this.listenersBySession) {
|
||||
this.listenersBySession.remove(session);
|
||||
}
|
||||
}
|
||||
|
||||
public void onNewChatTransaction(ChatTransactionData chatTransactionData) {
|
||||
for (Listener listener : getAllListeners())
|
||||
listener.notify(chatTransactionData);
|
||||
}
|
||||
|
||||
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());
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
@@ -18,6 +18,8 @@ import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.Properties;
|
||||
import java.util.Random;
|
||||
import java.util.concurrent.ExecutorService;
|
||||
import java.util.concurrent.Executors;
|
||||
import java.util.concurrent.locks.ReentrantLock;
|
||||
import java.util.function.Predicate;
|
||||
import java.util.stream.Collectors;
|
||||
@@ -46,6 +48,7 @@ import org.qortal.data.network.PeerData;
|
||||
import org.qortal.data.transaction.ArbitraryTransactionData;
|
||||
import org.qortal.data.transaction.TransactionData;
|
||||
import org.qortal.data.transaction.ArbitraryTransactionData.DataType;
|
||||
import org.qortal.data.transaction.ChatTransactionData;
|
||||
import org.qortal.globalization.Translator;
|
||||
import org.qortal.gui.Gui;
|
||||
import org.qortal.gui.SysTray;
|
||||
@@ -59,11 +62,9 @@ import org.qortal.network.message.GetBlockMessage;
|
||||
import org.qortal.network.message.GetBlockSummariesMessage;
|
||||
import org.qortal.network.message.GetOnlineAccountsMessage;
|
||||
import org.qortal.network.message.GetPeersMessage;
|
||||
import org.qortal.network.message.GetSignaturesMessage;
|
||||
import org.qortal.network.message.GetSignaturesV2Message;
|
||||
import org.qortal.network.message.GetTransactionMessage;
|
||||
import org.qortal.network.message.GetUnconfirmedTransactionsMessage;
|
||||
import org.qortal.network.message.HeightMessage;
|
||||
import org.qortal.network.message.HeightV2Message;
|
||||
import org.qortal.network.message.Message;
|
||||
import org.qortal.network.message.OnlineAccountsMessage;
|
||||
@@ -124,6 +125,9 @@ public class Controller extends Thread {
|
||||
private final long buildTimestamp; // seconds
|
||||
private final String[] savedArgs;
|
||||
|
||||
private ExecutorService callbackExecutor = Executors.newFixedThreadPool(3);
|
||||
private volatile boolean notifyGroupMembershipChange = false;
|
||||
|
||||
private volatile BlockData chainTip = null;
|
||||
|
||||
private long repositoryBackupTimestamp = startTime; // ms
|
||||
@@ -136,6 +140,8 @@ public class Controller extends Thread {
|
||||
|
||||
/** Whether we are attempting to synchronize. */
|
||||
private volatile boolean isSynchronizing = false;
|
||||
/** Temporary estimate of synchronization progress for SysTray use. */
|
||||
private volatile int syncPercent = 0;
|
||||
|
||||
/** Latest block signatures from other peers that we know are on inferior chains. */
|
||||
List<ByteArray> inferiorChainSignatures = new ArrayList<>();
|
||||
@@ -232,7 +238,7 @@ public class Controller extends Thread {
|
||||
return this.chainTip;
|
||||
}
|
||||
|
||||
/** Cache new blockchain tip, and also wipe cache of online accounts. */
|
||||
/** Cache new blockchain tip. */
|
||||
public void setChainTip(BlockData blockData) {
|
||||
this.chainTip = blockData;
|
||||
}
|
||||
@@ -258,6 +264,10 @@ public class Controller extends Thread {
|
||||
return this.isSynchronizing;
|
||||
}
|
||||
|
||||
public Integer getSyncPercent() {
|
||||
return this.isSynchronizing ? this.syncPercent : null;
|
||||
}
|
||||
|
||||
// Entry point
|
||||
|
||||
public static void main(String[] args) {
|
||||
@@ -330,7 +340,7 @@ public class Controller extends Thread {
|
||||
try {
|
||||
Network network = Network.getInstance();
|
||||
network.start();
|
||||
} catch (IOException e) {
|
||||
} catch (IOException | DataException e) {
|
||||
LOGGER.error("Unable to start networking", e);
|
||||
Controller.getInstance().shutdown();
|
||||
Gui.getInstance().fatalError("Networking failure", e);
|
||||
@@ -494,7 +504,7 @@ public class Controller extends Thread {
|
||||
};
|
||||
|
||||
private void potentiallySynchronize() throws InterruptedException {
|
||||
List<Peer> peers = Network.getInstance().getUniqueHandshakedPeers();
|
||||
List<Peer> peers = Network.getInstance().getHandshakedPeers();
|
||||
|
||||
// Disregard peers that have "misbehaved" recently
|
||||
peers.removeIf(hasMisbehaved);
|
||||
@@ -522,102 +532,95 @@ public class Controller extends Thread {
|
||||
int index = new SecureRandom().nextInt(peers.size());
|
||||
Peer peer = peers.get(index);
|
||||
|
||||
isSynchronizing = true;
|
||||
updateSysTray();
|
||||
|
||||
actuallySynchronize(peer, false);
|
||||
|
||||
isSynchronizing = false;
|
||||
requestSysTrayUpdate = true;
|
||||
}
|
||||
|
||||
public SynchronizationResult actuallySynchronize(Peer peer, boolean force) throws InterruptedException {
|
||||
BlockData latestBlockData = getChainTip();
|
||||
|
||||
SynchronizationResult syncResult = Synchronizer.getInstance().synchronize(peer, force);
|
||||
switch (syncResult) {
|
||||
case GENESIS_ONLY:
|
||||
case NO_COMMON_BLOCK:
|
||||
case TOO_DIVERGENT:
|
||||
case INVALID_DATA: {
|
||||
// These are more serious results that warrant a cool-off
|
||||
LOGGER.info(String.format("Failed to synchronize with peer %s (%s) - cooling off", peer, syncResult.name()));
|
||||
|
||||
// Don't use this peer again for a while
|
||||
PeerData peerData = peer.getPeerData();
|
||||
peerData.setLastMisbehaved(NTP.getTime());
|
||||
|
||||
// Only save to repository if outbound peer
|
||||
if (peer.isOutbound())
|
||||
try (final Repository repository = RepositoryManager.getRepository()) {
|
||||
repository.getNetworkRepository().save(peerData);
|
||||
repository.saveChanges();
|
||||
} catch (DataException e) {
|
||||
LOGGER.warn("Repository issue while updating peer synchronization info", e);
|
||||
}
|
||||
break;
|
||||
}
|
||||
|
||||
case INFERIOR_CHAIN: {
|
||||
// Update our list of inferior chain tips
|
||||
ByteArray inferiorChainSignature = new ByteArray(peer.getChainTipData().getLastBlockSignature());
|
||||
if (!inferiorChainSignatures.contains(inferiorChainSignature))
|
||||
inferiorChainSignatures.add(inferiorChainSignature);
|
||||
|
||||
// These are minor failure results so fine to try again
|
||||
LOGGER.debug(() -> String.format("Refused to synchronize with peer %s (%s)", peer, syncResult.name()));
|
||||
|
||||
// Notify peer of our superior chain
|
||||
if (!peer.sendMessage(Network.getInstance().buildHeightMessage(peer, latestBlockData)))
|
||||
peer.disconnect("failed to notify peer of our superior chain");
|
||||
break;
|
||||
}
|
||||
|
||||
case NO_REPLY:
|
||||
case NO_BLOCKCHAIN_LOCK:
|
||||
case REPOSITORY_ISSUE:
|
||||
// These are minor failure results so fine to try again
|
||||
LOGGER.debug(() -> String.format("Failed to synchronize with peer %s (%s)", peer, syncResult.name()));
|
||||
break;
|
||||
|
||||
case SHUTTING_DOWN:
|
||||
// Just quietly exit
|
||||
break;
|
||||
|
||||
case OK:
|
||||
requestSysTrayUpdate = true;
|
||||
// fall-through...
|
||||
case NOTHING_TO_DO: {
|
||||
// Update our list of inferior chain tips
|
||||
ByteArray inferiorChainSignature = new ByteArray(peer.getChainTipData().getLastBlockSignature());
|
||||
if (!inferiorChainSignatures.contains(inferiorChainSignature))
|
||||
inferiorChainSignatures.add(inferiorChainSignature);
|
||||
|
||||
LOGGER.debug(() -> String.format("Synchronized with peer %s (%s)", peer, syncResult.name()));
|
||||
break;
|
||||
}
|
||||
syncPercent = (this.chainTip.getHeight() * 100) / peer.getChainTipData().getLastHeight();
|
||||
// Only update SysTray if we're potentially changing height
|
||||
if (syncPercent < 100) {
|
||||
isSynchronizing = true;
|
||||
updateSysTray();
|
||||
}
|
||||
|
||||
// Has our chain tip changed?
|
||||
BlockData newLatestBlockData;
|
||||
BlockData priorChainTip = this.chainTip;
|
||||
|
||||
try {
|
||||
SynchronizationResult syncResult = Synchronizer.getInstance().synchronize(peer, force);
|
||||
switch (syncResult) {
|
||||
case GENESIS_ONLY:
|
||||
case NO_COMMON_BLOCK:
|
||||
case TOO_DIVERGENT:
|
||||
case INVALID_DATA: {
|
||||
// These are more serious results that warrant a cool-off
|
||||
LOGGER.info(String.format("Failed to synchronize with peer %s (%s) - cooling off", peer, syncResult.name()));
|
||||
|
||||
// Don't use this peer again for a while
|
||||
Network.getInstance().peerMisbehaved(peer);
|
||||
break;
|
||||
}
|
||||
|
||||
case INFERIOR_CHAIN: {
|
||||
// Update our list of inferior chain tips
|
||||
ByteArray inferiorChainSignature = new ByteArray(peer.getChainTipData().getLastBlockSignature());
|
||||
if (!inferiorChainSignatures.contains(inferiorChainSignature))
|
||||
inferiorChainSignatures.add(inferiorChainSignature);
|
||||
|
||||
// These are minor failure results so fine to try again
|
||||
LOGGER.debug(() -> String.format("Refused to synchronize with peer %s (%s)", peer, syncResult.name()));
|
||||
|
||||
// Notify peer of our superior chain
|
||||
if (!peer.sendMessage(Network.getInstance().buildHeightMessage(peer, priorChainTip)))
|
||||
peer.disconnect("failed to notify peer of our superior chain");
|
||||
break;
|
||||
}
|
||||
|
||||
case NO_REPLY:
|
||||
case NO_BLOCKCHAIN_LOCK:
|
||||
case REPOSITORY_ISSUE:
|
||||
// These are minor failure results so fine to try again
|
||||
LOGGER.debug(() -> String.format("Failed to synchronize with peer %s (%s)", peer, syncResult.name()));
|
||||
break;
|
||||
|
||||
case SHUTTING_DOWN:
|
||||
// Just quietly exit
|
||||
break;
|
||||
|
||||
case OK:
|
||||
// fall-through...
|
||||
case NOTHING_TO_DO: {
|
||||
// Update our list of inferior chain tips
|
||||
ByteArray inferiorChainSignature = new ByteArray(peer.getChainTipData().getLastBlockSignature());
|
||||
if (!inferiorChainSignatures.contains(inferiorChainSignature))
|
||||
inferiorChainSignatures.add(inferiorChainSignature);
|
||||
|
||||
LOGGER.debug(() -> String.format("Synchronized with peer %s (%s)", peer, syncResult.name()));
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
// Has our chain tip changed?
|
||||
BlockData newChainTip;
|
||||
|
||||
try (final Repository repository = RepositoryManager.getRepository()) {
|
||||
newChainTip = repository.getBlockRepository().getLastBlock();
|
||||
} catch (DataException e) {
|
||||
LOGGER.warn(String.format("Repository issue when trying to fetch post-synchronization chain tip: %s", e.getMessage()));
|
||||
return syncResult;
|
||||
}
|
||||
|
||||
if (!Arrays.equals(newChainTip.getSignature(), priorChainTip.getSignature())) {
|
||||
// Reset our cache of inferior chains
|
||||
inferiorChainSignatures.clear();
|
||||
|
||||
// Update chain-tip, systray, notify peers, websockets, etc.
|
||||
this.onNewBlock(newChainTip);
|
||||
}
|
||||
|
||||
try (final Repository repository = RepositoryManager.getRepository()) {
|
||||
newLatestBlockData = repository.getBlockRepository().getLastBlock();
|
||||
this.setChainTip(newLatestBlockData);
|
||||
} catch (DataException e) {
|
||||
LOGGER.warn(String.format("Repository issue when trying to fetch post-synchronization chain tip: %s", e.getMessage()));
|
||||
return syncResult;
|
||||
} finally {
|
||||
isSynchronizing = false;
|
||||
}
|
||||
|
||||
if (!Arrays.equals(newLatestBlockData.getSignature(), latestBlockData.getSignature())) {
|
||||
// Broadcast our new chain tip
|
||||
Network.getInstance().broadcast(recipientPeer -> Network.getInstance().buildHeightMessage(recipientPeer, newLatestBlockData));
|
||||
|
||||
// Reset our cache of inferior chains
|
||||
inferiorChainSignatures.clear();
|
||||
}
|
||||
|
||||
return syncResult;
|
||||
}
|
||||
|
||||
private void updateSysTray() {
|
||||
@@ -626,24 +629,29 @@ public class Controller extends Thread {
|
||||
return;
|
||||
}
|
||||
|
||||
final int numberOfPeers = Network.getInstance().getUniqueHandshakedPeers().size();
|
||||
final int numberOfPeers = Network.getInstance().getHandshakedPeers().size();
|
||||
|
||||
final int height = getChainHeight();
|
||||
|
||||
String connectionsText = Translator.INSTANCE.translate("SysTray", numberOfPeers != 1 ? "CONNECTIONS" : "CONNECTION");
|
||||
String heightText = Translator.INSTANCE.translate("SysTray", "BLOCK_HEIGHT");
|
||||
|
||||
String actionKey;
|
||||
String actionText;
|
||||
if (isMintingPossible)
|
||||
actionKey = "MINTING_ENABLED";
|
||||
actionText = Translator.INSTANCE.translate("SysTray", "MINTING_ENABLED");
|
||||
else if (isSynchronizing)
|
||||
actionKey = "SYNCHRONIZING_BLOCKCHAIN";
|
||||
actionText = String.format("%s - %d%%", Translator.INSTANCE.translate("SysTray", "SYNCHRONIZING_BLOCKCHAIN"), syncPercent);
|
||||
else if (numberOfPeers < Settings.getInstance().getMinBlockchainPeers())
|
||||
actionText = Translator.INSTANCE.translate("SysTray", "CONNECTING");
|
||||
else
|
||||
actionKey = "MINTING_DISABLED";
|
||||
String actionText = Translator.INSTANCE.translate("SysTray", actionKey);
|
||||
actionText = Translator.INSTANCE.translate("SysTray", "MINTING_DISABLED");
|
||||
|
||||
String tooltip = String.format("%s - %d %s - %s %d", actionText, numberOfPeers, connectionsText, heightText, height);
|
||||
SysTray.getInstance().setToolTipText(tooltip);
|
||||
|
||||
this.callbackExecutor.execute(() -> {
|
||||
StatusNotifier.getInstance().onStatusChange(NTP.getTime());
|
||||
});
|
||||
}
|
||||
|
||||
public void deleteExpiredTransactions() {
|
||||
@@ -730,6 +738,22 @@ public class Controller extends Thread {
|
||||
System.exit(0);
|
||||
}
|
||||
|
||||
// Callbacks
|
||||
|
||||
public void onGroupMembershipChange(int groupId) {
|
||||
/*
|
||||
* We've likely been called in the middle of block processing,
|
||||
* so set a flag for now as other repository sessions won't 'see'
|
||||
* the group membership change until a call to repository.saveChanges().
|
||||
*
|
||||
* Eventually, onNewBlock() will be executed and queue a callback task.
|
||||
* This callback task will check the flag and notify websocket listeners, etc.
|
||||
* and those listeners will be post-saveChanges() and hence see the new
|
||||
* group membership state.
|
||||
*/
|
||||
this.notifyGroupMembershipChange = true;
|
||||
}
|
||||
|
||||
// Callbacks for/from network
|
||||
|
||||
public void doNetworkBroadcast() {
|
||||
@@ -742,8 +766,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) {
|
||||
@@ -751,59 +777,44 @@ public class Controller extends Thread {
|
||||
requestSysTrayUpdate = true;
|
||||
}
|
||||
|
||||
public void onBlockMinted() {
|
||||
// Broadcast our new height info
|
||||
BlockData latestBlockData;
|
||||
|
||||
try (final Repository repository = RepositoryManager.getRepository()) {
|
||||
latestBlockData = repository.getBlockRepository().getLastBlock();
|
||||
this.setChainTip(latestBlockData);
|
||||
} catch (DataException e) {
|
||||
LOGGER.warn(String.format("Repository issue when trying to fetch post-mint chain tip: %s", e.getMessage()));
|
||||
return;
|
||||
}
|
||||
|
||||
Network network = Network.getInstance();
|
||||
network.broadcast(peer -> network.buildHeightMessage(peer, latestBlockData));
|
||||
|
||||
public void onNewBlock(BlockData latestBlockData) {
|
||||
this.setChainTip(latestBlockData);
|
||||
requestSysTrayUpdate = true;
|
||||
|
||||
// Broadcast our new height info and notify websocket listeners
|
||||
this.callbackExecutor.execute(() -> {
|
||||
Network network = Network.getInstance();
|
||||
network.broadcast(peer -> network.buildHeightMessage(peer, latestBlockData));
|
||||
|
||||
BlockNotifier.getInstance().onNewBlock(latestBlockData);
|
||||
|
||||
if (this.notifyGroupMembershipChange) {
|
||||
this.notifyGroupMembershipChange = false;
|
||||
ChatNotifier.getInstance().onGroupMembershipChange();
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
public void onNewTransaction(TransactionData transactionData) {
|
||||
// Send round to all peers
|
||||
Network network = Network.getInstance();
|
||||
network.broadcast(peer -> network.buildNewTransactionMessage(peer, transactionData));
|
||||
/** Callback for when we've received a new transaction via API or peer. */
|
||||
public void onNewTransaction(TransactionData transactionData, Peer peer) {
|
||||
this.callbackExecutor.execute(() -> {
|
||||
// Notify all peers (except maybe peer that sent it to us if applicable)
|
||||
Network.getInstance().broadcast(broadcastPeer -> broadcastPeer == peer ? null : new TransactionSignaturesMessage(Arrays.asList(transactionData.getSignature())));
|
||||
|
||||
// If this is a CHAT transaction, there may be extra listeners to notify
|
||||
if (transactionData.getType() == TransactionType.CHAT)
|
||||
ChatNotifier.getInstance().onNewChatTransaction((ChatTransactionData) transactionData);
|
||||
});
|
||||
}
|
||||
|
||||
public void onPeerHandshakeCompleted(Peer peer) {
|
||||
// Only send if outbound
|
||||
if (peer.isOutbound()) {
|
||||
if (peer.getVersion() < 2) {
|
||||
// Legacy mode
|
||||
|
||||
// Send our unconfirmed transactions
|
||||
try (final Repository repository = RepositoryManager.getRepository()) {
|
||||
List<TransactionData> transactions = repository.getTransactionRepository().getUnconfirmedTransactions();
|
||||
|
||||
for (TransactionData transactionData : transactions) {
|
||||
Message transactionMessage = new TransactionMessage(transactionData);
|
||||
if (!peer.sendMessage(transactionMessage)) {
|
||||
peer.disconnect("failed to send unconfirmed transaction");
|
||||
return;
|
||||
}
|
||||
}
|
||||
} catch (DataException e) {
|
||||
LOGGER.error("Repository issue while sending unconfirmed transactions", e);
|
||||
}
|
||||
} else {
|
||||
// V2 protocol
|
||||
|
||||
// Request peer's unconfirmed transactions
|
||||
Message message = new GetUnconfirmedTransactionsMessage();
|
||||
if (!peer.sendMessage(message)) {
|
||||
peer.disconnect("failed to send request for unconfirmed transactions");
|
||||
return;
|
||||
}
|
||||
// Request peer's unconfirmed transactions
|
||||
Message message = new GetUnconfirmedTransactionsMessage();
|
||||
if (!peer.sendMessage(message)) {
|
||||
peer.disconnect("failed to send request for unconfirmed transactions");
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -819,22 +830,10 @@ public class Controller extends Thread {
|
||||
|
||||
// Ordered by message type value
|
||||
switch (message.getType()) {
|
||||
case HEIGHT:
|
||||
onNetworkHeightMessage(peer, message);
|
||||
break;
|
||||
|
||||
case GET_SIGNATURES:
|
||||
onNetworkGetSignaturesMessage(peer, message);
|
||||
break;
|
||||
|
||||
case GET_BLOCK:
|
||||
onNetworkGetBlockMessage(peer, message);
|
||||
break;
|
||||
|
||||
case BLOCK:
|
||||
onNetworkBlockMessage(peer, message);
|
||||
break;
|
||||
|
||||
case TRANSACTION:
|
||||
onNetworkTransactionMessage(peer, message);
|
||||
break;
|
||||
@@ -880,56 +879,11 @@ public class Controller extends Thread {
|
||||
break;
|
||||
|
||||
default:
|
||||
LOGGER.debug(String.format("Unhandled %s message [ID %d] from peer %s", message.getType().name(), message.getId(), peer));
|
||||
LOGGER.debug(() -> String.format("Unhandled %s message [ID %d] from peer %s", message.getType().name(), message.getId(), peer));
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
private void onNetworkHeightMessage(Peer peer, Message message) {
|
||||
HeightMessage heightMessage = (HeightMessage) message;
|
||||
|
||||
// Update all peers with same ID
|
||||
|
||||
List<Peer> connectedPeers = Network.getInstance().getHandshakedPeers();
|
||||
for (Peer connectedPeer : connectedPeers) {
|
||||
if (connectedPeer.getPeerId() == null || !Arrays.equals(connectedPeer.getPeerId(), peer.getPeerId()))
|
||||
continue;
|
||||
|
||||
// Update peer chain tip data
|
||||
PeerChainTipData newChainTipData = new PeerChainTipData(heightMessage.getHeight(), null, null, null);
|
||||
connectedPeer.setChainTipData(newChainTipData);
|
||||
}
|
||||
|
||||
// Potentially synchronize
|
||||
requestSync = true;
|
||||
}
|
||||
|
||||
private void onNetworkGetSignaturesMessage(Peer peer, Message message) {
|
||||
GetSignaturesMessage getSignaturesMessage = (GetSignaturesMessage) message;
|
||||
byte[] parentSignature = getSignaturesMessage.getParentSignature();
|
||||
|
||||
try (final Repository repository = RepositoryManager.getRepository()) {
|
||||
List<byte[]> signatures = new ArrayList<>();
|
||||
|
||||
do {
|
||||
BlockData blockData = repository.getBlockRepository().fromReference(parentSignature);
|
||||
|
||||
if (blockData == null)
|
||||
break;
|
||||
|
||||
parentSignature = blockData.getSignature();
|
||||
signatures.add(parentSignature);
|
||||
} while (signatures.size() < Network.MAX_SIGNATURES_PER_REPLY);
|
||||
|
||||
Message signaturesMessage = new SignaturesMessage(signatures);
|
||||
signaturesMessage.setId(message.getId());
|
||||
if (!peer.sendMessage(signaturesMessage))
|
||||
peer.disconnect("failed to send signatures");
|
||||
} catch (DataException e) {
|
||||
LOGGER.error(String.format("Repository issue while sending signatures after %s to peer %s", Base58.encode(parentSignature), peer), e);
|
||||
}
|
||||
}
|
||||
|
||||
private void onNetworkGetBlockMessage(Peer peer, Message message) {
|
||||
GetBlockMessage getBlockMessage = (GetBlockMessage) message;
|
||||
byte[] signature = getBlockMessage.getSignature();
|
||||
@@ -953,40 +907,6 @@ public class Controller extends Thread {
|
||||
}
|
||||
}
|
||||
|
||||
private void onNetworkBlockMessage(Peer peer, Message message) {
|
||||
// From a v1 peer, with no message ID, this is a broadcast of peer's latest block
|
||||
// v2 peers announce new blocks using HEIGHT_V2
|
||||
|
||||
// Not version 1?
|
||||
if (peer.getVersion() == null || peer.getVersion() > 1)
|
||||
return;
|
||||
|
||||
// Message ID present?
|
||||
// XXX Why is this test here? If BLOCK had an ID then surely it would be a response to GET_BLOCK
|
||||
// and hence captured by Peer's reply queue?
|
||||
if (message.hasId())
|
||||
return;
|
||||
|
||||
BlockMessage blockMessage = (BlockMessage) message;
|
||||
BlockData blockData = blockMessage.getBlockData();
|
||||
|
||||
// Update all peers with same ID
|
||||
|
||||
List<Peer> connectedPeers = Network.getInstance().getHandshakedPeers();
|
||||
for (Peer connectedPeer : connectedPeers) {
|
||||
// Skip connectedPeer if they have no ID or their ID doesn't match sender's ID
|
||||
if (connectedPeer.getPeerId() == null || !Arrays.equals(connectedPeer.getPeerId(), peer.getPeerId()))
|
||||
continue;
|
||||
|
||||
// Update peer chain tip data
|
||||
PeerChainTipData newChainTipData = new PeerChainTipData(blockData.getHeight(), blockData.getSignature(), blockData.getTimestamp(), blockData.getMinterPublicKey());
|
||||
connectedPeer.setChainTipData(newChainTipData);
|
||||
}
|
||||
|
||||
// Potentially synchronize
|
||||
requestSync = true;
|
||||
}
|
||||
|
||||
private void onNetworkTransactionMessage(Peer peer, Message message) {
|
||||
TransactionMessage transactionMessage = (TransactionMessage) message;
|
||||
TransactionData transactionData = transactionMessage.getTransactionData();
|
||||
@@ -1021,6 +941,9 @@ public class Controller extends Thread {
|
||||
} catch (DataException e) {
|
||||
LOGGER.error(String.format("Repository issue while processing transaction %s from peer %s", Base58.encode(transactionData.getSignature()), peer), e);
|
||||
}
|
||||
|
||||
// Notify controller so it can notify other peers, etc.
|
||||
Controller.getInstance().onNewTransaction(transactionData, peer);
|
||||
}
|
||||
|
||||
private void onNetworkGetBlockSummariesMessage(Peer peer, Message message) {
|
||||
@@ -1089,18 +1012,9 @@ public class Controller extends Thread {
|
||||
if (!peer.isOutbound() && (peer.getChainTipData() == null || peer.getChainTipData().getLastHeight() == null))
|
||||
peer.sendMessage(Network.getInstance().buildHeightMessage(peer, getChainTip()));
|
||||
|
||||
// Update all peers with same ID
|
||||
|
||||
List<Peer> connectedPeers = Network.getInstance().getHandshakedPeers();
|
||||
for (Peer connectedPeer : connectedPeers) {
|
||||
// Skip connectedPeer if they have no ID or their ID doesn't match sender's ID
|
||||
if (connectedPeer.getPeerId() == null || !Arrays.equals(connectedPeer.getPeerId(), peer.getPeerId()))
|
||||
continue;
|
||||
|
||||
// Update peer chain tip data
|
||||
PeerChainTipData newChainTipData = new PeerChainTipData(heightV2Message.getHeight(), heightV2Message.getSignature(), heightV2Message.getTimestamp(), heightV2Message.getMinterPublicKey());
|
||||
connectedPeer.setChainTipData(newChainTipData);
|
||||
}
|
||||
// Update peer chain tip data
|
||||
PeerChainTipData newChainTipData = new PeerChainTipData(heightV2Message.getHeight(), heightV2Message.getSignature(), heightV2Message.getTimestamp(), heightV2Message.getMinterPublicKey());
|
||||
peer.setChainTipData(newChainTipData);
|
||||
|
||||
// Potentially synchronize
|
||||
requestSync = true;
|
||||
@@ -1129,7 +1043,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))
|
||||
@@ -1142,7 +1061,6 @@ public class Controller extends Thread {
|
||||
private void onNetworkTransactionSignaturesMessage(Peer peer, Message message) {
|
||||
TransactionSignaturesMessage transactionSignaturesMessage = (TransactionSignaturesMessage) message;
|
||||
List<byte[]> signatures = transactionSignaturesMessage.getSignatures();
|
||||
List<byte[]> newSignatures = new ArrayList<>();
|
||||
|
||||
try (final Repository repository = RepositoryManager.getRepository()) {
|
||||
for (byte[] signature : signatures) {
|
||||
@@ -1158,62 +1076,14 @@ public class Controller extends Thread {
|
||||
|
||||
// Fetch actual transaction data from peer
|
||||
Message getTransactionMessage = new GetTransactionMessage(signature);
|
||||
Message responseMessage = peer.getResponse(getTransactionMessage);
|
||||
if (!(responseMessage instanceof TransactionMessage)) {
|
||||
// Maybe peer no longer has this transaction
|
||||
LOGGER.trace(() -> String.format("Peer %s didn't send transaction %s", peer, Base58.encode(signature)));
|
||||
continue;
|
||||
}
|
||||
|
||||
// Check isInterrupted() here and exit fast
|
||||
if (Thread.currentThread().isInterrupted())
|
||||
if (!peer.sendMessage(getTransactionMessage)) {
|
||||
peer.disconnect("failed to request transaction");
|
||||
return;
|
||||
|
||||
TransactionMessage transactionMessage = (TransactionMessage) responseMessage;
|
||||
TransactionData transactionData = transactionMessage.getTransactionData();
|
||||
Transaction transaction = Transaction.fromData(repository, transactionData);
|
||||
|
||||
// Check signature
|
||||
if (!transaction.isSignatureValid()) {
|
||||
LOGGER.trace(() -> String.format("Ignoring %s transaction %s with invalid signature from peer %s", transactionData.getType().name(), Base58.encode(transactionData.getSignature()), peer));
|
||||
continue;
|
||||
}
|
||||
|
||||
ValidationResult validationResult = transaction.importAsUnconfirmed();
|
||||
|
||||
if (validationResult == ValidationResult.TRANSACTION_ALREADY_EXISTS) {
|
||||
LOGGER.trace(() -> String.format("Ignoring existing transaction %s from peer %s", Base58.encode(transactionData.getSignature()), peer));
|
||||
continue;
|
||||
}
|
||||
|
||||
if (validationResult == ValidationResult.NO_BLOCKCHAIN_LOCK) {
|
||||
LOGGER.trace(() -> String.format("Couldn't lock blockchain to import unconfirmed transaction %s from peer %s", Base58.encode(transactionData.getSignature()), peer));
|
||||
// Some other thread (e.g. Synchronizer) might have blockchain lock for a while so might as well give up for now
|
||||
break;
|
||||
}
|
||||
|
||||
if (validationResult != ValidationResult.OK) {
|
||||
LOGGER.trace(() -> String.format("Ignoring invalid (%s) %s transaction %s from peer %s", validationResult.name(), transactionData.getType().name(), Base58.encode(transactionData.getSignature()), peer));
|
||||
continue;
|
||||
}
|
||||
|
||||
LOGGER.debug(() -> String.format("Imported %s transaction %s from peer %s", transactionData.getType().name(), Base58.encode(transactionData.getSignature()), peer));
|
||||
|
||||
// We could collate signatures that are new to us and broadcast them to our peers too
|
||||
newSignatures.add(signature);
|
||||
}
|
||||
} catch (DataException e) {
|
||||
LOGGER.error(String.format("Repository issue while processing unconfirmed transactions from peer %s", peer), e);
|
||||
} catch (InterruptedException e) {
|
||||
// Shutdown
|
||||
return;
|
||||
}
|
||||
|
||||
if (newSignatures.isEmpty())
|
||||
return;
|
||||
|
||||
// Broadcast signatures that are new to us
|
||||
Network.getInstance().broadcast(broadcastPeer -> broadcastPeer == peer ? null : new TransactionSignaturesMessage(newSignatures));
|
||||
}
|
||||
|
||||
private void onNetworkGetArbitraryDataMessage(Peer peer, Message message) {
|
||||
@@ -1603,7 +1473,7 @@ public class Controller extends Thread {
|
||||
getArbitraryDataMessage.setId(id);
|
||||
|
||||
// Broadcast request
|
||||
Network.getInstance().broadcast(peer -> peer.getVersion() < 2 ? null : getArbitraryDataMessage);
|
||||
Network.getInstance().broadcast(peer -> getArbitraryDataMessage);
|
||||
|
||||
// Poll to see if data has arrived
|
||||
final long singleWait = 100;
|
||||
@@ -1635,7 +1505,7 @@ public class Controller extends Thread {
|
||||
if (minLatestBlockTimestamp == null)
|
||||
return null;
|
||||
|
||||
List<Peer> peers = Network.getInstance().getUniqueHandshakedPeers();
|
||||
List<Peer> peers = Network.getInstance().getHandshakedPeers();
|
||||
|
||||
// Filter out unsuitable peers
|
||||
Iterator<Peer> iterator = peers.iterator();
|
||||
@@ -1681,7 +1551,7 @@ public class Controller extends Thread {
|
||||
if (latestBlockData == null || latestBlockData.getTimestamp() < minLatestBlockTimestamp)
|
||||
return false;
|
||||
|
||||
List<Peer> peers = Network.getInstance().getUniqueHandshakedPeers();
|
||||
List<Peer> peers = Network.getInstance().getHandshakedPeers();
|
||||
if (peers == null)
|
||||
return false;
|
||||
|
||||
|
||||
56
src/main/java/org/qortal/controller/StatusNotifier.java
Normal file
56
src/main/java/org/qortal/controller/StatusNotifier.java
Normal file
@@ -0,0 +1,56 @@
|
||||
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;
|
||||
|
||||
public class StatusNotifier {
|
||||
|
||||
private static StatusNotifier instance;
|
||||
|
||||
@FunctionalInterface
|
||||
public interface Listener {
|
||||
void notify(long timestamp);
|
||||
}
|
||||
|
||||
private Map<Session, Listener> listenersBySession = new HashMap<>();
|
||||
|
||||
private StatusNotifier() {
|
||||
}
|
||||
|
||||
public static synchronized StatusNotifier getInstance() {
|
||||
if (instance == null)
|
||||
instance = new StatusNotifier();
|
||||
|
||||
return instance;
|
||||
}
|
||||
|
||||
public void register(Session session, Listener listener) {
|
||||
synchronized (this.listenersBySession) {
|
||||
this.listenersBySession.put(session, listener);
|
||||
}
|
||||
}
|
||||
|
||||
public void deregister(Session session) {
|
||||
synchronized (this.listenersBySession) {
|
||||
this.listenersBySession.remove(session);
|
||||
}
|
||||
}
|
||||
|
||||
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());
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
@@ -22,7 +22,6 @@ import org.qortal.network.message.BlockMessage;
|
||||
import org.qortal.network.message.BlockSummariesMessage;
|
||||
import org.qortal.network.message.GetBlockMessage;
|
||||
import org.qortal.network.message.GetBlockSummariesMessage;
|
||||
import org.qortal.network.message.GetSignaturesMessage;
|
||||
import org.qortal.network.message.GetSignaturesV2Message;
|
||||
import org.qortal.network.message.Message;
|
||||
import org.qortal.network.message.SignaturesMessage;
|
||||
@@ -372,12 +371,7 @@ public class Synchronizer {
|
||||
return SynchronizationResult.TOO_DIVERGENT;
|
||||
}
|
||||
|
||||
if (peer.getVersion() >= 2) {
|
||||
step <<= 1;
|
||||
} else {
|
||||
// Old v1 peers are hard-coded to return 500 signatures so we might as well go backward by 500 too
|
||||
step = 500;
|
||||
}
|
||||
step <<= 1;
|
||||
step = Math.min(step, MAXIMUM_BLOCK_STEP);
|
||||
|
||||
testHeight = Math.max(testHeight - step, 1);
|
||||
@@ -415,8 +409,7 @@ public class Synchronizer {
|
||||
}
|
||||
|
||||
private List<byte[]> getBlockSignatures(Peer peer, byte[] parentSignature, int numberRequested) throws InterruptedException {
|
||||
// numberRequested is v2+ feature
|
||||
Message getSignaturesMessage = peer.getVersion() >= 2 ? new GetSignaturesV2Message(parentSignature, numberRequested) : new GetSignaturesMessage(parentSignature);
|
||||
Message getSignaturesMessage = new GetSignaturesV2Message(parentSignature, numberRequested);
|
||||
|
||||
Message message = peer.getResponse(getSignaturesMessage);
|
||||
if (message == null || message.getType() != MessageType.SIGNATURES)
|
||||
|
||||
@@ -1,199 +1,72 @@
|
||||
package org.qortal.crosschain;
|
||||
|
||||
import java.io.ByteArrayInputStream;
|
||||
import java.io.DataOutputStream;
|
||||
import java.io.File;
|
||||
import java.io.FileInputStream;
|
||||
import java.io.FileNotFoundException;
|
||||
import java.io.FileOutputStream;
|
||||
import java.io.IOException;
|
||||
import java.io.InputStream;
|
||||
import java.io.OutputStreamWriter;
|
||||
import java.io.PrintWriter;
|
||||
import java.nio.ByteBuffer;
|
||||
import java.nio.charset.StandardCharsets;
|
||||
import java.security.DigestOutputStream;
|
||||
import java.security.MessageDigest;
|
||||
import java.util.Date;
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
import java.util.concurrent.ExecutionException;
|
||||
import java.util.concurrent.TimeoutException;
|
||||
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.BlockChain;
|
||||
import org.bitcoinj.core.CheckpointManager;
|
||||
import org.bitcoinj.core.Coin;
|
||||
import org.bitcoinj.core.ECKey;
|
||||
import org.bitcoinj.core.NetworkParameters;
|
||||
import org.bitcoinj.core.PeerGroup;
|
||||
import org.bitcoinj.core.Sha256Hash;
|
||||
import org.bitcoinj.core.StoredBlock;
|
||||
import org.bitcoinj.core.Transaction;
|
||||
import org.bitcoinj.core.TransactionOutput;
|
||||
import org.bitcoinj.core.VerificationException;
|
||||
import org.bitcoinj.core.listeners.NewBestBlockListener;
|
||||
import org.bitcoinj.net.discovery.DnsDiscovery;
|
||||
import org.bitcoinj.params.MainNetParams;
|
||||
import org.bitcoinj.params.RegTestParams;
|
||||
import org.bitcoinj.params.TestNet3Params;
|
||||
import org.bitcoinj.script.Script;
|
||||
import org.bitcoinj.store.BlockStore;
|
||||
import org.bitcoinj.store.BlockStoreException;
|
||||
import org.bitcoinj.store.SPVBlockStore;
|
||||
import org.bitcoinj.utils.Threading;
|
||||
import org.bitcoinj.wallet.KeyChainGroup;
|
||||
import org.bitcoinj.wallet.Wallet;
|
||||
import org.bitcoinj.wallet.listeners.WalletCoinsReceivedEventListener;
|
||||
import org.bitcoinj.script.ScriptBuilder;
|
||||
import org.bitcoinj.utils.MonetaryFormat;
|
||||
import org.qortal.settings.Settings;
|
||||
import org.qortal.utils.BitTwiddling;
|
||||
import org.qortal.utils.Pair;
|
||||
|
||||
public class BTC {
|
||||
|
||||
private static class RollbackBlockChain extends BlockChain {
|
||||
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;
|
||||
|
||||
public RollbackBlockChain(NetworkParameters params, BlockStore blockStore) throws BlockStoreException {
|
||||
super(params, blockStore);
|
||||
}
|
||||
protected static final Logger LOGGER = LogManager.getLogger(BTC.class);
|
||||
|
||||
@Override
|
||||
public void setChainHead(StoredBlock chainHead) throws BlockStoreException {
|
||||
super.setChainHead(chainHead);
|
||||
}
|
||||
private static final int TIMESTAMP_OFFSET = 4 + 32 + 32;
|
||||
|
||||
}
|
||||
|
||||
private static class UpdateableCheckpointManager extends CheckpointManager implements NewBestBlockListener {
|
||||
|
||||
private static final int checkpointInterval = 500;
|
||||
|
||||
private static final String minimalTestNet3TextFile = "TXT CHECKPOINTS 1\n0\n1\nAAAAAAAAB+EH4QfhAAAH4AEAAAApmwX6UCEnJcYIKTa7HO3pFkqqNhAzJVBMdEuGAAAAAPSAvVCBUypCbBW/OqU0oIF7ISF84h2spOqHrFCWN9Zw6r6/T///AB0E5oOO\n";
|
||||
private static final String minimalMainNetTextFile = "TXT CHECKPOINTS 1\n0\n1\nAAAAAAAAB+EH4QfhAAAH4AEAAABjl7tqvU/FIcDT9gcbVlA4nwtFUbxAtOawZzBpAAAAAKzkcK7NqciBjI/ldojNKncrWleVSgDfBCCn3VRrbSxXaw5/Sf//AB0z8Bkv\n";
|
||||
|
||||
public UpdateableCheckpointManager(NetworkParameters params) throws IOException {
|
||||
super(params, getMinimalTextFileStream(params));
|
||||
}
|
||||
|
||||
public UpdateableCheckpointManager(NetworkParameters params, InputStream inputStream) throws IOException {
|
||||
super(params, inputStream);
|
||||
}
|
||||
|
||||
private static ByteArrayInputStream getMinimalTextFileStream(NetworkParameters params) {
|
||||
if (params == MainNetParams.get())
|
||||
return new ByteArrayInputStream(minimalMainNetTextFile.getBytes());
|
||||
|
||||
if (params == TestNet3Params.get())
|
||||
return new ByteArrayInputStream(minimalTestNet3TextFile.getBytes());
|
||||
|
||||
throw new RuntimeException("Failed to construct empty UpdateableCheckpointManageer");
|
||||
}
|
||||
|
||||
@Override
|
||||
public void notifyNewBestBlock(StoredBlock block) throws VerificationException {
|
||||
int height = block.getHeight();
|
||||
|
||||
if (height % checkpointInterval == 0)
|
||||
checkpoints.put(block.getHeader().getTimeSeconds(), block);
|
||||
}
|
||||
|
||||
public void saveAsText(File textFile) throws FileNotFoundException {
|
||||
try (PrintWriter writer = new PrintWriter(new OutputStreamWriter(new FileOutputStream(textFile), StandardCharsets.US_ASCII))) {
|
||||
writer.println("TXT CHECKPOINTS 1");
|
||||
writer.println("0"); // Number of signatures to read. Do this later.
|
||||
writer.println(checkpoints.size());
|
||||
ByteBuffer buffer = ByteBuffer.allocate(StoredBlock.COMPACT_SERIALIZED_SIZE);
|
||||
for (StoredBlock block : checkpoints.values()) {
|
||||
block.serializeCompact(buffer);
|
||||
writer.println(CheckpointManager.BASE64.encode(buffer.array()));
|
||||
buffer.position(0);
|
||||
}
|
||||
public enum BitcoinNet {
|
||||
MAIN {
|
||||
@Override
|
||||
public NetworkParameters getParams() {
|
||||
return MainNetParams.get();
|
||||
}
|
||||
}
|
||||
|
||||
@SuppressWarnings("unused")
|
||||
public void saveAsBinary(File file) throws IOException {
|
||||
try (final FileOutputStream fileOutputStream = new FileOutputStream(file, false)) {
|
||||
MessageDigest digest = Sha256Hash.newDigest();
|
||||
|
||||
try (final DigestOutputStream digestOutputStream = new DigestOutputStream(fileOutputStream, digest)) {
|
||||
digestOutputStream.on(false);
|
||||
|
||||
try (final DataOutputStream dataOutputStream = new DataOutputStream(digestOutputStream)) {
|
||||
dataOutputStream.writeBytes("CHECKPOINTS 1");
|
||||
dataOutputStream.writeInt(0); // Number of signatures to read. Do this later.
|
||||
digestOutputStream.on(true);
|
||||
dataOutputStream.writeInt(checkpoints.size());
|
||||
ByteBuffer buffer = ByteBuffer.allocate(StoredBlock.COMPACT_SERIALIZED_SIZE);
|
||||
for (StoredBlock block : checkpoints.values()) {
|
||||
block.serializeCompact(buffer);
|
||||
dataOutputStream.write(buffer.array());
|
||||
buffer.position(0);
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
TEST3 {
|
||||
@Override
|
||||
public NetworkParameters getParams() {
|
||||
return TestNet3Params.get();
|
||||
}
|
||||
}
|
||||
},
|
||||
REGTEST {
|
||||
@Override
|
||||
public NetworkParameters getParams() {
|
||||
return RegTestParams.get();
|
||||
}
|
||||
};
|
||||
|
||||
public abstract NetworkParameters getParams();
|
||||
}
|
||||
|
||||
private static BTC instance;
|
||||
private static final Object instanceLock = new Object();
|
||||
private final NetworkParameters params;
|
||||
private final ElectrumX electrumX;
|
||||
|
||||
private static File directory;
|
||||
private static String chainFileName;
|
||||
private static String checkpointsFileName;
|
||||
|
||||
private static NetworkParameters params;
|
||||
private static PeerGroup peerGroup;
|
||||
private static BlockStore blockStore;
|
||||
private static RollbackBlockChain chain;
|
||||
private static UpdateableCheckpointManager manager;
|
||||
// Constructors and instance
|
||||
|
||||
private BTC() {
|
||||
// Start wallet
|
||||
if (Settings.getInstance().useBitcoinTestNet()) {
|
||||
params = TestNet3Params.get();
|
||||
chainFileName = "bitcoinj-testnet.spvchain";
|
||||
checkpointsFileName = "checkpoints-testnet.txt";
|
||||
} else {
|
||||
params = MainNetParams.get();
|
||||
chainFileName = "bitcoinj.spvchain";
|
||||
checkpointsFileName = "checkpoints.txt";
|
||||
}
|
||||
BitcoinNet bitcoinNet = Settings.getInstance().getBitcoinNet();
|
||||
this.params = bitcoinNet.getParams();
|
||||
|
||||
directory = new File("Qortal-BTC");
|
||||
if (!directory.exists())
|
||||
directory.mkdirs();
|
||||
LOGGER.info(() -> String.format("Starting Bitcoin support using %s", bitcoinNet.name()));
|
||||
|
||||
File chainFile = new File(directory, chainFileName);
|
||||
|
||||
try {
|
||||
blockStore = new SPVBlockStore(params, chainFile);
|
||||
} catch (BlockStoreException e) {
|
||||
throw new RuntimeException("Failed to open/create BTC SPVBlockStore", e);
|
||||
}
|
||||
|
||||
File checkpointsFile = new File(directory, checkpointsFileName);
|
||||
try (InputStream checkpointsStream = new FileInputStream(checkpointsFile)) {
|
||||
manager = new UpdateableCheckpointManager(params, checkpointsStream);
|
||||
} catch (FileNotFoundException e) {
|
||||
// Construct with no checkpoints then
|
||||
try {
|
||||
manager = new UpdateableCheckpointManager(params);
|
||||
} catch (IOException e2) {
|
||||
throw new RuntimeException("Failed to create new BTC checkpoints", e2);
|
||||
}
|
||||
} catch (IOException e) {
|
||||
throw new RuntimeException("Failed to load BTC checkpoints", e);
|
||||
}
|
||||
|
||||
try {
|
||||
chain = new RollbackBlockChain(params, blockStore);
|
||||
} catch (BlockStoreException e) {
|
||||
throw new RuntimeException("Failed to construct BTC blockchain", e);
|
||||
}
|
||||
|
||||
peerGroup = new PeerGroup(params, chain);
|
||||
peerGroup.setUserAgent("qortal", "1.0");
|
||||
peerGroup.addPeerDiscovery(new DnsDiscovery(params));
|
||||
peerGroup.start();
|
||||
this.electrumX = ElectrumX.getInstance(bitcoinNet.name());
|
||||
}
|
||||
|
||||
public static synchronized BTC getInstance() {
|
||||
@@ -203,109 +76,84 @@ public class BTC {
|
||||
return instance;
|
||||
}
|
||||
|
||||
public void shutdown() {
|
||||
synchronized (instanceLock) {
|
||||
if (instance == null)
|
||||
return;
|
||||
// Getters & setters
|
||||
|
||||
instance = null;
|
||||
}
|
||||
|
||||
peerGroup.stop();
|
||||
|
||||
try {
|
||||
blockStore.close();
|
||||
} catch (BlockStoreException e) {
|
||||
// What can we do?
|
||||
}
|
||||
public NetworkParameters getNetworkParameters() {
|
||||
return this.params;
|
||||
}
|
||||
|
||||
protected Wallet createEmptyWallet() {
|
||||
ECKey dummyKey = new ECKey();
|
||||
|
||||
KeyChainGroup keyChainGroup = KeyChainGroup.createBasic(params);
|
||||
keyChainGroup.importKeys(dummyKey);
|
||||
|
||||
Wallet wallet = new Wallet(params, keyChainGroup);
|
||||
|
||||
wallet.removeKey(dummyKey);
|
||||
|
||||
return wallet;
|
||||
public static synchronized void resetForTesting() {
|
||||
instance = null;
|
||||
}
|
||||
|
||||
public void watch(String base58Address, long startTime) throws InterruptedException, ExecutionException, TimeoutException, BlockStoreException {
|
||||
Wallet wallet = createEmptyWallet();
|
||||
// Actual useful methods for use by other classes
|
||||
|
||||
WalletCoinsReceivedEventListener coinsReceivedListener = new WalletCoinsReceivedEventListener() {
|
||||
@Override
|
||||
public void onCoinsReceived(Wallet wallet, Transaction tx, Coin prevBalance, Coin newBalance) {
|
||||
System.out.println("Coins received via transaction " + tx.getTxId().toString());
|
||||
}
|
||||
};
|
||||
wallet.addCoinsReceivedEventListener(coinsReceivedListener);
|
||||
/** Returns median timestamp from latest 11 blocks, in seconds. */
|
||||
public Integer getMedianBlockTime() {
|
||||
Integer height = this.electrumX.getCurrentHeight();
|
||||
if (height == null)
|
||||
return null;
|
||||
|
||||
Address address = Address.fromString(params, base58Address);
|
||||
wallet.addWatchedAddress(address, startTime);
|
||||
// Grab latest 11 blocks
|
||||
List<byte[]> blockHeaders = this.electrumX.getBlockHeaders(height - 11, 11);
|
||||
if (blockHeaders == null || blockHeaders.size() < 11)
|
||||
return null;
|
||||
|
||||
StoredBlock checkpoint = manager.getCheckpointBefore(startTime);
|
||||
blockStore.put(checkpoint);
|
||||
blockStore.setChainHead(checkpoint);
|
||||
chain.setChainHead(checkpoint);
|
||||
List<Integer> blockTimestamps = blockHeaders.stream().map(blockHeader -> BitTwiddling.fromLEBytes(blockHeader, TIMESTAMP_OFFSET)).collect(Collectors.toList());
|
||||
|
||||
chain.addWallet(wallet);
|
||||
peerGroup.addWallet(wallet);
|
||||
peerGroup.setFastCatchupTimeSecs(startTime);
|
||||
// Descending, but order shouldn't matter as we're picking median...
|
||||
blockTimestamps.sort((a, b) -> Integer.compare(b, a));
|
||||
|
||||
peerGroup.addBlocksDownloadedEventListener((peer, block, filteredBlock, blocksLeft) -> {
|
||||
if (blocksLeft % 1000 == 0)
|
||||
System.out.println("Blocks left: " + blocksLeft);
|
||||
});
|
||||
|
||||
System.out.println("Starting download...");
|
||||
peerGroup.downloadBlockChain();
|
||||
|
||||
List<TransactionOutput> outputs = wallet.getWatchedOutputs(true);
|
||||
|
||||
peerGroup.removeWallet(wallet);
|
||||
chain.removeWallet(wallet);
|
||||
|
||||
for (TransactionOutput output : outputs)
|
||||
System.out.println(output.toString());
|
||||
return blockTimestamps.get(5);
|
||||
}
|
||||
|
||||
public void watch(Script script) {
|
||||
// wallet.addWatchedScripts(scripts);
|
||||
public Coin getBalance(String base58Address) {
|
||||
Long balance = this.electrumX.getBalance(addressToScript(base58Address));
|
||||
if (balance == null)
|
||||
return null;
|
||||
|
||||
return Coin.valueOf(balance);
|
||||
}
|
||||
|
||||
public void updateCheckpoints() {
|
||||
final long now = new Date().getTime() / 1000 - 86400;
|
||||
public List<TransactionOutput> getUnspentOutputs(String base58Address) {
|
||||
List<Pair<byte[], Integer>> unspentOutputs = this.electrumX.getUnspentOutputs(addressToScript(base58Address));
|
||||
if (unspentOutputs == null)
|
||||
return null;
|
||||
|
||||
try {
|
||||
StoredBlock checkpoint = manager.getCheckpointBefore(now);
|
||||
blockStore.put(checkpoint);
|
||||
blockStore.setChainHead(checkpoint);
|
||||
chain.setChainHead(checkpoint);
|
||||
} catch (BlockStoreException e) {
|
||||
throw new RuntimeException("Failed to update BTC checkpoints", e);
|
||||
List<TransactionOutput> unspentTransactionOutputs = new ArrayList<>();
|
||||
for (Pair<byte[], Integer> unspentOutput : unspentOutputs) {
|
||||
List<TransactionOutput> transactionOutputs = getOutputs(unspentOutput.getA());
|
||||
if (transactionOutputs == null)
|
||||
return null;
|
||||
|
||||
unspentTransactionOutputs.add(transactionOutputs.get(unspentOutput.getB()));
|
||||
}
|
||||
|
||||
peerGroup.setFastCatchupTimeSecs(now);
|
||||
return unspentTransactionOutputs;
|
||||
}
|
||||
|
||||
chain.addNewBestBlockListener(Threading.SAME_THREAD, manager);
|
||||
public List<TransactionOutput> getOutputs(byte[] txHash) {
|
||||
byte[] rawTransactionBytes = this.electrumX.getRawTransaction(txHash);
|
||||
if (rawTransactionBytes == null)
|
||||
return null;
|
||||
|
||||
peerGroup.addBlocksDownloadedEventListener((peer, block, filteredBlock, blocksLeft) -> {
|
||||
if (blocksLeft % 1000 == 0)
|
||||
System.out.println("Blocks left: " + blocksLeft);
|
||||
});
|
||||
Transaction transaction = new Transaction(this.params, rawTransactionBytes);
|
||||
return transaction.getOutputs();
|
||||
}
|
||||
|
||||
System.out.println("Starting download...");
|
||||
peerGroup.downloadBlockChain();
|
||||
public List<byte[]> getAddressTransactions(String base58Address) {
|
||||
return this.electrumX.getAddressTransactions(addressToScript(base58Address));
|
||||
}
|
||||
|
||||
try {
|
||||
manager.saveAsText(new File(directory, checkpointsFileName));
|
||||
} catch (FileNotFoundException e) {
|
||||
throw new RuntimeException("Failed to save updated BTC checkpoints", e);
|
||||
}
|
||||
public boolean broadcastTransaction(Transaction transaction) {
|
||||
return this.electrumX.broadcastTransaction(transaction.bitcoinSerialize());
|
||||
}
|
||||
|
||||
// Utility methods for us
|
||||
|
||||
private byte[] addressToScript(String base58Address) {
|
||||
Address address = Address.fromString(this.params, base58Address);
|
||||
return ScriptBuilder.createOutputScript(address).getProgram();
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
669
src/main/java/org/qortal/crosschain/BTCACCT.java
Normal file
669
src/main/java/org/qortal/crosschain/BTCACCT.java
Normal file
@@ -0,0 +1,669 @@
|
||||
package org.qortal.crosschain;
|
||||
|
||||
import static org.ciyam.at.OpCode.calcOffset;
|
||||
|
||||
import java.nio.ByteBuffer;
|
||||
import java.util.Arrays;
|
||||
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.ciyam.at.API;
|
||||
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.qortal.account.Account;
|
||||
import org.qortal.asset.Asset;
|
||||
import org.qortal.at.QortalAtLoggerFactory;
|
||||
import org.qortal.block.BlockChain;
|
||||
import org.qortal.block.BlockChain.CiyamAtSettings;
|
||||
import org.qortal.crypto.Crypto;
|
||||
import org.qortal.data.at.ATData;
|
||||
import org.qortal.data.at.ATStateData;
|
||||
import org.qortal.data.block.BlockData;
|
||||
import org.qortal.data.crosschain.CrossChainTradeData;
|
||||
import org.qortal.repository.DataException;
|
||||
import org.qortal.repository.Repository;
|
||||
import org.qortal.utils.Base58;
|
||||
import org.qortal.utils.BitTwiddling;
|
||||
|
||||
import com.google.common.hash.HashCode;
|
||||
import com.google.common.primitives.Bytes;
|
||||
|
||||
/*
|
||||
* Bob generates Bitcoin private key
|
||||
* private key required to sign P2SH redeem tx
|
||||
* private key can be used to create 'secret' (e.g. double-SHA256)
|
||||
* encrypted private key could be stored in Qortal AT for access by Bob from any node
|
||||
* Bob creates Qortal AT
|
||||
* Alice finds Qortal AT and wants to trade
|
||||
* Alice generates Bitcoin private key
|
||||
* Alice will need to send Bob her Qortal address and Bitcoin refund address
|
||||
* Bob sends Alice's Qortal address to Qortal AT
|
||||
* Qortal AT sends initial QORT payment to Alice (so she has QORT to send message to AT and claim funds)
|
||||
* Alice receives funds and checks Qortal AT to confirm it's locked to her
|
||||
* Alice creates/funds Bitcoin P2SH
|
||||
* Alice requires: Bob's redeem Bitcoin address, Alice's refund Bitcoin address, derived locktime
|
||||
* Bob checks P2SH is funded
|
||||
* Bob requires: Bob's redeem Bitcoin address, Alice's refund Bitcoin address, derived locktime
|
||||
* Bob uses secret to redeem P2SH
|
||||
* Qortal core/UI will need to create, and sign, this transaction
|
||||
* Alice scans P2SH redeem tx and uses secret to redeem Qortal AT
|
||||
*/
|
||||
|
||||
public class BTCACCT {
|
||||
|
||||
public static final int SECRET_LENGTH = 32;
|
||||
public static final int MIN_LOCKTIME = 1500000000;
|
||||
public static final byte[] CODE_BYTES_HASH = HashCode.fromString("edcdb1feb36e079c5f956faff2f24219b12e5fbaaa05654335e615e33218282f").asBytes(); // SHA256 of AT code bytes
|
||||
|
||||
/*
|
||||
* 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 BTCACCT} 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
|
||||
* @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) {
|
||||
NetworkParameters params = BTC.getInstance().getNetworkParameters();
|
||||
|
||||
Transaction transaction = new Transaction(params);
|
||||
transaction.setVersion(2);
|
||||
|
||||
// Output is back to P2SH funder
|
||||
transaction.addOutput(amount, ScriptBuilder.createP2PKHOutputScript(spendKey.getPubKeyHash()));
|
||||
|
||||
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, so no lockTime and no RBF
|
||||
else
|
||||
input.setSequenceNumber(BTC.NO_LOCKTIME_NO_RBF_SEQUENCE); // Use max-value - 1, so lockTime can be used but not 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();
|
||||
};
|
||||
|
||||
return buildP2shTransaction(refundAmount, refundKey, fundingOutputs, redeemScriptBytes, lockTime, refundSigScriptBuilder);
|
||||
}
|
||||
|
||||
/**
|
||||
* 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
|
||||
* @return Signed Bitcoin transaction for redeeming P2SH
|
||||
*/
|
||||
public static Transaction buildRedeemTransaction(Coin redeemAmount, ECKey redeemKey, List<TransactionOutput> fundingOutputs, byte[] redeemScriptBytes, byte[] secret) {
|
||||
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);
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns Qortal AT creation bytes for cross-chain trading AT.
|
||||
* <p>
|
||||
* <tt>tradeTimeout</tt> (minutes) is the time window for the recipient to send the
|
||||
* 32-byte secret to the AT, before the AT automatically refunds the AT's creator.
|
||||
*
|
||||
* @param qortalCreator Qortal address for AT creator, also used for refunds
|
||||
* @param secretHash 20-byte HASH160 of 32-byte secret
|
||||
* @param tradeTimeout how many minutes, from start of 'trade mode' until AT auto-refunds AT creator
|
||||
* @param initialPayout how much QORT to pay trade partner upon switch to 'trade mode'
|
||||
* @param redeemPayout how much QORT to pay trade partner if they send correct 32-byte secret to AT
|
||||
* @param bitcoinAmount how much BTC the AT creator is expecting to trade
|
||||
* @return
|
||||
*/
|
||||
public static byte[] buildQortalAT(String qortalCreator, byte[] secretHash, int tradeTimeout, long initialPayout, long redeemPayout, long bitcoinAmount) {
|
||||
// Labels for data segment addresses
|
||||
int addrCounter = 0;
|
||||
|
||||
// Constants (with corresponding dataByteBuffer.put*() calls below)
|
||||
|
||||
final int addrQortalCreator1 = addrCounter++;
|
||||
final int addrQortalCreator2 = addrCounter++;
|
||||
final int addrQortalCreator3 = addrCounter++;
|
||||
final int addrQortalCreator4 = addrCounter++;
|
||||
|
||||
final int addrSecretHash = addrCounter;
|
||||
addrCounter += 4;
|
||||
|
||||
final int addrTradeTimeout = addrCounter++;
|
||||
final int addrInitialPayoutAmount = addrCounter++;
|
||||
final int addrRedeemPayoutAmount = addrCounter++;
|
||||
final int addrBitcoinAmount = addrCounter++;
|
||||
|
||||
final int addrMessageTxType = addrCounter++;
|
||||
|
||||
final int addrSecretHashPointer = addrCounter++;
|
||||
final int addrQortalRecipientPointer = addrCounter++;
|
||||
final int addrMessageSenderPointer = addrCounter++;
|
||||
|
||||
final int addrMessageDataPointer = addrCounter++;
|
||||
final int addrMessageDataLength = addrCounter++;
|
||||
|
||||
final int addrEndOfConstants = addrCounter;
|
||||
|
||||
// Variables
|
||||
|
||||
final int addrQortalRecipient1 = addrCounter++;
|
||||
final int addrQortalRecipient2 = addrCounter++;
|
||||
final int addrQortalRecipient3 = addrCounter++;
|
||||
final int addrQortalRecipient4 = addrCounter++;
|
||||
|
||||
final int addrTradeRefundTimestamp = addrCounter++;
|
||||
final int addrLastTxTimestamp = addrCounter++;
|
||||
final int addrBlockTimestamp = addrCounter++;
|
||||
final int addrTxType = addrCounter++;
|
||||
final int addrResult = addrCounter++;
|
||||
|
||||
final int addrMessageSender1 = addrCounter++;
|
||||
final int addrMessageSender2 = addrCounter++;
|
||||
final int addrMessageSender3 = addrCounter++;
|
||||
final int addrMessageSender4 = addrCounter++;
|
||||
|
||||
final int addrMessageData = addrCounter;
|
||||
addrCounter += 4;
|
||||
|
||||
// Data segment
|
||||
ByteBuffer dataByteBuffer = ByteBuffer.allocate(addrCounter * MachineState.VALUE_SIZE);
|
||||
|
||||
// AT creator's Qortal address, decoded from Base58
|
||||
assert dataByteBuffer.position() == addrQortalCreator1 * MachineState.VALUE_SIZE : "addrQortalCreator1 incorrect";
|
||||
byte[] qortalCreatorBytes = Base58.decode(qortalCreator);
|
||||
dataByteBuffer.put(Bytes.ensureCapacity(qortalCreatorBytes, 32, 0));
|
||||
|
||||
// Hash of secret
|
||||
assert dataByteBuffer.position() == addrSecretHash * MachineState.VALUE_SIZE : "addrSecretHash incorrect";
|
||||
dataByteBuffer.put(Bytes.ensureCapacity(secretHash, 32, 0));
|
||||
|
||||
// Trade timeout in minutes
|
||||
assert dataByteBuffer.position() == addrTradeTimeout * MachineState.VALUE_SIZE : "addrTradeTimeout incorrect";
|
||||
dataByteBuffer.putLong(tradeTimeout);
|
||||
|
||||
// Initial payout amount
|
||||
assert dataByteBuffer.position() == addrInitialPayoutAmount * MachineState.VALUE_SIZE : "addrInitialPayoutAmount incorrect";
|
||||
dataByteBuffer.putLong(initialPayout);
|
||||
|
||||
// Redeem payout amount
|
||||
assert dataByteBuffer.position() == addrRedeemPayoutAmount * MachineState.VALUE_SIZE : "addrRedeemPayoutAmount incorrect";
|
||||
dataByteBuffer.putLong(redeemPayout);
|
||||
|
||||
// Expected Bitcoin amount
|
||||
assert dataByteBuffer.position() == addrBitcoinAmount * MachineState.VALUE_SIZE : "addrBitcoinAmount incorrect";
|
||||
dataByteBuffer.putLong(bitcoinAmount);
|
||||
|
||||
// We're only interested in MESSAGE transactions
|
||||
assert dataByteBuffer.position() == addrMessageTxType * MachineState.VALUE_SIZE : "addrMessageTxType incorrect";
|
||||
dataByteBuffer.putLong(API.ATTransactionType.MESSAGE.value);
|
||||
|
||||
// Index into data segment of hash, used by GET_B_IND
|
||||
assert dataByteBuffer.position() == addrSecretHashPointer * MachineState.VALUE_SIZE : "addrSecretHashPointer incorrect";
|
||||
dataByteBuffer.putLong(addrSecretHash);
|
||||
|
||||
// Index into data segment of recipient address, used by SET_B_IND
|
||||
assert dataByteBuffer.position() == addrQortalRecipientPointer * MachineState.VALUE_SIZE : "addrQortalRecipientPointer incorrect";
|
||||
dataByteBuffer.putLong(addrQortalRecipient1);
|
||||
|
||||
// Index into data segment of (temporary) transaction's sender's address, used by GET_B_IND
|
||||
assert dataByteBuffer.position() == addrMessageSenderPointer * MachineState.VALUE_SIZE : "addrMessageSenderPointer incorrect";
|
||||
dataByteBuffer.putLong(addrMessageSender1);
|
||||
|
||||
// Source location and length for hashing any passed secret
|
||||
assert dataByteBuffer.position() == addrMessageDataPointer * MachineState.VALUE_SIZE : "addrMessageDataPointer incorrect";
|
||||
dataByteBuffer.putLong(addrMessageData);
|
||||
assert dataByteBuffer.position() == addrMessageDataLength * MachineState.VALUE_SIZE : "addrMessageDataLength incorrect";
|
||||
dataByteBuffer.putLong(32L);
|
||||
|
||||
assert dataByteBuffer.position() == addrEndOfConstants * MachineState.VALUE_SIZE : "dataByteBuffer position not at end of constants";
|
||||
|
||||
// Code labels
|
||||
Integer labelRefund = null;
|
||||
|
||||
Integer labelOfferTxLoop = null;
|
||||
Integer labelCheckOfferTx = null;
|
||||
|
||||
Integer labelTradeMode = null;
|
||||
Integer labelTradeTxLoop = null;
|
||||
Integer labelCheckTradeTx = 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 from AT owner containing trade partner details, or AT owner's address to cancel offer */
|
||||
|
||||
/* Transaction processing loop */
|
||||
labelOfferTxLoop = codeByteBuffer.position();
|
||||
|
||||
// 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, calcOffset(codeByteBuffer, labelCheckOfferTx)));
|
||||
// Stop and wait for next block
|
||||
codeByteBuffer.put(OpCode.STP_IMD.compile());
|
||||
|
||||
/* Check transaction */
|
||||
labelCheckOfferTx = 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));
|
||||
// Extract transaction type (message/payment) from transaction and save type in addrTxType
|
||||
codeByteBuffer.put(OpCode.EXT_FUN_RET.compile(FunctionCode.GET_TYPE_FROM_TX_IN_A, addrTxType));
|
||||
// If transaction type is not MESSAGE type then go look for another transaction
|
||||
codeByteBuffer.put(OpCode.BNE_DAT.compile(addrTxType, addrMessageTxType, calcOffset(codeByteBuffer, labelOfferTxLoop)));
|
||||
|
||||
/* Check transaction's sender */
|
||||
|
||||
// Extract sender address from transaction into B register
|
||||
codeByteBuffer.put(OpCode.EXT_FUN.compile(FunctionCode.PUT_ADDRESS_FROM_TX_IN_A_INTO_B));
|
||||
// Save B register into data segment starting at addrMessageSender1 (as pointed to by addrMessageSenderPointer)
|
||||
codeByteBuffer.put(OpCode.EXT_FUN_DAT.compile(FunctionCode.GET_B_IND, addrMessageSenderPointer));
|
||||
// Compare each part of transaction's sender's address with expected address. If they don't match, look for another transaction.
|
||||
codeByteBuffer.put(OpCode.BNE_DAT.compile(addrMessageSender1, addrQortalCreator1, calcOffset(codeByteBuffer, labelOfferTxLoop)));
|
||||
codeByteBuffer.put(OpCode.BNE_DAT.compile(addrMessageSender2, addrQortalCreator2, calcOffset(codeByteBuffer, labelOfferTxLoop)));
|
||||
codeByteBuffer.put(OpCode.BNE_DAT.compile(addrMessageSender3, addrQortalCreator3, calcOffset(codeByteBuffer, labelOfferTxLoop)));
|
||||
codeByteBuffer.put(OpCode.BNE_DAT.compile(addrMessageSender4, addrQortalCreator4, calcOffset(codeByteBuffer, labelOfferTxLoop)));
|
||||
|
||||
/* Extract trade partner info from message */
|
||||
|
||||
// Extract message from transaction into B register
|
||||
codeByteBuffer.put(OpCode.EXT_FUN.compile(FunctionCode.PUT_MESSAGE_FROM_TX_IN_A_INTO_B));
|
||||
// Save B register into data segment starting at addrQortalRecipient1 (as pointed to by addrQortalRecipientPointer)
|
||||
codeByteBuffer.put(OpCode.EXT_FUN_DAT.compile(FunctionCode.GET_B_IND, addrQortalRecipientPointer));
|
||||
// Compare each of recipient address with creator's address (for offer-cancel scenario). If they don't match, assume recipient is trade partner.
|
||||
codeByteBuffer.put(OpCode.BNE_DAT.compile(addrQortalRecipient1, addrQortalCreator1, calcOffset(codeByteBuffer, labelTradeMode)));
|
||||
codeByteBuffer.put(OpCode.BNE_DAT.compile(addrQortalRecipient2, addrQortalCreator2, calcOffset(codeByteBuffer, labelTradeMode)));
|
||||
codeByteBuffer.put(OpCode.BNE_DAT.compile(addrQortalRecipient3, addrQortalCreator3, calcOffset(codeByteBuffer, labelTradeMode)));
|
||||
codeByteBuffer.put(OpCode.BNE_DAT.compile(addrQortalRecipient4, addrQortalCreator4, calcOffset(codeByteBuffer, labelTradeMode)));
|
||||
// Recipient address is AT creator's address, so cancel offer and finish.
|
||||
codeByteBuffer.put(OpCode.JMP_ADR.compile(labelRefund == null ? 0 : labelRefund));
|
||||
|
||||
/* Switch to 'trade mode' */
|
||||
labelTradeMode = codeByteBuffer.position();
|
||||
|
||||
// Send initial payment to recipient so they have enough funds to message AT if all goes well
|
||||
codeByteBuffer.put(OpCode.EXT_FUN_DAT.compile(FunctionCode.PAY_TO_ADDRESS_IN_B, addrInitialPayoutAmount));
|
||||
|
||||
// Calculate trade timeout refund 'timestamp' by adding addrTradeTimeout minutes to above message's 'timestamp', then save into addrTradeRefundTimestamp
|
||||
codeByteBuffer.put(OpCode.EXT_FUN_RET_DAT_2.compile(FunctionCode.ADD_MINUTES_TO_TIMESTAMP, addrTradeRefundTimestamp, addrLastTxTimestamp, addrTradeTimeout));
|
||||
|
||||
// Set restart position to after this opcode
|
||||
codeByteBuffer.put(OpCode.SET_PCS.compile());
|
||||
|
||||
/* Loop, waiting for trade timeout or message from Qortal trade recipient containing secret */
|
||||
|
||||
// Fetch current block 'timestamp'
|
||||
codeByteBuffer.put(OpCode.EXT_FUN_RET.compile(FunctionCode.GET_BLOCK_TIMESTAMP, addrBlockTimestamp));
|
||||
// If we're not past refund 'timestamp' then look for next transaction
|
||||
codeByteBuffer.put(OpCode.BLT_DAT.compile(addrBlockTimestamp, addrTradeRefundTimestamp, calcOffset(codeByteBuffer, labelTradeTxLoop)));
|
||||
// We're past refund 'timestamp' so go refund everything back to AT creator
|
||||
codeByteBuffer.put(OpCode.JMP_ADR.compile(labelRefund == null ? 0 : labelRefund));
|
||||
|
||||
/* Transaction processing loop */
|
||||
labelTradeTxLoop = codeByteBuffer.position();
|
||||
|
||||
// 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, calcOffset(codeByteBuffer, labelCheckTradeTx)));
|
||||
// Stop and wait for next block
|
||||
codeByteBuffer.put(OpCode.STP_IMD.compile());
|
||||
|
||||
/* Check transaction */
|
||||
labelCheckTradeTx = 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));
|
||||
// Extract transaction type (message/payment) from transaction and save type in addrTxType
|
||||
codeByteBuffer.put(OpCode.EXT_FUN_RET.compile(FunctionCode.GET_TYPE_FROM_TX_IN_A, addrTxType));
|
||||
// If transaction type is not MESSAGE type then go look for another transaction
|
||||
codeByteBuffer.put(OpCode.BNE_DAT.compile(addrTxType, addrMessageTxType, calcOffset(codeByteBuffer, labelTradeTxLoop)));
|
||||
|
||||
/* Check transaction's sender */
|
||||
|
||||
// Extract sender address from transaction into B register
|
||||
codeByteBuffer.put(OpCode.EXT_FUN.compile(FunctionCode.PUT_ADDRESS_FROM_TX_IN_A_INTO_B));
|
||||
// Save B register into data segment starting at addrMessageSender1 (as pointed to by addrMessageSenderPointer)
|
||||
codeByteBuffer.put(OpCode.EXT_FUN_DAT.compile(FunctionCode.GET_B_IND, addrMessageSenderPointer));
|
||||
// Compare each part of transaction's sender's address with expected address. If they don't match, look for another transaction.
|
||||
codeByteBuffer.put(OpCode.BNE_DAT.compile(addrMessageSender1, addrQortalRecipient1, calcOffset(codeByteBuffer, labelTradeTxLoop)));
|
||||
codeByteBuffer.put(OpCode.BNE_DAT.compile(addrMessageSender2, addrQortalRecipient2, calcOffset(codeByteBuffer, labelTradeTxLoop)));
|
||||
codeByteBuffer.put(OpCode.BNE_DAT.compile(addrMessageSender3, addrQortalRecipient3, calcOffset(codeByteBuffer, labelTradeTxLoop)));
|
||||
codeByteBuffer.put(OpCode.BNE_DAT.compile(addrMessageSender4, addrQortalRecipient4, calcOffset(codeByteBuffer, labelTradeTxLoop)));
|
||||
|
||||
/* Check 'secret' in transaction's message */
|
||||
|
||||
// Extract message from transaction into B register
|
||||
codeByteBuffer.put(OpCode.EXT_FUN.compile(FunctionCode.PUT_MESSAGE_FROM_TX_IN_A_INTO_B));
|
||||
// Save B register into data segment starting at addrMessageData (as pointed to by addrMessageDataPointer)
|
||||
codeByteBuffer.put(OpCode.EXT_FUN_DAT.compile(FunctionCode.GET_B_IND, addrMessageDataPointer));
|
||||
// Load B register with expected hash result (as pointed to by addrSecretHashPointer)
|
||||
codeByteBuffer.put(OpCode.EXT_FUN_DAT.compile(FunctionCode.SET_B_IND, addrSecretHashPointer));
|
||||
// Perform HASH160 using source data at addrMessageData. (Location and length specified via addrMessageDataPointer and addrMessageDataLength).
|
||||
// Save the equality result (1 if they match, 0 otherwise) into addrResult.
|
||||
codeByteBuffer.put(OpCode.EXT_FUN_RET_DAT_2.compile(FunctionCode.CHECK_HASH160_WITH_B, addrResult, addrMessageDataPointer, addrMessageDataLength));
|
||||
// If hashes don't match, addrResult will be zero so go find another transaction
|
||||
codeByteBuffer.put(OpCode.BZR_DAT.compile(addrResult, calcOffset(codeByteBuffer, labelTradeTxLoop)));
|
||||
|
||||
/* Success! Pay arranged amount to intended recipient */
|
||||
|
||||
// Load B register with intended recipient address (as pointed to by addrQortalRecipientPointer)
|
||||
codeByteBuffer.put(OpCode.EXT_FUN_DAT.compile(FunctionCode.SET_B_IND, addrQortalRecipientPointer));
|
||||
// Pay AT's balance to recipient
|
||||
codeByteBuffer.put(OpCode.EXT_FUN_DAT.compile(FunctionCode.PAY_TO_ADDRESS_IN_B, addrRedeemPayoutAmount));
|
||||
// Fall-through to refunding any remaining balance back to AT creator
|
||||
|
||||
/* Refund balance back to AT creator */
|
||||
labelRefund = codeByteBuffer.position();
|
||||
|
||||
// Load B register with AT creator's address.
|
||||
codeByteBuffer.put(OpCode.EXT_FUN.compile(FunctionCode.PUT_CREATOR_INTO_B));
|
||||
// Pay AT's balance back to AT's creator.
|
||||
codeByteBuffer.put(OpCode.EXT_FUN.compile(FunctionCode.PAY_ALL_TO_ADDRESS_IN_B));
|
||||
// We're finished forever
|
||||
codeByteBuffer.put(OpCode.FIN_IMD.compile());
|
||||
} catch (CompilationException e) {
|
||||
throw new IllegalStateException("Unable to compile BTC-QORT ACCT?", e);
|
||||
}
|
||||
}
|
||||
|
||||
codeByteBuffer.flip();
|
||||
|
||||
byte[] codeBytes = new byte[codeByteBuffer.limit()];
|
||||
codeByteBuffer.get(codeBytes);
|
||||
|
||||
assert Arrays.equals(Crypto.digest(codeBytes), BTCACCT.CODE_BYTES_HASH)
|
||||
: String.format("BTCACCT.CODE_BYTES_HASH mismatch: expected %s, actual %s", HashCode.fromBytes(CODE_BYTES_HASH), HashCode.fromBytes(Crypto.digest(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);
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns CrossChainTradeData with useful info extracted from AT.
|
||||
*
|
||||
* @param repository
|
||||
* @param atAddress
|
||||
* @throws DataException
|
||||
*/
|
||||
public static CrossChainTradeData populateTradeData(Repository repository, ATData atData) throws DataException {
|
||||
String atAddress = atData.getATAddress();
|
||||
|
||||
ATStateData atStateData = repository.getATRepository().getLatestATState(atAddress);
|
||||
byte[] stateData = atStateData.getStateData();
|
||||
|
||||
QortalAtLoggerFactory loggerFactory = QortalAtLoggerFactory.getInstance();
|
||||
byte[] dataBytes = MachineState.extractDataBytes(loggerFactory, stateData);
|
||||
|
||||
CrossChainTradeData tradeData = new CrossChainTradeData();
|
||||
tradeData.qortalAtAddress = atAddress;
|
||||
tradeData.qortalCreator = Crypto.toAddress(atData.getCreatorPublicKey());
|
||||
tradeData.creationTimestamp = atData.getCreation();
|
||||
|
||||
Account atAccount = new Account(repository, atAddress);
|
||||
tradeData.qortBalance = atAccount.getConfirmedBalance(Asset.QORT);
|
||||
|
||||
ByteBuffer dataByteBuffer = ByteBuffer.wrap(dataBytes);
|
||||
byte[] addressBytes = new byte[32];
|
||||
|
||||
// Skip AT creator address
|
||||
dataByteBuffer.position(dataByteBuffer.position() + 32);
|
||||
|
||||
// Hash of secret
|
||||
tradeData.secretHash = new byte[20];
|
||||
dataByteBuffer.get(tradeData.secretHash);
|
||||
dataByteBuffer.position(dataByteBuffer.position() + 32 - 20); // skip to 32 bytes
|
||||
|
||||
// Trade timeout
|
||||
tradeData.tradeRefundTimeout = dataByteBuffer.getLong();
|
||||
|
||||
// Initial payout
|
||||
tradeData.initialPayout = dataByteBuffer.getLong();
|
||||
|
||||
// Redeem payout
|
||||
tradeData.redeemPayout = dataByteBuffer.getLong();
|
||||
|
||||
// Expected BTC amount
|
||||
tradeData.expectedBitcoin = dataByteBuffer.getLong();
|
||||
|
||||
// Skip MESSAGE transaction type
|
||||
dataByteBuffer.position(dataByteBuffer.position() + 8);
|
||||
|
||||
// Skip pointer to secretHash
|
||||
dataByteBuffer.position(dataByteBuffer.position() + 8);
|
||||
|
||||
// Skip pointer to Qortal recipient
|
||||
dataByteBuffer.position(dataByteBuffer.position() + 8);
|
||||
|
||||
// Skip pointer to message sender
|
||||
dataByteBuffer.position(dataByteBuffer.position() + 8);
|
||||
|
||||
// Skip pointer to message data
|
||||
dataByteBuffer.position(dataByteBuffer.position() + 8);
|
||||
|
||||
// Skip message data length
|
||||
dataByteBuffer.position(dataByteBuffer.position() + 8);
|
||||
|
||||
// Qortal recipient (if any)
|
||||
dataByteBuffer.get(addressBytes);
|
||||
|
||||
// Trade offer timeout (AT 'timestamp' converted to Qortal block height)
|
||||
long tradeRefundTimestamp = dataByteBuffer.getLong();
|
||||
|
||||
if (tradeRefundTimestamp != 0) {
|
||||
tradeData.mode = CrossChainTradeData.Mode.TRADE;
|
||||
tradeData.tradeRefundHeight = new Timestamp(tradeRefundTimestamp).blockHeight;
|
||||
|
||||
if (addressBytes[0] != 0)
|
||||
tradeData.qortalRecipient = Base58.encode(Arrays.copyOf(addressBytes, Account.ADDRESS_LENGTH));
|
||||
|
||||
// We'll suggest half of trade timeout
|
||||
CiyamAtSettings ciyamAtSettings = BlockChain.getInstance().getCiyamAtSettings();
|
||||
|
||||
int tradeModeSwitchHeight = (int) (tradeData.tradeRefundHeight - tradeData.tradeRefundTimeout / ciyamAtSettings.minutesPerBlock);
|
||||
|
||||
BlockData blockData = repository.getBlockRepository().fromHeight(tradeModeSwitchHeight);
|
||||
if (blockData != null) {
|
||||
tradeData.tradeModeTimestamp = blockData.getTimestamp(); // NOTE: milliseconds from epoch
|
||||
tradeData.lockTime = (int) (tradeData.tradeModeTimestamp / 1000L + tradeData.tradeRefundTimeout / 2 * 60);
|
||||
}
|
||||
} else {
|
||||
tradeData.mode = CrossChainTradeData.Mode.OFFER;
|
||||
}
|
||||
|
||||
return tradeData;
|
||||
}
|
||||
|
||||
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 != BTCACCT.SECRET_LENGTH)
|
||||
continue;
|
||||
|
||||
return secret;
|
||||
}
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
}
|
||||
387
src/main/java/org/qortal/crosschain/ElectrumX.java
Normal file
387
src/main/java/org/qortal/crosschain/ElectrumX.java
Normal file
@@ -0,0 +1,387 @@
|
||||
package org.qortal.crosschain;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.net.InetSocketAddress;
|
||||
import java.net.Socket;
|
||||
import java.net.SocketAddress;
|
||||
import java.util.ArrayList;
|
||||
import java.util.Arrays;
|
||||
import java.util.HashMap;
|
||||
import java.util.HashSet;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.NoSuchElementException;
|
||||
import java.util.Random;
|
||||
import java.util.Scanner;
|
||||
import java.util.Set;
|
||||
|
||||
import javax.net.ssl.SSLSocket;
|
||||
import javax.net.ssl.SSLSocketFactory;
|
||||
|
||||
import org.apache.logging.log4j.LogManager;
|
||||
import org.apache.logging.log4j.Logger;
|
||||
import org.json.simple.JSONArray;
|
||||
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;
|
||||
|
||||
public class ElectrumX {
|
||||
|
||||
private static final Logger LOGGER = LogManager.getLogger(ElectrumX.class);
|
||||
private static final Random RANDOM = new Random();
|
||||
|
||||
private static final int DEFAULT_TCP_PORT = 50001;
|
||||
private static final int DEFAULT_SSL_PORT = 50002;
|
||||
|
||||
private static final int BLOCK_HEADER_LENGTH = 80;
|
||||
|
||||
private static final Map<String, ElectrumX> instances = new HashMap<>();
|
||||
|
||||
static class Server {
|
||||
String hostname;
|
||||
|
||||
enum ConnectionType { TCP, SSL };
|
||||
ConnectionType connectionType;
|
||||
|
||||
int port;
|
||||
|
||||
public Server(String hostname, ConnectionType connectionType, int port) {
|
||||
this.hostname = hostname;
|
||||
this.connectionType = connectionType;
|
||||
this.port = port;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean equals(Object other) {
|
||||
if (other == this)
|
||||
return true;
|
||||
|
||||
if (!(other instanceof Server))
|
||||
return false;
|
||||
|
||||
Server otherServer = (Server) other;
|
||||
|
||||
return this.connectionType == otherServer.connectionType
|
||||
&& this.port == otherServer.port
|
||||
&& this.hostname.equals(otherServer.hostname);
|
||||
}
|
||||
|
||||
@Override
|
||||
public int hashCode() {
|
||||
return this.hostname.hashCode() ^ this.port;
|
||||
}
|
||||
|
||||
@Override
|
||||
public String toString() {
|
||||
return String.format("%s:%s:%d", this.connectionType.name(), this.hostname, this.port);
|
||||
}
|
||||
}
|
||||
private Set<Server> servers = new HashSet<>();
|
||||
|
||||
private Server currentServer;
|
||||
private Socket socket;
|
||||
private Scanner scanner;
|
||||
private int nextId = 1;
|
||||
|
||||
// Constructors
|
||||
|
||||
private ElectrumX(String bitcoinNetwork) {
|
||||
switch (bitcoinNetwork) {
|
||||
case "MAIN":
|
||||
servers.addAll(Arrays.asList());
|
||||
break;
|
||||
|
||||
case "TEST3":
|
||||
servers.addAll(Arrays.asList(
|
||||
new Server("tn.not.fyi", Server.ConnectionType.TCP, 55001),
|
||||
new Server("tn.not.fyi", Server.ConnectionType.SSL, 55002),
|
||||
new Server("testnet.aranguren.org", Server.ConnectionType.TCP, 51001),
|
||||
new Server("testnet.aranguren.org", Server.ConnectionType.SSL, 51002),
|
||||
new Server("testnet.hsmiths.com", Server.ConnectionType.SSL, 53012)));
|
||||
break;
|
||||
|
||||
case "REGTEST":
|
||||
servers.addAll(Arrays.asList(
|
||||
new Server("localhost", Server.ConnectionType.TCP, DEFAULT_TCP_PORT),
|
||||
new Server("localhost", Server.ConnectionType.SSL, DEFAULT_SSL_PORT)));
|
||||
break;
|
||||
|
||||
default:
|
||||
throw new IllegalArgumentException(String.format("Bitcoin network '%s' unknown", bitcoinNetwork));
|
||||
}
|
||||
|
||||
LOGGER.debug(() -> String.format("Starting ElectrumX support for %s Bitcoin network", bitcoinNetwork));
|
||||
rpc("server.banner");
|
||||
}
|
||||
|
||||
public static synchronized ElectrumX getInstance(String bitcoinNetwork) {
|
||||
if (!instances.containsKey(bitcoinNetwork))
|
||||
instances.put(bitcoinNetwork, new ElectrumX(bitcoinNetwork));
|
||||
|
||||
return instances.get(bitcoinNetwork);
|
||||
}
|
||||
|
||||
// Methods for use by other classes
|
||||
|
||||
public Integer getCurrentHeight() {
|
||||
JSONObject blockJson = (JSONObject) this.rpc("blockchain.headers.subscribe");
|
||||
if (blockJson == null || !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"))
|
||||
return null;
|
||||
|
||||
Long returnedCount = (Long) blockJson.get("count");
|
||||
String hex = (String) blockJson.get("hex");
|
||||
|
||||
byte[] raw = HashCode.fromString(hex).asBytes();
|
||||
if (raw.length != returnedCount * BLOCK_HEADER_LENGTH)
|
||||
return null;
|
||||
|
||||
List<byte[]> rawBlockHeaders = new ArrayList<>(returnedCount.intValue());
|
||||
for (int i = 0; i < returnedCount; ++i)
|
||||
rawBlockHeaders.add(Arrays.copyOfRange(raw, i * BLOCK_HEADER_LENGTH, (i + 1) * BLOCK_HEADER_LENGTH));
|
||||
|
||||
return rawBlockHeaders;
|
||||
}
|
||||
|
||||
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"))
|
||||
return null;
|
||||
|
||||
return (Long) balanceJson.get("confirmed");
|
||||
}
|
||||
|
||||
public List<Pair<byte[], Integer>> 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)
|
||||
return null;
|
||||
|
||||
List<Pair<byte[], Integer>> unspentOutputs = new ArrayList<>();
|
||||
for (Object rawUnspent : unspentJson) {
|
||||
JSONObject unspent = (JSONObject) rawUnspent;
|
||||
|
||||
byte[] txHash = HashCode.fromString((String) unspent.get("tx_hash")).asBytes();
|
||||
int outputIndex = ((Long) unspent.get("tx_pos")).intValue();
|
||||
|
||||
unspentOutputs.add(new Pair<>(txHash, outputIndex));
|
||||
}
|
||||
|
||||
return unspentOutputs;
|
||||
}
|
||||
|
||||
public byte[] getRawTransaction(byte[] txHash) {
|
||||
String rawTransactionHex = (String) this.rpc("blockchain.transaction.get", HashCode.fromBytes(txHash).toString());
|
||||
if (rawTransactionHex == null)
|
||||
return null;
|
||||
|
||||
return HashCode.fromString(rawTransactionHex).asBytes();
|
||||
}
|
||||
|
||||
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)
|
||||
return null;
|
||||
|
||||
List<byte[]> rawTransactions = new ArrayList<>();
|
||||
|
||||
for (Object rawTransactionInfo : transactionsJson) {
|
||||
JSONObject transactionInfo = (JSONObject) rawTransactionInfo;
|
||||
|
||||
// We only want confirmed transactions
|
||||
if (!transactionInfo.containsKey("height"))
|
||||
continue;
|
||||
|
||||
String txHash = (String) transactionInfo.get("tx_hash");
|
||||
String rawTransactionHex = (String) this.rpc("blockchain.transaction.get", txHash);
|
||||
if (rawTransactionHex == null)
|
||||
return null;
|
||||
|
||||
rawTransactions.add(HashCode.fromString(rawTransactionHex).asBytes());
|
||||
}
|
||||
|
||||
return rawTransactions;
|
||||
}
|
||||
|
||||
public boolean broadcastTransaction(byte[] transactionBytes) {
|
||||
Object rawBroadcastResult = this.rpc("blockchain.transaction.broadcast", HashCode.fromBytes(transactionBytes).toString());
|
||||
if (rawBroadcastResult == null)
|
||||
return false;
|
||||
|
||||
// If result is a String, then it is simply transaction hash.
|
||||
// Otherwise result is JSON and probably contains error info instead.
|
||||
return rawBroadcastResult instanceof String;
|
||||
}
|
||||
|
||||
// Class-private utility methods
|
||||
|
||||
private Set<Server> serverPeersSubscribe() {
|
||||
Set<Server> newServers = new HashSet<>();
|
||||
|
||||
JSONArray peers = (JSONArray) this.connectedRpc("server.peers.subscribe");
|
||||
if (peers == null)
|
||||
return newServers;
|
||||
|
||||
for (Object rawPeer : peers) {
|
||||
JSONArray peer = (JSONArray) rawPeer;
|
||||
if (peer.size() < 3)
|
||||
continue;
|
||||
|
||||
String hostname = (String) peer.get(1);
|
||||
JSONArray features = (JSONArray) peer.get(2);
|
||||
|
||||
for (Object rawFeature : features) {
|
||||
String feature = (String) rawFeature;
|
||||
Server.ConnectionType connectionType = null;
|
||||
int port = -1;
|
||||
|
||||
switch (feature.charAt(0)) {
|
||||
case 's':
|
||||
connectionType = Server.ConnectionType.SSL;
|
||||
port = DEFAULT_SSL_PORT;
|
||||
break;
|
||||
|
||||
case 't':
|
||||
connectionType = Server.ConnectionType.TCP;
|
||||
port = DEFAULT_TCP_PORT;
|
||||
break;
|
||||
}
|
||||
|
||||
if (connectionType == null)
|
||||
continue;
|
||||
|
||||
// Possible non-default port?
|
||||
if (feature.length() > 1)
|
||||
try {
|
||||
port = Integer.parseInt(feature.substring(1));
|
||||
} catch (NumberFormatException e) {
|
||||
// no good
|
||||
continue; // for-loop above
|
||||
}
|
||||
|
||||
Server newServer = new Server(hostname, connectionType, port);
|
||||
newServers.add(newServer);
|
||||
}
|
||||
}
|
||||
|
||||
return newServers;
|
||||
}
|
||||
|
||||
private synchronized Object rpc(String method, Object...params) {
|
||||
while (haveConnection()) {
|
||||
Object response = connectedRpc(method, params);
|
||||
if (response != null)
|
||||
return response;
|
||||
|
||||
this.currentServer = null;
|
||||
try {
|
||||
this.socket.close();
|
||||
} catch (IOException e) {
|
||||
/* ignore */
|
||||
}
|
||||
this.scanner = null;
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
private boolean haveConnection() {
|
||||
if (this.currentServer != null)
|
||||
return true;
|
||||
|
||||
List<Server> remainingServers = new ArrayList<>(this.servers);
|
||||
|
||||
while (!remainingServers.isEmpty()) {
|
||||
Server server = remainingServers.remove(RANDOM.nextInt(remainingServers.size()));
|
||||
LOGGER.trace(() -> String.format("Connecting to %s", server));
|
||||
|
||||
try {
|
||||
SocketAddress endpoint = new InetSocketAddress(server.hostname, server.port);
|
||||
int timeout = 5000; // ms
|
||||
|
||||
this.socket = new Socket();
|
||||
this.socket.connect(endpoint, timeout);
|
||||
this.socket.setTcpNoDelay(true);
|
||||
|
||||
if (server.connectionType == Server.ConnectionType.SSL) {
|
||||
SSLSocketFactory factory = TrustlessSSLSocketFactory.getSocketFactory();
|
||||
this.socket = (SSLSocket) factory.createSocket(this.socket, server.hostname, server.port, true);
|
||||
}
|
||||
|
||||
this.scanner = new Scanner(this.socket.getInputStream());
|
||||
this.scanner.useDelimiter("\n");
|
||||
|
||||
// Check connection works by asking for more servers
|
||||
Set<Server> moreServers = serverPeersSubscribe();
|
||||
moreServers.removeAll(this.servers);
|
||||
remainingServers.addAll(moreServers);
|
||||
this.servers.addAll(moreServers);
|
||||
|
||||
LOGGER.debug(() -> String.format("Connected to %s", server));
|
||||
this.currentServer = server;
|
||||
return true;
|
||||
} catch (IOException e) {
|
||||
// Try another server...
|
||||
this.socket = null;
|
||||
this.scanner = null;
|
||||
}
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
@SuppressWarnings("unchecked")
|
||||
private Object connectedRpc(String method, Object...params) {
|
||||
JSONObject requestJson = new JSONObject();
|
||||
requestJson.put("id", this.nextId++);
|
||||
requestJson.put("method", method);
|
||||
|
||||
JSONArray requestParams = new JSONArray();
|
||||
requestParams.addAll(Arrays.asList(params));
|
||||
requestJson.put("params", requestParams);
|
||||
|
||||
String request = requestJson.toJSONString() + "\n";
|
||||
LOGGER.trace(() -> String.format("Request: %s", request));
|
||||
|
||||
final String response;
|
||||
|
||||
try {
|
||||
this.socket.getOutputStream().write(request.getBytes());
|
||||
response = scanner.next();
|
||||
} catch (IOException | NoSuchElementException e) {
|
||||
return null;
|
||||
}
|
||||
|
||||
LOGGER.trace(() -> String.format("Response: %s", response));
|
||||
|
||||
if (response.isEmpty())
|
||||
return null;
|
||||
|
||||
JSONObject responseJson = (JSONObject) JSONValue.parse(response);
|
||||
if (responseJson == null)
|
||||
return null;
|
||||
|
||||
return responseJson.get("result");
|
||||
}
|
||||
|
||||
}
|
||||
@@ -4,16 +4,23 @@ import java.security.MessageDigest;
|
||||
import java.security.NoSuchAlgorithmException;
|
||||
import java.util.Arrays;
|
||||
|
||||
import org.bouncycastle.crypto.params.Ed25519PrivateKeyParameters;
|
||||
import org.bouncycastle.crypto.params.X25519PrivateKeyParameters;
|
||||
import org.bouncycastle.crypto.params.X25519PublicKeyParameters;
|
||||
import org.bouncycastle.math.ec.rfc8032.Ed25519;
|
||||
import org.qortal.account.Account;
|
||||
import org.qortal.block.BlockChain;
|
||||
import org.qortal.utils.Base58;
|
||||
|
||||
import com.google.common.primitives.Bytes;
|
||||
|
||||
public class Crypto {
|
||||
public abstract class Crypto {
|
||||
|
||||
public static final byte ADDRESS_VERSION = 58;
|
||||
public static final byte AT_ADDRESS_VERSION = 23;
|
||||
public static final int SIGNATURE_LENGTH = 64;
|
||||
public static final int SHARED_SECRET_LENGTH = 32;
|
||||
|
||||
public static final byte ADDRESS_VERSION = 58; // Q
|
||||
public static final byte AT_ADDRESS_VERSION = 23; // A
|
||||
public static final byte NODE_ADDRESS_VERSION = 53; // N
|
||||
|
||||
/**
|
||||
* Returns 32-byte SHA-256 digest of message passed in input.
|
||||
@@ -59,24 +66,29 @@ public class Crypto {
|
||||
return Bytes.concat(digest, digest);
|
||||
}
|
||||
|
||||
@SuppressWarnings("deprecation")
|
||||
/** Returns RMD160(SHA256(data)) */
|
||||
public static byte[] hash160(byte[] data) {
|
||||
byte[] interim = digest(data);
|
||||
|
||||
try {
|
||||
MessageDigest md160 = MessageDigest.getInstance("RIPEMD160");
|
||||
return md160.digest(interim);
|
||||
} catch (NoSuchAlgorithmException e) {
|
||||
throw new RuntimeException("RIPEMD160 message digest not available");
|
||||
}
|
||||
}
|
||||
|
||||
private static String toAddress(byte addressVersion, byte[] input) {
|
||||
// SHA2-256 input to create new data and of known size
|
||||
byte[] inputHash = digest(input);
|
||||
|
||||
// Use RIPEMD160 to create shorter address
|
||||
if (BlockChain.getInstance().getUseBrokenMD160ForAddresses()) {
|
||||
// Legacy BROKEN MD160
|
||||
BrokenMD160 brokenMD160 = new BrokenMD160();
|
||||
inputHash = brokenMD160.digest(inputHash);
|
||||
} else {
|
||||
// Use legit MD160
|
||||
try {
|
||||
MessageDigest md160 = MessageDigest.getInstance("RIPEMD160");
|
||||
inputHash = md160.digest(inputHash);
|
||||
} catch (NoSuchAlgorithmException e) {
|
||||
throw new RuntimeException("RIPEMD160 message digest not available");
|
||||
}
|
||||
// Use legit MD160
|
||||
try {
|
||||
MessageDigest md160 = MessageDigest.getInstance("RIPEMD160");
|
||||
inputHash = md160.digest(inputHash);
|
||||
} catch (NoSuchAlgorithmException e) {
|
||||
throw new RuntimeException("RIPEMD160 message digest not available");
|
||||
}
|
||||
|
||||
// Create address data using above hash and addressVersion (prepended)
|
||||
@@ -104,18 +116,23 @@ public class Crypto {
|
||||
return toAddress(AT_ADDRESS_VERSION, signature);
|
||||
}
|
||||
|
||||
public static String toNodeAddress(byte[] publicKey) {
|
||||
return toAddress(NODE_ADDRESS_VERSION, publicKey);
|
||||
}
|
||||
|
||||
public static boolean isValidAddress(String address) {
|
||||
return isValidTypedAddress(address, ADDRESS_VERSION, AT_ADDRESS_VERSION);
|
||||
}
|
||||
|
||||
public static boolean isValidAddress(byte[] addressBytes) {
|
||||
return areValidTypedAddressBytes(addressBytes, ADDRESS_VERSION, AT_ADDRESS_VERSION);
|
||||
}
|
||||
|
||||
public static boolean isValidAtAddress(String address) {
|
||||
return isValidTypedAddress(address, AT_ADDRESS_VERSION);
|
||||
}
|
||||
|
||||
private static boolean isValidTypedAddress(String address, byte...addressVersions) {
|
||||
if (addressVersions == null || addressVersions.length == 0)
|
||||
return false;
|
||||
|
||||
byte[] addressBytes;
|
||||
|
||||
try {
|
||||
@@ -125,6 +142,13 @@ public class Crypto {
|
||||
return false;
|
||||
}
|
||||
|
||||
return areValidTypedAddressBytes(addressBytes, addressVersions);
|
||||
}
|
||||
|
||||
private static boolean areValidTypedAddressBytes(byte[] addressBytes, byte...addressVersions) {
|
||||
if (addressVersions == null || addressVersions.length == 0)
|
||||
return false;
|
||||
|
||||
// Check address length
|
||||
if (addressBytes == null || addressBytes.length != Account.ADDRESS_LENGTH)
|
||||
return false;
|
||||
@@ -142,4 +166,33 @@ public class Crypto {
|
||||
return false;
|
||||
}
|
||||
|
||||
public static boolean verify(byte[] publicKey, byte[] signature, byte[] message) {
|
||||
try {
|
||||
return Ed25519.verify(signature, 0, publicKey, 0, message, 0, message.length);
|
||||
} catch (Exception e) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
public static byte[] sign(Ed25519PrivateKeyParameters edPrivateKeyParams, byte[] message) {
|
||||
byte[] signature = new byte[SIGNATURE_LENGTH];
|
||||
|
||||
edPrivateKeyParams.sign(Ed25519.Algorithm.Ed25519, edPrivateKeyParams.generatePublicKey(), null, message, 0, message.length, signature, 0);
|
||||
|
||||
return signature;
|
||||
}
|
||||
|
||||
public static byte[] getSharedSecret(byte[] privateKey, byte[] publicKey) {
|
||||
byte[] x25519PrivateKey = BouncyCastle25519.toX25519PrivateKey(privateKey);
|
||||
X25519PrivateKeyParameters xPrivateKeyParams = new X25519PrivateKeyParameters(x25519PrivateKey, 0);
|
||||
|
||||
byte[] x25519PublicKey = BouncyCastle25519.toX25519PublicKey(publicKey);
|
||||
X25519PublicKeyParameters xPublicKeyParams = new X25519PublicKeyParameters(x25519PublicKey, 0);
|
||||
|
||||
byte[] sharedSecret = new byte[SHARED_SECRET_LENGTH];
|
||||
xPrivateKeyParams.generateSecret(xPublicKeyParams, sharedSecret, 0);
|
||||
|
||||
return sharedSecret;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -1,83 +1,112 @@
|
||||
package org.qortal.crypto;
|
||||
|
||||
import com.google.common.primitives.Bytes;
|
||||
import java.nio.ByteBuffer;
|
||||
|
||||
public class MemoryPoW {
|
||||
|
||||
public static final int WORK_BUFFER_LENGTH = 4 * 1024 * 1024;
|
||||
private static final int WORK_BUFFER_LENGTH_MASK = WORK_BUFFER_LENGTH - 1;
|
||||
|
||||
private static final int HASH_LENGTH = 32;
|
||||
private static final int HASH_LENGTH_MASK = HASH_LENGTH - 1;
|
||||
|
||||
public static Integer compute(byte[] data, int start, int range, int difficulty) {
|
||||
if (range < 1)
|
||||
throw new IllegalArgumentException("range must be at least 1");
|
||||
|
||||
if (difficulty < 1)
|
||||
throw new IllegalArgumentException("difficulty must be at least 1");
|
||||
|
||||
public static Integer compute2(byte[] data, int workBufferLength, long difficulty) {
|
||||
// Hash data with SHA256
|
||||
byte[] hash = Crypto.digest(data);
|
||||
|
||||
assert hash.length == HASH_LENGTH;
|
||||
long[] longHash = new long[4];
|
||||
ByteBuffer byteBuffer = ByteBuffer.wrap(hash);
|
||||
longHash[0] = byteBuffer.getLong();
|
||||
longHash[1] = byteBuffer.getLong();
|
||||
longHash[2] = byteBuffer.getLong();
|
||||
longHash[3] = byteBuffer.getLong();
|
||||
byteBuffer = null;
|
||||
|
||||
byte[] perturbedHash = new byte[HASH_LENGTH];
|
||||
byte[] workBuffer = new byte[WORK_BUFFER_LENGTH];
|
||||
byte[] bufferHash = new byte[HASH_LENGTH];
|
||||
int longBufferLength = workBufferLength / 8;
|
||||
long[] workBuffer = new long[longBufferLength];
|
||||
long[] state = new long[4];
|
||||
|
||||
long seed = 8682522807148012L;
|
||||
long seedMultiplier = 1181783497276652981L;
|
||||
|
||||
// For each nonce...
|
||||
for (int nonce = start; nonce < start + range; ++nonce) {
|
||||
// Perturb hash using nonce
|
||||
int temp = nonce;
|
||||
for (int hi = 0; hi < HASH_LENGTH; ++hi) {
|
||||
perturbedHash[hi] = (byte) (hash[hi] ^ (temp & 0xff));
|
||||
temp >>>= 1;
|
||||
int nonce = -1;
|
||||
long result = 0;
|
||||
do {
|
||||
++nonce;
|
||||
|
||||
seed *= seedMultiplier; // per nonce
|
||||
|
||||
state[0] = longHash[0] ^ seed;
|
||||
state[1] = longHash[1] ^ seed;
|
||||
state[2] = longHash[2] ^ seed;
|
||||
state[3] = longHash[3] ^ seed;
|
||||
|
||||
// Fill work buffer with random
|
||||
for (int i = 0; i < workBuffer.length; ++i)
|
||||
workBuffer[i] = xoshiro256p(state);
|
||||
|
||||
// Random bounce through whole buffer
|
||||
result = workBuffer[0];
|
||||
for (int i = 0; i < 1024; ++i) {
|
||||
int index = (int) (xoshiro256p(state) & Integer.MAX_VALUE) % workBuffer.length;
|
||||
result ^= workBuffer[index];
|
||||
}
|
||||
|
||||
// Fill large working memory buffer using hash, further perturbing as we go
|
||||
int wanderingBufferOffset = 0;
|
||||
byte ch = 0;
|
||||
// Return if final value > difficulty
|
||||
} while (Long.numberOfLeadingZeros(result) < difficulty);
|
||||
|
||||
int hashOffset = 0;
|
||||
return nonce;
|
||||
}
|
||||
|
||||
for (int workBufferOffset = 0; workBufferOffset < WORK_BUFFER_LENGTH; workBufferOffset += HASH_LENGTH) {
|
||||
System.arraycopy(perturbedHash, 0, workBuffer, workBufferOffset, HASH_LENGTH);
|
||||
public static boolean verify2(byte[] data, int workBufferLength, long difficulty, int nonce) {
|
||||
// Hash data with SHA256
|
||||
byte[] hash = Crypto.digest(data);
|
||||
|
||||
hashOffset = ++hashOffset & HASH_LENGTH_MASK;
|
||||
long[] longHash = new long[4];
|
||||
ByteBuffer byteBuffer = ByteBuffer.wrap(hash);
|
||||
longHash[0] = byteBuffer.getLong();
|
||||
longHash[1] = byteBuffer.getLong();
|
||||
longHash[2] = byteBuffer.getLong();
|
||||
longHash[3] = byteBuffer.getLong();
|
||||
byteBuffer = null;
|
||||
|
||||
ch += perturbedHash[hashOffset];
|
||||
int longBufferLength = workBufferLength / 8;
|
||||
long[] workBuffer = new long[longBufferLength];
|
||||
long[] state = new long[4];
|
||||
|
||||
for (byte hi = 0; hi < HASH_LENGTH; ++hi) {
|
||||
byte hashByte = perturbedHash[hi];
|
||||
wanderingBufferOffset = (wanderingBufferOffset << 3) ^ (hashByte & 0xff);
|
||||
long seed = 8682522807148012L;
|
||||
long seedMultiplier = 1181783497276652981L;
|
||||
|
||||
perturbedHash[hi] = (byte) (hashByte ^ (ch + hi));
|
||||
}
|
||||
for (int i = 0; i <= nonce; ++i)
|
||||
seed *= seedMultiplier;
|
||||
|
||||
workBuffer[wanderingBufferOffset & WORK_BUFFER_LENGTH_MASK] ^= 0xAA;
|
||||
state[0] = longHash[0] ^ seed;
|
||||
state[1] = longHash[1] ^ seed;
|
||||
state[2] = longHash[2] ^ seed;
|
||||
state[3] = longHash[3] ^ seed;
|
||||
|
||||
// final int finalWanderingBufferOffset = wanderingBufferOffset & WORK_BUFFER_LENGTH_MASK;
|
||||
// System.out.println(String.format("wanderingBufferOffset: 0x%08x / 0x%08x - %02d%%", finalWanderingBufferOffset, WORK_BUFFER_LENGTH, finalWanderingBufferOffset * 100 / WORK_BUFFER_LENGTH));
|
||||
}
|
||||
// Fill work buffer with random
|
||||
for (int i = 0; i < workBuffer.length; ++i)
|
||||
workBuffer[i] = xoshiro256p(state);
|
||||
|
||||
Bytes.reverse(workBuffer);
|
||||
|
||||
// bufferHash = Crypto.digest(workBuffer);
|
||||
System.arraycopy(workBuffer, 0, bufferHash, 0, HASH_LENGTH);
|
||||
|
||||
int hi = 0;
|
||||
for (hi = 0; hi < difficulty; ++hi)
|
||||
if (bufferHash[hi] != 0)
|
||||
break;
|
||||
|
||||
if (hi == difficulty)
|
||||
return nonce;
|
||||
|
||||
Thread.yield();
|
||||
// Random bounce through whole buffer
|
||||
long result = workBuffer[0];
|
||||
for (int i = 0; i < 1024; ++i) {
|
||||
int index = (int) (xoshiro256p(state) & Integer.MAX_VALUE) % workBuffer.length;
|
||||
result ^= workBuffer[index];
|
||||
}
|
||||
|
||||
return null;
|
||||
return Long.numberOfLeadingZeros(result) >= difficulty;
|
||||
}
|
||||
|
||||
private static final long xoshiro256p(long[] state) {
|
||||
final long result = state[0] + state[3];
|
||||
final long temp = state[1] << 17;
|
||||
|
||||
state[2] ^= state[0];
|
||||
state[3] ^= state[1];
|
||||
state[1] ^= state[2];
|
||||
state[0] ^= state[3];
|
||||
|
||||
state[2] ^= temp;
|
||||
state[3] = (state[3] << 45) | (state[3] >>> (64 - 45)); // rol64(s[3], 45);
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -0,0 +1,42 @@
|
||||
package org.qortal.crypto;
|
||||
|
||||
import java.security.cert.X509Certificate;
|
||||
|
||||
import javax.net.ssl.SSLContext;
|
||||
import javax.net.ssl.SSLSocketFactory;
|
||||
import javax.net.ssl.TrustManager;
|
||||
import javax.net.ssl.X509TrustManager;
|
||||
|
||||
public abstract class TrustlessSSLSocketFactory {
|
||||
|
||||
// Create a trust manager that does not validate certificate chains
|
||||
private static final TrustManager[] TRUSTLESS_MANAGER = new TrustManager[] {
|
||||
new X509TrustManager() {
|
||||
public java.security.cert.X509Certificate[] getAcceptedIssuers() {
|
||||
return new X509Certificate[0];
|
||||
}
|
||||
|
||||
public void checkClientTrusted(java.security.cert.X509Certificate[] certs, String authType) {
|
||||
}
|
||||
|
||||
public void checkServerTrusted(java.security.cert.X509Certificate[] certs, String authType) {
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
// Install the all-trusting trust manager
|
||||
private static final SSLContext sc;
|
||||
static {
|
||||
try {
|
||||
sc = SSLContext.getInstance("SSL");
|
||||
sc.init(null, TRUSTLESS_MANAGER, new java.security.SecureRandom());
|
||||
} catch (Exception e) {
|
||||
throw new RuntimeException(e);
|
||||
}
|
||||
}
|
||||
|
||||
public static SSLSocketFactory getSocketFactory() {
|
||||
return sc.getSocketFactory();
|
||||
}
|
||||
|
||||
}
|
||||
@@ -1,18 +1,21 @@
|
||||
package org.qortal.data;
|
||||
|
||||
import java.math.BigDecimal;
|
||||
|
||||
import javax.xml.bind.annotation.XmlAccessType;
|
||||
import javax.xml.bind.annotation.XmlAccessorType;
|
||||
import javax.xml.bind.annotation.adapters.XmlJavaTypeAdapter;
|
||||
|
||||
// All properties to be converted to JSON via JAXB
|
||||
@XmlAccessorType(XmlAccessType.FIELD)
|
||||
public class PaymentData {
|
||||
|
||||
// Properties
|
||||
|
||||
private String recipient;
|
||||
|
||||
private long assetId;
|
||||
private BigDecimal amount;
|
||||
|
||||
@XmlJavaTypeAdapter(value = org.qortal.api.AmountTypeAdapter.class)
|
||||
private long amount;
|
||||
|
||||
// Constructors
|
||||
|
||||
@@ -20,7 +23,7 @@ public class PaymentData {
|
||||
protected PaymentData() {
|
||||
}
|
||||
|
||||
public PaymentData(String recipient, long assetId, BigDecimal amount) {
|
||||
public PaymentData(String recipient, long assetId, long amount) {
|
||||
this.recipient = recipient;
|
||||
this.assetId = assetId;
|
||||
this.amount = amount;
|
||||
@@ -36,7 +39,7 @@ public class PaymentData {
|
||||
return this.assetId;
|
||||
}
|
||||
|
||||
public BigDecimal getAmount() {
|
||||
public long getAmount() {
|
||||
return this.amount;
|
||||
}
|
||||
|
||||
|
||||
@@ -1,9 +1,11 @@
|
||||
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.adapters.XmlJavaTypeAdapter;
|
||||
|
||||
import org.qortal.utils.Amounts;
|
||||
|
||||
// All properties to be converted to JSON via JAXB
|
||||
@XmlAccessorType(XmlAccessType.FIELD)
|
||||
@@ -12,7 +14,9 @@ public class AccountBalanceData {
|
||||
// Properties
|
||||
private String address;
|
||||
private long assetId;
|
||||
private BigDecimal balance;
|
||||
|
||||
@XmlJavaTypeAdapter(value = org.qortal.api.AmountTypeAdapter.class)
|
||||
private long balance;
|
||||
|
||||
// Not always present:
|
||||
private Integer height;
|
||||
@@ -24,19 +28,19 @@ public class AccountBalanceData {
|
||||
protected AccountBalanceData() {
|
||||
}
|
||||
|
||||
public AccountBalanceData(String address, long assetId, BigDecimal balance) {
|
||||
public AccountBalanceData(String address, long assetId, long balance) {
|
||||
this.address = address;
|
||||
this.assetId = assetId;
|
||||
this.balance = balance;
|
||||
}
|
||||
|
||||
public AccountBalanceData(String address, long assetId, BigDecimal balance, int height) {
|
||||
public AccountBalanceData(String address, long assetId, long balance, int height) {
|
||||
this(address, assetId, balance);
|
||||
|
||||
this.height = height;
|
||||
}
|
||||
|
||||
public AccountBalanceData(String address, long assetId, BigDecimal balance, String assetName) {
|
||||
public AccountBalanceData(String address, long assetId, long balance, String assetName) {
|
||||
this(address, assetId, balance);
|
||||
|
||||
this.assetName = assetName;
|
||||
@@ -52,11 +56,11 @@ public class AccountBalanceData {
|
||||
return this.assetId;
|
||||
}
|
||||
|
||||
public BigDecimal getBalance() {
|
||||
public long getBalance() {
|
||||
return this.balance;
|
||||
}
|
||||
|
||||
public void setBalance(BigDecimal balance) {
|
||||
public void setBalance(long balance) {
|
||||
this.balance = balance;
|
||||
}
|
||||
|
||||
@@ -68,4 +72,8 @@ public class AccountBalanceData {
|
||||
return this.assetName;
|
||||
}
|
||||
|
||||
public String toString() {
|
||||
return String.format("%s has %s %s [assetId %d]", this.address, Amounts.prettyAmount(this.balance), (assetName != null ? assetName : ""), assetId);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -1,18 +1,23 @@
|
||||
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.adapters.XmlJavaTypeAdapter;
|
||||
|
||||
// All properties to be converted to JSON via JAXB
|
||||
@XmlAccessorType(XmlAccessType.FIELD)
|
||||
public class QortFromQoraData {
|
||||
|
||||
// Properties
|
||||
|
||||
private String address;
|
||||
// Not always present:
|
||||
private BigDecimal finalQortFromQora;
|
||||
|
||||
// Not always present
|
||||
@XmlJavaTypeAdapter(value = org.qortal.api.AmountTypeAdapter.class)
|
||||
private Long finalQortFromQora;
|
||||
|
||||
// Not always present
|
||||
private Integer finalBlockHeight;
|
||||
|
||||
// Constructors
|
||||
@@ -21,7 +26,7 @@ public class QortFromQoraData {
|
||||
protected QortFromQoraData() {
|
||||
}
|
||||
|
||||
public QortFromQoraData(String address, BigDecimal finalQortFromQora, Integer finalBlockHeight) {
|
||||
public QortFromQoraData(String address, Long finalQortFromQora, Integer finalBlockHeight) {
|
||||
this.address = address;
|
||||
this.finalQortFromQora = finalQortFromQora;
|
||||
this.finalBlockHeight = finalBlockHeight;
|
||||
@@ -33,11 +38,11 @@ public class QortFromQoraData {
|
||||
return this.address;
|
||||
}
|
||||
|
||||
public BigDecimal getFinalQortFromQora() {
|
||||
public Long getFinalQortFromQora() {
|
||||
return this.finalQortFromQora;
|
||||
}
|
||||
|
||||
public void setFinalQortFromQora(BigDecimal finalQortFromQora) {
|
||||
public void setFinalQortFromQora(Long finalQortFromQora) {
|
||||
this.finalQortFromQora = finalQortFromQora;
|
||||
}
|
||||
|
||||
|
||||
@@ -6,6 +6,9 @@ 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;
|
||||
|
||||
@@ -23,7 +26,9 @@ public class RewardShareData {
|
||||
|
||||
private String recipient;
|
||||
private byte[] rewardSharePublicKey;
|
||||
private BigDecimal sharePercent;
|
||||
|
||||
@XmlJavaTypeAdapter(value = org.qortal.api.RewardSharePercentTypeAdapter.class)
|
||||
private int sharePercent;
|
||||
|
||||
// Constructors
|
||||
|
||||
@@ -32,7 +37,7 @@ public class RewardShareData {
|
||||
}
|
||||
|
||||
// Used when fetching from repository
|
||||
public RewardShareData(byte[] minterPublicKey, String minter, String recipient, byte[] rewardSharePublicKey, BigDecimal sharePercent) {
|
||||
public RewardShareData(byte[] minterPublicKey, String minter, String recipient, byte[] rewardSharePublicKey, int sharePercent) {
|
||||
this.minterPublicKey = minterPublicKey;
|
||||
this.minter = minter;
|
||||
this.recipient = recipient;
|
||||
@@ -58,13 +63,25 @@ public class RewardShareData {
|
||||
return this.rewardSharePublicKey;
|
||||
}
|
||||
|
||||
public BigDecimal getSharePercent() {
|
||||
/** Returns share percent scaled by 100. i.e. 12.34% is represented by 1234 */
|
||||
public int getSharePercent() {
|
||||
return this.sharePercent;
|
||||
}
|
||||
|
||||
// Some JAXB/API-related getters
|
||||
|
||||
@XmlElement(name = "mintingAccount")
|
||||
public String getMintingAccount() {
|
||||
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));
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -20,19 +20,26 @@ public class AssetData {
|
||||
private String data;
|
||||
private boolean isUnspendable;
|
||||
private int creationGroupId;
|
||||
|
||||
// No need to expose this via API
|
||||
@XmlTransient
|
||||
@Schema(hidden = true)
|
||||
private byte[] reference;
|
||||
|
||||
// For internal use only
|
||||
@XmlTransient
|
||||
@Schema(hidden = true)
|
||||
private String reducedAssetName;
|
||||
|
||||
// Constructors
|
||||
|
||||
// necessary for JAX-RS serialization
|
||||
// necessary for JAXB serialization
|
||||
protected AssetData() {
|
||||
}
|
||||
|
||||
// NOTE: key is Long, not long, because it can be null if asset ID/key not yet assigned.
|
||||
public AssetData(Long assetId, String owner, String name, String description, long quantity, boolean isDivisible, String data, boolean isUnspendable, int creationGroupId, byte[] reference) {
|
||||
public AssetData(Long assetId, String owner, String name, String description, long quantity, boolean isDivisible,
|
||||
String data, boolean isUnspendable, int creationGroupId, byte[] reference, String reducedAssetName) {
|
||||
this.assetId = assetId;
|
||||
this.owner = owner;
|
||||
this.name = name;
|
||||
@@ -43,11 +50,13 @@ public class AssetData {
|
||||
this.isUnspendable = isUnspendable;
|
||||
this.creationGroupId = creationGroupId;
|
||||
this.reference = reference;
|
||||
this.reducedAssetName = reducedAssetName;
|
||||
}
|
||||
|
||||
// New asset with unassigned assetId
|
||||
public AssetData(String owner, String name, String description, long quantity, boolean isDivisible, String data, boolean isUnspendable, int creationGroupId, byte[] reference) {
|
||||
this(null, owner, name, description, quantity, isDivisible, data, isUnspendable, creationGroupId, reference);
|
||||
public AssetData(String owner, String name, String description, long quantity, boolean isDivisible, String data,
|
||||
boolean isUnspendable, int creationGroupId, byte[] reference, String reducedAssetName) {
|
||||
this(null, owner, name, description, quantity, isDivisible, data, isUnspendable, creationGroupId, reference, reducedAssetName);
|
||||
}
|
||||
|
||||
// Getters/Setters
|
||||
@@ -84,7 +93,7 @@ public class AssetData {
|
||||
return this.quantity;
|
||||
}
|
||||
|
||||
public boolean getIsDivisible() {
|
||||
public boolean isDivisible() {
|
||||
return this.isDivisible;
|
||||
}
|
||||
|
||||
@@ -96,7 +105,7 @@ public class AssetData {
|
||||
this.data = data;
|
||||
}
|
||||
|
||||
public boolean getIsUnspendable() {
|
||||
public boolean isUnspendable() {
|
||||
return this.isUnspendable;
|
||||
}
|
||||
|
||||
@@ -112,4 +121,12 @@ public class AssetData {
|
||||
this.reference = reference;
|
||||
}
|
||||
|
||||
public String getReducedAssetName() {
|
||||
return this.reducedAssetName;
|
||||
}
|
||||
|
||||
public void setReducedAssetName(String reducedAssetName) {
|
||||
this.reducedAssetName = reducedAssetName;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -1,10 +1,9 @@
|
||||
package org.qortal.data.asset;
|
||||
|
||||
import java.math.BigDecimal;
|
||||
|
||||
import javax.xml.bind.Marshaller;
|
||||
import javax.xml.bind.annotation.XmlAccessType;
|
||||
import javax.xml.bind.annotation.XmlAccessorType;
|
||||
import javax.xml.bind.annotation.adapters.XmlJavaTypeAdapter;
|
||||
|
||||
import org.qortal.crypto.Crypto;
|
||||
|
||||
@@ -26,13 +25,16 @@ public class OrderData implements Comparable<OrderData> {
|
||||
private long wantAssetId;
|
||||
|
||||
@Schema(description = "amount of highest-assetID asset to trade")
|
||||
private BigDecimal amount;
|
||||
@XmlJavaTypeAdapter(value = org.qortal.api.AmountTypeAdapter.class)
|
||||
private long amount;
|
||||
|
||||
@Schema(description = "price in lowest-assetID asset / highest-assetID asset")
|
||||
private BigDecimal price;
|
||||
@XmlJavaTypeAdapter(value = org.qortal.api.AmountTypeAdapter.class)
|
||||
private long price;
|
||||
|
||||
@Schema(description = "how much of \"amount\" has traded")
|
||||
private BigDecimal fulfilled;
|
||||
@XmlJavaTypeAdapter(value = org.qortal.api.AmountTypeAdapter.class)
|
||||
private long fulfilled;
|
||||
|
||||
private long timestamp;
|
||||
|
||||
@@ -73,26 +75,23 @@ public class OrderData implements Comparable<OrderData> {
|
||||
if (this.creator == null && this.creatorPublicKey != null)
|
||||
this.creator = Crypto.toAddress(this.creatorPublicKey);
|
||||
|
||||
this.amountAssetId = Math.max(this.haveAssetId, this.wantAssetId);
|
||||
|
||||
// If we don't have the extra asset name fields then we can't fill in the others
|
||||
if (this.haveAssetName == null)
|
||||
return;
|
||||
|
||||
// TODO: fill in for 'old' pricing scheme
|
||||
|
||||
// 'new' pricing scheme
|
||||
if (this.haveAssetId < this.wantAssetId) {
|
||||
this.amountAssetId = this.wantAssetId;
|
||||
this.amountAssetName = this.wantAssetName;
|
||||
this.pricePair = this.haveAssetName + "/" + this.wantAssetName;
|
||||
} else {
|
||||
this.amountAssetId = this.haveAssetId;
|
||||
this.amountAssetName = this.haveAssetName;
|
||||
this.pricePair = this.wantAssetName + "/" + this.haveAssetName;
|
||||
}
|
||||
}
|
||||
|
||||
/** Constructs OrderData using data from repository, including optional API fields. */
|
||||
public OrderData(byte[] orderId, byte[] creatorPublicKey, long haveAssetId, long wantAssetId, BigDecimal amount, BigDecimal fulfilled, BigDecimal price, long timestamp,
|
||||
public OrderData(byte[] orderId, byte[] creatorPublicKey, long haveAssetId, long wantAssetId, long amount, long fulfilled, long price, long timestamp,
|
||||
boolean isClosed, boolean isFulfilled, String haveAssetName, String wantAssetName) {
|
||||
this.orderId = orderId;
|
||||
this.creatorPublicKey = creatorPublicKey;
|
||||
@@ -110,13 +109,13 @@ public class OrderData implements Comparable<OrderData> {
|
||||
}
|
||||
|
||||
/** Constructs OrderData using data from repository, excluding optional API fields. */
|
||||
public OrderData(byte[] orderId, byte[] creatorPublicKey, long haveAssetId, long wantAssetId, BigDecimal amount, BigDecimal fulfilled, BigDecimal price, long timestamp, boolean isClosed, boolean isFulfilled) {
|
||||
public OrderData(byte[] orderId, byte[] creatorPublicKey, long haveAssetId, long wantAssetId, long amount, long fulfilled, long price, long timestamp, boolean isClosed, boolean isFulfilled) {
|
||||
this(orderId, creatorPublicKey, haveAssetId, wantAssetId, amount, fulfilled, price, timestamp, isClosed, isFulfilled, null, null);
|
||||
}
|
||||
|
||||
/** Constructs OrderData using data typically received from network. */
|
||||
public OrderData(byte[] orderId, byte[] creatorPublicKey, long haveAssetId, long wantAssetId, BigDecimal amount, BigDecimal price, long timestamp) {
|
||||
this(orderId, creatorPublicKey, haveAssetId, wantAssetId, amount, BigDecimal.ZERO.setScale(8), price, timestamp, false, false);
|
||||
public OrderData(byte[] orderId, byte[] creatorPublicKey, long haveAssetId, long wantAssetId, long amount, long price, long timestamp) {
|
||||
this(orderId, creatorPublicKey, haveAssetId, wantAssetId, amount, 0 /*fulfilled*/, price, timestamp, false, false);
|
||||
}
|
||||
|
||||
// Getters/setters
|
||||
@@ -137,19 +136,19 @@ public class OrderData implements Comparable<OrderData> {
|
||||
return this.wantAssetId;
|
||||
}
|
||||
|
||||
public BigDecimal getAmount() {
|
||||
public long getAmount() {
|
||||
return this.amount;
|
||||
}
|
||||
|
||||
public BigDecimal getFulfilled() {
|
||||
public long getFulfilled() {
|
||||
return this.fulfilled;
|
||||
}
|
||||
|
||||
public void setFulfilled(BigDecimal fulfilled) {
|
||||
public void setFulfilled(long fulfilled) {
|
||||
this.fulfilled = fulfilled;
|
||||
}
|
||||
|
||||
public BigDecimal getPrice() {
|
||||
public long getPrice() {
|
||||
return this.price;
|
||||
}
|
||||
|
||||
@@ -198,7 +197,7 @@ public class OrderData implements Comparable<OrderData> {
|
||||
@Override
|
||||
public int compareTo(OrderData orderData) {
|
||||
// Compare using prices
|
||||
return this.price.compareTo(orderData.getPrice());
|
||||
return Long.compare(this.price, orderData.getPrice());
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -20,9 +20,7 @@ public class RecentTradeData {
|
||||
|
||||
private BigDecimal amount;
|
||||
|
||||
@Schema(
|
||||
description = "when trade happened"
|
||||
)
|
||||
@Schema(description = "when trade happened")
|
||||
private long timestamp;
|
||||
|
||||
// Constructors
|
||||
@@ -31,11 +29,11 @@ public class RecentTradeData {
|
||||
protected RecentTradeData() {
|
||||
}
|
||||
|
||||
public RecentTradeData(long assetId, long otherAssetId, BigDecimal otherAmount, BigDecimal amount, long timestamp) {
|
||||
public RecentTradeData(long assetId, long otherAssetId, long otherAmount, long amount, long timestamp) {
|
||||
this.assetId = assetId;
|
||||
this.otherAssetId = otherAssetId;
|
||||
this.otherAmount = otherAmount;
|
||||
this.amount = amount;
|
||||
this.otherAmount = BigDecimal.valueOf(otherAmount, 8);
|
||||
this.amount = BigDecimal.valueOf(amount, 8);
|
||||
this.timestamp = timestamp;
|
||||
}
|
||||
|
||||
|
||||
@@ -1,12 +1,11 @@
|
||||
package org.qortal.data.asset;
|
||||
|
||||
import java.math.BigDecimal;
|
||||
|
||||
import javax.xml.bind.Marshaller;
|
||||
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 io.swagger.v3.oas.annotations.media.Schema;
|
||||
import io.swagger.v3.oas.annotations.media.Schema.AccessMode;
|
||||
@@ -24,17 +23,17 @@ public class TradeData {
|
||||
@XmlElement(name = "targetOrderId")
|
||||
private byte[] target;
|
||||
|
||||
@Schema(name = "targetAmount", description = "amount traded from target")
|
||||
@XmlElement(name = "targetAmount")
|
||||
private BigDecimal targetAmount;
|
||||
@Schema(description = "amount traded from target")
|
||||
@XmlJavaTypeAdapter(value = org.qortal.api.AmountTypeAdapter.class)
|
||||
private long targetAmount;
|
||||
|
||||
@Schema(name = "initiatorAmount", description = "amount traded from initiator")
|
||||
@XmlElement(name = "initiatorAmount")
|
||||
private BigDecimal initiatorAmount;
|
||||
@Schema(description = "amount traded from initiator")
|
||||
@XmlJavaTypeAdapter(value = org.qortal.api.AmountTypeAdapter.class)
|
||||
private long initiatorAmount;
|
||||
|
||||
@Schema(name = "initiatorSaving", description = "amount refunded to initiator due to price improvement")
|
||||
@XmlElement(name = "initiatorSaving")
|
||||
private BigDecimal initiatorSaving;
|
||||
@Schema(description = "amount refunded to initiator due to price improvement")
|
||||
@XmlJavaTypeAdapter(value = org.qortal.api.AmountTypeAdapter.class)
|
||||
private long initiatorSaving;
|
||||
|
||||
@Schema(description = "when trade happened")
|
||||
private long timestamp;
|
||||
@@ -95,7 +94,7 @@ public class TradeData {
|
||||
}
|
||||
}
|
||||
|
||||
public TradeData(byte[] initiator, byte[] target, BigDecimal targetAmount, BigDecimal initiatorAmount, BigDecimal initiatorSaving, long timestamp,
|
||||
public TradeData(byte[] initiator, byte[] target, long targetAmount, long initiatorAmount, long initiatorSaving, long timestamp,
|
||||
Long haveAssetId, String haveAssetName, Long wantAssetId, String wantAssetName) {
|
||||
this.initiator = initiator;
|
||||
this.target = target;
|
||||
@@ -110,7 +109,7 @@ public class TradeData {
|
||||
this.wantAssetName = wantAssetName;
|
||||
}
|
||||
|
||||
public TradeData(byte[] initiator, byte[] target, BigDecimal targetAmount, BigDecimal initiatorAmount, BigDecimal initiatorSaving, long timestamp) {
|
||||
public TradeData(byte[] initiator, byte[] target, long targetAmount, long initiatorAmount, long initiatorSaving, long timestamp) {
|
||||
this(initiator, target, targetAmount, initiatorAmount, initiatorSaving, timestamp, null, null, null, null);
|
||||
}
|
||||
|
||||
@@ -124,15 +123,15 @@ public class TradeData {
|
||||
return this.target;
|
||||
}
|
||||
|
||||
public BigDecimal getTargetAmount() {
|
||||
public long getTargetAmount() {
|
||||
return this.targetAmount;
|
||||
}
|
||||
|
||||
public BigDecimal getInitiatorAmount() {
|
||||
public long getInitiatorAmount() {
|
||||
return this.initiatorAmount;
|
||||
}
|
||||
|
||||
public BigDecimal getInitiatorSaving() {
|
||||
public long getInitiatorSaving() {
|
||||
return this.initiatorSaving;
|
||||
}
|
||||
|
||||
|
||||
@@ -1,7 +1,11 @@
|
||||
package org.qortal.data.at;
|
||||
|
||||
import java.math.BigDecimal;
|
||||
import javax.xml.bind.annotation.XmlAccessType;
|
||||
import javax.xml.bind.annotation.XmlAccessorType;
|
||||
import javax.xml.bind.annotation.adapters.XmlJavaTypeAdapter;
|
||||
|
||||
// All properties to be converted to JSON via JAXB
|
||||
@XmlAccessorType(XmlAccessType.FIELD)
|
||||
public class ATData {
|
||||
|
||||
// Properties
|
||||
@@ -11,23 +15,30 @@ public class ATData {
|
||||
private int version;
|
||||
private long assetId;
|
||||
private byte[] codeBytes;
|
||||
private byte[] codeHash;
|
||||
private boolean isSleeping;
|
||||
private Integer sleepUntilHeight;
|
||||
private boolean isFinished;
|
||||
private boolean hadFatalError;
|
||||
private boolean isFrozen;
|
||||
private BigDecimal frozenBalance;
|
||||
@XmlJavaTypeAdapter(value = org.qortal.api.AmountTypeAdapter.class)
|
||||
private Long frozenBalance;
|
||||
|
||||
// Constructors
|
||||
|
||||
public ATData(String ATAddress, byte[] creatorPublicKey, long creation, int version, long assetId, byte[] codeBytes, boolean isSleeping,
|
||||
Integer sleepUntilHeight, boolean isFinished, boolean hadFatalError, boolean isFrozen, BigDecimal frozenBalance) {
|
||||
// necessary for JAXB serialization
|
||||
protected ATData() {
|
||||
}
|
||||
|
||||
public ATData(String ATAddress, byte[] creatorPublicKey, long creation, int version, long assetId, byte[] codeBytes, byte[] codeHash,
|
||||
boolean isSleeping, Integer sleepUntilHeight, boolean isFinished, boolean hadFatalError, boolean isFrozen, Long frozenBalance) {
|
||||
this.ATAddress = ATAddress;
|
||||
this.creatorPublicKey = creatorPublicKey;
|
||||
this.creation = creation;
|
||||
this.version = version;
|
||||
this.assetId = assetId;
|
||||
this.codeBytes = codeBytes;
|
||||
this.codeHash = codeHash;
|
||||
this.isSleeping = isSleeping;
|
||||
this.sleepUntilHeight = sleepUntilHeight;
|
||||
this.isFinished = isFinished;
|
||||
@@ -36,14 +47,12 @@ public class ATData {
|
||||
this.frozenBalance = frozenBalance;
|
||||
}
|
||||
|
||||
public ATData(String ATAddress, byte[] creatorPublicKey, long creation, int version, long assetId, byte[] codeBytes, boolean isSleeping,
|
||||
Integer sleepUntilHeight, boolean isFinished, boolean hadFatalError, boolean isFrozen, Long frozenBalance) {
|
||||
this(ATAddress, creatorPublicKey, creation, version, assetId, codeBytes, isSleeping, sleepUntilHeight, isFinished, hadFatalError, isFrozen,
|
||||
(BigDecimal) null);
|
||||
|
||||
// Convert Long frozenBalance to BigDecimal
|
||||
if (frozenBalance != null)
|
||||
this.frozenBalance = BigDecimal.valueOf(frozenBalance, 8);
|
||||
/** For constructing skeleton ATData with bare minimum info. */
|
||||
public ATData(String ATAddress, byte[] creatorPublicKey, long creation, long assetId) {
|
||||
this.ATAddress = ATAddress;
|
||||
this.creatorPublicKey = creatorPublicKey;
|
||||
this.creation = creation;
|
||||
this.assetId = assetId;
|
||||
}
|
||||
|
||||
// Getters / setters
|
||||
@@ -72,6 +81,10 @@ public class ATData {
|
||||
return this.codeBytes;
|
||||
}
|
||||
|
||||
public byte[] getCodeHash() {
|
||||
return this.codeHash;
|
||||
}
|
||||
|
||||
public boolean getIsSleeping() {
|
||||
return this.isSleeping;
|
||||
}
|
||||
@@ -112,11 +125,11 @@ public class ATData {
|
||||
this.isFrozen = isFrozen;
|
||||
}
|
||||
|
||||
public BigDecimal getFrozenBalance() {
|
||||
public Long getFrozenBalance() {
|
||||
return this.frozenBalance;
|
||||
}
|
||||
|
||||
public void setFrozenBalance(BigDecimal frozenBalance) {
|
||||
public void setFrozenBalance(Long frozenBalance) {
|
||||
this.frozenBalance = frozenBalance;
|
||||
}
|
||||
|
||||
|
||||
@@ -1,7 +1,5 @@
|
||||
package org.qortal.data.at;
|
||||
|
||||
import java.math.BigDecimal;
|
||||
|
||||
public class ATStateData {
|
||||
|
||||
// Properties
|
||||
@@ -10,33 +8,39 @@ public class ATStateData {
|
||||
private Long creation;
|
||||
private byte[] stateData;
|
||||
private byte[] stateHash;
|
||||
private BigDecimal fees;
|
||||
private Long fees;
|
||||
private boolean isInitial;
|
||||
|
||||
// Constructors
|
||||
|
||||
/** Create new ATStateData */
|
||||
public ATStateData(String ATAddress, Integer height, Long creation, byte[] stateData, byte[] stateHash, BigDecimal fees) {
|
||||
public ATStateData(String ATAddress, Integer height, Long creation, byte[] stateData, byte[] stateHash, Long fees, boolean isInitial) {
|
||||
this.ATAddress = ATAddress;
|
||||
this.height = height;
|
||||
this.creation = creation;
|
||||
this.stateData = stateData;
|
||||
this.stateHash = stateHash;
|
||||
this.fees = fees;
|
||||
this.isInitial = isInitial;
|
||||
}
|
||||
|
||||
/** For recreating per-block ATStateData from repository where not all info is needed */
|
||||
public ATStateData(String ATAddress, int height, byte[] stateHash, BigDecimal fees) {
|
||||
this(ATAddress, height, null, null, stateHash, fees);
|
||||
public ATStateData(String ATAddress, int height, byte[] stateHash, Long fees, boolean isInitial) {
|
||||
this(ATAddress, height, null, null, stateHash, fees, isInitial);
|
||||
}
|
||||
|
||||
/** For creating ATStateData from serialized bytes when we don't have all the info */
|
||||
public ATStateData(String ATAddress, byte[] stateHash) {
|
||||
this(ATAddress, null, null, null, stateHash, null);
|
||||
// This won't ever be initial AT state from deployment as that's never serialized over the network,
|
||||
// but generated when the DeployAtTransaction is processed locally.
|
||||
this(ATAddress, null, null, null, stateHash, null, false);
|
||||
}
|
||||
|
||||
/** For creating ATStateData from serialized bytes when we don't have all the info */
|
||||
public ATStateData(String ATAddress, byte[] stateHash, BigDecimal fees) {
|
||||
this(ATAddress, null, null, null, stateHash, fees);
|
||||
public ATStateData(String ATAddress, byte[] stateHash, Long fees) {
|
||||
// This won't ever be initial AT state from deployment as that's never serialized over the network,
|
||||
// but generated when the DeployAtTransaction is processed locally.
|
||||
this(ATAddress, null, null, null, stateHash, fees, false);
|
||||
}
|
||||
|
||||
// Getters / setters
|
||||
@@ -66,8 +70,12 @@ public class ATStateData {
|
||||
return this.stateHash;
|
||||
}
|
||||
|
||||
public BigDecimal getFees() {
|
||||
public Long getFees() {
|
||||
return this.fees;
|
||||
}
|
||||
|
||||
public boolean isInitial() {
|
||||
return this.isInitial;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -3,11 +3,11 @@ package org.qortal.data.block;
|
||||
import com.google.common.primitives.Bytes;
|
||||
|
||||
import java.io.Serializable;
|
||||
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.adapters.XmlJavaTypeAdapter;
|
||||
|
||||
import org.qortal.crypto.Crypto;
|
||||
|
||||
@@ -23,14 +23,20 @@ public class BlockData implements Serializable {
|
||||
private int version;
|
||||
private byte[] reference;
|
||||
private int transactionCount;
|
||||
private BigDecimal totalFees;
|
||||
|
||||
@XmlJavaTypeAdapter(value = org.qortal.api.AmountTypeAdapter.class)
|
||||
private long totalFees;
|
||||
|
||||
private byte[] transactionsSignature;
|
||||
private Integer height;
|
||||
private long timestamp;
|
||||
private byte[] minterPublicKey;
|
||||
private byte[] minterSignature;
|
||||
private int atCount;
|
||||
private BigDecimal atFees;
|
||||
|
||||
@XmlJavaTypeAdapter(value = org.qortal.api.AmountTypeAdapter.class)
|
||||
private long atFees;
|
||||
|
||||
private byte[] encodedOnlineAccounts;
|
||||
private int onlineAccountsCount;
|
||||
private Long onlineAccountsTimestamp;
|
||||
@@ -42,8 +48,8 @@ public class BlockData implements Serializable {
|
||||
protected BlockData() {
|
||||
}
|
||||
|
||||
public BlockData(int version, byte[] reference, int transactionCount, BigDecimal totalFees, byte[] transactionsSignature, Integer height, long timestamp,
|
||||
byte[] minterPublicKey, byte[] minterSignature, int atCount, BigDecimal atFees,
|
||||
public BlockData(int version, byte[] reference, int transactionCount, long totalFees, byte[] transactionsSignature, Integer height, long timestamp,
|
||||
byte[] minterPublicKey, byte[] minterSignature, int atCount, long atFees,
|
||||
byte[] encodedOnlineAccounts, int onlineAccountsCount, Long onlineAccountsTimestamp, byte[] onlineAccountsSignatures) {
|
||||
this.version = version;
|
||||
this.reference = reference;
|
||||
@@ -67,8 +73,8 @@ public class BlockData implements Serializable {
|
||||
this.signature = null;
|
||||
}
|
||||
|
||||
public BlockData(int version, byte[] reference, int transactionCount, BigDecimal totalFees, byte[] transactionsSignature, Integer height, long timestamp,
|
||||
byte[] minterPublicKey, byte[] minterSignature, int atCount, BigDecimal atFees) {
|
||||
public BlockData(int version, byte[] reference, int transactionCount, long totalFees, byte[] transactionsSignature, Integer height, long timestamp,
|
||||
byte[] minterPublicKey, byte[] minterSignature, int atCount, long atFees) {
|
||||
this(version, reference, transactionCount, totalFees, transactionsSignature, height, timestamp, minterPublicKey, minterSignature, atCount, atFees,
|
||||
null, 0, null, null);
|
||||
}
|
||||
@@ -103,11 +109,11 @@ public class BlockData implements Serializable {
|
||||
this.transactionCount = transactionCount;
|
||||
}
|
||||
|
||||
public BigDecimal getTotalFees() {
|
||||
public long getTotalFees() {
|
||||
return this.totalFees;
|
||||
}
|
||||
|
||||
public void setTotalFees(BigDecimal totalFees) {
|
||||
public void setTotalFees(long totalFees) {
|
||||
this.totalFees = totalFees;
|
||||
}
|
||||
|
||||
@@ -151,11 +157,11 @@ public class BlockData implements Serializable {
|
||||
this.atCount = atCount;
|
||||
}
|
||||
|
||||
public BigDecimal getATFees() {
|
||||
public long getATFees() {
|
||||
return this.atFees;
|
||||
}
|
||||
|
||||
public void setATFees(BigDecimal atFees) {
|
||||
public void setATFees(long atFees) {
|
||||
this.atFees = atFees;
|
||||
}
|
||||
|
||||
|
||||
121
src/main/java/org/qortal/data/chat/ActiveChats.java
Normal file
121
src/main/java/org/qortal/data/chat/ActiveChats.java
Normal file
@@ -0,0 +1,121 @@
|
||||
package org.qortal.data.chat;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
import javax.xml.bind.annotation.XmlAccessType;
|
||||
import javax.xml.bind.annotation.XmlAccessorType;
|
||||
|
||||
@XmlAccessorType(XmlAccessType.FIELD)
|
||||
public class ActiveChats {
|
||||
|
||||
@XmlAccessorType(XmlAccessType.FIELD)
|
||||
public static class GroupChat {
|
||||
private int groupId;
|
||||
private String groupName;
|
||||
|
||||
// The following fields might not be present for groupId 0
|
||||
private Long timestamp;
|
||||
private String sender;
|
||||
private String senderName;
|
||||
|
||||
protected GroupChat() {
|
||||
/* JAXB */
|
||||
}
|
||||
|
||||
public GroupChat(int groupId, String groupName, Long timestamp, String sender, String senderName) {
|
||||
this.groupId = groupId;
|
||||
this.groupName = groupName;
|
||||
this.timestamp = timestamp;
|
||||
this.sender = sender;
|
||||
this.senderName = senderName;
|
||||
}
|
||||
|
||||
public int getGroupId() {
|
||||
return this.groupId;
|
||||
}
|
||||
|
||||
public String getGroupName() {
|
||||
return this.groupName;
|
||||
}
|
||||
|
||||
public Long getTimestamp() {
|
||||
return this.timestamp;
|
||||
}
|
||||
|
||||
public String getSender() {
|
||||
return this.sender;
|
||||
}
|
||||
|
||||
public String getSenderName() {
|
||||
return this.senderName;
|
||||
}
|
||||
}
|
||||
|
||||
@XmlAccessorType(XmlAccessType.FIELD)
|
||||
public static class DirectChat {
|
||||
private String address;
|
||||
private String name;
|
||||
private long timestamp;
|
||||
private String sender;
|
||||
private String senderName;
|
||||
|
||||
protected DirectChat() {
|
||||
/* JAXB */
|
||||
}
|
||||
|
||||
public DirectChat(String address, String name, long timestamp, String sender, String senderName) {
|
||||
this.address = address;
|
||||
this.name = name;
|
||||
this.timestamp = timestamp;
|
||||
this.sender = sender;
|
||||
this.senderName = senderName;
|
||||
}
|
||||
|
||||
public String getAddress() {
|
||||
return this.address;
|
||||
}
|
||||
|
||||
public String getName() {
|
||||
return this.name;
|
||||
}
|
||||
|
||||
public long getTimestamp() {
|
||||
return this.timestamp;
|
||||
}
|
||||
|
||||
public String getSender() {
|
||||
return this.sender;
|
||||
}
|
||||
|
||||
public String getSenderName() {
|
||||
return this.senderName;
|
||||
}
|
||||
}
|
||||
|
||||
// Properties
|
||||
|
||||
private List<GroupChat> groups;
|
||||
|
||||
private List<DirectChat> direct;
|
||||
|
||||
// Constructors
|
||||
|
||||
protected ActiveChats() {
|
||||
/* For JAXB */
|
||||
}
|
||||
|
||||
// For repository use
|
||||
public ActiveChats(List<GroupChat> groups, List<DirectChat> direct) {
|
||||
this.groups = groups;
|
||||
this.direct = direct;
|
||||
}
|
||||
|
||||
public List<GroupChat> getGroups() {
|
||||
return this.groups;
|
||||
}
|
||||
|
||||
public List<DirectChat> getDirect() {
|
||||
return this.direct;
|
||||
}
|
||||
|
||||
}
|
||||
109
src/main/java/org/qortal/data/chat/ChatMessage.java
Normal file
109
src/main/java/org/qortal/data/chat/ChatMessage.java
Normal file
@@ -0,0 +1,109 @@
|
||||
package org.qortal.data.chat;
|
||||
|
||||
import javax.xml.bind.annotation.XmlAccessType;
|
||||
import javax.xml.bind.annotation.XmlAccessorType;
|
||||
|
||||
@XmlAccessorType(XmlAccessType.FIELD)
|
||||
public class ChatMessage {
|
||||
|
||||
// Properties
|
||||
|
||||
private long timestamp;
|
||||
|
||||
private int txGroupId;
|
||||
|
||||
private byte[] reference;
|
||||
|
||||
private byte[] senderPublicKey;
|
||||
|
||||
/* Address of sender */
|
||||
private String sender;
|
||||
|
||||
// Not always present
|
||||
private String senderName;
|
||||
|
||||
/* Address of recipient (if any) */
|
||||
private String recipient; // can be null
|
||||
|
||||
private String recipientName;
|
||||
|
||||
private byte[] data;
|
||||
|
||||
private boolean isText;
|
||||
private boolean isEncrypted;
|
||||
|
||||
private byte[] signature;
|
||||
|
||||
// Constructors
|
||||
|
||||
protected ChatMessage() {
|
||||
/* For JAXB */
|
||||
}
|
||||
|
||||
// For repository use
|
||||
public ChatMessage(long timestamp, int txGroupId, byte[] reference, byte[] senderPublicKey, String sender,
|
||||
String senderName, String recipient, String recipientName, byte[] data, boolean isText,
|
||||
boolean isEncrypted, byte[] signature) {
|
||||
this.timestamp = timestamp;
|
||||
this.txGroupId = txGroupId;
|
||||
this.reference = reference;
|
||||
this.senderPublicKey = senderPublicKey;
|
||||
this.sender = sender;
|
||||
this.senderName = senderName;
|
||||
this.recipient = recipient;
|
||||
this.recipientName = recipientName;
|
||||
this.data = data;
|
||||
this.isText = isText;
|
||||
this.isEncrypted = isEncrypted;
|
||||
this.signature = signature;
|
||||
}
|
||||
|
||||
public long getTimestamp() {
|
||||
return this.timestamp;
|
||||
}
|
||||
|
||||
public int getTxGroupId() {
|
||||
return this.txGroupId;
|
||||
}
|
||||
|
||||
public byte[] getReference() {
|
||||
return this.reference;
|
||||
}
|
||||
|
||||
public byte[] getSenderPublicKey() {
|
||||
return this.senderPublicKey;
|
||||
}
|
||||
|
||||
public String getSender() {
|
||||
return this.sender;
|
||||
}
|
||||
|
||||
public String getSenderName() {
|
||||
return this.senderName;
|
||||
}
|
||||
|
||||
public String getRecipient() {
|
||||
return this.recipient;
|
||||
}
|
||||
|
||||
public String getRecipientName() {
|
||||
return this.recipientName;
|
||||
}
|
||||
|
||||
public byte[] getData() {
|
||||
return this.data;
|
||||
}
|
||||
|
||||
public boolean isText() {
|
||||
return this.isText;
|
||||
}
|
||||
|
||||
public boolean isEncrypted() {
|
||||
return this.isEncrypted;
|
||||
}
|
||||
|
||||
public byte[] getSignature() {
|
||||
return this.signature;
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,68 @@
|
||||
package org.qortal.data.crosschain;
|
||||
|
||||
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;
|
||||
|
||||
// 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")
|
||||
public String qortalAtAddress;
|
||||
|
||||
@Schema(description = "AT creator's Qortal address")
|
||||
public String qortalCreator;
|
||||
|
||||
@Schema(description = "Timestamp when AT was created (milliseconds since epoch)")
|
||||
public long creationTimestamp;
|
||||
|
||||
@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 = "Initial QORT payment that will be sent to Qortal trade partner")
|
||||
@XmlJavaTypeAdapter(value = org.qortal.api.AmountTypeAdapter.class)
|
||||
public long initialPayout;
|
||||
|
||||
@Schema(description = "Final QORT payment that will be sent to Qortal trade partner")
|
||||
@XmlJavaTypeAdapter(value = org.qortal.api.AmountTypeAdapter.class)
|
||||
public long redeemPayout;
|
||||
|
||||
@Schema(description = "Trade partner's Qortal address (trade begins when this is set)")
|
||||
public String qortalRecipient;
|
||||
|
||||
@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 = "Actual Qortal block height when AT will automatically refund to AT creator (after trade begins)")
|
||||
public Integer tradeRefundHeight;
|
||||
|
||||
@Schema(description = "Amount, in BTC, that AT creator expects Bitcoin P2SH to pay out (excluding miner fees)")
|
||||
@XmlJavaTypeAdapter(value = org.qortal.api.AmountTypeAdapter.class)
|
||||
public long expectedBitcoin;
|
||||
|
||||
public Mode mode;
|
||||
|
||||
@Schema(description = "Suggested Bitcoin P2SH nLockTime based on trade timeout")
|
||||
public Integer lockTime;
|
||||
|
||||
// Constructors
|
||||
|
||||
// Necessary for JAXB
|
||||
public CrossChainTradeData() {
|
||||
}
|
||||
|
||||
}
|
||||
@@ -23,18 +23,26 @@ public class GroupData {
|
||||
private ApprovalThreshold approvalThreshold;
|
||||
private int minimumBlockDelay;
|
||||
private int maximumBlockDelay;
|
||||
|
||||
/** Reference to CREATE_GROUP or UPDATE_GROUP transaction, used to rebuild group during orphaning. */
|
||||
// No need to ever expose this via API
|
||||
@XmlTransient
|
||||
@Schema(hidden = true)
|
||||
private byte[] reference;
|
||||
|
||||
// For internal use
|
||||
@XmlTransient
|
||||
@Schema(
|
||||
hidden = true
|
||||
)
|
||||
@Schema(hidden = true)
|
||||
private int creationGroupId;
|
||||
|
||||
// For internal use
|
||||
@XmlTransient
|
||||
@Schema(hidden = true)
|
||||
private String reducedGroupName;
|
||||
|
||||
// We abuse GroupData for API purposes by adding this unrelated field. Not always present.
|
||||
private Boolean isAdmin;
|
||||
|
||||
// Constructors
|
||||
|
||||
// necessary for JAX-RS serialization
|
||||
@@ -42,10 +50,12 @@ public class GroupData {
|
||||
}
|
||||
|
||||
/** Constructs new GroupData with nullable groupId and nullable updated [timestamp] */
|
||||
public GroupData(Integer groupId, String owner, String name, String description, long created, Long updated, boolean isOpen, ApprovalThreshold approvalThreshold, int minBlockDelay, int maxBlockDelay, byte[] reference, int creationGroupId) {
|
||||
public GroupData(Integer groupId, String owner, String groupName, String description, long created, Long updated,
|
||||
boolean isOpen, ApprovalThreshold approvalThreshold, int minBlockDelay, int maxBlockDelay, byte[] reference,
|
||||
int creationGroupId, String reducedGroupName) {
|
||||
this.groupId = groupId;
|
||||
this.owner = owner;
|
||||
this.groupName = name;
|
||||
this.groupName = groupName;
|
||||
this.description = description;
|
||||
this.created = created;
|
||||
this.updated = updated;
|
||||
@@ -55,11 +65,15 @@ public class GroupData {
|
||||
this.minimumBlockDelay = minBlockDelay;
|
||||
this.maximumBlockDelay = maxBlockDelay;
|
||||
this.creationGroupId = creationGroupId;
|
||||
this.reducedGroupName = reducedGroupName;
|
||||
}
|
||||
|
||||
/** Constructs new GroupData with unassigned groupId */
|
||||
public GroupData(String owner, String name, String description, long created, boolean isOpen, ApprovalThreshold approvalThreshold, int minBlockDelay, int maxBlockDelay, byte[] reference, int creationGroupId) {
|
||||
this(null, owner, name, description, created, null, isOpen, approvalThreshold, minBlockDelay, maxBlockDelay, reference, creationGroupId);
|
||||
public GroupData(String owner, String groupName, String description, long created, boolean isOpen,
|
||||
ApprovalThreshold approvalThreshold, int minBlockDelay, int maxBlockDelay, byte[] reference,
|
||||
int creationGroupId, String reducedGroupName) {
|
||||
this(null, owner, groupName, description, created, null, isOpen, approvalThreshold, minBlockDelay,
|
||||
maxBlockDelay, reference, creationGroupId, reducedGroupName);
|
||||
}
|
||||
|
||||
// Getters / setters
|
||||
@@ -112,7 +126,7 @@ public class GroupData {
|
||||
this.reference = reference;
|
||||
}
|
||||
|
||||
public boolean getIsOpen() {
|
||||
public boolean isOpen() {
|
||||
return this.isOpen;
|
||||
}
|
||||
|
||||
@@ -140,4 +154,22 @@ public class GroupData {
|
||||
return this.creationGroupId;
|
||||
}
|
||||
|
||||
public String getReducedGroupName() {
|
||||
return this.reducedGroupName;
|
||||
}
|
||||
|
||||
public void setReducedGroupName(String reducedGroupName) {
|
||||
this.reducedGroupName = reducedGroupName;
|
||||
}
|
||||
|
||||
// This is for API call GET /groups/member/{address}
|
||||
|
||||
public Boolean isAdmin() {
|
||||
return this.isAdmin;
|
||||
}
|
||||
|
||||
public void setIsAdmin(boolean isAdmin) {
|
||||
this.isAdmin = isAdmin;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -1,10 +1,9 @@
|
||||
package org.qortal.data.naming;
|
||||
|
||||
import java.math.BigDecimal;
|
||||
|
||||
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;
|
||||
|
||||
@@ -13,36 +12,47 @@ import io.swagger.v3.oas.annotations.media.Schema;
|
||||
public class NameData {
|
||||
|
||||
// Properties
|
||||
private String owner;
|
||||
|
||||
private String name;
|
||||
|
||||
private String reducedName;
|
||||
|
||||
private String owner;
|
||||
|
||||
private String data;
|
||||
|
||||
private long registered;
|
||||
private Long updated;
|
||||
// No need to expose this via API
|
||||
@XmlTransient
|
||||
@Schema(
|
||||
hidden = true
|
||||
)
|
||||
private byte[] reference;
|
||||
|
||||
private Long updated; // Not always present
|
||||
|
||||
private boolean isForSale;
|
||||
private BigDecimal salePrice;
|
||||
|
||||
@XmlJavaTypeAdapter(value = org.qortal.api.AmountTypeAdapter.class)
|
||||
private Long salePrice;
|
||||
|
||||
// For internal use - no need to expose this via API
|
||||
@XmlTransient
|
||||
@Schema(hidden = true)
|
||||
private byte[] reference;
|
||||
|
||||
// For internal use
|
||||
@XmlTransient
|
||||
@Schema(
|
||||
hidden = true
|
||||
)
|
||||
@Schema(hidden = true)
|
||||
private int creationGroupId;
|
||||
|
||||
// Constructors
|
||||
|
||||
// necessary for JAX-RS serialization
|
||||
// necessary for JAXB
|
||||
protected NameData() {
|
||||
}
|
||||
|
||||
public NameData(String owner, String name, String data, long registered, Long updated, byte[] reference, boolean isForSale, BigDecimal salePrice,
|
||||
int creationGroupId) {
|
||||
this.owner = owner;
|
||||
// Typically used when fetching from repository
|
||||
public NameData(String name, String reducedName, String owner, String data, long registered,
|
||||
Long updated, boolean isForSale, Long salePrice,
|
||||
byte[] reference, int creationGroupId) {
|
||||
this.name = name;
|
||||
this.reducedName = reducedName;
|
||||
this.owner = owner;
|
||||
this.data = data;
|
||||
this.registered = registered;
|
||||
this.updated = updated;
|
||||
@@ -52,12 +62,29 @@ public class NameData {
|
||||
this.creationGroupId = creationGroupId;
|
||||
}
|
||||
|
||||
public NameData(String owner, String name, String data, long registered, byte[] reference, int creationGroupId) {
|
||||
this(owner, name, data, registered, null, reference, false, null, creationGroupId);
|
||||
// Typically used when registering a new name
|
||||
public NameData(String name, String reducedName, String owner, String data, long registered, byte[] reference, int creationGroupId) {
|
||||
this(name, reducedName, owner, data, registered, null, false, null, reference, creationGroupId);
|
||||
}
|
||||
|
||||
// Getters / setters
|
||||
|
||||
public String getName() {
|
||||
return this.name;
|
||||
}
|
||||
|
||||
public void setName(String name) {
|
||||
this.name = name;
|
||||
}
|
||||
|
||||
public String getReducedName() {
|
||||
return this.reducedName;
|
||||
}
|
||||
|
||||
public void setReducedName(String reducedName) {
|
||||
this.reducedName = reducedName;
|
||||
}
|
||||
|
||||
public String getOwner() {
|
||||
return this.owner;
|
||||
}
|
||||
@@ -66,10 +93,6 @@ public class NameData {
|
||||
this.owner = owner;
|
||||
}
|
||||
|
||||
public String getName() {
|
||||
return this.name;
|
||||
}
|
||||
|
||||
public String getData() {
|
||||
return this.data;
|
||||
}
|
||||
@@ -90,6 +113,22 @@ public class NameData {
|
||||
this.updated = updated;
|
||||
}
|
||||
|
||||
public boolean isForSale() {
|
||||
return this.isForSale;
|
||||
}
|
||||
|
||||
public void setIsForSale(boolean isForSale) {
|
||||
this.isForSale = isForSale;
|
||||
}
|
||||
|
||||
public Long getSalePrice() {
|
||||
return this.salePrice;
|
||||
}
|
||||
|
||||
public void setSalePrice(Long salePrice) {
|
||||
this.salePrice = salePrice;
|
||||
}
|
||||
|
||||
public byte[] getReference() {
|
||||
return this.reference;
|
||||
}
|
||||
@@ -98,22 +137,6 @@ public class NameData {
|
||||
this.reference = reference;
|
||||
}
|
||||
|
||||
public boolean getIsForSale() {
|
||||
return this.isForSale;
|
||||
}
|
||||
|
||||
public void setIsForSale(boolean isForSale) {
|
||||
this.isForSale = isForSale;
|
||||
}
|
||||
|
||||
public BigDecimal getSalePrice() {
|
||||
return this.salePrice;
|
||||
}
|
||||
|
||||
public void setSalePrice(BigDecimal salePrice) {
|
||||
this.salePrice = salePrice;
|
||||
}
|
||||
|
||||
public int getCreationGroupId() {
|
||||
return this.creationGroupId;
|
||||
}
|
||||
|
||||
@@ -1,5 +1,7 @@
|
||||
package org.qortal.data.network;
|
||||
|
||||
import java.util.Arrays;
|
||||
|
||||
import javax.xml.bind.annotation.XmlAccessType;
|
||||
import javax.xml.bind.annotation.XmlAccessorType;
|
||||
import javax.xml.bind.annotation.XmlElement;
|
||||
@@ -44,4 +46,36 @@ public class OnlineAccountData {
|
||||
return new PublicKeyAccount(null, this.publicKey).getAddress();
|
||||
}
|
||||
|
||||
// Comparison
|
||||
|
||||
@Override
|
||||
public boolean equals(Object other) {
|
||||
if (other == this)
|
||||
return true;
|
||||
|
||||
if (!(other instanceof OnlineAccountData))
|
||||
return false;
|
||||
|
||||
OnlineAccountData otherOnlineAccountData = (OnlineAccountData) other;
|
||||
|
||||
// Very quick comparison
|
||||
if (otherOnlineAccountData.timestamp != this.timestamp)
|
||||
return false;
|
||||
|
||||
// Signature more likely to be unique than public key
|
||||
if (!Arrays.equals(otherOnlineAccountData.signature, this.signature))
|
||||
return false;
|
||||
|
||||
if (!Arrays.equals(otherOnlineAccountData.publicKey, this.publicKey))
|
||||
return false;
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
@Override
|
||||
public int hashCode() {
|
||||
// Pretty lazy implementation
|
||||
return (int) this.timestamp;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -1,12 +1,12 @@
|
||||
package org.qortal.data.transaction;
|
||||
|
||||
import java.math.BigDecimal;
|
||||
|
||||
import javax.xml.bind.Unmarshaller;
|
||||
import javax.xml.bind.annotation.XmlAccessType;
|
||||
import javax.xml.bind.annotation.XmlAccessorType;
|
||||
import javax.xml.bind.annotation.adapters.XmlJavaTypeAdapter;
|
||||
|
||||
import org.qortal.account.GenesisAccount;
|
||||
import org.qortal.account.NullAccount;
|
||||
import org.qortal.transaction.Transaction.TransactionType;
|
||||
|
||||
import io.swagger.v3.oas.annotations.media.Schema;
|
||||
@@ -17,10 +17,19 @@ import io.swagger.v3.oas.annotations.media.Schema;
|
||||
public class ATTransactionData extends TransactionData {
|
||||
|
||||
// Properties
|
||||
|
||||
private String atAddress;
|
||||
|
||||
private String recipient;
|
||||
private BigDecimal amount;
|
||||
|
||||
// Not always present
|
||||
@XmlJavaTypeAdapter(value = org.qortal.api.AmountTypeAdapter.class)
|
||||
private Long amount;
|
||||
|
||||
// Not always present
|
||||
private Long assetId;
|
||||
|
||||
// Not always present
|
||||
private byte[] message;
|
||||
|
||||
// Constructors
|
||||
@@ -31,14 +40,14 @@ public class ATTransactionData extends TransactionData {
|
||||
}
|
||||
|
||||
public void afterUnmarshal(Unmarshaller u, Object parent) {
|
||||
this.creatorPublicKey = GenesisAccount.PUBLIC_KEY;
|
||||
this.creatorPublicKey = NullAccount.PUBLIC_KEY;
|
||||
}
|
||||
|
||||
/** From repository */
|
||||
public ATTransactionData(BaseTransactionData baseTransactionData, String atAddress, String recipient, BigDecimal amount, Long assetId, byte[] message) {
|
||||
/** Constructing from repository */
|
||||
public ATTransactionData(BaseTransactionData baseTransactionData, String atAddress, String recipient, Long amount, Long assetId, byte[] message) {
|
||||
super(TransactionType.AT, baseTransactionData);
|
||||
|
||||
this.creatorPublicKey = GenesisAccount.PUBLIC_KEY;
|
||||
this.creatorPublicKey = NullAccount.PUBLIC_KEY;
|
||||
this.atAddress = atAddress;
|
||||
this.recipient = recipient;
|
||||
this.amount = amount;
|
||||
@@ -46,6 +55,16 @@ public class ATTransactionData extends TransactionData {
|
||||
this.message = message;
|
||||
}
|
||||
|
||||
/** Constructing a new MESSAGE-type AT transaction */
|
||||
public ATTransactionData(BaseTransactionData baseTransactionData, String atAddress, String recipient, byte[] message) {
|
||||
this(baseTransactionData, atAddress, recipient, null, null, message);
|
||||
}
|
||||
|
||||
/** Constructing a new PAYMENT-type AT transaction */
|
||||
public ATTransactionData(BaseTransactionData baseTransactionData, String atAddress, String recipient, long amount, long assetId) {
|
||||
this(baseTransactionData, atAddress, recipient, amount, assetId, null);
|
||||
}
|
||||
|
||||
// Getters/Setters
|
||||
|
||||
public String getATAddress() {
|
||||
@@ -56,7 +75,7 @@ public class ATTransactionData extends TransactionData {
|
||||
return this.recipient;
|
||||
}
|
||||
|
||||
public BigDecimal getAmount() {
|
||||
public Long getAmount() {
|
||||
return this.amount;
|
||||
}
|
||||
|
||||
|
||||
@@ -6,7 +6,7 @@ import javax.xml.bind.annotation.XmlAccessorType;
|
||||
import javax.xml.bind.annotation.XmlElement;
|
||||
|
||||
import org.eclipse.persistence.oxm.annotations.XmlDiscriminatorValue;
|
||||
import org.qortal.account.GenesisAccount;
|
||||
import org.qortal.account.NullAccount;
|
||||
import org.qortal.block.GenesisBlock;
|
||||
import org.qortal.transaction.Transaction.TransactionType;
|
||||
|
||||
@@ -36,10 +36,10 @@ public class AccountFlagsTransactionData extends TransactionData {
|
||||
/*
|
||||
* If we're being constructed as part of the genesis block info inside blockchain config
|
||||
* and no specific creator's public key is supplied
|
||||
* then use genesis account's public key.
|
||||
* then use null account's public key.
|
||||
*/
|
||||
if (parent instanceof GenesisBlock.GenesisInfo && this.creatorPublicKey == null)
|
||||
this.creatorPublicKey = GenesisAccount.PUBLIC_KEY;
|
||||
this.creatorPublicKey = NullAccount.PUBLIC_KEY;
|
||||
}
|
||||
|
||||
/** From repository */
|
||||
|
||||
@@ -6,7 +6,7 @@ import javax.xml.bind.annotation.XmlAccessorType;
|
||||
import javax.xml.bind.annotation.XmlElement;
|
||||
|
||||
import org.eclipse.persistence.oxm.annotations.XmlDiscriminatorValue;
|
||||
import org.qortal.account.GenesisAccount;
|
||||
import org.qortal.account.NullAccount;
|
||||
import org.qortal.block.GenesisBlock;
|
||||
import org.qortal.transaction.Transaction.TransactionType;
|
||||
|
||||
@@ -33,10 +33,10 @@ public class AccountLevelTransactionData extends TransactionData {
|
||||
/*
|
||||
* If we're being constructed as part of the genesis block info inside blockchain config
|
||||
* and no specific creator's public key is supplied
|
||||
* then use genesis account's public key.
|
||||
* then use null account's public key.
|
||||
*/
|
||||
if (parent instanceof GenesisBlock.GenesisInfo && this.creatorPublicKey == null)
|
||||
this.creatorPublicKey = GenesisAccount.PUBLIC_KEY;
|
||||
this.creatorPublicKey = NullAccount.PUBLIC_KEY;
|
||||
}
|
||||
|
||||
/** From repository, network/API */
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user