mirror of
https://github.com/Qortal/qortal.git
synced 2025-07-30 21:51:26 +00:00
Compare commits
63 Commits
Author | SHA1 | Date | |
---|---|---|---|
|
6a24f787c4 | ||
|
98564aa8bf | ||
|
9ceff90f42 | ||
|
6a4388fecc | ||
|
1958444bc4 | ||
|
a2038274e1 | ||
|
532c697026 | ||
|
5cf5c1e1f7 | ||
|
60621e8b81 | ||
|
a6a1f65d3e | ||
|
a681f741dd | ||
|
bed9837967 | ||
|
855cb2226a | ||
|
d85a3d17c8 | ||
|
81a5b154c2 | ||
|
a6f42df9d6 | ||
|
17ae7acc6d | ||
|
3d5fec3c30 | ||
|
21f48fba5f | ||
|
d0da5d7c48 | ||
|
4209cc6ee4 | ||
|
f3e1092dd5 | ||
|
43055b666f | ||
|
1720582f33 | ||
|
d93e9d570f | ||
|
5ea90f2fdd | ||
|
c628f97d8c | ||
|
8a1e2f4111 | ||
|
41f244d549 | ||
|
79641efa87 | ||
|
ca3fcc3c67 | ||
|
de8e5ec920 | ||
|
f833e44bd5 | ||
|
8b0b1db5a4 | ||
|
5b95f3af02 | ||
|
3cc66609e8 | ||
|
ce468d22dd | ||
|
3e19516f62 | ||
|
84dba739d9 | ||
|
99315c7378 | ||
|
1ca5b864a9 | ||
|
96eb60dca3 | ||
|
c67fcb0034 | ||
|
273dfe2365 | ||
|
5952ea4b54 | ||
|
1708ba077c | ||
|
b4301f125d | ||
|
9e52f20f71 | ||
|
31bf388cab | ||
|
276c479a5f | ||
|
9393689037 | ||
|
76485010ad | ||
|
b8ac128d5c | ||
|
06c75310a1 | ||
|
b9d819220d | ||
|
7a569f342f | ||
|
f1efae79c8 | ||
|
1cd4bbc078 | ||
|
0b5e5832c4 | ||
|
7db96c672f | ||
|
f8725d6313 | ||
|
2165c87b9d | ||
|
f61e320230 |
@@ -19,10 +19,10 @@
|
||||
<ROW Property="Manufacturer" Value="Qortal"/>
|
||||
<ROW Property="MsiLogging" MultiBuildValue="DefaultBuild:vp"/>
|
||||
<ROW Property="NTP_GOOD" Value="false"/>
|
||||
<ROW Property="ProductCode" Value="1033:{31E3EA92-5348-4D5E-BF92-403470774E62} 2052:{FD291A77-AFE6-4511-BF5C-643D2173F4E5} 2057:{AC09453B-3F19-49E9-984D-BF167F2E29B9} " Type="16"/>
|
||||
<ROW Property="ProductCode" Value="1033:{3F23DC7A-BC0B-4598-9FD4-C4B927A10D7F} 1049:{CF0D5DDC-7CB7-4308-8F98-DF8D2DB2D38D} 2052:{983B77E5-62CF-431C-B015-B96C5DCA6858} 2057:{BF1C757A-A3A0-4285-906A-6D8D91D74D0A} " Type="16"/>
|
||||
<ROW Property="ProductLanguage" Value="2057"/>
|
||||
<ROW Property="ProductName" Value="Qortal"/>
|
||||
<ROW Property="ProductVersion" Value="1.2.2" Type="32"/>
|
||||
<ROW Property="ProductVersion" Value="1.3.5" Type="32"/>
|
||||
<ROW Property="RECONFIG_NTP" Value="true"/>
|
||||
<ROW Property="REMOVE_BLOCKCHAIN" Value="YES" Type="4"/>
|
||||
<ROW Property="REPAIR_BLOCKCHAIN" Value="YES" Type="4"/>
|
||||
@@ -174,7 +174,7 @@
|
||||
<ROW Component="ADDITIONAL_LICENSE_INFO_97" ComponentId="{D5544706-E2A7-424F-AEA5-3963E355AA29}" Directory_="jdk.crypto.mscapi_Dir" Attributes="0" KeyPath="ADDITIONAL_LICENSE_INFO_97" Type="0"/>
|
||||
<ROW Component="ADDITIONAL_LICENSE_INFO_98" ComponentId="{104DBCE8-A458-4B3E-9EFA-2D8613561619}" Directory_="jdk.dynalink_Dir" Attributes="0" KeyPath="ADDITIONAL_LICENSE_INFO_98" Type="0"/>
|
||||
<ROW Component="ADDITIONAL_LICENSE_INFO_99" ComponentId="{D02E3C37-E81A-48FA-9E28-B26B728AECD9}" Directory_="jdk.httpserver_Dir" Attributes="0" KeyPath="ADDITIONAL_LICENSE_INFO_99" Type="0"/>
|
||||
<ROW Component="AI_CustomARPName" ComponentId="{761A8B87-CEA8-4F5D-82CC-552C48C9EC14}" Directory_="APPDIR" Attributes="260" KeyPath="DisplayName" Options="1"/>
|
||||
<ROW Component="AI_CustomARPName" ComponentId="{5FCFB67B-FDD0-4B15-9A58-2188092589E9}" Directory_="APPDIR" Attributes="260" KeyPath="DisplayName" Options="1"/>
|
||||
<ROW Component="AI_ExePath" ComponentId="{3644948D-AE0B-41BB-9FAF-A79E70490A08}" Directory_="APPDIR" Attributes="260" KeyPath="AI_ExePath"/>
|
||||
<ROW Component="APPDIR" ComponentId="{680DFDDE-3FB4-47A5-8FF5-934F576C6F91}" Directory_="APPDIR" Attributes="0"/>
|
||||
<ROW Component="DATA_PATH" ComponentId="{EE0B6107-E244-4CDB-B195-E9038D2F1E0E}" Directory_="DATA_PATH" Attributes="0"/>
|
||||
@@ -648,7 +648,7 @@
|
||||
<ROW BootstrOptKey="GlobalOptions" DownloadFolder="[AppDataFolder][|Manufacturer]\[|ProductName]\prerequisites" Options="2"/>
|
||||
</COMPONENT>
|
||||
<COMPONENT cid="caphyon.advinst.msicomp.BuildComponent">
|
||||
<ROW BuildKey="DefaultBuild" BuildName="DefaultBuild" BuildOrder="1" BuildType="1" Languages="en_GB;zh;en" LangOpt="1" InstallationType="2" CabsLocation="1" UseLzma="true" LzmaMethod="2" LzmaCompressionLevel="4" PackageType="1" FilesInsideExe="true" ExeIconPath="qortal.ico" ExtractionFolder="[AppDataFolder][|Manufacturer]-extract\[|ProductName] [|ProductVersion]\install" LangsDialog="true" UseLargeSchema="true" Unicode="true" ExeName="[|ProductName]-[|ProductVersion]" MsiPackageType="x64" JRE64Dir="jre64_Dir"/>
|
||||
<ROW BuildKey="DefaultBuild" BuildName="DefaultBuild" BuildOrder="1" BuildType="1" Languages="en_GB;zh;en;ru" LangOpt="1" InstallationType="2" CabsLocation="1" UseLzma="true" LzmaMethod="2" LzmaCompressionLevel="4" PackageType="1" FilesInsideExe="true" ExeIconPath="qortal.ico" ExtractionFolder="[AppDataFolder][|Manufacturer]-extract\[|ProductName] [|ProductVersion]\install" LangsDialog="true" UseLargeSchema="true" Unicode="true" ExeName="[|ProductName]-[|ProductVersion]" MsiPackageType="x64" JRE64Dir="jre64_Dir"/>
|
||||
</COMPONENT>
|
||||
<COMPONENT cid="caphyon.advinst.msicomp.DictionaryComponent">
|
||||
<ROW Path="<AI_DICTS>ui.ail"/>
|
||||
@@ -656,6 +656,7 @@
|
||||
<ROW Path="<AI_DICTS>ui_en.ail"/>
|
||||
<ROW Path="<AI_DICTS>ui_zh.ail"/>
|
||||
<ROW Path="dictionary.ail" Options="1"/>
|
||||
<ROW Path="<AI_DICTS>ui_ru.ail"/>
|
||||
</COMPONENT>
|
||||
<COMPONENT cid="caphyon.advinst.msicomp.FirewallExceptionComponent">
|
||||
<ROW FirewallException="EXE" DisplayName="[ProductName]" Enabled="1" Scope="*" Condition="((?Qortal.exe=2) AND ($Qortal.exe=3))" Profiles="0" AppPath="[#Qortal.exe]" Port="*" Protocol="ANY"/>
|
||||
@@ -1128,11 +1129,14 @@
|
||||
<ROW IniFile="ApplicationType" FileName="Qortal.ini" DirProperty="APPDIR" Section="Application" Key="Application Type" Value="gui" Action="0" Component_="Qortal.exe"/>
|
||||
<ROW IniFile="ClassPath" FileName="Qortal.ini" DirProperty="APPDIR" Section="Class Path" Key="Class Path" Value="[#qortal.jar];" Action="0" Component_="Qortal.exe"/>
|
||||
<ROW IniFile="FailureCheck" FileName="Qortal.ini" DirProperty="APPDIR" Section="Application" Key="Failure Check" Value="yes" Action="0" Component_="Qortal.exe"/>
|
||||
<ROW IniFile="JVMSource" FileName="Qortal.ini" DirProperty="APPDIR" Section="Java Runtime Environment" Key="JVM Source" Value="favor_JDK" Action="0" Component_="Qortal.exe"/>
|
||||
<ROW IniFile="JVMType" FileName="Qortal.ini" DirProperty="APPDIR" Section="Java Runtime Environment" Key="JVM Type" Value="favor_server" Action="0" Component_="Qortal.exe"/>
|
||||
<ROW IniFile="MainClass" FileName="Qortal.ini" DirProperty="APPDIR" Section="Java Runtime Environment" Key="Main Class" Value="org.qortal.controller.Controller" Action="0" Component_="Qortal.exe"/>
|
||||
<ROW IniFile="MaximumVersion" FileName="Qortal.ini" DirProperty="APPDIR" Section="Java Runtime Environment" Key="Maximum Version" Value="any" Action="0" Component_="Qortal.exe"/>
|
||||
<ROW IniFile="MinimumVersion" FileName="Qortal.ini" DirProperty="APPDIR" Section="Java Runtime Environment" Key="Minimum Version" Value="11" Action="0" Component_="Qortal.exe"/>
|
||||
<ROW IniFile="OverrideWorkingDir" FileName="Qortal.ini" DirProperty="APPDIR" Section="Application" Key="Override WorkingDir" Value="yes" Action="0" Component_="Qortal.exe"/>
|
||||
<ROW IniFile="SingleInstance" FileName="Qortal.ini" DirProperty="APPDIR" Section="Application" Key="Single Instance" Value="yes" Action="0" Component_="Qortal.exe"/>
|
||||
<ROW IniFile="VMProvider" FileName="Qortal.ini" DirProperty="APPDIR" Section="Java Runtime Environment" Key="VM Provider" Value="any" Action="0" Component_="Qortal.exe"/>
|
||||
</COMPONENT>
|
||||
<COMPONENT cid="caphyon.advinst.msicomp.MsiInstExSeqComponent">
|
||||
<ROW Action="AI_DOWNGRADE" Condition="AI_NEWERPRODUCTFOUND AND (UILevel <> 5)" Sequence="210"/>
|
||||
@@ -1189,7 +1193,7 @@
|
||||
<ROW Action="Set_DATA_PATH_property" Condition="( NOT Installed )" Sequence="77"/>
|
||||
</COMPONENT>
|
||||
<COMPONENT cid="caphyon.advinst.msicomp.MsiJavaComponent">
|
||||
<ROW Name="Qortal.exe" Launcher="Qortal.exe" MainClass="MainClass" ClassPath="ClassPath" JREMin="MinimumVersion" JREMax="MaximumVersion" IconPath="qortal.ico" AppType="ApplicationType" SingleInstance="SingleInstance" PlusList="APPDIR;" MinusList="#Qortal.exe;" MacDescription="[|CurrentJavaProductName] [|ProductVersion] © [|Manufacturer], Inc, 2019" MacBundleId="com.[|Manufacturer].[|ProductName].[|CurrentJavaProductName]" FailureCheck="FailureCheck" OverrideWkDir="OverrideWorkingDir" UACExecutionLevel="0" Platform64="true"/>
|
||||
<ROW Name="Qortal.exe" Launcher="Qortal.exe" MainClass="MainClass" ClassPath="ClassPath" JREMin="MinimumVersion" JREMax="MaximumVersion" IconPath="qortal.ico" AppType="ApplicationType" JVMType="JVMType" SingleInstance="SingleInstance" PlusList="APPDIR;" MinusList="#Qortal.exe;" MacDescription="[|CurrentJavaProductName] [|ProductVersion] © [|Manufacturer], Inc, 2019" MacBundleId="com.[|Manufacturer].[|ProductName].[|CurrentJavaProductName]" JVMSourcePreference="JVMSource" FailureCheck="FailureCheck" OverrideWkDir="OverrideWorkingDir" UACExecutionLevel="0" VMProviderOptions="VMProvider" Platform64="true"/>
|
||||
</COMPONENT>
|
||||
<COMPONENT cid="caphyon.advinst.msicomp.MsiLaunchConditionsComponent">
|
||||
<ROW Condition="( Version9X OR VersionNT64 )" Description="[ProductName] can not be installed on [WindowsTypeNTDisplay]." DescriptionLocId="AI.LaunchCondition.NoNT" IsPredefined="true" Builds="DefaultBuild"/>
|
||||
|
20
WindowsInstaller/dictionary.ail
Executable file → Normal file
20
WindowsInstaller/dictionary.ail
Executable file → Normal file
@@ -3,95 +3,115 @@
|
||||
<!-- 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="ru" value="Вы можете выбрать место хранения блокчейна и других данных."/>
|
||||
<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="ru" value="Выберите один из вариантов ниже, затем нажмите Далее"/>
|
||||
<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="ru" value="Выберите место хранения данных."/>
|
||||
<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="ru" value="Вы можете выбрать место хранения блокчейна и других данных."/>
|
||||
<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="ru" value="Выберите место хранения данных."/>
|
||||
<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="ru" value="Это папка, в которой будет храниться блокчейн и другие данные."/>
|
||||
<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="ru" value="Чтобы сохранить данные в этой папке, нажмите "[Text_Next]". Чтобы сохранить данные в другой папке, введите ее ниже или нажмите "Обзор"."/>
|
||||
<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="ru" value="Выберите папку для хранения данных"/>
|
||||
<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="ru" value="Это папка, в которой будет храниться блокчейн и другие данные."/>
|
||||
<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="ru" value="Выберите папку для хранения данных"/>
|
||||
<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="ru" value="Настроить синхронизацию времени системы Windows?"/>
|
||||
<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="ru" value="Для подключения к сети Qortal и совершения транзакций требуется точная настройка времени Windows"/>
|
||||
<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="ru" value="Выберите один из вариантов ниже, затем нажмите"/>
|
||||
<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="ru" value="Точность времени вашего компьютера должна составлять 0.5 секунд."/>
|
||||
<STRING lang="zh" value="您的计算机时钟需要准确到0.5秒内。"/>
|
||||
</ENTRY>
|
||||
<ENTRY id="Control.Text.NTPDialog#Title">
|
||||
<STRING lang="en" value="Windows clock accuracy"/>
|
||||
<STRING lang="ru" value="Настройка времени системы Windows"/>
|
||||
<STRING lang="zh" value="Windows 时钟精度"/>
|
||||
</ENTRY>
|
||||
<ENTRY id="Control.Text.VerifyRemoveDlg#RemoveBlockchainCheckbox">
|
||||
<STRING lang="en" value="Remove downloaded blockchain and other data"/>
|
||||
<STRING lang="ru" value="Удалить загруженный блокчейн и другие данные"/>
|
||||
<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="ru" value="Выбрать папку для хранения данных..."/>
|
||||
<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="ru" value="Использовать папку по умолчанию"/>
|
||||
<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="ru" value="Да, настроить синхронизацию времени Windows (Рекомендуется)"/>
|
||||
<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="ru" value="Нет, я сам буду управлять настройками часов"/>
|
||||
<STRING lang="zh" value="不,我会自己管理时钟精度。"/>
|
||||
</ENTRY>
|
||||
</DICTIONARY>
|
||||
|
BIN
lib/org/ciyam/AT/1.3.8/AT-1.3.8.jar
Normal file
BIN
lib/org/ciyam/AT/1.3.8/AT-1.3.8.jar
Normal file
Binary file not shown.
9
lib/org/ciyam/AT/1.3.8/AT-1.3.8.pom
Normal file
9
lib/org/ciyam/AT/1.3.8/AT-1.3.8.pom
Normal file
@@ -0,0 +1,9 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<project xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd" xmlns="http://maven.apache.org/POM/4.0.0"
|
||||
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance">
|
||||
<modelVersion>4.0.0</modelVersion>
|
||||
<groupId>org.ciyam</groupId>
|
||||
<artifactId>AT</artifactId>
|
||||
<version>1.3.8</version>
|
||||
<description>POM was created from install:install-file</description>
|
||||
</project>
|
@@ -3,13 +3,14 @@
|
||||
<groupId>org.ciyam</groupId>
|
||||
<artifactId>AT</artifactId>
|
||||
<versioning>
|
||||
<release>1.3.7</release>
|
||||
<release>1.3.8</release>
|
||||
<versions>
|
||||
<version>1.3.4</version>
|
||||
<version>1.3.5</version>
|
||||
<version>1.3.6</version>
|
||||
<version>1.3.7</version>
|
||||
<version>1.3.8</version>
|
||||
</versions>
|
||||
<lastUpdated>20200812131412</lastUpdated>
|
||||
<lastUpdated>20200925114415</lastUpdated>
|
||||
</versioning>
|
||||
</metadata>
|
||||
|
13
pom.xml
13
pom.xml
@@ -3,19 +3,18 @@
|
||||
<modelVersion>4.0.0</modelVersion>
|
||||
<groupId>org.qortal</groupId>
|
||||
<artifactId>qortal</artifactId>
|
||||
<version>1.3.2</version>
|
||||
<version>1.3.6</version>
|
||||
<packaging>jar</packaging>
|
||||
<properties>
|
||||
<bitcoinj.version>0.15.5</bitcoinj.version>
|
||||
<bouncycastle.version>1.64</bouncycastle.version>
|
||||
<build.timestamp>${maven.build.timestamp}</build.timestamp>
|
||||
<ciyam-at.version>1.3.7</ciyam-at.version>
|
||||
<ciyam-at.version>1.3.8</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>
|
||||
<guava.version>28.1-jre</guava.version>
|
||||
<hsqldb.version>2.5.0-fixed</hsqldb.version>
|
||||
<hsqldb-sqltool.version>2.5.0</hsqldb-sqltool.version>
|
||||
<hsqldb.version>2.5.1</hsqldb.version>
|
||||
<jersey.version>2.29.1</jersey.version>
|
||||
<jetty.version>9.4.29.v20200521</jetty.version>
|
||||
<log4j.version>2.12.1</log4j.version>
|
||||
@@ -397,12 +396,6 @@
|
||||
<artifactId>hsqldb</artifactId>
|
||||
<version>${hsqldb.version}</version>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>org.hsqldb</groupId>
|
||||
<artifactId>sqltool</artifactId>
|
||||
<version>${hsqldb-sqltool.version}</version>
|
||||
<scope>test</scope>
|
||||
</dependency>
|
||||
<!-- CIYAM AT (automated transactions) -->
|
||||
<dependency>
|
||||
<groupId>org.ciyam</groupId>
|
||||
|
51
run.sh
51
run.sh
@@ -1,51 +0,0 @@
|
||||
#!/bin/sh
|
||||
|
||||
# There's no need to run as root, so don't allow it, for security reasons
|
||||
if [ "$USER" = "root" ]; then
|
||||
echo "Please su to non-root user before running"
|
||||
exit
|
||||
fi
|
||||
|
||||
# Validate Java is installed and the minimum version is available
|
||||
MIN_JAVA_VER='11'
|
||||
|
||||
if command -v java > /dev/null 2>&1; then
|
||||
version=$(java -version 2>&1 | awk -F '"' '/version/ {print $2}')
|
||||
version=$(echo $version | cut -d'.' -f1,2)
|
||||
if [ `echo "${version}>=${MIN_JAVA_VER}" | bc` -eq 1 ]; then
|
||||
echo 'Passed Java version check'
|
||||
else
|
||||
echo 'Please upgrade your Java to version 11 or greater'
|
||||
exit 1
|
||||
fi
|
||||
else
|
||||
echo 'Java is not available, please install Java 11 or greater'
|
||||
exit 1
|
||||
fi
|
||||
|
||||
# No qortal.jar but we have a Maven built one?
|
||||
# Be helpful and copy across to correct location
|
||||
if [ ! -e qortal.jar -a -f target/qortal*.jar ]; then
|
||||
echo "Copying Maven-built Qortal JAR to correct pathname"
|
||||
cp target/qortal*.jar qortal.jar
|
||||
fi
|
||||
|
||||
# Limits Java JVM stack size and maximum heap usage.
|
||||
# Comment out for bigger systems, e.g. non-routers
|
||||
# or when API documentation is enabled
|
||||
# JVM_MEMORY_ARGS="-Xss256k -Xmx128m"
|
||||
|
||||
# Although java.net.preferIPv4Stack is supposed to be false
|
||||
# by default in Java 11, on some platforms (e.g. FreeBSD 12),
|
||||
# it is overriden to be true by default. Hence we explicitly
|
||||
# set it to true to obtain desired behaviour.
|
||||
nohup nice -n 20 java \
|
||||
-Djava.net.preferIPv4Stack=false \
|
||||
-XX:NativeMemoryTracking=summary \
|
||||
${JVM_MEMORY_ARGS} \
|
||||
-jar qortal.jar \
|
||||
1>run.log 2>&1 &
|
||||
|
||||
# Save backgrounded process's PID
|
||||
echo $! > run.pid
|
||||
echo qortal running as pid $!
|
@@ -35,6 +35,8 @@ public class ApplyUpdate {
|
||||
private static final String JAR_FILENAME = AutoUpdate.JAR_FILENAME;
|
||||
private static final String NEW_JAR_FILENAME = AutoUpdate.NEW_JAR_FILENAME;
|
||||
private static final String WINDOWS_EXE_LAUNCHER = "qortal.exe";
|
||||
private static final String JAVA_TOOL_OPTIONS_NAME = "JAVA_TOOL_OPTIONS";
|
||||
private static final String JAVA_TOOL_OPTIONS_VALUE = "-XX:MaxRAMFraction=4";
|
||||
|
||||
private static final long CHECK_INTERVAL = 10 * 1000L; // ms
|
||||
private static final int MAX_ATTEMPTS = 12;
|
||||
@@ -65,17 +67,19 @@ public class ApplyUpdate {
|
||||
}
|
||||
|
||||
private static boolean shutdownNode() {
|
||||
String BASE_URI = "http://localhost:" + Settings.getInstance().getApiPort() + "/";
|
||||
LOGGER.info(String.format("Shutting down node using API via %s", BASE_URI));
|
||||
String baseUri = "http://localhost:" + Settings.getInstance().getApiPort() + "/";
|
||||
LOGGER.info(() -> String.format("Shutting down node using API via %s", baseUri));
|
||||
|
||||
int attempt;
|
||||
for (attempt = 0; attempt < MAX_ATTEMPTS; ++attempt) {
|
||||
LOGGER.info(String.format("Attempt #%d out of %d to shutdown node", attempt + 1, MAX_ATTEMPTS));
|
||||
String response = ApiRequest.perform(BASE_URI + "admin/stop", null);
|
||||
final int attemptForLogging = attempt;
|
||||
LOGGER.info(() -> String.format("Attempt #%d out of %d to shutdown node", attemptForLogging + 1, MAX_ATTEMPTS));
|
||||
String response = ApiRequest.perform(baseUri + "admin/stop", null);
|
||||
if (response == null)
|
||||
break;
|
||||
// No response - consider node shut down
|
||||
return true;
|
||||
|
||||
LOGGER.info(String.format("Response from API: %s", response));
|
||||
LOGGER.info(() -> String.format("Response from API: %s", response));
|
||||
|
||||
try {
|
||||
Thread.sleep(CHECK_INTERVAL);
|
||||
@@ -99,19 +103,20 @@ public class ApplyUpdate {
|
||||
Path newJar = Paths.get(NEW_JAR_FILENAME);
|
||||
|
||||
if (!Files.exists(newJar)) {
|
||||
LOGGER.warn(String.format("Replacement JAR '%s' not found?", newJar));
|
||||
LOGGER.warn(() -> String.format("Replacement JAR '%s' not found?", newJar));
|
||||
return;
|
||||
}
|
||||
|
||||
int attempt;
|
||||
for (attempt = 0; attempt < MAX_ATTEMPTS; ++attempt) {
|
||||
LOGGER.info(String.format("Attempt #%d out of %d to replace JAR", attempt + 1, MAX_ATTEMPTS));
|
||||
final int attemptForLogging = attempt;
|
||||
LOGGER.info(() -> String.format("Attempt #%d out of %d to replace JAR", attemptForLogging + 1, MAX_ATTEMPTS));
|
||||
|
||||
try {
|
||||
Files.copy(newJar, realJar, StandardCopyOption.REPLACE_EXISTING);
|
||||
break;
|
||||
} catch (IOException e) {
|
||||
LOGGER.info(String.format("Unable to replace JAR: %s", e.getMessage()));
|
||||
LOGGER.info(() -> String.format("Unable to replace JAR: %s", e.getMessage()));
|
||||
|
||||
// Try again
|
||||
}
|
||||
@@ -119,6 +124,7 @@ public class ApplyUpdate {
|
||||
try {
|
||||
Thread.sleep(CHECK_INTERVAL);
|
||||
} catch (InterruptedException e) {
|
||||
LOGGER.warn("Ignoring interrupt...");
|
||||
// Doggedly retry
|
||||
}
|
||||
}
|
||||
@@ -129,13 +135,13 @@ public class ApplyUpdate {
|
||||
|
||||
private static void restartNode(String[] args) {
|
||||
String javaHome = System.getProperty("java.home");
|
||||
LOGGER.info(String.format("Java home: %s", javaHome));
|
||||
LOGGER.info(() -> String.format("Java home: %s", javaHome));
|
||||
|
||||
Path javaBinary = Paths.get(javaHome, "bin", "java");
|
||||
LOGGER.info(String.format("Java binary: %s", javaBinary));
|
||||
LOGGER.info(() -> String.format("Java binary: %s", javaBinary));
|
||||
|
||||
Path exeLauncher = Paths.get(WINDOWS_EXE_LAUNCHER);
|
||||
LOGGER.info(String.format("Windows EXE launcher: %s", exeLauncher));
|
||||
LOGGER.info(() -> String.format("Windows EXE launcher: %s", exeLauncher));
|
||||
|
||||
List<String> javaCmd;
|
||||
if (Files.exists(exeLauncher)) {
|
||||
@@ -156,9 +162,16 @@ public class ApplyUpdate {
|
||||
}
|
||||
|
||||
try {
|
||||
LOGGER.info(String.format("Restarting node with: %s", String.join(" ", javaCmd)));
|
||||
LOGGER.info(() -> String.format("Restarting node with: %s", String.join(" ", javaCmd)));
|
||||
|
||||
new ProcessBuilder(javaCmd).start();
|
||||
ProcessBuilder processBuilder = new ProcessBuilder(javaCmd);
|
||||
|
||||
if (Files.exists(exeLauncher)) {
|
||||
LOGGER.info(() -> String.format("Setting env %s to %s", JAVA_TOOL_OPTIONS_NAME, JAVA_TOOL_OPTIONS_VALUE));
|
||||
processBuilder.environment().put(JAVA_TOOL_OPTIONS_NAME, JAVA_TOOL_OPTIONS_VALUE);
|
||||
}
|
||||
|
||||
processBuilder.start();
|
||||
} catch (IOException e) {
|
||||
LOGGER.error(String.format("Failed to restart node (BAD): %s", e.getMessage()));
|
||||
}
|
||||
|
75
src/main/java/org/qortal/RepositoryMaintenance.java
Normal file
75
src/main/java/org/qortal/RepositoryMaintenance.java
Normal file
@@ -0,0 +1,75 @@
|
||||
package org.qortal;
|
||||
|
||||
import java.security.Security;
|
||||
|
||||
import org.apache.logging.log4j.LogManager;
|
||||
import org.apache.logging.log4j.Logger;
|
||||
import org.bouncycastle.jce.provider.BouncyCastleProvider;
|
||||
import org.bouncycastle.jsse.provider.BouncyCastleJsseProvider;
|
||||
import org.qortal.controller.Controller;
|
||||
import org.qortal.repository.DataException;
|
||||
import org.qortal.repository.Repository;
|
||||
import org.qortal.repository.RepositoryFactory;
|
||||
import org.qortal.repository.RepositoryManager;
|
||||
import org.qortal.repository.hsqldb.HSQLDBRepositoryFactory;
|
||||
import org.qortal.settings.Settings;
|
||||
|
||||
public class RepositoryMaintenance {
|
||||
|
||||
static {
|
||||
// This must go before any calls to LogManager/Logger
|
||||
System.setProperty("java.util.logging.manager", "org.apache.logging.log4j.jul.LogManager");
|
||||
}
|
||||
|
||||
private static final Logger LOGGER = LogManager.getLogger(RepositoryMaintenance.class);
|
||||
|
||||
public static void main(String[] args) {
|
||||
LOGGER.info("Repository maintenance starting up...");
|
||||
|
||||
Security.insertProviderAt(new BouncyCastleProvider(), 0);
|
||||
Security.insertProviderAt(new BouncyCastleJsseProvider(), 1);
|
||||
|
||||
// Load/check settings, which potentially sets up blockchain config, etc.
|
||||
try {
|
||||
if (args.length > 0)
|
||||
Settings.fileInstance(args[0]);
|
||||
else
|
||||
Settings.getInstance();
|
||||
} catch (Throwable t) {
|
||||
LOGGER.error("Settings file error: " + t.getMessage());
|
||||
System.exit(2);
|
||||
}
|
||||
|
||||
LOGGER.info("Opening repository");
|
||||
try {
|
||||
RepositoryFactory repositoryFactory = new HSQLDBRepositoryFactory(Controller.getRepositoryUrl());
|
||||
RepositoryManager.setRepositoryFactory(repositoryFactory);
|
||||
} catch (DataException e) {
|
||||
// If exception has no cause then repository is in use by some other process.
|
||||
if (e.getCause() == null) {
|
||||
LOGGER.info("Repository in use by another process?");
|
||||
} else {
|
||||
LOGGER.error("Unable to start repository", e);
|
||||
}
|
||||
|
||||
System.exit(1);
|
||||
}
|
||||
|
||||
LOGGER.info("Starting repository periodic maintenance. This can take a while...");
|
||||
try (final Repository repository = RepositoryManager.getRepository()) {
|
||||
repository.performPeriodicMaintenance();
|
||||
|
||||
LOGGER.info("Repository periodic maintenance completed");
|
||||
} catch (DataException e) {
|
||||
LOGGER.error("Repository periodic maintenance failed", e);
|
||||
}
|
||||
|
||||
try {
|
||||
LOGGER.info("Shutting down repository");
|
||||
RepositoryManager.closeRepositoryFactory();
|
||||
} catch (DataException e) {
|
||||
LOGGER.error("Error occurred while shutting down repository", e);
|
||||
}
|
||||
}
|
||||
|
||||
}
|
@@ -22,6 +22,9 @@ public class CrossChainBitcoinRefundRequest {
|
||||
@Schema(description = "Bitcoin miner fee", example = "0.00001000")
|
||||
public BigDecimal bitcoinMinerFee;
|
||||
|
||||
@Schema(description = "Bitcoin HASH160(public key) for receiving funds, or omit to derive from private key", example = "u17kBVKkKSp12oUzaxFwNnq1JZf")
|
||||
public byte[] receivingAccountInfo;
|
||||
|
||||
public CrossChainBitcoinRefundRequest() {
|
||||
}
|
||||
|
||||
|
@@ -83,4 +83,10 @@ public class CrossChainOfferSummary {
|
||||
return this.partnerQortalReceivingAddress;
|
||||
}
|
||||
|
||||
// For debugging mostly
|
||||
|
||||
public String toString() {
|
||||
return String.format("%s: %s", this.qortalAtAddress, this.mode.name());
|
||||
}
|
||||
|
||||
}
|
||||
|
@@ -11,6 +11,7 @@ public class NodeInfo {
|
||||
public String buildVersion;
|
||||
public long buildTimestamp;
|
||||
public String nodeId;
|
||||
public boolean isTestNet;
|
||||
|
||||
public NodeInfo() {
|
||||
}
|
||||
|
@@ -20,17 +20,14 @@ public class NodeStatus {
|
||||
public final int height;
|
||||
|
||||
public NodeStatus() {
|
||||
isMintingPossible = Controller.getInstance().isMintingPossible();
|
||||
isSynchronizing = Controller.getInstance().isSynchronizing();
|
||||
this.isMintingPossible = Controller.getInstance().isMintingPossible();
|
||||
|
||||
if (isSynchronizing)
|
||||
syncPercent = Controller.getInstance().getSyncPercent();
|
||||
else
|
||||
syncPercent = null;
|
||||
this.syncPercent = Controller.getInstance().getSyncPercent();
|
||||
this.isSynchronizing = this.syncPercent != null;
|
||||
|
||||
numberOfConnections = Network.getInstance().getHandshakedPeers().size();
|
||||
this.numberOfConnections = Network.getInstance().getHandshakedPeers().size();
|
||||
|
||||
height = Controller.getInstance().getChainHeight();
|
||||
this.height = Controller.getInstance().getChainHeight();
|
||||
}
|
||||
|
||||
}
|
||||
|
@@ -8,13 +8,13 @@ import io.swagger.v3.oas.annotations.media.Schema;
|
||||
@XmlAccessorType(XmlAccessType.FIELD)
|
||||
public class TradeBotRespondRequest {
|
||||
|
||||
@Schema(description = "Qortal AT address", example = "AH3e3jHEsGHPVQPDiJx4pYqgVi72auxgVy")
|
||||
@Schema(description = "Qortal AT address", example = "Aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa")
|
||||
public String atAddress;
|
||||
|
||||
@Schema(description = "Bitcoin BIP32 extended private key", example = "tprv8ZgxMBicQKsPdahhFSrCdvC1bsWyzHHZfTneTVqUXN6s1wEtZLwAkZXzFP6TbTVGajEB55L1HYLg2aQMecZLXLre5YJcawpdFG66STVAWPJ")
|
||||
@Schema(description = "Bitcoin BIP32 extended private key", example = "xprv___________________________________________________________________________________________________________")
|
||||
public String xprv58;
|
||||
|
||||
@Schema(description = "Qortal address for receiving QORT from AT")
|
||||
@Schema(description = "Qortal address for receiving QORT from AT", example = "Qqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqq")
|
||||
public String receivingAddress;
|
||||
|
||||
public TradeBotRespondRequest() {
|
||||
|
@@ -40,7 +40,6 @@ import org.qortal.account.Account;
|
||||
import org.qortal.account.PrivateKeyAccount;
|
||||
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.ActivitySummary;
|
||||
@@ -57,6 +56,7 @@ import org.qortal.network.PeerAddress;
|
||||
import org.qortal.repository.DataException;
|
||||
import org.qortal.repository.Repository;
|
||||
import org.qortal.repository.RepositoryManager;
|
||||
import org.qortal.settings.Settings;
|
||||
import org.qortal.utils.Base58;
|
||||
import org.qortal.utils.NTP;
|
||||
|
||||
@@ -118,6 +118,7 @@ public class AdminResource {
|
||||
nodeInfo.buildVersion = Controller.getInstance().getVersionString();
|
||||
nodeInfo.buildTimestamp = Controller.getInstance().getBuildTimestamp();
|
||||
nodeInfo.nodeId = Network.getInstance().getOurNodeId();
|
||||
nodeInfo.isTestNet = Settings.getInstance().isTestNet();
|
||||
|
||||
return nodeInfo;
|
||||
}
|
||||
@@ -435,8 +436,6 @@ public class AdminResource {
|
||||
throw ApiExceptionFactory.INSTANCE.createException(request, ApiError.REPOSITORY_ISSUE, e);
|
||||
} catch (NumberFormatException e) {
|
||||
throw ApiExceptionFactory.INSTANCE.createException(request, ApiError.INVALID_HEIGHT);
|
||||
} catch (ApiException e) {
|
||||
throw e;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -492,8 +491,6 @@ public class AdminResource {
|
||||
return syncResult.name();
|
||||
} catch (IllegalArgumentException e) {
|
||||
throw ApiExceptionFactory.INSTANCE.createException(request, ApiError.INVALID_DATA);
|
||||
} catch (ApiException e) {
|
||||
throw e;
|
||||
} catch (UnknownHostException e) {
|
||||
throw ApiExceptionFactory.INSTANCE.createException(request, ApiError.INVALID_DATA);
|
||||
} catch (InterruptedException e) {
|
||||
@@ -501,4 +498,31 @@ public class AdminResource {
|
||||
}
|
||||
}
|
||||
|
||||
@DELETE
|
||||
@Path("/repository")
|
||||
@Operation(
|
||||
summary = "Perform maintenance on repository.",
|
||||
description = "Requires enough free space to rebuild repository. This will pause your node for a while."
|
||||
)
|
||||
@ApiErrors({ApiError.REPOSITORY_ISSUE})
|
||||
public void performRepositoryMaintenance() {
|
||||
Security.checkApiCallAllowed(request);
|
||||
|
||||
try (final Repository repository = RepositoryManager.getRepository()) {
|
||||
ReentrantLock blockchainLock = Controller.getInstance().getBlockchainLock();
|
||||
|
||||
blockchainLock.lockInterruptibly();
|
||||
|
||||
try {
|
||||
repository.performPeriodicMaintenance();
|
||||
} finally {
|
||||
blockchainLock.unlock();
|
||||
}
|
||||
} catch (InterruptedException e) {
|
||||
// No big deal
|
||||
} catch (DataException e) {
|
||||
throw ApiExceptionFactory.INSTANCE.createException(request, ApiError.REPOSITORY_ISSUE, e);
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
@@ -12,6 +12,7 @@ import io.swagger.v3.oas.annotations.tags.Tag;
|
||||
import java.math.BigDecimal;
|
||||
import java.util.ArrayList;
|
||||
import java.util.Arrays;
|
||||
import java.util.Collections;
|
||||
import java.util.List;
|
||||
import java.util.Random;
|
||||
import java.util.function.Function;
|
||||
@@ -57,6 +58,7 @@ import org.qortal.controller.TradeBot;
|
||||
import org.qortal.crosschain.BTC;
|
||||
import org.qortal.crosschain.BTCACCT;
|
||||
import org.qortal.crosschain.BTCP2SH;
|
||||
import org.qortal.crosschain.BitcoinException;
|
||||
import org.qortal.crypto.Crypto;
|
||||
import org.qortal.data.at.ATData;
|
||||
import org.qortal.data.at.ATStateData;
|
||||
@@ -601,17 +603,12 @@ public class CrossChainResource {
|
||||
|
||||
Address p2shAddress = LegacyAddress.fromScriptHash(params, redeemScriptHash);
|
||||
|
||||
Integer medianBlockTime = BTC.getInstance().getMedianBlockTime();
|
||||
if (medianBlockTime == null)
|
||||
throw ApiExceptionFactory.INSTANCE.createException(request, ApiError.BTC_NETWORK_ISSUE);
|
||||
int medianBlockTime = BTC.getInstance().getMedianBlockTime();
|
||||
|
||||
long now = NTP.getTime();
|
||||
|
||||
// Check P2SH is funded
|
||||
|
||||
Long p2shBalance = BTC.getInstance().getBalance(p2shAddress.toString());
|
||||
if (p2shBalance == null)
|
||||
throw ApiExceptionFactory.INSTANCE.createException(request, ApiError.ADDRESS_UNKNOWN);
|
||||
long p2shBalance = BTC.getInstance().getConfirmedBalance(p2shAddress.toString());
|
||||
|
||||
CrossChainBitcoinP2SHStatus p2shStatus = new CrossChainBitcoinP2SHStatus();
|
||||
p2shStatus.bitcoinP2shAddress = p2shAddress.toString();
|
||||
@@ -633,6 +630,8 @@ public class CrossChainResource {
|
||||
return p2shStatus;
|
||||
} catch (DataException e) {
|
||||
throw ApiExceptionFactory.INSTANCE.createException(request, ApiError.REPOSITORY_ISSUE, e);
|
||||
} catch (BitcoinException e) {
|
||||
throw ApiExceptionFactory.INSTANCE.createException(request, ApiError.BTC_NETWORK_ISSUE);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -718,6 +717,13 @@ public class CrossChainResource {
|
||||
if (refundRequest.atAddress == null || !Crypto.isValidAtAddress(refundRequest.atAddress))
|
||||
throw ApiExceptionFactory.INSTANCE.createException(request, ApiError.INVALID_ADDRESS);
|
||||
|
||||
if (refundRequest.receivingAccountInfo == null)
|
||||
refundRequest.receivingAccountInfo = refundKey.getPubKeyHash();
|
||||
|
||||
if (refundRequest.receivingAccountInfo.length != 20)
|
||||
throw ApiExceptionFactory.INSTANCE.createException(request, ApiError.INVALID_PUBLIC_KEY);
|
||||
|
||||
|
||||
// Extract data from cross-chain trading AT
|
||||
try (final Repository repository = RepositoryManager.getRepository()) {
|
||||
ATData atData = fetchAtDataWithChecking(repository, refundRequest.atAddress);
|
||||
@@ -738,9 +744,7 @@ public class CrossChainResource {
|
||||
|
||||
// Check P2SH is funded
|
||||
|
||||
Long p2shBalance = BTC.getInstance().getBalance(p2shAddress.toString());
|
||||
if (p2shBalance == null)
|
||||
throw ApiExceptionFactory.INSTANCE.createException(request, ApiError.ADDRESS_UNKNOWN);
|
||||
long p2shBalance = BTC.getInstance().getConfirmedBalance(p2shAddress.toString());
|
||||
|
||||
List<TransactionOutput> fundingOutputs = BTC.getInstance().getUnspentOutputs(p2shAddress.toString());
|
||||
if (fundingOutputs.isEmpty())
|
||||
@@ -755,15 +759,15 @@ public class CrossChainResource {
|
||||
|
||||
Coin refundAmount = Coin.valueOf(p2shBalance - refundRequest.bitcoinMinerFee.unscaledValue().longValue());
|
||||
|
||||
org.bitcoinj.core.Transaction refundTransaction = BTCP2SH.buildRefundTransaction(refundAmount, refundKey, fundingOutputs, redeemScriptBytes, lockTime);
|
||||
boolean wasBroadcast = BTC.getInstance().broadcastTransaction(refundTransaction);
|
||||
org.bitcoinj.core.Transaction refundTransaction = BTCP2SH.buildRefundTransaction(refundAmount, refundKey, fundingOutputs, redeemScriptBytes, lockTime, refundRequest.receivingAccountInfo);
|
||||
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);
|
||||
} catch (BitcoinException e) {
|
||||
throw ApiExceptionFactory.INSTANCE.createException(request, ApiError.BTC_NETWORK_ISSUE);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -876,16 +880,12 @@ public class CrossChainResource {
|
||||
|
||||
Address p2shAddress = LegacyAddress.fromScriptHash(params, redeemScriptHash);
|
||||
|
||||
Integer medianBlockTime = BTC.getInstance().getMedianBlockTime();
|
||||
if (medianBlockTime == null)
|
||||
throw ApiExceptionFactory.INSTANCE.createException(request, ApiError.BTC_NETWORK_ISSUE);
|
||||
int medianBlockTime = BTC.getInstance().getMedianBlockTime();
|
||||
|
||||
long now = NTP.getTime();
|
||||
|
||||
// Check P2SH is funded
|
||||
Long p2shBalance = BTC.getInstance().getBalance(p2shAddress.toString());
|
||||
if (p2shBalance == null)
|
||||
throw ApiExceptionFactory.INSTANCE.createException(request, ApiError.ADDRESS_UNKNOWN);
|
||||
long p2shBalance = BTC.getInstance().getConfirmedBalance(p2shAddress.toString());
|
||||
|
||||
if (p2shBalance < crossChainTradeData.expectedBitcoin)
|
||||
throw ApiExceptionFactory.INSTANCE.createException(request, ApiError.BTC_BALANCE_ISSUE);
|
||||
@@ -901,14 +901,14 @@ public class CrossChainResource {
|
||||
Coin redeemAmount = Coin.valueOf(p2shBalance - redeemRequest.bitcoinMinerFee.unscaledValue().longValue());
|
||||
|
||||
org.bitcoinj.core.Transaction redeemTransaction = BTCP2SH.buildRedeemTransaction(redeemAmount, redeemKey, fundingOutputs, redeemScriptBytes, redeemRequest.secret, redeemRequest.receivingAccountInfo);
|
||||
boolean wasBroadcast = BTC.getInstance().broadcastTransaction(redeemTransaction);
|
||||
|
||||
if (!wasBroadcast)
|
||||
throw ApiExceptionFactory.INSTANCE.createException(request, ApiError.BTC_NETWORK_ISSUE);
|
||||
BTC.getInstance().broadcastTransaction(redeemTransaction);
|
||||
|
||||
return redeemTransaction.getTxId().toString();
|
||||
} catch (DataException e) {
|
||||
throw ApiExceptionFactory.INSTANCE.createException(request, ApiError.REPOSITORY_ISSUE, e);
|
||||
} catch (BitcoinException e) {
|
||||
throw ApiExceptionFactory.INSTANCE.createException(request, ApiError.BTC_NETWORK_ISSUE);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -993,8 +993,11 @@ public class CrossChainResource {
|
||||
if (spendTransaction == null)
|
||||
throw ApiExceptionFactory.INSTANCE.createException(request, ApiError.BTC_BALANCE_ISSUE);
|
||||
|
||||
if (!BTC.getInstance().broadcastTransaction(spendTransaction))
|
||||
try {
|
||||
BTC.getInstance().broadcastTransaction(spendTransaction);
|
||||
} catch (BitcoinException e) {
|
||||
throw ApiExceptionFactory.INSTANCE.createException(request, ApiError.BTC_NETWORK_ISSUE);
|
||||
}
|
||||
|
||||
return "true";
|
||||
}
|
||||
@@ -1223,6 +1226,10 @@ public class CrossChainResource {
|
||||
)
|
||||
@ApiErrors({ApiError.INVALID_CRITERIA, ApiError.REPOSITORY_ISSUE})
|
||||
public List<CrossChainTradeSummary> getCompletedTrades(
|
||||
@Parameter(
|
||||
description = "Only return trades that completed on/after this timestamp (milliseconds since epoch)",
|
||||
example = "1597310000000"
|
||||
) @QueryParam("minimumTimestamp") Long minimumTimestamp,
|
||||
@Parameter( ref = "limit") @QueryParam("limit") Integer limit,
|
||||
@Parameter( ref = "offset" ) @QueryParam("offset") Integer offset,
|
||||
@Parameter( ref = "reverse" ) @QueryParam("reverse") Boolean reverse) {
|
||||
@@ -1230,10 +1237,27 @@ public class CrossChainResource {
|
||||
if (limit != null && limit > 100)
|
||||
throw ApiExceptionFactory.INSTANCE.createException(request, ApiError.INVALID_CRITERIA);
|
||||
|
||||
// minimumTimestamp (if given) needs to be positive
|
||||
if (minimumTimestamp != null && minimumTimestamp <= 0)
|
||||
throw ApiExceptionFactory.INSTANCE.createException(request, ApiError.INVALID_CRITERIA);
|
||||
|
||||
final Boolean isFinished = Boolean.TRUE;
|
||||
final Integer minimumFinalHeight = null;
|
||||
|
||||
try (final Repository repository = RepositoryManager.getRepository()) {
|
||||
Integer minimumFinalHeight = null;
|
||||
|
||||
if (minimumTimestamp != null) {
|
||||
minimumFinalHeight = repository.getBlockRepository().getHeightFromTimestamp(minimumTimestamp);
|
||||
|
||||
if (minimumFinalHeight == 0)
|
||||
// We don't have any blocks since minimumTimestamp, let alone trades, so nothing to return
|
||||
return Collections.emptyList();
|
||||
|
||||
// height returned from repository is for block BEFORE timestamp
|
||||
// but we want trades AFTER timestamp so bump height accordingly
|
||||
minimumFinalHeight++;
|
||||
}
|
||||
|
||||
List<ATStateData> atStates = repository.getATRepository().getMatchingFinalATStates(BTCACCT.CODE_BYTES_HASH,
|
||||
isFinished,
|
||||
BTCACCT.MODE_BYTE_OFFSET, (long) BTCACCT.Mode.REDEEMED.value,
|
||||
|
@@ -31,6 +31,7 @@ public class ActiveChatsWebSocket extends ApiWebSocket {
|
||||
}
|
||||
|
||||
@OnWebSocketConnect
|
||||
@Override
|
||||
public void onWebSocketConnect(Session session) {
|
||||
Map<String, String> pathParams = getPathParams(session, "/{address}");
|
||||
|
||||
@@ -49,16 +50,19 @@ public class ActiveChatsWebSocket extends ApiWebSocket {
|
||||
}
|
||||
|
||||
@OnWebSocketClose
|
||||
@Override
|
||||
public void onWebSocketClose(Session session, int statusCode, String reason) {
|
||||
ChatNotifier.getInstance().deregister(session);
|
||||
}
|
||||
|
||||
@OnWebSocketError
|
||||
public void onWebSocketError(Session session, Throwable throwable) {
|
||||
/* ignored */
|
||||
}
|
||||
|
||||
@OnWebSocketMessage
|
||||
public void onWebSocketMessage(Session session, String message) {
|
||||
/* ignored */
|
||||
}
|
||||
|
||||
private void onNotify(Session session, ChatTransactionData chatTransactionData, String ourAddress, AtomicReference<String> previousOutput) {
|
||||
|
@@ -13,59 +13,87 @@ import org.eclipse.jetty.websocket.api.annotations.OnWebSocketMessage;
|
||||
import org.eclipse.jetty.websocket.api.annotations.WebSocket;
|
||||
import org.eclipse.jetty.websocket.servlet.WebSocketServletFactory;
|
||||
import org.qortal.api.model.NodeStatus;
|
||||
import org.qortal.controller.StatusNotifier;
|
||||
import org.qortal.repository.DataException;
|
||||
import org.qortal.repository.Repository;
|
||||
import org.qortal.repository.RepositoryManager;
|
||||
import org.qortal.controller.Controller;
|
||||
import org.qortal.event.Event;
|
||||
import org.qortal.event.EventBus;
|
||||
import org.qortal.event.Listener;
|
||||
|
||||
@WebSocket
|
||||
@SuppressWarnings("serial")
|
||||
public class AdminStatusWebSocket extends ApiWebSocket {
|
||||
public class AdminStatusWebSocket extends ApiWebSocket implements Listener {
|
||||
|
||||
private static final AtomicReference<String> previousOutput = new AtomicReference<>(null);
|
||||
|
||||
@Override
|
||||
public void configure(WebSocketServletFactory factory) {
|
||||
factory.register(AdminStatusWebSocket.class);
|
||||
|
||||
try {
|
||||
previousOutput.set(buildStatusString());
|
||||
} catch (IOException e) {
|
||||
// How to fail properly?
|
||||
return;
|
||||
}
|
||||
|
||||
EventBus.INSTANCE.addListener(this::listen);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void listen(Event event) {
|
||||
if (!(event instanceof Controller.StatusChangeEvent))
|
||||
return;
|
||||
|
||||
String newOutput;
|
||||
try {
|
||||
newOutput = buildStatusString();
|
||||
} catch (IOException e) {
|
||||
// Ignore this time?
|
||||
return;
|
||||
}
|
||||
|
||||
if (previousOutput.getAndUpdate(currentValue -> newOutput).equals(newOutput))
|
||||
// Output hasn't changed, so don't send anything
|
||||
return;
|
||||
|
||||
for (Session session : getSessions())
|
||||
this.sendStatus(session, newOutput);
|
||||
}
|
||||
|
||||
@OnWebSocketConnect
|
||||
@Override
|
||||
public void onWebSocketConnect(Session session) {
|
||||
AtomicReference<String> previousOutput = new AtomicReference<>(null);
|
||||
this.sendStatus(session, previousOutput.get());
|
||||
|
||||
StatusNotifier.Listener listener = timestamp -> onNotify(session, previousOutput);
|
||||
StatusNotifier.getInstance().register(session, listener);
|
||||
|
||||
this.onNotify(session, previousOutput);
|
||||
super.onWebSocketConnect(session);
|
||||
}
|
||||
|
||||
@OnWebSocketClose
|
||||
@Override
|
||||
public void onWebSocketClose(Session session, int statusCode, String reason) {
|
||||
StatusNotifier.getInstance().deregister(session);
|
||||
super.onWebSocketClose(session, statusCode, reason);
|
||||
}
|
||||
|
||||
@OnWebSocketError
|
||||
public void onWebSocketError(Session session, Throwable throwable) {
|
||||
/* We ignore errors for now, but method here to silence log spam */
|
||||
}
|
||||
|
||||
@OnWebSocketMessage
|
||||
public void onWebSocketMessage(Session session, String message) {
|
||||
/* ignored */
|
||||
}
|
||||
|
||||
private void onNotify(Session session,AtomicReference<String> previousOutput) {
|
||||
try (final Repository repository = RepositoryManager.getRepository()) {
|
||||
NodeStatus nodeStatus = new NodeStatus();
|
||||
private static String buildStatusString() throws IOException {
|
||||
NodeStatus nodeStatus = new NodeStatus();
|
||||
StringWriter stringWriter = new StringWriter();
|
||||
marshall(stringWriter, nodeStatus);
|
||||
return stringWriter.toString();
|
||||
}
|
||||
|
||||
StringWriter stringWriter = new StringWriter();
|
||||
|
||||
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) {
|
||||
private void sendStatus(Session session, String status) {
|
||||
try {
|
||||
session.getRemote().sendStringByFuture(status);
|
||||
} catch (WebSocketException e) {
|
||||
// No output this time?
|
||||
}
|
||||
}
|
||||
|
@@ -14,7 +14,11 @@ import org.eclipse.jetty.websocket.api.annotations.WebSocket;
|
||||
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.controller.Controller;
|
||||
import org.qortal.data.block.BlockData;
|
||||
import org.qortal.event.Event;
|
||||
import org.qortal.event.EventBus;
|
||||
import org.qortal.event.Listener;
|
||||
import org.qortal.repository.DataException;
|
||||
import org.qortal.repository.Repository;
|
||||
import org.qortal.repository.RepositoryManager;
|
||||
@@ -22,26 +26,42 @@ import org.qortal.utils.Base58;
|
||||
|
||||
@WebSocket
|
||||
@SuppressWarnings("serial")
|
||||
public class BlocksWebSocket extends ApiWebSocket {
|
||||
public class BlocksWebSocket extends ApiWebSocket implements Listener {
|
||||
|
||||
@Override
|
||||
public void configure(WebSocketServletFactory factory) {
|
||||
factory.register(BlocksWebSocket.class);
|
||||
|
||||
EventBus.INSTANCE.addListener(this::listen);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void listen(Event event) {
|
||||
if (!(event instanceof Controller.NewBlockEvent))
|
||||
return;
|
||||
|
||||
BlockData blockData = ((Controller.NewBlockEvent) event).getBlockData();
|
||||
BlockInfo blockInfo = new BlockInfo(blockData);
|
||||
|
||||
for (Session session : getSessions())
|
||||
sendBlockInfo(session, blockInfo);
|
||||
}
|
||||
|
||||
@OnWebSocketConnect
|
||||
@Override
|
||||
public void onWebSocketConnect(Session session) {
|
||||
BlockNotifier.Listener listener = blockInfo -> onNotify(session, blockInfo);
|
||||
BlockNotifier.getInstance().register(session, listener);
|
||||
super.onWebSocketConnect(session);
|
||||
}
|
||||
|
||||
@OnWebSocketClose
|
||||
@Override
|
||||
public void onWebSocketClose(Session session, int statusCode, String reason) {
|
||||
BlockNotifier.getInstance().deregister(session);
|
||||
super.onWebSocketClose(session, statusCode, reason);
|
||||
}
|
||||
|
||||
@OnWebSocketError
|
||||
public void onWebSocketError(Session session, Throwable throwable) {
|
||||
/* We ignore errors for now, but method here to silence log spam */
|
||||
}
|
||||
|
||||
@OnWebSocketMessage
|
||||
@@ -71,7 +91,7 @@ public class BlocksWebSocket extends ApiWebSocket {
|
||||
return;
|
||||
}
|
||||
|
||||
onNotify(session, blockInfos.get(0));
|
||||
sendBlockInfo(session, blockInfos.get(0));
|
||||
} catch (DataException e) {
|
||||
sendError(session, ApiError.REPOSITORY_ISSUE);
|
||||
}
|
||||
@@ -100,13 +120,13 @@ public class BlocksWebSocket extends ApiWebSocket {
|
||||
return;
|
||||
}
|
||||
|
||||
onNotify(session, blockInfos.get(0));
|
||||
sendBlockInfo(session, blockInfos.get(0));
|
||||
} catch (DataException e) {
|
||||
sendError(session, ApiError.REPOSITORY_ISSUE);
|
||||
}
|
||||
}
|
||||
|
||||
private void onNotify(Session session, BlockInfo blockInfo) {
|
||||
private void sendBlockInfo(Session session, BlockInfo blockInfo) {
|
||||
StringWriter stringWriter = new StringWriter();
|
||||
|
||||
try {
|
||||
|
@@ -32,6 +32,7 @@ public class ChatMessagesWebSocket extends ApiWebSocket {
|
||||
}
|
||||
|
||||
@OnWebSocketConnect
|
||||
@Override
|
||||
public void onWebSocketConnect(Session session) {
|
||||
Map<String, List<String>> queryParams = session.getUpgradeRequest().getParameterMap();
|
||||
|
||||
@@ -86,16 +87,19 @@ public class ChatMessagesWebSocket extends ApiWebSocket {
|
||||
}
|
||||
|
||||
@OnWebSocketClose
|
||||
@Override
|
||||
public void onWebSocketClose(Session session, int statusCode, String reason) {
|
||||
ChatNotifier.getInstance().deregister(session);
|
||||
}
|
||||
|
||||
@OnWebSocketError
|
||||
public void onWebSocketError(Session session, Throwable throwable) {
|
||||
/* ignored */
|
||||
}
|
||||
|
||||
@OnWebSocketMessage
|
||||
public void onWebSocketMessage(Session session, String message) {
|
||||
/* ignored */
|
||||
}
|
||||
|
||||
private void onNotify(Session session, ChatTransactionData chatTransactionData, int txGroupId) {
|
||||
|
@@ -11,6 +11,7 @@ import java.util.stream.Collectors;
|
||||
import org.eclipse.jetty.websocket.api.Session;
|
||||
import org.eclipse.jetty.websocket.api.annotations.OnWebSocketClose;
|
||||
import org.eclipse.jetty.websocket.api.annotations.OnWebSocketConnect;
|
||||
import org.eclipse.jetty.websocket.api.annotations.OnWebSocketError;
|
||||
import org.eclipse.jetty.websocket.api.annotations.OnWebSocketMessage;
|
||||
import org.eclipse.jetty.websocket.api.annotations.WebSocket;
|
||||
import org.eclipse.jetty.websocket.servlet.WebSocketServletFactory;
|
||||
@@ -71,6 +72,7 @@ public class TradeBotWebSocket extends ApiWebSocket implements Listener {
|
||||
}
|
||||
|
||||
@OnWebSocketConnect
|
||||
@Override
|
||||
public void onWebSocketConnect(Session session) {
|
||||
// Send all known trade-bot entries
|
||||
try (final Repository repository = RepositoryManager.getRepository()) {
|
||||
@@ -92,10 +94,16 @@ public class TradeBotWebSocket extends ApiWebSocket implements Listener {
|
||||
}
|
||||
|
||||
@OnWebSocketClose
|
||||
@Override
|
||||
public void onWebSocketClose(Session session, int statusCode, String reason) {
|
||||
super.onWebSocketClose(session, statusCode, reason);
|
||||
}
|
||||
|
||||
@OnWebSocketError
|
||||
public void onWebSocketError(Session session, Throwable throwable) {
|
||||
/* ignored */
|
||||
}
|
||||
|
||||
@OnWebSocketMessage
|
||||
public void onWebSocketMessage(Session session, String message) {
|
||||
/* ignored */
|
||||
|
@@ -6,20 +6,27 @@ import java.util.ArrayList;
|
||||
import java.util.HashMap;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.function.Predicate;
|
||||
import java.util.stream.Collectors;
|
||||
|
||||
import org.apache.logging.log4j.LogManager;
|
||||
import org.apache.logging.log4j.Logger;
|
||||
import org.eclipse.jetty.websocket.api.Session;
|
||||
import org.eclipse.jetty.websocket.api.annotations.OnWebSocketClose;
|
||||
import org.eclipse.jetty.websocket.api.annotations.OnWebSocketConnect;
|
||||
import org.eclipse.jetty.websocket.api.annotations.OnWebSocketError;
|
||||
import org.eclipse.jetty.websocket.api.annotations.OnWebSocketMessage;
|
||||
import org.eclipse.jetty.websocket.api.annotations.WebSocket;
|
||||
import org.eclipse.jetty.websocket.servlet.WebSocketServletFactory;
|
||||
import org.qortal.api.model.BlockInfo;
|
||||
import org.qortal.api.model.CrossChainOfferSummary;
|
||||
import org.qortal.controller.BlockNotifier;
|
||||
import org.qortal.controller.Controller;
|
||||
import org.qortal.crosschain.BTCACCT;
|
||||
import org.qortal.data.at.ATStateData;
|
||||
import org.qortal.data.block.BlockData;
|
||||
import org.qortal.data.crosschain.CrossChainTradeData;
|
||||
import org.qortal.event.Event;
|
||||
import org.qortal.event.EventBus;
|
||||
import org.qortal.event.Listener;
|
||||
import org.qortal.repository.DataException;
|
||||
import org.qortal.repository.Repository;
|
||||
import org.qortal.repository.RepositoryManager;
|
||||
@@ -27,120 +34,55 @@ import org.qortal.utils.NTP;
|
||||
|
||||
@WebSocket
|
||||
@SuppressWarnings("serial")
|
||||
public class TradeOffersWebSocket extends ApiWebSocket {
|
||||
public class TradeOffersWebSocket extends ApiWebSocket implements Listener {
|
||||
|
||||
private static final Logger LOGGER = LogManager.getLogger(TradeOffersWebSocket.class);
|
||||
|
||||
private static final Map<String, BTCACCT.Mode> previousAtModes = new HashMap<>();
|
||||
|
||||
// OFFERING
|
||||
private static final Map<String, CrossChainOfferSummary> currentSummaries = new HashMap<>();
|
||||
// REDEEMED/REFUNDED/CANCELLED
|
||||
private static final Map<String, CrossChainOfferSummary> historicSummaries = new HashMap<>();
|
||||
|
||||
private static final Predicate<CrossChainOfferSummary> isHistoric = offerSummary
|
||||
-> offerSummary.getMode() == BTCACCT.Mode.REDEEMED
|
||||
|| offerSummary.getMode() == BTCACCT.Mode.REFUNDED
|
||||
|| offerSummary.getMode() == BTCACCT.Mode.CANCELLED;
|
||||
|
||||
|
||||
@Override
|
||||
public void configure(WebSocketServletFactory factory) {
|
||||
factory.register(TradeOffersWebSocket.class);
|
||||
|
||||
try (final Repository repository = RepositoryManager.getRepository()) {
|
||||
populateCurrentSummaries(repository);
|
||||
|
||||
populateHistoricSummaries(repository);
|
||||
} catch (DataException e) {
|
||||
// How to fail properly?
|
||||
return;
|
||||
}
|
||||
|
||||
EventBus.INSTANCE.addListener(this::listen);
|
||||
}
|
||||
|
||||
@OnWebSocketConnect
|
||||
public void onWebSocketConnect(Session session) {
|
||||
Map<String, List<String>> queryParams = session.getUpgradeRequest().getParameterMap();
|
||||
@Override
|
||||
public void listen(Event event) {
|
||||
if (!(event instanceof Controller.NewBlockEvent))
|
||||
return;
|
||||
|
||||
final boolean includeHistoric = queryParams.get("includeHistoric") != null;
|
||||
final Map<String, BTCACCT.Mode> previousAtModes = new HashMap<>();
|
||||
BlockData blockData = ((Controller.NewBlockEvent) event).getBlockData();
|
||||
|
||||
// Process any new info
|
||||
List<CrossChainOfferSummary> crossChainOfferSummaries;
|
||||
|
||||
try (final Repository repository = RepositoryManager.getRepository()) {
|
||||
List<ATStateData> initialAtStates;
|
||||
|
||||
// We want ALL OFFERING trades
|
||||
Boolean isFinished = Boolean.FALSE;
|
||||
Integer dataByteOffset = BTCACCT.MODE_BYTE_OFFSET;
|
||||
Long expectedValue = (long) BTCACCT.Mode.OFFERING.value;
|
||||
Integer minimumFinalHeight = null;
|
||||
|
||||
initialAtStates = repository.getATRepository().getMatchingFinalATStates(BTCACCT.CODE_BYTES_HASH,
|
||||
isFinished, dataByteOffset, expectedValue, minimumFinalHeight,
|
||||
null, null, null);
|
||||
|
||||
if (initialAtStates == null) {
|
||||
session.close(4001, "repository issue fetching OFFERING trades");
|
||||
return;
|
||||
}
|
||||
|
||||
// Save initial AT modes
|
||||
previousAtModes.putAll(initialAtStates.stream().collect(Collectors.toMap(ATStateData::getATAddress, atState -> BTCACCT.Mode.OFFERING)));
|
||||
|
||||
// Convert to offer summaries
|
||||
crossChainOfferSummaries = produceSummaries(repository, initialAtStates, null);
|
||||
|
||||
if (includeHistoric) {
|
||||
// We also want REDEEMED/REFUNDED/CANCELLED trades over the last 24 hours
|
||||
long timestamp = NTP.getTime() - 24 * 60 * 60 * 1000L;
|
||||
minimumFinalHeight = repository.getBlockRepository().getHeightFromTimestamp(timestamp);
|
||||
|
||||
if (minimumFinalHeight != 0) {
|
||||
isFinished = Boolean.TRUE;
|
||||
dataByteOffset = null;
|
||||
expectedValue = null;
|
||||
++minimumFinalHeight; // because height is just *before* timestamp
|
||||
|
||||
List<ATStateData> historicAtStates = repository.getATRepository().getMatchingFinalATStates(BTCACCT.CODE_BYTES_HASH,
|
||||
isFinished, dataByteOffset, expectedValue, minimumFinalHeight,
|
||||
null, null, null);
|
||||
|
||||
if (historicAtStates == null) {
|
||||
session.close(4002, "repository issue fetching historic trades");
|
||||
return;
|
||||
}
|
||||
|
||||
for (ATStateData historicAtState : historicAtStates) {
|
||||
CrossChainOfferSummary historicOfferSummary = produceSummary(repository, historicAtState, null);
|
||||
|
||||
switch (historicOfferSummary.getMode()) {
|
||||
case REDEEMED:
|
||||
case REFUNDED:
|
||||
case CANCELLED:
|
||||
break;
|
||||
|
||||
default:
|
||||
continue;
|
||||
}
|
||||
|
||||
// Add summary to initial burst
|
||||
crossChainOfferSummaries.add(historicOfferSummary);
|
||||
|
||||
// Save initial AT mode
|
||||
previousAtModes.put(historicAtState.getATAddress(), historicOfferSummary.getMode());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
} catch (DataException e) {
|
||||
session.close(4003, "generic repository issue");
|
||||
return;
|
||||
}
|
||||
|
||||
if (!sendOfferSummaries(session, crossChainOfferSummaries)) {
|
||||
session.close(4004, "websocket issue");
|
||||
return;
|
||||
}
|
||||
|
||||
BlockNotifier.Listener listener = blockInfo -> onNotify(session, blockInfo, previousAtModes);
|
||||
BlockNotifier.getInstance().register(session, listener);
|
||||
}
|
||||
|
||||
@OnWebSocketClose
|
||||
public void onWebSocketClose(Session session, int statusCode, String reason) {
|
||||
BlockNotifier.getInstance().deregister(session);
|
||||
}
|
||||
|
||||
@OnWebSocketMessage
|
||||
public void onWebSocketMessage(Session session, String message) {
|
||||
/* ignored */
|
||||
}
|
||||
|
||||
private void onNotify(Session session, BlockInfo blockInfo, final Map<String, BTCACCT.Mode> previousAtModes) {
|
||||
List<CrossChainOfferSummary> crossChainOfferSummaries = null;
|
||||
|
||||
try (final Repository repository = RepositoryManager.getRepository()) {
|
||||
// Find any new trade ATs since this block
|
||||
// Find any new/changed trade ATs since this block
|
||||
final Boolean isFinished = null;
|
||||
final Integer dataByteOffset = null;
|
||||
final Long expectedValue = null;
|
||||
final Integer minimumFinalHeight = blockInfo.getHeight();
|
||||
final Integer minimumFinalHeight = blockData.getHeight();
|
||||
|
||||
List<ATStateData> atStates = repository.getATRepository().getMatchingFinalATStates(BTCACCT.CODE_BYTES_HASH,
|
||||
isFinished, dataByteOffset, expectedValue, minimumFinalHeight,
|
||||
@@ -149,12 +91,13 @@ public class TradeOffersWebSocket extends ApiWebSocket {
|
||||
if (atStates == null)
|
||||
return;
|
||||
|
||||
crossChainOfferSummaries = produceSummaries(repository, atStates, blockInfo.getTimestamp());
|
||||
crossChainOfferSummaries = produceSummaries(repository, atStates, blockData.getTimestamp());
|
||||
} catch (DataException e) {
|
||||
// No output this time
|
||||
return;
|
||||
}
|
||||
|
||||
synchronized (previousAtModes) { //NOSONAR squid:S2445 suppressed because previousAtModes is final and curried in lambda
|
||||
synchronized (previousAtModes) {
|
||||
// Remove any entries unchanged from last time
|
||||
crossChainOfferSummaries.removeIf(offerSummary -> previousAtModes.get(offerSummary.getQortalAtAddress()) == offerSummary.getMode());
|
||||
|
||||
@@ -162,13 +105,78 @@ public class TradeOffersWebSocket extends ApiWebSocket {
|
||||
if (crossChainOfferSummaries.isEmpty())
|
||||
return;
|
||||
|
||||
final boolean wasSent = sendOfferSummaries(session, crossChainOfferSummaries);
|
||||
// Update
|
||||
for (CrossChainOfferSummary offerSummary : crossChainOfferSummaries) {
|
||||
previousAtModes.put(offerSummary.qortalAtAddress, offerSummary.getMode());
|
||||
LOGGER.trace(() -> String.format("Block height: %d, AT: %s, mode: %s", blockData.getHeight(), offerSummary.qortalAtAddress, offerSummary.getMode().name()));
|
||||
|
||||
if (!wasSent)
|
||||
return;
|
||||
switch (offerSummary.getMode()) {
|
||||
case OFFERING:
|
||||
currentSummaries.put(offerSummary.qortalAtAddress, offerSummary);
|
||||
historicSummaries.remove(offerSummary.qortalAtAddress);
|
||||
break;
|
||||
|
||||
previousAtModes.putAll(crossChainOfferSummaries.stream().collect(Collectors.toMap(CrossChainOfferSummary::getQortalAtAddress, CrossChainOfferSummary::getMode)));
|
||||
case REDEEMED:
|
||||
case REFUNDED:
|
||||
case CANCELLED:
|
||||
currentSummaries.remove(offerSummary.qortalAtAddress);
|
||||
historicSummaries.put(offerSummary.qortalAtAddress, offerSummary);
|
||||
break;
|
||||
|
||||
case TRADING:
|
||||
currentSummaries.remove(offerSummary.qortalAtAddress);
|
||||
historicSummaries.remove(offerSummary.qortalAtAddress);
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
// Remove any historic offers that are over 24 hours old
|
||||
final long tooOldTimestamp = NTP.getTime() - 24 * 60 * 60 * 1000L;
|
||||
historicSummaries.values().removeIf(historicSummary -> historicSummary.getTimestamp() < tooOldTimestamp);
|
||||
}
|
||||
|
||||
// Notify sessions
|
||||
for (Session session : getSessions())
|
||||
sendOfferSummaries(session, crossChainOfferSummaries);
|
||||
}
|
||||
|
||||
@OnWebSocketConnect
|
||||
@Override
|
||||
public void onWebSocketConnect(Session session) {
|
||||
Map<String, List<String>> queryParams = session.getUpgradeRequest().getParameterMap();
|
||||
final boolean includeHistoric = queryParams.get("includeHistoric") != null;
|
||||
|
||||
List<CrossChainOfferSummary> crossChainOfferSummaries = new ArrayList<>();
|
||||
|
||||
synchronized (previousAtModes) {
|
||||
crossChainOfferSummaries.addAll(currentSummaries.values());
|
||||
|
||||
if (includeHistoric)
|
||||
crossChainOfferSummaries.addAll(historicSummaries.values());
|
||||
}
|
||||
|
||||
if (!sendOfferSummaries(session, crossChainOfferSummaries)) {
|
||||
session.close(4002, "websocket issue");
|
||||
return;
|
||||
}
|
||||
|
||||
super.onWebSocketConnect(session);
|
||||
}
|
||||
|
||||
@OnWebSocketClose
|
||||
@Override
|
||||
public void onWebSocketClose(Session session, int statusCode, String reason) {
|
||||
super.onWebSocketClose(session, statusCode, reason);
|
||||
}
|
||||
|
||||
@OnWebSocketError
|
||||
public void onWebSocketError(Session session, Throwable throwable) {
|
||||
/* ignored */
|
||||
}
|
||||
|
||||
@OnWebSocketMessage
|
||||
public void onWebSocketMessage(Session session, String message) {
|
||||
/* ignored */
|
||||
}
|
||||
|
||||
private boolean sendOfferSummaries(Session session, List<CrossChainOfferSummary> crossChainOfferSummaries) {
|
||||
@@ -186,6 +194,61 @@ public class TradeOffersWebSocket extends ApiWebSocket {
|
||||
return true;
|
||||
}
|
||||
|
||||
private static void populateCurrentSummaries(Repository repository) throws DataException {
|
||||
// We want ALL OFFERING trades
|
||||
Boolean isFinished = Boolean.FALSE;
|
||||
Integer dataByteOffset = BTCACCT.MODE_BYTE_OFFSET;
|
||||
Long expectedValue = (long) BTCACCT.Mode.OFFERING.value;
|
||||
Integer minimumFinalHeight = null;
|
||||
|
||||
List<ATStateData> initialAtStates = repository.getATRepository().getMatchingFinalATStates(BTCACCT.CODE_BYTES_HASH,
|
||||
isFinished, dataByteOffset, expectedValue, minimumFinalHeight,
|
||||
null, null, null);
|
||||
|
||||
if (initialAtStates == null)
|
||||
throw new DataException("Couldn't fetch current trades from repository");
|
||||
|
||||
// Save initial AT modes
|
||||
previousAtModes.putAll(initialAtStates.stream().collect(Collectors.toMap(ATStateData::getATAddress, atState -> BTCACCT.Mode.OFFERING)));
|
||||
|
||||
// Convert to offer summaries
|
||||
currentSummaries.putAll(produceSummaries(repository, initialAtStates, null).stream().collect(Collectors.toMap(CrossChainOfferSummary::getQortalAtAddress, offerSummary -> offerSummary)));
|
||||
}
|
||||
|
||||
private static void populateHistoricSummaries(Repository repository) throws DataException {
|
||||
// We want REDEEMED/REFUNDED/CANCELLED trades over the last 24 hours
|
||||
long timestamp = System.currentTimeMillis() - 24 * 60 * 60 * 1000L;
|
||||
int minimumFinalHeight = repository.getBlockRepository().getHeightFromTimestamp(timestamp);
|
||||
|
||||
if (minimumFinalHeight == 0)
|
||||
throw new DataException("Couldn't fetch block timestamp from repository");
|
||||
|
||||
Boolean isFinished = Boolean.TRUE;
|
||||
Integer dataByteOffset = null;
|
||||
Long expectedValue = null;
|
||||
++minimumFinalHeight; // because height is just *before* timestamp
|
||||
|
||||
List<ATStateData> historicAtStates = repository.getATRepository().getMatchingFinalATStates(BTCACCT.CODE_BYTES_HASH,
|
||||
isFinished, dataByteOffset, expectedValue, minimumFinalHeight,
|
||||
null, null, null);
|
||||
|
||||
if (historicAtStates == null)
|
||||
throw new DataException("Couldn't fetch historic trades from repository");
|
||||
|
||||
for (ATStateData historicAtState : historicAtStates) {
|
||||
CrossChainOfferSummary historicOfferSummary = produceSummary(repository, historicAtState, null);
|
||||
|
||||
if (!isHistoric.test(historicOfferSummary))
|
||||
continue;
|
||||
|
||||
// Add summary to initial burst
|
||||
historicSummaries.put(historicOfferSummary.getQortalAtAddress(), historicOfferSummary);
|
||||
|
||||
// Save initial AT mode
|
||||
previousAtModes.put(historicOfferSummary.getQortalAtAddress(), historicOfferSummary.getMode());
|
||||
}
|
||||
}
|
||||
|
||||
private static CrossChainOfferSummary produceSummary(Repository repository, ATStateData atState, Long timestamp) throws DataException {
|
||||
CrossChainTradeData crossChainTradeData = BTCACCT.populateTradeData(repository, atState);
|
||||
|
||||
@@ -193,7 +256,7 @@ public class TradeOffersWebSocket extends ApiWebSocket {
|
||||
|
||||
if (crossChainTradeData.mode == BTCACCT.Mode.OFFERING)
|
||||
// We want when trade was created, not when it was last updated
|
||||
atStateTimestamp = atState.getCreation();
|
||||
atStateTimestamp = crossChainTradeData.creationTimestamp;
|
||||
else
|
||||
atStateTimestamp = timestamp != null ? timestamp : repository.getBlockRepository().getTimestampFromHeight(atState.getHeight());
|
||||
|
||||
|
@@ -51,16 +51,17 @@ public class AT {
|
||||
|
||||
MachineState machineState = new MachineState(api, loggerFactory, deployATTransactionData.getCreationBytes());
|
||||
|
||||
byte[] codeHash = Crypto.digest(machineState.getCodeBytes());
|
||||
byte[] codeBytes = machineState.getCodeBytes();
|
||||
byte[] codeHash = Crypto.digest(codeBytes);
|
||||
|
||||
this.atData = new ATData(atAddress, creatorPublicKey, creation, machineState.version, assetId, machineState.getCodeBytes(), codeHash,
|
||||
this.atData = new ATData(atAddress, creatorPublicKey, creation, machineState.version, assetId, codeBytes, codeHash,
|
||||
machineState.isSleeping(), machineState.getSleepUntilHeight(), machineState.isFinished(), machineState.hadFatalError(),
|
||||
machineState.isFrozen(), machineState.getFrozenBalance());
|
||||
|
||||
byte[] stateData = machineState.toBytes();
|
||||
byte[] stateHash = Crypto.digest(stateData);
|
||||
|
||||
this.atStateData = new ATStateData(atAddress, height, creation, stateData, stateHash, 0L, true);
|
||||
this.atStateData = new ATStateData(atAddress, height, stateData, stateHash, 0L, true);
|
||||
}
|
||||
|
||||
// Getters / setters
|
||||
@@ -106,12 +107,11 @@ public class AT {
|
||||
throw new DataException(String.format("Uncaught exception while running AT '%s'", atAddress), e);
|
||||
}
|
||||
|
||||
long creation = this.atData.getCreation();
|
||||
byte[] stateData = state.toBytes();
|
||||
byte[] stateHash = Crypto.digest(stateData);
|
||||
long atFees = api.calcFinalFees(state);
|
||||
|
||||
this.atStateData = new ATStateData(atAddress, blockHeight, creation, stateData, stateHash, atFees, false);
|
||||
this.atStateData = new ATStateData(atAddress, blockHeight, stateData, stateHash, atFees, false);
|
||||
|
||||
return api.getTransactions();
|
||||
}
|
||||
|
@@ -29,6 +29,7 @@ import org.qortal.controller.Controller;
|
||||
import org.qortal.crypto.Crypto;
|
||||
import org.qortal.data.account.AccountBalanceData;
|
||||
import org.qortal.data.account.AccountData;
|
||||
import org.qortal.data.account.EligibleQoraHolderData;
|
||||
import org.qortal.data.account.QortFromQoraData;
|
||||
import org.qortal.data.account.RewardShareData;
|
||||
import org.qortal.data.at.ATData;
|
||||
@@ -53,7 +54,6 @@ import org.qortal.transform.transaction.TransactionTransformer;
|
||||
import org.qortal.utils.Amounts;
|
||||
import org.qortal.utils.Base58;
|
||||
import org.qortal.utils.NTP;
|
||||
import org.roaringbitmap.IntIterator;
|
||||
|
||||
import com.google.common.primitives.Bytes;
|
||||
import com.google.common.primitives.Longs;
|
||||
@@ -128,7 +128,7 @@ public class Block {
|
||||
|
||||
@FunctionalInterface
|
||||
private interface BlockRewardDistributor {
|
||||
long distribute(long amount, Map<Account, Long> balanceChanges) throws DataException;
|
||||
long distribute(long amount, Map<String, Long> balanceChanges) throws DataException;
|
||||
}
|
||||
|
||||
/** Lazy-instantiated expanded info on block's online accounts. */
|
||||
@@ -144,8 +144,8 @@ public class Block {
|
||||
private final Account recipientAccount;
|
||||
private final AccountData recipientAccountData;
|
||||
|
||||
ExpandedAccount(Repository repository, int accountIndex) throws DataException {
|
||||
this.rewardShareData = repository.getAccountRepository().getRewardShareByIndex(accountIndex);
|
||||
ExpandedAccount(Repository repository, RewardShareData rewardShareData) throws DataException {
|
||||
this.rewardShareData = rewardShareData;
|
||||
this.sharePercent = this.rewardShareData.getSharePercent();
|
||||
|
||||
this.mintingAccount = new Account(repository, this.rewardShareData.getMinter());
|
||||
@@ -188,12 +188,12 @@ public class Block {
|
||||
return shareBinsByLevel[accountLevel];
|
||||
}
|
||||
|
||||
public long distribute(long accountAmount, Map<Account, Long> balanceChanges) {
|
||||
public long distribute(long accountAmount, Map<String, Long> balanceChanges) {
|
||||
if (this.isRecipientAlsoMinter) {
|
||||
// minter & recipient the same - simpler case
|
||||
LOGGER.trace(() -> String.format("Minter/recipient account %s share: %s", this.mintingAccount.getAddress(), Amounts.prettyAmount(accountAmount)));
|
||||
if (accountAmount != 0)
|
||||
balanceChanges.merge(this.mintingAccount, accountAmount, Long::sum);
|
||||
balanceChanges.merge(this.mintingAccount.getAddress(), accountAmount, Long::sum);
|
||||
} else {
|
||||
// minter & recipient different - extra work needed
|
||||
long recipientAmount = (accountAmount * this.sharePercent) / 100L / 100L; // because scaled by 2dp and 'percent' means "per 100"
|
||||
@@ -201,11 +201,11 @@ public class Block {
|
||||
|
||||
LOGGER.trace(() -> String.format("Minter account %s share: %s", this.mintingAccount.getAddress(), Amounts.prettyAmount(minterAmount)));
|
||||
if (minterAmount != 0)
|
||||
balanceChanges.merge(this.mintingAccount, minterAmount, Long::sum);
|
||||
balanceChanges.merge(this.mintingAccount.getAddress(), minterAmount, Long::sum);
|
||||
|
||||
LOGGER.trace(() -> String.format("Recipient account %s share: %s", this.recipientAccount.getAddress(), Amounts.prettyAmount(recipientAmount)));
|
||||
if (recipientAmount != 0)
|
||||
balanceChanges.merge(this.recipientAccount, recipientAmount, Long::sum);
|
||||
balanceChanges.merge(this.recipientAccount.getAddress(), recipientAmount, Long::sum);
|
||||
}
|
||||
|
||||
// We always distribute all of the amount
|
||||
@@ -217,6 +217,8 @@ public class Block {
|
||||
|
||||
/** Opportunistic cache of this block's valid online accounts. Only created by call to isValid(). */
|
||||
private List<OnlineAccountData> cachedValidOnlineAccounts = null;
|
||||
/** Opportunistic cache of this block's valid online reward-shares. Only created by call to isValid(). */
|
||||
private List<RewardShareData> cachedOnlineRewardShares = null;
|
||||
|
||||
// Other useful constants
|
||||
|
||||
@@ -567,22 +569,28 @@ public class Block {
|
||||
/**
|
||||
* Return expanded info on block's online accounts.
|
||||
* <p>
|
||||
* Typically called as part of Block.process() or Block.orphan()
|
||||
* so ideally after any calls to Block.isValid().
|
||||
*
|
||||
* @throws DataException
|
||||
*/
|
||||
public List<ExpandedAccount> getExpandedAccounts() throws DataException {
|
||||
if (this.cachedExpandedAccounts != null)
|
||||
return this.cachedExpandedAccounts;
|
||||
|
||||
ConciseSet accountIndexes = BlockTransformer.decodeOnlineAccounts(this.blockData.getEncodedOnlineAccounts());
|
||||
// We might already have a cache of online, reward-shares thanks to isValid()
|
||||
if (this.cachedOnlineRewardShares == null) {
|
||||
ConciseSet accountIndexes = BlockTransformer.decodeOnlineAccounts(this.blockData.getEncodedOnlineAccounts());
|
||||
this.cachedOnlineRewardShares = repository.getAccountRepository().getRewardSharesByIndexes(accountIndexes.toArray());
|
||||
|
||||
if (this.cachedOnlineRewardShares == null)
|
||||
throw new DataException("Online accounts invalid?");
|
||||
}
|
||||
|
||||
List<ExpandedAccount> expandedAccounts = new ArrayList<>();
|
||||
|
||||
IntIterator iterator = accountIndexes.iterator();
|
||||
while (iterator.hasNext()) {
|
||||
int accountIndex = iterator.next();
|
||||
|
||||
ExpandedAccount accountInfo = new ExpandedAccount(repository, accountIndex);
|
||||
expandedAccounts.add(accountInfo);
|
||||
}
|
||||
for (RewardShareData rewardShare : this.cachedOnlineRewardShares)
|
||||
expandedAccounts.add(new ExpandedAccount(repository, rewardShare));
|
||||
|
||||
this.cachedExpandedAccounts = expandedAccounts;
|
||||
|
||||
@@ -917,19 +925,9 @@ public class Block {
|
||||
if (accountIndexes.size() != this.blockData.getOnlineAccountsCount())
|
||||
return ValidationResult.ONLINE_ACCOUNTS_INVALID;
|
||||
|
||||
List<RewardShareData> expandedAccounts = new ArrayList<>();
|
||||
|
||||
IntIterator iterator = accountIndexes.iterator();
|
||||
while (iterator.hasNext()) {
|
||||
int accountIndex = iterator.next();
|
||||
RewardShareData rewardShareData = repository.getAccountRepository().getRewardShareByIndex(accountIndex);
|
||||
|
||||
// Check that claimed online account actually exists
|
||||
if (rewardShareData == null)
|
||||
return ValidationResult.ONLINE_ACCOUNT_UNKNOWN;
|
||||
|
||||
expandedAccounts.add(rewardShareData);
|
||||
}
|
||||
List<RewardShareData> onlineRewardShares = repository.getAccountRepository().getRewardSharesByIndexes(accountIndexes.toArray());
|
||||
if (onlineRewardShares == null)
|
||||
return ValidationResult.ONLINE_ACCOUNT_UNKNOWN;
|
||||
|
||||
// If block is past a certain age then we simply assume the signatures were correct
|
||||
long signatureRequirementThreshold = NTP.getTime() - BlockChain.getInstance().getOnlineAccountSignaturesMinLifetime();
|
||||
@@ -939,7 +937,7 @@ public class Block {
|
||||
if (this.blockData.getOnlineAccountsSignatures() == null || this.blockData.getOnlineAccountsSignatures().length == 0)
|
||||
return ValidationResult.ONLINE_ACCOUNT_SIGNATURES_MISSING;
|
||||
|
||||
if (this.blockData.getOnlineAccountsSignatures().length != expandedAccounts.size() * Transformer.SIGNATURE_LENGTH)
|
||||
if (this.blockData.getOnlineAccountsSignatures().length != onlineRewardShares.size() * Transformer.SIGNATURE_LENGTH)
|
||||
return ValidationResult.ONLINE_ACCOUNT_SIGNATURES_MALFORMED;
|
||||
|
||||
// Check signatures
|
||||
@@ -961,7 +959,7 @@ public class Block {
|
||||
|
||||
for (int i = 0; i < onlineAccountsSignatures.size(); ++i) {
|
||||
byte[] signature = onlineAccountsSignatures.get(i);
|
||||
byte[] publicKey = expandedAccounts.get(i).getRewardSharePublicKey();
|
||||
byte[] publicKey = onlineRewardShares.get(i).getRewardSharePublicKey();
|
||||
|
||||
OnlineAccountData onlineAccountData = new OnlineAccountData(onlineTimestamp, signature, publicKey);
|
||||
ourOnlineAccounts.add(onlineAccountData);
|
||||
@@ -982,6 +980,7 @@ public class Block {
|
||||
|
||||
// All online accounts valid, so save our list of online accounts for potential later use
|
||||
this.cachedValidOnlineAccounts = ourOnlineAccounts;
|
||||
this.cachedOnlineRewardShares = onlineRewardShares;
|
||||
|
||||
return ValidationResult.OK;
|
||||
}
|
||||
@@ -1316,13 +1315,16 @@ public class Block {
|
||||
allUniqueExpandedAccounts.add(expandedAccount.recipientAccountData);
|
||||
}
|
||||
|
||||
// Decrease blocks minted count for all accounts
|
||||
// Increase blocks minted count for all accounts
|
||||
|
||||
// Batch update in repository
|
||||
repository.getAccountRepository().modifyMintedBlockCounts(allUniqueExpandedAccounts.stream().map(AccountData::getAddress).collect(Collectors.toList()), +1);
|
||||
|
||||
// Local changes and also checks for level bump
|
||||
for (AccountData accountData : allUniqueExpandedAccounts) {
|
||||
// Adjust count locally (in Java)
|
||||
accountData.setBlocksMinted(accountData.getBlocksMinted() + 1);
|
||||
|
||||
int rowCount = repository.getAccountRepository().modifyMintedBlockCount(accountData.getAddress(), +1);
|
||||
LOGGER.trace(() -> String.format("Block minter %s up to %d minted block%s (rowCount: %d)", accountData.getAddress(), accountData.getBlocksMinted(), (accountData.getBlocksMinted() != 1 ? "s" : ""), rowCount));
|
||||
LOGGER.trace(() -> String.format("Block minter %s up to %d minted block%s", accountData.getAddress(), accountData.getBlocksMinted(), (accountData.getBlocksMinted() != 1 ? "s" : "")));
|
||||
|
||||
final int effectiveBlocksMinted = accountData.getBlocksMinted() + accountData.getBlocksMintedAdjustment();
|
||||
|
||||
@@ -1612,12 +1614,14 @@ public class Block {
|
||||
}
|
||||
|
||||
// Decrease blocks minted count for all accounts
|
||||
|
||||
// Batch update in repository
|
||||
repository.getAccountRepository().modifyMintedBlockCounts(allUniqueExpandedAccounts.stream().map(AccountData::getAddress).collect(Collectors.toList()), -1);
|
||||
|
||||
for (AccountData accountData : allUniqueExpandedAccounts) {
|
||||
// Adjust count locally (in Java)
|
||||
accountData.setBlocksMinted(accountData.getBlocksMinted() - 1);
|
||||
|
||||
int rowCount = repository.getAccountRepository().modifyMintedBlockCount(accountData.getAddress(), -1);
|
||||
LOGGER.trace(() -> String.format("Block minter %s down to %d minted block%s (rowCount: %d)", accountData.getAddress(), accountData.getBlocksMinted(), (accountData.getBlocksMinted() != 1 ? "s" : ""), rowCount));
|
||||
LOGGER.trace(() -> String.format("Block minter %s down to %d minted block%s", accountData.getAddress(), accountData.getBlocksMinted(), (accountData.getBlocksMinted() != 1 ? "s" : "")));
|
||||
|
||||
final int effectiveBlocksMinted = accountData.getBlocksMinted() + accountData.getBlocksMintedAdjustment();
|
||||
|
||||
@@ -1646,7 +1650,7 @@ public class Block {
|
||||
this.distributionMethod = distributionMethod;
|
||||
}
|
||||
|
||||
public long distribute(long distibutionAmount, Map<Account, Long> balanceChanges) throws DataException {
|
||||
public long distribute(long distibutionAmount, Map<String, Long> balanceChanges) throws DataException {
|
||||
return this.distributionMethod.distribute(distibutionAmount, balanceChanges);
|
||||
}
|
||||
}
|
||||
@@ -1663,7 +1667,7 @@ public class Block {
|
||||
// Now distribute to candidates
|
||||
|
||||
// Collate all balance changes and then apply in one final step
|
||||
Map<Account, Long> balanceChanges = new HashMap<>();
|
||||
Map<String, Long> balanceChanges = new HashMap<>();
|
||||
|
||||
long remainingAmount = totalAmount;
|
||||
for (int r = 0; r < rewardCandidates.size(); ++r) {
|
||||
@@ -1688,8 +1692,10 @@ public class Block {
|
||||
}
|
||||
|
||||
// Apply balance changes
|
||||
for (Map.Entry<Account, Long> balanceChange : balanceChanges.entrySet())
|
||||
balanceChange.getKey().modifyAssetBalance(Asset.QORT, balanceChange.getValue());
|
||||
List<AccountBalanceData> accountBalanceDeltas = balanceChanges.entrySet().stream()
|
||||
.map(entry -> new AccountBalanceData(entry.getKey(), Asset.QORT, entry.getValue()))
|
||||
.collect(Collectors.toList());
|
||||
this.repository.getAccountRepository().modifyAssetBalances(accountBalanceDeltas);
|
||||
}
|
||||
|
||||
protected List<BlockRewardCandidate> determineBlockRewardCandidates(boolean isProcessingNotOrphaning) throws DataException {
|
||||
@@ -1759,7 +1765,7 @@ public class Block {
|
||||
}
|
||||
|
||||
// Fetch list of legacy QORA holders who haven't reached their cap of QORT reward.
|
||||
List<AccountBalanceData> qoraHolders = this.repository.getAccountRepository().getEligibleLegacyQoraHolders(isProcessingNotOrphaning ? null : this.blockData.getHeight());
|
||||
List<EligibleQoraHolderData> qoraHolders = this.repository.getAccountRepository().getEligibleLegacyQoraHolders(isProcessingNotOrphaning ? null : this.blockData.getHeight());
|
||||
final boolean haveQoraHolders = !qoraHolders.isEmpty();
|
||||
final long qoraHoldersShare = BlockChain.getInstance().getQoraHoldersShare();
|
||||
|
||||
@@ -1812,7 +1818,7 @@ public class Block {
|
||||
return rewardCandidates;
|
||||
}
|
||||
|
||||
private static long distributeBlockRewardShare(long distributionAmount, List<ExpandedAccount> accounts, Map<Account, Long> balanceChanges) {
|
||||
private static long distributeBlockRewardShare(long distributionAmount, List<ExpandedAccount> accounts, Map<String, Long> balanceChanges) {
|
||||
// Collate all expanded accounts by minting account
|
||||
Map<String, List<ExpandedAccount>> accountsByMinter = new HashMap<>();
|
||||
|
||||
@@ -1841,7 +1847,7 @@ public class Block {
|
||||
return sharedAmount;
|
||||
}
|
||||
|
||||
private static long distributeBlockRewardToQoraHolders(long qoraHoldersAmount, List<AccountBalanceData> qoraHolders, Map<Account, Long> balanceChanges, Block block) throws DataException {
|
||||
private static long distributeBlockRewardToQoraHolders(long qoraHoldersAmount, List<EligibleQoraHolderData> qoraHolders, Map<String, Long> balanceChanges, Block block) throws DataException {
|
||||
final boolean isProcessingNotOrphaning = qoraHoldersAmount >= 0;
|
||||
|
||||
long qoraPerQortReward = BlockChain.getInstance().getQoraPerQortReward();
|
||||
@@ -1849,7 +1855,7 @@ public class Block {
|
||||
|
||||
long totalQoraHeld = 0;
|
||||
for (int i = 0; i < qoraHolders.size(); ++i)
|
||||
totalQoraHeld += qoraHolders.get(i).getBalance();
|
||||
totalQoraHeld += qoraHolders.get(i).getQoraBalance();
|
||||
|
||||
long finalTotalQoraHeld = totalQoraHeld;
|
||||
LOGGER.trace(() -> String.format("Total legacy QORA held: %s", Amounts.prettyAmount(finalTotalQoraHeld)));
|
||||
@@ -1862,9 +1868,13 @@ public class Block {
|
||||
BigInteger totalQoraHeldBI = BigInteger.valueOf(totalQoraHeld);
|
||||
|
||||
long sharedAmount = 0;
|
||||
// For batched update of QORT_FROM_QORA balances
|
||||
List<AccountBalanceData> newQortFromQoraBalances = new ArrayList<>();
|
||||
|
||||
for (int h = 0; h < qoraHolders.size(); ++h) {
|
||||
AccountBalanceData qoraHolder = qoraHolders.get(h);
|
||||
BigInteger qoraHolderBalanceBI = BigInteger.valueOf(qoraHolder.getBalance());
|
||||
EligibleQoraHolderData qoraHolder = qoraHolders.get(h);
|
||||
BigInteger qoraHolderBalanceBI = BigInteger.valueOf(qoraHolder.getQoraBalance());
|
||||
String qoraHolderAddress = qoraHolder.getAddress();
|
||||
|
||||
// This is where a 128bit integer library could help:
|
||||
// long holderReward = (qoraHoldersAmount * qoraHolder.getBalance()) / totalQoraHeld;
|
||||
@@ -1872,15 +1882,13 @@ public class Block {
|
||||
|
||||
final long holderRewardForLogging = holderReward;
|
||||
LOGGER.trace(() -> String.format("QORA holder %s has %s / %s QORA so share: %s",
|
||||
qoraHolder.getAddress(), Amounts.prettyAmount(qoraHolder.getBalance()), finalTotalQoraHeld, Amounts.prettyAmount(holderRewardForLogging)));
|
||||
qoraHolderAddress, Amounts.prettyAmount(qoraHolder.getQoraBalance()), finalTotalQoraHeld, Amounts.prettyAmount(holderRewardForLogging)));
|
||||
|
||||
// Too small to register this time?
|
||||
if (holderReward == 0)
|
||||
continue;
|
||||
|
||||
Account qoraHolderAccount = new Account(block.repository, qoraHolder.getAddress());
|
||||
|
||||
long newQortFromQoraBalance = qoraHolderAccount.getConfirmedBalance(Asset.QORT_FROM_QORA) + holderReward;
|
||||
long newQortFromQoraBalance = qoraHolder.getQortFromQoraBalance() + holderReward;
|
||||
|
||||
// If processing, make sure we don't overpay
|
||||
if (isProcessingNotOrphaning) {
|
||||
@@ -1894,44 +1902,43 @@ public class Block {
|
||||
newQortFromQoraBalance -= adjustment;
|
||||
|
||||
// This is also the QORA holder's final QORT-from-QORA block
|
||||
QortFromQoraData qortFromQoraData = new QortFromQoraData(qoraHolder.getAddress(), holderReward, block.blockData.getHeight());
|
||||
QortFromQoraData qortFromQoraData = new QortFromQoraData(qoraHolderAddress, holderReward, block.blockData.getHeight());
|
||||
block.repository.getAccountRepository().save(qortFromQoraData);
|
||||
|
||||
long finalAdjustedHolderReward = holderReward;
|
||||
LOGGER.trace(() -> String.format("QORA holder %s final share %s at height %d",
|
||||
qoraHolder.getAddress(), Amounts.prettyAmount(finalAdjustedHolderReward), block.blockData.getHeight()));
|
||||
qoraHolderAddress, Amounts.prettyAmount(finalAdjustedHolderReward), block.blockData.getHeight()));
|
||||
}
|
||||
} else {
|
||||
// Orphaning
|
||||
QortFromQoraData qortFromQoraData = block.repository.getAccountRepository().getQortFromQoraInfo(qoraHolder.getAddress());
|
||||
if (qortFromQoraData != null) {
|
||||
if (qoraHolder.getFinalBlockHeight() != null) {
|
||||
// Final QORT-from-QORA amount from repository was stored during processing, and hence positive.
|
||||
// So we use + here as qortFromQora is negative during orphaning.
|
||||
// More efficient than "holderReward - (0 - final-qort-from-qora)"
|
||||
long adjustment = holderReward + qortFromQoraData.getFinalQortFromQora();
|
||||
long adjustment = holderReward + qoraHolder.getFinalQortFromQora();
|
||||
|
||||
holderReward -= adjustment;
|
||||
newQortFromQoraBalance -= adjustment;
|
||||
|
||||
block.repository.getAccountRepository().deleteQortFromQoraInfo(qoraHolder.getAddress());
|
||||
block.repository.getAccountRepository().deleteQortFromQoraInfo(qoraHolderAddress);
|
||||
|
||||
long finalAdjustedHolderReward = holderReward;
|
||||
LOGGER.trace(() -> String.format("QORA holder %s final share %s was at height %d",
|
||||
qoraHolder.getAddress(), Amounts.prettyAmount(finalAdjustedHolderReward), block.blockData.getHeight()));
|
||||
qoraHolderAddress, Amounts.prettyAmount(finalAdjustedHolderReward), block.blockData.getHeight()));
|
||||
}
|
||||
}
|
||||
|
||||
balanceChanges.merge(qoraHolderAccount, holderReward, Long::sum);
|
||||
balanceChanges.merge(qoraHolderAddress, holderReward, Long::sum);
|
||||
|
||||
if (newQortFromQoraBalance > 0)
|
||||
qoraHolderAccount.setConfirmedBalance(Asset.QORT_FROM_QORA, newQortFromQoraBalance);
|
||||
else
|
||||
// Remove QORT_FROM_QORA balance as it's zero
|
||||
qoraHolderAccount.deleteBalance(Asset.QORT_FROM_QORA);
|
||||
// Add to batched QORT_FROM_QORA balance update list
|
||||
newQortFromQoraBalances.add(new AccountBalanceData(qoraHolderAddress, Asset.QORT_FROM_QORA, newQortFromQoraBalance));
|
||||
|
||||
sharedAmount += holderReward;
|
||||
}
|
||||
|
||||
// Perform batched update of QORT_FROM_QORA balances
|
||||
block.repository.getAccountRepository().setAssetBalances(newQortFromQoraBalances);
|
||||
|
||||
return sharedAmount;
|
||||
}
|
||||
|
||||
|
@@ -32,7 +32,6 @@ import org.qortal.repository.DataException;
|
||||
import org.qortal.repository.Repository;
|
||||
import org.qortal.repository.RepositoryManager;
|
||||
import org.qortal.settings.Settings;
|
||||
import org.qortal.utils.NTP;
|
||||
import org.qortal.utils.StringLongMapXmlAdapter;
|
||||
|
||||
/**
|
||||
@@ -554,17 +553,23 @@ public class BlockChain {
|
||||
|
||||
try {
|
||||
try (final Repository repository = RepositoryManager.getRepository()) {
|
||||
for (int height = repository.getBlockRepository().getBlockchainHeight(); height > targetHeight; --height) {
|
||||
int height = repository.getBlockRepository().getBlockchainHeight();
|
||||
BlockData orphanBlockData = repository.getBlockRepository().fromHeight(height);
|
||||
|
||||
while (height > targetHeight) {
|
||||
LOGGER.info(String.format("Forcably orphaning block %d", height));
|
||||
|
||||
BlockData blockData = repository.getBlockRepository().fromHeight(height);
|
||||
Block block = new Block(repository, blockData);
|
||||
Block block = new Block(repository, orphanBlockData);
|
||||
block.orphan();
|
||||
repository.saveChanges();
|
||||
}
|
||||
|
||||
BlockData lastBlockData = repository.getBlockRepository().getLastBlock();
|
||||
Controller.getInstance().setChainTip(lastBlockData);
|
||||
repository.saveChanges();
|
||||
|
||||
--height;
|
||||
orphanBlockData = repository.getBlockRepository().fromHeight(height);
|
||||
|
||||
repository.discardChanges(); // clear transaction status to prevent deadlocks
|
||||
Controller.getInstance().onNewBlock(orphanBlockData);
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
@@ -573,33 +578,4 @@ public class BlockChain {
|
||||
}
|
||||
}
|
||||
|
||||
public static void trimOldOnlineAccountsSignatures() {
|
||||
final Long now = NTP.getTime();
|
||||
if (now == null)
|
||||
return;
|
||||
|
||||
ReentrantLock blockchainLock = Controller.getInstance().getBlockchainLock();
|
||||
if (!blockchainLock.tryLock())
|
||||
// Too busy to trim right now, try again later
|
||||
return;
|
||||
|
||||
try {
|
||||
try (final Repository repository = RepositoryManager.tryRepository()) {
|
||||
if (repository == null)
|
||||
return;
|
||||
|
||||
int numBlocksTrimmed = repository.getBlockRepository().trimOldOnlineAccountsSignatures(now - BlockChain.getInstance().getOnlineAccountSignaturesMaxLifetime());
|
||||
|
||||
if (numBlocksTrimmed > 0)
|
||||
LOGGER.debug(String.format("Trimmed old online accounts signatures from %d block%s", numBlocksTrimmed, (numBlocksTrimmed != 1 ? "s" : "")));
|
||||
|
||||
repository.saveChanges();
|
||||
} catch (DataException e) {
|
||||
LOGGER.warn(String.format("Repository issue trying to trim old online accounts signatures: %s", e.getMessage()));
|
||||
}
|
||||
} finally {
|
||||
blockchainLock.unlock();
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
73
src/main/java/org/qortal/controller/AtStatesTrimmer.java
Normal file
73
src/main/java/org/qortal/controller/AtStatesTrimmer.java
Normal file
@@ -0,0 +1,73 @@
|
||||
package org.qortal.controller;
|
||||
|
||||
import org.apache.logging.log4j.LogManager;
|
||||
import org.apache.logging.log4j.Logger;
|
||||
import org.qortal.data.block.BlockData;
|
||||
import org.qortal.repository.DataException;
|
||||
import org.qortal.repository.Repository;
|
||||
import org.qortal.repository.RepositoryManager;
|
||||
import org.qortal.settings.Settings;
|
||||
import org.qortal.utils.NTP;
|
||||
|
||||
public class AtStatesTrimmer implements Runnable {
|
||||
|
||||
private static final Logger LOGGER = LogManager.getLogger(AtStatesTrimmer.class);
|
||||
|
||||
@Override
|
||||
public void run() {
|
||||
Thread.currentThread().setName("AT States trimmer");
|
||||
|
||||
try (final Repository repository = RepositoryManager.getRepository()) {
|
||||
repository.getATRepository().prepareForAtStateTrimming();
|
||||
repository.saveChanges();
|
||||
|
||||
while (!Controller.isStopping()) {
|
||||
repository.discardChanges();
|
||||
|
||||
Thread.sleep(Settings.getInstance().getAtStatesTrimInterval());
|
||||
|
||||
BlockData chainTip = Controller.getInstance().getChainTip();
|
||||
if (chainTip == null || NTP.getTime() == null)
|
||||
continue;
|
||||
|
||||
long currentTrimmableTimestamp = NTP.getTime() - Settings.getInstance().getAtStatesMaxLifetime();
|
||||
// We want to keep AT states near the tip of our copy of blockchain so we can process/orphan nearby blocks
|
||||
long chainTrimmableTimestamp = chainTip.getTimestamp() - Settings.getInstance().getAtStatesMaxLifetime();
|
||||
|
||||
long upperTrimmableTimestamp = Math.min(currentTrimmableTimestamp, chainTrimmableTimestamp);
|
||||
int upperTrimmableHeight = repository.getBlockRepository().getHeightFromTimestamp(upperTrimmableTimestamp);
|
||||
|
||||
int trimStartHeight = repository.getATRepository().getAtTrimHeight();
|
||||
|
||||
int upperBatchHeight = trimStartHeight + Settings.getInstance().getAtStatesTrimBatchSize();
|
||||
int upperTrimHeight = Math.min(upperBatchHeight, upperTrimmableHeight);
|
||||
|
||||
if (trimStartHeight >= upperTrimHeight)
|
||||
continue;
|
||||
|
||||
int numAtStatesTrimmed = repository.getATRepository().trimAtStates(trimStartHeight, upperTrimHeight, Settings.getInstance().getAtStatesTrimLimit());
|
||||
repository.saveChanges();
|
||||
|
||||
if (numAtStatesTrimmed > 0) {
|
||||
LOGGER.debug(() -> String.format("Trimmed %d AT state%s between blocks %d and %d",
|
||||
numAtStatesTrimmed, (numAtStatesTrimmed != 1 ? "s" : ""),
|
||||
trimStartHeight, upperTrimHeight));
|
||||
} else {
|
||||
// Can we move onto next batch?
|
||||
if (upperTrimmableHeight > upperBatchHeight) {
|
||||
repository.getATRepository().setAtTrimHeight(upperBatchHeight);
|
||||
repository.getATRepository().prepareForAtStateTrimming();
|
||||
repository.saveChanges();
|
||||
|
||||
LOGGER.debug(() -> String.format("Bumping AT state trim height to %d", upperBatchHeight));
|
||||
}
|
||||
}
|
||||
}
|
||||
} catch (DataException e) {
|
||||
LOGGER.warn(String.format("Repository issue trying to trim AT states: %s", e.getMessage()));
|
||||
} catch (InterruptedException e) {
|
||||
// Time to exit
|
||||
}
|
||||
}
|
||||
|
||||
}
|
@@ -1,4 +1,4 @@
|
||||
package org.qortal.block;
|
||||
package org.qortal.controller;
|
||||
|
||||
import java.math.BigInteger;
|
||||
import java.util.ArrayList;
|
||||
@@ -13,8 +13,9 @@ import org.apache.logging.log4j.LogManager;
|
||||
import org.apache.logging.log4j.Logger;
|
||||
import org.qortal.account.Account;
|
||||
import org.qortal.account.PrivateKeyAccount;
|
||||
import org.qortal.block.Block;
|
||||
import org.qortal.block.Block.ValidationResult;
|
||||
import org.qortal.controller.Controller;
|
||||
import org.qortal.block.BlockChain;
|
||||
import org.qortal.data.account.MintingAccountData;
|
||||
import org.qortal.data.account.RewardShareData;
|
||||
import org.qortal.data.block.BlockData;
|
||||
@@ -60,7 +61,7 @@ public class BlockMinter extends Thread {
|
||||
List<TransactionData> unconfirmedTransactions = repository.getTransactionRepository().getUnconfirmedTransactions();
|
||||
|
||||
for (TransactionData transactionData : unconfirmedTransactions) {
|
||||
LOGGER.trace(String.format("Deleting unconfirmed transaction %s", Base58.encode(transactionData.getSignature())));
|
||||
LOGGER.trace(() -> String.format("Deleting unconfirmed transaction %s", Base58.encode(transactionData.getSignature())));
|
||||
repository.getTransactionRepository().delete(transactionData);
|
||||
}
|
||||
|
||||
@@ -69,7 +70,7 @@ public class BlockMinter extends Thread {
|
||||
|
||||
// Going to need this a lot...
|
||||
BlockRepository blockRepository = repository.getBlockRepository();
|
||||
Block previousBlock = null;
|
||||
BlockData previousBlockData = null;
|
||||
|
||||
List<Block> newBlocks = new ArrayList<>();
|
||||
|
||||
@@ -115,7 +116,7 @@ public class BlockMinter extends Thread {
|
||||
|
||||
RewardShareData rewardShareData = repository.getAccountRepository().getRewardShare(mintingAccountData.getPublicKey());
|
||||
if (rewardShareData == null) {
|
||||
// Reward-share doesn't even exist - probably not a good sign
|
||||
// Reward-share doesn't exist - probably cancelled but not yet removed from node's list of minting accounts
|
||||
madi.remove();
|
||||
continue;
|
||||
}
|
||||
@@ -150,8 +151,8 @@ public class BlockMinter extends Thread {
|
||||
isMintingPossible = true;
|
||||
|
||||
// Check blockchain hasn't changed
|
||||
if (previousBlock == null || !Arrays.equals(previousBlock.getSignature(), lastBlockData.getSignature())) {
|
||||
previousBlock = new Block(repository, lastBlockData);
|
||||
if (previousBlockData == null || !Arrays.equals(previousBlockData.getSignature(), lastBlockData.getSignature())) {
|
||||
previousBlockData = lastBlockData;
|
||||
newBlocks.clear();
|
||||
|
||||
// Reduce log timeout
|
||||
@@ -162,12 +163,12 @@ public class BlockMinter extends Thread {
|
||||
mintingAccountsData.removeIf(mintingAccountData -> newBlocks.stream().anyMatch(newBlock -> Arrays.equals(newBlock.getBlockData().getMinterPublicKey(), mintingAccountData.getPublicKey())));
|
||||
|
||||
// Do we need to build any potential new blocks?
|
||||
List<PrivateKeyAccount> mintingAccounts = mintingAccountsData.stream().map(accountData -> new PrivateKeyAccount(repository, accountData.getPrivateKey())).collect(Collectors.toList());
|
||||
List<PrivateKeyAccount> newBlocksMintingAccounts = mintingAccountsData.stream().map(accountData -> new PrivateKeyAccount(repository, accountData.getPrivateKey())).collect(Collectors.toList());
|
||||
|
||||
for (PrivateKeyAccount mintingAccount : mintingAccounts) {
|
||||
for (PrivateKeyAccount mintingAccount : newBlocksMintingAccounts) {
|
||||
// First block does the AT heavy-lifting
|
||||
if (newBlocks.isEmpty()) {
|
||||
Block newBlock = Block.mint(repository, previousBlock.getBlockData(), mintingAccount);
|
||||
Block newBlock = Block.mint(repository, previousBlockData, mintingAccount);
|
||||
if (newBlock == null) {
|
||||
// For some reason we can't mint right now
|
||||
moderatedLog(() -> LOGGER.error("Couldn't build a to-be-minted block"));
|
||||
@@ -195,7 +196,7 @@ public class BlockMinter extends Thread {
|
||||
// Make sure we're the only thread modifying the blockchain
|
||||
ReentrantLock blockchainLock = Controller.getInstance().getBlockchainLock();
|
||||
if (!blockchainLock.tryLock(30, TimeUnit.SECONDS)) {
|
||||
LOGGER.warn("Couldn't acquire blockchain lock even after waiting 30 seconds");
|
||||
LOGGER.debug("Couldn't acquire blockchain lock even after waiting 30 seconds");
|
||||
continue;
|
||||
}
|
||||
|
||||
@@ -233,8 +234,8 @@ public class BlockMinter extends Thread {
|
||||
continue;
|
||||
|
||||
// Pick best block
|
||||
final int parentHeight = previousBlock.getBlockData().getHeight();
|
||||
final byte[] parentBlockSignature = previousBlock.getSignature();
|
||||
final int parentHeight = previousBlockData.getHeight();
|
||||
final byte[] parentBlockSignature = previousBlockData.getSignature();
|
||||
|
||||
BigInteger bestWeight = null;
|
||||
|
||||
@@ -274,9 +275,10 @@ public class BlockMinter extends Thread {
|
||||
try {
|
||||
newBlock.process();
|
||||
|
||||
LOGGER.info(String.format("Minted new block: %d", newBlock.getBlockData().getHeight()));
|
||||
repository.saveChanges();
|
||||
|
||||
LOGGER.info(String.format("Minted new block: %d", newBlock.getBlockData().getHeight()));
|
||||
|
||||
RewardShareData rewardShareData = repository.getAccountRepository().getRewardShare(newBlock.getBlockData().getMinterPublicKey());
|
||||
|
||||
if (rewardShareData != null) {
|
||||
@@ -292,9 +294,7 @@ public class BlockMinter extends Thread {
|
||||
newBlock.getMinter().getAddress()));
|
||||
}
|
||||
|
||||
repository.saveChanges();
|
||||
|
||||
// Notify controller
|
||||
// Notify controller after we're released blockchain lock
|
||||
newBlockMinted = true;
|
||||
} catch (DataException e) {
|
||||
// Unable to process block - report and discard
|
||||
@@ -305,8 +305,16 @@ public class BlockMinter extends Thread {
|
||||
blockchainLock.unlock();
|
||||
}
|
||||
|
||||
if (newBlockMinted)
|
||||
Controller.getInstance().onNewBlock(newBlock.getBlockData());
|
||||
if (newBlockMinted) {
|
||||
// Notify Controller and broadcast our new chain to network
|
||||
BlockData newBlockData = newBlock.getBlockData();
|
||||
|
||||
repository.discardChanges(); // clear transaction status to prevent deadlocks
|
||||
Controller.getInstance().onNewBlock(newBlockData);
|
||||
|
||||
Network network = Network.getInstance();
|
||||
network.broadcast(broadcastPeer -> network.buildHeightMessage(broadcastPeer, newBlockData));
|
||||
}
|
||||
}
|
||||
} catch (DataException e) {
|
||||
LOGGER.warn("Repository issue while running block minter", e);
|
@@ -1,61 +0,0 @@
|
||||
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());
|
||||
}
|
||||
}
|
||||
|
||||
}
|
@@ -22,6 +22,7 @@ import java.util.Properties;
|
||||
import java.util.Random;
|
||||
import java.util.concurrent.ExecutorService;
|
||||
import java.util.concurrent.Executors;
|
||||
import java.util.concurrent.TimeUnit;
|
||||
import java.util.concurrent.locks.ReentrantLock;
|
||||
import java.util.function.Predicate;
|
||||
import java.util.stream.Collectors;
|
||||
@@ -36,7 +37,6 @@ import org.qortal.account.PublicKeyAccount;
|
||||
import org.qortal.api.ApiService;
|
||||
import org.qortal.block.Block;
|
||||
import org.qortal.block.BlockChain;
|
||||
import org.qortal.block.BlockMinter;
|
||||
import org.qortal.block.BlockChain.BlockTimingByHeight;
|
||||
import org.qortal.controller.Synchronizer.SynchronizationResult;
|
||||
import org.qortal.crypto.Crypto;
|
||||
@@ -50,6 +50,8 @@ 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.event.Event;
|
||||
import org.qortal.event.EventBus;
|
||||
import org.qortal.data.transaction.ChatTransactionData;
|
||||
import org.qortal.globalization.Translator;
|
||||
import org.qortal.gui.Gui;
|
||||
@@ -85,6 +87,7 @@ import org.qortal.transaction.Transaction.TransactionType;
|
||||
import org.qortal.transaction.Transaction.ValidationResult;
|
||||
import org.qortal.utils.Base58;
|
||||
import org.qortal.utils.ByteArray;
|
||||
import org.qortal.utils.DaemonThreadFactory;
|
||||
import org.qortal.utils.NTP;
|
||||
import org.qortal.utils.Triple;
|
||||
|
||||
@@ -137,11 +140,14 @@ public class Controller extends Thread {
|
||||
private long repositoryBackupTimestamp = startTime; // ms
|
||||
private long ntpCheckTimestamp = startTime; // ms
|
||||
private long deleteExpiredTimestamp = startTime + DELETE_EXPIRED_INTERVAL; // ms
|
||||
|
||||
private long onlineAccountsTasksTimestamp = startTime + ONLINE_ACCOUNTS_TASKS_INTERVAL; // ms
|
||||
|
||||
/** Whether we can mint new blocks, as reported by BlockMinter. */
|
||||
private volatile boolean isMintingPossible = false;
|
||||
|
||||
/** Synchronization object for sync variables below */
|
||||
private final Object syncLock = new Object();
|
||||
/** Whether we are attempting to synchronize. */
|
||||
private volatile boolean isSynchronizing = false;
|
||||
/** Temporary estimate of synchronization progress for SysTray use. */
|
||||
@@ -271,7 +277,9 @@ public class Controller extends Thread {
|
||||
}
|
||||
|
||||
public Integer getSyncPercent() {
|
||||
return this.isSynchronizing ? this.syncPercent : null;
|
||||
synchronized (this.syncLock) {
|
||||
return this.isSynchronizing ? this.syncPercent : null;
|
||||
}
|
||||
}
|
||||
|
||||
// Entry point
|
||||
@@ -366,6 +374,9 @@ public class Controller extends Thread {
|
||||
blockMinter = new BlockMinter();
|
||||
blockMinter.start();
|
||||
|
||||
LOGGER.info("Starting trade-bot");
|
||||
TradeBot.getInstance();
|
||||
|
||||
// Arbitrary transaction data manager
|
||||
// LOGGER.info("Starting arbitrary-transaction data manager");
|
||||
// ArbitraryDataManager.getInstance().start();
|
||||
@@ -405,6 +416,10 @@ public class Controller extends Thread {
|
||||
|
||||
final long repositoryBackupInterval = Settings.getInstance().getRepositoryBackupInterval();
|
||||
|
||||
ExecutorService trimExecutor = Executors.newCachedThreadPool(new DaemonThreadFactory());
|
||||
trimExecutor.execute(new AtStatesTrimmer());
|
||||
trimExecutor.execute(new OnlineAccountsSignaturesTrimmer());
|
||||
|
||||
try {
|
||||
while (!isStopping) {
|
||||
// Maybe update SysTray
|
||||
@@ -477,7 +492,17 @@ public class Controller extends Thread {
|
||||
}
|
||||
}
|
||||
} catch (InterruptedException e) {
|
||||
// Clear interrupted flag so we can shutdown trim threads
|
||||
Thread.interrupted();
|
||||
// Fall-through to exit
|
||||
} finally {
|
||||
trimExecutor.shutdownNow();
|
||||
|
||||
try {
|
||||
trimExecutor.awaitTermination(2L, TimeUnit.SECONDS);
|
||||
} catch (InterruptedException e) {
|
||||
// We tried...
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -510,6 +535,10 @@ public class Controller extends Thread {
|
||||
};
|
||||
|
||||
private void potentiallySynchronize() throws InterruptedException {
|
||||
// Already synchronizing via another thread?
|
||||
if (this.isSynchronizing)
|
||||
return;
|
||||
|
||||
List<Peer> peers = Network.getInstance().getHandshakedPeers();
|
||||
|
||||
// Disregard peers that have "misbehaved" recently
|
||||
@@ -542,13 +571,21 @@ public class Controller extends Thread {
|
||||
}
|
||||
|
||||
public SynchronizationResult actuallySynchronize(Peer peer, boolean force) throws InterruptedException {
|
||||
syncPercent = (this.chainTip.getHeight() * 100) / peer.getChainTipData().getLastHeight();
|
||||
// Only update SysTray if we're potentially changing height
|
||||
if (syncPercent < 100) {
|
||||
isSynchronizing = true;
|
||||
updateSysTray();
|
||||
boolean hasStatusChanged = false;
|
||||
|
||||
synchronized (this.syncLock) {
|
||||
this.syncPercent = (this.chainTip.getHeight() * 100) / peer.getChainTipData().getLastHeight();
|
||||
|
||||
// Only update SysTray if we're potentially changing height
|
||||
if (this.syncPercent < 100) {
|
||||
this.isSynchronizing = true;
|
||||
hasStatusChanged = true;
|
||||
}
|
||||
}
|
||||
|
||||
if (hasStatusChanged)
|
||||
updateSysTray();
|
||||
|
||||
BlockData priorChainTip = this.chainTip;
|
||||
|
||||
try {
|
||||
@@ -621,6 +658,9 @@ public class Controller extends Thread {
|
||||
|
||||
// Update chain-tip, systray, notify peers, websockets, etc.
|
||||
this.onNewBlock(newChainTip);
|
||||
|
||||
Network network = Network.getInstance();
|
||||
network.broadcast(broadcastPeer -> network.buildHeightMessage(broadcastPeer, newChainTip));
|
||||
}
|
||||
|
||||
return syncResult;
|
||||
@@ -629,6 +669,11 @@ public class Controller extends Thread {
|
||||
}
|
||||
}
|
||||
|
||||
public static class StatusChangeEvent implements Event {
|
||||
public StatusChangeEvent() {
|
||||
}
|
||||
}
|
||||
|
||||
private void updateSysTray() {
|
||||
if (NTP.getTime() == null) {
|
||||
SysTray.getInstance().setToolTipText(Translator.INSTANCE.translate("SysTray", "SYNCHRONIZING_CLOCK"));
|
||||
@@ -643,20 +688,23 @@ public class Controller extends Thread {
|
||||
String heightText = Translator.INSTANCE.translate("SysTray", "BLOCK_HEIGHT");
|
||||
|
||||
String actionText;
|
||||
if (isMintingPossible)
|
||||
actionText = Translator.INSTANCE.translate("SysTray", "MINTING_ENABLED");
|
||||
else if (isSynchronizing)
|
||||
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
|
||||
actionText = Translator.INSTANCE.translate("SysTray", "MINTING_DISABLED");
|
||||
|
||||
synchronized (this.syncLock) {
|
||||
if (this.isMintingPossible)
|
||||
actionText = Translator.INSTANCE.translate("SysTray", "MINTING_ENABLED");
|
||||
else if (this.isSynchronizing)
|
||||
actionText = String.format("%s - %d%%", Translator.INSTANCE.translate("SysTray", "SYNCHRONIZING_BLOCKCHAIN"), this.syncPercent);
|
||||
else if (numberOfPeers < Settings.getInstance().getMinBlockchainPeers())
|
||||
actionText = Translator.INSTANCE.translate("SysTray", "CONNECTING");
|
||||
else
|
||||
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());
|
||||
EventBus.INSTANCE.notify(new StatusChangeEvent());
|
||||
});
|
||||
}
|
||||
|
||||
@@ -783,25 +831,38 @@ public class Controller extends Thread {
|
||||
requestSysTrayUpdate = true;
|
||||
}
|
||||
|
||||
public static class NewBlockEvent implements Event {
|
||||
private final BlockData blockData;
|
||||
|
||||
public NewBlockEvent(BlockData blockData) {
|
||||
this.blockData = blockData;
|
||||
}
|
||||
|
||||
public BlockData getBlockData() {
|
||||
return this.blockData;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Callback for when we've received a new block.
|
||||
* <p>
|
||||
* See <b>WARNING</b> for {@link EventBus#notify(Event)}
|
||||
* to prevent deadlocks.
|
||||
*/
|
||||
public void onNewBlock(BlockData latestBlockData) {
|
||||
this.setChainTip(latestBlockData);
|
||||
// Protective copy
|
||||
BlockData blockDataCopy = new BlockData(latestBlockData);
|
||||
|
||||
this.setChainTip(blockDataCopy);
|
||||
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));
|
||||
// Notify listeners, trade-bot, etc.
|
||||
EventBus.INSTANCE.notify(new NewBlockEvent(blockDataCopy));
|
||||
|
||||
BlockNotifier.getInstance().onNewBlock(latestBlockData);
|
||||
|
||||
if (this.notifyGroupMembershipChange) {
|
||||
this.notifyGroupMembershipChange = false;
|
||||
ChatNotifier.getInstance().onGroupMembershipChange();
|
||||
}
|
||||
|
||||
// Trade-bot might want to perform some actions too
|
||||
TradeBot.getInstance().onChainTipChange();
|
||||
});
|
||||
if (this.notifyGroupMembershipChange) {
|
||||
this.notifyGroupMembershipChange = false;
|
||||
ChatNotifier.getInstance().onGroupMembershipChange();
|
||||
}
|
||||
}
|
||||
|
||||
/** Callback for when we've received a new transaction via API or peer. */
|
||||
@@ -1367,9 +1428,6 @@ public class Controller extends Thread {
|
||||
|
||||
// Refresh our online accounts signatures?
|
||||
sendOurOnlineAccountsInfo();
|
||||
|
||||
// Trim blockchain by removing 'old' online accounts signatures
|
||||
BlockChain.trimOldOnlineAccountsSignatures();
|
||||
}
|
||||
|
||||
private void sendOurOnlineAccountsInfo() {
|
||||
|
@@ -0,0 +1,71 @@
|
||||
package org.qortal.controller;
|
||||
|
||||
import org.apache.logging.log4j.LogManager;
|
||||
import org.apache.logging.log4j.Logger;
|
||||
import org.qortal.block.BlockChain;
|
||||
import org.qortal.data.block.BlockData;
|
||||
import org.qortal.repository.DataException;
|
||||
import org.qortal.repository.Repository;
|
||||
import org.qortal.repository.RepositoryManager;
|
||||
import org.qortal.settings.Settings;
|
||||
import org.qortal.utils.NTP;
|
||||
|
||||
public class OnlineAccountsSignaturesTrimmer implements Runnable {
|
||||
|
||||
private static final Logger LOGGER = LogManager.getLogger(OnlineAccountsSignaturesTrimmer.class);
|
||||
|
||||
private static final long INITIAL_SLEEP_PERIOD = 5 * 60 * 1000L + 1234L; // ms
|
||||
|
||||
public void run() {
|
||||
Thread.currentThread().setName("Online Accounts trimmer");
|
||||
|
||||
try (final Repository repository = RepositoryManager.getRepository()) {
|
||||
// Don't even start trimming until initial rush has ended
|
||||
Thread.sleep(INITIAL_SLEEP_PERIOD);
|
||||
|
||||
while (!Controller.isStopping()) {
|
||||
repository.discardChanges();
|
||||
|
||||
Thread.sleep(Settings.getInstance().getOnlineSignaturesTrimInterval());
|
||||
|
||||
BlockData chainTip = Controller.getInstance().getChainTip();
|
||||
if (chainTip == null || NTP.getTime() == null)
|
||||
continue;
|
||||
|
||||
// Trim blockchain by removing 'old' online accounts signatures
|
||||
long upperTrimmableTimestamp = NTP.getTime() - BlockChain.getInstance().getOnlineAccountSignaturesMaxLifetime();
|
||||
int upperTrimmableHeight = repository.getBlockRepository().getHeightFromTimestamp(upperTrimmableTimestamp);
|
||||
|
||||
int trimStartHeight = repository.getBlockRepository().getOnlineAccountsSignaturesTrimHeight();
|
||||
|
||||
int upperBatchHeight = trimStartHeight + Settings.getInstance().getOnlineSignaturesTrimBatchSize();
|
||||
int upperTrimHeight = Math.min(upperBatchHeight, upperTrimmableHeight);
|
||||
|
||||
if (trimStartHeight >= upperTrimHeight)
|
||||
continue;
|
||||
|
||||
int numSigsTrimmed = repository.getBlockRepository().trimOldOnlineAccountsSignatures(trimStartHeight, upperTrimHeight);
|
||||
repository.saveChanges();
|
||||
|
||||
if (numSigsTrimmed > 0) {
|
||||
LOGGER.debug(() -> String.format("Trimmed %d online accounts signature%s between blocks %d and %d",
|
||||
numSigsTrimmed, (numSigsTrimmed != 1 ? "s" : ""),
|
||||
trimStartHeight, upperTrimHeight));
|
||||
} else {
|
||||
// Can we move onto next batch?
|
||||
if (upperTrimmableHeight > upperBatchHeight) {
|
||||
repository.getBlockRepository().setOnlineAccountsSignaturesTrimHeight(upperBatchHeight);
|
||||
repository.saveChanges();
|
||||
|
||||
LOGGER.debug(() -> String.format("Bumping online accounts signatures trim height to %d", upperBatchHeight));
|
||||
}
|
||||
}
|
||||
}
|
||||
} catch (DataException e) {
|
||||
LOGGER.warn(String.format("Repository issue trying to trim online accounts signatures: %s", e.getMessage()));
|
||||
} catch (InterruptedException e) {
|
||||
// Time to exit
|
||||
}
|
||||
}
|
||||
|
||||
}
|
@@ -1,56 +0,0 @@
|
||||
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());
|
||||
}
|
||||
}
|
||||
|
||||
}
|
@@ -4,6 +4,7 @@ import java.math.BigInteger;
|
||||
import java.text.DecimalFormat;
|
||||
import java.text.NumberFormat;
|
||||
import java.util.ArrayList;
|
||||
import java.util.Arrays;
|
||||
import java.util.List;
|
||||
import java.util.concurrent.locks.ReentrantLock;
|
||||
import java.util.stream.Collectors;
|
||||
@@ -11,11 +12,13 @@ import java.util.stream.Collectors;
|
||||
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.Block;
|
||||
import org.qortal.block.Block.ValidationResult;
|
||||
import org.qortal.data.block.BlockData;
|
||||
import org.qortal.data.block.BlockSummaryData;
|
||||
import org.qortal.data.network.PeerChainTipData;
|
||||
import org.qortal.data.transaction.RewardShareTransactionData;
|
||||
import org.qortal.data.transaction.TransactionData;
|
||||
import org.qortal.network.Peer;
|
||||
import org.qortal.network.message.BlockMessage;
|
||||
@@ -238,15 +241,15 @@ public class Synchronizer {
|
||||
blockSummariesFromCommon.addAll(blockSummariesBatch);
|
||||
|
||||
// Trim summaries so that first summary is common block.
|
||||
// Currently we work back from the end until we hit a block we also have.
|
||||
// Currently we work forward from common block until we hit a block we don't have
|
||||
// TODO: rewrite as modified binary search!
|
||||
for (int i = blockSummariesFromCommon.size() - 1; i > 0; --i) {
|
||||
if (repository.getBlockRepository().exists(blockSummariesFromCommon.get(i).getSignature())) {
|
||||
// Note: index i isn't cleared: List.subList is fromIndex inclusive to toIndex exclusive
|
||||
blockSummariesFromCommon.subList(0, i).clear();
|
||||
int i;
|
||||
for (i = 1; i < blockSummariesFromCommon.size(); ++i)
|
||||
if (!repository.getBlockRepository().exists(blockSummariesFromCommon.get(i).getSignature()))
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
// Note: index i - 1 isn't cleared: List.subList is fromIndex inclusive to toIndex exclusive
|
||||
blockSummariesFromCommon.subList(0, i - 1).clear();
|
||||
|
||||
return SynchronizationResult.OK;
|
||||
}
|
||||
@@ -394,15 +397,21 @@ public class Synchronizer {
|
||||
// Unwind to common block (unless common block is our latest block)
|
||||
LOGGER.debug(String.format("Orphaning blocks back to common block height %d, sig %.8s", commonBlockHeight, commonBlockSig58));
|
||||
|
||||
BlockData orphanBlockData = repository.getBlockRepository().fromHeight(ourHeight);
|
||||
while (ourHeight > commonBlockHeight) {
|
||||
if (Controller.isStopping())
|
||||
return SynchronizationResult.SHUTTING_DOWN;
|
||||
|
||||
BlockData blockData = repository.getBlockRepository().fromHeight(ourHeight);
|
||||
Block block = new Block(repository, blockData);
|
||||
Block block = new Block(repository, orphanBlockData);
|
||||
block.orphan();
|
||||
|
||||
repository.saveChanges();
|
||||
|
||||
--ourHeight;
|
||||
orphanBlockData = repository.getBlockRepository().fromHeight(ourHeight);
|
||||
|
||||
repository.discardChanges(); // clear transaction status to prevent deadlocks
|
||||
Controller.getInstance().onNewBlock(orphanBlockData);
|
||||
}
|
||||
|
||||
LOGGER.debug(String.format("Orphaned blocks back to height %d, sig %.8s - applying new blocks from peer %s", commonBlockHeight, commonBlockSig58, peer));
|
||||
@@ -423,9 +432,9 @@ public class Synchronizer {
|
||||
|
||||
newBlock.process();
|
||||
|
||||
// If we've grown our blockchain then at least save progress so far
|
||||
if (ourHeight > ourInitialHeight)
|
||||
repository.saveChanges();
|
||||
repository.saveChanges();
|
||||
|
||||
Controller.getInstance().onNewBlock(newBlock.getBlockData());
|
||||
}
|
||||
|
||||
return SynchronizationResult.OK;
|
||||
@@ -505,9 +514,9 @@ public class Synchronizer {
|
||||
|
||||
newBlock.process();
|
||||
|
||||
// If we've grown our blockchain then at least save progress so far
|
||||
if (ourHeight > ourInitialHeight)
|
||||
repository.saveChanges();
|
||||
repository.saveChanges();
|
||||
|
||||
Controller.getInstance().onNewBlock(newBlock.getBlockData());
|
||||
}
|
||||
|
||||
return SynchronizationResult.OK;
|
||||
@@ -550,16 +559,34 @@ public class Synchronizer {
|
||||
}
|
||||
|
||||
private void populateBlockSummariesMinterLevels(Repository repository, List<BlockSummaryData> blockSummaries) throws DataException {
|
||||
final int firstBlockHeight = blockSummaries.get(0).getHeight();
|
||||
|
||||
for (int i = 0; i < blockSummaries.size(); ++i) {
|
||||
BlockSummaryData blockSummary = blockSummaries.get(i);
|
||||
|
||||
// Qortal: minter is always a reward-share, so find actual minter and get their effective minting level
|
||||
int minterLevel = Account.getRewardShareEffectiveMintingLevel(repository, blockSummary.getMinterPublicKey());
|
||||
if (minterLevel == 0) {
|
||||
// We don't want to throw, or use zero, as this will kill Controller thread and make client unstable.
|
||||
// So we log this but use 1 instead
|
||||
LOGGER.warn(String.format("Unexpected zero effective minter level for reward-share %s - using 1 instead!", Base58.encode(blockSummary.getMinterPublicKey())));
|
||||
minterLevel = 1;
|
||||
// It looks like this block's minter's reward-share has been cancelled.
|
||||
// So search for REWARD_SHARE transactions since common block to find missing minter info
|
||||
List<byte[]> transactionSignatures = repository.getTransactionRepository().getSignaturesMatchingCriteria(Transaction.TransactionType.REWARD_SHARE, null, firstBlockHeight, null);
|
||||
|
||||
for (byte[] transactionSignature : transactionSignatures) {
|
||||
RewardShareTransactionData transactionData = (RewardShareTransactionData) repository.getTransactionRepository().fromSignature(transactionSignature);
|
||||
|
||||
if (transactionData != null && Arrays.equals(transactionData.getRewardSharePublicKey(), blockSummary.getMinterPublicKey())) {
|
||||
Account rewardShareMinter = new PublicKeyAccount(repository, transactionData.getMinterPublicKey());
|
||||
minterLevel = rewardShareMinter.getEffectiveMintingLevel();
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
if (minterLevel == 0) {
|
||||
// We don't want to throw, or use zero, as this will kill Controller thread and make client unstable.
|
||||
// So we log this but use 1 instead
|
||||
LOGGER.debug(() -> String.format("Unexpected zero effective minter level for reward-share %s - using 1 instead!", Base58.encode(blockSummary.getMinterPublicKey())));
|
||||
minterLevel = 1;
|
||||
}
|
||||
}
|
||||
|
||||
blockSummary.setMinterLevel(minterLevel);
|
||||
|
File diff suppressed because it is too large
Load Diff
@@ -10,6 +10,7 @@ import org.apache.logging.log4j.LogManager;
|
||||
import org.apache.logging.log4j.Logger;
|
||||
import org.bitcoinj.core.Address;
|
||||
import org.bitcoinj.core.Coin;
|
||||
import org.bitcoinj.core.Context;
|
||||
import org.bitcoinj.core.ECKey;
|
||||
import org.bitcoinj.core.InsufficientMoneyException;
|
||||
import org.bitcoinj.core.LegacyAddress;
|
||||
@@ -20,6 +21,7 @@ import org.bitcoinj.core.TransactionOutput;
|
||||
import org.bitcoinj.core.UTXO;
|
||||
import org.bitcoinj.core.UTXOProvider;
|
||||
import org.bitcoinj.core.UTXOProviderException;
|
||||
import org.bitcoinj.crypto.ChildNumber;
|
||||
import org.bitcoinj.crypto.DeterministicHierarchy;
|
||||
import org.bitcoinj.crypto.DeterministicKey;
|
||||
import org.bitcoinj.params.MainNetParams;
|
||||
@@ -31,7 +33,6 @@ import org.bitcoinj.utils.MonetaryFormat;
|
||||
import org.bitcoinj.wallet.DeterministicKeyChain;
|
||||
import org.bitcoinj.wallet.SendRequest;
|
||||
import org.bitcoinj.wallet.Wallet;
|
||||
import org.qortal.crosschain.ElectrumX.UnspentOutput;
|
||||
import org.qortal.crypto.Crypto;
|
||||
import org.qortal.settings.Settings;
|
||||
import org.qortal.utils.BitTwiddling;
|
||||
@@ -44,8 +45,17 @@ public class BTC {
|
||||
public static final long LOCKTIME_NO_RBF_SEQUENCE = NO_LOCKTIME_NO_RBF_SEQUENCE - 1;
|
||||
public static final int HASH160_LENGTH = 20;
|
||||
|
||||
public static final boolean INCLUDE_UNCONFIRMED = true;
|
||||
public static final boolean EXCLUDE_UNCONFIRMED = false;
|
||||
|
||||
protected static final Logger LOGGER = LogManager.getLogger(BTC.class);
|
||||
|
||||
// Temporary values until a dynamic fee system is written.
|
||||
private static final long OLD_FEE_AMOUNT = 4_000L; // Not 5000 so that existing P2SH-B can output 1000, avoiding dust issue, leaving 4000 for fees.
|
||||
private static final long NEW_FEE_TIMESTAMP = 1598280000000L; // milliseconds since epoch
|
||||
private static final long NEW_FEE_AMOUNT = 10_000L;
|
||||
private static final long NON_MAINNET_FEE = 1000L; // enough for TESTNET3 and should be OK for REGTEST
|
||||
|
||||
private static final int TIMESTAMP_OFFSET = 4 + 32 + 32;
|
||||
private static final MonetaryFormat FORMAT = new MonetaryFormat().minDecimals(8).postfixCode();
|
||||
|
||||
@@ -75,6 +85,7 @@ public class BTC {
|
||||
private static BTC instance;
|
||||
private final NetworkParameters params;
|
||||
private final ElectrumX electrumX;
|
||||
private final Context bitcoinjContext;
|
||||
|
||||
// Let ECKey.equals() do the hard work
|
||||
private final Set<ECKey> spentKeys = new HashSet<>();
|
||||
@@ -88,6 +99,7 @@ public class BTC {
|
||||
LOGGER.info(() -> String.format("Starting Bitcoin support using %s", bitcoinNet.name()));
|
||||
|
||||
this.electrumX = ElectrumX.getInstance(bitcoinNet.name());
|
||||
this.bitcoinjContext = new Context(this.params);
|
||||
}
|
||||
|
||||
public static synchronized BTC getInstance() {
|
||||
@@ -119,6 +131,7 @@ public class BTC {
|
||||
|
||||
public boolean isValidXprv(String xprv58) {
|
||||
try {
|
||||
Context.propagate(bitcoinjContext);
|
||||
DeterministicKey.deserializeB58(null, xprv58, this.params);
|
||||
return true;
|
||||
} catch (IllegalArgumentException e) {
|
||||
@@ -128,25 +141,29 @@ public class BTC {
|
||||
|
||||
/** Returns P2PKH Bitcoin address using passed public key hash. */
|
||||
public String pkhToAddress(byte[] publicKeyHash) {
|
||||
Context.propagate(bitcoinjContext);
|
||||
return LegacyAddress.fromPubKeyHash(this.params, publicKeyHash).toString();
|
||||
}
|
||||
|
||||
public String deriveP2shAddress(byte[] redeemScriptBytes) {
|
||||
byte[] redeemScriptHash = Crypto.hash160(redeemScriptBytes);
|
||||
Context.propagate(bitcoinjContext);
|
||||
Address p2shAddress = LegacyAddress.fromScriptHash(params, redeemScriptHash);
|
||||
return p2shAddress.toString();
|
||||
}
|
||||
|
||||
/** Returns median timestamp from latest 11 blocks, in seconds. */
|
||||
public Integer getMedianBlockTime() {
|
||||
Integer height = this.electrumX.getCurrentHeight();
|
||||
if (height == null)
|
||||
return null;
|
||||
/**
|
||||
* Returns median timestamp from latest 11 blocks, in seconds.
|
||||
* <p>
|
||||
* @throws BitcoinException if error occurs
|
||||
*/
|
||||
public Integer getMedianBlockTime() throws BitcoinException {
|
||||
int height = this.electrumX.getCurrentHeight();
|
||||
|
||||
// Grab latest 11 blocks
|
||||
List<byte[]> blockHeaders = this.electrumX.getBlockHeaders(height - 11, 11);
|
||||
if (blockHeaders == null || blockHeaders.size() < 11)
|
||||
return null;
|
||||
if (blockHeaders.size() < 11)
|
||||
throw new BitcoinException("Not enough blocks to determine median block time");
|
||||
|
||||
List<Integer> blockTimestamps = blockHeaders.stream().map(blockHeader -> BitTwiddling.intFromLEBytes(blockHeader, TIMESTAMP_OFFSET)).collect(Collectors.toList());
|
||||
|
||||
@@ -157,20 +174,45 @@ public class BTC {
|
||||
return blockTimestamps.get(5);
|
||||
}
|
||||
|
||||
public Long getBalance(String base58Address) {
|
||||
return this.electrumX.getBalance(addressToScript(base58Address));
|
||||
/**
|
||||
* Returns estimated BTC fee, in sats per 1000bytes, optionally for historic timestamp.
|
||||
*
|
||||
* @param timestamp optional milliseconds since epoch, or null for 'now'
|
||||
* @return sats per 1000bytes, or throws BitcoinException if something went wrong
|
||||
*/
|
||||
public long estimateFee(Long timestamp) throws BitcoinException {
|
||||
if (!this.params.getId().equals(NetworkParameters.ID_MAINNET))
|
||||
return NON_MAINNET_FEE;
|
||||
|
||||
// TODO: This will need to be replaced with something better in the near future!
|
||||
if (timestamp != null && timestamp < NEW_FEE_TIMESTAMP)
|
||||
return OLD_FEE_AMOUNT;
|
||||
|
||||
return NEW_FEE_AMOUNT;
|
||||
}
|
||||
|
||||
public List<TransactionOutput> getUnspentOutputs(String base58Address) {
|
||||
List<UnspentOutput> unspentOutputs = this.electrumX.getUnspentOutputs(addressToScript(base58Address));
|
||||
if (unspentOutputs == null)
|
||||
return null;
|
||||
/**
|
||||
* Returns confirmed balance, based on passed payment script.
|
||||
* <p>
|
||||
* @return confirmed balance, or zero if script unknown
|
||||
* @throws BitcoinException if there was an error
|
||||
*/
|
||||
public long getConfirmedBalance(String base58Address) throws BitcoinException {
|
||||
return this.electrumX.getConfirmedBalance(addressToScript(base58Address));
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns list of unspent outputs pertaining to passed address.
|
||||
* <p>
|
||||
* @return list of unspent outputs, or empty list if address unknown
|
||||
* @throws BitcoinException if there was an error.
|
||||
*/
|
||||
public List<TransactionOutput> getUnspentOutputs(String base58Address) throws BitcoinException {
|
||||
List<UnspentOutput> unspentOutputs = this.electrumX.getUnspentOutputs(addressToScript(base58Address), false);
|
||||
|
||||
List<TransactionOutput> unspentTransactionOutputs = new ArrayList<>();
|
||||
for (UnspentOutput unspentOutput : unspentOutputs) {
|
||||
List<TransactionOutput> transactionOutputs = getOutputs(unspentOutput.hash);
|
||||
if (transactionOutputs == null)
|
||||
return null;
|
||||
List<TransactionOutput> transactionOutputs = this.getOutputs(unspentOutput.hash);
|
||||
|
||||
unspentTransactionOutputs.add(transactionOutputs.get(unspentOutput.index));
|
||||
}
|
||||
@@ -178,22 +220,65 @@ public class BTC {
|
||||
return unspentTransactionOutputs;
|
||||
}
|
||||
|
||||
public List<TransactionOutput> getOutputs(byte[] txHash) {
|
||||
/**
|
||||
* Returns list of outputs pertaining to passed transaction hash.
|
||||
* <p>
|
||||
* @return list of outputs, or empty list if transaction unknown
|
||||
* @throws BitcoinException if there was an error.
|
||||
*/
|
||||
public List<TransactionOutput> getOutputs(byte[] txHash) throws BitcoinException {
|
||||
byte[] rawTransactionBytes = this.electrumX.getRawTransaction(txHash);
|
||||
if (rawTransactionBytes == null)
|
||||
return null;
|
||||
|
||||
// XXX bitcoinj: replace with getTransaction() below
|
||||
Context.propagate(bitcoinjContext);
|
||||
Transaction transaction = new Transaction(this.params, rawTransactionBytes);
|
||||
return transaction.getOutputs();
|
||||
}
|
||||
|
||||
/** Returns list of raw transactions spending passed address. */
|
||||
public List<byte[]> getAddressTransactions(String base58Address) {
|
||||
return this.electrumX.getAddressTransactions(addressToScript(base58Address));
|
||||
/**
|
||||
* Returns list of transaction hashes pertaining to passed address.
|
||||
* <p>
|
||||
* @return list of unspent outputs, or empty list if script unknown
|
||||
* @throws BitcoinException if there was an error.
|
||||
*/
|
||||
public List<TransactionHash> getAddressTransactions(String base58Address, boolean includeUnconfirmed) throws BitcoinException {
|
||||
return this.electrumX.getAddressTransactions(addressToScript(base58Address), includeUnconfirmed);
|
||||
}
|
||||
|
||||
public boolean broadcastTransaction(Transaction transaction) {
|
||||
return this.electrumX.broadcastTransaction(transaction.bitcoinSerialize());
|
||||
/**
|
||||
* Returns list of raw, confirmed transactions involving given address.
|
||||
* <p>
|
||||
* @throws BitcoinException if there was an error
|
||||
*/
|
||||
public List<byte[]> getAddressTransactions(String base58Address) throws BitcoinException {
|
||||
List<TransactionHash> transactionHashes = this.electrumX.getAddressTransactions(addressToScript(base58Address), false);
|
||||
|
||||
List<byte[]> rawTransactions = new ArrayList<>();
|
||||
for (TransactionHash transactionInfo : transactionHashes) {
|
||||
byte[] rawTransaction = this.electrumX.getRawTransaction(HashCode.fromString(transactionInfo.txHash).asBytes());
|
||||
rawTransactions.add(rawTransaction);
|
||||
}
|
||||
|
||||
return rawTransactions;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns transaction info for passed transaction hash.
|
||||
* <p>
|
||||
* @throws BitcoinException.NotFoundException if transaction unknown
|
||||
* @throws BitcoinException if error occurs
|
||||
*/
|
||||
public BitcoinTransaction getTransaction(String txHash) throws BitcoinException {
|
||||
return this.electrumX.getTransaction(txHash);
|
||||
}
|
||||
|
||||
/**
|
||||
* Broadcasts raw transaction to Bitcoin network.
|
||||
* <p>
|
||||
* @throws BitcoinException if error occurs
|
||||
*/
|
||||
public void broadcastTransaction(Transaction transaction) throws BitcoinException {
|
||||
this.electrumX.broadcastTransaction(transaction.bitcoinSerialize());
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -205,8 +290,9 @@ public class BTC {
|
||||
* @return transaction, or null if insufficient funds
|
||||
*/
|
||||
public Transaction buildSpend(String xprv58, String recipient, long amount) {
|
||||
Context.propagate(bitcoinjContext);
|
||||
Wallet wallet = Wallet.fromSpendingKeyB58(this.params, xprv58, DeterministicHierarchy.BIP32_STANDARDISATION_TIME_SECS);
|
||||
wallet.setUTXOProvider(new WalletAwareUTXOProvider(this, wallet, WalletAwareUTXOProvider.KeySearchMode.REQUEST_MORE_IF_ALL_SPENT));
|
||||
wallet.setUTXOProvider(new WalletAwareUTXOProvider(this, wallet, WalletAwareUTXOProvider.KeySearchMode.REQUEST_MORE_IF_ANY_SPENT));
|
||||
|
||||
Address destination = Address.fromString(this.params, recipient);
|
||||
SendRequest sendRequest = SendRequest.to(destination, Coin.valueOf(amount));
|
||||
@@ -230,6 +316,7 @@ public class BTC {
|
||||
* @return unspent BTC balance, or null if unable to determine balance
|
||||
*/
|
||||
public Long getWalletBalance(String xprv58) {
|
||||
Context.propagate(bitcoinjContext);
|
||||
Wallet wallet = Wallet.fromSpendingKeyB58(this.params, xprv58, DeterministicHierarchy.BIP32_STANDARDISATION_TIME_SECS);
|
||||
wallet.setUTXOProvider(new WalletAwareUTXOProvider(this, wallet, WalletAwareUTXOProvider.KeySearchMode.REQUEST_MORE_IF_ANY_SPENT));
|
||||
|
||||
@@ -240,6 +327,87 @@ public class BTC {
|
||||
return balance.value;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns first unused receive address given 'm' BIP32 key.
|
||||
*
|
||||
* @param xprv58 BIP32 extended Bitcoin private key
|
||||
* @return Bitcoin P2PKH address
|
||||
* @throws BitcoinException if something went wrong
|
||||
*/
|
||||
public String getUnusedReceiveAddress(String xprv58) throws BitcoinException {
|
||||
Context.propagate(bitcoinjContext);
|
||||
Wallet wallet = Wallet.fromSpendingKeyB58(this.params, xprv58, DeterministicHierarchy.BIP32_STANDARDISATION_TIME_SECS);
|
||||
DeterministicKeyChain keyChain = wallet.getActiveKeyChain();
|
||||
|
||||
keyChain.setLookaheadSize(WalletAwareUTXOProvider.LOOKAHEAD_INCREMENT);
|
||||
keyChain.maybeLookAhead();
|
||||
|
||||
final int keyChainPathSize = keyChain.getAccountPath().size();
|
||||
List<DeterministicKey> keys = new ArrayList<>(keyChain.getLeafKeys());
|
||||
|
||||
int ki = 0;
|
||||
do {
|
||||
for (; ki < keys.size(); ++ki) {
|
||||
DeterministicKey dKey = keys.get(ki);
|
||||
List<ChildNumber> dKeyPath = dKey.getPath();
|
||||
|
||||
// If keyChain is based on 'm', then make sure dKey is m/0/ki
|
||||
if (dKeyPath.size() != keyChainPathSize + 2 || dKeyPath.get(dKeyPath.size() - 2) != ChildNumber.ZERO)
|
||||
continue;
|
||||
|
||||
// Check unspent
|
||||
Address address = Address.fromKey(this.params, dKey, ScriptType.P2PKH);
|
||||
byte[] script = ScriptBuilder.createOutputScript(address).getProgram();
|
||||
|
||||
List<UnspentOutput> unspentOutputs = this.electrumX.getUnspentOutputs(script, false);
|
||||
|
||||
/*
|
||||
* If there are no unspent outputs then either:
|
||||
* a) all the outputs have been spent
|
||||
* b) address has never been used
|
||||
*
|
||||
* For case (a) we want to remember not to check this address (key) again.
|
||||
*/
|
||||
|
||||
if (unspentOutputs.isEmpty()) {
|
||||
// If this is a known key that has been spent before, then we can skip asking for transaction history
|
||||
if (this.spentKeys.contains(dKey)) {
|
||||
wallet.getActiveKeyChain().markKeyAsUsed((DeterministicKey) dKey);
|
||||
continue;
|
||||
}
|
||||
|
||||
// Ask for transaction history - if it's empty then key has never been used
|
||||
List<TransactionHash> historicTransactionHashes = this.electrumX.getAddressTransactions(script, false);
|
||||
|
||||
if (!historicTransactionHashes.isEmpty()) {
|
||||
// Fully spent key - case (a)
|
||||
this.spentKeys.add(dKey);
|
||||
wallet.getActiveKeyChain().markKeyAsUsed(dKey);
|
||||
} else {
|
||||
// Key never been used - case (b)
|
||||
return address.toString();
|
||||
}
|
||||
}
|
||||
|
||||
// Key has unspent outputs, hence used, so no good to us
|
||||
this.spentKeys.remove(dKey);
|
||||
}
|
||||
|
||||
// Generate some more keys
|
||||
keyChain.setLookaheadSize(keyChain.getLookaheadSize() + WalletAwareUTXOProvider.LOOKAHEAD_INCREMENT);
|
||||
keyChain.maybeLookAhead();
|
||||
|
||||
// This returns all keys, including those already in 'keys'
|
||||
List<DeterministicKey> allLeafKeys = keyChain.getLeafKeys();
|
||||
// Add only new keys onto our list of keys to search
|
||||
List<DeterministicKey> newKeys = allLeafKeys.subList(ki, allLeafKeys.size());
|
||||
keys.addAll(newKeys);
|
||||
// Fall-through to checking more keys as now 'ki' is smaller than 'keys.size()' again
|
||||
|
||||
// Process new keys
|
||||
} while (true);
|
||||
}
|
||||
|
||||
// UTXOProvider support
|
||||
|
||||
static class WalletAwareUTXOProvider implements UTXOProvider {
|
||||
@@ -280,9 +448,12 @@ public class BTC {
|
||||
Address address = Address.fromKey(btc.params, key, ScriptType.P2PKH);
|
||||
byte[] script = ScriptBuilder.createOutputScript(address).getProgram();
|
||||
|
||||
List<UnspentOutput> unspentOutputs = btc.electrumX.getUnspentOutputs(script);
|
||||
if (unspentOutputs == null)
|
||||
List<UnspentOutput> unspentOutputs;
|
||||
try {
|
||||
unspentOutputs = btc.electrumX.getUnspentOutputs(script, false);
|
||||
} catch (BitcoinException e) {
|
||||
throw new UTXOProviderException(String.format("Unable to fetch unspent outputs for %s", address));
|
||||
}
|
||||
|
||||
/*
|
||||
* If there are no unspent outputs then either:
|
||||
@@ -301,10 +472,12 @@ public class BTC {
|
||||
}
|
||||
|
||||
// Ask for transaction history - if it's empty then key has never been used
|
||||
List<byte[]> historicTransactionHashes = btc.electrumX.getAddressTransactions(script);
|
||||
if (historicTransactionHashes == null)
|
||||
throw new UTXOProviderException(
|
||||
String.format("Unable to fetch transaction history for %s", address));
|
||||
List<TransactionHash> historicTransactionHashes;
|
||||
try {
|
||||
historicTransactionHashes = btc.electrumX.getAddressTransactions(script, false);
|
||||
} catch (BitcoinException e) {
|
||||
throw new UTXOProviderException(String.format("Unable to fetch transaction history for %s", address));
|
||||
}
|
||||
|
||||
if (!historicTransactionHashes.isEmpty()) {
|
||||
// Fully spent key - case (a)
|
||||
@@ -320,13 +493,17 @@ public class BTC {
|
||||
}
|
||||
|
||||
// If we reach here, then there's definitely at least one unspent key
|
||||
btc.spentKeys.remove(key);
|
||||
areAllKeysSpent = false;
|
||||
|
||||
for (UnspentOutput unspentOutput : unspentOutputs) {
|
||||
List<TransactionOutput> transactionOutputs = btc.getOutputs(unspentOutput.hash);
|
||||
if (transactionOutputs == null)
|
||||
List<TransactionOutput> transactionOutputs;
|
||||
try {
|
||||
transactionOutputs = btc.getOutputs(unspentOutput.hash);
|
||||
} catch (BitcoinException e) {
|
||||
throw new UTXOProviderException(String.format("Unable to fetch outputs for TX %s",
|
||||
HashCode.fromBytes(unspentOutput.hash)));
|
||||
}
|
||||
|
||||
TransactionOutput transactionOutput = transactionOutputs.get(unspentOutput.index);
|
||||
|
||||
@@ -359,11 +536,11 @@ public class BTC {
|
||||
}
|
||||
|
||||
public int getChainHeadHeight() throws UTXOProviderException {
|
||||
Integer height = btc.electrumX.getCurrentHeight();
|
||||
if (height == null)
|
||||
try {
|
||||
return btc.electrumX.getCurrentHeight();
|
||||
} catch (BitcoinException e) {
|
||||
throw new UTXOProviderException("Unable to determine Bitcoin chain height");
|
||||
|
||||
return height.intValue();
|
||||
}
|
||||
}
|
||||
|
||||
public NetworkParameters getParams() {
|
||||
@@ -374,6 +551,7 @@ public class BTC {
|
||||
// Utility methods for us
|
||||
|
||||
private byte[] addressToScript(String base58Address) {
|
||||
Context.propagate(bitcoinjContext);
|
||||
Address address = Address.fromString(this.params, base58Address);
|
||||
return ScriptBuilder.createOutputScript(address).getProgram();
|
||||
}
|
||||
|
@@ -611,7 +611,7 @@ public class BTCACCT {
|
||||
*/
|
||||
public static CrossChainTradeData populateTradeData(Repository repository, ATData atData) throws DataException {
|
||||
ATStateData atStateData = repository.getATRepository().getLatestATState(atData.getATAddress());
|
||||
return populateTradeData(repository, atData.getCreatorPublicKey(), atStateData);
|
||||
return populateTradeData(repository, atData.getCreatorPublicKey(), atData.getCreation(), atStateData);
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -622,8 +622,8 @@ public class BTCACCT {
|
||||
* @throws DataException
|
||||
*/
|
||||
public static CrossChainTradeData populateTradeData(Repository repository, ATStateData atStateData) throws DataException {
|
||||
byte[] creatorPublicKey = repository.getATRepository().getCreatorPublicKey(atStateData.getATAddress());
|
||||
return populateTradeData(repository, creatorPublicKey, atStateData);
|
||||
ATData atData = repository.getATRepository().fromATAddress(atStateData.getATAddress());
|
||||
return populateTradeData(repository, atData.getCreatorPublicKey(), atData.getCreation(), atStateData);
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -633,7 +633,7 @@ public class BTCACCT {
|
||||
* @param atAddress
|
||||
* @throws DataException
|
||||
*/
|
||||
public static CrossChainTradeData populateTradeData(Repository repository, byte[] creatorPublicKey, ATStateData atStateData) throws DataException {
|
||||
public static CrossChainTradeData populateTradeData(Repository repository, byte[] creatorPublicKey, long creationTimestamp, ATStateData atStateData) throws DataException {
|
||||
byte[] addressBytes = new byte[25]; // for general use
|
||||
String atAddress = atStateData.getATAddress();
|
||||
|
||||
@@ -641,7 +641,7 @@ public class BTCACCT {
|
||||
|
||||
tradeData.qortalAtAddress = atAddress;
|
||||
tradeData.qortalCreator = Crypto.toAddress(creatorPublicKey);
|
||||
tradeData.creationTimestamp = atStateData.getCreation();
|
||||
tradeData.creationTimestamp = creationTimestamp;
|
||||
|
||||
Account atAccount = new Account(repository, atAddress);
|
||||
tradeData.qortBalance = atAccount.getConfirmedBalance(Asset.QORT);
|
||||
@@ -874,7 +874,8 @@ public class BTCACCT {
|
||||
String atAddress = crossChainTradeData.qortalAtAddress;
|
||||
String redeemerAddress = crossChainTradeData.qortalPartnerAddress;
|
||||
|
||||
List<MessageTransactionData> messageTransactionsData = repository.getTransactionRepository().getMessagesByRecipient(atAddress, null, null, null);
|
||||
// We don't have partner's public key so we check every message to AT
|
||||
List<MessageTransactionData> messageTransactionsData = repository.getMessageRepository().getMessagesByParticipants(null, atAddress, null, null, null);
|
||||
if (messageTransactionsData == null)
|
||||
return null;
|
||||
|
||||
|
@@ -1,9 +1,15 @@
|
||||
package org.qortal.crosschain;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.Arrays;
|
||||
import java.util.Collections;
|
||||
import java.util.HashMap;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.function.Function;
|
||||
|
||||
import org.bitcoinj.core.Address;
|
||||
import org.bitcoinj.core.Base58;
|
||||
import org.bitcoinj.core.Coin;
|
||||
import org.bitcoinj.core.ECKey;
|
||||
import org.bitcoinj.core.LegacyAddress;
|
||||
@@ -25,6 +31,10 @@ import com.google.common.primitives.Bytes;
|
||||
|
||||
public class BTCP2SH {
|
||||
|
||||
public enum Status {
|
||||
UNFUNDED, FUNDING_IN_PROGRESS, FUNDED, REDEEM_IN_PROGRESS, REDEEMED, REFUND_IN_PROGRESS, REFUNDED
|
||||
}
|
||||
|
||||
public static final int SECRET_LENGTH = 32;
|
||||
public static final int MIN_LOCKTIME = 1500000000;
|
||||
|
||||
@@ -131,9 +141,10 @@ public class BTCP2SH {
|
||||
* @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
|
||||
* @param receivingAccountInfo Bitcoin PKH used for output
|
||||
* @return Signed Bitcoin transaction for refunding P2SH
|
||||
*/
|
||||
public static Transaction buildRefundTransaction(Coin refundAmount, ECKey refundKey, List<TransactionOutput> fundingOutputs, byte[] redeemScriptBytes, long lockTime) {
|
||||
public static Transaction buildRefundTransaction(Coin refundAmount, ECKey refundKey, List<TransactionOutput> fundingOutputs, byte[] redeemScriptBytes, long lockTime, byte[] receivingAccountInfo) {
|
||||
Function<byte[], Script> refundSigScriptBuilder = (txSigBytes) -> {
|
||||
// Build scriptSig with...
|
||||
ScriptBuilder scriptBuilder = new ScriptBuilder();
|
||||
@@ -152,7 +163,7 @@ public class BTCP2SH {
|
||||
};
|
||||
|
||||
// Send funds back to funding address
|
||||
return buildP2shTransaction(refundAmount, refundKey, fundingOutputs, redeemScriptBytes, lockTime, refundSigScriptBuilder, refundKey.getPubKeyHash());
|
||||
return buildP2shTransaction(refundAmount, refundKey, fundingOutputs, redeemScriptBytes, lockTime, refundSigScriptBuilder, receivingAccountInfo);
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -233,4 +244,129 @@ public class BTCP2SH {
|
||||
return null;
|
||||
}
|
||||
|
||||
/** Returns P2SH status, given P2SH address and expected redeem/refund amount, or throws BitcoinException if error occurs. */
|
||||
public static Status determineP2shStatus(String p2shAddress, long minimumAmount) throws BitcoinException {
|
||||
final BTC btc = BTC.getInstance();
|
||||
|
||||
List<TransactionHash> transactionHashes = btc.getAddressTransactions(p2shAddress, BTC.INCLUDE_UNCONFIRMED);
|
||||
|
||||
// Sort by confirmed first, followed by ascending height
|
||||
transactionHashes.sort(TransactionHash.CONFIRMED_FIRST.thenComparing(TransactionHash::getHeight));
|
||||
|
||||
// Transaction cache
|
||||
Map<String, BitcoinTransaction> transactionsByHash = new HashMap<>();
|
||||
// HASH160(redeem script) for this p2shAddress
|
||||
byte[] ourRedeemScriptHash = addressToRedeemScriptHash(p2shAddress);
|
||||
|
||||
// Check for spends first, caching full transaction info as we progress just in case we don't return in this loop
|
||||
for (TransactionHash transactionInfo : transactionHashes) {
|
||||
BitcoinTransaction bitcoinTransaction = btc.getTransaction(transactionInfo.txHash);
|
||||
|
||||
// Cache for possible later reuse
|
||||
transactionsByHash.put(transactionInfo.txHash, bitcoinTransaction);
|
||||
|
||||
// Acceptable funding is one transaction output, so we're expecting only one input
|
||||
if (bitcoinTransaction.inputs.size() != 1)
|
||||
// Wrong number of inputs
|
||||
continue;
|
||||
|
||||
String scriptSig = bitcoinTransaction.inputs.get(0).scriptSig;
|
||||
|
||||
List<byte[]> scriptSigChunks = extractScriptSigChunks(HashCode.fromString(scriptSig).asBytes());
|
||||
if (scriptSigChunks.size() < 3 || scriptSigChunks.size() > 4)
|
||||
// Not spending one of these P2SH
|
||||
continue;
|
||||
|
||||
// Last chunk is redeem script
|
||||
byte[] redeemScriptBytes = scriptSigChunks.get(scriptSigChunks.size() - 1);
|
||||
byte[] redeemScriptHash = Crypto.hash160(redeemScriptBytes);
|
||||
if (!Arrays.equals(redeemScriptHash, ourRedeemScriptHash))
|
||||
// Not spending our specific P2SH
|
||||
continue;
|
||||
|
||||
// If we have 4 chunks, then secret is present
|
||||
return scriptSigChunks.size() == 4
|
||||
? (transactionInfo.height == 0 ? Status.REDEEM_IN_PROGRESS : Status.REDEEMED)
|
||||
: (transactionInfo.height == 0 ? Status.REFUND_IN_PROGRESS : Status.REFUNDED);
|
||||
}
|
||||
|
||||
String ourScriptPubKey = HashCode.fromBytes(addressToScriptPubKey(p2shAddress)).toString();
|
||||
|
||||
// Check for funding
|
||||
for (TransactionHash transactionInfo : transactionHashes) {
|
||||
BitcoinTransaction bitcoinTransaction = transactionsByHash.get(transactionInfo.txHash);
|
||||
if (bitcoinTransaction == null)
|
||||
// Should be present in map!
|
||||
throw new BitcoinException("Cached Bitcoin transaction now missing?");
|
||||
|
||||
// Check outputs for our specific P2SH
|
||||
for (BitcoinTransaction.Output output : bitcoinTransaction.outputs) {
|
||||
// Check amount
|
||||
if (output.value < minimumAmount)
|
||||
// Output amount too small (not taking fees into account)
|
||||
continue;
|
||||
|
||||
String scriptPubKey = output.scriptPubKey;
|
||||
if (!scriptPubKey.equals(ourScriptPubKey))
|
||||
// Not funding our specific P2SH
|
||||
continue;
|
||||
|
||||
return transactionInfo.height == 0 ? Status.FUNDING_IN_PROGRESS : Status.FUNDED;
|
||||
}
|
||||
}
|
||||
|
||||
return Status.UNFUNDED;
|
||||
}
|
||||
|
||||
private static List<byte[]> extractScriptSigChunks(byte[] scriptSigBytes) {
|
||||
List<byte[]> chunks = new ArrayList<>();
|
||||
|
||||
int offset = 0;
|
||||
int previousOffset = 0;
|
||||
while (offset < scriptSigBytes.length) {
|
||||
byte pushOp = scriptSigBytes[offset++];
|
||||
|
||||
if (pushOp < 0 || pushOp > 0x4c)
|
||||
// Unacceptable OP
|
||||
return Collections.emptyList();
|
||||
|
||||
// Special treatment for OP_PUSHDATA1
|
||||
if (pushOp == 0x4c) {
|
||||
if (offset >= scriptSigBytes.length)
|
||||
// Run out of scriptSig bytes?
|
||||
return Collections.emptyList();
|
||||
|
||||
pushOp = scriptSigBytes[offset++];
|
||||
}
|
||||
|
||||
previousOffset = offset;
|
||||
offset += Byte.toUnsignedInt(pushOp);
|
||||
|
||||
byte[] chunk = Arrays.copyOfRange(scriptSigBytes, previousOffset, offset);
|
||||
chunks.add(chunk);
|
||||
}
|
||||
|
||||
return chunks;
|
||||
}
|
||||
|
||||
private static byte[] addressToScriptPubKey(String p2shAddress) {
|
||||
// We want the HASH160 part of the P2SH address
|
||||
byte[] p2shAddressBytes = Base58.decode(p2shAddress);
|
||||
|
||||
byte[] scriptPubKey = new byte[1 + 1 + 20 + 1];
|
||||
scriptPubKey[0x00] = (byte) 0xa9; /* OP_HASH160 */
|
||||
scriptPubKey[0x01] = (byte) 0x14; /* PUSH 0x14 bytes */
|
||||
System.arraycopy(p2shAddressBytes, 1, scriptPubKey, 2, 0x14);
|
||||
scriptPubKey[0x16] = (byte) 0x87; /* OP_EQUAL */
|
||||
|
||||
return scriptPubKey;
|
||||
}
|
||||
|
||||
private static byte[] addressToRedeemScriptHash(String p2shAddress) {
|
||||
// We want the HASH160 part of the P2SH address
|
||||
byte[] p2shAddressBytes = Base58.decode(p2shAddress);
|
||||
|
||||
return Arrays.copyOfRange(p2shAddressBytes, 1, 1 + 20);
|
||||
}
|
||||
|
||||
}
|
||||
|
57
src/main/java/org/qortal/crosschain/BitcoinException.java
Normal file
57
src/main/java/org/qortal/crosschain/BitcoinException.java
Normal file
@@ -0,0 +1,57 @@
|
||||
package org.qortal.crosschain;
|
||||
|
||||
@SuppressWarnings("serial")
|
||||
public class BitcoinException extends Exception {
|
||||
|
||||
public BitcoinException() {
|
||||
super();
|
||||
}
|
||||
|
||||
public BitcoinException(String message) {
|
||||
super(message);
|
||||
}
|
||||
|
||||
public static class NetworkException extends BitcoinException {
|
||||
private final Integer daemonErrorCode;
|
||||
|
||||
public NetworkException() {
|
||||
super();
|
||||
this.daemonErrorCode = null;
|
||||
}
|
||||
|
||||
public NetworkException(String message) {
|
||||
super(message);
|
||||
this.daemonErrorCode = null;
|
||||
}
|
||||
|
||||
public NetworkException(int errorCode, String message) {
|
||||
super(message);
|
||||
this.daemonErrorCode = errorCode;
|
||||
}
|
||||
|
||||
public Integer getDaemonErrorCode() {
|
||||
return this.daemonErrorCode;
|
||||
}
|
||||
}
|
||||
|
||||
public static class NotFoundException extends BitcoinException {
|
||||
public NotFoundException() {
|
||||
super();
|
||||
}
|
||||
|
||||
public NotFoundException(String message) {
|
||||
super(message);
|
||||
}
|
||||
}
|
||||
|
||||
public static class InsufficientFundsException extends BitcoinException {
|
||||
public InsufficientFundsException() {
|
||||
super();
|
||||
}
|
||||
|
||||
public InsufficientFundsException(String message) {
|
||||
super(message);
|
||||
}
|
||||
}
|
||||
|
||||
}
|
@@ -0,0 +1,31 @@
|
||||
package org.qortal.crosschain;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
interface BitcoinNetworkProvider {
|
||||
|
||||
/** Returns current blockchain height. */
|
||||
int getCurrentHeight() throws BitcoinException;
|
||||
|
||||
/** Returns a list of raw block headers, starting at <tt>startHeight</tt> (inclusive), up to <tt>count</tt> max. */
|
||||
List<byte[]> getRawBlockHeaders(int startHeight, int count) throws BitcoinException;
|
||||
|
||||
/** Returns balance of address represented by <tt>scriptPubKey</tt>. */
|
||||
long getConfirmedBalance(byte[] scriptPubKey) throws BitcoinException;
|
||||
|
||||
/** Returns raw, serialized, transaction bytes given <tt>txHash</tt>. */
|
||||
byte[] getRawTransaction(String txHash) throws BitcoinException;
|
||||
|
||||
/** Returns unpacked transaction given <tt>txHash</tt>. */
|
||||
BitcoinTransaction getTransaction(String txHash) throws BitcoinException;
|
||||
|
||||
/** Returns list of transaction hashes (and heights) for address represented by <tt>scriptPubKey</tt>, optionally including unconfirmed transactions. */
|
||||
List<TransactionHash> getAddressTransactions(byte[] scriptPubKey, boolean includeUnconfirmed) throws BitcoinException;
|
||||
|
||||
/** Returns list of unspent transaction outputs for address represented by <tt>scriptPubKey</tt>, optionally including unconfirmed transactions. */
|
||||
List<UnspentOutput> getUnspentOutputs(byte[] scriptPubKey, boolean includeUnconfirmed) throws BitcoinException;
|
||||
|
||||
/** Broadcasts raw, serialized, transaction bytes to network, returning success/failure. */
|
||||
boolean broadcastTransaction(byte[] rawTransaction) throws BitcoinException;
|
||||
|
||||
}
|
70
src/main/java/org/qortal/crosschain/BitcoinTransaction.java
Normal file
70
src/main/java/org/qortal/crosschain/BitcoinTransaction.java
Normal file
@@ -0,0 +1,70 @@
|
||||
package org.qortal.crosschain;
|
||||
|
||||
import java.util.List;
|
||||
import java.util.stream.Collectors;
|
||||
|
||||
public class BitcoinTransaction {
|
||||
|
||||
public final String txHash;
|
||||
public final int size;
|
||||
public final int locktime;
|
||||
// Not present if transaction is unconfirmed
|
||||
public final Integer timestamp;
|
||||
|
||||
public static class Input {
|
||||
public final String scriptSig;
|
||||
public final int sequence;
|
||||
public final String outputTxHash;
|
||||
public final int outputVout;
|
||||
|
||||
public Input(String scriptSig, int sequence, String outputTxHash, int outputVout) {
|
||||
this.scriptSig = scriptSig;
|
||||
this.sequence = sequence;
|
||||
this.outputTxHash = outputTxHash;
|
||||
this.outputVout = outputVout;
|
||||
}
|
||||
|
||||
public String toString() {
|
||||
return String.format("{output %s:%d, sequence %d, scriptSig %s}",
|
||||
this.outputTxHash, this.outputVout, this.sequence, this.scriptSig);
|
||||
}
|
||||
}
|
||||
public final List<Input> inputs;
|
||||
|
||||
public static class Output {
|
||||
public final String scriptPubKey;
|
||||
public final long value;
|
||||
|
||||
public Output(String scriptPubKey, long value) {
|
||||
this.scriptPubKey = scriptPubKey;
|
||||
this.value = value;
|
||||
}
|
||||
|
||||
public String toString() {
|
||||
return String.format("{value %d, scriptPubKey %s}", this.value, this.scriptPubKey);
|
||||
}
|
||||
}
|
||||
public final List<Output> outputs;
|
||||
|
||||
public BitcoinTransaction(String txHash, int size, int locktime, Integer timestamp,
|
||||
List<Input> inputs, List<Output> outputs) {
|
||||
this.txHash = txHash;
|
||||
this.size = size;
|
||||
this.locktime = locktime;
|
||||
this.timestamp = timestamp;
|
||||
this.inputs = inputs;
|
||||
this.outputs = outputs;
|
||||
}
|
||||
|
||||
public String toString() {
|
||||
return String.format("txHash %s, size %d, locktime %d, timestamp %d\n"
|
||||
+ "\tinputs: [%s]\n"
|
||||
+ "\toutputs: [%s]\n",
|
||||
this.txHash,
|
||||
this.size,
|
||||
this.locktime,
|
||||
this.timestamp,
|
||||
this.inputs.stream().map(Input::toString).collect(Collectors.joining(",\n\t\t")),
|
||||
this.outputs.stream().map(Output::toString).collect(Collectors.joining(",\n\t\t")));
|
||||
}
|
||||
}
|
@@ -14,8 +14,9 @@ import java.util.NoSuchElementException;
|
||||
import java.util.Random;
|
||||
import java.util.Scanner;
|
||||
import java.util.Set;
|
||||
import java.util.regex.Matcher;
|
||||
import java.util.regex.Pattern;
|
||||
|
||||
import javax.net.ssl.SSLSocket;
|
||||
import javax.net.ssl.SSLSocketFactory;
|
||||
|
||||
import org.apache.logging.log4j.LogManager;
|
||||
@@ -35,17 +36,27 @@ public class ElectrumX {
|
||||
private static final Logger LOGGER = LogManager.getLogger(ElectrumX.class);
|
||||
private static final Random RANDOM = new Random();
|
||||
|
||||
private static final double MIN_PROTOCOL_VERSION = 1.2;
|
||||
|
||||
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 String MAIN_GENESIS_HASH = "000000000019d6689c085ae165831e934ff763ae46a2a6c172b3f1b60a8ce26f";
|
||||
private static final String TEST3_GENESIS_HASH = "000000000933ea01ad0ee984209779baaec3ced90fa3f408719526f8d77f4943";
|
||||
// We won't know REGTEST (i.e. local) genesis block hash
|
||||
|
||||
// "message": "daemon error: DaemonError({'code': -5, 'message': 'No such mempool or blockchain transaction. Use gettransaction for wallet transactions.'})"
|
||||
private static final Pattern DAEMON_ERROR_REGEX = Pattern.compile("DaemonError\\(\\{.*'code': ?(-?[0-9]+).*\\}\\)\\z"); // Capture 'code' inside curly-brace content
|
||||
|
||||
// Key: Bitcoin network (e.g. "MAIN", "TEST3", "REGTEST"), value: ElectrumX instance
|
||||
private static final Map<String, ElectrumX> instances = new HashMap<>();
|
||||
|
||||
static class Server {
|
||||
private static class Server {
|
||||
String hostname;
|
||||
|
||||
enum ConnectionType { TCP, SSL };
|
||||
enum ConnectionType { TCP, SSL }
|
||||
ConnectionType connectionType;
|
||||
|
||||
int port;
|
||||
@@ -82,7 +93,9 @@ public class ElectrumX {
|
||||
}
|
||||
}
|
||||
private Set<Server> servers = new HashSet<>();
|
||||
private List<Server> remainingServers = new ArrayList<>();
|
||||
|
||||
private String expectedGenesisHash;
|
||||
private Server currentServer;
|
||||
private Socket socket;
|
||||
private Scanner scanner;
|
||||
@@ -93,34 +106,59 @@ public class ElectrumX {
|
||||
private ElectrumX(String bitcoinNetwork) {
|
||||
switch (bitcoinNetwork) {
|
||||
case "MAIN":
|
||||
servers.addAll(Arrays.asList(
|
||||
this.expectedGenesisHash = MAIN_GENESIS_HASH;
|
||||
|
||||
this.servers.addAll(Arrays.asList(
|
||||
// Servers chosen on NO BASIS WHATSOEVER from various sources!
|
||||
new Server("tardis.bauerj.eu", Server.ConnectionType.SSL, 50002),
|
||||
new Server("rbx.curalle.ovh", Server.ConnectionType.SSL, 50002),
|
||||
new Server("quick.electumx.live", Server.ConnectionType.SSL, 50002),
|
||||
new Server("enode.duckdns.org", Server.ConnectionType.SSL, 50002),
|
||||
new Server("electrumx.ddns.net", Server.ConnectionType.SSL, 50002),
|
||||
new Server("electrumx.ml", Server.ConnectionType.SSL, 50002),
|
||||
new Server("electrum.eff.ro", Server.ConnectionType.SSL, 50002),
|
||||
new Server("electrum.bitkoins.nl", Server.ConnectionType.SSL, 50512),
|
||||
new Server("E-X.not.fyi", Server.ConnectionType.SSL, 50002),
|
||||
new Server("btc.electroncash.dk", Server.ConnectionType.SSL, 60002),
|
||||
new Server("electrum.blockstream.info", Server.ConnectionType.TCP, 50001),
|
||||
new Server("electrum.blockstream.info", Server.ConnectionType.SSL, 50002),
|
||||
new Server("electrumx.electricnewyear.net", Server.ConnectionType.SSL, 50002),
|
||||
new Server("dxm.no-ip.biz", Server.ConnectionType.TCP, 50001),
|
||||
new Server("kirsche.emzy.de", Server.ConnectionType.TCP, 50001),
|
||||
new Server("2AZZARITA.hopto.org", Server.ConnectionType.TCP, 50001),
|
||||
new Server("xtrum.com", Server.ConnectionType.TCP, 50001),
|
||||
new Server("electrum.srvmin.network", Server.ConnectionType.TCP, 50001),
|
||||
new Server("electrumx.alexridevski.net", Server.ConnectionType.TCP, 50001),
|
||||
new Server("bitcoin.lukechilds.co", Server.ConnectionType.TCP, 50001),
|
||||
new Server("electrum.poiuty.com", Server.ConnectionType.TCP, 50001),
|
||||
new Server("horsey.cryptocowboys.net", Server.ConnectionType.TCP, 50001),
|
||||
new Server("electrum.emzy.de", Server.ConnectionType.TCP, 50001),
|
||||
new Server("electrum-server.ninja", Server.ConnectionType.TCP, 50081),
|
||||
new Server("bitcoin.electrumx.multicoin.co", Server.ConnectionType.TCP, 50001),
|
||||
new Server("esx.geekhosters.com", Server.ConnectionType.TCP, 50001),
|
||||
new Server("bitcoin.grey.pw", Server.ConnectionType.TCP, 50003),
|
||||
new Server("exs.ignorelist.com", Server.ConnectionType.TCP, 50001),
|
||||
new Server("electrum.coinext.com.br", Server.ConnectionType.TCP, 50001),
|
||||
new Server("bitcoin.aranguren.org", Server.ConnectionType.TCP, 50001),
|
||||
new Server("skbxmit.coinjoined.com", Server.ConnectionType.TCP, 50001),
|
||||
new Server("alviss.coinjoined.com", Server.ConnectionType.TCP, 50001),
|
||||
new Server("electrum2.privateservers.network", Server.ConnectionType.TCP, 50001),
|
||||
new Server("electrumx.schulzemic.net", Server.ConnectionType.TCP, 50001),
|
||||
new Server("bitcoins.sk", Server.ConnectionType.TCP, 56001),
|
||||
new Server("node.mendonca.xyz", Server.ConnectionType.TCP, 50001),
|
||||
new Server("bitcoin.aranguren.org", Server.ConnectionType.TCP, 50001)));
|
||||
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),
|
||||
this.expectedGenesisHash = TEST3_GENESIS_HASH;
|
||||
|
||||
this.servers.addAll(Arrays.asList(
|
||||
new Server("electrum.blockstream.info", Server.ConnectionType.TCP, 60001),
|
||||
new Server("electrum.blockstream.info", Server.ConnectionType.SSL, 60002),
|
||||
new Server("electrumx-test.1209k.com", Server.ConnectionType.SSL, 50002),
|
||||
new Server("testnet.qtornado.com", Server.ConnectionType.TCP, 51001),
|
||||
new Server("testnet.qtornado.com", Server.ConnectionType.SSL, 51002),
|
||||
new Server("testnet.aranguren.org", Server.ConnectionType.TCP, 51001),
|
||||
new Server("testnet.aranguren.org", Server.ConnectionType.SSL, 51002),
|
||||
new Server("testnet.hsmiths.com", Server.ConnectionType.SSL, 53012)));
|
||||
break;
|
||||
|
||||
case "REGTEST":
|
||||
servers.addAll(Arrays.asList(
|
||||
this.expectedGenesisHash = null;
|
||||
|
||||
this.servers.addAll(Arrays.asList(
|
||||
new Server("localhost", Server.ConnectionType.TCP, DEFAULT_TCP_PORT),
|
||||
new Server("localhost", Server.ConnectionType.SSL, DEFAULT_SSL_PORT)));
|
||||
break;
|
||||
@@ -130,7 +168,6 @@ public class ElectrumX {
|
||||
}
|
||||
|
||||
LOGGER.debug(() -> String.format("Starting ElectrumX support for %s Bitcoin network", bitcoinNetwork));
|
||||
rpc("server.banner");
|
||||
}
|
||||
|
||||
/** Returns ElectrumX instance linked to passed Bitcoin network, one of "MAIN", "TEST3" or "REGTEST". */
|
||||
@@ -143,35 +180,50 @@ public class ElectrumX {
|
||||
|
||||
// Methods for use by other classes
|
||||
|
||||
public Integer getCurrentHeight() {
|
||||
/**
|
||||
* Returns current blockchain height.
|
||||
* <p>
|
||||
* @throws BitcoinException if error occurs
|
||||
*/
|
||||
public int getCurrentHeight() throws BitcoinException {
|
||||
Object blockObj = this.rpc("blockchain.headers.subscribe");
|
||||
if (!(blockObj instanceof JSONObject))
|
||||
return null;
|
||||
throw new BitcoinException.NetworkException("Unexpected output from ElectrumX blockchain.headers.subscribe RPC");
|
||||
|
||||
JSONObject blockJson = (JSONObject) blockObj;
|
||||
|
||||
if (!blockJson.containsKey("height"))
|
||||
return null;
|
||||
Object heightObj = blockJson.get("height");
|
||||
|
||||
return ((Long) blockJson.get("height")).intValue();
|
||||
if (!(heightObj instanceof Long))
|
||||
throw new BitcoinException.NetworkException("Missing/invalid 'height' in JSON from ElectrumX blockchain.headers.subscribe RPC");
|
||||
|
||||
return ((Long) heightObj).intValue();
|
||||
}
|
||||
|
||||
public List<byte[]> getBlockHeaders(int startHeight, long count) {
|
||||
/**
|
||||
* Returns list of raw block headers, starting from <tt>startHeight</tt> inclusive.
|
||||
* <p>
|
||||
* @throws BitcoinException if error occurs
|
||||
*/
|
||||
public List<byte[]> getBlockHeaders(int startHeight, long count) throws BitcoinException {
|
||||
Object blockObj = this.rpc("blockchain.block.headers", startHeight, count);
|
||||
if (!(blockObj instanceof JSONObject))
|
||||
return null;
|
||||
throw new BitcoinException.NetworkException("Unexpected output from ElectrumX blockchain.block.headers RPC");
|
||||
|
||||
JSONObject blockJson = (JSONObject) blockObj;
|
||||
|
||||
if (!blockJson.containsKey("count") || !blockJson.containsKey("hex"))
|
||||
return null;
|
||||
Object countObj = blockJson.get("count");
|
||||
Object hexObj = blockJson.get("hex");
|
||||
|
||||
Long returnedCount = (Long) blockJson.get("count");
|
||||
String hex = (String) blockJson.get("hex");
|
||||
if (!(countObj instanceof Long) || !(hexObj instanceof String))
|
||||
throw new BitcoinException.NetworkException("Missing/invalid 'count' or 'hex' entries in JSON from ElectrumX blockchain.block.headers RPC");
|
||||
|
||||
Long returnedCount = (Long) countObj;
|
||||
String hex = (String) hexObj;
|
||||
|
||||
byte[] raw = HashCode.fromString(hex).asBytes();
|
||||
if (raw.length != returnedCount * BLOCK_HEADER_LENGTH)
|
||||
return null;
|
||||
throw new BitcoinException.NetworkException("Unexpected raw header length in JSON from ElectrumX blockchain.block.headers RPC");
|
||||
|
||||
List<byte[]> rawBlockHeaders = new ArrayList<>(returnedCount.intValue());
|
||||
for (int i = 0; i < returnedCount; ++i)
|
||||
@@ -180,46 +232,43 @@ public class ElectrumX {
|
||||
return rawBlockHeaders;
|
||||
}
|
||||
|
||||
/** Returns confirmed balance, based on passed payment script, or null if there was an error or no known balance. */
|
||||
public Long getBalance(byte[] script) {
|
||||
/**
|
||||
* Returns confirmed balance, based on passed payment script.
|
||||
* <p>
|
||||
* @return confirmed balance, or zero if script unknown
|
||||
* @throws BitcoinException if there was an error
|
||||
*/
|
||||
public long getConfirmedBalance(byte[] script) throws BitcoinException {
|
||||
byte[] scriptHash = Crypto.digest(script);
|
||||
Bytes.reverse(scriptHash);
|
||||
|
||||
Object balanceObj = this.rpc("blockchain.scripthash.get_balance", HashCode.fromBytes(scriptHash).toString());
|
||||
if (!(balanceObj instanceof JSONObject))
|
||||
return null;
|
||||
throw new BitcoinException.NetworkException("Unexpected output from ElectrumX blockchain.scripthash.get_balance RPC");
|
||||
|
||||
JSONObject balanceJson = (JSONObject) balanceObj;
|
||||
|
||||
if (!balanceJson.containsKey("confirmed"))
|
||||
return null;
|
||||
Object confirmedBalanceObj = balanceJson.get("confirmed");
|
||||
|
||||
if (!(confirmedBalanceObj instanceof Long))
|
||||
throw new BitcoinException.NetworkException("Missing confirmed balance from ElectrumX blockchain.scripthash.get_balance RPC");
|
||||
|
||||
return (Long) balanceJson.get("confirmed");
|
||||
}
|
||||
|
||||
/** Unspent output info as returned by ElectrumX network. */
|
||||
public static class UnspentOutput {
|
||||
public final byte[] hash;
|
||||
public final int index;
|
||||
public final int height;
|
||||
public final long value;
|
||||
|
||||
public UnspentOutput(byte[] hash, int index, int height, long value) {
|
||||
this.hash = hash;
|
||||
this.index = index;
|
||||
this.height = height;
|
||||
this.value = value;
|
||||
}
|
||||
}
|
||||
|
||||
/** Returns list of unspent outputs pertaining to passed payment script, or null if there was an error. */
|
||||
public List<UnspentOutput> getUnspentOutputs(byte[] script) {
|
||||
/**
|
||||
* Returns list of unspent outputs pertaining to passed payment script.
|
||||
* <p>
|
||||
* @return list of unspent outputs, or empty list if script unknown
|
||||
* @throws BitcoinException if there was an error.
|
||||
*/
|
||||
public List<UnspentOutput> getUnspentOutputs(byte[] script, boolean includeUnconfirmed) throws BitcoinException {
|
||||
byte[] scriptHash = Crypto.digest(script);
|
||||
Bytes.reverse(scriptHash);
|
||||
|
||||
Object unspentJson = this.rpc("blockchain.scripthash.listunspent", HashCode.fromBytes(scriptHash).toString());
|
||||
if (!(unspentJson instanceof JSONArray))
|
||||
return null;
|
||||
throw new BitcoinException("Expected array output from ElectrumX blockchain.scripthash.listunspent RPC");
|
||||
|
||||
List<UnspentOutput> unspentOutputs = new ArrayList<>();
|
||||
for (Object rawUnspent : (JSONArray) unspentJson) {
|
||||
@@ -227,7 +276,7 @@ public class ElectrumX {
|
||||
|
||||
int height = ((Long) unspent.get("height")).intValue();
|
||||
// We only want unspent outputs from confirmed transactions (and definitely not mempool duplicates with height 0)
|
||||
if (height <= 0)
|
||||
if (!includeUnconfirmed && height <= 0)
|
||||
continue;
|
||||
|
||||
byte[] txHash = HashCode.fromString((String) unspent.get("tx_hash")).asBytes();
|
||||
@@ -240,68 +289,163 @@ public class ElectrumX {
|
||||
return unspentOutputs;
|
||||
}
|
||||
|
||||
/** Returns raw transaction for passed transaction hash, or null if not found. */
|
||||
public byte[] getRawTransaction(byte[] txHash) {
|
||||
Object rawTransactionHex = this.rpc("blockchain.transaction.get", HashCode.fromBytes(txHash).toString());
|
||||
/**
|
||||
* Returns raw transaction for passed transaction hash.
|
||||
* <p>
|
||||
* @throws BitcoinException.NotFoundException if transaction not found
|
||||
* @throws BitcoinException if error occurs
|
||||
*/
|
||||
public byte[] getRawTransaction(byte[] txHash) throws BitcoinException {
|
||||
Object rawTransactionHex;
|
||||
try {
|
||||
rawTransactionHex = this.rpc("blockchain.transaction.get", HashCode.fromBytes(txHash).toString());
|
||||
} catch (BitcoinException.NetworkException e) {
|
||||
// DaemonError({'code': -5, 'message': 'No such mempool or blockchain transaction. Use gettransaction for wallet transactions.'})
|
||||
if (Integer.valueOf(-5).equals(e.getDaemonErrorCode()))
|
||||
throw new BitcoinException.NotFoundException(e.getMessage());
|
||||
|
||||
throw e;
|
||||
}
|
||||
|
||||
if (!(rawTransactionHex instanceof String))
|
||||
return null;
|
||||
throw new BitcoinException.NetworkException("Expected hex string as raw transaction from ElectrumX blockchain.transaction.get RPC");
|
||||
|
||||
return HashCode.fromString((String) rawTransactionHex).asBytes();
|
||||
}
|
||||
|
||||
/** Returns list of raw transactions, relating to passed payment script, if null if there's an error. */
|
||||
public List<byte[]> getAddressTransactions(byte[] script) {
|
||||
/**
|
||||
* Returns transaction info for passed transaction hash.
|
||||
* <p>
|
||||
* @throws BitcoinException.NotFoundException if transaction not found
|
||||
* @throws BitcoinException if error occurs
|
||||
*/
|
||||
public BitcoinTransaction getTransaction(String txHash) throws BitcoinException {
|
||||
Object transactionObj;
|
||||
try {
|
||||
transactionObj = this.rpc("blockchain.transaction.get", txHash, true);
|
||||
} catch (BitcoinException.NetworkException e) {
|
||||
// DaemonError({'code': -5, 'message': 'No such mempool or blockchain transaction. Use gettransaction for wallet transactions.'})
|
||||
if (Integer.valueOf(-5).equals(e.getDaemonErrorCode()))
|
||||
throw new BitcoinException.NotFoundException(e.getMessage());
|
||||
|
||||
throw e;
|
||||
}
|
||||
|
||||
if (!(transactionObj instanceof JSONObject))
|
||||
throw new BitcoinException.NetworkException("Expected JSONObject as response from ElectrumX blockchain.transaction.get RPC");
|
||||
|
||||
JSONObject transactionJson = (JSONObject) transactionObj;
|
||||
|
||||
Object inputsObj = transactionJson.get("vin");
|
||||
if (!(inputsObj instanceof JSONArray))
|
||||
throw new BitcoinException.NetworkException("Expected JSONArray for 'vin' from ElectrumX blockchain.transaction.get RPC");
|
||||
|
||||
Object outputsObj = transactionJson.get("vout");
|
||||
if (!(outputsObj instanceof JSONArray))
|
||||
throw new BitcoinException.NetworkException("Expected JSONArray for 'vout' from ElectrumX blockchain.transaction.get RPC");
|
||||
|
||||
try {
|
||||
int size = ((Long) transactionJson.get("size")).intValue();
|
||||
int locktime = ((Long) transactionJson.get("locktime")).intValue();
|
||||
|
||||
// Timestamp might not be present, e.g. for unconfirmed transaction
|
||||
Object timeObj = transactionJson.get("time");
|
||||
Integer timestamp = timeObj != null
|
||||
? ((Long) timeObj).intValue()
|
||||
: null;
|
||||
|
||||
List<BitcoinTransaction.Input> inputs = new ArrayList<>();
|
||||
for (Object inputObj : (JSONArray) inputsObj) {
|
||||
JSONObject inputJson = (JSONObject) inputObj;
|
||||
|
||||
String scriptSig = (String) ((JSONObject) inputJson.get("scriptSig")).get("hex");
|
||||
int sequence = ((Long) inputJson.get("sequence")).intValue();
|
||||
String outputTxHash = (String) inputJson.get("txid");
|
||||
int outputVout = ((Long) inputJson.get("vout")).intValue();
|
||||
|
||||
inputs.add(new BitcoinTransaction.Input(scriptSig, sequence, outputTxHash, outputVout));
|
||||
}
|
||||
|
||||
List<BitcoinTransaction.Output> outputs = new ArrayList<>();
|
||||
for (Object outputObj : (JSONArray) outputsObj) {
|
||||
JSONObject outputJson = (JSONObject) outputObj;
|
||||
|
||||
String scriptPubKey = (String) ((JSONObject) outputJson.get("scriptPubKey")).get("hex");
|
||||
long value = (long) (((Double) outputJson.get("value")) * 1e8);
|
||||
|
||||
outputs.add(new BitcoinTransaction.Output(scriptPubKey, value));
|
||||
}
|
||||
|
||||
return new BitcoinTransaction(txHash, size, locktime, timestamp, inputs, outputs);
|
||||
} catch (NullPointerException | ClassCastException e) {
|
||||
// Unexpected / invalid response from ElectrumX server
|
||||
}
|
||||
|
||||
throw new BitcoinException.NetworkException("Unexpected JSON format from ElectrumX blockchain.transaction.get RPC");
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns list of transactions, relating to passed payment script.
|
||||
* <p>
|
||||
* @return list of related transactions, or empty list if script unknown
|
||||
* @throws BitcoinException if error occurs
|
||||
*/
|
||||
public List<TransactionHash> getAddressTransactions(byte[] script, boolean includeUnconfirmed) throws BitcoinException {
|
||||
byte[] scriptHash = Crypto.digest(script);
|
||||
Bytes.reverse(scriptHash);
|
||||
|
||||
Object transactionsJson = this.rpc("blockchain.scripthash.get_history", HashCode.fromBytes(scriptHash).toString());
|
||||
if (!(transactionsJson instanceof JSONArray))
|
||||
return null;
|
||||
throw new BitcoinException.NetworkException("Expected array output from ElectrumX blockchain.scripthash.get_history RPC");
|
||||
|
||||
List<byte[]> rawTransactions = new ArrayList<>();
|
||||
List<TransactionHash> transactionHashes = new ArrayList<>();
|
||||
|
||||
for (Object rawTransactionInfo : (JSONArray) transactionsJson) {
|
||||
JSONObject transactionInfo = (JSONObject) rawTransactionInfo;
|
||||
|
||||
// We only want confirmed transactions
|
||||
if (!transactionInfo.containsKey("height"))
|
||||
Long height = (Long) transactionInfo.get("height");
|
||||
if (!includeUnconfirmed && (height == null || height == 0))
|
||||
// We only want confirmed transactions
|
||||
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());
|
||||
transactionHashes.add(new TransactionHash(height.intValue(), txHash));
|
||||
}
|
||||
|
||||
return rawTransactions;
|
||||
return transactionHashes;
|
||||
}
|
||||
|
||||
/** Returns true if raw transaction successfully broadcast. */
|
||||
public boolean broadcastTransaction(byte[] transactionBytes) {
|
||||
/**
|
||||
* Broadcasts raw transaction to Bitcoin network.
|
||||
* <p>
|
||||
* @throws BitcoinException if error occurs
|
||||
*/
|
||||
public void broadcastTransaction(byte[] transactionBytes) throws BitcoinException {
|
||||
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;
|
||||
// We're expecting a simple string that is the transaction hash
|
||||
if (!(rawBroadcastResult instanceof String))
|
||||
throw new BitcoinException.NetworkException("Unexpected response from ElectrumX blockchain.transaction.broadcast RPC");
|
||||
}
|
||||
|
||||
// Class-private utility methods
|
||||
|
||||
/** Query current server for its list of peer servers, and return those we can parse. */
|
||||
private Set<Server> serverPeersSubscribe() {
|
||||
/**
|
||||
* Query current server for its list of peer servers, and return those we can parse.
|
||||
* <p>
|
||||
* @throws BitcoinException
|
||||
* @throws ClassCastException to be handled by caller
|
||||
*/
|
||||
private Set<Server> serverPeersSubscribe() throws BitcoinException {
|
||||
Set<Server> newServers = new HashSet<>();
|
||||
|
||||
Object peers = this.connectedRpc("server.peers.subscribe");
|
||||
if (!(peers instanceof JSONArray))
|
||||
return newServers;
|
||||
|
||||
for (Object rawPeer : (JSONArray) peers) {
|
||||
JSONArray peer = (JSONArray) rawPeer;
|
||||
if (peer.size() < 3)
|
||||
// We're expecting at least 3 fields for each peer entry: IP, hostname, features
|
||||
continue;
|
||||
|
||||
String hostname = (String) peer.get(1);
|
||||
@@ -322,9 +466,14 @@ public class ElectrumX {
|
||||
connectionType = Server.ConnectionType.TCP;
|
||||
port = DEFAULT_TCP_PORT;
|
||||
break;
|
||||
|
||||
default:
|
||||
// e.g. could be 'v' for protocol version, or 'p' for pruning limit
|
||||
break;
|
||||
}
|
||||
|
||||
if (connectionType == null)
|
||||
// We couldn't extract any peer connection info?
|
||||
continue;
|
||||
|
||||
// Possible non-default port?
|
||||
@@ -344,8 +493,16 @@ public class ElectrumX {
|
||||
return newServers;
|
||||
}
|
||||
|
||||
/** Return output from RPC call, with automatic reconnection to different server if needed. */
|
||||
private synchronized Object rpc(String method, Object...params) {
|
||||
/**
|
||||
* Performs RPC call, with automatic reconnection to different server if needed.
|
||||
* <p>
|
||||
* @return "result" object from within JSON output
|
||||
* @throws BitcoinException if server returns error or something goes wrong
|
||||
*/
|
||||
private synchronized Object rpc(String method, Object...params) throws BitcoinException {
|
||||
if (this.remainingServers.isEmpty())
|
||||
this.remainingServers.addAll(this.servers);
|
||||
|
||||
while (haveConnection()) {
|
||||
Object response = connectedRpc(method, params);
|
||||
if (response != null)
|
||||
@@ -360,18 +517,17 @@ public class ElectrumX {
|
||||
this.scanner = null;
|
||||
}
|
||||
|
||||
return null;
|
||||
// Failed to perform RPC - maybe lack of servers?
|
||||
throw new BitcoinException.NetworkException("Failed to perform Bitcoin RPC");
|
||||
}
|
||||
|
||||
/** Returns true if we have, or create, a connection to an ElectrumX server. */
|
||||
private boolean haveConnection() {
|
||||
private boolean haveConnection() throws BitcoinException {
|
||||
if (this.currentServer != null)
|
||||
return true;
|
||||
|
||||
List<Server> remainingServers = new ArrayList<>(this.servers);
|
||||
|
||||
while (!remainingServers.isEmpty()) {
|
||||
Server server = remainingServers.remove(RANDOM.nextInt(remainingServers.size()));
|
||||
while (!this.remainingServers.isEmpty()) {
|
||||
Server server = this.remainingServers.remove(RANDOM.nextInt(this.remainingServers.size()));
|
||||
LOGGER.trace(() -> String.format("Connecting to %s", server));
|
||||
|
||||
try {
|
||||
@@ -384,23 +540,41 @@ public class ElectrumX {
|
||||
|
||||
if (server.connectionType == Server.ConnectionType.SSL) {
|
||||
SSLSocketFactory factory = TrustlessSSLSocketFactory.getSocketFactory();
|
||||
this.socket = (SSLSocket) factory.createSocket(this.socket, server.hostname, server.port, true);
|
||||
this.socket = 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
|
||||
// Check connection is suitable by asking for server features, including genesis block hash
|
||||
JSONObject featuresJson = (JSONObject) this.connectedRpc("server.features");
|
||||
|
||||
if (featuresJson == null || Double.valueOf((String) featuresJson.get("protocol_min")) < MIN_PROTOCOL_VERSION)
|
||||
continue;
|
||||
|
||||
if (this.expectedGenesisHash != null && !((String) featuresJson.get("genesis_hash")).equals(this.expectedGenesisHash))
|
||||
continue;
|
||||
|
||||
// Ask for more servers
|
||||
Set<Server> moreServers = serverPeersSubscribe();
|
||||
// Discard duplicate servers we already know
|
||||
moreServers.removeAll(this.servers);
|
||||
remainingServers.addAll(moreServers);
|
||||
// Add to both lists
|
||||
this.remainingServers.addAll(moreServers);
|
||||
this.servers.addAll(moreServers);
|
||||
|
||||
LOGGER.debug(() -> String.format("Connected to %s", server));
|
||||
this.currentServer = server;
|
||||
return true;
|
||||
} catch (IOException e) {
|
||||
} catch (IOException | BitcoinException | ClassCastException | NullPointerException e) {
|
||||
// Try another server...
|
||||
if (this.socket != null && !this.socket.isClosed())
|
||||
try {
|
||||
this.socket.close();
|
||||
} catch (IOException e1) {
|
||||
// We did try...
|
||||
}
|
||||
|
||||
this.socket = null;
|
||||
this.scanner = null;
|
||||
}
|
||||
@@ -409,11 +583,20 @@ public class ElectrumX {
|
||||
return false;
|
||||
}
|
||||
|
||||
/**
|
||||
* Perform RPC using currently connected server.
|
||||
* <p>
|
||||
* @param method
|
||||
* @param params
|
||||
* @return response Object, or null if server fails to respond
|
||||
* @throws BitcoinException if server returns error
|
||||
*/
|
||||
@SuppressWarnings("unchecked")
|
||||
private Object connectedRpc(String method, Object...params) {
|
||||
private Object connectedRpc(String method, Object...params) throws BitcoinException {
|
||||
JSONObject requestJson = new JSONObject();
|
||||
requestJson.put("id", this.nextId++);
|
||||
requestJson.put("method", method);
|
||||
requestJson.put("jsonrpc", "2.0");
|
||||
|
||||
JSONArray requestParams = new JSONArray();
|
||||
requestParams.addAll(Arrays.asList(params));
|
||||
@@ -428,20 +611,52 @@ public class ElectrumX {
|
||||
this.socket.getOutputStream().write(request.getBytes());
|
||||
response = scanner.next();
|
||||
} catch (IOException | NoSuchElementException e) {
|
||||
// Unable to send, or receive -- try another server?
|
||||
return null;
|
||||
}
|
||||
|
||||
LOGGER.trace(() -> String.format("Response: %s", response));
|
||||
|
||||
if (response.isEmpty())
|
||||
// Empty response - try another server?
|
||||
return null;
|
||||
|
||||
Object responseObj = JSONValue.parse(response);
|
||||
if (!(responseObj instanceof JSONObject))
|
||||
// Unexpected response - try another server?
|
||||
return null;
|
||||
|
||||
JSONObject responseJson = (JSONObject) responseObj;
|
||||
|
||||
Object errorObj = responseJson.get("error");
|
||||
if (errorObj != null) {
|
||||
if (!(errorObj instanceof JSONObject))
|
||||
throw new BitcoinException.NetworkException(String.format("Unexpected error response from ElectrumX RPC %s", method));
|
||||
|
||||
JSONObject errorJson = (JSONObject) errorObj;
|
||||
|
||||
Object messageObj = errorJson.get("message");
|
||||
|
||||
if (!(messageObj instanceof String))
|
||||
throw new BitcoinException.NetworkException(String.format("Missing/invalid message in error response from ElectrumX RPC %s", method));
|
||||
|
||||
String message = (String) messageObj;
|
||||
|
||||
// Some error 'messages' are actually wrapped upstream bitcoind errors:
|
||||
// "message": "daemon error: DaemonError({'code': -5, 'message': 'No such mempool or blockchain transaction. Use gettransaction for wallet transactions.'})"
|
||||
// We want to detect these and extract the upstream error code for caller's use
|
||||
Matcher messageMatcher = DAEMON_ERROR_REGEX.matcher(message);
|
||||
if (messageMatcher.find())
|
||||
try {
|
||||
int daemonErrorCode = Integer.parseInt(messageMatcher.group(1));
|
||||
throw new BitcoinException.NetworkException(daemonErrorCode, message);
|
||||
} catch (NumberFormatException e) {
|
||||
// We couldn't parse the error code integer? Fall-through to generic exception...
|
||||
}
|
||||
|
||||
throw new BitcoinException.NetworkException(message);
|
||||
}
|
||||
|
||||
return responseJson.get("result");
|
||||
}
|
||||
|
||||
|
31
src/main/java/org/qortal/crosschain/TransactionHash.java
Normal file
31
src/main/java/org/qortal/crosschain/TransactionHash.java
Normal file
@@ -0,0 +1,31 @@
|
||||
package org.qortal.crosschain;
|
||||
|
||||
import java.util.Comparator;
|
||||
|
||||
public class TransactionHash {
|
||||
|
||||
public static final Comparator<TransactionHash> CONFIRMED_FIRST = (a, b) -> Boolean.compare(a.height != 0, b.height != 0);
|
||||
|
||||
public final int height;
|
||||
public final String txHash;
|
||||
|
||||
public TransactionHash(int height, String txHash) {
|
||||
this.height = height;
|
||||
this.txHash = txHash;
|
||||
}
|
||||
|
||||
public int getHeight() {
|
||||
return this.height;
|
||||
}
|
||||
|
||||
public String getTxHash() {
|
||||
return this.txHash;
|
||||
}
|
||||
|
||||
public String toString() {
|
||||
return this.height == 0
|
||||
? String.format("txHash %s (unconfirmed)", this.txHash)
|
||||
: String.format("txHash %s (height %d)", this.txHash, this.height);
|
||||
}
|
||||
|
||||
}
|
16
src/main/java/org/qortal/crosschain/UnspentOutput.java
Normal file
16
src/main/java/org/qortal/crosschain/UnspentOutput.java
Normal file
@@ -0,0 +1,16 @@
|
||||
package org.qortal.crosschain;
|
||||
|
||||
/** Unspent output info as returned by ElectrumX network. */
|
||||
public class UnspentOutput {
|
||||
public final byte[] hash;
|
||||
public final int index;
|
||||
public final int height;
|
||||
public final long value;
|
||||
|
||||
public UnspentOutput(byte[] hash, int index, int height, long value) {
|
||||
this.hash = hash;
|
||||
this.index = index;
|
||||
this.height = height;
|
||||
this.value = value;
|
||||
}
|
||||
}
|
@@ -29,6 +29,10 @@ public class MemoryPoW {
|
||||
do {
|
||||
++nonce;
|
||||
|
||||
// If we've been interrupted, exit fast with invalid value
|
||||
if (Thread.currentThread().isInterrupted())
|
||||
return -1;
|
||||
|
||||
seed *= seedMultiplier; // per nonce
|
||||
|
||||
state[0] = longHash[0] ^ seed;
|
||||
|
@@ -0,0 +1,48 @@
|
||||
package org.qortal.data.account;
|
||||
|
||||
public class EligibleQoraHolderData {
|
||||
|
||||
// Properties
|
||||
|
||||
private String address;
|
||||
|
||||
private long qoraBalance;
|
||||
private long qortFromQoraBalance;
|
||||
|
||||
private Long finalQortFromQora;
|
||||
private Integer finalBlockHeight;
|
||||
|
||||
// Constructors
|
||||
|
||||
public EligibleQoraHolderData(String address, long qoraBalance, long qortFromQoraBalance, Long finalQortFromQora,
|
||||
Integer finalBlockHeight) {
|
||||
this.address = address;
|
||||
this.qoraBalance = qoraBalance;
|
||||
this.qortFromQoraBalance = qortFromQoraBalance;
|
||||
this.finalQortFromQora = finalQortFromQora;
|
||||
this.finalBlockHeight = finalBlockHeight;
|
||||
}
|
||||
|
||||
// Getters/Setters
|
||||
|
||||
public String getAddress() {
|
||||
return this.address;
|
||||
}
|
||||
|
||||
public long getQoraBalance() {
|
||||
return this.qoraBalance;
|
||||
}
|
||||
|
||||
public long getQortFromQoraBalance() {
|
||||
return this.qortFromQoraBalance;
|
||||
}
|
||||
|
||||
public Long getFinalQortFromQora() {
|
||||
return this.finalQortFromQora;
|
||||
}
|
||||
|
||||
public Integer getFinalBlockHeight() {
|
||||
return this.finalBlockHeight;
|
||||
}
|
||||
|
||||
}
|
@@ -5,7 +5,6 @@ public class ATStateData {
|
||||
// Properties
|
||||
private String ATAddress;
|
||||
private Integer height;
|
||||
private Long creation;
|
||||
private byte[] stateData;
|
||||
private byte[] stateHash;
|
||||
private Long fees;
|
||||
@@ -14,10 +13,9 @@ public class ATStateData {
|
||||
// Constructors
|
||||
|
||||
/** Create new ATStateData */
|
||||
public ATStateData(String ATAddress, Integer height, Long creation, byte[] stateData, byte[] stateHash, Long fees, boolean isInitial) {
|
||||
public ATStateData(String ATAddress, Integer height, 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;
|
||||
@@ -26,21 +24,21 @@ public class ATStateData {
|
||||
|
||||
/** For recreating per-block ATStateData from repository where not all info is needed */
|
||||
public ATStateData(String ATAddress, int height, byte[] stateHash, Long fees, boolean isInitial) {
|
||||
this(ATAddress, height, null, null, stateHash, fees, isInitial);
|
||||
this(ATAddress, height, 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 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);
|
||||
this(ATAddress, 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, 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);
|
||||
this(ATAddress, null, null, stateHash, fees, false);
|
||||
}
|
||||
|
||||
// Getters / setters
|
||||
@@ -58,10 +56,6 @@ public class ATStateData {
|
||||
this.height = height;
|
||||
}
|
||||
|
||||
public Long getCreation() {
|
||||
return this.creation;
|
||||
}
|
||||
|
||||
public byte[] getStateData() {
|
||||
return this.stateData;
|
||||
}
|
||||
|
@@ -79,6 +79,25 @@ public class BlockData implements Serializable {
|
||||
null, 0, null, null);
|
||||
}
|
||||
|
||||
public BlockData(BlockData other) {
|
||||
this.version = other.version;
|
||||
this.reference = other.reference;
|
||||
this.transactionCount = other.transactionCount;
|
||||
this.totalFees = other.totalFees;
|
||||
this.transactionsSignature = other.transactionsSignature;
|
||||
this.height = other.height;
|
||||
this.timestamp = other.timestamp;
|
||||
this.minterPublicKey = other.minterPublicKey;
|
||||
this.minterSignature = other.minterSignature;
|
||||
this.atCount = other.atCount;
|
||||
this.atFees = other.atFees;
|
||||
this.encodedOnlineAccounts = other.encodedOnlineAccounts;
|
||||
this.onlineAccountsCount = other.onlineAccountsCount;
|
||||
this.onlineAccountsTimestamp = other.onlineAccountsTimestamp;
|
||||
this.onlineAccountsSignatures = other.onlineAccountsSignatures;
|
||||
this.signature = other.signature;
|
||||
}
|
||||
|
||||
// Getters/setters
|
||||
|
||||
public byte[] getSignature() {
|
||||
|
@@ -190,4 +190,9 @@ public class TradeBotData {
|
||||
return this.receivingAccountInfo;
|
||||
}
|
||||
|
||||
// Mostly for debugging
|
||||
public String toString() {
|
||||
return String.format("%s: %s", this.atAddress, this.tradeState.name());
|
||||
}
|
||||
|
||||
}
|
||||
|
@@ -20,6 +20,21 @@ public enum EventBus {
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* <b>WARNING:</b> before calling this method,
|
||||
* make sure repository holds no locks, e.g. by calling
|
||||
* <tt>repository.discardChanges()</tt>.
|
||||
* <p>
|
||||
* This is because event listeners might open a new
|
||||
* repository session which will deadlock HSQLDB
|
||||
* if it tries to CHECKPOINT.
|
||||
* <p>
|
||||
* The HSQLDB deadlock occurs because the caller's
|
||||
* repository session blocks the CHECKPOINT until
|
||||
* their transaction is closed, yet event listeners
|
||||
* new sessions are blocked until CHECKPOINT is
|
||||
* completed, hence deadlock.
|
||||
*/
|
||||
public void notify(Event event) {
|
||||
List<Listener> clonedListeners;
|
||||
|
||||
|
@@ -10,12 +10,12 @@ import java.util.Set;
|
||||
|
||||
import org.apache.logging.log4j.LogManager;
|
||||
import org.apache.logging.log4j.Logger;
|
||||
import org.qortal.settings.Settings;
|
||||
|
||||
public enum Translator {
|
||||
INSTANCE;
|
||||
|
||||
private static final Logger LOGGER = LogManager.getLogger(Translator.class);
|
||||
private static final String DEFAULT_LANG = Locale.getDefault().getLanguage();
|
||||
|
||||
private static final Map<String, ResourceBundle> resourceBundles = new HashMap<>();
|
||||
|
||||
@@ -34,7 +34,7 @@ public enum Translator {
|
||||
}
|
||||
|
||||
public String translate(String className, String key) {
|
||||
return this.translate(className, DEFAULT_LANG, key);
|
||||
return this.translate(className, Settings.getInstance().getLocaleLang(), key);
|
||||
}
|
||||
|
||||
public Set<String> keySet(String className, String lang) {
|
||||
|
@@ -50,7 +50,6 @@ import org.qortal.repository.DataException;
|
||||
import org.qortal.repository.Repository;
|
||||
import org.qortal.repository.RepositoryManager;
|
||||
import org.qortal.settings.Settings;
|
||||
import org.qortal.transform.Transformer;
|
||||
import org.qortal.utils.ExecuteProduceConsume;
|
||||
// import org.qortal.utils.ExecutorDumper;
|
||||
import org.qortal.utils.ExecuteProduceConsume.StatsSnapshot;
|
||||
@@ -91,56 +90,41 @@ public class Network {
|
||||
public static final int MAX_SIGNATURES_PER_REPLY = 500;
|
||||
public static final int MAX_BLOCK_SUMMARIES_PER_REPLY = 500;
|
||||
|
||||
private final Ed25519PrivateKeyParameters edPrivateKeyParams;
|
||||
private final Ed25519PublicKeyParameters edPublicKeyParams;
|
||||
private final String ourNodeId;
|
||||
// Generate our node keys / ID
|
||||
private final Ed25519PrivateKeyParameters edPrivateKeyParams = new Ed25519PrivateKeyParameters(new SecureRandom());
|
||||
private final Ed25519PublicKeyParameters edPublicKeyParams = edPrivateKeyParams.generatePublicKey();
|
||||
private final String ourNodeId = Crypto.toNodeAddress(edPublicKeyParams.getEncoded());
|
||||
|
||||
private final int maxMessageSize;
|
||||
private final int minOutboundPeers;
|
||||
private final int maxPeers;
|
||||
|
||||
private List<PeerData> allKnownPeers;
|
||||
private List<Peer> connectedPeers;
|
||||
private List<PeerAddress> selfPeers;
|
||||
private final List<PeerData> allKnownPeers = new ArrayList<>();
|
||||
private final List<Peer> connectedPeers = new ArrayList<>();
|
||||
private final List<PeerAddress> selfPeers = new ArrayList<>();
|
||||
|
||||
private ExecuteProduceConsume networkEPC;
|
||||
private final ExecuteProduceConsume networkEPC;
|
||||
private Selector channelSelector;
|
||||
private ServerSocketChannel serverChannel;
|
||||
private Iterator<SelectionKey> channelIterator = null;
|
||||
|
||||
private int minOutboundPeers;
|
||||
private int maxPeers;
|
||||
private long nextConnectTaskTimestamp;
|
||||
// volatile because value is updated inside any one of the EPC threads
|
||||
private volatile long nextConnectTaskTimestamp = 0L; // ms - try first connect once NTP syncs
|
||||
|
||||
private ExecutorService broadcastExecutor;
|
||||
private long nextBroadcastTimestamp;
|
||||
private ExecutorService broadcastExecutor = Executors.newCachedThreadPool();
|
||||
// volatile because value is updated inside any one of the EPC threads
|
||||
private volatile long nextBroadcastTimestamp = 0L; // ms - try first broadcast once NTP syncs
|
||||
|
||||
private Lock mergePeersLock;
|
||||
private final Lock mergePeersLock = new ReentrantLock();
|
||||
|
||||
// Constructors
|
||||
|
||||
private Network() {
|
||||
connectedPeers = new ArrayList<>();
|
||||
selfPeers = new ArrayList<>();
|
||||
|
||||
// Generate our ID
|
||||
byte[] seed = new byte[Transformer.PRIVATE_KEY_LENGTH];
|
||||
new SecureRandom().nextBytes(seed);
|
||||
|
||||
edPrivateKeyParams = new Ed25519PrivateKeyParameters(seed, 0);
|
||||
edPublicKeyParams = edPrivateKeyParams.generatePublicKey();
|
||||
ourNodeId = Crypto.toNodeAddress(edPublicKeyParams.getEncoded());
|
||||
|
||||
maxMessageSize = 4 + 1 + 4 + BlockChain.getInstance().getMaxBlockSize();
|
||||
|
||||
minOutboundPeers = Settings.getInstance().getMinOutboundPeers();
|
||||
maxPeers = Settings.getInstance().getMaxPeers();
|
||||
|
||||
nextConnectTaskTimestamp = 0; // First connect once NTP syncs
|
||||
|
||||
broadcastExecutor = Executors.newCachedThreadPool();
|
||||
nextBroadcastTimestamp = 0; // First broadcast once NTP syncs
|
||||
|
||||
mergePeersLock = new ReentrantLock();
|
||||
|
||||
// We'll use a cached thread pool but with more aggressive timeout.
|
||||
ExecutorService networkExecutor = new ThreadPoolExecutor(1,
|
||||
Settings.getInstance().getMaxNetworkThreadPoolSize(),
|
||||
@@ -177,7 +161,9 @@ public class Network {
|
||||
|
||||
// Load all known peers from repository
|
||||
try (final Repository repository = RepositoryManager.getRepository()) {
|
||||
allKnownPeers = repository.getNetworkRepository().getAllPeers();
|
||||
synchronized (this.allKnownPeers) {
|
||||
this.allKnownPeers.addAll(repository.getNetworkRepository().getAllPeers());
|
||||
}
|
||||
}
|
||||
|
||||
// Start up first networking thread
|
||||
@@ -445,35 +431,38 @@ public class Network {
|
||||
private Task maybeProduceChannelTask(boolean canBlock) throws InterruptedException {
|
||||
final SelectionKey nextSelectionKey;
|
||||
|
||||
// anything to do?
|
||||
if (channelIterator == null) {
|
||||
try {
|
||||
if (canBlock)
|
||||
channelSelector.select(1000L);
|
||||
else
|
||||
channelSelector.selectNow();
|
||||
} catch (IOException e) {
|
||||
LOGGER.warn(String.format("Channel selection threw IOException: %s", e.getMessage()));
|
||||
return null;
|
||||
// Synchronization here to enforce thread-safety on channelIterator
|
||||
synchronized (channelSelector) {
|
||||
// anything to do?
|
||||
if (channelIterator == null) {
|
||||
try {
|
||||
if (canBlock)
|
||||
channelSelector.select(1000L);
|
||||
else
|
||||
channelSelector.selectNow();
|
||||
} catch (IOException e) {
|
||||
LOGGER.warn(String.format("Channel selection threw IOException: %s", e.getMessage()));
|
||||
return null;
|
||||
}
|
||||
|
||||
if (Thread.currentThread().isInterrupted())
|
||||
throw new InterruptedException();
|
||||
|
||||
channelIterator = channelSelector.selectedKeys().iterator();
|
||||
}
|
||||
|
||||
if (Thread.currentThread().isInterrupted())
|
||||
throw new InterruptedException();
|
||||
if (channelIterator.hasNext()) {
|
||||
nextSelectionKey = channelIterator.next();
|
||||
channelIterator.remove();
|
||||
} else {
|
||||
nextSelectionKey = null;
|
||||
channelIterator = null; // Nothing to do so reset iterator to cause new select
|
||||
}
|
||||
|
||||
channelIterator = channelSelector.selectedKeys().iterator();
|
||||
LOGGER.trace(() -> String.format("Thread %d, nextSelectionKey %s, channelIterator now %s",
|
||||
Thread.currentThread().getId(), nextSelectionKey, channelIterator));
|
||||
}
|
||||
|
||||
if (channelIterator.hasNext()) {
|
||||
nextSelectionKey = channelIterator.next();
|
||||
channelIterator.remove();
|
||||
} else {
|
||||
nextSelectionKey = null;
|
||||
channelIterator = null; // Nothing to do so reset iterator to cause new select
|
||||
}
|
||||
|
||||
LOGGER.trace(() -> String.format("Thread %d, nextSelectionKey %s, channelIterator now %s",
|
||||
Thread.currentThread().getId(), nextSelectionKey, channelIterator));
|
||||
|
||||
if (nextSelectionKey == null)
|
||||
return null;
|
||||
|
||||
|
@@ -87,6 +87,18 @@ public interface ATRepository {
|
||||
*/
|
||||
public List<ATStateData> getBlockATStatesAtHeight(int height) throws DataException;
|
||||
|
||||
/** Returns height of first trimmable AT state. */
|
||||
public int getAtTrimHeight() throws DataException;
|
||||
|
||||
/** Sets new base height for AT state trimming. */
|
||||
public void setAtTrimHeight(int trimHeight) throws DataException;
|
||||
|
||||
/** Hook to allow repository to prepare/cache info for AT state trimming. */
|
||||
public void prepareForAtStateTrimming() throws DataException;
|
||||
|
||||
/** Trims full AT state data between passed heights. Returns number of trimmed rows. */
|
||||
public int trimAtStates(int minHeight, int maxHeight, int limit) throws DataException;
|
||||
|
||||
/**
|
||||
* Save ATStateData into repository.
|
||||
* <p>
|
||||
|
@@ -4,6 +4,7 @@ import java.util.List;
|
||||
|
||||
import org.qortal.data.account.AccountBalanceData;
|
||||
import org.qortal.data.account.AccountData;
|
||||
import org.qortal.data.account.EligibleQoraHolderData;
|
||||
import org.qortal.data.account.MintingAccountData;
|
||||
import org.qortal.data.account.QortFromQoraData;
|
||||
import org.qortal.data.account.RewardShareData;
|
||||
@@ -89,6 +90,13 @@ public interface AccountRepository {
|
||||
*/
|
||||
public int modifyMintedBlockCount(String address, int delta) throws DataException;
|
||||
|
||||
/**
|
||||
* Modifies batch of accounts' minted block count only.
|
||||
* <p>
|
||||
* This is a one-shot, batch version of modifyMintedBlockCount(String, int) above.
|
||||
*/
|
||||
public void modifyMintedBlockCounts(List<String> addresses, int delta) throws DataException;
|
||||
|
||||
/** Delete account from repository. */
|
||||
public void delete(String address) throws DataException;
|
||||
|
||||
@@ -106,6 +114,9 @@ public interface AccountRepository {
|
||||
*/
|
||||
public AccountBalanceData getBalance(String address, long assetId) throws DataException;
|
||||
|
||||
/** Returns all account balances for given assetID, optionally excluding zero balances. */
|
||||
public List<AccountBalanceData> getAssetBalances(long assetId, Boolean excludeZero) throws DataException;
|
||||
|
||||
/** How to order results when fetching asset balances. */
|
||||
public enum BalanceOrdering {
|
||||
/** assetID first, then balance, then account address */
|
||||
@@ -116,15 +127,18 @@ public interface AccountRepository {
|
||||
ASSET_ACCOUNT
|
||||
}
|
||||
|
||||
/** Returns all account balances for given assetID, optionally excluding zero balances. */
|
||||
public List<AccountBalanceData> getAssetBalances(long assetId, Boolean excludeZero) throws DataException;
|
||||
|
||||
/** Returns account balances for matching addresses / assetIDs, optionally excluding zero balances, with pagination, used by API. */
|
||||
public List<AccountBalanceData> getAssetBalances(List<String> addresses, List<Long> assetIds, BalanceOrdering balanceOrdering, Boolean excludeZero, Integer limit, Integer offset, Boolean reverse) throws DataException;
|
||||
|
||||
/** Modifies account's asset balance by <tt>deltaBalance</tt>. */
|
||||
public void modifyAssetBalance(String address, long assetId, long deltaBalance) throws DataException;
|
||||
|
||||
/** Modifies a batch of account asset balances, treating AccountBalanceData.balance as <tt>deltaBalance</tt>. */
|
||||
public void modifyAssetBalances(List<AccountBalanceData> accountBalanceDeltas) throws DataException;
|
||||
|
||||
/** Batch update of account asset balances. */
|
||||
public void setAssetBalances(List<AccountBalanceData> accountBalances) throws DataException;
|
||||
|
||||
public void save(AccountBalanceData accountBalanceData) throws DataException;
|
||||
|
||||
public void delete(String address, long assetId) throws DataException;
|
||||
@@ -156,6 +170,16 @@ public interface AccountRepository {
|
||||
*/
|
||||
public RewardShareData getRewardShareByIndex(int index) throws DataException;
|
||||
|
||||
/**
|
||||
* Returns list of reward-share data using array of indexes into list of reward-shares (sorted by reward-share public key).
|
||||
* <p>
|
||||
* This is a one-shot, batch form of the above <tt>getRewardShareByIndex(int)</tt> call.
|
||||
*
|
||||
* @return list of reward-share data, or null if one (or more) index is invalid
|
||||
* @throws DataException
|
||||
*/
|
||||
public List<RewardShareData> getRewardSharesByIndexes(int[] indexes) throws DataException;
|
||||
|
||||
public boolean rewardShareExists(byte[] rewardSharePublicKey) throws DataException;
|
||||
|
||||
public void save(RewardShareData rewardShareData) throws DataException;
|
||||
@@ -175,7 +199,7 @@ public interface AccountRepository {
|
||||
// Managing QORT from legacy QORA
|
||||
|
||||
/**
|
||||
* Returns balance data for accounts with legacy QORA asset that are eligible
|
||||
* Returns full info for accounts with legacy QORA asset that are eligible
|
||||
* for more block reward (block processing) or for block reward removal (block orphaning).
|
||||
* <p>
|
||||
* For block processing, accounts that have already received their final QORT reward for owning
|
||||
@@ -187,7 +211,7 @@ public interface AccountRepository {
|
||||
* @param blockHeight QORT reward must have be present at this height (for orphaning only)
|
||||
* @throws DataException
|
||||
*/
|
||||
public List<AccountBalanceData> getEligibleLegacyQoraHolders(Integer blockHeight) throws DataException;
|
||||
public List<EligibleQoraHolderData> getEligibleLegacyQoraHolders(Integer blockHeight) throws DataException;
|
||||
|
||||
public QortFromQoraData getQortFromQoraInfo(String address) throws DataException;
|
||||
|
||||
|
@@ -143,13 +143,18 @@ public interface BlockRepository {
|
||||
*/
|
||||
public List<BlockInfo> getBlockInfos(Integer startHeight, Integer endHeight, Integer count) throws DataException;
|
||||
|
||||
/** Returns height of first trimmable online accounts signatures. */
|
||||
public int getOnlineAccountsSignaturesTrimHeight() throws DataException;
|
||||
|
||||
/** Sets new base height for trimming online accounts signatures. */
|
||||
public void setOnlineAccountsSignaturesTrimHeight(int trimHeight) throws DataException;
|
||||
|
||||
/**
|
||||
* Trim online accounts signatures from blocks older than passed timestamp.
|
||||
* Trim online accounts signatures from blocks between passed heights.
|
||||
*
|
||||
* @param timestamp
|
||||
* @return number of blocks trimmed
|
||||
*/
|
||||
public int trimOldOnlineAccountsSignatures(long timestamp) throws DataException;
|
||||
public int trimOldOnlineAccountsSignatures(int minHeight, int maxHeight) throws DataException;
|
||||
|
||||
/**
|
||||
* Returns first (lowest height) block that doesn't link back to specified block.
|
||||
|
31
src/main/java/org/qortal/repository/MessageRepository.java
Normal file
31
src/main/java/org/qortal/repository/MessageRepository.java
Normal file
@@ -0,0 +1,31 @@
|
||||
package org.qortal.repository;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
import org.qortal.data.transaction.MessageTransactionData;
|
||||
|
||||
public interface MessageRepository {
|
||||
|
||||
/**
|
||||
* Returns list of confirmed MESSAGE transaction data matching (some) participants.
|
||||
* <p>
|
||||
* At least one of <tt>senderPublicKey</tt> or <tt>recipient</tt> must be specified.
|
||||
* <p>
|
||||
* @throws DataException
|
||||
*/
|
||||
public List<MessageTransactionData> getMessagesByParticipants(byte[] senderPublicKey,
|
||||
String recipient, Integer limit, Integer offset, Boolean reverse) throws DataException;
|
||||
|
||||
/**
|
||||
* Does a MESSAGE exist with matching sender (pubkey), recipient and message payload?
|
||||
* <p>
|
||||
* Includes both confirmed and unconfirmed transactions!
|
||||
* <p>
|
||||
* @param senderPublicKey
|
||||
* @param recipient
|
||||
* @param messageData
|
||||
* @return true if a message exists, false otherwise
|
||||
*/
|
||||
public boolean exists(byte[] senderPublicKey, String recipient, byte[] messageData) throws DataException;
|
||||
|
||||
}
|
@@ -18,6 +18,8 @@ public interface Repository extends AutoCloseable {
|
||||
|
||||
public GroupRepository getGroupRepository();
|
||||
|
||||
public MessageRepository getMessageRepository();
|
||||
|
||||
public NameRepository getNameRepository();
|
||||
|
||||
public NetworkRepository getNetworkRepository();
|
||||
@@ -45,4 +47,6 @@ public interface Repository extends AutoCloseable {
|
||||
|
||||
public void backup(boolean quick) throws DataException;
|
||||
|
||||
public void performPeriodicMaintenance() throws DataException;
|
||||
|
||||
}
|
||||
|
@@ -6,7 +6,6 @@ import java.util.Map;
|
||||
import org.qortal.api.resource.TransactionsResource.ConfirmationStatus;
|
||||
import org.qortal.data.group.GroupApprovalData;
|
||||
import org.qortal.data.transaction.GroupApprovalTransactionData;
|
||||
import org.qortal.data.transaction.MessageTransactionData;
|
||||
import org.qortal.data.transaction.TransactionData;
|
||||
import org.qortal.data.transaction.TransferAssetTransactionData;
|
||||
import org.qortal.transaction.Transaction.TransactionType;
|
||||
@@ -91,6 +90,22 @@ public interface TransactionRepository {
|
||||
public List<byte[]> getSignaturesMatchingCriteria(TransactionType txType, byte[] publicKey,
|
||||
ConfirmationStatus confirmationStatus, Integer limit, Integer offset, Boolean reverse) throws DataException;
|
||||
|
||||
/**
|
||||
* Returns signatures for transactions that match search criteria.
|
||||
* <p>
|
||||
* Simpler version that only checks accepts one (optional) transaction type,
|
||||
* and one (optional) public key, within an block height range.
|
||||
*
|
||||
* @param txType
|
||||
* @param publicKey
|
||||
* @param minBlockHeight
|
||||
* @param maxBlockHeight
|
||||
* @return
|
||||
* @throws DataException
|
||||
*/
|
||||
public List<byte[]> getSignaturesMatchingCriteria(TransactionType txType, byte[] publicKey,
|
||||
Integer minBlockHeight, Integer maxBlockHeight) throws DataException;
|
||||
|
||||
/**
|
||||
* Returns signature for latest auto-update transaction.
|
||||
* <p>
|
||||
@@ -108,18 +123,6 @@ public interface TransactionRepository {
|
||||
*/
|
||||
public byte[] getLatestAutoUpdateTransaction(TransactionType txType, int txGroupId, Integer service) throws DataException;
|
||||
|
||||
/**
|
||||
* Returns list of MESSAGE transaction data matching recipient.
|
||||
* @param recipient
|
||||
* @param limit
|
||||
* @param offset
|
||||
* @param reverse
|
||||
* @return
|
||||
* @throws DataException
|
||||
*/
|
||||
public List<MessageTransactionData> getMessagesByRecipient(String recipient,
|
||||
Integer limit, Integer offset, Boolean reverse) throws DataException;
|
||||
|
||||
/**
|
||||
* Returns list of transactions relating to specific asset ID.
|
||||
*
|
||||
|
@@ -10,6 +10,8 @@ import org.qortal.data.at.ATStateData;
|
||||
import org.qortal.repository.ATRepository;
|
||||
import org.qortal.repository.DataException;
|
||||
|
||||
import com.google.common.primitives.Longs;
|
||||
|
||||
public class HSQLDBATRepository implements ATRepository {
|
||||
|
||||
protected HSQLDBRepository repository;
|
||||
@@ -135,16 +137,21 @@ public class HSQLDBATRepository implements ATRepository {
|
||||
@Override
|
||||
public List<ATData> getATsByFunctionality(byte[] codeHash, Boolean isExecutable, Integer limit, Integer offset, Boolean reverse) throws DataException {
|
||||
StringBuilder sql = new StringBuilder(512);
|
||||
List<Object> bindParams = new ArrayList<>();
|
||||
|
||||
sql.append("SELECT AT_address, creator, created_when, version, asset_id, code_bytes, ")
|
||||
.append("is_sleeping, sleep_until_height, is_finished, had_fatal_error, ")
|
||||
.append("is_frozen, frozen_balance ")
|
||||
.append("FROM ATs ")
|
||||
.append("WHERE code_hash = ? ");
|
||||
bindParams.add(codeHash);
|
||||
|
||||
if (isExecutable != null)
|
||||
sql.append("AND is_finished = ").append(isExecutable ? "false" : "true");
|
||||
if (isExecutable != null) {
|
||||
sql.append("AND is_finished != ? ");
|
||||
bindParams.add(isExecutable);
|
||||
}
|
||||
|
||||
sql.append(" ORDER BY created_when ");
|
||||
sql.append("ORDER BY created_when ");
|
||||
if (reverse != null && reverse)
|
||||
sql.append("DESC");
|
||||
|
||||
@@ -152,7 +159,7 @@ public class HSQLDBATRepository implements ATRepository {
|
||||
|
||||
List<ATData> matchingATs = new ArrayList<>();
|
||||
|
||||
try (ResultSet resultSet = this.repository.checkedExecute(sql.toString(), codeHash)) {
|
||||
try (ResultSet resultSet = this.repository.checkedExecute(sql.toString(), bindParams.toArray())) {
|
||||
if (resultSet == null)
|
||||
return matchingATs;
|
||||
|
||||
@@ -241,7 +248,7 @@ public class HSQLDBATRepository implements ATRepository {
|
||||
|
||||
@Override
|
||||
public ATStateData getATStateAtHeight(String atAddress, int height) throws DataException {
|
||||
String sql = "SELECT created_when, state_data, state_hash, fees, is_initial "
|
||||
String sql = "SELECT state_data, state_hash, fees, is_initial "
|
||||
+ "FROM ATStates "
|
||||
+ "WHERE AT_address = ? AND height = ? "
|
||||
+ "LIMIT 1";
|
||||
@@ -250,13 +257,12 @@ public class HSQLDBATRepository implements ATRepository {
|
||||
if (resultSet == null)
|
||||
return null;
|
||||
|
||||
long created = resultSet.getLong(1);
|
||||
byte[] stateData = resultSet.getBytes(2); // Actually BLOB
|
||||
byte[] stateHash = resultSet.getBytes(3);
|
||||
long fees = resultSet.getLong(4);
|
||||
boolean isInitial = resultSet.getBoolean(5);
|
||||
byte[] stateData = resultSet.getBytes(1); // Actually BLOB
|
||||
byte[] stateHash = resultSet.getBytes(2);
|
||||
long fees = resultSet.getLong(3);
|
||||
boolean isInitial = resultSet.getBoolean(4);
|
||||
|
||||
return new ATStateData(atAddress, height, created, stateData, stateHash, fees, isInitial);
|
||||
return new ATStateData(atAddress, height, stateData, stateHash, fees, isInitial);
|
||||
} catch (SQLException e) {
|
||||
throw new DataException("Unable to fetch AT state from repository", e);
|
||||
}
|
||||
@@ -264,25 +270,25 @@ public class HSQLDBATRepository implements ATRepository {
|
||||
|
||||
@Override
|
||||
public ATStateData getLatestATState(String atAddress) throws DataException {
|
||||
String sql = "SELECT height, created_when, state_data, state_hash, fees, is_initial "
|
||||
String sql = "SELECT height, state_data, state_hash, fees, is_initial "
|
||||
+ "FROM ATStates "
|
||||
+ "WHERE AT_address = ? "
|
||||
+ "ORDER BY height DESC "
|
||||
+ "LIMIT 1 "
|
||||
+ "USING INDEX";
|
||||
// AT_address then height so the compound primary key is used as an index
|
||||
// Both must be the same direction also
|
||||
+ "ORDER BY AT_address DESC, height DESC "
|
||||
+ "LIMIT 1 ";
|
||||
|
||||
try (ResultSet resultSet = this.repository.checkedExecute(sql, atAddress)) {
|
||||
if (resultSet == null)
|
||||
return null;
|
||||
|
||||
int height = resultSet.getInt(1);
|
||||
long created = resultSet.getLong(2);
|
||||
byte[] stateData = resultSet.getBytes(3); // Actually BLOB
|
||||
byte[] stateHash = resultSet.getBytes(4);
|
||||
long fees = resultSet.getLong(5);
|
||||
boolean isInitial = resultSet.getBoolean(6);
|
||||
byte[] stateData = resultSet.getBytes(2); // Actually BLOB
|
||||
byte[] stateHash = resultSet.getBytes(3);
|
||||
long fees = resultSet.getLong(4);
|
||||
boolean isInitial = resultSet.getBoolean(5);
|
||||
|
||||
return new ATStateData(atAddress, height, created, stateData, stateHash, fees, isInitial);
|
||||
return new ATStateData(atAddress, height, stateData, stateHash, fees, isInitial);
|
||||
} catch (SQLException e) {
|
||||
throw new DataException("Unable to fetch latest AT state from repository", e);
|
||||
}
|
||||
@@ -293,40 +299,42 @@ public class HSQLDBATRepository implements ATRepository {
|
||||
Integer dataByteOffset, Long expectedValue, Integer minimumFinalHeight,
|
||||
Integer limit, Integer offset, Boolean reverse) throws DataException {
|
||||
StringBuilder sql = new StringBuilder(1024);
|
||||
sql.append("SELECT AT_address, height, created_when, state_data, state_hash, fees, is_initial "
|
||||
List<Object> bindParams = new ArrayList<>();
|
||||
|
||||
sql.append("SELECT AT_address, height, state_data, state_hash, fees, is_initial "
|
||||
+ "FROM ATs "
|
||||
+ "CROSS JOIN LATERAL("
|
||||
+ "SELECT height, created_when, state_data, state_hash, fees, is_initial "
|
||||
+ "SELECT height, state_data, state_hash, fees, is_initial "
|
||||
+ "FROM ATStates "
|
||||
+ "WHERE ATStates.AT_address = ATs.AT_address "
|
||||
+ "ORDER BY height DESC "
|
||||
+ "WHERE ATStates.AT_address = ATs.AT_address ");
|
||||
|
||||
if (minimumFinalHeight != null) {
|
||||
sql.append("AND height >= ? ");
|
||||
bindParams.add(minimumFinalHeight);
|
||||
}
|
||||
|
||||
// AT_address then height so the compound primary key is used as an index
|
||||
// Both must be the same direction also
|
||||
sql.append("ORDER BY AT_address DESC, height DESC "
|
||||
+ "LIMIT 1 "
|
||||
+ "USING INDEX"
|
||||
+ ") AS FinalATStates "
|
||||
+ "WHERE code_hash = ? ");
|
||||
|
||||
List<Object> bindParams = new ArrayList<>();
|
||||
bindParams.add(codeHash);
|
||||
|
||||
if (isFinished != null) {
|
||||
sql.append("AND is_finished = ?");
|
||||
sql.append("AND is_finished = ? ");
|
||||
bindParams.add(isFinished);
|
||||
}
|
||||
|
||||
if (dataByteOffset != null && expectedValue != null) {
|
||||
sql.append("AND RAWTOHEX(SUBSTRING(state_data FROM ? FOR 8)) = ? ");
|
||||
sql.append("AND SUBSTRING(state_data FROM ? FOR 8) = ? ");
|
||||
|
||||
// We convert our long to hex Java-side to control endian
|
||||
String expectedHexValue = String.format("%016x", expectedValue); // left-zero-padding and conversion
|
||||
// We convert our long on Java-side to control endian
|
||||
byte[] rawExpectedValue = Longs.toByteArray(expectedValue);
|
||||
|
||||
// SQL binary data offsets start at 1
|
||||
bindParams.add(dataByteOffset + 1);
|
||||
bindParams.add(expectedHexValue);
|
||||
}
|
||||
|
||||
if (minimumFinalHeight != null) {
|
||||
sql.append("AND height >= ");
|
||||
sql.append(minimumFinalHeight);
|
||||
bindParams.add(rawExpectedValue);
|
||||
}
|
||||
|
||||
sql.append(" ORDER BY height ");
|
||||
@@ -344,13 +352,12 @@ public class HSQLDBATRepository implements ATRepository {
|
||||
do {
|
||||
String atAddress = resultSet.getString(1);
|
||||
int height = resultSet.getInt(2);
|
||||
long created = resultSet.getLong(3);
|
||||
byte[] stateData = resultSet.getBytes(4); // Actually BLOB
|
||||
byte[] stateHash = resultSet.getBytes(5);
|
||||
long fees = resultSet.getLong(6);
|
||||
boolean isInitial = resultSet.getBoolean(7);
|
||||
byte[] stateData = resultSet.getBytes(3); // Actually BLOB
|
||||
byte[] stateHash = resultSet.getBytes(4);
|
||||
long fees = resultSet.getLong(5);
|
||||
boolean isInitial = resultSet.getBoolean(6);
|
||||
|
||||
ATStateData atStateData = new ATStateData(atAddress, height, created, stateData, stateHash, fees, isInitial);
|
||||
ATStateData atStateData = new ATStateData(atAddress, height, stateData, stateHash, fees, isInitial);
|
||||
|
||||
atStates.add(atStateData);
|
||||
} while (resultSet.next());
|
||||
@@ -365,6 +372,7 @@ public class HSQLDBATRepository implements ATRepository {
|
||||
public List<ATStateData> getBlockATStatesAtHeight(int height) throws DataException {
|
||||
String sql = "SELECT AT_address, state_hash, fees, is_initial "
|
||||
+ "FROM ATStates "
|
||||
+ "LEFT OUTER JOIN ATs USING (AT_address) "
|
||||
+ "WHERE height = ? "
|
||||
+ "ORDER BY created_when ASC";
|
||||
|
||||
@@ -391,18 +399,100 @@ public class HSQLDBATRepository implements ATRepository {
|
||||
return atStates;
|
||||
}
|
||||
|
||||
@Override
|
||||
public int getAtTrimHeight() throws DataException {
|
||||
String sql = "SELECT AT_trim_height FROM DatabaseInfo";
|
||||
|
||||
try (ResultSet resultSet = this.repository.checkedExecute(sql)) {
|
||||
if (resultSet == null)
|
||||
return 0;
|
||||
|
||||
return resultSet.getInt(1);
|
||||
} catch (SQLException e) {
|
||||
throw new DataException("Unable to fetch AT state trim height from repository", e);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void setAtTrimHeight(int trimHeight) throws DataException {
|
||||
String updateSql = "UPDATE DatabaseInfo SET AT_trim_height = ?";
|
||||
|
||||
try {
|
||||
this.repository.executeCheckedUpdate(updateSql, trimHeight);
|
||||
} catch (SQLException e) {
|
||||
repository.examineException(e);
|
||||
throw new DataException("Unable to set AT state trim height in repository", e);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void prepareForAtStateTrimming() throws DataException {
|
||||
// Rebuild cache of latest, non-finished AT states that we can't trim
|
||||
String dropSql = "DROP TABLE IF EXISTS LatestATStates";
|
||||
|
||||
try {
|
||||
this.repository.executeCheckedUpdate(dropSql);
|
||||
} catch (SQLException e) {
|
||||
repository.examineException(e);
|
||||
throw new DataException("Unable to drop temporary latest AT states cache from repository", e);
|
||||
}
|
||||
|
||||
String createSql = "CREATE TEMPORARY TABLE LatestATStates "
|
||||
+ "AS ("
|
||||
+ "SELECT AT_address, height FROM ATs "
|
||||
+ "CROSS JOIN LATERAL("
|
||||
+ "SELECT height FROM ATStates "
|
||||
+ "WHERE ATStates.AT_address = ATs.AT_address "
|
||||
+ "ORDER BY AT_address DESC, height DESC LIMIT 1"
|
||||
+ ") "
|
||||
+ ") "
|
||||
+ "WITH DATA "
|
||||
+ "ON COMMIT PRESERVE ROWS";
|
||||
|
||||
try {
|
||||
this.repository.executeCheckedUpdate(createSql);
|
||||
} catch (SQLException e) {
|
||||
repository.examineException(e);
|
||||
throw new DataException("Unable to recreate temporary latest AT states cache in repository", e);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public int trimAtStates(int minHeight, int maxHeight, int limit) throws DataException {
|
||||
if (minHeight >= maxHeight)
|
||||
return 0;
|
||||
|
||||
// We're often called so no need to trim all states in one go.
|
||||
// Limit updates to reduce CPU and memory load.
|
||||
String sql = "UPDATE ATStates SET state_data = NULL "
|
||||
+ "WHERE state_data IS NOT NULL "
|
||||
+ "AND height BETWEEN ? AND ? "
|
||||
+ "AND NOT EXISTS("
|
||||
+ "SELECT TRUE FROM LatestATStates "
|
||||
+ "WHERE LatestATStates.AT_address = ATStates.AT_address "
|
||||
+ "AND LatestATStates.height = ATStates.height"
|
||||
+ ") "
|
||||
+ "LIMIT ?";
|
||||
|
||||
try {
|
||||
return this.repository.executeCheckedUpdate(sql, minHeight, maxHeight, limit);
|
||||
} catch (SQLException e) {
|
||||
repository.examineException(e);
|
||||
throw new DataException("Unable to trim AT states in repository", e);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void save(ATStateData atStateData) throws DataException {
|
||||
// We shouldn't ever save partial ATStateData
|
||||
if (atStateData.getCreation() == null || atStateData.getStateHash() == null || atStateData.getHeight() == null)
|
||||
if (atStateData.getStateHash() == null || atStateData.getHeight() == null)
|
||||
throw new IllegalArgumentException("Refusing to save partial AT state into repository!");
|
||||
|
||||
HSQLDBSaver saveHelper = new HSQLDBSaver("ATStates");
|
||||
|
||||
saveHelper.bind("AT_address", atStateData.getATAddress()).bind("height", atStateData.getHeight())
|
||||
.bind("created_when", atStateData.getCreation()).bind("state_data", atStateData.getStateData())
|
||||
.bind("state_hash", atStateData.getStateHash()).bind("fees", atStateData.getFees())
|
||||
.bind("is_initial", atStateData.isInitial());
|
||||
.bind("state_data", atStateData.getStateData()).bind("state_hash", atStateData.getStateHash())
|
||||
.bind("fees", atStateData.getFees()).bind("is_initial", atStateData.isInitial());
|
||||
|
||||
try {
|
||||
saveHelper.execute(this.repository);
|
||||
|
@@ -1,13 +1,17 @@
|
||||
package org.qortal.repository.hsqldb;
|
||||
|
||||
import static org.qortal.utils.Amounts.prettyAmount;
|
||||
|
||||
import java.sql.ResultSet;
|
||||
import java.sql.SQLException;
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
import java.util.stream.Collectors;
|
||||
|
||||
import org.qortal.asset.Asset;
|
||||
import org.qortal.data.account.AccountBalanceData;
|
||||
import org.qortal.data.account.AccountData;
|
||||
import org.qortal.data.account.EligibleQoraHolderData;
|
||||
import org.qortal.data.account.MintingAccountData;
|
||||
import org.qortal.data.account.QortFromQoraData;
|
||||
import org.qortal.data.account.RewardShareData;
|
||||
@@ -145,7 +149,7 @@ public class HSQLDBAccountRepository implements AccountRepository {
|
||||
public void ensureAccount(AccountData accountData) throws DataException {
|
||||
String sql = "INSERT IGNORE INTO Accounts (account, public_key) VALUES (?, ?)"; // MySQL syntax
|
||||
try {
|
||||
this.repository.checkedExecuteUpdateCount(sql, accountData.getAddress(), accountData.getPublicKey());
|
||||
this.repository.executeCheckedUpdate(sql, accountData.getAddress(), accountData.getPublicKey());
|
||||
} catch (SQLException e) {
|
||||
throw new DataException("Unable to ensure minimal account in repository", e);
|
||||
}
|
||||
@@ -260,12 +264,26 @@ public class HSQLDBAccountRepository implements AccountRepository {
|
||||
"ON DUPLICATE KEY UPDATE blocks_minted = blocks_minted + ?";
|
||||
|
||||
try {
|
||||
return this.repository.checkedExecuteUpdateCount(sql, address, delta, delta);
|
||||
return this.repository.executeCheckedUpdate(sql, address, delta, delta);
|
||||
} catch (SQLException e) {
|
||||
throw new DataException("Unable to modify account's minted block count in repository", e);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void modifyMintedBlockCounts(List<String> addresses, int delta) throws DataException {
|
||||
String sql = "INSERT INTO Accounts (account, blocks_minted) VALUES (?, ?) " +
|
||||
"ON DUPLICATE KEY UPDATE blocks_minted = blocks_minted + ?";
|
||||
|
||||
List<Object[]> bindParamRows = addresses.stream().map(address -> new Object[] { address, delta, delta }).collect(Collectors.toList());
|
||||
|
||||
try {
|
||||
this.repository.executeCheckedBatchUpdate(sql, bindParamRows);
|
||||
} catch (SQLException e) {
|
||||
throw new DataException("Unable to modify many account minted block counts in repository", e);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void delete(String address) throws DataException {
|
||||
// NOTE: Account balances are deleted automatically by the database thanks to "ON DELETE CASCADE" in AccountBalances' FOREIGN KEY
|
||||
@@ -447,7 +465,7 @@ public class HSQLDBAccountRepository implements AccountRepository {
|
||||
// Perform actual balance change
|
||||
String sql = "UPDATE AccountBalances set balance = balance + ? WHERE account = ? AND asset_id = ?";
|
||||
try {
|
||||
this.repository.checkedExecuteUpdateCount(sql, deltaBalance, address, assetId);
|
||||
this.repository.executeCheckedUpdate(sql, deltaBalance, address, assetId);
|
||||
} catch (SQLException e) {
|
||||
throw new DataException("Unable to reduce account balance in repository", e);
|
||||
}
|
||||
@@ -455,7 +473,7 @@ public class HSQLDBAccountRepository implements AccountRepository {
|
||||
// We have to ensure parent row exists to satisfy foreign key constraint
|
||||
try {
|
||||
String sql = "INSERT IGNORE INTO Accounts (account) VALUES (?)"; // MySQL syntax
|
||||
this.repository.checkedExecuteUpdateCount(sql, address);
|
||||
this.repository.executeCheckedUpdate(sql, address);
|
||||
} catch (SQLException e) {
|
||||
throw new DataException("Unable to ensure minimal account in repository", e);
|
||||
}
|
||||
@@ -464,13 +482,95 @@ public class HSQLDBAccountRepository implements AccountRepository {
|
||||
String sql = "INSERT INTO AccountBalances (account, asset_id, balance) VALUES (?, ?, ?) " +
|
||||
"ON DUPLICATE KEY UPDATE balance = balance + ?";
|
||||
try {
|
||||
this.repository.checkedExecuteUpdateCount(sql, address, assetId, deltaBalance, deltaBalance);
|
||||
this.repository.executeCheckedUpdate(sql, address, assetId, deltaBalance, deltaBalance);
|
||||
} catch (SQLException e) {
|
||||
throw new DataException("Unable to increase account balance in repository", e);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public void modifyAssetBalances(List<AccountBalanceData> accountBalanceDeltas) throws DataException {
|
||||
// Nothing to do?
|
||||
if (accountBalanceDeltas == null || accountBalanceDeltas.isEmpty())
|
||||
return;
|
||||
|
||||
// Map balance changes into SQL bind params, filtering out no-op changes
|
||||
List<Object[]> modifyBalanceParams = accountBalanceDeltas.stream()
|
||||
.filter(accountBalance -> accountBalance.getBalance() != 0L)
|
||||
.map(accountBalance -> new Object[] { accountBalance.getAddress(), accountBalance.getAssetId(), accountBalance.getBalance(), accountBalance.getBalance() })
|
||||
.collect(Collectors.toList());
|
||||
|
||||
// Before we modify balances, ensure parent accounts exist
|
||||
String ensureSql = "INSERT IGNORE INTO Accounts (account) VALUES (?)"; // MySQL syntax
|
||||
try {
|
||||
this.repository.executeCheckedBatchUpdate(ensureSql, modifyBalanceParams.stream().map(objects -> new Object[] { objects[0] }).collect(Collectors.toList()));
|
||||
} catch (SQLException e) {
|
||||
throw new DataException("Unable to ensure minimal accounts in repository", e);
|
||||
}
|
||||
|
||||
// Perform actual balance changes
|
||||
String sql = "INSERT INTO AccountBalances (account, asset_id, balance) VALUES (?, ?, ?) " +
|
||||
"ON DUPLICATE KEY UPDATE balance = balance + ?";
|
||||
try {
|
||||
this.repository.executeCheckedBatchUpdate(sql, modifyBalanceParams);
|
||||
} catch (SQLException e) {
|
||||
throw new DataException("Unable to modify account balances in repository", e);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@Override
|
||||
public void setAssetBalances(List<AccountBalanceData> accountBalances) throws DataException {
|
||||
// Nothing to do?
|
||||
if (accountBalances == null || accountBalances.isEmpty())
|
||||
return;
|
||||
|
||||
/*
|
||||
* Split workload into zero and non-zero balances,
|
||||
* checking for negative balances as we progress.
|
||||
*/
|
||||
|
||||
List<Object[]> zeroAccountBalanceParams = new ArrayList<>();
|
||||
List<Object[]> nonZeroAccountBalanceParams = new ArrayList<>();
|
||||
|
||||
for (AccountBalanceData accountBalanceData : accountBalances) {
|
||||
final long balance = accountBalanceData.getBalance();
|
||||
|
||||
if (balance < 0)
|
||||
throw new DataException(String.format("Refusing to set negative balance %s [assetId %d] for %s",
|
||||
prettyAmount(balance), accountBalanceData.getAssetId(), accountBalanceData.getAddress()));
|
||||
|
||||
if (balance == 0)
|
||||
zeroAccountBalanceParams.add(new Object[] { accountBalanceData.getAddress(), accountBalanceData.getAssetId() });
|
||||
else
|
||||
nonZeroAccountBalanceParams.add(new Object[] { accountBalanceData.getAddress(), accountBalanceData.getAssetId(), balance, balance });
|
||||
}
|
||||
|
||||
// Batch update (actually delete) of zero balances
|
||||
try {
|
||||
this.repository.deleteBatch("AccountBalances", "account = ? AND asset_id = ?", zeroAccountBalanceParams);
|
||||
} catch (SQLException e) {
|
||||
throw new DataException("Unable to delete account balances from repository", e);
|
||||
}
|
||||
|
||||
// Before we set new balances, ensure parent accounts exist
|
||||
String ensureSql = "INSERT IGNORE INTO Accounts (account) VALUES (?)"; // MySQL syntax
|
||||
try {
|
||||
this.repository.executeCheckedBatchUpdate(ensureSql, nonZeroAccountBalanceParams.stream().map(objects -> new Object[] { objects[0] }).collect(Collectors.toList()));
|
||||
} catch (SQLException e) {
|
||||
throw new DataException("Unable to ensure minimal accounts in repository", e);
|
||||
}
|
||||
|
||||
// Now set all balances in one go
|
||||
String setSql = "INSERT INTO AccountBalances (account, asset_id, balance) VALUES (?, ?, ?) " +
|
||||
"ON DUPLICATE KEY UPDATE balance = ?";
|
||||
try {
|
||||
this.repository.executeCheckedBatchUpdate(setSql, nonZeroAccountBalanceParams);
|
||||
} catch (SQLException e) {
|
||||
throw new DataException("Unable to set account balances in repository", e);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void save(AccountBalanceData accountBalanceData) throws DataException {
|
||||
HSQLDBSaver saveHelper = new HSQLDBSaver("AccountBalances");
|
||||
@@ -699,7 +799,52 @@ public class HSQLDBAccountRepository implements AccountRepository {
|
||||
|
||||
return new RewardShareData(minterPublicKey, minter, recipient, rewardSharePublicKey, sharePercent);
|
||||
} catch (SQLException e) {
|
||||
throw new DataException("Unable to fetch reward-share info from repository", e);
|
||||
throw new DataException("Unable to fetch indexed reward-share from repository", e);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public List<RewardShareData> getRewardSharesByIndexes(int[] indexes) throws DataException {
|
||||
String sql = "SELECT minter_public_key, minter, recipient, share_percent, reward_share_public_key FROM RewardShares "
|
||||
+ "ORDER BY reward_share_public_key ASC";
|
||||
|
||||
if (indexes == null)
|
||||
return null;
|
||||
|
||||
List<RewardShareData> rewardShares = new ArrayList<>();
|
||||
if (indexes.length == 0)
|
||||
return rewardShares;
|
||||
|
||||
try (ResultSet resultSet = this.repository.checkedExecute(sql)) {
|
||||
if (resultSet == null)
|
||||
return null;
|
||||
|
||||
int rowNum = 1;
|
||||
for (int i = 0; i < indexes.length; ++i) {
|
||||
final int index = indexes[i];
|
||||
|
||||
while (rowNum < index + 1) { // +1 because in JDBC, first row is row 1
|
||||
if (!resultSet.next())
|
||||
// Index is out of bounds
|
||||
return null;
|
||||
|
||||
++rowNum;
|
||||
}
|
||||
|
||||
byte[] minterPublicKey = resultSet.getBytes(1);
|
||||
String minter = resultSet.getString(2);
|
||||
String recipient = resultSet.getString(3);
|
||||
int sharePercent = resultSet.getInt(4);
|
||||
byte[] rewardSharePublicKey = resultSet.getBytes(5);
|
||||
|
||||
RewardShareData rewardShareData = new RewardShareData(minterPublicKey, minter, recipient, rewardSharePublicKey, sharePercent);
|
||||
|
||||
rewardShares.add(rewardShareData);
|
||||
}
|
||||
|
||||
return rewardShares;
|
||||
} catch (SQLException e) {
|
||||
throw new DataException("Unable to fetch indexed reward-shares from repository", e);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -785,35 +930,49 @@ public class HSQLDBAccountRepository implements AccountRepository {
|
||||
// Managing QORT from legacy QORA
|
||||
|
||||
@Override
|
||||
public List<AccountBalanceData> getEligibleLegacyQoraHolders(Integer blockHeight) throws DataException {
|
||||
public List<EligibleQoraHolderData> getEligibleLegacyQoraHolders(Integer blockHeight) throws DataException {
|
||||
StringBuilder sql = new StringBuilder(1024);
|
||||
sql.append("SELECT account, balance from AccountBalances ");
|
||||
List<Object> bindParams = new ArrayList<>();
|
||||
|
||||
sql.append("SELECT account, Qora.balance, QortFromQora.balance, final_qort_from_qora, final_block_height ");
|
||||
sql.append("FROM AccountBalances AS Qora ");
|
||||
sql.append("LEFT OUTER JOIN AccountQortFromQoraInfo USING (account) ");
|
||||
sql.append("WHERE asset_id = ");
|
||||
sql.append("LEFT OUTER JOIN AccountBalances AS QortFromQora ON QortFromQora.account = Qora.account AND QortFromQora.asset_id = ");
|
||||
sql.append(Asset.QORT_FROM_QORA); // int is safe to use literally
|
||||
sql.append(" WHERE Qora.asset_id = ");
|
||||
sql.append(Asset.LEGACY_QORA); // int is safe to use literally
|
||||
sql.append(" AND (final_block_height IS NULL");
|
||||
|
||||
if (blockHeight != null) {
|
||||
sql.append(" OR final_block_height >= ");
|
||||
sql.append(blockHeight);
|
||||
sql.append(" OR final_block_height >= ?");
|
||||
bindParams.add(blockHeight);
|
||||
}
|
||||
|
||||
sql.append(")");
|
||||
|
||||
List<AccountBalanceData> accountBalances = new ArrayList<>();
|
||||
List<EligibleQoraHolderData> eligibleLegacyQoraHolders = new ArrayList<>();
|
||||
|
||||
try (ResultSet resultSet = this.repository.checkedExecute(sql.toString())) {
|
||||
try (ResultSet resultSet = this.repository.checkedExecute(sql.toString(), bindParams.toArray())) {
|
||||
if (resultSet == null)
|
||||
return accountBalances;
|
||||
return eligibleLegacyQoraHolders;
|
||||
|
||||
do {
|
||||
String address = resultSet.getString(1);
|
||||
long balance = resultSet.getLong(2);
|
||||
long qoraBalance = resultSet.getLong(2);
|
||||
long qortFromQoraBalance = resultSet.getLong(3);
|
||||
|
||||
accountBalances.add(new AccountBalanceData(address, Asset.LEGACY_QORA, balance));
|
||||
Long finalQortFromQora = resultSet.getLong(4);
|
||||
if (finalQortFromQora == 0 && resultSet.wasNull())
|
||||
finalQortFromQora = null;
|
||||
|
||||
Integer finalBlockHeight = resultSet.getInt(5);
|
||||
if (finalBlockHeight == 0 && resultSet.wasNull())
|
||||
finalBlockHeight = null;
|
||||
|
||||
eligibleLegacyQoraHolders.add(new EligibleQoraHolderData(address, qoraBalance, qortFromQoraBalance, finalQortFromQora, finalBlockHeight));
|
||||
} while (resultSet.next());
|
||||
|
||||
return accountBalances;
|
||||
return eligibleLegacyQoraHolders;
|
||||
} catch (SQLException e) {
|
||||
throw new DataException("Unable to fetch eligible legacy QORA holders from repository", e);
|
||||
}
|
||||
|
@@ -120,7 +120,7 @@ public class HSQLDBBlockRepository implements BlockRepository {
|
||||
@Override
|
||||
public int getHeightFromTimestamp(long timestamp) throws DataException {
|
||||
// Uses (minted_when, height) index
|
||||
String sql = "SELECT height FROM Blocks WHERE minted_when <= ? ORDER BY minted_when DESC LIMIT 1";
|
||||
String sql = "SELECT height FROM Blocks WHERE minted_when <= ? ORDER BY minted_when DESC, height DESC LIMIT 1";
|
||||
|
||||
try (ResultSet resultSet = this.repository.checkedExecute(sql, timestamp)) {
|
||||
if (resultSet == null)
|
||||
@@ -175,7 +175,11 @@ public class HSQLDBBlockRepository implements BlockRepository {
|
||||
public List<TransactionData> getTransactionsFromSignature(byte[] signature, Integer limit, Integer offset, Boolean reverse) throws DataException {
|
||||
StringBuilder sql = new StringBuilder(256);
|
||||
|
||||
sql.append("SELECT transaction_signature FROM BlockTransactions WHERE block_signature = ? ORDER BY sequence");
|
||||
sql.append("SELECT transaction_signature FROM BlockTransactions WHERE block_signature = ? ORDER BY block_signature");
|
||||
if (reverse != null && reverse)
|
||||
sql.append(" DESC");
|
||||
|
||||
sql.append(", sequence");
|
||||
if (reverse != null && reverse)
|
||||
sql.append(" DESC");
|
||||
|
||||
@@ -378,6 +382,8 @@ public class HSQLDBBlockRepository implements BlockRepository {
|
||||
@Override
|
||||
public List<BlockInfo> getBlockInfos(Integer startHeight, Integer endHeight, Integer count) throws DataException {
|
||||
StringBuilder sql = new StringBuilder(512);
|
||||
List<Object> bindParams = new ArrayList<>();
|
||||
|
||||
sql.append("SELECT signature, height, minted_when, transaction_count, RewardShares.minter ");
|
||||
|
||||
/*
|
||||
@@ -396,10 +402,9 @@ public class HSQLDBBlockRepository implements BlockRepository {
|
||||
if (startHeight != null && endHeight != null) {
|
||||
sql.append("FROM Blocks ");
|
||||
sql.append("JOIN RewardShares ON RewardShares.reward_share_public_key = Blocks.minter ");
|
||||
sql.append("WHERE height BETWEEN ");
|
||||
sql.append(startHeight);
|
||||
sql.append(" AND ");
|
||||
sql.append(endHeight - 1);
|
||||
sql.append("WHERE height BETWEEN ? AND ?");
|
||||
bindParams.add(startHeight);
|
||||
bindParams.add(Integer.valueOf(endHeight - 1));
|
||||
} else if (endHeight != null || (startHeight == null && count != null)) {
|
||||
// we are going to return blocks from the end of the chain
|
||||
if (count == null)
|
||||
@@ -407,17 +412,15 @@ public class HSQLDBBlockRepository implements BlockRepository {
|
||||
|
||||
if (endHeight == null) {
|
||||
sql.append("FROM (SELECT height FROM Blocks ORDER BY height DESC LIMIT 1) AS MaxHeights (max_height) ");
|
||||
sql.append("JOIN Blocks ON height BETWEEN (max_height - ");
|
||||
sql.append(count);
|
||||
sql.append(" + 1) AND max_height ");
|
||||
sql.append("JOIN Blocks ON height BETWEEN (max_height - ? + 1) AND max_height ");
|
||||
sql.append("JOIN RewardShares ON RewardShares.reward_share_public_key = Blocks.minter");
|
||||
bindParams.add(count);
|
||||
} else {
|
||||
sql.append("FROM Blocks ");
|
||||
sql.append("JOIN RewardShares ON RewardShares.reward_share_public_key = Blocks.minter ");
|
||||
sql.append("WHERE height BETWEEN ");
|
||||
sql.append(endHeight - count);
|
||||
sql.append(" AND ");
|
||||
sql.append(endHeight - 1);
|
||||
sql.append("WHERE height BETWEEN ? AND ?");
|
||||
bindParams.add(Integer.valueOf(endHeight - count));
|
||||
bindParams.add(Integer.valueOf(endHeight - 1));
|
||||
}
|
||||
|
||||
} else {
|
||||
@@ -430,15 +433,14 @@ public class HSQLDBBlockRepository implements BlockRepository {
|
||||
|
||||
sql.append("FROM Blocks ");
|
||||
sql.append("JOIN RewardShares ON RewardShares.reward_share_public_key = Blocks.minter ");
|
||||
sql.append("WHERE height BETWEEN ");
|
||||
sql.append(startHeight);
|
||||
sql.append(" AND ");
|
||||
sql.append(startHeight + count - 1);
|
||||
sql.append("WHERE height BETWEEN ? AND ?");
|
||||
bindParams.add(startHeight);
|
||||
bindParams.add(Integer.valueOf(startHeight + count - 1));
|
||||
}
|
||||
|
||||
List<BlockInfo> blockInfos = new ArrayList<>();
|
||||
|
||||
try (ResultSet resultSet = this.repository.checkedExecute(sql.toString())) {
|
||||
try (ResultSet resultSet = this.repository.checkedExecute(sql.toString(), bindParams.toArray())) {
|
||||
if (resultSet == null)
|
||||
return blockInfos;
|
||||
|
||||
@@ -460,12 +462,43 @@ public class HSQLDBBlockRepository implements BlockRepository {
|
||||
}
|
||||
|
||||
@Override
|
||||
public int trimOldOnlineAccountsSignatures(long timestamp) throws DataException {
|
||||
String sql = "UPDATE Blocks set online_accounts_signatures = NULL WHERE minted_when < ? AND online_accounts_signatures IS NOT NULL";
|
||||
public int getOnlineAccountsSignaturesTrimHeight() throws DataException {
|
||||
String sql = "SELECT online_signatures_trim_height FROM DatabaseInfo";
|
||||
|
||||
try (ResultSet resultSet = this.repository.checkedExecute(sql)) {
|
||||
if (resultSet == null)
|
||||
return 0;
|
||||
|
||||
return resultSet.getInt(1);
|
||||
} catch (SQLException e) {
|
||||
throw new DataException("Unable to fetch online accounts signatures trim height from repository", e);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void setOnlineAccountsSignaturesTrimHeight(int trimHeight) throws DataException {
|
||||
String updateSql = "UPDATE DatabaseInfo SET online_signatures_trim_height = ?";
|
||||
|
||||
try {
|
||||
return this.repository.checkedExecuteUpdateCount(sql, timestamp);
|
||||
this.repository.executeCheckedUpdate(updateSql, trimHeight);
|
||||
} catch (SQLException e) {
|
||||
repository.examineException(e);
|
||||
throw new DataException("Unable to set online accounts signatures trim height in repository", e);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public int trimOldOnlineAccountsSignatures(int minHeight, int maxHeight) throws DataException {
|
||||
// We're often called so no need to trim all blocks in one go.
|
||||
// Limit updates to reduce CPU and memory load.
|
||||
String sql = "UPDATE Blocks SET online_accounts_signatures = NULL "
|
||||
+ "WHERE online_accounts_signatures IS NOT NULL "
|
||||
+ "AND height BETWEEN ? AND ?";
|
||||
|
||||
try {
|
||||
return this.repository.executeCheckedUpdate(sql, minHeight, maxHeight);
|
||||
} catch (SQLException e) {
|
||||
repository.examineException(e);
|
||||
throw new DataException("Unable to trim old online accounts signatures in repository", e);
|
||||
}
|
||||
}
|
||||
|
@@ -141,7 +141,7 @@ public class HSQLDBDatabaseUpdates {
|
||||
+ "transaction_count INTEGER NOT NULL, total_fees QortalAmount NOT NULL, transactions_signature Signature NOT NULL, "
|
||||
+ "height INTEGER NOT NULL, minted_when EpochMillis NOT NULL, "
|
||||
+ "minter QortalPublicKey NOT NULL, minter_signature Signature NOT NULL, AT_count INTEGER NOT NULL, AT_fees QortalAmount NOT NULL, "
|
||||
+ "online_accounts VARBINARY(1204), online_accounts_count INTEGER NOT NULL, online_accounts_timestamp EpochMillis, online_accounts_signatures VARBINARY(1M), "
|
||||
+ "online_accounts VARBINARY(1024), online_accounts_count INTEGER NOT NULL, online_accounts_timestamp EpochMillis, online_accounts_signatures VARBINARY(1M), "
|
||||
+ "PRIMARY KEY (signature))");
|
||||
// For finding blocks by height.
|
||||
stmt.execute("CREATE INDEX BlockHeightIndex ON Blocks (height)");
|
||||
@@ -153,16 +153,6 @@ public class HSQLDBDatabaseUpdates {
|
||||
stmt.execute("CREATE INDEX BlockTimestampHeightIndex ON Blocks (minted_when, height)");
|
||||
// Use a separate table space as this table will be very large.
|
||||
stmt.execute("SET TABLE Blocks NEW SPACE");
|
||||
|
||||
// Table to hold next block height.
|
||||
stmt.execute("CREATE TABLE NextBlockHeight (height INT NOT NULL)");
|
||||
// Initial value - should work for empty DB or populated DB.
|
||||
stmt.execute("INSERT INTO NextBlockHeight VALUES (SELECT IFNULL(MAX(height), 0) + 1 FROM Blocks)");
|
||||
// We use triggers on Blocks to update a simple "next block height" table
|
||||
String blockUpdateSql = "UPDATE NextBlockHeight SET height = (SELECT height + 1 FROM Blocks ORDER BY height DESC LIMIT 1)";
|
||||
stmt.execute("CREATE TRIGGER Next_block_height_insert_trigger AFTER INSERT ON Blocks " + blockUpdateSql);
|
||||
stmt.execute("CREATE TRIGGER Next_block_height_update_trigger AFTER UPDATE ON Blocks " + blockUpdateSql);
|
||||
stmt.execute("CREATE TRIGGER Next_block_height_delete_trigger AFTER DELETE ON Blocks " + blockUpdateSql);
|
||||
break;
|
||||
|
||||
case 2:
|
||||
@@ -222,6 +212,8 @@ public class HSQLDBDatabaseUpdates {
|
||||
+ "PRIMARY KEY (account))");
|
||||
// For looking up an account by public key
|
||||
stmt.execute("CREATE INDEX AccountPublicKeyIndex on Accounts (public_key)");
|
||||
// Use a separate table space as this table will be very large.
|
||||
stmt.execute("SET TABLE Accounts NEW SPACE");
|
||||
|
||||
// Account balances
|
||||
stmt.execute("CREATE TABLE AccountBalances (account QortalAddress, asset_id AssetID, balance QortalAmount NOT NULL, "
|
||||
@@ -230,6 +222,8 @@ public class HSQLDBDatabaseUpdates {
|
||||
stmt.execute("CREATE INDEX AccountBalancesAssetBalanceIndex ON AccountBalances (asset_id, balance)");
|
||||
// Add CHECK constraint to account balances
|
||||
stmt.execute("ALTER TABLE AccountBalances ADD CONSTRAINT CheckBalanceNotNegative CHECK (balance >= 0)");
|
||||
// Use a separate table space as this table will be very large.
|
||||
stmt.execute("SET TABLE AccountBalances NEW SPACE");
|
||||
|
||||
// Keeping track of QORT gained from holding legacy QORA
|
||||
stmt.execute("CREATE TABLE AccountQortFromQoraInfo (account QortalAddress, final_qort_from_qora QortalAmount, final_block_height INT, "
|
||||
@@ -427,6 +421,8 @@ public class HSQLDBDatabaseUpdates {
|
||||
+ "PRIMARY KEY (AT_address, height), FOREIGN KEY (AT_address) REFERENCES ATs (AT_address) ON DELETE CASCADE)");
|
||||
// For finding per-block AT states, ordered by creation timestamp
|
||||
stmt.execute("CREATE INDEX BlockATStateIndex on ATStates (height, created_when)");
|
||||
// Use a separate table space as this table will be very large.
|
||||
stmt.execute("SET TABLE ATStates NEW SPACE");
|
||||
|
||||
// Deploy CIYAM AT Transactions
|
||||
stmt.execute("CREATE TABLE DeployATTransactions (signature Signature, creator QortalPublicKey NOT NULL, AT_name ATName NOT NULL, "
|
||||
@@ -650,6 +646,37 @@ public class HSQLDBDatabaseUpdates {
|
||||
stmt.execute("CHECKPOINT");
|
||||
break;
|
||||
|
||||
case 23:
|
||||
// MESSAGE transactions index
|
||||
stmt.execute("CREATE INDEX IF NOT EXISTS MessageTransactionsRecipientIndex ON MessageTransactions (recipient, sender)");
|
||||
break;
|
||||
|
||||
case 24:
|
||||
// Remove unused NextBlockHeight table and corresponding triggers
|
||||
stmt.execute("DROP TRIGGER IF EXISTS Next_block_height_insert_trigger");
|
||||
stmt.execute("DROP TRIGGER IF EXISTS Next_block_height_update_trigger");
|
||||
stmt.execute("DROP TRIGGER IF EXISTS Next_block_height_delete_trigger");
|
||||
stmt.execute("DROP TABLE IF EXISTS NextBlockHeight");
|
||||
break;
|
||||
|
||||
case 25:
|
||||
// Remove excess created_when from ATStates
|
||||
stmt.execute("ALTER TABLE ATStates DROP created_when");
|
||||
stmt.execute("CREATE INDEX ATStateHeightIndex on ATStates (height)");
|
||||
break;
|
||||
|
||||
case 26:
|
||||
// Support for trimming
|
||||
stmt.execute("ALTER TABLE DatabaseInfo ADD AT_trim_height INT NOT NULL DEFAULT 0");
|
||||
stmt.execute("ALTER TABLE DatabaseInfo ADD online_signatures_trim_height INT NOT NULL DEFAULT 0");
|
||||
break;
|
||||
|
||||
case 27:
|
||||
// More indexes
|
||||
stmt.execute("CREATE INDEX IF NOT EXISTS PaymentTransactionsRecipientIndex ON PaymentTransactions (recipient)");
|
||||
stmt.execute("CREATE INDEX IF NOT EXISTS ATTransactionsRecipientIndex ON ATTransactions (recipient)");
|
||||
break;
|
||||
|
||||
default:
|
||||
// nothing to do
|
||||
return false;
|
||||
|
@@ -0,0 +1,85 @@
|
||||
package org.qortal.repository.hsqldb;
|
||||
|
||||
import java.sql.ResultSet;
|
||||
import java.sql.SQLException;
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
|
||||
import org.qortal.data.transaction.MessageTransactionData;
|
||||
import org.qortal.data.transaction.TransactionData;
|
||||
import org.qortal.repository.DataException;
|
||||
import org.qortal.repository.MessageRepository;
|
||||
import org.qortal.transaction.Transaction.TransactionType;
|
||||
|
||||
public class HSQLDBMessageRepository implements MessageRepository {
|
||||
|
||||
protected HSQLDBRepository repository;
|
||||
|
||||
public HSQLDBMessageRepository(HSQLDBRepository repository) {
|
||||
this.repository = repository;
|
||||
}
|
||||
|
||||
@Override
|
||||
public List<MessageTransactionData> getMessagesByParticipants(byte[] senderPublicKey,
|
||||
String recipient, Integer limit, Integer offset, Boolean reverse) throws DataException {
|
||||
if (senderPublicKey == null && recipient == null)
|
||||
throw new DataException("At least one of senderPublicKey or recipient required to fetch matching messages");
|
||||
|
||||
StringBuilder sql = new StringBuilder(1024);
|
||||
sql.append("SELECT signature from MessageTransactions "
|
||||
+ "JOIN Transactions USING (signature) "
|
||||
+ "JOIN BlockTransactions ON transaction_signature = signature "
|
||||
+ "WHERE ");
|
||||
|
||||
List<String> whereClauses = new ArrayList<>();
|
||||
List<Object> bindParams = new ArrayList<>();
|
||||
|
||||
if (senderPublicKey != null) {
|
||||
whereClauses.add("sender = ?");
|
||||
bindParams.add(senderPublicKey);
|
||||
}
|
||||
|
||||
if (recipient != null) {
|
||||
whereClauses.add("recipient = ?");
|
||||
bindParams.add(recipient);
|
||||
}
|
||||
|
||||
sql.append(String.join(" AND ", whereClauses));
|
||||
|
||||
sql.append("ORDER BY Transactions.created_when");
|
||||
sql.append((reverse == null || !reverse) ? " ASC" : " DESC");
|
||||
|
||||
HSQLDBRepository.limitOffsetSql(sql, limit, offset);
|
||||
|
||||
List<MessageTransactionData> messageTransactionsData = new ArrayList<>();
|
||||
|
||||
try (ResultSet resultSet = this.repository.checkedExecute(sql.toString(), bindParams.toArray())) {
|
||||
if (resultSet == null)
|
||||
return messageTransactionsData;
|
||||
|
||||
do {
|
||||
byte[] signature = resultSet.getBytes(1);
|
||||
|
||||
TransactionData transactionData = this.repository.getTransactionRepository().fromSignature(signature);
|
||||
if (transactionData == null || transactionData.getType() != TransactionType.MESSAGE)
|
||||
throw new DataException("Inconsistent data from repository when fetching message");
|
||||
|
||||
messageTransactionsData.add((MessageTransactionData) transactionData);
|
||||
} while (resultSet.next());
|
||||
|
||||
return messageTransactionsData;
|
||||
} catch (SQLException e) {
|
||||
throw new DataException("Unable to fetch matching messages from repository", e);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean exists(byte[] senderPublicKey, String recipient, byte[] messageData) throws DataException {
|
||||
try {
|
||||
return this.repository.exists("MessageTransactions", "sender = ? AND recipient = ? AND data = ?", senderPublicKey, recipient, messageData);
|
||||
} catch (SQLException e) {
|
||||
throw new DataException("Unable to check for existing message in repository", e);
|
||||
}
|
||||
}
|
||||
|
||||
}
|
@@ -16,11 +16,15 @@ import java.sql.Savepoint;
|
||||
import java.sql.Statement;
|
||||
import java.util.ArrayDeque;
|
||||
import java.util.ArrayList;
|
||||
import java.util.Collections;
|
||||
import java.util.Comparator;
|
||||
import java.util.Deque;
|
||||
import java.util.HashMap;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.regex.Matcher;
|
||||
import java.util.regex.Pattern;
|
||||
import java.util.stream.Stream;
|
||||
|
||||
import org.apache.logging.log4j.LogManager;
|
||||
import org.apache.logging.log4j.Logger;
|
||||
@@ -35,6 +39,7 @@ import org.qortal.repository.ChatRepository;
|
||||
import org.qortal.repository.CrossChainRepository;
|
||||
import org.qortal.repository.DataException;
|
||||
import org.qortal.repository.GroupRepository;
|
||||
import org.qortal.repository.MessageRepository;
|
||||
import org.qortal.repository.NameRepository;
|
||||
import org.qortal.repository.NetworkRepository;
|
||||
import org.qortal.repository.Repository;
|
||||
@@ -49,18 +54,32 @@ public class HSQLDBRepository implements Repository {
|
||||
private static final Logger LOGGER = LogManager.getLogger(HSQLDBRepository.class);
|
||||
|
||||
protected Connection connection;
|
||||
protected Deque<Savepoint> savepoints;
|
||||
protected final Deque<Savepoint> savepoints = new ArrayDeque<>(3);
|
||||
protected boolean debugState = false;
|
||||
protected Long slowQueryThreshold = null;
|
||||
protected List<String> sqlStatements;
|
||||
protected long sessionId;
|
||||
protected final Map<String, PreparedStatement> preparedStatementCache = new HashMap<>();
|
||||
|
||||
private final ATRepository atRepository = new HSQLDBATRepository(this);
|
||||
private final AccountRepository accountRepository = new HSQLDBAccountRepository(this);
|
||||
private final ArbitraryRepository arbitraryRepository = new HSQLDBArbitraryRepository(this);
|
||||
private final AssetRepository assetRepository = new HSQLDBAssetRepository(this);
|
||||
private final BlockRepository blockRepository = new HSQLDBBlockRepository(this);
|
||||
private final ChatRepository chatRepository = new HSQLDBChatRepository(this);
|
||||
private final CrossChainRepository crossChainRepository = new HSQLDBCrossChainRepository(this);
|
||||
private final GroupRepository groupRepository = new HSQLDBGroupRepository(this);
|
||||
private final MessageRepository messageRepository = new HSQLDBMessageRepository(this);
|
||||
private final NameRepository nameRepository = new HSQLDBNameRepository(this);
|
||||
private final NetworkRepository networkRepository = new HSQLDBNetworkRepository(this);
|
||||
private final TransactionRepository transactionRepository = new HSQLDBTransactionRepository(this);
|
||||
private final VotingRepository votingRepository = new HSQLDBVotingRepository(this);
|
||||
|
||||
// Constructors
|
||||
|
||||
// NB: no visibility modifier so only callable from within same package
|
||||
/* package */ HSQLDBRepository(Connection connection) throws DataException {
|
||||
this.connection = connection;
|
||||
this.savepoints = new ArrayDeque<>(3);
|
||||
|
||||
this.slowQueryThreshold = Settings.getInstance().getSlowQueryThreshold();
|
||||
if (this.slowQueryThreshold != null)
|
||||
@@ -88,62 +107,67 @@ public class HSQLDBRepository implements Repository {
|
||||
|
||||
@Override
|
||||
public ATRepository getATRepository() {
|
||||
return new HSQLDBATRepository(this);
|
||||
return this.atRepository;
|
||||
}
|
||||
|
||||
@Override
|
||||
public AccountRepository getAccountRepository() {
|
||||
return new HSQLDBAccountRepository(this);
|
||||
return this.accountRepository;
|
||||
}
|
||||
|
||||
@Override
|
||||
public ArbitraryRepository getArbitraryRepository() {
|
||||
return new HSQLDBArbitraryRepository(this);
|
||||
return this.arbitraryRepository;
|
||||
}
|
||||
|
||||
@Override
|
||||
public AssetRepository getAssetRepository() {
|
||||
return new HSQLDBAssetRepository(this);
|
||||
return this.assetRepository;
|
||||
}
|
||||
|
||||
@Override
|
||||
public BlockRepository getBlockRepository() {
|
||||
return new HSQLDBBlockRepository(this);
|
||||
return this.blockRepository;
|
||||
}
|
||||
|
||||
@Override
|
||||
public ChatRepository getChatRepository() {
|
||||
return new HSQLDBChatRepository(this);
|
||||
return this.chatRepository;
|
||||
}
|
||||
|
||||
@Override
|
||||
public CrossChainRepository getCrossChainRepository() {
|
||||
return new HSQLDBCrossChainRepository(this);
|
||||
return this.crossChainRepository;
|
||||
}
|
||||
|
||||
@Override
|
||||
public GroupRepository getGroupRepository() {
|
||||
return new HSQLDBGroupRepository(this);
|
||||
return this.groupRepository;
|
||||
}
|
||||
|
||||
@Override
|
||||
public MessageRepository getMessageRepository() {
|
||||
return this.messageRepository;
|
||||
}
|
||||
|
||||
@Override
|
||||
public NameRepository getNameRepository() {
|
||||
return new HSQLDBNameRepository(this);
|
||||
return this.nameRepository;
|
||||
}
|
||||
|
||||
@Override
|
||||
public NetworkRepository getNetworkRepository() {
|
||||
return new HSQLDBNetworkRepository(this);
|
||||
return this.networkRepository;
|
||||
}
|
||||
|
||||
@Override
|
||||
public TransactionRepository getTransactionRepository() {
|
||||
return new HSQLDBTransactionRepository(this);
|
||||
return this.transactionRepository;
|
||||
}
|
||||
|
||||
@Override
|
||||
public VotingRepository getVotingRepository() {
|
||||
return new HSQLDBVotingRepository(this);
|
||||
return this.votingRepository;
|
||||
}
|
||||
|
||||
@Override
|
||||
@@ -185,7 +209,7 @@ public class HSQLDBRepository implements Repository {
|
||||
this.savepoints.clear();
|
||||
|
||||
// Before clearing statements so we can log what led to assertion error
|
||||
assertEmptyTransaction("transaction commit");
|
||||
assertEmptyTransaction("transaction rollback");
|
||||
|
||||
if (this.sqlStatements != null)
|
||||
this.sqlStatements.clear();
|
||||
@@ -240,7 +264,12 @@ public class HSQLDBRepository implements Repository {
|
||||
try (Statement stmt = this.connection.createStatement()) {
|
||||
assertEmptyTransaction("connection close");
|
||||
|
||||
// give connection back to the pool
|
||||
// Assume we are not going to be GC'd for a while
|
||||
this.preparedStatementCache.clear();
|
||||
this.sqlStatements = null;
|
||||
this.savepoints.clear();
|
||||
|
||||
// Give connection back to the pool
|
||||
this.connection.close();
|
||||
this.connection = null;
|
||||
} catch (SQLException e) {
|
||||
@@ -270,11 +299,12 @@ public class HSQLDBRepository implements Repository {
|
||||
Path oldRepoDirPath = Paths.get(dbPathname).getParent();
|
||||
|
||||
// Delete old repository files
|
||||
Files.walk(oldRepoDirPath)
|
||||
.sorted(Comparator.reverseOrder())
|
||||
try (Stream<Path> paths = Files.walk(oldRepoDirPath)) {
|
||||
paths.sorted(Comparator.reverseOrder())
|
||||
.map(Path::toFile)
|
||||
.filter(file -> file.getPath().startsWith(dbPathname))
|
||||
.forEach(File::delete);
|
||||
}
|
||||
}
|
||||
} catch (NoSuchFileException e) {
|
||||
// Nothing to remove
|
||||
@@ -314,11 +344,12 @@ public class HSQLDBRepository implements Repository {
|
||||
Path backupDirPath = Paths.get(backupPathname).getParent();
|
||||
String backupDirPathname = backupDirPath.toString();
|
||||
|
||||
Files.walk(backupDirPath)
|
||||
.sorted(Comparator.reverseOrder())
|
||||
try (Stream<Path> paths = Files.walk(backupDirPath)) {
|
||||
paths.sorted(Comparator.reverseOrder())
|
||||
.map(Path::toFile)
|
||||
.filter(file -> file.getPath().startsWith(backupDirPathname))
|
||||
.forEach(File::delete);
|
||||
}
|
||||
} catch (NoSuchFileException e) {
|
||||
// Nothing to remove
|
||||
} catch (SQLException | IOException e) {
|
||||
@@ -333,6 +364,19 @@ public class HSQLDBRepository implements Repository {
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void performPeriodicMaintenance() throws DataException {
|
||||
// Defrag DB - takes a while!
|
||||
try (Statement stmt = this.connection.createStatement()) {
|
||||
LOGGER.info("performing maintenance - this will take a while");
|
||||
stmt.execute("CHECKPOINT");
|
||||
stmt.execute("CHECKPOINT DEFRAG");
|
||||
LOGGER.info("maintenance completed");
|
||||
} catch (SQLException e) {
|
||||
throw new DataException("Unable to defrag repository");
|
||||
}
|
||||
}
|
||||
|
||||
/** Returns DB pathname from passed connection URL. If memory DB, returns "mem". */
|
||||
private static String getDbPathname(String connectionUrl) {
|
||||
Pattern pattern = Pattern.compile("hsqldb:(mem|file):(.*?)(;|$)");
|
||||
@@ -370,11 +414,12 @@ public class HSQLDBRepository implements Repository {
|
||||
LOGGER.info("Attempting repository recovery using backup");
|
||||
|
||||
// Move old repository files out the way
|
||||
Files.walk(oldRepoDirPath)
|
||||
.sorted(Comparator.reverseOrder())
|
||||
try (Stream<Path> paths = Files.walk(oldRepoDirPath)) {
|
||||
paths.sorted(Comparator.reverseOrder())
|
||||
.map(Path::toFile)
|
||||
.filter(file -> file.getPath().startsWith(dbPathname))
|
||||
.forEach(File::delete);
|
||||
}
|
||||
|
||||
try (Statement stmt = connection.createStatement()) {
|
||||
// Now "backup" the backup back to original repository location (the parent).
|
||||
@@ -414,7 +459,33 @@ public class HSQLDBRepository implements Repository {
|
||||
if (this.sqlStatements != null)
|
||||
this.sqlStatements.add(sql);
|
||||
|
||||
return this.connection.prepareStatement(sql);
|
||||
return cachePreparedStatement(sql);
|
||||
}
|
||||
|
||||
private PreparedStatement cachePreparedStatement(String sql) throws SQLException {
|
||||
/*
|
||||
* We cache a duplicate PreparedStatement for this SQL string,
|
||||
* which we never close, which means HSQLDB also caches a parsed,
|
||||
* prepared statement that can be reused for subsequent
|
||||
* calls to HSQLDB.prepareStatement(sql).
|
||||
*
|
||||
* See org.hsqldb.StatementManager for more details.
|
||||
*/
|
||||
PreparedStatement preparedStatement = this.preparedStatementCache.get(sql);
|
||||
if (preparedStatement == null || preparedStatement.isClosed()) {
|
||||
if (preparedStatement != null)
|
||||
// This shouldn't occur, so log, but recompile
|
||||
LOGGER.debug(() -> String.format("Recompiling closed PreparedStatement: %s", sql));
|
||||
|
||||
preparedStatement = this.connection.prepareStatement(sql);
|
||||
this.preparedStatementCache.put(sql, preparedStatement);
|
||||
} else {
|
||||
// Clean up ready for reuse
|
||||
preparedStatement.clearBatch();
|
||||
preparedStatement.clearParameters();
|
||||
}
|
||||
|
||||
return preparedStatement;
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -430,9 +501,8 @@ public class HSQLDBRepository implements Repository {
|
||||
public ResultSet checkedExecute(String sql, Object... objects) throws SQLException {
|
||||
PreparedStatement preparedStatement = this.prepareStatement(sql);
|
||||
|
||||
// Close the PreparedStatement when the ResultSet is closed otherwise there's a potential resource leak.
|
||||
// We can't use try-with-resources here as closing the PreparedStatement on return would also prematurely close the ResultSet.
|
||||
preparedStatement.closeOnCompletion();
|
||||
// We don't close the PreparedStatement when the ResultSet is closed because we cached PreparedStatements now.
|
||||
// They are cleaned up when connection/session is closed.
|
||||
|
||||
long beforeQuery = this.slowQueryThreshold == null ? 0 : System.currentTimeMillis();
|
||||
|
||||
@@ -460,7 +530,7 @@ public class HSQLDBRepository implements Repository {
|
||||
* @param objects
|
||||
* @throws SQLException
|
||||
*/
|
||||
private void prepareExecute(PreparedStatement preparedStatement, Object... objects) throws SQLException {
|
||||
private void bindStatementParams(PreparedStatement preparedStatement, Object... objects) throws SQLException {
|
||||
for (int i = 0; i < objects.length; ++i)
|
||||
// Special treatment for BigDecimals so that they retain their "scale",
|
||||
// which would otherwise be assumed as 0.
|
||||
@@ -481,7 +551,7 @@ public class HSQLDBRepository implements Repository {
|
||||
* @throws SQLException
|
||||
*/
|
||||
private ResultSet checkedExecuteResultSet(PreparedStatement preparedStatement, Object... objects) throws SQLException {
|
||||
prepareExecute(preparedStatement, objects);
|
||||
bindStatementParams(preparedStatement, objects);
|
||||
|
||||
if (!preparedStatement.execute())
|
||||
throw new SQLException("Fetching from database produced no results");
|
||||
@@ -504,31 +574,52 @@ public class HSQLDBRepository implements Repository {
|
||||
* @return number of changed rows
|
||||
* @throws SQLException
|
||||
*/
|
||||
/* package */ int checkedExecuteUpdateCount(String sql, Object... objects) throws SQLException {
|
||||
try (PreparedStatement preparedStatement = this.prepareStatement(sql)) {
|
||||
prepareExecute(preparedStatement, objects);
|
||||
/* package */ int executeCheckedUpdate(String sql, Object... objects) throws SQLException {
|
||||
return this.executeCheckedBatchUpdate(sql, Collections.singletonList(objects));
|
||||
}
|
||||
|
||||
long beforeQuery = this.slowQueryThreshold == null ? 0 : System.currentTimeMillis();
|
||||
/**
|
||||
* Execute batched PreparedStatement
|
||||
*
|
||||
* @param preparedStatement
|
||||
* @param objects
|
||||
* @return number of changed rows
|
||||
* @throws SQLException
|
||||
*/
|
||||
/* package */ int executeCheckedBatchUpdate(String sql, List<Object[]> batchedObjects) throws SQLException {
|
||||
// Nothing to do?
|
||||
if (batchedObjects == null || batchedObjects.isEmpty())
|
||||
return 0;
|
||||
|
||||
if (preparedStatement.execute())
|
||||
throw new SQLException("Database produced results, not row count");
|
||||
PreparedStatement preparedStatement = this.prepareStatement(sql);
|
||||
for (Object[] objects : batchedObjects) {
|
||||
this.bindStatementParams(preparedStatement, objects);
|
||||
preparedStatement.addBatch();
|
||||
}
|
||||
|
||||
if (this.slowQueryThreshold != null) {
|
||||
long queryTime = System.currentTimeMillis() - beforeQuery;
|
||||
long beforeQuery = this.slowQueryThreshold == null ? 0 : System.currentTimeMillis();
|
||||
|
||||
if (queryTime > this.slowQueryThreshold) {
|
||||
LOGGER.info(() -> String.format("HSQLDB query took %d ms: %s", queryTime, sql), new SQLException("slow query"));
|
||||
int[] updateCounts = preparedStatement.executeBatch();
|
||||
|
||||
logStatements();
|
||||
}
|
||||
if (this.slowQueryThreshold != null) {
|
||||
long queryTime = System.currentTimeMillis() - beforeQuery;
|
||||
|
||||
if (queryTime > this.slowQueryThreshold) {
|
||||
LOGGER.info(() -> String.format("HSQLDB query took %d ms: %s", queryTime, sql), new SQLException("slow query"));
|
||||
|
||||
logStatements();
|
||||
}
|
||||
}
|
||||
|
||||
int rowCount = preparedStatement.getUpdateCount();
|
||||
if (rowCount == -1)
|
||||
int totalCount = 0;
|
||||
for (int i = 0; i < updateCounts.length; ++i) {
|
||||
if (updateCounts[i] < 0)
|
||||
throw new SQLException("Database returned invalid row count");
|
||||
|
||||
return rowCount;
|
||||
totalCount += updateCounts[i];
|
||||
}
|
||||
|
||||
return totalCount;
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -598,7 +689,25 @@ public class HSQLDBRepository implements Repository {
|
||||
sql.append(" WHERE ");
|
||||
sql.append(whereClause);
|
||||
|
||||
return this.checkedExecuteUpdateCount(sql.toString(), objects);
|
||||
return this.executeCheckedUpdate(sql.toString(), objects);
|
||||
}
|
||||
|
||||
/**
|
||||
* Delete rows from database table.
|
||||
*
|
||||
* @param tableName
|
||||
* @param whereClause
|
||||
* @param objects
|
||||
* @throws SQLException
|
||||
*/
|
||||
public int deleteBatch(String tableName, String whereClause, List<Object[]> batchedObjects) throws SQLException {
|
||||
StringBuilder sql = new StringBuilder(256);
|
||||
sql.append("DELETE FROM ");
|
||||
sql.append(tableName);
|
||||
sql.append(" WHERE ");
|
||||
sql.append(whereClause);
|
||||
|
||||
return this.executeCheckedBatchUpdate(sql.toString(), batchedObjects);
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -612,7 +721,7 @@ public class HSQLDBRepository implements Repository {
|
||||
sql.append("DELETE FROM ");
|
||||
sql.append(tableName);
|
||||
|
||||
return this.checkedExecuteUpdateCount(sql.toString());
|
||||
return this.executeCheckedUpdate(sql.toString());
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -696,9 +805,9 @@ public class HSQLDBRepository implements Repository {
|
||||
LOGGER.info(sql);
|
||||
}
|
||||
|
||||
/** Logs other HSQLDB sessions then re-throws passed exception */
|
||||
public SQLException examineException(SQLException e) throws SQLException {
|
||||
LOGGER.error(String.format("HSQLDB error (session %d): %s", this.sessionId, e.getMessage()), e);
|
||||
/** Logs other HSQLDB sessions then returns passed exception */
|
||||
public SQLException examineException(SQLException e) {
|
||||
LOGGER.error(() -> String.format("HSQLDB error (session %d): %s", this.sessionId, e.getMessage()), e);
|
||||
|
||||
logStatements();
|
||||
|
||||
@@ -716,7 +825,11 @@ public class HSQLDBRepository implements Repository {
|
||||
String thisWaitingFor = resultSet.getString(5);
|
||||
String currentStatement = resultSet.getString(6);
|
||||
|
||||
LOGGER.error(String.format("Session %d, %s transaction (size %d), waiting for this '%s', this waiting for '%s', current statement: %s",
|
||||
// Skip logging idle sessions
|
||||
if (transactionSize == 0 && waitingForThis.isEmpty() && thisWaitingFor.isEmpty() && currentStatement.isEmpty())
|
||||
continue;
|
||||
|
||||
LOGGER.error(() -> String.format("Session %d, %s transaction (size %d), waiting for this '%s', this waiting for '%s', current statement: %s",
|
||||
systemSessionId, (inTransaction ? "in" : "not in"), transactionSize, waitingForThis, thisWaitingFor, currentStatement));
|
||||
} while (resultSet.next());
|
||||
} catch (SQLException de) {
|
||||
@@ -728,14 +841,19 @@ public class HSQLDBRepository implements Repository {
|
||||
}
|
||||
|
||||
private void assertEmptyTransaction(String context) throws DataException {
|
||||
try (Statement stmt = this.connection.createStatement()) {
|
||||
String sql = "SELECT transaction, transaction_size FROM information_schema.system_sessions WHERE session_id = ?";
|
||||
|
||||
try {
|
||||
PreparedStatement stmt = this.cachePreparedStatement(sql);
|
||||
stmt.setLong(1, this.sessionId);
|
||||
|
||||
// Diagnostic check for uncommitted changes
|
||||
if (!stmt.execute("SELECT transaction, transaction_size FROM information_schema.system_sessions WHERE session_id = " + this.sessionId)) // TRANSACTION_SIZE() broken?
|
||||
if (!stmt.execute()) // TRANSACTION_SIZE() broken?
|
||||
throw new DataException("Unable to check repository status after " + context);
|
||||
|
||||
try (ResultSet resultSet = stmt.getResultSet()) {
|
||||
if (resultSet == null || !resultSet.next()) {
|
||||
LOGGER.warn(String.format("Unable to check repository status after %s", context));
|
||||
LOGGER.warn(() -> String.format("Unable to check repository status after %s", context));
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -743,7 +861,11 @@ public class HSQLDBRepository implements Repository {
|
||||
int transactionCount = resultSet.getInt(2);
|
||||
|
||||
if (inTransaction && transactionCount != 0) {
|
||||
LOGGER.warn(String.format("Uncommitted changes (%d) after %s, session [%d]", transactionCount, context, this.sessionId), new Exception("Uncommitted repository changes"));
|
||||
LOGGER.warn(() -> String.format("Uncommitted changes (%d) after %s, session [%d]",
|
||||
transactionCount,
|
||||
context,
|
||||
this.sessionId),
|
||||
new Exception("Uncommitted repository changes"));
|
||||
logStatements();
|
||||
}
|
||||
}
|
||||
|
@@ -60,7 +60,9 @@ public class HSQLDBSaver {
|
||||
*/
|
||||
public boolean execute(HSQLDBRepository repository) throws SQLException {
|
||||
String sql = this.formatInsertWithPlaceholders();
|
||||
try (PreparedStatement preparedStatement = repository.prepareStatement(sql)) {
|
||||
|
||||
try {
|
||||
PreparedStatement preparedStatement = repository.prepareStatement(sql);
|
||||
this.bindValues(preparedStatement);
|
||||
|
||||
return preparedStatement.execute();
|
||||
|
@@ -19,7 +19,6 @@ import org.qortal.data.PaymentData;
|
||||
import org.qortal.data.group.GroupApprovalData;
|
||||
import org.qortal.data.transaction.BaseTransactionData;
|
||||
import org.qortal.data.transaction.GroupApprovalTransactionData;
|
||||
import org.qortal.data.transaction.MessageTransactionData;
|
||||
import org.qortal.data.transaction.TransactionData;
|
||||
import org.qortal.data.transaction.TransferAssetTransactionData;
|
||||
import org.qortal.repository.DataException;
|
||||
@@ -586,6 +585,69 @@ public class HSQLDBTransactionRepository implements TransactionRepository {
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public List<byte[]> getSignaturesMatchingCriteria(TransactionType txType, byte[] publicKey,
|
||||
Integer minBlockHeight, Integer maxBlockHeight) throws DataException {
|
||||
List<byte[]> signatures = new ArrayList<>();
|
||||
|
||||
StringBuilder sql = new StringBuilder(1024);
|
||||
sql.append("SELECT signature FROM Transactions ");
|
||||
|
||||
List<String> whereClauses = new ArrayList<>();
|
||||
List<Object> bindParams = new ArrayList<>();
|
||||
|
||||
if (txType != null) {
|
||||
whereClauses.add("type = ?");
|
||||
bindParams.add(txType.value);
|
||||
}
|
||||
|
||||
if (publicKey != null) {
|
||||
whereClauses.add("creator = ?");
|
||||
bindParams.add(publicKey);
|
||||
}
|
||||
|
||||
if (minBlockHeight != null) {
|
||||
whereClauses.add("Transactions.block_height >= ?");
|
||||
bindParams.add(minBlockHeight);
|
||||
}
|
||||
|
||||
if (maxBlockHeight != null) {
|
||||
whereClauses.add("Transactions.block_height <= ?");
|
||||
bindParams.add(maxBlockHeight);
|
||||
}
|
||||
|
||||
if (!whereClauses.isEmpty()) {
|
||||
sql.append(" WHERE ");
|
||||
|
||||
final int whereClausesSize = whereClauses.size();
|
||||
for (int wci = 0; wci < whereClausesSize; ++wci) {
|
||||
if (wci != 0)
|
||||
sql.append(" AND ");
|
||||
|
||||
sql.append(whereClauses.get(wci));
|
||||
}
|
||||
}
|
||||
|
||||
sql.append(" ORDER BY Transactions.created_when");
|
||||
|
||||
LOGGER.trace(() -> String.format("Transaction search SQL: %s", sql));
|
||||
|
||||
try (ResultSet resultSet = this.repository.checkedExecute(sql.toString(), bindParams.toArray())) {
|
||||
if (resultSet == null)
|
||||
return signatures;
|
||||
|
||||
do {
|
||||
byte[] signature = resultSet.getBytes(1);
|
||||
|
||||
signatures.add(signature);
|
||||
} while (resultSet.next());
|
||||
|
||||
return signatures;
|
||||
} catch (SQLException e) {
|
||||
throw new DataException("Unable to fetch matching transaction signatures from repository", e);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public byte[] getLatestAutoUpdateTransaction(TransactionType txType, int txGroupId, Integer service) throws DataException {
|
||||
StringBuilder sql = new StringBuilder(1024);
|
||||
@@ -631,43 +693,6 @@ public class HSQLDBTransactionRepository implements TransactionRepository {
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public List<MessageTransactionData> getMessagesByRecipient(String recipient,
|
||||
Integer limit, Integer offset, Boolean reverse) throws DataException {
|
||||
StringBuilder sql = new StringBuilder(1024);
|
||||
sql.append("SELECT signature from MessageTransactions "
|
||||
+ "JOIN Transactions USING (signature) "
|
||||
+ "JOIN BlockTransactions ON transaction_signature = signature "
|
||||
+ "WHERE recipient = ?");
|
||||
|
||||
sql.append("ORDER BY Transactions.created_when");
|
||||
sql.append((reverse == null || !reverse) ? " ASC" : " DESC");
|
||||
|
||||
HSQLDBRepository.limitOffsetSql(sql, limit, offset);
|
||||
|
||||
List<MessageTransactionData> messageTransactionsData = new ArrayList<>();
|
||||
|
||||
try (ResultSet resultSet = this.repository.checkedExecute(sql.toString(), recipient)) {
|
||||
if (resultSet == null)
|
||||
return messageTransactionsData;
|
||||
|
||||
do {
|
||||
byte[] signature = resultSet.getBytes(1);
|
||||
|
||||
TransactionData transactionData = this.fromSignature(signature);
|
||||
if (transactionData == null || transactionData.getType() != TransactionType.MESSAGE)
|
||||
return null;
|
||||
|
||||
messageTransactionsData.add((MessageTransactionData) transactionData);
|
||||
} while (resultSet.next());
|
||||
|
||||
return messageTransactionsData;
|
||||
} catch (SQLException e) {
|
||||
throw new DataException("Unable to fetch trade-bot messages from repository", e);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@Override
|
||||
public List<TransactionData> getAssetTransactions(long assetId, ConfirmationStatus confirmationStatus, Integer limit, Integer offset, Boolean reverse)
|
||||
throws DataException {
|
||||
|
@@ -5,6 +5,7 @@ import java.io.FileNotFoundException;
|
||||
import java.io.FileReader;
|
||||
import java.io.IOException;
|
||||
import java.io.Reader;
|
||||
import java.util.Locale;
|
||||
|
||||
import javax.xml.bind.JAXBContext;
|
||||
import javax.xml.bind.JAXBException;
|
||||
@@ -41,6 +42,9 @@ public class Settings {
|
||||
// Settings, and other config files
|
||||
private String userPath;
|
||||
|
||||
// General
|
||||
private String localeLang = Locale.getDefault().getLanguage();
|
||||
|
||||
// Common to all networking (API/P2P)
|
||||
private String bindAddress = "::"; // Use IPv6 wildcard to listen on all local addresses
|
||||
|
||||
@@ -80,6 +84,22 @@ public class Settings {
|
||||
/** Whether to show a notification when we backup repository. */
|
||||
private boolean showBackupNotification = false;
|
||||
|
||||
/** How long to keep old, full, AT state data (ms). */
|
||||
private long atStatesMaxLifetime = 2 * 7 * 24 * 60 * 60 * 1000L; // milliseconds
|
||||
/** How often to attempt AT state trimming (ms). */
|
||||
private long atStatesTrimInterval = 5678L; // milliseconds
|
||||
/** Block height range to scan for trimmable AT states.<br>
|
||||
* This has a significant effect on execution time. */
|
||||
private int atStatesTrimBatchSize = 100; // blocks
|
||||
/** Max number of AT states to trim in one go. */
|
||||
private int atStatesTrimLimit = 4000; // records
|
||||
|
||||
/** How often to attempt online accounts signatures trimming (ms). */
|
||||
private long onlineSignaturesTrimInterval = 9876L; // milliseconds
|
||||
/** Block height range to scan for trimmable online accounts signatures.<br>
|
||||
* This has a significant effect on execution time. */
|
||||
private int onlineSignaturesTrimBatchSize = 100; // blocks
|
||||
|
||||
// Peer-to-peer related
|
||||
private boolean isTestNet = false;
|
||||
/** Port number for inbound peer-to-peer connections. */
|
||||
@@ -98,6 +118,9 @@ public class Settings {
|
||||
// Which blockchains this node is running
|
||||
private String blockchainConfig = null; // use default from resources
|
||||
private BitcoinNet bitcoinNet = BitcoinNet.MAIN;
|
||||
// Also crosschain-related:
|
||||
/** Whether to show SysTray pop-up notifications when trade-bot entries change state */
|
||||
private boolean tradebotSystrayEnabled = false;
|
||||
|
||||
// Repository related
|
||||
/** Queries that take longer than this are logged. (milliseconds) */
|
||||
@@ -256,6 +279,10 @@ public class Settings {
|
||||
return this.userPath;
|
||||
}
|
||||
|
||||
public String getLocaleLang() {
|
||||
return this.localeLang;
|
||||
}
|
||||
|
||||
public int getUiServerPort() {
|
||||
return this.uiPort;
|
||||
}
|
||||
@@ -367,6 +394,10 @@ public class Settings {
|
||||
return this.bitcoinNet;
|
||||
}
|
||||
|
||||
public boolean isTradebotSystrayEnabled() {
|
||||
return this.tradebotSystrayEnabled;
|
||||
}
|
||||
|
||||
public Long getSlowQueryThreshold() {
|
||||
return this.slowQueryThreshold;
|
||||
}
|
||||
@@ -399,4 +430,28 @@ public class Settings {
|
||||
return this.showBackupNotification;
|
||||
}
|
||||
|
||||
public long getAtStatesMaxLifetime() {
|
||||
return this.atStatesMaxLifetime;
|
||||
}
|
||||
|
||||
public long getAtStatesTrimInterval() {
|
||||
return this.atStatesTrimInterval;
|
||||
}
|
||||
|
||||
public int getAtStatesTrimBatchSize() {
|
||||
return this.atStatesTrimBatchSize;
|
||||
}
|
||||
|
||||
public int getAtStatesTrimLimit() {
|
||||
return this.atStatesTrimLimit;
|
||||
}
|
||||
|
||||
public long getOnlineSignaturesTrimInterval() {
|
||||
return this.onlineSignaturesTrimInterval;
|
||||
}
|
||||
|
||||
public int getOnlineSignaturesTrimBatchSize() {
|
||||
return this.onlineSignaturesTrimBatchSize;
|
||||
}
|
||||
|
||||
}
|
||||
|
@@ -28,7 +28,8 @@ public class NTP implements Runnable {
|
||||
private static volatile boolean isStopping = false;
|
||||
private static ExecutorService instanceExecutor;
|
||||
private static NTP instance;
|
||||
private static volatile Long offset = null;
|
||||
private static volatile boolean isOffsetSet = false;
|
||||
private static volatile long offset = 0;
|
||||
|
||||
static class NTPServer {
|
||||
private static final int MIN_POLL = 64;
|
||||
@@ -136,6 +137,7 @@ public class NTP implements Runnable {
|
||||
public static synchronized void setFixedOffset(Long offset) {
|
||||
// Fix offset, e.g. for testing
|
||||
NTP.offset = offset;
|
||||
isOffsetSet = true;
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -144,7 +146,7 @@ public class NTP implements Runnable {
|
||||
* @return internet time (ms), or null if unsynchronized.
|
||||
*/
|
||||
public static Long getTime() {
|
||||
if (NTP.offset == null)
|
||||
if (!isOffsetSet)
|
||||
return null;
|
||||
|
||||
return System.currentTimeMillis() + NTP.offset;
|
||||
@@ -248,6 +250,7 @@ public class NTP implements Runnable {
|
||||
thresholdStddev, filteredMean, filteredStddev, numberValues, ntpServers.size()));
|
||||
|
||||
NTP.offset = (long) filteredMean;
|
||||
isOffsetSet = true;
|
||||
LOGGER.debug(() -> String.format("New NTP offset: %d", NTP.offset));
|
||||
}
|
||||
}
|
||||
|
@@ -8,6 +8,12 @@ BLOCKCHAIN_NEEDS_SYNC = blockchain needs to synchronize first
|
||||
# Blocks
|
||||
BLOCK_UNKNOWN = block unknown
|
||||
|
||||
BTC_BALANCE_ISSUE = insufficient Bitcoin balance
|
||||
|
||||
BTC_NETWORK_ISSUE = Bitcoin/ElectrumX network issue
|
||||
|
||||
BTC_TOO_SOON = too soon to broadcast Bitcoin transaction (lockTime/median block time)
|
||||
|
||||
CANNOT_MINT = account cannot mint
|
||||
|
||||
GROUP_UNKNOWN = group unknown
|
||||
@@ -15,7 +21,7 @@ GROUP_UNKNOWN = group unknown
|
||||
INVALID_ADDRESS = invalid address
|
||||
|
||||
# Assets
|
||||
INVALID_ASSET_ID = invalid asset id
|
||||
INVALID_ASSET_ID = invalid asset ID
|
||||
|
||||
INVALID_CRITERIA = invalid search criteria
|
||||
|
||||
@@ -36,18 +42,21 @@ INVALID_REFERENCE = invalid reference
|
||||
# Validation
|
||||
INVALID_SIGNATURE = invalid signature
|
||||
|
||||
JSON = failed to parse json message
|
||||
JSON = failed to parse JSON message
|
||||
|
||||
NAME_UNKNOWN = name unknown
|
||||
|
||||
NON_PRODUCTION = this API call is not permitted for production systems
|
||||
|
||||
NO_TIME_SYNC = no clock synchronization yet
|
||||
|
||||
ORDER_UNKNOWN = unknown asset order ID
|
||||
|
||||
PUBLIC_KEY_NOT_FOUND = public key not found
|
||||
|
||||
REPOSITORY_ISSUE = repository error
|
||||
|
||||
# This one is special in that caller expected to pass two additional strings, hence the two %s
|
||||
TRANSACTION_INVALID = transaction invalid: %s (%s)
|
||||
|
||||
TRANSACTION_UNKNOWN = transaction unknown
|
||||
|
53
src/main/resources/i18n/ApiError_ru.properties
Normal file
53
src/main/resources/i18n/ApiError_ru.properties
Normal file
@@ -0,0 +1,53 @@
|
||||
#Generated by ResourceBundle Editor (http://essiembre.github.io/eclipse-rbe/)
|
||||
# Keys are from api.ApiError enum
|
||||
|
||||
ADDRESS_UNKNOWN = \u00D0\u00BD\u00D0\u00B5\u00D0\u00B8\u00D0\u00B7\u00D0\u00B2\u00D0\u00B5\u00D1\u0081\u00D1\u0082\u00D0\u00BD\u00D0\u00B0\u00D1\u008F \u00D1\u0083\u00D1\u0087\u00D0\u00B5\u00D1\u0082\u00D0\u00BD\u00D0\u00B0\u00D1\u008F \u00D0\u00B7\u00D0\u00B0\u00D0\u00BF\u00D0\u00B8\u00D1\u0081\u00D1\u008C
|
||||
|
||||
# Blocks
|
||||
BLOCK_UNKNOWN = \u00D0\u00BD\u00D0\u00B5\u00D0\u00B8\u00D0\u00B7\u00D0\u00B2\u00D0\u00B5\u00D1\u0081\u00D1\u0082\u00D0\u00BD\u00D1\u008B\u00D0\u00B9 \u00D0\u00B1\u00D0\u00BB\u00D0\u00BE\u00D0\u00BA
|
||||
|
||||
CANNOT_MINT = \u00D0\u00B0\u00D0\u00BA\u00D0\u00BA\u00D0\u00B0\u00D1\u0083\u00D0\u00BD\u00D1\u0082 \u00D0\u00BD\u00D0\u00B5 \u00D0\u00BC\u00D0\u00BE\u00D0\u00B6\u00D0\u00B5\u00D1\u0082 \u00D1\u0087\u00D0\u00B5\u00D0\u00BA\u00D0\u00B0\u00D0\u00BD\u00D0\u00B8\u00D1\u0082\u00D1\u008C
|
||||
|
||||
GROUP_UNKNOWN = \u00D0\u00BD\u00D0\u00B5\u00D0\u00B8\u00D0\u00B7\u00D0\u00B2\u00D0\u00B5\u00D1\u0081\u00D1\u0082\u00D0\u00BD\u00D0\u00B0\u00D1\u008F \u00D0\u00B3\u00D1\u0080\u00D1\u0083\u00D0\u00BF\u00D0\u00BF\u00D0\u00B0
|
||||
|
||||
INVALID_ADDRESS = \u00D0\u00BD\u00D0\u00B5\u00D0\u00B8\u00D0\u00B7\u00D0\u00B2\u00D0\u00B5\u00D1\u0081\u00D1\u0082\u00D0\u00BD\u00D1\u008B\u00D0\u00B9 \u00D0\u00B0\u00D0\u00B4\u00D1\u0080\u00D0\u00B5\u00D1\u0081
|
||||
|
||||
# Assets
|
||||
INVALID_ASSET_ID = \u00D0\u00BD\u00D0\u00B5\u00D0\u00B2\u00D0\u00B5\u00D1\u0080\u00D0\u00BD\u00D1\u008B\u00D0\u00B9 \u00D0\u00B8\u00D0\u00B4\u00D0\u00B5\u00D0\u00BD\u00D1\u0082\u00D0\u00B8\u00D1\u0084\u00D0\u00B8\u00D0\u00BA\u00D0\u00B0\u00D1\u0082\u00D0\u00BE\u00D1\u0080 \u00D0\u00B0\u00D0\u00BA\u00D1\u0082\u00D0\u00B8\u00D0\u00B2\u00D0\u00B0
|
||||
|
||||
INVALID_CRITERIA = \u00D0\u00BD\u00D0\u00B5\u00D0\u00B2\u00D0\u00B5\u00D1\u0080\u00D0\u00BD\u00D1\u008B\u00D0\u00B5 \u00D0\u00BA\u00D1\u0080\u00D0\u00B8\u00D1\u0082\u00D0\u00B5\u00D1\u0080\u00D0\u00B8\u00D0\u00B8 \u00D0\u00BF\u00D0\u00BE\u00D0\u00B8\u00D1\u0081\u00D0\u00BA\u00D0\u00B0
|
||||
|
||||
INVALID_DATA = \u00D0\u00BD\u00D0\u00B5\u00D0\u00B2\u00D0\u00B5\u00D1\u0080\u00D0\u00BD\u00D1\u008B\u00D0\u00B5 \u00D0\u00B4\u00D0\u00B0\u00D0\u00BD\u00D0\u00BD\u00D1\u008B\u00D0\u00B5
|
||||
|
||||
INVALID_HEIGHT = \u00D0\u00BD\u00D0\u00B5\u00D0\u00B4\u00D0\u00BE\u00D0\u00BF\u00D1\u0083\u00D1\u0081\u00D1\u0082\u00D0\u00B8\u00D0\u00BC\u00D0\u00B0\u00D1\u008F \u00D0\u00B2\u00D1\u008B\u00D1\u0081\u00D0\u00BE\u00D1\u0082\u00D0\u00B0 \u00D0\u00B1\u00D0\u00BB\u00D0\u00BE\u00D0\u00BA\u00D0\u00B0
|
||||
|
||||
INVALID_NETWORK_ADDRESS = \u00D0\u00BD\u00D0\u00B5\u00D0\u00B2\u00D0\u00B5\u00D1\u0080\u00D0\u00BD\u00D1\u008B\u00D0\u00B9 \u00D1\u0081\u00D0\u00B5\u00D1\u0082\u00D0\u00B5\u00D0\u00B2\u00D0\u00BE\u00D0\u00B9 \u00D0\u00B0\u00D0\u00B4\u00D1\u0080\u00D0\u00B5\u00D1\u0081
|
||||
|
||||
INVALID_ORDER_ID = \u00D0\u00BD\u00D0\u00B5\u00D0\u00B2\u00D0\u00B5\u00D1\u0080\u00D0\u00BD\u00D1\u008B\u00D0\u00B9 \u00D0\u00B8\u00D0\u00B4\u00D0\u00B5\u00D0\u00BD\u00D1\u0082\u00D0\u00B8\u00D1\u0084\u00D0\u00B8\u00D0\u00BA\u00D0\u00B0\u00D1\u0082\u00D0\u00BE\u00D1\u0080 \u00D0\u00B7\u00D0\u00B0\u00D0\u00BA\u00D0\u00B0\u00D0\u00B7\u00D0\u00B0 \u00D0\u00B0\u00D0\u00BA\u00D1\u0082\u00D0\u00B8\u00D0\u00B2\u00D0\u00B0
|
||||
|
||||
INVALID_PRIVATE_KEY = \u00D0\u00BD\u00D0\u00B5\u00D0\u00B2\u00D0\u00B5\u00D1\u0080\u00D0\u00BD\u00D1\u008B\u00D0\u00B9 \u00D0\u00BF\u00D1\u0080\u00D0\u00B8\u00D0\u00B2\u00D0\u00B0\u00D1\u0082\u00D0\u00BD\u00D1\u008B\u00D0\u00B9 \u00D0\u00BA\u00D0\u00BB\u00D1\u008E\u00D1\u0087
|
||||
|
||||
INVALID_PUBLIC_KEY = \u00D0\u00BD\u00D0\u00B5\u00D0\u00B4\u00D0\u00B5\u00D0\u00B9\u00D1\u0081\u00D1\u0082\u00D0\u00B2\u00D0\u00B8\u00D1\u0082\u00D0\u00B5\u00D0\u00BB\u00D1\u008C\u00D0\u00BD\u00D1\u008B\u00D0\u00B9 \u00D0\u00BE\u00D1\u0082\u00D0\u00BA\u00D1\u0080\u00D1\u008B\u00D1\u0082\u00D1\u008B\u00D0\u00B9 \u00D0\u00BA\u00D0\u00BB\u00D1\u008E\u00D1\u0087
|
||||
|
||||
INVALID_REFERENCE = \u00D0\u00BD\u00D0\u00B5\u00D0\u00B2\u00D0\u00B5\u00D1\u0080\u00D0\u00BD\u00D0\u00B0\u00D1\u008F \u00D1\u0081\u00D1\u0081\u00D1\u008B\u00D0\u00BB\u00D0\u00BA\u00D0\u00B0
|
||||
|
||||
# Validation
|
||||
INVALID_SIGNATURE = \u00D0\u00BD\u00D0\u00B5\u00D0\u00B4\u00D0\u00B5\u00D0\u00B9\u00D1\u0081\u00D1\u0082\u00D0\u00B2\u00D0\u00B8\u00D1\u0082\u00D0\u00B5\u00D0\u00BB\u00D1\u008C\u00D0\u00BD\u00D0\u00B0\u00D1\u008F \u00D0\u00BF\u00D0\u00BE\u00D0\u00B4\u00D0\u00BF\u00D0\u00B8\u00D1\u0081\u00D1\u008C
|
||||
|
||||
JSON = \u00D0\u00BD\u00D0\u00B5 \u00D1\u0083\u00D0\u00B4\u00D0\u00B0\u00D0\u00BB\u00D0\u00BE\u00D1\u0081\u00D1\u008C \u00D1\u0080\u00D0\u00B0\u00D0\u00B7\u00D0\u00BE\u00D0\u00B1\u00D1\u0080\u00D0\u00B0\u00D1\u0082\u00D1\u008C \u00D1\u0081\u00D0\u00BE\u00D0\u00BE\u00D0\u00B1\u00D1\u0089\u00D0\u00B5\u00D0\u00BD\u00D0\u00B8\u00D0\u00B5 json
|
||||
|
||||
NAME_UNKNOWN = \u00D0\u00B8\u00D0\u00BC\u00D1\u008F \u00D0\u00BD\u00D0\u00B5\u00D0\u00B8\u00D0\u00B7\u00D0\u00B2\u00D0\u00B5\u00D1\u0081\u00D1\u0082\u00D0\u00BD\u00D0\u00BE
|
||||
|
||||
ORDER_UNKNOWN = \u00D0\u00BD\u00D0\u00B5\u00D0\u00B8\u00D0\u00B7\u00D0\u00B2\u00D0\u00B5\u00D1\u0081\u00D1\u0082\u00D0\u00BD\u00D1\u008B\u00D0\u00B9 \u00D0\u00B8\u00D0\u00B4\u00D0\u00B5\u00D0\u00BD\u00D1\u0082\u00D0\u00B8\u00D1\u0084\u00D0\u00B8\u00D0\u00BA\u00D0\u00B0\u00D1\u0082\u00D0\u00BE\u00D1\u0080 \u00D0\u00B7\u00D0\u00B0\u00D0\u00BA\u00D0\u00B0\u00D0\u00B7\u00D0\u00B0 \u00D0\u00B0\u00D0\u00BA\u00D1\u0082\u00D0\u00B8\u00D0\u00B2\u00D0\u00B0
|
||||
|
||||
PUBLIC_KEY_NOT_FOUND = \u00D0\u00BE\u00D1\u0082\u00D0\u00BA\u00D1\u0080\u00D1\u008B\u00D1\u0082\u00D1\u008B\u00D0\u00B9 \u00D0\u00BA\u00D0\u00BB\u00D1\u008E\u00D1\u0087 \u00D0\u00BD\u00D0\u00B5 \u00D0\u00BD\u00D0\u00B0\u00D0\u00B9\u00D0\u00B4\u00D0\u00B5\u00D0\u00BD
|
||||
|
||||
REPOSITORY_ISSUE = \u00D0\u00BE\u00D1\u0088\u00D0\u00B8\u00D0\u00B1\u00D0\u00BA\u00D0\u00B0 \u00D1\u0080\u00D0\u00B5\u00D0\u00BF\u00D0\u00BE\u00D0\u00B7\u00D0\u00B8\u00D1\u0082\u00D0\u00BE\u00D1\u0080\u00D0\u00B8\u00D1\u008F
|
||||
|
||||
TRANSACTION_INVALID = \u00D1\u0082\u00D1\u0080\u00D0\u00B0\u00D0\u00BD\u00D0\u00B7\u00D0\u00B0\u00D0\u00BA\u00D1\u0086\u00D0\u00B8\u00D1\u008F \u00D0\u00BD\u00D0\u00B5\u00D0\u00B4\u00D0\u00B5\u00D0\u00B9\u00D1\u0081\u00D1\u0082\u00D0\u00B2\u00D0\u00B8\u00D1\u0082\u00D0\u00B5\u00D0\u00BB\u00D1\u008C\u00D0\u00BD\u00D0\u00B0: %s (%s)
|
||||
|
||||
TRANSACTION_UNKNOWN = \u00D1\u0082\u00D1\u0080\u00D0\u00B0\u00D0\u00BD\u00D0\u00B7\u00D0\u00B0\u00D0\u00BA\u00D1\u0086\u00D0\u00B8\u00D1\u008F \u00D0\u00BD\u00D0\u00B5\u00D0\u00B8\u00D0\u00B7\u00D0\u00B2\u00D0\u00B5\u00D1\u0081\u00D1\u0082\u00D0\u00BD\u00D0\u00B0
|
||||
|
||||
TRANSFORMATION_ERROR = \u00D0\u00BD\u00D0\u00B5 \u00D1\u0083\u00D0\u00B4\u00D0\u00B0\u00D0\u00BB\u00D0\u00BE\u00D1\u0081\u00D1\u008C \u00D0\u00BF\u00D1\u0080\u00D0\u00B5\u00D0\u00BE\u00D0\u00B1\u00D1\u0080\u00D0\u00B0\u00D0\u00B7\u00D0\u00BE\u00D0\u00B2\u00D0\u00B0\u00D1\u0082\u00D1\u008C JSON \u00D0\u00B2 \u00D1\u0082\u00D1\u0080\u00D0\u00B0\u00D0\u00BD\u00D0\u00B7\u00D0\u00B0\u00D0\u00BA\u00D1\u0086\u00D0\u00B8\u00D1\u008E
|
||||
|
||||
UNAUTHORIZED = \u00D0\u00B2\u00D1\u008B\u00D0\u00B7\u00D0\u00BE\u00D0\u00B2 API \u00D0\u00BD\u00D0\u00B5 \u00D0\u00B0\u00D0\u00B2\u00D1\u0082\u00D0\u00BE\u00D1\u0080\u00D0\u00B8\u00D0\u00B7\u00D0\u00BE\u00D0\u00B2\u00D0\u00B0\u00D0\u00BD
|
41
src/main/resources/i18n/SysTray_ru.properties
Normal file
41
src/main/resources/i18n/SysTray_ru.properties
Normal file
@@ -0,0 +1,41 @@
|
||||
#Generated by ResourceBundle Editor (http://essiembre.github.io/eclipse-rbe/)
|
||||
# SysTray pop-up menu
|
||||
|
||||
APPLYING_UPDATE_AND_RESTARTING = Применение автоматического обновления и перезапуска...
|
||||
|
||||
AUTO_UPDATE = Автоматическое обновление
|
||||
|
||||
BLOCK_HEIGHT = Высота блока
|
||||
|
||||
CHECK_TIME_ACCURACY = Проверка точного времени
|
||||
|
||||
CONNECTING = Подключение
|
||||
|
||||
CONNECTION = Соединение
|
||||
|
||||
CONNECTIONS = Соединений
|
||||
|
||||
CREATING_BACKUP_OF_DB_FILES = Создание резервной копии файлов базы данных...
|
||||
|
||||
DB_BACKUP = Резервное копирование базы данных
|
||||
|
||||
EXIT = Выход
|
||||
|
||||
MINTING_DISABLED = Чеканка отключена
|
||||
|
||||
MINTING_ENABLED = Чеканка активна
|
||||
|
||||
# Nagging about lack of NTP time sync
|
||||
NTP_NAG_CAPTION = Часы компьютера неточны!
|
||||
|
||||
NTP_NAG_TEXT_UNIX = Установите службу NTP, чтобы получить точное время
|
||||
|
||||
NTP_NAG_TEXT_WINDOWS = Выберите "Синхронизация времени" из меню, чтобы исправить
|
||||
|
||||
OPEN_UI = Открыть пользовательский интерфейс
|
||||
|
||||
SYNCHRONIZE_CLOCK = Синхронизировать время
|
||||
|
||||
SYNCHRONIZING_BLOCKCHAIN = Синхронизация цепи
|
||||
|
||||
SYNCHRONIZING_CLOCK = Проверка времени
|
@@ -11,166 +11,174 @@ ALREADY_VOTED_FOR_THAT_OPTION = already voted for that option
|
||||
|
||||
ASSET_ALREADY_EXISTS = asset already exists
|
||||
|
||||
ASSET_DOES_NOT_EXIST = ASSET_DOES_NOT_EXIST
|
||||
ASSET_DOES_NOT_EXIST = asset does not exist
|
||||
|
||||
ASSET_DOES_NOT_MATCH_AT = ASSET_DOES_NOT_MATCH_AT
|
||||
ASSET_DOES_NOT_MATCH_AT = asset does not match AT's asset
|
||||
|
||||
ASSET_NOT_SPENDABLE = ASSET_NOT_SPENDABLE
|
||||
ASSET_NOT_SPENDABLE = asset is not spendable
|
||||
|
||||
AT_ALREADY_EXISTS = AT_ALREADY_EXISTS
|
||||
AT_ALREADY_EXISTS = AT already exists
|
||||
|
||||
AT_IS_FINISHED = AT_IS_FINISHED
|
||||
AT_IS_FINISHED = AT has finished
|
||||
|
||||
AT_UNKNOWN = AT_UNKNOWN
|
||||
AT_UNKNOWN = AT unknown
|
||||
|
||||
BANNED_FROM_GROUP = BANNED_FROM_GROUP
|
||||
BANNED_FROM_GROUP = banned from group
|
||||
|
||||
BAN_EXISTS = BAN_EXISTS
|
||||
BAN_EXISTS = ban already exists
|
||||
|
||||
BAN_UNKNOWN = BAN_UNKNOWN
|
||||
BAN_UNKNOWN = ban unknown
|
||||
|
||||
BUYER_ALREADY_OWNER = BUYER_ALREADY_OWNER
|
||||
BUYER_ALREADY_OWNER = buyer is already owner
|
||||
|
||||
CLOCK_NOT_SYNCED = CLOCK_NOT_SYNCED
|
||||
CHAT = CHAT transactions are never valid for inclusion into blocks
|
||||
|
||||
DUPLICATE_OPTION = DUPLICATE_OPTION
|
||||
CLOCK_NOT_SYNCED = clock not synchronized
|
||||
|
||||
GROUP_ALREADY_EXISTS = GROUP_ALREADY_EXISTS
|
||||
DUPLICATE_OPTION = duplicate option
|
||||
|
||||
GROUP_APPROVAL_DECIDED = GROUP_APPROVAL_DECIDED
|
||||
GROUP_ALREADY_EXISTS = group already exists
|
||||
|
||||
GROUP_APPROVAL_NOT_REQUIRED = GROUP_APPROVAL_NOT_REQUIRED
|
||||
GROUP_APPROVAL_DECIDED = group-approval already decided
|
||||
|
||||
GROUP_DOES_NOT_EXIST = GROUP_DOES_NOT_EXIST
|
||||
GROUP_APPROVAL_NOT_REQUIRED = group-approval not required
|
||||
|
||||
GROUP_ID_MISMATCH = GROUP_ID_MISMATCH
|
||||
GROUP_DOES_NOT_EXIST = group does not exist
|
||||
|
||||
GROUP_OWNER_CANNOT_LEAVE = GROUP_OWNER_CANNOT_LEAVE
|
||||
GROUP_ID_MISMATCH = group ID mismatch
|
||||
|
||||
HAVE_EQUALS_WANT = HAVE_EQUALS_WANT
|
||||
GROUP_OWNER_CANNOT_LEAVE = group owner cannot leave group
|
||||
|
||||
INSUFFICIENT_FEE = INSUFFICIENT_FEE
|
||||
HAVE_EQUALS_WANT = have-asset is the same as want-asset
|
||||
|
||||
INVALID_ADDRESS = INVALID_ADDRESS
|
||||
INCORRECT_NONCE = incorrect PoW nonce
|
||||
|
||||
INVALID_AMOUNT = INVALID_AMOUNT
|
||||
INSUFFICIENT_FEE = insufficient fee
|
||||
|
||||
INVALID_ASSET_OWNER = INVALID_ASSET_OWNER
|
||||
INVALID_ADDRESS = invalid address
|
||||
|
||||
INVALID_AT_TRANSACTION = INVALID_AT_TRANSACTION
|
||||
INVALID_AMOUNT = invalid amount
|
||||
|
||||
INVALID_AT_TYPE_LENGTH = INVALID_AT_TYPE_LENGTH
|
||||
INVALID_ASSET_OWNER = invalid asset owner
|
||||
|
||||
INVALID_CREATION_BYTES = INVALID_CREATION_BYTES
|
||||
INVALID_AT_TRANSACTION = invalid AT transaction
|
||||
|
||||
INVALID_DATA_LENGTH = INVALID_DATA_LENGTH
|
||||
INVALID_AT_TYPE_LENGTH = invalid AT 'type' length
|
||||
|
||||
INVALID_DESCRIPTION_LENGTH = INVALID_DESCRIPTION_LENGTH
|
||||
INVALID_CREATION_BYTES = invalid creation bytes
|
||||
|
||||
INVALID_GROUP_APPROVAL_THRESHOLD = INVALID_GROUP_APPROVAL_THRESHOLD
|
||||
INVALID_DATA_LENGTH = invalid data length
|
||||
|
||||
INVALID_GROUP_ID = INVALID_GROUP_ID
|
||||
INVALID_DESCRIPTION_LENGTH = invalid description length
|
||||
|
||||
INVALID_GROUP_OWNER = INVALID_GROUP_OWNER
|
||||
INVALID_GROUP_APPROVAL_THRESHOLD = invalid group-approval threshold
|
||||
|
||||
INVALID_LIFETIME = INVALID_LIFETIME
|
||||
INVALID_GROUP_BLOCK_DELAY = invalid group-approval block delay
|
||||
|
||||
INVALID_NAME_LENGTH = INVALID_NAME_LENGTH
|
||||
INVALID_GROUP_ID = invalid group ID
|
||||
|
||||
INVALID_NAME_OWNER = INVALID_NAME_OWNER
|
||||
INVALID_GROUP_OWNER = invalid group owner
|
||||
|
||||
INVALID_OPTIONS_COUNT = INVALID_OPTIONS_COUNT
|
||||
INVALID_LIFETIME = invalid lifetime
|
||||
|
||||
INVALID_OPTION_LENGTH = INVALID_OPTION_LENGTH
|
||||
INVALID_NAME_LENGTH = invalid name length
|
||||
|
||||
INVALID_ORDER_CREATOR = INVALID_ORDER_CREATOR
|
||||
INVALID_NAME_OWNER = invalid name owner
|
||||
|
||||
INVALID_PAYMENTS_COUNT = INVALID_PAYMENTS_COUNT
|
||||
INVALID_OPTIONS_COUNT = invalid options count
|
||||
|
||||
INVALID_PUBLIC_KEY = INVALID_PUBLIC_KEY
|
||||
INVALID_OPTION_LENGTH = invalid options length
|
||||
|
||||
INVALID_QUANTITY = INVALID_QUANTITY
|
||||
INVALID_ORDER_CREATOR = invalid order creator
|
||||
|
||||
INVALID_REFERENCE = INVALID_REFERENCE
|
||||
INVALID_PAYMENTS_COUNT = invalid payments count
|
||||
|
||||
INVALID_RETURN = INVALID_RETURN
|
||||
INVALID_PUBLIC_KEY = invalid public key
|
||||
|
||||
INVALID_REWARD_SHARE_PERCENT = INVALID_REWARD_SHARE_PERCENT
|
||||
INVALID_QUANTITY = invalid quantity
|
||||
|
||||
INVALID_SELLER = INVALID_SELLER
|
||||
INVALID_REFERENCE = invalid reference
|
||||
|
||||
INVALID_TAGS_LENGTH = INVALID_TAGS_LENGTH
|
||||
INVALID_RETURN = invalid return
|
||||
|
||||
INVALID_TX_GROUP_ID = INVALID_TX_GROUP_ID
|
||||
INVALID_REWARD_SHARE_PERCENT = invalid reward-share percent
|
||||
|
||||
INVALID_VALUE_LENGTH = INVALID_VALUE_LENGTH
|
||||
INVALID_SELLER = invalid seller
|
||||
|
||||
INVITE_UNKNOWN = INVITE_UNKNOWN
|
||||
INVALID_TAGS_LENGTH = invalid 'tags' length
|
||||
|
||||
JOIN_REQUEST_EXISTS = JOIN_REQUEST_EXISTS
|
||||
INVALID_TX_GROUP_ID = invalid transaction group ID
|
||||
|
||||
MAXIMUM_REWARD_SHARES = MAXIMUM_REWARD_SHARES
|
||||
INVALID_VALUE_LENGTH = invalid 'value' length
|
||||
|
||||
MISSING_CREATOR = MISSING_CREATOR
|
||||
INVITE_UNKNOWN = group invite unknown
|
||||
|
||||
MULTIPLE_NAMES_FORBIDDEN = MULTIPLE_NAMES_FORBIDDEN
|
||||
JOIN_REQUEST_EXISTS = group join request already exists
|
||||
|
||||
NAME_ALREADY_FOR_SALE = NAME_ALREADY_FOR_SALE
|
||||
MAXIMUM_REWARD_SHARES = already at maximum number of reward-shares for this account
|
||||
|
||||
NAME_ALREADY_REGISTERED = NAME_ALREADY_REGISTERED
|
||||
MISSING_CREATOR = missing creator
|
||||
|
||||
NAME_DOES_NOT_EXIST = NAME_DOES_NOT_EXIST
|
||||
MULTIPLE_NAMES_FORBIDDEN = multiple registered names per account is forbidden
|
||||
|
||||
NAME_NOT_FOR_SALE = NAME_NOT_FOR_SALE
|
||||
NAME_ALREADY_FOR_SALE = name already for sale
|
||||
|
||||
NAME_NOT_LOWER_CASE = NAME_NOT_LOWER_CASE
|
||||
NAME_ALREADY_REGISTERED = name already registered
|
||||
|
||||
NEGATIVE_AMOUNT = NEGATIVE_AMOUNT
|
||||
NAME_DOES_NOT_EXIST = name does not exist
|
||||
|
||||
NEGATIVE_FEE = NEGATIVE_FEE
|
||||
NAME_NOT_FOR_SALE = name is not for sale
|
||||
|
||||
NEGATIVE_PRICE = NEGATIVE_PRICE
|
||||
NAME_NOT_NORMALIZED = name not in Unicode 'normalized' form
|
||||
|
||||
NOT_GROUP_ADMIN = NOT_GROUP_ADMIN
|
||||
NEGATIVE_AMOUNT = invalid/negative amount
|
||||
|
||||
NOT_GROUP_MEMBER = NOT_GROUP_MEMBER
|
||||
NEGATIVE_FEE = invalid/negative fee
|
||||
|
||||
NOT_MINTING_ACCOUNT = NOT_MINTING_ACCOUNT
|
||||
NEGATIVE_PRICE = invalid/negative price
|
||||
|
||||
NOT_YET_RELEASED = NOT_YET_RELEASED
|
||||
NOT_GROUP_ADMIN = account is not a group admin
|
||||
|
||||
NO_BALANCE = NO_BALANCE
|
||||
NOT_GROUP_MEMBER = account is not a group member
|
||||
|
||||
NOT_MINTING_ACCOUNT = account cannot mint
|
||||
|
||||
NOT_YET_RELEASED = feature not yet released
|
||||
|
||||
NO_BALANCE = insufficient balance
|
||||
|
||||
NO_BLOCKCHAIN_LOCK = node's blockchain currently busy
|
||||
|
||||
NO_FLAG_PERMISSION = NO_FLAG_PERMISSION
|
||||
NO_FLAG_PERMISSION = account does not have that permission
|
||||
|
||||
OK = OK
|
||||
|
||||
ORDER_ALREADY_CLOSED = ORDER_ALREADY_CLOSED
|
||||
ORDER_ALREADY_CLOSED = asset trade order is already closed
|
||||
|
||||
ORDER_DOES_NOT_EXIST = ORDER_DOES_NOT_EXIST
|
||||
ORDER_DOES_NOT_EXIST = asset trade order does not exist
|
||||
|
||||
POLL_ALREADY_EXISTS = POLL_ALREADY_EXISTS
|
||||
POLL_ALREADY_EXISTS = poll already exists
|
||||
|
||||
POLL_DOES_NOT_EXIST = POLL_DOES_NOT_EXIST
|
||||
POLL_DOES_NOT_EXIST = poll does not exist
|
||||
|
||||
POLL_OPTION_DOES_NOT_EXIST = POLL_OPTION_DOES_NOT_EXIST
|
||||
POLL_OPTION_DOES_NOT_EXIST = poll option does not exist
|
||||
|
||||
PUBLIC_KEY_UNKNOWN = PUBLIC_KEY_UNKNOWN
|
||||
PUBLIC_KEY_UNKNOWN = public key unknown
|
||||
|
||||
SELF_SHARE_EXISTS = SELF_SHARE_EXISTS
|
||||
REWARD_SHARE_UNKNOWN = reward-share unknown
|
||||
|
||||
TIMESTAMP_TOO_NEW = TIMESTAMP_TOO_NEW
|
||||
SELF_SHARE_EXISTS = self-share (reward-share) already exists
|
||||
|
||||
TIMESTAMP_TOO_OLD = TIMESTAMP_TOO_OLD
|
||||
TIMESTAMP_TOO_NEW = timestamp too new
|
||||
|
||||
TOO_MANY_UNCONFIRMED = TOO_MANY_UNCONFIRMED
|
||||
TIMESTAMP_TOO_OLD = timestamp too old
|
||||
|
||||
TRANSACTION_ALREADY_CONFIRMED = TRANSACTION_ALREADY_CONFIRMED
|
||||
TOO_MANY_UNCONFIRMED = account has too many unconfirmed transactions pending
|
||||
|
||||
TRANSACTION_ALREADY_EXISTS = TRANSACTION_ALREADY_EXISTS
|
||||
TRANSACTION_ALREADY_CONFIRMED = transaction has already confirmed
|
||||
|
||||
TRANSACTION_UNKNOWN = TRANSACTION_UNKNOWN
|
||||
TRANSACTION_ALREADY_EXISTS = transaction already exists
|
||||
|
||||
TX_GROUP_ID_MISMATCH = TX_GROUP_ID_MISMATCH
|
||||
TRANSACTION_UNKNOWN = transaction unknown
|
||||
|
||||
TX_GROUP_ID_MISMATCH = transaction's group ID does not match
|
||||
|
164
src/main/resources/i18n/TransactionValidity_ru.properties
Normal file
164
src/main/resources/i18n/TransactionValidity_ru.properties
Normal file
@@ -0,0 +1,164 @@
|
||||
|
||||
ACCOUNT_ALREADY_EXISTS = \u00D0\u00B0\u00D0\u00BA\u00D0\u00BA\u00D0\u00B0\u00D1\u0083\u00D0\u00BD\u00D1\u0082 \u00D1\u0083\u00D0\u00B6\u00D0\u00B5 \u00D1\u0081\u00D1\u0083\u00D1\u0089\u00D0\u00B5\u00D1\u0081\u00D1\u0082\u00D0\u00B2\u00D1\u0083\u00D0\u00B5\u00D1\u0082
|
||||
|
||||
ACCOUNT_CANNOT_REWARD_SHARE = \u00D0\u00B0\u00D0\u00BA\u00D0\u00BA\u00D0\u00B0\u00D1\u0083\u00D0\u00BD\u00D1\u0082 \u00D0\u00BD\u00D0\u00B5 \u00D0\u00BC\u00D0\u00BE\u00D0\u00B6\u00D0\u00B5\u00D1\u0082 \u00D0\u00B4\u00D0\u00B5\u00D0\u00BB\u00D0\u00B8\u00D1\u0082\u00D1\u008C\u00D1\u0081\u00D1\u008F \u00D0\u00B2\u00D0\u00BE\u00D0\u00B7\u00D0\u00BD\u00D0\u00B0\u00D0\u00B3\u00D1\u0080\u00D0\u00B0\u00D0\u00B6\u00D0\u00B4\u00D0\u00B5\u00D0\u00BD\u00D0\u00B8\u00D0\u00B5\u00D0\u00BC
|
||||
|
||||
ALREADY_GROUP_ADMIN = \u00D1\u0083\u00D0\u00B6\u00D0\u00B5 \u00D0\u00B0\u00D0\u00B4\u00D0\u00BC\u00D0\u00B8\u00D0\u00BD\u00D0\u00B8\u00D1\u0081\u00D1\u0082\u00D1\u0080\u00D0\u00B0\u00D1\u0082\u00D0\u00BE\u00D1\u0080 \u00D0\u00B3\u00D1\u0080\u00D1\u0083\u00D0\u00BF\u00D0\u00BF\u00D1\u008B
|
||||
|
||||
ALREADY_GROUP_MEMBER = \u00D1\u0083\u00D0\u00B6\u00D0\u00B5 \u00D1\u0087\u00D0\u00BB\u00D0\u00B5\u00D0\u00BD \u00D0\u00B3\u00D1\u0080\u00D1\u0083\u00D0\u00BF\u00D0\u00BF\u00D1\u008B
|
||||
|
||||
ALREADY_VOTED_FOR_THAT_OPTION = \u00D1\u0083\u00D0\u00B6\u00D0\u00B5 \u00D0\u00BF\u00D1\u0080\u00D0\u00BE\u00D0\u00B3\u00D0\u00BE\u00D0\u00BB\u00D0\u00BE\u00D1\u0081\u00D0\u00BE\u00D0\u00B2\u00D0\u00B0\u00D0\u00BB\u00D0\u00B8 \u00D0\u00B7\u00D0\u00B0 \u00D1\u008D\u00D1\u0082\u00D0\u00BE\u00D1\u0082 \u00D0\u00B2\u00D0\u00B0\u00D1\u0080\u00D0\u00B8\u00D0\u00B0\u00D0\u00BD\u00D1\u0082
|
||||
|
||||
ASSET_ALREADY_EXISTS = \u00D0\u00B0\u00D0\u00BA\u00D1\u0082\u00D0\u00B8\u00D0\u00B2 \u00D1\u0083\u00D0\u00B6\u00D0\u00B5 \u00D1\u0081\u00D1\u0083\u00D1\u0089\u00D0\u00B5\u00D1\u0081\u00D1\u0082\u00D0\u00B2\u00D1\u0083\u00D0\u00B5\u00D1\u0082
|
||||
|
||||
ASSET_DOES_NOT_EXIST = \u00D0\u0090\u00D0\u00BA\u00D1\u0082\u00D0\u00B8\u00D0\u00B2 \u00D0\u00BD\u00D0\u00B5 \u00D1\u0081\u00D1\u0083\u00D1\u0089\u00D0\u00B5\u00D1\u0081\u00D1\u0082\u00D0\u00B2\u00D1\u0083\u00D0\u00B5\u00D1\u0082
|
||||
|
||||
ASSET_DOES_NOT_MATCH_AT = \u00D0\u00B0\u00D0\u00BA\u00D1\u0082\u00D0\u00B8\u00D0\u00B2 \u00D0\u00BD\u00D0\u00B5 \u00D1\u0081\u00D0\u00BE\u00D0\u00B2\u00D0\u00BF\u00D0\u00B0\u00D0\u00B4\u00D0\u00B0\u00D0\u00B5\u00D1\u0082 \u00D1\u0081 \u00D0\u0090\u00D0\u00A2
|
||||
|
||||
AT_ALREADY_EXISTS = AT \u00D1\u0083\u00D0\u00B6\u00D0\u00B5 \u00D1\u0081\u00D1\u0083\u00D1\u0089\u00D0\u00B5\u00D1\u0081\u00D1\u0082\u00D0\u00B2\u00D1\u0083\u00D0\u00B5\u00D1\u0082
|
||||
|
||||
AT_IS_FINISHED = AT \u00D0\u00B2 \u00D0\u00B7\u00D0\u00B0\u00D0\u00B2\u00D0\u00B5\u00D1\u0080\u00D1\u0088\u00D0\u00B5\u00D0\u00BD\u00D0\u00B8\u00D0\u00B8
|
||||
|
||||
AT_UNKNOWN = \u00D0\u00BD\u00D0\u00B5 \u00D0\u00B8\u00D0\u00B7\u00D0\u00B2\u00D0\u00B5\u00D1\u0081\u00D1\u0082\u00D0\u00BD\u00D1\u008B\u00D0\u00B9 \u00D0\u0090\u00D0\u00A2
|
||||
|
||||
BANNED_FROM_GROUP = \u00D0\u00B8\u00D1\u0081\u00D0\u00BA\u00D0\u00BB\u00D1\u008E\u00D1\u0087\u00D0\u00B5\u00D0\u00BD \u00D0\u00B8\u00D0\u00B7 \u00D0\u00B3\u00D1\u0080\u00D1\u0083\u00D0\u00BF\u00D0\u00BF\u00D1\u008B
|
||||
|
||||
BAN_EXISTS = \u00D0\u0091\u00D0\u00B0\u00D0\u00BD
|
||||
|
||||
BAN_UNKNOWN = \u00D0\u00BD\u00D0\u00B5 \u00D0\u00B8\u00D0\u00B7\u00D0\u00B2\u00D0\u00B5\u00D1\u0081\u00D1\u0082\u00D0\u00BD\u00D1\u008B\u00D0\u00B9 \u00D0\u00B1\u00D0\u00B0\u00D0\u00BD
|
||||
|
||||
BUYER_ALREADY_OWNER = \u00D0\u00BF\u00D0\u00BE\u00D0\u00BA\u00D1\u0083\u00D0\u00BF\u00D0\u00B0\u00D1\u0082\u00D0\u00B5\u00D0\u00BB\u00D1\u008C \u00D1\u0083\u00D0\u00B6\u00D0\u00B5 \u00D1\u0081\u00D0\u00BE\u00D0\u00B1\u00D1\u0081\u00D1\u0082\u00D0\u00B2\u00D0\u00B5\u00D0\u00BD\u00D0\u00BD\u00D0\u00B8\u00D0\u00BA
|
||||
|
||||
DUPLICATE_OPTION = \u00D0\u00B4\u00D1\u0083\u00D0\u00B1\u00D0\u00BB\u00D0\u00B8\u00D1\u0080\u00D0\u00BE\u00D0\u00B2\u00D0\u00B0\u00D1\u0082\u00D1\u008C \u00D0\u00B2\u00D0\u00B0\u00D1\u0080\u00D0\u00B8\u00D0\u00B0\u00D0\u00BD\u00D1\u0082
|
||||
|
||||
GROUP_ALREADY_EXISTS = \u00D0\u00B3\u00D1\u0080\u00D1\u0083\u00D0\u00BF\u00D0\u00BF\u00D0\u00B0 \u00D1\u0083\u00D0\u00B6\u00D0\u00B5 \u00D1\u0081\u00D1\u0083\u00D1\u0089\u00D0\u00B5\u00D1\u0081\u00D1\u0082\u00D0\u00B2\u00D1\u0083\u00D0\u00B5\u00D1\u0082
|
||||
|
||||
GROUP_APPROVAL_DECIDED = \u00D0\u00B3\u00D1\u0083\u00D0\u00BF\u00D0\u00BF\u00D0\u00B0 \u00D0\u00BE\u00D0\u00B4\u00D0\u00BE\u00D0\u00B1\u00D1\u0080\u00D0\u00B5\u00D0\u00BD\u00D0\u00B0
|
||||
|
||||
GROUP_APPROVAL_NOT_REQUIRED = \u00D0\u00B3\u00D1\u0083\u00D0\u00BF\u00D0\u00BF\u00D0\u00BE\u00D0\u00B2\u00D0\u00BE\u00D0\u00B5 \u00D0\u00BE\u00D0\u00B4\u00D0\u00BE\u00D0\u00B1\u00D1\u0080\u00D0\u00B5\u00D0\u00BD\u00D0\u00B8\u00D0\u00B5 \u00D0\u00BD\u00D0\u00B5 \u00D1\u0082\u00D1\u0080\u00D0\u00B5\u00D0\u00B1\u00D1\u0083\u00D0\u00B5\u00D1\u0082\u00D1\u0081\u00D1\u008F
|
||||
|
||||
GROUP_DOES_NOT_EXIST = \u00D0\u00B3\u00D1\u0080\u00D1\u0083\u00D0\u00BF\u00D0\u00BF\u00D0\u00B0 \u00D0\u00BD\u00D0\u00B5 \u00D1\u0081\u00D1\u0083\u00D1\u0089\u00D0\u00B5\u00D1\u0081\u00D1\u0082\u00D0\u00B2\u00D1\u0083\u00D0\u00B5\u00D1\u0082
|
||||
|
||||
GROUP_ID_MISMATCH = \u00D0\u00BD\u00D0\u00B5 \u00D1\u0081\u00D0\u00BE\u00D0\u00BE\u00D1\u0082\u00D0\u00B2\u00D0\u00B5\u00D1\u0082\u00D1\u0081\u00D1\u0082\u00D0\u00B2\u00D0\u00B8\u00D0\u00B5 \u00D0\u00B8\u00D0\u00B4\u00D0\u00B5\u00D0\u00BD\u00D1\u0082\u00D0\u00B8\u00D1\u0084\u00D0\u00B8\u00D0\u00BA\u00D0\u00B0\u00D1\u0082\u00D0\u00BE\u00D1\u0080\u00D0\u00B0 \u00D0\u00B3\u00D1\u0080\u00D1\u0083\u00D0\u00BF\u00D0\u00BF\u00D1\u008B
|
||||
|
||||
GROUP_OWNER_CANNOT_LEAVE = \u00D0\u00B2\u00D0\u00BB\u00D0\u00B0\u00D0\u00B4\u00D0\u00B5\u00D0\u00BB\u00D0\u00B5\u00D1\u0086 \u00D0\u00B3\u00D1\u0080\u00D1\u0083\u00D0\u00BF\u00D0\u00BF\u00D1\u008B \u00D0\u00BD\u00D0\u00B5 \u00D0\u00BC\u00D0\u00BE\u00D0\u00B6\u00D0\u00B5\u00D1\u0082 \u00D1\u0083\u00D0\u00B9\u00D1\u0082\u00D0\u00B8
|
||||
|
||||
HAVE_EQUALS_WANT = \u00D0\u00B8\u00D0\u00BC\u00D0\u00BC\u00D0\u00B5\u00D1\u008E\u00D1\u0082\u00D1\u0081\u00D1\u008F \u00D1\u0080\u00D0\u00B0\u00D0\u00B2\u00D0\u00BD\u00D1\u008B\u00D0\u00B5 \u00D0\u00B6\u00D0\u00B5\u00D0\u00BB\u00D0\u00B0\u00D0\u00BD\u00D0\u00B8\u00D1\u008F
|
||||
|
||||
INSUFFICIENT_FEE = \u00D0\u00BD\u00D0\u00B5\u00D0\u00B4\u00D0\u00BE\u00D1\u0081\u00D1\u0082\u00D0\u00B0\u00D1\u0082\u00D0\u00BE\u00D1\u0087\u00D0\u00BD\u00D0\u00B0\u00D1\u008F \u00D0\u00BF\u00D0\u00BB\u00D0\u00B0\u00D1\u0082\u00D0\u00B0
|
||||
|
||||
INVALID_ADDRESS = \u00D0\u00BD\u00D0\u00B5\u00D0\u00B4\u00D0\u00B5\u00D0\u00B9\u00D1\u0081\u00D1\u0082\u00D0\u00B2\u00D0\u00B8\u00D1\u0082\u00D0\u00B5\u00D0\u00BB\u00D1\u008C\u00D0\u00BD\u00D1\u008B\u00D0\u00B9 \u00D0\u00B0\u00D0\u00B4\u00D1\u0080\u00D0\u00B5\u00D1\u0081
|
||||
|
||||
INVALID_AMOUNT = \u00D0\u00BD\u00D0\u00B5\u00D0\u00B4\u00D0\u00BE\u00D0\u00BF\u00D1\u0083\u00D1\u0081\u00D1\u0082\u00D0\u00B8\u00D0\u00BC\u00D0\u00B0\u00D1\u008F \u00D1\u0081\u00D1\u0083\u00D0\u00BC\u00D0\u00BC\u00D0\u00B0
|
||||
|
||||
INVALID_ASSET_OWNER = \u00D0\u00BD\u00D0\u00B5\u00D0\u00B4\u00D0\u00B5\u00D0\u00B9\u00D1\u0081\u00D1\u0082\u00D0\u00B2\u00D0\u00B8\u00D1\u0082\u00D0\u00B5\u00D0\u00BB\u00D1\u008C\u00D0\u00BD\u00D1\u008B\u00D0\u00B9 \u00D0\u00B2\u00D0\u00BB\u00D0\u00B0\u00D0\u00B4\u00D0\u00B5\u00D0\u00BB\u00D0\u00B5\u00D1\u0086 \u00D0\u00B0\u00D0\u00BA\u00D1\u0082\u00D0\u00B8\u00D0\u00B2\u00D0\u00B0
|
||||
|
||||
INVALID_AT_TRANSACTION = \u00D0\u00BD\u00D0\u00B5\u00D0\u00B4\u00D0\u00B5\u00D0\u00B9\u00D1\u0081\u00D1\u0082\u00D0\u00B2\u00D0\u00B8\u00D1\u0082\u00D0\u00B5\u00D0\u00BB\u00D1\u008C\u00D0\u00BD\u00D0\u00B0\u00D1\u008F \u00D0\u0090\u00D0\u00A2 \u00D1\u0082\u00D1\u0080\u00D0\u00B0\u00D0\u00BD\u00D0\u00B7\u00D0\u00B0\u00D0\u00BA\u00D1\u0086\u00D0\u00B8\u00D1\u008F
|
||||
|
||||
INVALID_AT_TYPE_LENGTH = \u00D0\u00BD\u00D0\u00B5\u00D0\u00B4\u00D0\u00B5\u00D0\u00B9\u00D1\u0081\u00D1\u0082\u00D0\u00B2\u00D0\u00B8\u00D1\u0082\u00D0\u00B5\u00D0\u00BB\u00D1\u008C\u00D0\u00BD\u00D0\u00BE \u00D0\u00B4\u00D0\u00BB\u00D1\u008F \u00D1\u0082\u00D0\u00B8\u00D0\u00BF\u00D0\u00B0 \u00D0\u00B4\u00D0\u00BB\u00D0\u00B8\u00D0\u00BD\u00D1\u008B AT
|
||||
|
||||
INVALID_CREATION_BYTES = \u00D0\u00BD\u00D0\u00B5\u00D0\u00B4\u00D0\u00BE\u00D0\u00BF\u00D1\u0083\u00D1\u0081\u00D1\u0082\u00D0\u00B8\u00D0\u00BC\u00D1\u008B\u00D0\u00B5 \u00D0\u00B1\u00D0\u00B0\u00D0\u00B9\u00D1\u0082\u00D1\u008B \u00D1\u0081\u00D0\u00BE\u00D0\u00B7\u00D0\u00B4\u00D0\u00B0\u00D0\u00BD\u00D0\u00B8\u00D1\u008F
|
||||
|
||||
INVALID_DESCRIPTION_LENGTH = \u00D0\u00BD\u00D0\u00B5\u00D0\u00B4\u00D0\u00BE\u00D0\u00BF\u00D1\u0083\u00D1\u0081\u00D1\u0082\u00D0\u00B8\u00D0\u00BC\u00D0\u00B0\u00D1\u008F \u00D0\u00B4\u00D0\u00BB\u00D0\u00B8\u00D0\u00BD\u00D0\u00B0 \u00D0\u00BE\u00D0\u00BF\u00D0\u00B8\u00D1\u0081\u00D0\u00B0\u00D0\u00BD\u00D0\u00B8\u00D1\u008F
|
||||
|
||||
INVALID_GROUP_APPROVAL_THRESHOLD = \u00D0\u00BD\u00D0\u00B5\u00D0\u00B4\u00D0\u00BE\u00D0\u00BF\u00D1\u0083\u00D1\u0081\u00D1\u0082\u00D0\u00B8\u00D0\u00BC\u00D1\u008B\u00D0\u00B9 \u00D0\u00BF\u00D0\u00BE\u00D1\u0080\u00D0\u00BE\u00D0\u00B3 \u00D1\u0083\u00D1\u0082\u00D0\u00B2\u00D0\u00B5\u00D1\u0080\u00D0\u00B6\u00D0\u00B4\u00D0\u00B5\u00D0\u00BD\u00D0\u00B8\u00D1\u008F \u00D0\u00B3\u00D1\u0080\u00D1\u0083\u00D0\u00BF\u00D0\u00BF\u00D1\u008B
|
||||
|
||||
INVALID_GROUP_ID = \u00D0\u00BD\u00D0\u00B5\u00D0\u00B4\u00D0\u00BE\u00D0\u00BF\u00D1\u0083\u00D1\u0081\u00D1\u0082\u00D0\u00B8\u00D0\u00BC\u00D1\u008B\u00D0\u00B9 \u00D0\u00B8\u00D0\u00B4\u00D0\u00B5\u00D0\u00BD\u00D1\u0082\u00D0\u00B8\u00D1\u0084\u00D0\u00B8\u00D0\u00BA\u00D0\u00B0\u00D1\u0082\u00D0\u00BE\u00D1\u0080 \u00D0\u00B3\u00D1\u0080\u00D1\u0083\u00D0\u00BF\u00D0\u00BF\u00D1\u008B
|
||||
|
||||
INVALID_GROUP_OWNER = \u00D0\u00BD\u00D0\u00B5\u00D0\u00B4\u00D0\u00BE\u00D0\u00BF\u00D1\u0083 \u00D0\u00B2\u00D0\u00BB\u00D0\u00B0\u00D0\u00B4\u00D0\u00B5\u00D0\u00BB\u00D0\u00B5\u00D1\u0086 \u00D0\u00B3\u00D1\u0080\u00D1\u0083\u00D0\u00BF\u00D0\u00BF\u00D1\u008B
|
||||
|
||||
INVALID_LIFETIME = \u00D0\u00BD\u00D0\u00B5\u00D0\u00B4\u00D0\u00BE\u00D0\u00BF\u00D1\u0083 \u00D1\u0081\u00D1\u0080\u00D0\u00BE\u00D0\u00BA \u00D1\u0081\u00D0\u00BB\u00D1\u0083\u00D0\u00B6\u00D0\u00B1\u00D1\u008B
|
||||
|
||||
INVALID_NAME_LENGTH = \u00D0\u00BD\u00D0\u00B5\u00D0\u00B4\u00D0\u00BE\u00D0\u00BF\u00D1\u0083\u00D1\u0081\u00D1\u0082\u00D0\u00B8\u00D0\u00BC\u00D0\u00B0\u00D1\u008F \u00D0\u00B4\u00D0\u00BB\u00D0\u00B8\u00D0\u00BD\u00D0\u00B0 \u00D0\u00B3\u00D1\u0080\u00D1\u0083\u00D0\u00BF\u00D0\u00BF\u00D1\u008B
|
||||
|
||||
INVALID_NAME_OWNER = \u00D0\u00BD\u00D0\u00B5\u00D0\u00B4\u00D0\u00BE\u00D0\u00BF\u00D1\u0083\u00D1\u0081\u00D1\u0082\u00D0\u00B8\u00D0\u00BC\u00D0\u00BE\u00D0\u00B5 \u00D0\u00B8\u00D0\u00BC\u00D1\u008F \u00D0\u00B2\u00D0\u00BB\u00D0\u00B0\u00D0\u00B4\u00D0\u00B5\u00D0\u00BB\u00D1\u008C\u00D1\u0086\u00D0\u00B0
|
||||
|
||||
INVALID_OPTIONS_COUNT = \u00D0\u00BD\u00D0\u00B5\u00D0\u00B2\u00D0\u00B5\u00D1\u0080\u00D0\u00BD\u00D0\u00BE\u00D0\u00B5 \u00D0\u00BA\u00D0\u00BE\u00D0\u00BB\u00D0\u00B8\u00D1\u0087\u00D0\u00B5\u00D1\u0081\u00D1\u0082\u00D0\u00B2\u00D0\u00BE \u00D0\u00BE\u00D0\u00BF\u00D1\u0086\u00D0\u00B8\u00D0\u00B9
|
||||
|
||||
INVALID_OPTION_LENGTH = \u00D0\u00BD\u00D0\u00B5\u00D0\u00B4\u00D0\u00BE\u00D0\u00BF\u00D1\u0083\u00D1\u0081\u00D1\u0082\u00D0\u00B8\u00D0\u00BC\u00D0\u00B0\u00D1\u008F \u00D0\u00B4\u00D0\u00BB\u00D0\u00B8\u00D0\u00BD\u00D0\u00B0 \u00D0\u00BE\u00D0\u00BF\u00D1\u0086\u00D0\u00B8\u00D0\u00B8
|
||||
|
||||
INVALID_ORDER_CREATOR = \u00D0\u00BD\u00D0\u00B5\u00D0\u00B4\u00D0\u00BE\u00D0\u00BF\u00D1\u0083\u00D1\u0081\u00D1\u0082\u00D0\u00B8\u00D0\u00BC\u00D1\u008B\u00D0\u00B9 \u00D1\u0081\u00D0\u00BE\u00D0\u00B7\u00D0\u00B4\u00D0\u00B0\u00D1\u0082\u00D0\u00B5\u00D0\u00BB\u00D1\u008C \u00D0\u00B7\u00D0\u00B0\u00D0\u00BA\u00D0\u00B0\u00D0\u00B7\u00D0\u00B0
|
||||
|
||||
INVALID_PAYMENTS_COUNT = \u00D0\u00BD\u00D0\u00B5\u00D0\u00B2\u00D0\u00B5\u00D1\u0080\u00D0\u00BD\u00D1\u008B\u00D0\u00B9 \u00D0\u00BF\u00D0\u00BE\u00D0\u00B4\u00D1\u0081\u00D1\u0087\u00D0\u00B5\u00D1\u0082 \u00D0\u00BF\u00D0\u00BB\u00D0\u00B0\u00D1\u0082\u00D0\u00B5\u00D0\u00B6\u00D0\u00B5\u00D0\u00B9
|
||||
|
||||
INVALID_PUBLIC_KEY = \u00D0\u00BD\u00D0\u00B5\u00D0\u00B4\u00D0\u00B5\u00D0\u00B9\u00D1\u0081\u00D1\u0082\u00D0\u00B2\u00D0\u00B8\u00D1\u0082\u00D0\u00B5\u00D0\u00BB\u00D1\u008C\u00D0\u00BD\u00D1\u008B\u00D0\u00B9 \u00D0\u00BE\u00D1\u0082\u00D0\u00BA\u00D1\u0080\u00D1\u008B\u00D1\u0082\u00D1\u008B\u00D0\u00B9 \u00D0\u00BA\u00D0\u00BB\u00D1\u008E\u00D1\u0087
|
||||
|
||||
INVALID_QUANTITY = \u00D0\u00BD\u00D0\u00B5\u00D0\u00B4\u00D0\u00BE\u00D0\u00BF\u00D1\u0083\u00D1\u0081\u00D1\u0082\u00D0\u00B8\u00D0\u00BC\u00D0\u00BE\u00D0\u00B5 \u00D0\u00BA\u00D0\u00BE\u00D0\u00BB\u00D0\u00B8\u00D1\u0087\u00D0\u00B5\u00D1\u0081\u00D1\u0082\u00D0\u00B2\u00D0\u00BE
|
||||
|
||||
INVALID_REFERENCE = \u00D0\u00BD\u00D0\u00B5\u00D0\u00B2\u00D0\u00B5\u00D1\u0080\u00D0\u00BD\u00D0\u00B0\u00D1\u008F \u00D1\u0081\u00D1\u0081\u00D1\u008B\u00D0\u00BB\u00D0\u00BA\u00D0\u00B0
|
||||
|
||||
INVALID_RETURN = \u00D0\u00BD\u00D0\u00B5\u00D0\u00B4\u00D0\u00BE\u00D0\u00BF\u00D1\u0083\u00D1\u0081\u00D1\u0082\u00D0\u00B8\u00D0\u00BC\u00D1\u008B\u00D0\u00B9 \u00D0\u00B2\u00D0\u00BE\u00D0\u00B7\u00D0\u00B2\u00D1\u0080\u00D0\u00B0\u00D1\u0082
|
||||
|
||||
INVALID_REWARD_SHARE_PERCENT = \u00D0\u00BD\u00D0\u00B5\u00D0\u00B4\u00D0\u00B5\u00D0\u00B9\u00D1\u0081\u00D1\u0082\u00D0\u00B2\u00D0\u00B8\u00D1\u0082\u00D0\u00B5\u00D0\u00BB\u00D1\u008C\u00D0\u00BD\u00D1\u008B\u00D0\u00B9 \u00D0\u00BF\u00D1\u0080\u00D0\u00BE\u00D1\u0086\u00D0\u00B5\u00D0\u00BD\u00D1\u0082 \u00D0\u00BD\u00D0\u00B0\u00D0\u00B3\u00D1\u0080\u00D0\u00B0\u00D0\u00B6\u00D0\u00B4\u00D0\u00B5\u00D0\u00BD\u00D0\u00B8\u00D1\u008F
|
||||
|
||||
INVALID_SELLER = \u00D0\u00BD\u00D0\u00B5\u00D0\u00B4\u00D0\u00B5\u00D0\u00B9\u00D1\u0081\u00D1\u0082\u00D0\u00B2\u00D0\u00B8\u00D1\u0082\u00D0\u00B5\u00D0\u00BB\u00D1\u008C\u00D0\u00BD\u00D1\u008B\u00D0\u00B9 \u00D0\u00BF\u00D1\u0080\u00D0\u00BE\u00D0\u00B4\u00D0\u00B0\u00D0\u00B2\u00D0\u00B5\u00D1\u0086
|
||||
|
||||
INVALID_TAGS_LENGTH = \u00D0\u00BD\u00D0\u00B5\u00D0\u00B4\u00D0\u00B5\u00D0\u00B9\u00D1\u0081\u00D1\u0082\u00D0\u00B2\u00D0\u00B8\u00D1\u0082\u00D0\u00B5\u00D0\u00BB\u00D1\u008C\u00D0\u00BD\u00D0\u00B0\u00D1\u008F \u00D0\u00B4\u00D0\u00BB\u00D0\u00B8\u00D0\u00BD\u00D0\u00B0 \u00D1\u0082\u00D1\u008D\u00D0\u00B3\u00D0\u00BE\u00D0\u00B2
|
||||
|
||||
INVALID_TX_GROUP_ID = \u00D0\u00BD\u00D0\u00B5\u00D0\u00B4\u00D0\u00B5\u00D0\u00B9\u00D1\u0081\u00D1\u0082\u00D0\u00B2\u00D0\u00B8\u00D1\u0082\u00D0\u00B5\u00D0\u00BB\u00D1\u008C\u00D0\u00BD\u00D1\u008B\u00D0\u00B9 \u00D0\u00B8\u00D0\u00B4\u00D0\u00B5\u00D0\u00BD\u00D1\u0082\u00D0\u00B8\u00D1\u0084\u00D0\u00B8\u00D0\u00BA\u00D0\u00B0\u00D1\u0082\u00D0\u00BE\u00D1\u0080 \u00D0\u00B3\u00D1\u0080\u00D1\u0083\u00D0\u00BF\u00D0\u00BF\u00D1\u008B \u00D0\u00BF\u00D0\u00B5\u00D1\u0080\u00D0\u00B5\u00D0\u00B4\u00D0\u00B0\u00D1\u0087\u00D0\u00B8
|
||||
|
||||
INVALID_VALUE_LENGTH = \u00D0\u00BD\u00D0\u00B5\u00D0\u00B4\u00D0\u00BE\u00D0\u00BF\u00D1\u0083\u00D1\u0081\u00D1\u0082\u00D0\u00B8\u00D0\u00BC\u00D0\u00BE\u00D0\u00B5 \u00D0\u00B7\u00D0\u00BD\u00D0\u00B0\u00D1\u0087\u00D0\u00B5\u00D0\u00BD\u00D0\u00B8\u00D0\u00B5 \u00D0\u00B4\u00D0\u00BB\u00D0\u00B8\u00D0\u00BD\u00D1\u008B
|
||||
|
||||
JOIN_REQUEST_EXISTS = \u00D0\u00B7\u00D0\u00B0\u00D0\u00BF\u00D1\u0080\u00D0\u00BE\u00D1\u0081 \u00D0\u00BD\u00D0\u00B0 \u00D0\u00BF\u00D1\u0080\u00D0\u00B8\u00D1\u0081\u00D0\u00BE\u00D0\u00B5\u00D0\u00B4\u00D0\u00B8\u00D0\u00BD\u00D0\u00B5\u00D0\u00BD\u00D0\u00B8\u00D0\u00B5 \u00D1\u0081\u00D1\u0083\u00D1\u0089\u00D0\u00B5\u00D1\u0081\u00D1\u0082\u00D0\u00B2\u00D1\u0083\u00D0\u00B5\u00D1\u0082
|
||||
|
||||
MAXIMUM_REWARD_SHARES = \u00D0\u00BC\u00D0\u00B0\u00D0\u00BA\u00D1\u0081\u00D0\u00B8\u00D0\u00BC\u00D0\u00B0\u00D0\u00BB\u00D1\u008C\u00D0\u00BD\u00D0\u00BE\u00D0\u00B5 \u00D0\u00B2\u00D0\u00BE\u00D0\u00B7\u00D0\u00BD\u00D0\u00B0\u00D0\u00B3\u00D1\u0080\u00D0\u00B0\u00D0\u00B6\u00D0\u00B4\u00D0\u00B5\u00D0\u00BD\u00D0\u00B8\u00D0\u00B5
|
||||
|
||||
MISSING_CREATOR = \u00D0\u00BE\u00D1\u0082\u00D1\u0081\u00D1\u0083\u00D1\u0082\u00D1\u0081\u00D1\u0082\u00D0\u00B2\u00D1\u0083\u00D1\u008E\u00D1\u0089\u00D0\u00B8\u00D0\u00B9 \u00D1\u0081\u00D0\u00BE\u00D0\u00B7\u00D0\u00B4\u00D0\u00B0\u00D1\u0082\u00D0\u00B5\u00D0\u00BB\u00D1\u008C
|
||||
|
||||
MULTIPLE_NAMES_FORBIDDEN = \u00D0\u00BD\u00D0\u00B5\u00D1\u0081\u00D0\u00BA\u00D0\u00BE\u00D0\u00BB\u00D1\u008C\u00D0\u00BA\u00D0\u00BE \u00D0\u00B8\u00D0\u00BC\u00D0\u00B5\u00D0\u00BD \u00D0\u00B7\u00D0\u00B0\u00D0\u00BF\u00D1\u0080\u00D0\u00B5\u00D1\u0089\u00D0\u00B5\u00D0\u00BD\u00D0\u00BE
|
||||
|
||||
NAME_ALREADY_FOR_SALE = \u00D0\u00B8\u00D0\u00BC\u00D1\u008F \u00D1\u0083\u00D0\u00B6\u00D0\u00B5 \u00D0\u00B2 \u00D0\u00BF\u00D1\u0080\u00D0\u00BE\u00D0\u00B4\u00D0\u00B0\u00D0\u00B6\u00D0\u00B5
|
||||
|
||||
NAME_ALREADY_REGISTERED = \u00D0\u00B8\u00D0\u00BC\u00D1\u008F \u00D1\u0083\u00D0\u00B6\u00D0\u00B5 \u00D0\u00B7\u00D0\u00B0\u00D1\u0080\u00D0\u00B5\u00D0\u00B3\u00D0\u00B8\u00D1\u0081\u00D1\u0082\u00D1\u0080\u00D0\u00B8\u00D1\u0080\u00D0\u00BE\u00D0\u00B2\u00D0\u00B0\u00D0\u00BD\u00D0\u00BE
|
||||
|
||||
NAME_DOES_NOT_EXIST = \u00D0\u00B8\u00D0\u00BC\u00D1\u008F \u00D0\u00BD\u00D0\u00B5 \u00D1\u0081\u00D1\u0083\u00D1\u0089\u00D0\u00B5\u00D1\u0081\u00D1\u0082\u00D0\u00B2\u00D1\u0083\u00D0\u00B5\u00D1\u0082
|
||||
|
||||
NAME_NOT_FOR_SALE = \u00D0\u00B8\u00D0\u00BC\u00D1\u008F \u00D0\u00BD\u00D0\u00B5 \u00D0\u00BF\u00D1\u0080\u00D0\u00BE\u00D0\u00B4\u00D0\u00B0\u00D0\u00B5\u00D1\u0082\u00D1\u0081\u00D1\u008F
|
||||
|
||||
NAME_NOT_LOWER_CASE = \u00D0\u00B8\u00D0\u00BC\u00D0\u00BC\u00D1\u008F \u00D0\u00BD\u00D0\u00B5 \u00D0\u00B4\u00D0\u00BE\u00D0\u00BB\u00D0\u00B6\u00D0\u00BD\u00D0\u00BE \u00D1\u0081\u00D0\u00BE\u00D0\u00B4\u00D0\u00B5\u00D1\u0080\u00D0\u00B6\u00D0\u00B0\u00D1\u0082\u00D1\u008C \u00D1\u0081\u00D1\u0082\u00D1\u0080\u00D0\u00BE\u00D1\u0087\u00D0\u00BD\u00D1\u008B\u00D0\u00B9 \u00D1\u0080\u00D0\u00B5\u00D0\u00B3\u00D0\u00B8\u00D1\u0081\u00D1\u0082\u00D1\u0080
|
||||
|
||||
NEGATIVE_AMOUNT = \u00D0\u00BD\u00D0\u00B5\u00D0\u00B4\u00D0\u00BE\u00D1\u0081\u00D1\u0082\u00D0\u00B0\u00D1\u0082\u00D0\u00BE\u00D1\u0087\u00D0\u00BD\u00D0\u00B0\u00D1\u008F \u00D1\u0081\u00D1\u0083\u00D0\u00BC\u00D0\u00BC\u00D0\u00B0
|
||||
|
||||
NEGATIVE_FEE = \u00D0\u00BD\u00D0\u00B5\u00D0\u00B4\u00D0\u00BE\u00D1\u0081\u00D1\u0082\u00D0\u00B0\u00D1\u0082\u00D0\u00BE\u00D1\u0087\u00D0\u00BD\u00D0\u00B0\u00D1\u008F \u00D0\u00BA\u00D0\u00BE\u00D0\u00BC\u00D0\u00B8\u00D1\u0081\u00D1\u0081\u00D0\u00B8\u00D1\u008F
|
||||
|
||||
NEGATIVE_PRICE = \u00D0\u00BD\u00D0\u00B5\u00D0\u00B4\u00D0\u00BE\u00D1\u0081\u00D1\u0082\u00D0\u00B0\u00D1\u0082\u00D0\u00BE\u00D1\u0087\u00D0\u00BD\u00D0\u00B0\u00D1\u008F \u00D1\u0081\u00D1\u0082\u00D0\u00BE\u00D0\u00B8\u00D0\u00BC\u00D0\u00BE\u00D1\u0081\u00D1\u0082\u00D1\u008C
|
||||
|
||||
NOT_GROUP_ADMIN = \u00D0\u00BD\u00D0\u00B5 \u00D0\u00B0\u00D0\u00B4\u00D0\u00BC\u00D0\u00B8\u00D0\u00BD\u00D0\u00B8\u00D1\u0081\u00D1\u0082\u00D1\u0080\u00D0\u00B0\u00D1\u0082\u00D0\u00BE\u00D1\u0080 \u00D0\u00B3\u00D1\u0080\u00D1\u0083\u00D0\u00BF\u00D0\u00BF\u00D1\u008B
|
||||
|
||||
NOT_GROUP_MEMBER = \u00D0\u00BD\u00D0\u00B5 \u00D1\u0087\u00D0\u00BB\u00D0\u00B5\u00D0\u00BD \u00D0\u00B3\u00D1\u0080\u00D1\u0083\u00D0\u00BF\u00D0\u00BF\u00D1\u008B
|
||||
|
||||
NOT_MINTING_ACCOUNT = \u00D1\u0081\u00D1\u0087\u00D0\u00B5\u00D1\u0082 \u00D0\u00BD\u00D0\u00B5 \u00D1\u0087\u00D0\u00B5\u00D0\u00BA\u00D0\u00B0\u00D0\u00BD\u00D0\u00B8\u00D1\u0082
|
||||
|
||||
NOT_YET_RELEASED = \u00D0\u00B5\u00D1\u0089\u00D0\u00B5 \u00D0\u00BD\u00D0\u00B5 \u00D0\u00B2\u00D1\u008B\u00D0\u00BF\u00D1\u0083\u00D1\u0089\u00D0\u00B5\u00D0\u00BD\u00D0\u00BE
|
||||
|
||||
NO_BALANCE = \u00D0\u00BD\u00D0\u00B5\u00D1\u0082 \u00D0\u00B1\u00D0\u00B0\u00D0\u00BB\u00D0\u00B0\u00D0\u00BD\u00D1\u0081\u00D0\u00B0
|
||||
|
||||
NO_BLOCKCHAIN_LOCK = \u00D0\u00B1\u00D0\u00BB\u00D0\u00BE\u00D0\u00BA\u00D1\u0087\u00D0\u00B5\u00D0\u00B9\u00D0\u00BD \u00D1\u0083\u00D0\u00B7\u00D0\u00BB\u00D0\u00B0 \u00D0\u00B2 \u00D0\u00BD\u00D0\u00B0\u00D1\u0081\u00D1\u0082\u00D0\u00BE\u00D1\u008F\u00D1\u0089\u00D0\u00B5\u00D0\u00B5 \u00D0\u00B2\u00D1\u0080\u00D0\u00B5\u00D0\u00BC\u00D1\u008F \u00D0\u00B7\u00D0\u00B0\u00D0\u00BD\u00D1\u008F\u00D1\u0082
|
||||
|
||||
NO_FLAG_PERMISSION = \u00D0\u00BD\u00D0\u00B5\u00D1\u0082 \u00D1\u0080\u00D0\u00B0\u00D0\u00B7\u00D1\u0080\u00D0\u00B5\u00D1\u0088\u00D0\u00B5\u00D0\u00BD\u00D0\u00B8\u00D1\u008F \u00D0\u00BD\u00D0\u00B0 \u00D1\u0084\u00D0\u00BB\u00D0\u00B0\u00D0\u00B3
|
||||
|
||||
OK = OK
|
||||
|
||||
ORDER_ALREADY_CLOSED = \u00D0\u00B7\u00D0\u00B0\u00D0\u00BA\u00D0\u00B0\u00D0\u00B7 \u00D0\u00B7\u00D0\u00B0\u00D0\u00BA\u00D1\u0080\u00D1\u008B\u00D1\u0082
|
||||
|
||||
ORDER_DOES_NOT_EXIST = \u00D0\u00B7\u00D0\u00B0\u00D0\u00BA\u00D0\u00B0\u00D0\u00B7\u00D0\u00B0 \u00D0\u00BD\u00D0\u00B5 \u00D1\u0081\u00D1\u0083\u00D1\u0089\u00D0\u00B5\u00D1\u0081\u00D1\u0082\u00D0\u00B2\u00D1\u0083\u00D0\u00B5\u00D1\u0082
|
||||
|
||||
POLL_ALREADY_EXISTS = \u00D0\u00BE\u00D0\u00BF\u00D1\u0080\u00D0\u00BE\u00D1\u0081 \u00D1\u0083\u00D0\u00B6\u00D0\u00B5 \u00D1\u0081\u00D1\u0083\u00D1\u0089\u00D0\u00B5\u00D1\u0081\u00D1\u0082\u00D0\u00B2\u00D1\u0083\u00D0\u00B5\u00D1\u0082
|
||||
|
||||
POLL_DOES_NOT_EXIST = \u00D0\u00BE\u00D0\u00BF\u00D1\u0080\u00D0\u00BE\u00D1\u0081\u00D0\u00B0 \u00D0\u00BD\u00D0\u00B5 \u00D1\u0081\u00D1\u0083\u00D1\u0089\u00D0\u00B5\u00D1\u0081\u00D1\u0082\u00D0\u00B2\u00D1\u0083\u00D0\u00B5\u00D1\u0082
|
||||
|
||||
POLL_OPTION_DOES_NOT_EXIST = \u00D0\u00B2\u00D0\u00B0\u00D1\u0080\u00D0\u00B8\u00D0\u00B0\u00D0\u00BD\u00D1\u0082\u00D0\u00BE\u00D0\u00B2 \u00D0\u00BE\u00D1\u0082\u00D0\u00B2\u00D0\u00B5\u00D1\u0082\u00D0\u00B0 \u00D0\u00BD\u00D0\u00B5 \u00D1\u0081\u00D1\u0083\u00D1\u0089\u00D0\u00B5\u00D1\u0081\u00D1\u0082\u00D0\u00B2\u00D1\u0083\u00D0\u00B5\u00D1\u0082
|
||||
|
||||
PUBLIC_KEY_UNKNOWN = \u00D0\u00BE\u00D1\u0082\u00D0\u00BA\u00D1\u0080\u00D1\u008B\u00D1\u0082\u00D1\u008B\u00D0\u00B9 \u00D0\u00BA\u00D0\u00BB\u00D1\u008E\u00D1\u0087 \u00D0\u00BD\u00D0\u00B5\u00D0\u00B8\u00D0\u00B7\u00D0\u00B2\u00D0\u00B5\u00D1\u0081\u00D1\u0082\u00D0\u00B5\u00D0\u00BD
|
||||
|
||||
SELF_SHARE_EXISTS = \u00D0\u00BF\u00D0\u00BE\u00D0\u00B4\u00D0\u00B5\u00D0\u00BB\u00D0\u00B8\u00D1\u0082\u00D1\u008C\u00D1\u0081\u00D1\u008F \u00D0\u00B4\u00D0\u00BE\u00D0\u00BB\u00D0\u00B5\u00D0\u00B9
|
||||
|
||||
TIMESTAMP_TOO_NEW = \u00D0\u00BD\u00D0\u00BE\u00D0\u00B2\u00D0\u00B0\u00D1\u008F \u00D0\u00BC\u00D0\u00B5\u00D1\u0082\u00D0\u00BA\u00D0\u00B0 \u00D0\u00B2\u00D1\u0080\u00D0\u00B5\u00D0\u00BC\u00D0\u00B5\u00D0\u00BD\u00D0\u00B8
|
||||
|
||||
TIMESTAMP_TOO_OLD = \u00D1\u0081\u00D1\u0082\u00D0\u00B0\u00D1\u0080\u00D0\u00B0\u00D1\u008F \u00D0\u00BC\u00D0\u00B5\u00D1\u0082\u00D0\u00BA\u00D0\u00B0 \u00D0\u00B2\u00D1\u0080\u00D0\u00B5\u00D0\u00BC\u00D0\u00B5\u00D0\u00BD\u00D0\u00B8
|
||||
|
||||
TRANSACTION_ALREADY_CONFIRMED = \u00D1\u0082\u00D1\u0080\u00D0\u00B0\u00D0\u00BD\u00D0\u00B7\u00D0\u00B0\u00D0\u00BA\u00D1\u0086\u00D0\u00B8\u00D1\u008F \u00D1\u0083\u00D0\u00B6\u00D0\u00B5 \u00D0\u00BF\u00D0\u00BE\u00D0\u00B4\u00D1\u0082\u00D0\u00B2\u00D0\u00B5\u00D1\u0080\u00D0\u00B6\u00D0\u00B4\u00D0\u00B5\u00D0\u00BD\u00D0\u00B0
|
||||
|
||||
TRANSACTION_ALREADY_EXISTS = \u00D1\u0082\u00D1\u0080\u00D0\u00B0\u00D0\u00BD\u00D0\u00B7\u00D0\u00B0\u00D0\u00BA\u00D1\u0086\u00D0\u00B8\u00D1\u008F \u00D1\u0081\u00D1\u0083\u00D1\u0089\u00D0\u00B5\u00D1\u0081\u00D1\u0082\u00D0\u00B2\u00D1\u0083\u00D0\u00B5\u00D1\u0082
|
||||
|
||||
TRANSACTION_UNKNOWN = \u00D0\u00BD\u00D0\u00B5\u00D0\u00B8\u00D0\u00B7\u00D0\u00B2\u00D0\u00B5\u00D1\u0081\u00D1\u0082\u00D0\u00BD\u00D0\u00B0\u00D1\u008F \u00D1\u0082\u00D1\u0080\u00D0\u00B0\u00D0\u00BD\u00D0\u00B7\u00D0\u00B0\u00D0\u00BA\u00D1\u0086\u00D0\u00B8\u00D1\u008F
|
@@ -171,4 +171,84 @@ public class AccountBalanceTests extends Common {
|
||||
Common.useDefaultSettings();
|
||||
}
|
||||
|
||||
/** Test batch set/delete of account balances */
|
||||
@Test
|
||||
public void testBatchedBalanceChanges() throws DataException, SQLException {
|
||||
Random random = new Random();
|
||||
int ai;
|
||||
|
||||
try (final Repository repository = RepositoryManager.getRepository()) {
|
||||
System.out.println("Creating random accounts...");
|
||||
|
||||
// Generate some random accounts
|
||||
List<Account> accounts = new ArrayList<>();
|
||||
for (ai = 0; ai < 2000; ++ai) {
|
||||
byte[] publicKey = new byte[32];
|
||||
random.nextBytes(publicKey);
|
||||
|
||||
PublicKeyAccount account = new PublicKeyAccount(repository, publicKey);
|
||||
accounts.add(account);
|
||||
}
|
||||
|
||||
List<AccountBalanceData> accountBalances = new ArrayList<>();
|
||||
|
||||
System.out.println("Setting random balances...");
|
||||
|
||||
// Fill with lots of random balances
|
||||
for (ai = 0; ai < accounts.size(); ++ai) {
|
||||
Account account = accounts.get(ai);
|
||||
int assetId = random.nextInt(2);
|
||||
// random zero, or non-zero, balance
|
||||
long balance = random.nextBoolean() ? 0L : random.nextInt(100000);
|
||||
|
||||
accountBalances.add(new AccountBalanceData(account.getAddress(), assetId, balance));
|
||||
}
|
||||
|
||||
repository.getAccountRepository().setAssetBalances(accountBalances);
|
||||
repository.saveChanges();
|
||||
|
||||
System.out.println("Setting new random balances...");
|
||||
|
||||
// Now flip zero-ness for first half of balances
|
||||
for (ai = 0; ai < accountBalances.size() / 2; ++ai) {
|
||||
AccountBalanceData accountBalanceData = accountBalances.get(ai);
|
||||
|
||||
accountBalanceData.setBalance(accountBalanceData.getBalance() != 0 ? 0L : random.nextInt(100000));
|
||||
}
|
||||
// ...and randomize the rest
|
||||
for (/*use ai from before*/; ai < accountBalances.size(); ++ai) {
|
||||
AccountBalanceData accountBalanceData = accountBalances.get(ai);
|
||||
|
||||
accountBalanceData.setBalance(random.nextBoolean() ? 0L : random.nextInt(100000));
|
||||
}
|
||||
|
||||
repository.getAccountRepository().setAssetBalances(accountBalances);
|
||||
repository.saveChanges();
|
||||
|
||||
System.out.println("Modifying random balances...");
|
||||
|
||||
// Fill with lots of random balance changes
|
||||
for (ai = 0; ai < accounts.size(); ++ai) {
|
||||
Account account = accounts.get(ai);
|
||||
int assetId = random.nextInt(2);
|
||||
// random zero, or non-zero, balance
|
||||
long balance = random.nextBoolean() ? 0L : random.nextInt(100000);
|
||||
|
||||
accountBalances.add(new AccountBalanceData(account.getAddress(), assetId, balance));
|
||||
}
|
||||
|
||||
repository.getAccountRepository().modifyAssetBalances(accountBalances);
|
||||
repository.saveChanges();
|
||||
|
||||
System.out.println("Deleting all balances...");
|
||||
|
||||
// Now simply delete all balances
|
||||
for (ai = 0; ai < accountBalances.size(); ++ai)
|
||||
accountBalances.get(ai).setBalance(0L);
|
||||
|
||||
repository.getAccountRepository().setAssetBalances(accountBalances);
|
||||
repository.saveChanges();
|
||||
}
|
||||
}
|
||||
|
||||
}
|
@@ -133,4 +133,48 @@ public class BlockTests extends Common {
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testCommonBlockSearch() {
|
||||
// Given a list of block summaries, trim all trailing summaries after common block
|
||||
|
||||
// We'll represent known block summaries as a list of booleans,
|
||||
// where the boolean value indicates whether peer's block is also in our repository.
|
||||
|
||||
// Trivial case, single element array
|
||||
assertCommonBlock(0, new boolean[] { true });
|
||||
|
||||
// Test odd and even array lengths
|
||||
for (int arrayLength = 5; arrayLength <= 6; ++arrayLength) {
|
||||
boolean[] testBlocks = new boolean[arrayLength];
|
||||
|
||||
// Test increasing amount of common blocks
|
||||
for (int c = 1; c <= testBlocks.length; ++c) {
|
||||
testBlocks[c - 1] = true;
|
||||
|
||||
assertCommonBlock(c - 1, testBlocks);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private void assertCommonBlock(int expectedIndex, boolean[] testBlocks) {
|
||||
int commonBlockIndex = findCommonBlockIndex(testBlocks);
|
||||
assertEquals(expectedIndex, commonBlockIndex);
|
||||
}
|
||||
|
||||
private int findCommonBlockIndex(boolean[] testBlocks) {
|
||||
int low = 1;
|
||||
int high = testBlocks.length - 1;
|
||||
|
||||
while (low <= high) {
|
||||
int mid = (low + high) >>> 1;
|
||||
|
||||
if (testBlocks[mid])
|
||||
low = mid + 1;
|
||||
else
|
||||
high = mid - 1;
|
||||
}
|
||||
|
||||
return low - 1;
|
||||
}
|
||||
|
||||
}
|
||||
|
@@ -4,6 +4,8 @@ import org.junit.Before;
|
||||
import org.junit.Test;
|
||||
import org.qortal.account.Account;
|
||||
import org.qortal.asset.Asset;
|
||||
import org.qortal.crosschain.BTCACCT;
|
||||
import org.qortal.crypto.Crypto;
|
||||
import org.qortal.repository.DataException;
|
||||
import org.qortal.repository.Repository;
|
||||
import org.qortal.repository.RepositoryManager;
|
||||
@@ -13,7 +15,11 @@ import org.qortal.test.common.Common;
|
||||
|
||||
import static org.junit.Assert.*;
|
||||
|
||||
import java.sql.ResultSet;
|
||||
import java.sql.SQLException;
|
||||
import java.util.ArrayList;
|
||||
import java.util.Collections;
|
||||
import java.util.List;
|
||||
import java.util.concurrent.Executors;
|
||||
import java.util.concurrent.ScheduledExecutorService;
|
||||
import java.util.concurrent.TimeUnit;
|
||||
@@ -71,7 +77,7 @@ public class RepositoryTests extends Common {
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testDeadlock() throws DataException {
|
||||
public void testDeadlock() {
|
||||
// Open connection 1
|
||||
try (final Repository repository1 = RepositoryManager.getRepository()) {
|
||||
|
||||
@@ -92,18 +98,50 @@ public class RepositoryTests extends Common {
|
||||
// Update account in 1
|
||||
account1.setConfirmedBalance(Asset.QORT, 5678L);
|
||||
repository1.saveChanges();
|
||||
} catch (DataException e) {
|
||||
fail("deadlock bug");
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testUpdateReadDeadlock() {
|
||||
// Open connection 1
|
||||
try (final Repository repository1 = RepositoryManager.getRepository()) {
|
||||
// Mint blocks so we have data (online account signatures) to work with
|
||||
for (int i = 0; i < 10; ++i)
|
||||
BlockUtils.mintBlock(repository1);
|
||||
|
||||
// Perform database 'update', but don't commit at this stage
|
||||
repository1.getBlockRepository().trimOldOnlineAccountsSignatures(1, 10);
|
||||
|
||||
// Open connection 2
|
||||
try (final Repository repository2 = RepositoryManager.getRepository()) {
|
||||
// Perform database read on same blocks - this should not deadlock
|
||||
repository2.getBlockRepository().getTimestampFromHeight(5);
|
||||
}
|
||||
|
||||
// Save updates - this should not deadlock
|
||||
repository1.saveChanges();
|
||||
} catch (DataException e) {
|
||||
fail("deadlock bug");
|
||||
}
|
||||
}
|
||||
|
||||
/** Check that the <i>sub-query</i> used to fetch highest block height is optimized by HSQLDB. */
|
||||
@Test
|
||||
public void testBlockHeightSpeed() throws DataException, SQLException {
|
||||
final int mintBlockCount = 30000;
|
||||
|
||||
try (final Repository repository = RepositoryManager.getRepository()) {
|
||||
// Mint some blocks
|
||||
System.out.println("Minting test blocks - should take approx. 30 seconds...");
|
||||
for (int i = 0; i < 30000; ++i)
|
||||
System.out.println(String.format("Minting %d test blocks - should take approx. 30 seconds...", mintBlockCount));
|
||||
|
||||
long beforeBigMint = System.currentTimeMillis();
|
||||
for (int i = 0; i < mintBlockCount; ++i)
|
||||
BlockUtils.mintBlock(repository);
|
||||
|
||||
System.out.println(String.format("Minting %d blocks actually took %d seconds", mintBlockCount, (System.currentTimeMillis() - beforeBigMint) / 1000L));
|
||||
|
||||
final HSQLDBRepository hsqldb = (HSQLDBRepository) repository;
|
||||
|
||||
// Too slow:
|
||||
@@ -158,6 +196,119 @@ public class RepositoryTests extends Common {
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Test HSQLDB bug-fix for INSERT INTO...ON DUPLICATE KEY UPDATE... bug
|
||||
* <p>
|
||||
* @see <A HREF="https://sourceforge.net/p/hsqldb/discussion/73674/thread/d8d35adb5d/">Behaviour of 'ON DUPLICATE KEY UPDATE'</A> SourceForge discussion
|
||||
*/
|
||||
@Test
|
||||
public void testOnDuplicateKeyUpdateBugFix() throws SQLException, DataException {
|
||||
ResultSet resultSet;
|
||||
|
||||
try (final HSQLDBRepository hsqldb = (HSQLDBRepository) RepositoryManager.getRepository()) {
|
||||
hsqldb.prepareStatement("DROP TABLE IF EXISTS bugtest").execute();
|
||||
hsqldb.prepareStatement("CREATE TABLE bugtest (id INT NOT NULL, counter INT NOT NULL, PRIMARY KEY(id))").execute();
|
||||
|
||||
// No existing row, so new row's "counter" is set to value from VALUES clause, i.e. 1
|
||||
hsqldb.prepareStatement("INSERT INTO bugtest (id, counter) VALUES (1, 1) ON DUPLICATE KEY UPDATE counter = counter + 1").execute();
|
||||
resultSet = hsqldb.checkedExecute("SELECT counter FROM bugtest WHERE id = 1");
|
||||
assertNotNull(resultSet);
|
||||
assertEquals(1, resultSet.getInt(1));
|
||||
|
||||
// Prior to bug-fix, "counter = counter + 1" would always use the 100 from VALUES, instead of existing row's value, for "counter"
|
||||
hsqldb.prepareStatement("INSERT INTO bugtest (id, counter) VALUES (1, 100) ON DUPLICATE KEY UPDATE counter = counter + 1").execute();
|
||||
resultSet = hsqldb.checkedExecute("SELECT counter FROM bugtest WHERE id = 1");
|
||||
assertNotNull(resultSet);
|
||||
// Prior to bug-fix, this would be 100 + 1 = 101
|
||||
assertEquals(2, resultSet.getInt(1));
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Test HSQLDB bug-fix for "General Error" in non-fully-qualified columns inside LATERAL()
|
||||
* <p>
|
||||
* @see <A HREF="https://sourceforge.net/p/hsqldb/bugs/1580/">#1580 General error with LATERAL and transitive join column</A> SourceForge ticket
|
||||
*/
|
||||
@Test
|
||||
public void testOnLateralGeneralError() {
|
||||
try (final HSQLDBRepository hsqldb = (HSQLDBRepository) RepositoryManager.getRepository()) {
|
||||
hsqldb.prepareStatement("DROP TABLE IF EXISTS tableA").execute();
|
||||
hsqldb.prepareStatement("DROP TABLE IF EXISTS tableB").execute();
|
||||
hsqldb.prepareStatement("DROP TABLE IF EXISTS tableC").execute();
|
||||
|
||||
hsqldb.prepareStatement("CREATE TABLE tableA (col1 INT)").execute();
|
||||
hsqldb.prepareStatement("CREATE TABLE tableB (col1 INT)").execute();
|
||||
hsqldb.prepareStatement("CREATE TABLE tableC (col2 INT, PRIMARY KEY (col2))").execute();
|
||||
|
||||
// Prior to bug-fix #1580 this would throw a General Error SQL Exception
|
||||
hsqldb.prepareStatement("SELECT col3 FROM tableA JOIN tableB USING (col1) CROSS JOIN LATERAL(SELECT col2 FROM tableC WHERE col2 = col1) AS tableC (col3)").execute();
|
||||
} catch (SQLException | DataException e) {
|
||||
fail("HSQLDB bug #1580");
|
||||
}
|
||||
}
|
||||
|
||||
/** Specifically test LATERAL() usage in Asset repository */
|
||||
@Test
|
||||
public void testAssetLateral() {
|
||||
try (final HSQLDBRepository hsqldb = (HSQLDBRepository) RepositoryManager.getRepository()) {
|
||||
List<Long> assetIds = Collections.emptyList();
|
||||
List<Long> otherAssetIds = Collections.emptyList();
|
||||
Integer limit = null;
|
||||
Integer offset = null;
|
||||
Boolean reverse = null;
|
||||
|
||||
hsqldb.getAssetRepository().getRecentTrades(assetIds, otherAssetIds, limit, offset, reverse);
|
||||
} catch (DataException e) {
|
||||
fail("HSQLDB bug #1580");
|
||||
}
|
||||
}
|
||||
|
||||
/** Specifically test LATERAL() usage in AT repository */
|
||||
@Test
|
||||
public void testAtLateral() {
|
||||
try (final HSQLDBRepository hsqldb = (HSQLDBRepository) RepositoryManager.getRepository()) {
|
||||
byte[] codeHash = BTCACCT.CODE_BYTES_HASH;
|
||||
Boolean isFinished = null;
|
||||
Integer dataByteOffset = null;
|
||||
Long expectedValue = null;
|
||||
Integer minimumFinalHeight = 2;
|
||||
Integer limit = null;
|
||||
Integer offset = null;
|
||||
Boolean reverse = null;
|
||||
|
||||
hsqldb.getATRepository().getMatchingFinalATStates(codeHash, isFinished, dataByteOffset, expectedValue, minimumFinalHeight, limit, offset, reverse);
|
||||
} catch (DataException e) {
|
||||
fail("HSQLDB bug #1580");
|
||||
}
|
||||
}
|
||||
|
||||
/** Specifically test LATERAL() usage in Chat repository */
|
||||
@Test
|
||||
public void testChatLateral() {
|
||||
try (final HSQLDBRepository hsqldb = (HSQLDBRepository) RepositoryManager.getRepository()) {
|
||||
String address = Crypto.toAddress(new byte[32]);
|
||||
|
||||
hsqldb.getChatRepository().getActiveChats(address);
|
||||
} catch (DataException e) {
|
||||
fail("HSQLDB bug #1580");
|
||||
}
|
||||
}
|
||||
|
||||
/** Test batched DELETE */
|
||||
@Test
|
||||
public void testBatchedDelete() {
|
||||
// Generate test data
|
||||
List<Object[]> batchedObjects = new ArrayList<>();
|
||||
for (int i = 0; i < 100; ++i)
|
||||
batchedObjects.add(new Object[] { String.valueOf(i), 1L });
|
||||
|
||||
try (final HSQLDBRepository hsqldb = (HSQLDBRepository) RepositoryManager.getRepository()) {
|
||||
hsqldb.deleteBatch("AccountBalances", "account = ? AND asset_id = ?", batchedObjects);
|
||||
} catch (DataException | SQLException e) {
|
||||
fail("Batched delete failed: " + e.getMessage());
|
||||
}
|
||||
}
|
||||
|
||||
public static void hsqldbSleep(int millis) throws SQLException {
|
||||
System.out.println(String.format("HSQLDB sleep() thread ID: %s", Thread.currentThread().getId()));
|
||||
|
||||
|
76
src/test/java/org/qortal/test/TransactionSearchTests.java
Normal file
76
src/test/java/org/qortal/test/TransactionSearchTests.java
Normal file
@@ -0,0 +1,76 @@
|
||||
package org.qortal.test;
|
||||
|
||||
import static org.junit.Assert.*;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
import org.junit.Before;
|
||||
import org.junit.Test;
|
||||
import org.qortal.account.PrivateKeyAccount;
|
||||
import org.qortal.repository.DataException;
|
||||
import org.qortal.repository.Repository;
|
||||
import org.qortal.repository.RepositoryManager;
|
||||
import org.qortal.test.common.AccountUtils;
|
||||
import org.qortal.test.common.BlockUtils;
|
||||
import org.qortal.test.common.Common;
|
||||
import org.qortal.transaction.Transaction.TransactionType;
|
||||
|
||||
public class TransactionSearchTests extends Common {
|
||||
|
||||
@Before
|
||||
public void beforeTest() throws DataException {
|
||||
Common.useDefaultSettings();
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testFindingSpecificTransactionsWithinHeight() throws DataException {
|
||||
try (final Repository repository = RepositoryManager.getRepository()) {
|
||||
PrivateKeyAccount alice = Common.getTestAccount(repository, "alice");
|
||||
PrivateKeyAccount chloe = Common.getTestAccount(repository, "chloe");
|
||||
|
||||
// Block 2
|
||||
BlockUtils.mintBlock(repository);
|
||||
|
||||
// Block 3
|
||||
AccountUtils.pay(repository, alice, chloe.getAddress(), 1234L);
|
||||
|
||||
// Block 4
|
||||
AccountUtils.pay(repository, chloe, alice.getAddress(), 5678L);
|
||||
|
||||
// Block 5
|
||||
BlockUtils.mintBlock(repository);
|
||||
|
||||
List<byte[]> signatures;
|
||||
|
||||
// No transactions with this type
|
||||
signatures = repository.getTransactionRepository().getSignaturesMatchingCriteria(TransactionType.GROUP_KICK, null, null, null);
|
||||
assertEquals(0, signatures.size());
|
||||
|
||||
// 2 payments
|
||||
signatures = repository.getTransactionRepository().getSignaturesMatchingCriteria(TransactionType.PAYMENT, null, null, null);
|
||||
assertEquals(2, signatures.size());
|
||||
|
||||
// 1 payment by Alice
|
||||
signatures = repository.getTransactionRepository().getSignaturesMatchingCriteria(TransactionType.PAYMENT, alice.getPublicKey(), null, null);
|
||||
assertEquals(1, signatures.size());
|
||||
|
||||
// 1 transaction by Chloe
|
||||
signatures = repository.getTransactionRepository().getSignaturesMatchingCriteria(null, chloe.getPublicKey(), null, null);
|
||||
assertEquals(1, signatures.size());
|
||||
|
||||
// 1 transaction from blocks 4 onwards
|
||||
signatures = repository.getTransactionRepository().getSignaturesMatchingCriteria(null, null, 4, null);
|
||||
assertEquals(1, signatures.size());
|
||||
|
||||
// 1 transaction from blocks 2 to 3
|
||||
signatures = repository.getTransactionRepository().getSignaturesMatchingCriteria(null, null, 2, 3);
|
||||
assertEquals(1, signatures.size());
|
||||
|
||||
// No transaction of this type from blocks 2 to 5
|
||||
signatures = repository.getTransactionRepository().getSignaturesMatchingCriteria(TransactionType.ISSUE_ASSET, null, 2, 5);
|
||||
assertEquals(0, signatures.size());
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
}
|
@@ -7,7 +7,7 @@ import org.qortal.account.Account;
|
||||
import org.qortal.account.PrivateKeyAccount;
|
||||
import org.qortal.account.PublicKeyAccount;
|
||||
import org.qortal.block.BlockChain;
|
||||
import org.qortal.block.BlockMinter;
|
||||
import org.qortal.controller.BlockMinter;
|
||||
import org.qortal.data.account.AccountData;
|
||||
import org.qortal.data.transaction.BaseTransactionData;
|
||||
import org.qortal.data.transaction.PaymentTransactionData;
|
||||
|
@@ -9,6 +9,7 @@ import java.util.List;
|
||||
import org.junit.Before;
|
||||
import org.junit.Test;
|
||||
import org.qortal.account.PrivateKeyAccount;
|
||||
import org.qortal.api.ApiError;
|
||||
import org.qortal.api.resource.BlocksResource;
|
||||
import org.qortal.block.GenesisBlock;
|
||||
import org.qortal.repository.DataException;
|
||||
@@ -82,6 +83,19 @@ public class BlockApiTests extends ApiCommon {
|
||||
@Test
|
||||
public void testGetBlockRange() {
|
||||
assertNotNull(this.blocksResource.getBlockRange(1, 1));
|
||||
|
||||
List<Integer> testValues = Arrays.asList(null, Integer.valueOf(1));
|
||||
|
||||
for (Integer startHeight : testValues)
|
||||
for (Integer endHeight : testValues)
|
||||
for (Integer count : testValues) {
|
||||
if (startHeight != null && endHeight != null && count != null) {
|
||||
assertApiError(ApiError.INVALID_CRITERIA, () -> this.blocksResource.getBlockRange(startHeight, endHeight, count));
|
||||
continue;
|
||||
}
|
||||
|
||||
assertNotNull(this.blocksResource.getBlockRange(startHeight, endHeight, count));
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
|
38
src/test/java/org/qortal/test/api/CrossChainApiTests.java
Normal file
38
src/test/java/org/qortal/test/api/CrossChainApiTests.java
Normal file
@@ -0,0 +1,38 @@
|
||||
package org.qortal.test.api;
|
||||
|
||||
import org.junit.Before;
|
||||
import org.junit.Test;
|
||||
import org.qortal.api.ApiError;
|
||||
import org.qortal.api.resource.CrossChainResource;
|
||||
import org.qortal.test.common.ApiCommon;
|
||||
|
||||
public class CrossChainApiTests extends ApiCommon {
|
||||
|
||||
private CrossChainResource crossChainResource;
|
||||
|
||||
@Before
|
||||
public void buildResource() {
|
||||
this.crossChainResource = (CrossChainResource) ApiCommon.buildResource(CrossChainResource.class);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testGetTradeOffers() {
|
||||
assertNoApiError((limit, offset, reverse) -> this.crossChainResource.getTradeOffers(limit, offset, reverse));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testGetCompletedTrades() {
|
||||
assertNoApiError((limit, offset, reverse) -> this.crossChainResource.getCompletedTrades(System.currentTimeMillis() /*minimumTimestamp*/, limit, offset, reverse));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testInvalidGetCompletedTrades() {
|
||||
Integer limit = null;
|
||||
Integer offset = null;
|
||||
Boolean reverse = null;
|
||||
|
||||
assertApiError(ApiError.INVALID_CRITERIA, () -> this.crossChainResource.getCompletedTrades(-1L /*minimumTimestamp*/, limit, offset, reverse));
|
||||
assertApiError(ApiError.INVALID_CRITERIA, () -> this.crossChainResource.getCompletedTrades(0L /*minimumTimestamp*/, limit, offset, reverse));
|
||||
}
|
||||
|
||||
}
|
@@ -0,0 +1,63 @@
|
||||
package org.qortal.test.apps;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.lang.management.ManagementFactory;
|
||||
import java.nio.file.Files;
|
||||
import java.nio.file.Path;
|
||||
import java.nio.file.Paths;
|
||||
import java.util.ArrayList;
|
||||
import java.util.Arrays;
|
||||
import java.util.List;
|
||||
|
||||
public class LaunchExeWIthJvmOptions {
|
||||
|
||||
private static final String JAR_FILENAME = "qortal.jar";
|
||||
private static final String WINDOWS_EXE_LAUNCHER = "qortal.exe";
|
||||
private static final String JAVA_TOOL_OPTIONS_NAME = "JAVA_TOOL_OPTIONS";
|
||||
private static final String JAVA_TOOL_OPTIONS_VALUE = "-XX:MaxRAMFraction=4";
|
||||
|
||||
public static void main(String[] args) {
|
||||
String javaHome = System.getProperty("java.home");
|
||||
System.out.println(String.format("Java home: %s", javaHome));
|
||||
|
||||
Path javaBinary = Paths.get(javaHome, "bin", "java");
|
||||
System.out.println(String.format("Java binary: %s", javaBinary));
|
||||
|
||||
Path exeLauncher = Paths.get(WINDOWS_EXE_LAUNCHER);
|
||||
System.out.println(String.format("Windows EXE launcher: %s", exeLauncher));
|
||||
|
||||
List<String> javaCmd;
|
||||
if (Files.exists(exeLauncher)) {
|
||||
javaCmd = Arrays.asList(exeLauncher.toString());
|
||||
} else {
|
||||
javaCmd = new ArrayList<>();
|
||||
// Java runtime binary itself
|
||||
javaCmd.add(javaBinary.toString());
|
||||
|
||||
// JVM arguments
|
||||
javaCmd.addAll(ManagementFactory.getRuntimeMXBean().getInputArguments());
|
||||
|
||||
// Call mainClass in JAR
|
||||
javaCmd.addAll(Arrays.asList("-jar", JAR_FILENAME));
|
||||
|
||||
// Add saved command-line args
|
||||
javaCmd.addAll(Arrays.asList(args));
|
||||
}
|
||||
|
||||
try {
|
||||
System.out.println(String.format("Restarting node with: %s", String.join(" ", javaCmd)));
|
||||
|
||||
ProcessBuilder processBuilder = new ProcessBuilder(javaCmd);
|
||||
|
||||
if (Files.exists(exeLauncher)) {
|
||||
System.out.println(String.format("Setting env %s to %s", JAVA_TOOL_OPTIONS_NAME, JAVA_TOOL_OPTIONS_VALUE));
|
||||
processBuilder.environment().put(JAVA_TOOL_OPTIONS_NAME, JAVA_TOOL_OPTIONS_VALUE);
|
||||
}
|
||||
|
||||
processBuilder.start();
|
||||
} catch (IOException e) {
|
||||
System.err.println(String.format("Failed to restart node (BAD): %s", e.getMessage()));
|
||||
}
|
||||
}
|
||||
|
||||
}
|
@@ -12,6 +12,7 @@ import org.junit.Before;
|
||||
import org.junit.Test;
|
||||
import org.qortal.crosschain.BTC;
|
||||
import org.qortal.crosschain.BTCP2SH;
|
||||
import org.qortal.crosschain.BitcoinException;
|
||||
import org.qortal.repository.DataException;
|
||||
import org.qortal.test.common.Common;
|
||||
|
||||
@@ -28,7 +29,7 @@ public class BtcTests extends Common {
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testGetMedianBlockTime() throws BlockStoreException {
|
||||
public void testGetMedianBlockTime() throws BlockStoreException, BitcoinException {
|
||||
System.out.println(String.format("Starting BTC instance..."));
|
||||
BTC btc = BTC.getInstance();
|
||||
System.out.println(String.format("BTC instance started"));
|
||||
@@ -50,7 +51,7 @@ public class BtcTests extends Common {
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testFindP2shSecret() {
|
||||
public void testFindP2shSecret() throws BitcoinException {
|
||||
// This actually exists on TEST3 but can take a while to fetch
|
||||
String p2shAddress = "2N8WCg52ULCtDSMjkgVTm5mtPdCsUptkHWE";
|
||||
|
||||
@@ -104,4 +105,17 @@ public class BtcTests extends Common {
|
||||
assertEquals(balance, repeatBalance);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testGetUnusedReceiveAddress() throws BitcoinException {
|
||||
BTC btc = BTC.getInstance();
|
||||
|
||||
String xprv58 = "tprv8ZgxMBicQKsPdahhFSrCdvC1bsWyzHHZfTneTVqUXN6s1wEtZLwAkZXzFP6TYLg2aQMecZLXLre5bTVGajEB55L1HYJcawpdFG66STVAWPJ";
|
||||
|
||||
String address = btc.getUnusedReceiveAddress(xprv58);
|
||||
|
||||
assertNotNull(address);
|
||||
|
||||
System.out.println(address);
|
||||
}
|
||||
|
||||
}
|
||||
|
@@ -16,6 +16,7 @@ import org.bouncycastle.jce.provider.BouncyCastleProvider;
|
||||
import org.qortal.controller.Controller;
|
||||
import org.qortal.crosschain.BTC;
|
||||
import org.qortal.crosschain.BTCP2SH;
|
||||
import org.qortal.crosschain.BitcoinException;
|
||||
import org.qortal.crypto.Crypto;
|
||||
import org.qortal.repository.DataException;
|
||||
import org.qortal.repository.Repository;
|
||||
@@ -135,11 +136,7 @@ public class CheckP2SH {
|
||||
System.out.println(String.format("Too soon (%s) to redeem based on median block time %s", LocalDateTime.ofInstant(Instant.ofEpochMilli(now), ZoneOffset.UTC), LocalDateTime.ofInstant(Instant.ofEpochSecond(medianBlockTime), ZoneOffset.UTC)));
|
||||
|
||||
// Check P2SH is funded
|
||||
Long p2shBalance = BTC.getInstance().getBalance(p2shAddress.toString());
|
||||
if (p2shBalance == null) {
|
||||
System.err.println(String.format("Unable to check P2SH address %s balance", p2shAddress));
|
||||
System.exit(2);
|
||||
}
|
||||
long p2shBalance = BTC.getInstance().getConfirmedBalance(p2shAddress.toString());
|
||||
System.out.println(String.format("P2SH address %s balance: %s", p2shAddress, BTC.format(p2shBalance)));
|
||||
|
||||
// Grab all P2SH funding transactions (just in case there are more than one)
|
||||
@@ -164,7 +161,9 @@ public class CheckP2SH {
|
||||
System.exit(2);
|
||||
}
|
||||
} catch (DataException e) {
|
||||
throw new RuntimeException("Repository issue: " + e.getMessage());
|
||||
System.err.println("Repository issue: " + e.getMessage());
|
||||
} catch (BitcoinException e) {
|
||||
System.err.println("Bitcoin issue: " + e.getMessage());
|
||||
}
|
||||
}
|
||||
|
||||
|
@@ -11,8 +11,11 @@ import org.bitcoinj.script.ScriptBuilder;
|
||||
import org.bouncycastle.jce.provider.BouncyCastleProvider;
|
||||
import org.bouncycastle.jsse.provider.BouncyCastleJsseProvider;
|
||||
import org.junit.Test;
|
||||
import org.qortal.crosschain.BitcoinException;
|
||||
import org.qortal.crosschain.BitcoinTransaction;
|
||||
import org.qortal.crosschain.ElectrumX;
|
||||
import org.qortal.crosschain.ElectrumX.UnspentOutput;
|
||||
import org.qortal.crosschain.TransactionHash;
|
||||
import org.qortal.crosschain.UnspentOutput;
|
||||
import org.qortal.utils.BitTwiddling;
|
||||
|
||||
import com.google.common.hash.HashCode;
|
||||
@@ -34,26 +37,36 @@ public class ElectrumXTests {
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testGetCurrentHeight() {
|
||||
public void testGetCurrentHeight() throws BitcoinException {
|
||||
ElectrumX electrumX = ElectrumX.getInstance("TEST3");
|
||||
|
||||
Integer height = electrumX.getCurrentHeight();
|
||||
int height = electrumX.getCurrentHeight();
|
||||
|
||||
assertNotNull(height);
|
||||
assertTrue(height > 10000);
|
||||
System.out.println("Current TEST3 height: " + height);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testGetRecentBlocks() {
|
||||
public void testInvalidRequest() {
|
||||
ElectrumX electrumX = ElectrumX.getInstance("TEST3");
|
||||
try {
|
||||
electrumX.getBlockHeaders(-1, -1);
|
||||
} catch (BitcoinException e) {
|
||||
// Should throw due to negative start block height
|
||||
return;
|
||||
}
|
||||
|
||||
fail("Negative start block height should cause error");
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testGetRecentBlocks() throws BitcoinException {
|
||||
ElectrumX electrumX = ElectrumX.getInstance("TEST3");
|
||||
|
||||
Integer height = electrumX.getCurrentHeight();
|
||||
assertNotNull(height);
|
||||
int height = electrumX.getCurrentHeight();
|
||||
assertTrue(height > 10000);
|
||||
|
||||
List<byte[]> recentBlockHeaders = electrumX.getBlockHeaders(height - 11, 11);
|
||||
assertNotNull(recentBlockHeaders);
|
||||
|
||||
System.out.println(String.format("Returned %d recent blocks", recentBlockHeaders.size()));
|
||||
for (int i = 0; i < recentBlockHeaders.size(); ++i) {
|
||||
@@ -67,42 +80,39 @@ public class ElectrumXTests {
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testGetP2PKHBalance() {
|
||||
public void testGetP2PKHBalance() throws BitcoinException {
|
||||
ElectrumX electrumX = ElectrumX.getInstance("TEST3");
|
||||
|
||||
Address address = Address.fromString(TestNet3Params.get(), "n3GNqMveyvaPvUbH469vDRadqpJMPc84JA");
|
||||
byte[] script = ScriptBuilder.createOutputScript(address).getProgram();
|
||||
Long balance = electrumX.getBalance(script);
|
||||
long balance = electrumX.getConfirmedBalance(script);
|
||||
|
||||
assertNotNull(balance);
|
||||
assertTrue(balance > 0L);
|
||||
|
||||
System.out.println(String.format("TestNet address %s has balance: %d sats / %d.%08d BTC", address, balance, (balance / 100000000L), (balance % 100000000L)));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testGetP2SHBalance() {
|
||||
public void testGetP2SHBalance() throws BitcoinException {
|
||||
ElectrumX electrumX = ElectrumX.getInstance("TEST3");
|
||||
|
||||
Address address = Address.fromString(TestNet3Params.get(), "2N4szZUfigj7fSBCEX4PaC8TVbC5EvidaVF");
|
||||
byte[] script = ScriptBuilder.createOutputScript(address).getProgram();
|
||||
Long balance = electrumX.getBalance(script);
|
||||
long balance = electrumX.getConfirmedBalance(script);
|
||||
|
||||
assertNotNull(balance);
|
||||
assertTrue(balance > 0L);
|
||||
|
||||
System.out.println(String.format("TestNet address %s has balance: %d sats / %d.%08d BTC", address, balance, (balance / 100000000L), (balance % 100000000L)));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testGetUnspentOutputs() {
|
||||
public void testGetUnspentOutputs() throws BitcoinException {
|
||||
ElectrumX electrumX = ElectrumX.getInstance("TEST3");
|
||||
|
||||
Address address = Address.fromString(TestNet3Params.get(), "2N4szZUfigj7fSBCEX4PaC8TVbC5EvidaVF");
|
||||
byte[] script = ScriptBuilder.createOutputScript(address).getProgram();
|
||||
List<UnspentOutput> unspentOutputs = electrumX.getUnspentOutputs(script);
|
||||
List<UnspentOutput> unspentOutputs = electrumX.getUnspentOutputs(script, false);
|
||||
|
||||
assertNotNull(unspentOutputs);
|
||||
assertFalse(unspentOutputs.isEmpty());
|
||||
|
||||
for (UnspentOutput unspentOutput : unspentOutputs)
|
||||
@@ -110,27 +120,68 @@ public class ElectrumXTests {
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testGetRawTransaction() {
|
||||
public void testGetRawTransaction() throws BitcoinException {
|
||||
ElectrumX electrumX = ElectrumX.getInstance("TEST3");
|
||||
|
||||
byte[] txHash = HashCode.fromString("7653fea9ffcd829d45ed2672938419a94951b08175982021e77d619b553f29af").asBytes();
|
||||
|
||||
byte[] rawTransactionBytes = electrumX.getRawTransaction(txHash);
|
||||
|
||||
assertNotNull(rawTransactionBytes);
|
||||
assertFalse(rawTransactionBytes.length == 0);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testGetAddressTransactions() {
|
||||
public void testGetUnknownRawTransaction() {
|
||||
ElectrumX electrumX = ElectrumX.getInstance("TEST3");
|
||||
|
||||
byte[] txHash = HashCode.fromString("f0f0f0f0f0f0f0f0f0f0f0f0f0f0f0f0f0f0f0f0f0f0f0f0f0f0f0f0f0f0f0f0").asBytes();
|
||||
|
||||
try {
|
||||
electrumX.getRawTransaction(txHash);
|
||||
fail("Bitcoin transaction should be unknown and hence throw exception");
|
||||
} catch (BitcoinException e) {
|
||||
if (!(e instanceof BitcoinException.NotFoundException))
|
||||
fail("Bitcoin transaction should be unknown and hence throw NotFoundException");
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testGetTransaction() throws BitcoinException {
|
||||
ElectrumX electrumX = ElectrumX.getInstance("TEST3");
|
||||
|
||||
String txHash = "7653fea9ffcd829d45ed2672938419a94951b08175982021e77d619b553f29af";
|
||||
|
||||
BitcoinTransaction transaction = electrumX.getTransaction(txHash);
|
||||
|
||||
assertNotNull(transaction);
|
||||
assertTrue(transaction.txHash.equals(txHash));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testGetUnknownTransaction() {
|
||||
ElectrumX electrumX = ElectrumX.getInstance("TEST3");
|
||||
|
||||
String txHash = "f0f0f0f0f0f0f0f0f0f0f0f0f0f0f0f0f0f0f0f0f0f0f0f0f0f0f0f0f0f0f0f0";
|
||||
|
||||
try {
|
||||
electrumX.getTransaction(txHash);
|
||||
fail("Bitcoin transaction should be unknown and hence throw exception");
|
||||
} catch (BitcoinException e) {
|
||||
if (!(e instanceof BitcoinException.NotFoundException))
|
||||
fail("Bitcoin transaction should be unknown and hence throw NotFoundException");
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testGetAddressTransactions() throws BitcoinException {
|
||||
ElectrumX electrumX = ElectrumX.getInstance("TEST3");
|
||||
|
||||
Address address = Address.fromString(TestNet3Params.get(), "2N8WCg52ULCtDSMjkgVTm5mtPdCsUptkHWE");
|
||||
byte[] script = ScriptBuilder.createOutputScript(address).getProgram();
|
||||
|
||||
List<byte[]> rawTransactions = electrumX.getAddressTransactions(script);
|
||||
List<TransactionHash> transactionHashes = electrumX.getAddressTransactions(script, false);
|
||||
|
||||
assertNotNull(rawTransactions);
|
||||
assertFalse(rawTransactions.isEmpty());
|
||||
assertFalse(transactionHashes.isEmpty());
|
||||
}
|
||||
|
||||
}
|
||||
|
@@ -7,6 +7,7 @@ import org.bitcoinj.core.AddressFormatException;
|
||||
import org.bitcoinj.core.TransactionOutput;
|
||||
import org.bouncycastle.jce.provider.BouncyCastleProvider;
|
||||
import org.qortal.crosschain.BTC;
|
||||
import org.qortal.crosschain.BitcoinException;
|
||||
import org.qortal.settings.Settings;
|
||||
|
||||
import com.google.common.hash.HashCode;
|
||||
@@ -46,9 +47,11 @@ public class GetTransaction {
|
||||
}
|
||||
|
||||
// Grab all outputs from transaction
|
||||
List<TransactionOutput> fundingOutputs = BTC.getInstance().getOutputs(transactionId);
|
||||
if (fundingOutputs == null) {
|
||||
System.out.println(String.format("Transaction not found"));
|
||||
List<TransactionOutput> fundingOutputs;
|
||||
try {
|
||||
fundingOutputs = BTC.getInstance().getOutputs(transactionId);
|
||||
} catch (BitcoinException e) {
|
||||
System.out.println(String.format("Transaction not found (or error occurred)"));
|
||||
return;
|
||||
}
|
||||
|
||||
|
53
src/test/java/org/qortal/test/btcacct/P2shTests.java
Normal file
53
src/test/java/org/qortal/test/btcacct/P2shTests.java
Normal file
@@ -0,0 +1,53 @@
|
||||
package org.qortal.test.btcacct;
|
||||
|
||||
import static org.junit.Assert.*;
|
||||
|
||||
import java.util.Arrays;
|
||||
import java.util.List;
|
||||
|
||||
import org.junit.After;
|
||||
import org.junit.Before;
|
||||
import org.junit.Test;
|
||||
import org.qortal.crosschain.BTC;
|
||||
import org.qortal.crosschain.BTCP2SH;
|
||||
import org.qortal.crosschain.BitcoinException;
|
||||
import org.qortal.repository.DataException;
|
||||
import org.qortal.test.common.Common;
|
||||
|
||||
public class P2shTests extends Common {
|
||||
|
||||
@Before
|
||||
public void beforeTest() throws DataException {
|
||||
Common.useDefaultSettings(); // TestNet3
|
||||
}
|
||||
|
||||
@After
|
||||
public void afterTest() {
|
||||
BTC.resetForTesting();
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testFindP2shSecret() throws BitcoinException {
|
||||
// This actually exists on TEST3 but can take a while to fetch
|
||||
String p2shAddress = "2N8WCg52ULCtDSMjkgVTm5mtPdCsUptkHWE";
|
||||
|
||||
List<byte[]> rawTransactions = BTC.getInstance().getAddressTransactions(p2shAddress);
|
||||
|
||||
byte[] expectedSecret = "This string is exactly 32 bytes!".getBytes();
|
||||
byte[] secret = BTCP2SH.findP2shSecret(p2shAddress, rawTransactions);
|
||||
|
||||
assertNotNull(secret);
|
||||
assertTrue("secret incorrect", Arrays.equals(expectedSecret, secret));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testDetermineP2shStatus() throws BitcoinException {
|
||||
// This actually exists on TEST3 but can take a while to fetch
|
||||
String p2shAddress = "2N8WCg52ULCtDSMjkgVTm5mtPdCsUptkHWE";
|
||||
|
||||
BTCP2SH.Status p2shStatus = BTCP2SH.determineP2shStatus(p2shAddress, 1L);
|
||||
|
||||
System.out.println(String.format("P2SH %s status: %s", p2shAddress, p2shStatus.name()));
|
||||
}
|
||||
|
||||
}
|
@@ -19,6 +19,7 @@ import org.bouncycastle.jce.provider.BouncyCastleProvider;
|
||||
import org.qortal.controller.Controller;
|
||||
import org.qortal.crosschain.BTC;
|
||||
import org.qortal.crosschain.BTCP2SH;
|
||||
import org.qortal.crosschain.BitcoinException;
|
||||
import org.qortal.crypto.Crypto;
|
||||
import org.qortal.repository.DataException;
|
||||
import org.qortal.repository.Repository;
|
||||
@@ -136,7 +137,14 @@ public class Redeem {
|
||||
|
||||
System.out.println("\nProcessing:");
|
||||
|
||||
long medianBlockTime = BTC.getInstance().getMedianBlockTime();
|
||||
long medianBlockTime;
|
||||
try {
|
||||
medianBlockTime = BTC.getInstance().getMedianBlockTime();
|
||||
} catch (BitcoinException e1) {
|
||||
System.err.println("Unable to determine median block time");
|
||||
System.exit(2);
|
||||
return;
|
||||
}
|
||||
System.out.println(String.format("Median block time: %s", LocalDateTime.ofInstant(Instant.ofEpochSecond(medianBlockTime), ZoneOffset.UTC)));
|
||||
|
||||
long now = System.currentTimeMillis();
|
||||
@@ -147,18 +155,24 @@ public class Redeem {
|
||||
}
|
||||
|
||||
// Check P2SH is funded
|
||||
Long p2shBalance = BTC.getInstance().getBalance(p2shAddress.toString());
|
||||
if (p2shBalance == null) {
|
||||
long p2shBalance;
|
||||
try {
|
||||
p2shBalance = BTC.getInstance().getConfirmedBalance(p2shAddress.toString());
|
||||
} catch (BitcoinException e) {
|
||||
System.err.println(String.format("Unable to check P2SH address %s balance", p2shAddress));
|
||||
System.exit(2);
|
||||
return;
|
||||
}
|
||||
System.out.println(String.format("P2SH address %s balance: %s", p2shAddress, BTC.format(p2shBalance)));
|
||||
|
||||
// Grab all P2SH funding transactions (just in case there are more than one)
|
||||
List<TransactionOutput> fundingOutputs = BTC.getInstance().getUnspentOutputs(p2shAddress.toString());
|
||||
if (fundingOutputs == null) {
|
||||
List<TransactionOutput> fundingOutputs;
|
||||
try {
|
||||
fundingOutputs = BTC.getInstance().getUnspentOutputs(p2shAddress.toString());
|
||||
} catch (BitcoinException e) {
|
||||
System.err.println(String.format("Can't find outputs for P2SH"));
|
||||
System.exit(2);
|
||||
return;
|
||||
}
|
||||
|
||||
System.out.println(String.format("Found %d output%s for P2SH", fundingOutputs.size(), (fundingOutputs.size() != 1 ? "s" : "")));
|
||||
|
@@ -19,6 +19,7 @@ import org.bouncycastle.jce.provider.BouncyCastleProvider;
|
||||
import org.qortal.controller.Controller;
|
||||
import org.qortal.crosschain.BTC;
|
||||
import org.qortal.crosschain.BTCP2SH;
|
||||
import org.qortal.crosschain.BitcoinException;
|
||||
import org.qortal.crypto.Crypto;
|
||||
import org.qortal.repository.DataException;
|
||||
import org.qortal.repository.Repository;
|
||||
@@ -135,7 +136,14 @@ public class Refund {
|
||||
|
||||
System.out.println("\nProcessing:");
|
||||
|
||||
long medianBlockTime = BTC.getInstance().getMedianBlockTime();
|
||||
long medianBlockTime;
|
||||
try {
|
||||
medianBlockTime = BTC.getInstance().getMedianBlockTime();
|
||||
} catch (BitcoinException e) {
|
||||
System.err.println("Unable to determine median block time");
|
||||
System.exit(2);
|
||||
return;
|
||||
}
|
||||
System.out.println(String.format("Median block time: %s", LocalDateTime.ofInstant(Instant.ofEpochSecond(medianBlockTime), ZoneOffset.UTC)));
|
||||
|
||||
long now = System.currentTimeMillis();
|
||||
@@ -151,18 +159,24 @@ public class Refund {
|
||||
}
|
||||
|
||||
// Check P2SH is funded
|
||||
Long p2shBalance = BTC.getInstance().getBalance(p2shAddress.toString());
|
||||
if (p2shBalance == null) {
|
||||
long p2shBalance;
|
||||
try {
|
||||
p2shBalance = BTC.getInstance().getConfirmedBalance(p2shAddress.toString());
|
||||
} catch (BitcoinException e) {
|
||||
System.err.println(String.format("Unable to check P2SH address %s balance", p2shAddress));
|
||||
System.exit(2);
|
||||
return;
|
||||
}
|
||||
System.out.println(String.format("P2SH address %s balance: %s", p2shAddress, BTC.format(p2shBalance)));
|
||||
|
||||
// Grab all P2SH funding transactions (just in case there are more than one)
|
||||
List<TransactionOutput> fundingOutputs = BTC.getInstance().getUnspentOutputs(p2shAddress.toString());
|
||||
if (fundingOutputs == null) {
|
||||
List<TransactionOutput> fundingOutputs;
|
||||
try {
|
||||
fundingOutputs = BTC.getInstance().getUnspentOutputs(p2shAddress.toString());
|
||||
} catch (BitcoinException e) {
|
||||
System.err.println(String.format("Can't find outputs for P2SH"));
|
||||
System.exit(2);
|
||||
return;
|
||||
}
|
||||
|
||||
System.out.println(String.format("Found %d output%s for P2SH", fundingOutputs.size(), (fundingOutputs.size() != 1 ? "s" : "")));
|
||||
@@ -186,7 +200,7 @@ public class Refund {
|
||||
Coin refundAmount = Coin.valueOf(p2shBalance).subtract(bitcoinFee);
|
||||
System.out.println(String.format("Spending %s of output, with %s as mining fee", BTC.format(refundAmount), BTC.format(bitcoinFee)));
|
||||
|
||||
Transaction redeemTransaction = BTCP2SH.buildRefundTransaction(refundAmount, refundKey, fundingOutputs, redeemScriptBytes, lockTime);
|
||||
Transaction redeemTransaction = BTCP2SH.buildRefundTransaction(refundAmount, refundKey, fundingOutputs, redeemScriptBytes, lockTime, refundKey.getPubKeyHash());
|
||||
|
||||
byte[] redeemBytes = redeemTransaction.bitcoinSerialize();
|
||||
|
||||
|
@@ -1,16 +1,30 @@
|
||||
package org.qortal.test.common;
|
||||
|
||||
import static org.junit.Assert.*;
|
||||
|
||||
import java.lang.reflect.Field;
|
||||
|
||||
import org.eclipse.jetty.server.Request;
|
||||
import org.junit.Before;
|
||||
import org.qortal.api.ApiError;
|
||||
import org.qortal.api.ApiException;
|
||||
import org.qortal.repository.DataException;
|
||||
|
||||
public class ApiCommon extends Common {
|
||||
|
||||
public static final long MAX_API_RESPONSE_PERIOD = 2_000L; // ms
|
||||
|
||||
public static final Boolean[] ALL_BOOLEAN_VALUES = new Boolean[] { null, true, false };
|
||||
public static final Boolean[] TF_BOOLEAN_VALUES = new Boolean[] { true, false };
|
||||
|
||||
public static final Integer[] SAMPLE_LIMIT_VALUES = new Integer[] { null, 0, 1, 20 };
|
||||
public static final Integer[] SAMPLE_OFFSET_VALUES = new Integer[] { null, 0, 1, 5 };
|
||||
|
||||
@FunctionalInterface
|
||||
public interface SlicedApiCall {
|
||||
public abstract void call(Integer limit, Integer offset, Boolean reverse);
|
||||
}
|
||||
|
||||
public static class FakeRequest extends Request {
|
||||
public FakeRequest() {
|
||||
super(null, null);
|
||||
@@ -48,4 +62,50 @@ public class ApiCommon extends Common {
|
||||
}
|
||||
}
|
||||
|
||||
public static void assertApiError(ApiError expectedApiError, Runnable apiCall, Long maxResponsePeriod) {
|
||||
try {
|
||||
long beforeTimestamp = System.currentTimeMillis();
|
||||
apiCall.run();
|
||||
|
||||
if (maxResponsePeriod != null) {
|
||||
long responsePeriod = System.currentTimeMillis() - beforeTimestamp;
|
||||
if (responsePeriod > maxResponsePeriod)
|
||||
fail(String.format("API call response period %d ms greater than max allowed (%d ms)", responsePeriod, maxResponsePeriod));
|
||||
}
|
||||
} catch (ApiException e) {
|
||||
ApiError actualApiError = ApiError.fromCode(e.error);
|
||||
assertEquals(expectedApiError, actualApiError);
|
||||
}
|
||||
}
|
||||
|
||||
public static void assertApiError(ApiError expectedApiError, Runnable apiCall) {
|
||||
assertApiError(expectedApiError, apiCall, MAX_API_RESPONSE_PERIOD);
|
||||
}
|
||||
|
||||
public static void assertNoApiError(Runnable apiCall, Long maxResponsePeriod) {
|
||||
try {
|
||||
long beforeTimestamp = System.currentTimeMillis();
|
||||
apiCall.run();
|
||||
|
||||
if (maxResponsePeriod != null) {
|
||||
long responsePeriod = System.currentTimeMillis() - beforeTimestamp;
|
||||
if (responsePeriod > maxResponsePeriod)
|
||||
fail(String.format("API call response period %d ms greater than max allowed (%d ms)", responsePeriod, maxResponsePeriod));
|
||||
}
|
||||
} catch (ApiException e) {
|
||||
fail("ApiException unexpected");
|
||||
}
|
||||
}
|
||||
|
||||
public static void assertNoApiError(Runnable apiCall) {
|
||||
assertNoApiError(apiCall, MAX_API_RESPONSE_PERIOD);
|
||||
}
|
||||
|
||||
public static void assertNoApiError(SlicedApiCall apiCall) {
|
||||
for (Integer limit : SAMPLE_LIMIT_VALUES)
|
||||
for (Integer offset : SAMPLE_OFFSET_VALUES)
|
||||
for (Boolean reverse : ALL_BOOLEAN_VALUES)
|
||||
assertNoApiError(() -> apiCall.call(limit, offset, reverse));
|
||||
}
|
||||
|
||||
}
|
||||
|
@@ -5,7 +5,7 @@ import org.apache.logging.log4j.Logger;
|
||||
import org.qortal.account.PrivateKeyAccount;
|
||||
import org.qortal.block.Block;
|
||||
import org.qortal.block.BlockChain;
|
||||
import org.qortal.block.BlockMinter;
|
||||
import org.qortal.controller.BlockMinter;
|
||||
import org.qortal.data.block.BlockData;
|
||||
import org.qortal.repository.DataException;
|
||||
import org.qortal.repository.Repository;
|
||||
|
@@ -7,7 +7,7 @@ import org.junit.After;
|
||||
import org.junit.Before;
|
||||
import org.junit.Test;
|
||||
import org.qortal.account.PrivateKeyAccount;
|
||||
import org.qortal.block.BlockMinter;
|
||||
import org.qortal.controller.BlockMinter;
|
||||
import org.qortal.controller.Controller;
|
||||
import org.qortal.data.account.RewardShareData;
|
||||
import org.qortal.repository.DataException;
|
||||
|
@@ -10,7 +10,7 @@ import org.junit.After;
|
||||
import org.junit.Before;
|
||||
import org.junit.Test;
|
||||
import org.qortal.account.PrivateKeyAccount;
|
||||
import org.qortal.block.BlockMinter;
|
||||
import org.qortal.controller.BlockMinter;
|
||||
import org.qortal.controller.Controller;
|
||||
import org.qortal.data.account.RewardShareData;
|
||||
import org.qortal.data.block.BlockData;
|
||||
|
@@ -121,7 +121,7 @@ public class RewardShareTests extends Common {
|
||||
Transaction transaction = Transaction.fromData(repository, transactionData);
|
||||
|
||||
ValidationResult validationResult = transaction.isValidUnconfirmed();
|
||||
assertEquals("Initial 0% share should be invalid", ValidationResult.INVALID_REWARD_SHARE_PERCENT, validationResult);
|
||||
assertNotSame("Creating reward-share with 'cancel' share-percent should be invalid", ValidationResult.OK, validationResult);
|
||||
}
|
||||
}
|
||||
|
||||
|
@@ -14,8 +14,8 @@ import org.junit.Test;
|
||||
import org.qortal.account.PrivateKeyAccount;
|
||||
import org.qortal.asset.Asset;
|
||||
import org.qortal.block.BlockChain;
|
||||
import org.qortal.block.BlockMinter;
|
||||
import org.qortal.block.BlockChain.RewardByHeight;
|
||||
import org.qortal.controller.BlockMinter;
|
||||
import org.qortal.data.account.AccountBalanceData;
|
||||
import org.qortal.repository.DataException;
|
||||
import org.qortal.repository.Repository;
|
||||
|
50
start.sh
Executable file
50
start.sh
Executable file
@@ -0,0 +1,50 @@
|
||||
#!/bin/sh
|
||||
|
||||
# There's no need to run as root, so don't allow it, for security reasons
|
||||
if [ "$USER" = "root" ]; then
|
||||
echo "Please su to non-root user before running"
|
||||
exit
|
||||
fi
|
||||
|
||||
# Validate Java is installed and the minimum version is available
|
||||
MIN_JAVA_VER='11'
|
||||
|
||||
if command -v java > /dev/null 2>&1; then
|
||||
# Example: openjdk version "11.0.6" 2020-01-14
|
||||
version=$(java -version 2>&1 | awk -F '"' '/version/ {print $2}' | cut -d'.' -f1,2)
|
||||
if echo "${version}" "${MIN_JAVA_VER}" | awk '{ if ($2 > 0 && $1 >= $2) exit 0; else exit 1}'; then
|
||||
echo 'Passed Java version check'
|
||||
else
|
||||
echo "Please upgrade your Java to version ${MIN_JAVA_VER} or greater"
|
||||
exit 1
|
||||
fi
|
||||
else
|
||||
echo "Java is not available, please install Java ${MIN_JAVA_VER} or greater"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
# No qortal.jar but we have a Maven built one?
|
||||
# Be helpful and copy across to correct location
|
||||
if [ ! -e qortal.jar -a -f target/qortal*.jar ]; then
|
||||
echo "Copying Maven-built Qortal JAR to correct pathname"
|
||||
cp target/qortal*.jar qortal.jar
|
||||
fi
|
||||
|
||||
# Limits Java JVM stack size and maximum heap usage.
|
||||
# Comment out for bigger systems, e.g. non-routers
|
||||
# or when API documentation is enabled
|
||||
# JVM_MEMORY_ARGS="-Xss256k -Xmx128m"
|
||||
|
||||
# Although java.net.preferIPv4Stack is supposed to be false
|
||||
# by default in Java 11, on some platforms (e.g. FreeBSD 12),
|
||||
# it is overridden to be true by default. Hence we explicitly
|
||||
# set it to false to obtain desired behaviour.
|
||||
nohup nice -n 20 java \
|
||||
-Djava.net.preferIPv4Stack=false \
|
||||
${JVM_MEMORY_ARGS} \
|
||||
-jar qortal.jar \
|
||||
1>run.log 2>&1 &
|
||||
|
||||
# Save backgrounded process's PID
|
||||
echo $! > run.pid
|
||||
echo qortal running as pid $!
|
@@ -2,6 +2,12 @@
|
||||
|
||||
set -e
|
||||
|
||||
# Optional git tag?
|
||||
if [ $# -ge 1 ]; then
|
||||
git_tag="$1"
|
||||
shift
|
||||
fi
|
||||
|
||||
saved_pwd=$PWD
|
||||
|
||||
# Check we are within a git repo
|
||||
@@ -31,10 +37,12 @@ if [ -z "${project}" ]; then
|
||||
fi
|
||||
|
||||
# Extract git tag
|
||||
git_tag=$( git tag --points-at HEAD )
|
||||
if [ -z "${git_tag}" ]; then
|
||||
echo "Unable to extract git tag"
|
||||
exit 1
|
||||
git_tag=$( git tag --points-at HEAD )
|
||||
if [ -z "${git_tag}" ]; then
|
||||
echo "Unable to extract git tag"
|
||||
exit 1
|
||||
fi
|
||||
fi
|
||||
|
||||
build_dir=/tmp/${project}
|
||||
@@ -47,7 +55,8 @@ cp target/${project}*.jar ${build_dir}/${project}.jar
|
||||
|
||||
git show HEAD:log4j2.properties > ${build_dir}/log4j2.properties
|
||||
|
||||
git show HEAD:run.sh > ${build_dir}/run.sh
|
||||
git show HEAD:start.sh > ${build_dir}/start.sh
|
||||
git show HEAD:stop.sh > ${build_dir}/stop.sh
|
||||
|
||||
printf "{\n}\n" > ${build_dir}/settings.json
|
||||
|
||||
|
Reference in New Issue
Block a user