Compare commits

...

35 Commits

Author SHA1 Message Date
catbref
5b95f3af02 Bump to v1.3.5 2020-08-31 11:53:12 +01:00
catbref
3cc66609e8 Trial implementation of offline repository periodic maintenance.
Requires node shutdown, lots of time (10s of minutes), spare storage space.
Called via: java -cp qortal.jar org.qortal.RepositoryMaintenance
Not (yet) for general consumption.
2020-08-31 11:51:38 +01:00
catbref
ce468d22dd Fix updating of current tradeoffers list as used by tradeoffers websocket 2020-08-31 11:25:21 +01:00
catbref
3e19516f62 Correct poor synchronization on NTP offset, potentially fixing issue #22 2020-08-31 10:24:10 +01:00
catbref
84dba739d9 Give up on cross-chain trade if initial AT doesn't confirm within 24 hours 2020-08-31 09:21:15 +01:00
catbref
99315c7378 Correct wrong source for lockTimeA when Bob waiting for P2SH-B. Spotted by tcallahan14. In lieu of PR #23 2020-08-31 09:14:15 +01:00
catbref
1ca5b864a9 Repository optimizations!
Added Qortal-side HSQLDB PreparedStatement cache, hashed
by SQL query string, to reduce re-preparing statements.
(HSQLDB actually does the work in avoiding re-preparing
by comparing its own query-to-statement cache map, but we
need to keep an 'open' statement on our side for this to
happen).

Support added for batched INSERT/UPDATE SQL statements to
update many rows in one call.

Several specific repository calls, e.g. modifyMintedBlockCount
or modifyAssetBalance, now have batch versions that allow
many rows to be updated in one call.

In Block, when distributing block rewards, although we still
build a map of balance changes to apply after all calculations,
this map is now handed off wholesale to the repository to
apply in one (or two) queries, instead of a repository call
per account. The balanceChanges map is now keyed by account
address, as opposed to actual Account.

Also in Block, we try to cache the fetched online reward-shares
(typically in Block.isValid et al) to avoid re-fetching them
later when calculating block rewards.

In addition, actually fetching online reward-shares is no longer
done index-by-index, but the whole array of indexes is passed
wholesale to the repository which then returns the corresponding
reward-shares as a list.

In Block.increaseAccountLevels, blocks minted counts are also
updated in one single repository call, rather than one
repository call per account.

When distributing Block rewards to legacy QORA holders,
all necessary info is fetched from the repository in one hit
instead of two-phases of: 1. fetching eligible QORA holders,
and 2. fetching extra data for that QORA holder as needed.

In addition, updated QORT_FROM_QORA asset balances are done
via one batch repository call, rather than per update.
2020-08-26 17:16:45 +01:00
catbref
96eb60dca3 More HSQLDB tests to cover fixes for various HSQLDB issues, especially when using custom HSQLDB build 2020-08-25 17:02:14 +01:00
catbref
c67fcb0034 Updated AdvancedInstaller project file based on v1.3.4 release 2020-08-24 15:40:29 +01:00
catbref
273dfe2365 Bump to v1.3.4 2020-08-24 15:05:51 +01:00
catbref
5952ea4b54 RU translations thanks to Alexander45 2020-08-24 15:04:33 +01:00
catbref
1708ba077c Actually define static constants for BTC fees until dynamic fees happen 2020-08-24 14:28:54 +01:00
catbref
b4301f125d Potential fix for issue #22 2020-08-24 14:27:03 +01:00
catbref
9e52f20f71 Revert back to HSQLDB v2.5.0-fixed until out-of-memory issue located 2020-08-24 14:07:36 +01:00
catbref
31bf388cab BlockMinter (now under org.qortal.controller package) doesn't need full previous block, only previous block data 2020-08-24 14:04:11 +01:00
catbref
276c479a5f Refactor to allow better Bitcoin fee estimation in the future. 2020-08-21 17:37:04 +01:00
catbref
9393689037 Send BTCACCT refunds to first unused received address instead of address derived from tradePrivateKey.
Added BTC.getUnusedReceiveAddress() to support above.
2020-08-21 17:35:33 +01:00
catbref
76485010ad Merge pull request #16 from tcallahan14/feature/electrum_nodes
Updated Electrum nodes list
2020-08-21 13:34:49 +01:00
catbref
b8ac128d5c Improve comparing chains where some blocks signed with cancelled reward-share
Symptoms include this in logs:

Unexpected zero effective minter level for reward-share %s - using 1 instead!

This occurs when Synchronizer compares two sub-chains from a common block,
and one of the blocks is signed by a reward-share key that has
subsequently been cancelled.

Although this is catered for, excessive log-spam is emited.

So in addition to demoting the log level from WARN to DEBUG,
more code has been added to try harder to find the actual data needed,
thus preventing the logging in the first place.

New repository transaction search method added to support above,
along with corresponding tests.
2020-08-21 12:27:06 +01:00
CalDescent
06c75310a1 Updated Electrum nodes list. All nodes have been tested to ensure they respond to jsonrpc calls. 2020-08-15 16:53:09 +01:00
catbref
b9d819220d Bumped HSQLDB to v2.5.1 and AT/cross-chain SQL speed-ups! 2020-08-15 11:12:10 +01:00
catbref
7a569f342f Reduce confusing BlockMinter log spam - issue #9 2020-08-15 10:52:48 +01:00
catbref
f1efae79c8 Speed-ups for some AT-related SQL queries 2020-08-14 11:54:33 +01:00
catbref
1cd4bbc078 Refactored various websockets to event bus from old BlockNotifier/StatusNotifier 2020-08-14 10:03:51 +01:00
catbref
0b5e5832c4 Added another repository deadlock test while investigating a deadlock case 2020-08-14 09:57:08 +01:00
catbref
7db96c672f Bump to v1.3.3 2020-08-13 14:16:50 +01:00
catbref
f8725d6313 Modify ApplyUpdate to pass JVM options to Windows launcher EXE
ApplyUpdate is the 2nd-stage of the auto-update system, called
after core has downloaded the update.

As old versions of the Windows launcher EXE selects a 'client'
JVM mode, heap memory could be limited to only 256MB.

Until users upgrade via Windows installer, which replaces the EXE
with 'server' JVM mode baked-in, then a work-around is to
pass -XX:MaxRAMFraction=4 to the new JVM in order to emulate
heap size in 'server' JVM mode.
2020-08-13 14:08:47 +01:00
catbref
2165c87b9d Fix race condition between Network.start() and Controller calling Network.prunePeers()
Modified synchronized Lists to be final.
Moved some initializers out of constructor.
2020-08-13 13:45:06 +01:00
catbref
f61e320230 Fix API call GET /crosschain/trades (get completed trades) due to poorly performing SQL query.
Added "minimumTimestamp" param to same API call to allow fetching results for scenarios like:
* completed trades since midnight
* completed trades within last 24 hours

Added corresponding tests for above API call, including checking call response times.
2020-08-13 11:56:08 +01:00
catbref
6c1b21da22 Bump to v1.3.2 2020-08-12 17:30:37 +01:00
catbref
f6216b9745 Respect repositoryBackupInterval when deciding whether to perform backup during auto-update. Issue #10 2020-08-12 14:22:36 +01:00
catbref
91e82d1e3c Add HSQLDB CHECKPOINT statements between database reshaping statements to reduce failures 2020-08-12 14:20:23 +01:00
catbref
50e2bda020 Quicken blockchain validity check at start-up by only checking most recent 1440 blocks 2020-08-12 14:19:14 +01:00
catbref
ab1de1aafa Improve HSQLDB query for finding latest AT state data 2020-08-12 14:18:05 +01:00
catbref
d4ac87f91d Update to more efficient CIYAM AT v1.3.7 2020-08-12 14:17:09 +01:00
62 changed files with 2132 additions and 631 deletions

View File

@@ -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:{DD2EB0DF-3D3A-41B1-85FA-BBC5B8A864E5} 1049:{CD267E1D-38BD-4CEE-8F51-C96CB24B5714} 2052:{11B6C0D1-D9DD-4149-AD84-5FC3CE945DDB} 2057:{3ADB4B92-08C3-4CC4-B5CF-1EBBE22E69B3} " 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.4" 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="{2DFA6687-2747-4EE5-8717-9CFFFF907AA8}" 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="&lt;AI_DICTS&gt;ui.ail"/>
@@ -656,6 +656,7 @@
<ROW Path="&lt;AI_DICTS&gt;ui_en.ail"/>
<ROW Path="&lt;AI_DICTS&gt;ui_zh.ail"/>
<ROW Path="dictionary.ail" Options="1"/>
<ROW Path="&lt;AI_DICTS&gt;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 &lt;&gt; 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
View 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 &quot;Next&quot;."/>
<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 &quot;[Text_Next]&quot;. To store data in a different folder, enter it below or click &quot;Browse&quot;."/>
<STRING lang="ru" value="Чтобы сохранить данные в этой папке, нажмите &quot;[Text_Next]&quot;. Чтобы сохранить данные в другой папке, введите ее ниже или нажмите &quot;Обзор&quot;."/>
<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 &quot;Next&quot;."/>
<STRING lang="ru" value="Выберите один из вариантов ниже, затем нажмите"/>
<STRING lang="zh" value="请选择,然后“下一步”"/>
</ENTRY>
<ENTRY id="Control.Text.NTPDialog#Text_3">
<STRING lang="en" value="Your computer&apos;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>

View File

@@ -4,6 +4,6 @@
<modelVersion>4.0.0</modelVersion>
<groupId>org.ciyam</groupId>
<artifactId>AT</artifactId>
<version>1.3.6</version>
<version>1.3.7</version>
<description>POM was created from install:install-file</description>
</project>

View File

@@ -3,12 +3,13 @@
<groupId>org.ciyam</groupId>
<artifactId>AT</artifactId>
<versioning>
<release>1.3.6</release>
<release>1.3.7</release>
<versions>
<version>1.3.4</version>
<version>1.3.5</version>
<version>1.3.6</version>
<version>1.3.7</version>
</versions>
<lastUpdated>20200810130321</lastUpdated>
<lastUpdated>20200812131412</lastUpdated>
</versioning>
</metadata>

View File

@@ -3,13 +3,13 @@
<modelVersion>4.0.0</modelVersion>
<groupId>org.qortal</groupId>
<artifactId>qortal</artifactId>
<version>1.3.1</version>
<version>1.3.5</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.6</ciyam-at.version>
<ciyam-at.version>1.3.7</ciyam-at.version>
<commons-net.version>3.6</commons-net.version>
<commons-text.version>1.8</commons-text.version>
<dagger.version>1.2.2</dagger.version>

View File

@@ -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()));
}

View 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);
}
}
}

View File

@@ -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() {
}

View File

@@ -25,7 +25,6 @@ import org.qortal.api.ApiError;
import org.qortal.api.ApiErrors;
import org.qortal.api.ApiException;
import org.qortal.api.ApiExceptionFactory;
import org.qortal.at.QortalAtLoggerFactory;
import org.qortal.data.at.ATData;
import org.qortal.data.at.ATStateData;
import org.qortal.data.transaction.DeployAtTransactionData;
@@ -147,8 +146,7 @@ public class AtResource {
ATStateData atStateData = repository.getATRepository().getLatestATState(atAddress);
byte[] stateData = atStateData.getStateData();
QortalAtLoggerFactory loggerFactory = QortalAtLoggerFactory.getInstance();
byte[] dataBytes = MachineState.extractDataBytes(loggerFactory, stateData);
byte[] dataBytes = MachineState.extractDataBytes(stateData);
return dataBytes;
} catch (ApiException e) {

View File

@@ -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;
@@ -718,6 +719,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);
@@ -755,7 +763,7 @@ public class CrossChainResource {
Coin refundAmount = Coin.valueOf(p2shBalance - refundRequest.bitcoinMinerFee.unscaledValue().longValue());
org.bitcoinj.core.Transaction refundTransaction = BTCP2SH.buildRefundTransaction(refundAmount, refundKey, fundingOutputs, redeemScriptBytes, lockTime);
org.bitcoinj.core.Transaction refundTransaction = BTCP2SH.buildRefundTransaction(refundAmount, refundKey, fundingOutputs, redeemScriptBytes, lockTime, refundRequest.receivingAccountInfo);
boolean wasBroadcast = BTC.getInstance().broadcastTransaction(refundTransaction);
if (!wasBroadcast)
@@ -1223,6 +1231,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 +1242,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,

View File

@@ -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?
}
}

View File

@@ -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 {

View File

@@ -6,6 +6,7 @@ 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.eclipse.jetty.websocket.api.Session;
@@ -14,12 +15,15 @@ import org.eclipse.jetty.websocket.api.annotations.OnWebSocketConnect;
import org.eclipse.jetty.websocket.api.annotations.OnWebSocketMessage;
import org.eclipse.jetty.websocket.api.annotations.WebSocket;
import org.eclipse.jetty.websocket.servlet.WebSocketServletFactory;
import org.qortal.api.model.BlockInfo;
import org.qortal.api.model.CrossChainOfferSummary;
import org.qortal.controller.BlockNotifier;
import org.qortal.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 +31,56 @@ import org.qortal.utils.NTP;
@WebSocket
@SuppressWarnings("serial")
public class TradeOffersWebSocket extends ApiWebSocket {
public class TradeOffersWebSocket extends ApiWebSocket implements Listener {
private static final Map<String, BTCACCT.Mode> previousAtModes = new HashMap<>();
// OFFERING
private static final List<CrossChainOfferSummary> currentSummaries = new ArrayList<>();
// REDEEMED/REFUNDED/CANCELLED
private static final List<CrossChainOfferSummary> historicSummaries = new ArrayList<>();
private static final Predicate<CrossChainOfferSummary> isCurrent = offerSummary
-> offerSummary.getMode() == BTCACCT.Mode.OFFERING;
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 +89,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 +103,69 @@ public class TradeOffersWebSocket extends ApiWebSocket {
if (crossChainOfferSummaries.isEmpty())
return;
final boolean wasSent = sendOfferSummaries(session, crossChainOfferSummaries);
if (!wasSent)
return;
// Update
previousAtModes.putAll(crossChainOfferSummaries.stream().collect(Collectors.toMap(CrossChainOfferSummary::getQortalAtAddress, CrossChainOfferSummary::getMode)));
// Find 'historic' (REDEEMED/REFUNDED/CANCELLED) entries for use below:
List<CrossChainOfferSummary> historicOffers = crossChainOfferSummaries.stream().filter(isHistoric).collect(Collectors.toList());
synchronized (currentSummaries) {
// Add any OFFERING to 'current'
currentSummaries.addAll(crossChainOfferSummaries.stream().filter(isCurrent).collect(Collectors.toList()));
// Remove any offers that have become REDEEMED/REFUNDED/CANCELLED
currentSummaries.removeAll(historicOffers);
}
final long tooOldTimestamp = NTP.getTime() - 24 * 60 * 60 * 1000L;
synchronized (historicSummaries) {
// Add any REDEEMED/REFUNDED/CANCELLED
historicSummaries.addAll(historicOffers);
// But also remove any that are over 24 hours old
historicSummaries.removeIf(offerSummary -> offerSummary.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;
synchronized (currentSummaries) {
crossChainOfferSummaries = new ArrayList<>(currentSummaries);
}
if (includeHistoric)
synchronized (historicSummaries) {
crossChainOfferSummaries.addAll(historicSummaries);
}
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);
}
@OnWebSocketMessage
public void onWebSocketMessage(Session session, String message) {
/* ignored */
}
private boolean sendOfferSummaries(Session session, List<CrossChainOfferSummary> crossChainOfferSummaries) {
@@ -186,6 +183,68 @@ 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.addAll(produceSummaries(repository, initialAtStates, null));
}
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);
switch (historicOfferSummary.getMode()) {
case REDEEMED:
case REFUNDED:
case CANCELLED:
break;
default:
continue;
}
// Add summary to initial burst
historicSummaries.add(historicOfferSummary);
// Save initial AT mode
previousAtModes.put(historicAtState.getATAddress(), historicOfferSummary.getMode());
}
}
private static CrossChainOfferSummary produceSummary(Repository repository, ATStateData atState, Long timestamp) throws DataException {
CrossChainTradeData crossChainTradeData = BTCACCT.populateTradeData(repository, atState);

View File

@@ -117,12 +117,8 @@ public class AT {
}
public void update(int blockHeight, long blockTimestamp) throws DataException {
// [Re]create AT machine state using AT state data or from scratch as applicable
QortalATAPI api = new QortalATAPI(repository, this.atData, blockTimestamp);
QortalAtLoggerFactory loggerFactory = QortalAtLoggerFactory.getInstance();
byte[] codeBytes = this.atData.getCodeBytes();
MachineState state = MachineState.fromBytes(api, loggerFactory, this.atStateData.getStateData(), codeBytes);
// Extract minimal/flags-only AT machine state using AT state data
MachineState state = MachineState.flagsOnlyfromBytes(this.atStateData.getStateData());
// Save latest AT state data
this.repository.getATRepository().save(this.atStateData);
@@ -151,12 +147,8 @@ public class AT {
if (previousStateData == null)
throw new DataException("Can't find previous AT state data for " + atAddress);
// [Re]create AT machine state using AT state data or from scratch as applicable
QortalATAPI api = new QortalATAPI(repository, this.atData, blockTimestamp);
QortalAtLoggerFactory loggerFactory = QortalAtLoggerFactory.getInstance();
byte[] codeBytes = this.atData.getCodeBytes();
MachineState state = MachineState.fromBytes(api, loggerFactory, previousStateData.getStateData(), codeBytes);
// Extract minimal/flags-only AT machine state using AT state data
MachineState state = MachineState.flagsOnlyfromBytes(previousStateData.getStateData());
// Update AT info in repository
this.atData.setIsSleeping(state.isSleeping());

View File

@@ -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;
}

View File

@@ -482,7 +482,7 @@ public class BlockChain {
}
/**
* Some sort start-up/initialization/checking method.
* Some sort of start-up/initialization/checking method.
*
* @throws SQLException
*/
@@ -492,7 +492,9 @@ public class BlockChain {
rebuildBlockchain();
try (final Repository repository = RepositoryManager.getRepository()) {
BlockData detachedBlockData = repository.getBlockRepository().getDetachedBlockSignature();
int startHeight = Math.max(repository.getBlockRepository().getBlockchainHeight() - 1440, 1);
BlockData detachedBlockData = repository.getBlockRepository().getDetachedBlockSignature(startHeight);
if (detachedBlockData != null) {
LOGGER.error(String.format("Block %d's reference does not match any block's signature", detachedBlockData.getHeight()));

View File

@@ -214,8 +214,9 @@ public class AutoUpdate extends Thread {
return false; // failed - try another repo
}
// Give repository a chance to backup in case things go badly wrong
RepositoryManager.backup(true);
// Give repository a chance to backup in case things go badly wrong (if enabled)
if (Settings.getInstance().getRepositoryBackupInterval() > 0)
RepositoryManager.backup(true);
// Call ApplyUpdate to end this process (unlocking current JAR so it can be replaced)
String javaHome = System.getProperty("java.home");

View File

@@ -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;

View File

@@ -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());
}
}
}

View File

@@ -36,7 +36,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 +49,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;
@@ -629,6 +630,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"));
@@ -656,7 +662,7 @@ public class Controller extends Thread {
SysTray.getInstance().setToolTipText(tooltip);
this.callbackExecutor.execute(() -> {
StatusNotifier.getInstance().onStatusChange(NTP.getTime());
EventBus.INSTANCE.notify(new StatusChangeEvent());
});
}
@@ -783,6 +789,18 @@ 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;
}
}
public void onNewBlock(BlockData latestBlockData) {
this.setChainTip(latestBlockData);
requestSysTrayUpdate = true;
@@ -792,7 +810,8 @@ public class Controller extends Thread {
Network network = Network.getInstance();
network.broadcast(peer -> network.buildHeightMessage(peer, latestBlockData));
BlockNotifier.getInstance().onNewBlock(latestBlockData);
// Notify listeners of new block
EventBus.INSTANCE.notify(new NewBlockEvent(latestBlockData));
if (this.notifyGroupMembershipChange) {
this.notifyGroupMembershipChange = false;

View File

@@ -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());
}
}
}

View File

@@ -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;
@@ -550,16 +553,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);

View File

@@ -63,7 +63,11 @@ public class TradeBot {
private static final Logger LOGGER = LogManager.getLogger(TradeBot.class);
private static final Random RANDOM = new SecureRandom();
private static final long FEE_AMOUNT = 5000L;
/** Maximum time Bob waits for his AT creation transaction to be confirmed into a block. */
private static final long MAX_AT_CONFIRMATION_PERIOD = 24 * 60 * 60 * 1000L; // ms
private static final long P2SH_B_OUTPUT_AMOUNT = 1000L; // P2SH-B output amount needs to be higher than the dust threshold (3000 sats/kB).
private static TradeBot instance;
@@ -233,7 +237,7 @@ public class TradeBot {
byte[] tradeForeignPublicKeyHash = Crypto.hash160(tradeForeignPublicKey);
byte[] receivingPublicKeyHash = Base58.decode(receivingAddress); // Actually the whole address, not just PKH
// We need to generate lockTime-A: halfway of refundTimeout from now
// We need to generate lockTime-A: add tradeTimeout to now
int lockTimeA = crossChainTradeData.tradeTimeout * 60 + (int) (NTP.getTime() / 1000L);
TradeBotData tradeBotData = new TradeBotData(tradePrivateKey, TradeBotData.State.ALICE_WAITING_FOR_P2SH_A,
@@ -246,7 +250,14 @@ public class TradeBot {
// Check we have enough funds via xprv58 to fund both P2SHs to cover expectedBitcoin
String tradeForeignAddress = BTC.getInstance().pkhToAddress(tradeForeignPublicKeyHash);
long totalFundsRequired = crossChainTradeData.expectedBitcoin + FEE_AMOUNT /* P2SH-A */ + FEE_AMOUNT /* P2SH-B */;
Long estimatedFee = BTC.getInstance().estimateFee(lockTimeA * 1000L);
if (estimatedFee == null) {
LOGGER.debug(() -> String.format("Couldn't estimate Bitcoin fees?"));
return ResponseResult.BTC_NETWORK_ISSUE;
}
long totalFundsRequired = crossChainTradeData.expectedBitcoin + estimatedFee /* P2SH-A */
+ P2SH_B_OUTPUT_AMOUNT + estimatedFee /* P2SH-B */;
Transaction fundingCheckTransaction = BTC.getInstance().buildSpend(xprv58, tradeForeignAddress, totalFundsRequired);
if (fundingCheckTransaction == null)
@@ -257,7 +268,7 @@ public class TradeBot {
String p2shAddress = BTC.getInstance().deriveP2shAddress(redeemScriptBytes);
// Fund P2SH-A
Transaction p2shFundingTransaction = BTC.getInstance().buildSpend(tradeBotData.getXprv58(), p2shAddress, crossChainTradeData.expectedBitcoin + FEE_AMOUNT);
Transaction p2shFundingTransaction = BTC.getInstance().buildSpend(tradeBotData.getXprv58(), p2shAddress, crossChainTradeData.expectedBitcoin + estimatedFee);
if (p2shFundingTransaction == null) {
LOGGER.warn(() -> String.format("Unable to build P2SH-A funding transaction - lack of funds?"));
return ResponseResult.BTC_BALANCE_ISSUE;
@@ -376,8 +387,21 @@ public class TradeBot {
* If AT is deployed, then trade-bot's next step is to wait for MESSAGE from Alice.
*/
private void handleBobWaitingForAtConfirm(Repository repository, TradeBotData tradeBotData) throws DataException {
if (!repository.getATRepository().exists(tradeBotData.getAtAddress()))
if (!repository.getATRepository().exists(tradeBotData.getAtAddress())) {
if (NTP.getTime() - tradeBotData.getTimestamp() <= MAX_AT_CONFIRMATION_PERIOD)
return;
// We've waited ages for AT to be confirmed into a block but something has gone awry.
// After this long we assume transaction loss so give up with trade-bot entry too.
tradeBotData.setState(TradeBotData.State.BOB_REFUNDED);
tradeBotData.setTimestamp(NTP.getTime());
repository.getCrossChainRepository().delete(tradeBotData.getTradePrivateKey());
repository.saveChanges();
LOGGER.info(() -> String.format("AT %s never confirmed. Giving up on trade", tradeBotData.getAtAddress()));
notifyStateChange(tradeBotData);
return;
}
tradeBotData.setState(TradeBotData.State.BOB_WAITING_FOR_MESSAGE);
tradeBotData.setTimestamp(NTP.getTime());
@@ -539,9 +563,23 @@ public class TradeBot {
byte[] redeemScript = BTCP2SH.buildScript(aliceForeignPublicKeyHash, lockTimeA, tradeBotData.getTradeForeignPublicKeyHash(), hashOfSecretA);
String p2shAddress = BTC.getInstance().deriveP2shAddress(redeemScript);
Long estimatedFee = BTC.getInstance().estimateFee(lockTimeA * 1000L);
if (estimatedFee == null) {
LOGGER.debug(() -> String.format("Couldn't estimate Bitcoin fees?"));
// Not worth trying other MESSAGEs... give up for now
return;
}
final long minimumBalance = tradeBotData.getBitcoinAmount() + estimatedFee;
Long balance = BTC.getInstance().getBalance(p2shAddress);
if (balance == null || balance < tradeBotData.getBitcoinAmount())
if (balance == null || balance < minimumBalance) {
// P2SH-A has no, or insufficient, balance
if (balance != null && balance > 0)
LOGGER.debug(() -> String.format("P2SH-A %s balance %s lower than expected %s", p2shAddress, BTC.format(balance), BTC.format(minimumBalance)));
// There might be another MESSAGE from someone else with an actually funded P2SH-A...
continue;
}
// Good to go - send MESSAGE to AT
@@ -694,7 +732,13 @@ public class TradeBot {
byte[] redeemScriptBytes = BTCP2SH.buildScript(tradeBotData.getTradeForeignPublicKeyHash(), lockTimeB, crossChainTradeData.creatorBitcoinPKH, crossChainTradeData.hashOfSecretB);
String p2shAddress = BTC.getInstance().deriveP2shAddress(redeemScriptBytes);
Transaction p2shFundingTransaction = BTC.getInstance().buildSpend(tradeBotData.getXprv58(), p2shAddress, FEE_AMOUNT);
Long estimatedFee = BTC.getInstance().estimateFee(lockTimeA * 1000L);
if (estimatedFee == null) {
LOGGER.debug(() -> String.format("Couldn't estimate Bitcoin fees?"));
return;
}
Transaction p2shFundingTransaction = BTC.getInstance().buildSpend(tradeBotData.getXprv58(), p2shAddress, P2SH_B_OUTPUT_AMOUNT + estimatedFee);
if (p2shFundingTransaction == null) {
LOGGER.warn(() -> String.format("Unable to build P2SH-B funding transaction - lack of funds?"));
return;
@@ -757,16 +801,24 @@ public class TradeBot {
byte[] redeemScriptBytes = BTCP2SH.buildScript(crossChainTradeData.partnerBitcoinPKH, crossChainTradeData.lockTimeB, crossChainTradeData.creatorBitcoinPKH, crossChainTradeData.hashOfSecretB);
String p2shAddress = BTC.getInstance().deriveP2shAddress(redeemScriptBytes);
int lockTimeA = crossChainTradeData.lockTimeA;
Long estimatedFee = BTC.getInstance().estimateFee(lockTimeA * 1000L);
if (estimatedFee == null) {
LOGGER.debug(() -> String.format("Couldn't estimate Bitcoin fees?"));
return;
}
final long minimumBalance = P2SH_B_OUTPUT_AMOUNT + estimatedFee;
Long balance = BTC.getInstance().getBalance(p2shAddress);
if (balance == null || balance < FEE_AMOUNT) {
if (balance == null || balance < minimumBalance) {
if (balance != null && balance > 0)
LOGGER.debug(() -> String.format("P2SH-B balance %s lower than expected %s", BTC.format(balance), BTC.format(FEE_AMOUNT)));
LOGGER.debug(() -> String.format("P2SH-B %s balance %s lower than expected %s", p2shAddress, BTC.format(balance), BTC.format(minimumBalance)));
return;
}
// Redeem P2SH-B using secret-B
Coin redeemAmount = Coin.ZERO; // The real funds are in P2SH-A
Coin redeemAmount = Coin.valueOf(P2SH_B_OUTPUT_AMOUNT); // An actual amount to avoid dust filter, remaining used as fees. The real funds are in P2SH-A.
ECKey redeemKey = ECKey.fromPrivate(tradeBotData.getTradePrivateKey());
List<TransactionOutput> fundingOutputs = BTC.getInstance().getUnspentOutputs(p2shAddress);
byte[] receivingAccountInfo = tradeBotData.getReceivingAccountInfo();
@@ -976,11 +1028,21 @@ public class TradeBot {
byte[] redeemScriptBytes = BTCP2SH.buildScript(tradeBotData.getTradeForeignPublicKeyHash(), crossChainTradeData.lockTimeB, crossChainTradeData.creatorBitcoinPKH, crossChainTradeData.hashOfSecretB);
String p2shAddress = BTC.getInstance().deriveP2shAddress(redeemScriptBytes);
Coin refundAmount = Coin.ZERO;
Coin refundAmount = Coin.valueOf(P2SH_B_OUTPUT_AMOUNT); // An actual amount to avoid dust filter, remaining used as fees.
ECKey refundKey = ECKey.fromPrivate(tradeBotData.getTradePrivateKey());
List<TransactionOutput> fundingOutputs = BTC.getInstance().getUnspentOutputs(p2shAddress);
Transaction p2shRefundTransaction = BTCP2SH.buildRefundTransaction(refundAmount, refundKey, fundingOutputs, redeemScriptBytes, crossChainTradeData.lockTimeB);
// Determine receive address for refund
String receiveAddress = BTC.getInstance().getUnusedReceiveAddress(tradeBotData.getXprv58());
if (receiveAddress == null) {
LOGGER.debug(() -> String.format("Couldn't determine a receive address for P2SH-B refund?"));
return;
}
Address receiving = Address.fromString(BTC.getInstance().getNetworkParameters(), receiveAddress);
Transaction p2shRefundTransaction = BTCP2SH.buildRefundTransaction(refundAmount, refundKey, fundingOutputs, redeemScriptBytes, crossChainTradeData.lockTimeB, receiving.getHash());
if (!BTC.getInstance().broadcastTransaction(p2shRefundTransaction)) {
// We couldn't refund P2SH-B at this time
LOGGER.debug(() -> String.format("Couldn't broadcast P2SH-B refund transaction?"));
@@ -1025,7 +1087,16 @@ public class TradeBot {
return;
}
Transaction p2shRefundTransaction = BTCP2SH.buildRefundTransaction(refundAmount, refundKey, fundingOutputs, redeemScriptBytes, tradeBotData.getLockTimeA());
// Determine receive address for refund
String receiveAddress = BTC.getInstance().getUnusedReceiveAddress(tradeBotData.getXprv58());
if (receiveAddress == null) {
LOGGER.debug(() -> String.format("Couldn't determine a receive address for P2SH-A refund?"));
return;
}
Address receiving = Address.fromString(BTC.getInstance().getNetworkParameters(), receiveAddress);
Transaction p2shRefundTransaction = BTCP2SH.buildRefundTransaction(refundAmount, refundKey, fundingOutputs, redeemScriptBytes, tradeBotData.getLockTimeA(), receiving.getHash());
if (!BTC.getInstance().broadcastTransaction(p2shRefundTransaction)) {
// We couldn't refund P2SH-A at this time
LOGGER.debug(() -> String.format("Couldn't broadcast P2SH-A refund transaction?"));

View File

@@ -20,6 +20,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;
@@ -46,6 +47,11 @@ public class BTC {
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 int TIMESTAMP_OFFSET = 4 + 32 + 32;
private static final MonetaryFormat FORMAT = new MonetaryFormat().minDecimals(8).postfixCode();
@@ -157,6 +163,20 @@ public class BTC {
return blockTimestamps.get(5);
}
/**
* Returns estimated BTC fee, in sats per 1000bytes, optionally for historic timestamp.
*
* @param timestamp optional milliseconds since epoch
* @return sats per 1000bytes, or null if something went wrong
*/
public Long estimateFee(Long timestamp) {
// 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 Long getBalance(String base58Address) {
return this.electrumX.getBalance(addressToScript(base58Address));
}
@@ -240,6 +260,89 @@ 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, or null if something went wrong
*/
public String getUnusedReceiveAddress(String xprv58) {
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);
if (unspentOutputs == null)
return null;
/*
* 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<byte[]> historicTransactionHashes = this.electrumX.getAddressTransactions(script);
if (historicTransactionHashes == null)
return null;
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 {
@@ -320,6 +423,7 @@ 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) {

View File

@@ -131,9 +131,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 +153,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);
}
/**

View File

@@ -95,18 +95,34 @@ public class ElectrumX {
case "MAIN":
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;

View File

@@ -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;
}
}

View File

@@ -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;

View File

@@ -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;

View File

@@ -152,11 +152,12 @@ public interface BlockRepository {
public int trimOldOnlineAccountsSignatures(long timestamp) throws DataException;
/**
* Returns first (lowest height) block that doesn't link back to genesis block.
* Returns first (lowest height) block that doesn't link back to specified block.
*
* @param startHeight height of specified block
* @throws DataException
*/
public BlockData getDetachedBlockSignature() throws DataException;
public BlockData getDetachedBlockSignature(int startHeight) throws DataException;
/**
* Saves block into repository.

View File

@@ -45,4 +45,6 @@ public interface Repository extends AutoCloseable {
public void backup(boolean quick) throws DataException;
public void performPeriodicMaintenance() throws DataException;
}

View File

@@ -91,6 +91,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>

View File

@@ -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;
@@ -267,8 +269,10 @@ public class HSQLDBATRepository implements ATRepository {
String sql = "SELECT height, created_when, state_data, state_hash, fees, is_initial "
+ "FROM ATStates "
+ "WHERE AT_address = ? "
+ "ORDER BY height DESC "
+ "LIMIT 1";
// 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)
@@ -297,9 +301,17 @@ public class HSQLDBATRepository implements ATRepository {
+ "CROSS JOIN LATERAL("
+ "SELECT height, created_when, state_data, state_hash, fees, is_initial "
+ "FROM ATStates "
+ "WHERE ATStates.AT_address = ATs.AT_address "
+ "ORDER BY height DESC "
+ "LIMIT 1"
+ "WHERE ATStates.AT_address = ATs.AT_address ");
if (minimumFinalHeight != null) {
sql.append("AND height >= ");
sql.append(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 "
+ ") AS FinalATStates "
+ "WHERE code_hash = ? ");
@@ -307,24 +319,19 @@ public class HSQLDBATRepository implements ATRepository {
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 ");

View File

@@ -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,11 +930,14 @@ 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 ");
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");
@@ -800,20 +948,29 @@ public class HSQLDBAccountRepository implements AccountRepository {
sql.append(")");
List<AccountBalanceData> accountBalances = new ArrayList<>();
List<EligibleQoraHolderData> eligibleLegacyQoraHolders = new ArrayList<>();
try (ResultSet resultSet = this.repository.checkedExecute(sql.toString())) {
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);
}

View File

@@ -464,21 +464,21 @@ public class HSQLDBBlockRepository implements BlockRepository {
String sql = "UPDATE Blocks set online_accounts_signatures = NULL WHERE minted_when < ? AND online_accounts_signatures IS NOT NULL";
try {
return this.repository.checkedExecuteUpdateCount(sql, timestamp);
return this.repository.executeCheckedUpdate(sql, timestamp);
} catch (SQLException e) {
throw new DataException("Unable to trim old online accounts signatures in repository", e);
}
}
@Override
public BlockData getDetachedBlockSignature() throws DataException {
public BlockData getDetachedBlockSignature(int startHeight) throws DataException {
String sql = "SELECT " + BLOCK_DB_COLUMNS + " FROM Blocks "
+ "LEFT OUTER JOIN Blocks AS ParentBlocks "
+ "ON ParentBlocks.signature = Blocks.reference "
+ "WHERE ParentBlocks.signature IS NULL AND Blocks.height > 1 "
+ "WHERE ParentBlocks.signature IS NULL AND Blocks.height > ? "
+ "ORDER BY Blocks.height ASC LIMIT 1";
try (ResultSet resultSet = this.repository.checkedExecute(sql)) {
try (ResultSet resultSet = this.repository.checkedExecute(sql, startHeight)) {
return getBlockFromResultSet(resultSet);
} catch (SQLException e) {
throw new DataException("Error fetching block by signature from repository", e);

View File

@@ -636,13 +636,18 @@ public class HSQLDBDatabaseUpdates {
case 22:
// LOB downsizing
stmt.execute("ALTER TABLE Blocks ALTER COLUMN online_accounts VARBINARY(1024)");
stmt.execute("CHECKPOINT");
stmt.execute("ALTER TABLE Blocks ALTER COLUMN online_accounts_signatures VARBINARY(1048576)");
stmt.execute("CHECKPOINT");
stmt.execute("ALTER TABLE DeployATTransactions ALTER COLUMN creation_bytes VARBINARY(4096)");
stmt.execute("CHECKPOINT");
stmt.execute("ALTER TABLE ATs ALTER COLUMN code_bytes VARBINARY(1024)");
stmt.execute("CHECKPOINT");
stmt.execute("ALTER TABLE ATStates ALTER COLUMN state_data VARBINARY(1024)");
stmt.execute("CHECKPOINT");
break;
default:

View File

@@ -16,9 +16,12 @@ 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;
@@ -49,18 +52,18 @@ public class HSQLDBRepository implements Repository {
private static final Logger LOGGER = LogManager.getLogger(HSQLDBRepository.class);
protected Connection connection;
protected Deque<Savepoint> savepoints;
protected Deque<Savepoint> savepoints = new ArrayDeque<>(3);
protected boolean debugState = false;
protected Long slowQueryThreshold = null;
protected List<String> sqlStatements;
protected long sessionId;
protected Map<String, PreparedStatement> preparedStatementCache = new HashMap<>();
// 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)
@@ -240,7 +243,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) {
@@ -333,6 +341,16 @@ public class HSQLDBRepository implements Repository {
}
}
@Override
public void performPeriodicMaintenance() throws DataException {
// Defrag DB - takes a while!
try (Statement stmt = this.connection.createStatement()) {
stmt.execute("CHECKPOINT DEFRAG");
} 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):(.*?)(;|$)");
@@ -414,6 +432,17 @@ public class HSQLDBRepository implements Repository {
if (this.sqlStatements != null)
this.sqlStatements.add(sql);
/*
* 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.
*/
if (!this.preparedStatementCache.containsKey(sql))
this.preparedStatementCache.put(sql, this.connection.prepareStatement(sql));
return this.connection.prepareStatement(sql);
}
@@ -460,7 +489,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 +510,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,14 +533,32 @@ public class HSQLDBRepository implements Repository {
* @return number of changed rows
* @throws SQLException
*/
/* package */ int checkedExecuteUpdateCount(String sql, Object... objects) throws SQLException {
/* package */ int executeCheckedUpdate(String sql, Object... objects) throws SQLException {
return this.executeCheckedBatchUpdate(sql, Collections.singletonList(objects));
}
/**
* 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;
try (PreparedStatement preparedStatement = this.prepareStatement(sql)) {
prepareExecute(preparedStatement, objects);
for (Object[] objects : batchedObjects) {
this.bindStatementParams(preparedStatement, objects);
preparedStatement.addBatch();
}
long beforeQuery = this.slowQueryThreshold == null ? 0 : System.currentTimeMillis();
if (preparedStatement.execute())
throw new SQLException("Database produced results, not row count");
int[] updateCounts = preparedStatement.executeBatch();
if (this.slowQueryThreshold != null) {
long queryTime = System.currentTimeMillis() - beforeQuery;
@@ -523,11 +570,15 @@ public class HSQLDBRepository implements Repository {
}
}
int rowCount = preparedStatement.getUpdateCount();
if (rowCount == -1)
throw new SQLException("Database returned invalid row count");
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 +649,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 +681,7 @@ public class HSQLDBRepository implements Repository {
sql.append("DELETE FROM ");
sql.append(tableName);
return this.checkedExecuteUpdateCount(sql.toString());
return this.executeCheckedUpdate(sql.toString());
}
/**

View File

@@ -586,6 +586,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);

View File

@@ -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));
}
}

View File

@@ -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

View 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

View 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 = Проверка времени

View File

@@ -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

View 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

View File

@@ -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();
}
}
}

View File

@@ -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(System.currentTimeMillis());
// 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()));

View 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());
}
}
}

View File

@@ -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;

View 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));
}
}

View File

@@ -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()));
}
}
}

View File

@@ -13,7 +13,6 @@ import org.junit.Before;
import org.junit.Test;
import org.qortal.account.PrivateKeyAccount;
import org.qortal.asset.Asset;
import org.qortal.at.QortalAtLoggerFactory;
import org.qortal.at.QortalFunctionCode;
import org.qortal.data.at.ATStateData;
import org.qortal.data.transaction.BaseTransactionData;
@@ -67,8 +66,7 @@ public class GetMessageLengthTests extends Common {
ATStateData atStateData = repository.getATRepository().getLatestATState(atAddress);
byte[] stateData = atStateData.getStateData();
QortalAtLoggerFactory loggerFactory = QortalAtLoggerFactory.getInstance();
byte[] dataBytes = MachineState.extractDataBytes(loggerFactory, stateData);
byte[] dataBytes = MachineState.extractDataBytes(stateData);
long extractedLength = BitTwiddling.longFromBEBytes(dataBytes, 0);
@@ -88,8 +86,7 @@ public class GetMessageLengthTests extends Common {
ATStateData atStateData = repository.getATRepository().getLatestATState(atAddress);
byte[] stateData = atStateData.getStateData();
QortalAtLoggerFactory loggerFactory = QortalAtLoggerFactory.getInstance();
byte[] dataBytes = MachineState.extractDataBytes(loggerFactory, stateData);
byte[] dataBytes = MachineState.extractDataBytes(stateData);
long extractedLength = BitTwiddling.longFromBEBytes(dataBytes, 0);

View File

@@ -15,7 +15,6 @@ import org.junit.Before;
import org.junit.Test;
import org.qortal.account.PrivateKeyAccount;
import org.qortal.asset.Asset;
import org.qortal.at.QortalAtLoggerFactory;
import org.qortal.block.Block;
import org.qortal.data.at.ATStateData;
import org.qortal.data.block.BlockData;
@@ -192,8 +191,7 @@ public class GetNextTransactionTests extends Common {
ATStateData atStateData = repository.getATRepository().getLatestATState(atAddress);
byte[] stateData = atStateData.getStateData();
QortalAtLoggerFactory loggerFactory = QortalAtLoggerFactory.getInstance();
byte[] dataBytes = MachineState.extractDataBytes(loggerFactory, stateData);
byte[] dataBytes = MachineState.extractDataBytes(stateData);
System.arraycopy(dataBytes, 0, rawNextTimestamp, 0, rawNextTimestamp.length);
}

View File

@@ -12,7 +12,6 @@ import org.junit.Before;
import org.junit.Test;
import org.qortal.account.PrivateKeyAccount;
import org.qortal.asset.Asset;
import org.qortal.at.QortalAtLoggerFactory;
import org.qortal.at.QortalFunctionCode;
import org.qortal.data.at.ATStateData;
import org.qortal.data.transaction.BaseTransactionData;
@@ -64,8 +63,7 @@ public class GetPartialMessageTests extends Common {
ATStateData atStateData = repository.getATRepository().getLatestATState(atAddress);
byte[] stateData = atStateData.getStateData();
QortalAtLoggerFactory loggerFactory = QortalAtLoggerFactory.getInstance();
byte[] dataBytes = MachineState.extractDataBytes(loggerFactory, stateData);
byte[] dataBytes = MachineState.extractDataBytes(stateData);
byte[] actualData = new byte[32];
System.arraycopy(dataBytes, MachineState.VALUE_SIZE, actualData, 0, 32);

View File

@@ -104,4 +104,17 @@ public class BtcTests extends Common {
assertEquals(balance, repeatBalance);
}
@Test
public void testGetUnusedReceiveAddress() {
BTC btc = BTC.getInstance();
String xprv58 = "tprv8ZgxMBicQKsPdahhFSrCdvC1bsWyzHHZfTneTVqUXN6s1wEtZLwAkZXzFP6TYLg2aQMecZLXLre5bTVGajEB55L1HYJcawpdFG66STVAWPJ";
String address = btc.getUnusedReceiveAddress(xprv58);
assertNotNull(address);
System.out.println(address);
}
}

View File

@@ -186,7 +186,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();

View File

@@ -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));
}
}

View File

@@ -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;

View File

@@ -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;

View File

@@ -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;

View File

@@ -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);
}
}

View File

@@ -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;