Compare commits

..

1 Commits

Author SHA1 Message Date
catbref
bd0bc79ea6 Bump to v1.0.8 2020-05-05 16:18:35 +01:00
483 changed files with 10237 additions and 41153 deletions

BIN
.DS_Store vendored

Binary file not shown.

3
.gitignore vendored
View File

@@ -16,6 +16,3 @@
/settings*.json
/testchain.json
/run-testnet.sh
/.idea
/qortal.iml
*.DS_Store

View File

@@ -1,20 +1,5 @@
# Auto Updates
## TL;DR: how-to
* Prepare new release version (see way below for details)
* Assuming you are in git 'master' branch, at HEAD
* Shutdown local node if running
* Build auto-update download: `tools/build-auto-update.sh` - uploads auto-update file into new git branch
* Restart local node
* Publish auto-update transaction using *private key* for **non-admin** member of "dev" group:
`tools/publish-auto-update.sh non-admin-dev-member-private-key-in-base58`
* Wait for auto-update `ARBITRARY` transaction to be confirmed into a block
* Have "dev" group admins 'approve' auto-update using `tools/approve-auto-update.sh`
This tool will prompt for *private key* of **admin** of "dev" group
* A minimum number of admins are required for approval, and a minimum number of blocks must pass also.
* Nodes will start to download, and apply, the update over the next 20 minutes or so (see CHECK_INTERVAL in AutoUpdate.java)
## Theory
* Using a specific git commit (e.g. abcdef123) we produce a determinstic JAR with consistent hash.
* To avoid issues with over-eager anti-virus / firewalls we obfuscate JAR using very simplistic XOR-based method.
@@ -40,8 +25,8 @@ The same method is used to obfuscate and de-obfuscate:
## Typical download locations
The git SHA1 commit hash is used to replace `%s` in various download locations, e.g.:
* https://github.com/Qortal/qortal/raw/%s/qortal.update
* https://raw.githubusercontent.com@151.101.16.133/Qortal/qortal/%s/qortal.update
* https://github.com/QORT/qortal/raw/%s/qortal.update
* https://raw.githubusercontent.com@151.101.16.133/QORT/qortal/%s/qortal.update
These locations are part of the org.qortal.settings.Settings class and can be overriden in settings.json like:
```
@@ -60,12 +45,4 @@ $ java -cp qortal.jar org.qortal.XorUpdate
usage: XorUpdate <input-file> <output-file>
$ java -cp qortal.jar org.qortal.XorUpdate qortal.jar qortal.update
$
```
## Preparing new release version
* Shutdown local node
* Modify `pom.xml` and increase version inside `<version>` tag
* Commit new `pom.xml` and push to github, e.g. `git commit -m 'Bumped to v1.4.2' -- pom.xml; git push`
* Tag this new commit with same version: `git tag v1.4.2`
* Push tag up to github: `git push origin v1.4.2`
```

View File

@@ -4,10 +4,10 @@ You can examine your node's database using [HSQLDB's "sqltool"](http://www.hsqld
It's a good idea to install "rlwrap" (ReadLine wrapper) too as sqltool doesn't support command history/editing.
Typical command line for sqltool would be:
`rlwrap java -cp ${HSQLDB_JAR}:${SQLTOOL_JAR} org.hsqldb.cmdline.SqlTool --rcFile=${SQLTOOL_RC} qortal`
`rlwrap java -cp ${HSQLDB_JAR}:${SQLTOOL_JAR} org.hsqldb.cmdline.SqlTool --rcFile=${SQLTOOL_RC} qora`
`${HSQLDB_JAR}` should be set with pathname where Maven downloaded hsqldb,
typically `${HOME}/.m2/repository/org/hsqldb/hsqldb/2.5.1/hsqldb-2.5.1.jar`
typically `${HOME}/.m2/repository/org/hsqldb/hsqldb/2.5.0/hsqldb-2.5.0.jar`
`${SQLTOOL_JAR}` should be set with pathname where Maven downloaded sqltool,
typically `${HOME}/.m2/repository/org/hsqldb/sqltool/2.5.0/sqltool-2.5.0.jar`
@@ -25,16 +25,10 @@ Above `url` component `file:db/blockchain` assumes you will call `sqltool` from
Another idea is to assign a shell alias in your `.bashrc` like:
```
export HSQLDB_JAR=${HOME}/.m2/repository/org/hsqldb/hsqldb/2.5.1/hsqldb-2.5.1.jar
export HSQLDB_JAR=${HOME}/.m2/repository/org/hsqldb/hsqldb/2.5.0/hsqldb-2.5.0.jar
export SQLTOOL_JAR=${HOME}/.m2/repository/org/hsqldb/sqltool/2.5.0/sqltool-2.5.0.jar
alias sqltool='rlwrap java -cp ${HSQLDB_JAR}:${SQLTOOL_JAR} org.hsqldb.cmdline.SqlTool --rcFile=${SQLTOOL_RC}'
```
So you can simply type: `sqltool qortal`
Don't forget to use `SHUTDOWN;` before exiting sqltool so that database files are closed cleanly.
## Quick and dirty version
With `sqltool-2.5.0.jar` and `qortal.jar` in current directory, and database in `db/`
`java -cp qortal.jar:sqltool-2.5.0.jar org.hsqldb.cmdline.SqlTool --inlineRc=url=jdbc:hsqldb:file:db/blockchain`

View File

@@ -9,4 +9,4 @@
- Create basic *settings.json* file: `echo '{}' > settings.json`
- Run JAR in same working directory as *settings.json*: `java -jar target/qortal-1.0.jar`
- Wrap in shell script, add JVM flags, redirection, backgrounding, etc. as necessary.
- Or use supplied example shell script: *start.sh*
- Or use supplied example shell script: *run.sh*

View File

@@ -1,69 +0,0 @@
# How to build a testnet
## Create testnet blockchain config
- You can begin by copying the mainnet blockchain config `src/main/resources/blockchain.json`
- Insert `"isTestChain": true,` after the opening `{`
- Modify testnet genesis block
### Testnet genesis block
- Set `timestamp` to a nearby future value, e.g. 15 mins from 'now'
This is to give yourself enough time to set up other testnet nodes
- Retain the initial `ISSUE_ASSET` transactions!
- Add `ACCOUNT_FLAGS` transactions with `"andMask": -1, "orMask": 1, "xorMask": 0` to create founders
- Add at least one `REWARD_SHARE` transaction otherwise no-one can mint initial blocks!
You will need to calculate `rewardSharePublicKey` (and private key),
or make a new account on mainnet and use self-share key values
- Add `ACCOUNT_LEVEL` transactions to set initial level of accounts as needed
- Add `GENESIS` transactions to add QORT/LEGACY_QORA funds to accounts as needed
## Testnet `settings.json`
- Create a new `settings-test.json`
- Make sure to add `"isTestNet": true,`
- Make sure to reference testnet blockchain config file: `"blockchainConfig": "testchain.json",`
- It is a good idea to use a separate database: `"repositoryPath": "db-testnet",`
- You might also need to add `"bitcoinNet": "TEST3",` and `"litecoinNet": "TEST3",`
## Other nodes
- Copy `testchain.json` and `settings-test.json` to other nodes
- Alternatively, you can run multiple nodes on the same machine by:
* Copying `settings-test.json` to `settings-test-1.json`
* Configure different `repositoryPath`
* Configure use of different ports:
+ `"apiPort": 22391,`
+ `"listenPort": 22392,`
## Starting-up
- Start up at least as many nodes as `minBlockchainPeers` (or adjust this value instead)
- Probably best to perform API call `DELETE /peers/known`
- Add other nodes via API call `POST /peers <peer-hostname-or-IP>`
- Add minting private key to node(s) via API call `POST /admin/mintingaccounts <minting-private-key>`
This key must have corresponding `REWARD_SHARE` transaction in testnet genesis block
- Wait for genesis block timestamp to pass
- A node should mint block 2 approximately 60 seconds after genesis block timestamp
- Other testnet nodes will sync *as long as there is at least `minBlockchainPeers` peers with an "up-to-date" chain`
- You can also use API call `POST /admin/forcesync <connected-peer-IP-and-port>` on stuck nodes
## Dealing with stuck chain
Maybe your nodes have been offline and no-one has minted a recent testnet block.
Your options are:
- Start a new testnet from scratch
- Fire up your testnet node(s)
- Force one of your nodes to mint by:
+ Set a debugger breakpoint on Settings.getMinBlockchainPeers()
+ When breakpoint is hit, change `this.minBlockchainPeers` to zero, then continue
- Once one of your nodes has minted blocks up to 'now', you can use "forcesync" on the other nodes
## Tools
- `qort` tool, but use `-t` option for default testnet API port (62391)
- `qort` tool, but first set shell variable: `export BASE_URL=some-node-hostname-or-ip:port`
- `qort` tool, but prepend with one-time shell variable: `BASE_URL=some-node-hostname-or-ip:port qort ......`
- `peer-heights`, but use `-t` option, or `BASE_URL` shell variable as above

View File

@@ -1,3 +0,0 @@
{
"apiDocumentationEnabled": true
}

View File

@@ -1,70 +0,0 @@
rootLogger.level = info
# On Windows, uncomment next line to set dirname:
# property.dirname = ${sys:user.home}\\AppData\\Local\\qortal\\
property.filename = ${sys:log4j2.filenameTemplate:-log.txt}
rootLogger.appenderRef.console.ref = stdout
rootLogger.appenderRef.rolling.ref = FILE
# Suppress extraneous bitcoinj library output
logger.bitcoinj.name = org.bitcoinj
logger.bitcoinj.level = error
# Override HSQLDB logging level to "warn" as too much is logged at "info"
logger.hsqldb.name = hsqldb.db
logger.hsqldb.level = warn
# Support optional, per-session HSQLDB debugging
logger.hsqldbRepository.name = org.qortal.repository.hsqldb
logger.hsqldbRepository.level = debug
# Suppress extraneous Jersey warning
logger.jerseyInject.name = org.glassfish.jersey.internal.inject.Providers
logger.jerseyInject.level = off
# Suppress extraneous Jersey EOF 'errors' (actually remote peers disconnecting early)
logger.jerseyEOF.name = org.glassfish.jersey.server.internal
logger.jerseyEOF.level = off
# Suppress extraneous Jetty entries
# 2019-02-14 11:46:27 INFO ContextHandler:851 - Started o.e.j.s.ServletContextHandler@6949e948{/,null,AVAILABLE}
# 2019-02-14 11:46:27 INFO AbstractConnector:289 - Started ServerConnector@50ad322b{HTTP/1.1,[http/1.1]}{0.0.0.0:9085}
# 2019-02-14 11:46:27 INFO Server:374 - jetty-9.4.11.v20180605; built: 2018-06-05T18:24:03.829Z; git: d5fc0523cfa96bfebfbda19606cad384d772f04c; jvm 1.8.0_181-b13
# 2019-02-14 11:46:27 INFO Server:411 - Started @2539ms
logger.jetty.name = org.eclipse.jetty
logger.jetty.level = warn
# Even more extraneous Jetty output
# 2019-01-26 02:18:10 WARN ResourceService:718 - java.util.concurrent.TimeoutException: Idle timeout expired: 30000/30000 ms
logger.jettyRS.name = org.eclipse.jetty.server.ResourceService
logger.jettyRS.level = error
# Suppress extraneous slf4j entries
# 2019-02-14 11:46:27 INFO log:193 - Logging initialized @1636ms to org.eclipse.jetty.util.log.Slf4jLog
logger.slf4j.name = org.slf4j
logger.slf4j.level = warn
# Suppress extraneous Reflections entry
# 2019-02-27 10:45:25 WARN Reflections:179 - given scan urls are empty. set urls in the configuration
logger.orgReflections.name = org.reflections.Reflections
logger.orgReflections.level = off
logger.sunReflections.name = sun.reflect.Reflection
logger.sunReflections.level = off
appender.console.type = Console
appender.console.name = stdout
appender.console.layout.type = PatternLayout
appender.console.layout.pattern = %d{yyyy-MM-dd HH:mm:ss} %-5p %c{1}:%L - %m%n
appender.console.filter.threshold.type = ThresholdFilter
appender.console.filter.threshold.level = error
appender.rolling.type = RollingFile
appender.rolling.name = FILE
appender.rolling.layout.type = PatternLayout
appender.rolling.layout.pattern = %d{yyyy-MM-dd HH:mm:ss} %-5p %c{1}:%L - %m%n
appender.rolling.filePattern = ${dirname:-}${filename}.%i
appender.rolling.policy.type = SizeBasedTriggeringPolicy
appender.rolling.policy.size = 4MB
# Set the immediate flush to true (default)
# appender.rolling.immediateFlush = true
# Set the append to true (default), should not overwrite
# appender.rolling.append=true

View File

@@ -1,33 +0,0 @@
@echo off
:: BatchGotAdmin
:-------------------------------------
REM --> Check for permissions
>nul 2>&1 "%SYSTEMROOT%\system32\cacls.exe" "%SYSTEMROOT%\system32\config\system"
REM --> If error flag set, we do not have admin.
if '%errorlevel%' NEQ '0' (
echo Requesting administrative privileges...
goto UACPrompt
) else ( goto gotAdmin )
:UACPrompt
echo Set UAC = CreateObject^("Shell.Application"^) > "%temp%\getadmin.vbs"
echo UAC.ShellExecute "%~s0", "", "", "runas", 1 >> "%temp%\getadmin.vbs"
"%temp%\getadmin.vbs"
exit /B
:gotAdmin
if exist "%temp%\getadmin.vbs" ( del "%temp%\getadmin.vbs" )
pushd "%CD%"
CD /D "%~dp0"
:--------------------------------------
net stop "Windows Time"
w32tm /config "/manualpeerlist:pool.ntp.org 0.pool.ntp.org 1.pool.ntp.org 2.pool.ntp.org 3.pool.ntp.org cn.pool.ntp.org 0.cn.pool.ntp.org 1.cn.pool.ntp.org 2.cn.pool.ntp.org 3.cn.pool.ntp.org"
net start "Windows Time"
sc config w32time start= auto

File diff suppressed because it is too large Load Diff

View File

@@ -1,26 +0,0 @@
# Windows installer
## Prerequisites
* AdvancedInstaller v16 or better, and enterprise licence if translations are required
* Installed AdoptOpenJDK v11 64bit, full JDK *not* JRE
## General build instructions
If this is your first time opening the `qortal.aip` file then you might need to adjust
configured paths, or create a dummy `D:` drive with the expected layout.
Typical build procedure:
* Overwrite the `qortal.jar` file in `Install-Files\`
* Open AdvancedInstaller with qortal.aip file
* If releasing a new version, change version number in:
+ "Product Information" side menu
+ "Product Details" side menu entry
+ "Product Details" tab in "Product Details" pane
+ "Product Version" entry box
* Click away to a different side menu entry, e.g. "Resources" -> "Files and Folders"
* You should be prompted whether to generate a new product key, click "Generate New"
* Click "Build" button
* New EXE should be generated in `Qortal-SetupFiles\` folder with correct version number

View File

@@ -1,117 +0,0 @@
<?xml version="1.0" encoding="UTF-8" standalone="yes"?>
<DICTIONARY type="multilanguage">
<!-- Control table -->
<ENTRY id="Control.Text.CustomizeDataPathDlg#Description">
<STRING lang="en" value="Do you want to store the blockchain, and other data, in a specific folder?"/>
<STRING lang="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>

Binary file not shown.

Before

Width:  |  Height:  |  Size: 250 KiB

Binary file not shown.

Binary file not shown.

View File

@@ -1,9 +0,0 @@
<?xml version="1.0" encoding="UTF-8"?>
<project xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd" xmlns="http://maven.apache.org/POM/4.0.0"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance">
<modelVersion>4.0.0</modelVersion>
<groupId>org.ciyam</groupId>
<artifactId>AT</artifactId>
<version>1.3.8</version>
<description>POM was created from install:install-file</description>
</project>

View File

@@ -1,16 +0,0 @@
<?xml version="1.0" encoding="UTF-8"?>
<metadata>
<groupId>org.ciyam</groupId>
<artifactId>AT</artifactId>
<versioning>
<release>1.3.8</release>
<versions>
<version>1.3.4</version>
<version>1.3.5</version>
<version>1.3.6</version>
<version>1.3.7</version>
<version>1.3.8</version>
</versions>
<lastUpdated>20200925114415</lastUpdated>
</versioning>
</metadata>

Binary file not shown.

View File

@@ -3,7 +3,7 @@
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance">
<modelVersion>4.0.0</modelVersion>
<groupId>org.ciyam</groupId>
<artifactId>AT</artifactId>
<version>1.3.7</version>
<artifactId>at</artifactId>
<version>1.0</version>
<description>POM was created from install:install-file</description>
</project>

View File

@@ -0,0 +1,12 @@
<?xml version="1.0" encoding="UTF-8"?>
<metadata>
<groupId>org.ciyam</groupId>
<artifactId>at</artifactId>
<versioning>
<release>1.0</release>
<versions>
<version>1.0</version>
</versions>
<lastUpdated>20181105100741</lastUpdated>
</versioning>
</metadata>

View File

@@ -1,15 +1,11 @@
rootLogger.level = info
# On Windows, uncomment next line to set dirname:
# property.dirname = ${sys:user.home}\\AppData\\Local\\qortal\\
# On Windows, uncomment this:
# property.dirname = ${sys:user.home}\\AppData\\Roaming\\qortal\\
property.filename = ${sys:log4j2.filenameTemplate:-log.txt}
rootLogger.appenderRef.console.ref = stdout
rootLogger.appenderRef.rolling.ref = FILE
# Suppress extraneous bitcoinj library output
logger.bitcoinj.name = org.bitcoinj
logger.bitcoinj.level = error
# Override HSQLDB logging level to "warn" as too much is logged at "info"
logger.hsqldb.name = hsqldb.db
logger.hsqldb.level = warn

63
pom.xml
View File

@@ -3,22 +3,20 @@
<modelVersion>4.0.0</modelVersion>
<groupId>org.qortal</groupId>
<artifactId>qortal</artifactId>
<version>1.4.3</version>
<version>1.0.8</version>
<packaging>jar</packaging>
<properties>
<skipTests>true</skipTests>
<altcoinj.version>bf9fb80</altcoinj.version>
<bitcoinj.version>0.15.6</bitcoinj.version>
<bitcoin.version>0.15.4</bitcoin.version>
<bouncycastle.version>1.64</bouncycastle.version>
<build.timestamp>${maven.build.timestamp}</build.timestamp>
<ciyam-at.version>1.3.8</ciyam-at.version>
<commons-net.version>3.6</commons-net.version>
<commons-text.version>1.8</commons-text.version>
<dagger.version>1.2.2</dagger.version>
<guava.version>28.1-jre</guava.version>
<hsqldb.version>2.5.1</hsqldb.version>
<hsqldb.version>2.5.0-fixed</hsqldb.version>
<hsqldb-sqltool.version>2.5.0</hsqldb-sqltool.version>
<jersey.version>2.29.1</jersey.version>
<jetty.version>9.4.29.v20200521</jetty.version>
<jetty.version>9.4.22.v20191022</jetty.version>
<log4j.version>2.12.1</log4j.version>
<project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
<slf4j.version>1.7.12</slf4j.version>
@@ -200,10 +198,6 @@
<pattern>org.qortal.api.model**</pattern>
<template>${project.build.sourceDirectory}/org/qortal/data/package-info.java</template>
</package>
<package>
<pattern>org.qortal.api.model.**</pattern>
<template>${project.build.sourceDirectory}/org/qortal/data/package-info.java</template>
</package>
</packages>
<outputDirectory>${project.build.directory}/generated-sources/package-info</outputDirectory>
</configuration>
@@ -318,14 +312,6 @@
</execution>
</executions>
</plugin>
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-surefire-plugin</artifactId>
<version>2.22.2</version>
<configuration>
<skipTests>${skipTests}</skipTests>
</configuration>
</plugin>
</plugins>
<pluginManagement>
<plugins>
@@ -388,11 +374,6 @@
<name>project</name>
<url>file:${project.basedir}/lib</url>
</repository>
<!-- jitpack for build-on-demand of altcoinj -->
<repository>
<id>jitpack.io</id>
<url>https://jitpack.io</url>
</repository>
</repositories>
<dependencies>
<!-- https://mvnrepository.com/artifact/org.codehaus.mojo/build-helper-maven-plugin -->
@@ -415,23 +396,23 @@
<artifactId>hsqldb</artifactId>
<version>${hsqldb.version}</version>
</dependency>
<dependency>
<groupId>org.hsqldb</groupId>
<artifactId>sqltool</artifactId>
<version>${hsqldb-sqltool.version}</version>
<scope>test</scope>
</dependency>
<!-- CIYAM AT (automated transactions) -->
<dependency>
<groupId>org.ciyam</groupId>
<artifactId>AT</artifactId>
<version>${ciyam-at.version}</version>
<artifactId>at</artifactId>
<version>1.0</version>
</dependency>
<!-- Bitcoin support -->
<dependency>
<groupId>org.bitcoinj</groupId>
<artifactId>bitcoinj-core</artifactId>
<version>${bitcoinj.version}</version>
</dependency>
<!-- For Litecoin, etc. support, requires bitcoinj -->
<dependency>
<groupId>com.github.jjos2372</groupId>
<artifactId>altcoinj</artifactId>
<version>${altcoinj.version}</version>
<version>${bitcoin.version}</version>
</dependency>
<!-- Utilities -->
<dependency>
@@ -469,10 +450,6 @@
<groupId>org.asynchttpclient</groupId>
<artifactId>async-http-client</artifactId>
</exclusion>
<exclusion>
<groupId>io.druid</groupId>
<artifactId>java-util</artifactId>
</exclusion>
</exclusions>
</dependency>
<!-- For NTP -->
@@ -526,12 +503,6 @@
<artifactId>mail</artifactId>
<version>1.5.0-b01</version>
</dependency>
<!-- Unicode homoglyph utilities -->
<dependency>
<groupId>net.codebox</groupId>
<artifactId>homoglyph</artifactId>
<version>1.2.0</version>
</dependency>
<!-- Jetty -->
<dependency>
<groupId>org.eclipse.jetty</groupId>
@@ -560,12 +531,6 @@
<artifactId>jetty-client</artifactId>
<version>${jetty.version}</version>
</dependency>
<!-- Websocket support -->
<dependency>
<groupId>org.eclipse.jetty.websocket</groupId>
<artifactId>javax-websocket-server-impl</artifactId>
<version>${jetty.version}</version>
</dependency>
<!-- Jersey -->
<dependency>
<groupId>org.glassfish.jersey.core</groupId>

1
run.sh
View File

@@ -1 +0,0 @@
start.sh

51
run.sh Executable file
View File

@@ -0,0 +1,51 @@
#!/bin/sh
# There's no need to run as root, so don't allow it, for security reasons
if [ "$USER" = "root" ]; then
echo "Please su to non-root user before running"
exit
fi
# Validate Java is installed and the minimum version is available
MIN_JAVA_VER='11'
if command -v java > /dev/null 2>&1; then
version=$(java -version 2>&1 | awk -F '"' '/version/ {print $2}')
version=$(echo $version | cut -d'.' -f1,2)
if [ `echo "${version}>=${MIN_JAVA_VER}" | bc` -eq 1 ]; then
echo 'Passed Java version check'
else
echo 'Please upgrade your Java to version 11 or greater'
exit 1
fi
else
echo 'Java is not available, please install Java 11 or greater'
exit 1
fi
# No qortal.jar but we have a Maven built one?
# Be helpful and copy across to correct location
if [ ! -e qortal.jar -a -f target/qortal*.jar ]; then
echo "Copying Maven-built Qortal JAR to correct pathname"
cp target/qortal*.jar qortal.jar
fi
# Limits Java JVM stack size and maximum heap usage.
# Comment out for bigger systems, e.g. non-routers
# or when API documentation is enabled
# JVM_MEMORY_ARGS="-Xss256k -Xmx128m"
# Although java.net.preferIPv4Stack is supposed to be false
# by default in Java 11, on some platforms (e.g. FreeBSD 12),
# it is overriden to be true by default. Hence we explicitly
# set it to true to obtain desired behaviour.
nohup nice -n 20 java \
-Djava.net.preferIPv4Stack=false \
-XX:NativeMemoryTracking=summary \
${JVM_MEMORY_ARGS} \
-jar qortal.jar \
1>run.log 2>&1 &
# Save backgrounded process's PID
echo $! > run.pid
echo qortal running as pid $!

BIN
src/.DS_Store vendored

Binary file not shown.

BIN
src/main/.DS_Store vendored

Binary file not shown.

View File

@@ -21,28 +21,18 @@ public class HSQLDBPool extends JDBCPool {
public Connection tryConnection() throws SQLException {
for (int i = 0; i < states.length(); i++) {
if (states.compareAndSet(i, RefState.available, RefState.allocated)) {
JDBCPooledConnection pooledConnection = connections[i];
if (pooledConnection == null)
// Probably shutdown situation
return null;
return pooledConnection.getConnection();
return connections[i].getConnection();
}
if (states.compareAndSet(i, RefState.empty, RefState.allocated)) {
try {
JDBCPooledConnection pooledConnection = (JDBCPooledConnection) source.getPooledConnection();
JDBCPooledConnection connection = (JDBCPooledConnection) source.getPooledConnection();
if (pooledConnection == null)
// Probably shutdown situation
return null;
connection.addConnectionEventListener(this);
connection.addStatementEventListener(this);
connections[i] = connection;
pooledConnection.addConnectionEventListener(this);
pooledConnection.addStatementEventListener(this);
connections[i] = pooledConnection;
return pooledConnection.getConnection();
return connections[i].getConnection();
} catch (SQLException e) {
states.set(i, RefState.empty);
}

View File

@@ -35,8 +35,6 @@ 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;
@@ -67,19 +65,17 @@ public class ApplyUpdate {
}
private static boolean shutdownNode() {
String baseUri = "http://localhost:" + Settings.getInstance().getApiPort() + "/";
LOGGER.info(() -> String.format("Shutting down node using API via %s", baseUri));
String BASE_URI = "http://localhost:" + Settings.getInstance().getApiPort() + "/";
LOGGER.info(String.format("Shutting down node using API via %s", BASE_URI));
int attempt;
for (attempt = 0; attempt < MAX_ATTEMPTS; ++attempt) {
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);
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);
if (response == null)
// No response - consider node shut down
return true;
break;
LOGGER.info(() -> String.format("Response from API: %s", response));
LOGGER.info(String.format("Response from API: %s", response));
try {
Thread.sleep(CHECK_INTERVAL);
@@ -103,20 +99,19 @@ 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) {
final int attemptForLogging = attempt;
LOGGER.info(() -> String.format("Attempt #%d out of %d to replace JAR", attemptForLogging + 1, MAX_ATTEMPTS));
LOGGER.info(String.format("Attempt #%d out of %d to replace JAR", attempt + 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
}
@@ -124,7 +119,6 @@ public class ApplyUpdate {
try {
Thread.sleep(CHECK_INTERVAL);
} catch (InterruptedException e) {
LOGGER.warn("Ignoring interrupt...");
// Doggedly retry
}
}
@@ -135,13 +129,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)) {
@@ -162,16 +156,9 @@ 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)));
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();
new ProcessBuilder(javaCmd).start();
} catch (IOException e) {
LOGGER.error(String.format("Failed to restart node (BAD): %s", e.getMessage()));
}

View File

@@ -1,75 +0,0 @@
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

@@ -1,9 +1,7 @@
package org.qortal.account;
import static org.qortal.utils.Amounts.prettyAmount;
import javax.xml.bind.annotation.XmlAccessType;
import javax.xml.bind.annotation.XmlAccessorType;
import java.math.BigDecimal;
import java.util.List;
import org.apache.logging.log4j.LogManager;
import org.apache.logging.log4j.Logger;
@@ -11,11 +9,12 @@ import org.qortal.block.BlockChain;
import org.qortal.data.account.AccountBalanceData;
import org.qortal.data.account.AccountData;
import org.qortal.data.account.RewardShareData;
import org.qortal.data.transaction.TransactionData;
import org.qortal.repository.DataException;
import org.qortal.repository.Repository;
import org.qortal.transaction.Transaction;
import org.qortal.utils.Base58;
@XmlAccessorType(XmlAccessType.NONE) // Stops JAX-RS errors when unmarshalling blockchain config
public class Account {
private static final Logger LOGGER = LogManager.getLogger(Account.class);
@@ -52,52 +51,45 @@ public class Account {
return new AccountData(this.address);
}
public void ensureAccount() throws DataException {
this.repository.getAccountRepository().ensureAccount(this.buildAccountData());
}
// Balance manipulations - assetId is 0 for QORT
public long getConfirmedBalance(long assetId) throws DataException {
public BigDecimal getBalance(long assetId) throws DataException {
AccountBalanceData accountBalanceData = this.repository.getAccountRepository().getBalance(this.address, assetId);
if (accountBalanceData == null)
return 0;
return BigDecimal.ZERO.setScale(8);
return accountBalanceData.getBalance();
}
public void setConfirmedBalance(long assetId, long balance) throws DataException {
public BigDecimal getConfirmedBalance(long assetId) throws DataException {
AccountBalanceData accountBalanceData = this.repository.getAccountRepository().getBalance(this.address, assetId);
if (accountBalanceData == null)
return BigDecimal.ZERO.setScale(8);
return accountBalanceData.getBalance();
}
public void setConfirmedBalance(long assetId, BigDecimal balance) throws DataException {
// Safety feature!
if (balance < 0) {
String message = String.format("Refusing to set negative balance %s [assetId %d] for %s", prettyAmount(balance), assetId, this.address);
if (balance.compareTo(BigDecimal.ZERO) < 0) {
String message = String.format("Refusing to set negative balance %s [assetId %d] for %s", balance.toPlainString(), assetId, this.address);
LOGGER.error(message);
throw new DataException(message);
}
// Delete account balance record instead of setting balance to zero
if (balance == 0) {
if (balance.signum() == 0) {
this.repository.getAccountRepository().delete(this.address, assetId);
return;
}
// Can't have a balance without an account - make sure it exists!
this.ensureAccount();
this.repository.getAccountRepository().ensureAccount(this.buildAccountData());
AccountBalanceData accountBalanceData = new AccountBalanceData(this.address, assetId, balance);
this.repository.getAccountRepository().save(accountBalanceData);
LOGGER.trace(() -> String.format("%s balance now %s [assetId %s]", this.address, prettyAmount(balance), assetId));
}
// Convenience method
public void modifyAssetBalance(long assetId, long deltaBalance) throws DataException {
this.repository.getAccountRepository().modifyAssetBalance(this.getAddress(), assetId, deltaBalance);
LOGGER.trace(() -> String.format("%s balance %s by %s [assetId %s]",
this.address,
(deltaBalance >= 0 ? "increased" : "decreased"),
prettyAmount(Math.abs(deltaBalance)),
assetId));
LOGGER.trace(() -> String.format("%s balance now %s [assetId %s]", this.address, balance.toPlainString(), assetId));
}
public void deleteBalance(long assetId) throws DataException {
@@ -113,11 +105,38 @@ public class Account {
* @throws DataException
*/
public byte[] getLastReference() throws DataException {
byte[] reference = AccountRefCache.getLastReference(this.repository, this.address);
byte[] reference = this.repository.getAccountRepository().getLastReference(this.address);
LOGGER.trace(() -> String.format("Last reference for %s is %s", this.address, reference == null ? "null" : Base58.encode(reference)));
return reference;
}
/**
* Fetch last reference for account, considering unconfirmed transactions only, or return null.
* <p>
* NOTE: calls Transaction.getUnconfirmedTransactions which discards uncommitted
* repository changes.
*
* @return byte[] reference, or null if no unconfirmed transactions for this account.
* @throws DataException
*/
public byte[] getUnconfirmedLastReference() throws DataException {
// Newest unconfirmed transaction takes priority
List<TransactionData> unconfirmedTransactions = Transaction.getUnconfirmedTransactions(repository);
byte[] reference = null;
for (TransactionData transactionData : unconfirmedTransactions) {
String unconfirmedTransactionAddress = PublicKeyAccount.getAddress(transactionData.getCreatorPublicKey());
if (unconfirmedTransactionAddress.equals(this.address))
reference = transactionData.getSignature();
}
final byte[] loggingReference = reference;
LOGGER.trace(() -> String.format("Last unconfirmed reference for %s is %s", this.address, loggingReference == null ? "null" : Base58.encode(loggingReference)));
return reference;
}
/**
* Set last reference for account.
*
@@ -130,7 +149,7 @@ public class Account {
AccountData accountData = this.buildAccountData();
accountData.setReference(reference);
AccountRefCache.setLastReference(this.repository, accountData);
this.repository.getAccountRepository().setLastReference(accountData);
}
// Default groupID manipulations
@@ -266,7 +285,11 @@ public class Account {
if (Account.isFounder(accountData.getFlags()))
return BlockChain.getInstance().getFounderEffectiveMintingLevel();
return accountData.getLevel();
Integer level = accountData.getLevel();
if (level == null)
return 0;
return level;
}
/**

View File

@@ -1,217 +0,0 @@
package org.qortal.account;
import java.util.HashMap;
import java.util.Map;
import java.util.Map.Entry;
import java.util.function.BinaryOperator;
import org.qortal.data.account.AccountData;
import org.qortal.repository.DataException;
import org.qortal.repository.Repository;
import org.qortal.utils.Pair;
/**
* Account lastReference caching
* <p>
* When checking an account's lastReference, the value returned should be the
* most recent value set after processing the most recent block.
* <p>
* However, when processing a batch of transactions, e.g. during block processing or validation,
* each transaction needs to check, and maybe update, multiple accounts' lastReference values.
* <p>
* Because the intermediate updates would affect future checks, we set up a cache of that
* maintains a consistent value for fetching lastReference, but also tracks the latest new
* value, without the overhead of repository calls.
* <p>
* Thus, when batch transaction processing is finished, only the latest new lastReference values
* can be committed to the repository, via {@link AccountRefCache#commit()}.
* <p>
* Getting and setting lastReferences values are done the usual way via
* {@link Account#getLastReference()} and {@link Account#setLastReference(byte[])} which call
* package-visibility methods in <tt>AccountRefCache</tt>.
* <p>
* If {@link Account#getLastReference()} or {@link Account#setLastReference(byte[])} are called
* outside of caching then lastReference values are fetched/set directly from/to the repository.
* <p>
* <tt>AccountRefCache</tt> implements <tt>AutoCloseable</tt> for (typical) use in a try-with-resources block.
*
* @see Account#getLastReference()
* @see Account#setLastReference(byte[])
* @see org.qortal.block.Block#process()
*/
public class AccountRefCache implements AutoCloseable {
private static final Map<Repository, RefCache> CACHE = new HashMap<>();
private static class RefCache {
private final Map<String, byte[]> getLastReferenceValues = new HashMap<>();
private final Map<String, Pair<byte[], byte[]>> setLastReferenceValues = new HashMap<>();
/**
* Function for merging publicKey from new data with old publicKey from map.
* <p>
* Last reference is <tt>A</tt> element in pair.<br>
* Public key is <tt>B</tt> element in pair.
*/
private static final BinaryOperator<Pair<byte[], byte[]>> mergePublicKey = (oldPair, newPair) -> {
// If passed new pair contains non-null publicKey, then we use that one in preference.
if (newPair.getB() == null)
// Otherwise, inherit publicKey from old map value.
newPair.setB(oldPair.getB());
// We always use new lastReference from new pair.
return newPair;
};
public byte[] getLastReference(Repository repository, String address) throws DataException {
synchronized (this.getLastReferenceValues) {
byte[] lastReference = getLastReferenceValues.get(address);
if (lastReference != null)
// address is present in map, lastReference not null
return lastReference;
// address is present in map, just lastReference is null
if (getLastReferenceValues.containsKey(address))
return null;
lastReference = repository.getAccountRepository().getLastReference(address);
this.getLastReferenceValues.put(address, lastReference);
return lastReference;
}
}
public void setLastReference(AccountData accountData) {
// We're only interested in lastReference and publicKey
Pair<byte[], byte[]> newPair = new Pair<>(accountData.getReference(), accountData.getPublicKey());
synchronized (this.setLastReferenceValues) {
setLastReferenceValues.merge(accountData.getAddress(), newPair, mergePublicKey);
}
}
Map<String, Pair<byte[], byte[]>> getNewLastReferences() {
return setLastReferenceValues;
}
}
private Repository repository;
/**
* Constructs a new account reference cache, unique to passed <tt>repository</tt> handle.
*
* @param repository
* @throws IllegalStateException if a cache already exists for <tt>repository</tt>
*/
public AccountRefCache(Repository repository) {
RefCache refCache = new RefCache();
synchronized (CACHE) {
if (CACHE.putIfAbsent(repository, refCache) != null)
throw new IllegalStateException("Account reference cache entry already exists");
}
this.repository = repository;
}
/**
* Save all cached setLastReference account-reference values into repository.
* <p>
* Closes cache to prevent any future setLastReference() attempts post-commit.
*
* @throws DataException
*/
public void commit() throws DataException {
RefCache refCache;
// Also duplicated in close(), this prevents future setLastReference() attempts post-commit.
synchronized (CACHE) {
refCache = CACHE.remove(this.repository);
}
if (refCache == null)
throw new IllegalStateException("Tried to commit non-existent account reference cache");
Map<String, Pair<byte[], byte[]>> newLastReferenceValues = refCache.getNewLastReferences();
for (Entry<String, Pair<byte[], byte[]>> entry : newLastReferenceValues.entrySet()) {
AccountData accountData = new AccountData(entry.getKey());
accountData.setReference(entry.getValue().getA());
if (entry.getValue().getB() != null)
accountData.setPublicKey(entry.getValue().getB());
this.repository.getAccountRepository().setLastReference(accountData);
}
}
@Override
public void close() {
synchronized (CACHE) {
CACHE.remove(this.repository);
}
}
/**
* Returns lastReference value for account.
* <p>
* If cache is not in effect for passed <tt>repository</tt> handle,
* then this method fetches lastReference directly from repository.
* <p>
* If cache <i>is</i> in effect, then this method returns cached
* lastReference, which is <b>not</b> affected by calls to
* <tt>setLastReference</tt>.
* <p>
* Typically called by corresponding method in Account class.
*
* @param repository
* @param address account's address
* @return account's lastReference, or null if account unknown, or lastReference not set
* @throws DataException
*/
/*package*/ static byte[] getLastReference(Repository repository, String address) throws DataException {
RefCache refCache;
synchronized (CACHE) {
refCache = CACHE.get(repository);
}
if (refCache == null)
return repository.getAccountRepository().getLastReference(address);
return refCache.getLastReference(repository, address);
}
/**
* Sets lastReference value for account.
* <p>
* If cache is not in effect for passed <tt>repository</tt> handle,
* then this method sets lastReference directly in repository.
* <p>
* If cache <i>is</i> in effect, then this method caches the new
* lastReference, which is <b>not</b> returned by calls to
* <tt>getLastReference</tt>.
* <p>
* Typically called by corresponding method in Account class.
*
* @param repository
* @param accountData
* @throws DataException
*/
/*package*/ static void setLastReference(Repository repository, AccountData accountData) throws DataException {
RefCache refCache;
synchronized (CACHE) {
refCache = CACHE.get(repository);
}
if (refCache == null) {
repository.getAccountRepository().setLastReference(accountData);
return;
}
refCache.setLastReference(accountData);
}
}

View File

@@ -0,0 +1,13 @@
package org.qortal.account;
import org.qortal.repository.Repository;
public final class GenesisAccount extends PublicKeyAccount {
public static final byte[] PUBLIC_KEY = new byte[] { 1, 1, 1, 1, 1, 1, 1, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0 };
public GenesisAccount(Repository repository) {
super(repository, PUBLIC_KEY);
}
}

View File

@@ -1,24 +0,0 @@
package org.qortal.account;
import org.qortal.crypto.Crypto;
import org.qortal.repository.Repository;
public final class NullAccount extends PublicKeyAccount {
public static final byte[] PUBLIC_KEY = new byte[32];
public static final String ADDRESS = Crypto.toAddress(PUBLIC_KEY);
public NullAccount(Repository repository) {
super(repository, PUBLIC_KEY, ADDRESS);
}
protected NullAccount() {
}
@Override
public boolean verify(byte[] signature, byte[] message) {
// Can't sign, hence can't verify.
return false;
}
}

View File

@@ -2,11 +2,18 @@ package org.qortal.account;
import org.bouncycastle.crypto.params.Ed25519PrivateKeyParameters;
import org.bouncycastle.crypto.params.Ed25519PublicKeyParameters;
import org.bouncycastle.crypto.params.X25519PrivateKeyParameters;
import org.bouncycastle.crypto.params.X25519PublicKeyParameters;
import org.bouncycastle.math.ec.rfc8032.Ed25519;
import org.qortal.crypto.BouncyCastle25519;
import org.qortal.crypto.Crypto;
import org.qortal.repository.Repository;
public class PrivateKeyAccount extends PublicKeyAccount {
private static final int SIGNATURE_LENGTH = 64;
private static final int SHARED_SECRET_LENGTH = 32;
private final byte[] privateKey;
private final Ed25519PrivateKeyParameters edPrivateKeyParams;
@@ -42,11 +49,24 @@ public class PrivateKeyAccount extends PublicKeyAccount {
}
public byte[] sign(byte[] message) {
return Crypto.sign(this.edPrivateKeyParams, message);
byte[] signature = new byte[SIGNATURE_LENGTH];
this.edPrivateKeyParams.sign(Ed25519.Algorithm.Ed25519, edPublicKeyParams, null, message, 0, message.length, signature, 0);
return signature;
}
public byte[] getSharedSecret(byte[] publicKey) {
return Crypto.getSharedSecret(this.privateKey, publicKey);
byte[] x25519PrivateKey = BouncyCastle25519.toX25519PrivateKey(this.privateKey);
X25519PrivateKeyParameters xPrivateKeyParams = new X25519PrivateKeyParameters(x25519PrivateKey, 0);
byte[] x25519PublicKey = BouncyCastle25519.toX25519PublicKey(publicKey);
X25519PublicKeyParameters xPublicKeyParams = new X25519PublicKeyParameters(x25519PublicKey, 0);
byte[] sharedSecret = new byte[SHARED_SECRET_LENGTH];
xPrivateKeyParams.generateSecret(xPublicKeyParams, sharedSecret, 0);
return sharedSecret;
}
public byte[] getRewardSharePrivateKey(byte[] publicKey) {

View File

@@ -1,6 +1,7 @@
package org.qortal.account;
import org.bouncycastle.crypto.params.Ed25519PublicKeyParameters;
import org.bouncycastle.math.ec.rfc8032.Ed25519;
import org.qortal.crypto.Crypto;
import org.qortal.data.account.AccountData;
import org.qortal.repository.Repository;
@@ -21,18 +22,6 @@ public class PublicKeyAccount extends Account {
this.publicKey = edPublicKeyParams.getEncoded();
}
protected PublicKeyAccount(Repository repository, byte[] publicKey, String address) {
super(repository, address);
this.publicKey = publicKey;
this.edPublicKeyParams = null;
}
protected PublicKeyAccount() {
this.publicKey = null;
this.edPublicKeyParams = null;
}
public byte[] getPublicKey() {
return this.publicKey;
}
@@ -45,7 +34,15 @@ public class PublicKeyAccount extends Account {
}
public boolean verify(byte[] signature, byte[] message) {
return Crypto.verify(this.publicKey, signature, message);
return PublicKeyAccount.verify(this.publicKey, signature, message);
}
public static boolean verify(byte[] publicKey, byte[] signature, byte[] message) {
try {
return Ed25519.verify(signature, 0, publicKey, 0, message, 0, message.length);
} catch (Exception e) {
return false;
}
}
public static String getAddress(byte[] publicKey) {

View File

@@ -1,27 +0,0 @@
package org.qortal.api;
import java.math.BigDecimal;
import javax.xml.bind.annotation.adapters.XmlAdapter;
import org.qortal.utils.Amounts;
public class AmountTypeAdapter extends XmlAdapter<String, Long> {
@Override
public Long unmarshal(String input) throws Exception {
if (input == null)
return null;
return new BigDecimal(input).setScale(8).unscaledValue().longValue();
}
@Override
public String marshal(Long output) throws Exception {
if (output == null)
return null;
return Amounts.prettyAmount(output);
}
}

View File

@@ -5,17 +5,11 @@ import static java.util.stream.Collectors.toMap;
import java.util.Map;
import javax.xml.bind.annotation.XmlAccessType;
import javax.xml.bind.annotation.XmlAccessorType;
import javax.xml.bind.annotation.XmlRootElement;
@XmlAccessorType(XmlAccessType.NONE)
@XmlRootElement
public enum ApiError {
// COMMON
// UNKNOWN(0, 500),
JSON(1, 400),
INSUFFICIENT_BALANCE(2, 402),
// NO_BALANCE(2, 422),
// NOT_YET_RELEASED(3, 422),
UNAUTHORIZED(4, 403),
REPOSITORY_ISSUE(5, 500),
@@ -124,12 +118,7 @@ public enum ApiError {
// MESSAGESIZE_EXCEEDED(1004, 400),
// Groups
GROUP_UNKNOWN(1101, 404),
// Foreign blockchain
FOREIGN_BLOCKCHAIN_NETWORK_ISSUE(1201, 500),
FOREIGN_BLOCKCHAIN_BALANCE_ISSUE(1202, 402),
FOREIGN_BLOCKCHAIN_TOO_SOON(1203, 408);
GROUP_UNKNOWN(1101, 404);
private static final Map<Integer, ApiError> map = stream(ApiError.values()).collect(toMap(apiError -> apiError.code, apiError -> apiError));

View File

@@ -3,7 +3,6 @@ package org.qortal.api;
import java.io.IOException;
import javax.servlet.RequestDispatcher;
import javax.servlet.ServletException;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
@@ -18,7 +17,7 @@ public class ApiErrorHandler extends ErrorHandler {
private static final Logger LOGGER = LogManager.getLogger(ApiErrorHandler.class);
@Override
public void handle(String target, Request baseRequest, HttpServletRequest request, HttpServletResponse response) throws IOException, ServletException {
public void handle(String target, Request baseRequest, HttpServletRequest request, HttpServletResponse response) throws IOException {
if (Settings.getInstance().isApiLoggingEnabled()) {
String requestURI = request.getRequestURI();

View File

@@ -1,20 +0,0 @@
package org.qortal.api;
import javax.xml.bind.annotation.XmlElement;
import javax.xml.bind.annotation.adapters.XmlJavaTypeAdapter;
public class ApiErrorRoot {
private ApiError apiError;
@XmlJavaTypeAdapter(ApiErrorTypeAdapter.class)
@XmlElement(name = "error")
public ApiError getApiError() {
return this.apiError;
}
public void setApiError(ApiError apiError) {
this.apiError = apiError;
}
}

View File

@@ -1,32 +0,0 @@
package org.qortal.api;
import javax.xml.bind.annotation.adapters.XmlAdapter;
public class ApiErrorTypeAdapter extends XmlAdapter<ApiErrorTypeAdapter.AdaptedApiError, ApiError> {
public static class AdaptedApiError {
public int code;
public String description;
}
@Override
public ApiError unmarshal(AdaptedApiError adaptedInput) throws Exception {
if (adaptedInput == null)
return null;
return ApiError.fromCode(adaptedInput.code);
}
@Override
public AdaptedApiError marshal(ApiError output) throws Exception {
if (output == null)
return null;
AdaptedApiError adaptedOutput = new AdaptedApiError();
adaptedOutput.code = output.getCode();
adaptedOutput.description = output.name();
return adaptedOutput;
}
}

View File

@@ -2,31 +2,15 @@ package org.qortal.api;
import io.swagger.v3.jaxrs2.integration.resources.OpenApiResource;
import java.io.InputStream;
import java.net.InetAddress;
import java.net.InetSocketAddress;
import java.nio.file.Files;
import java.nio.file.Path;
import java.nio.file.Paths;
import java.security.KeyStore;
import java.security.SecureRandom;
import javax.net.ssl.KeyManagerFactory;
import javax.net.ssl.SSLContext;
import org.eclipse.jetty.http.HttpVersion;
import org.eclipse.jetty.rewrite.handler.RedirectPatternRule;
import org.eclipse.jetty.rewrite.handler.RewriteHandler;
import org.eclipse.jetty.server.CustomRequestLog;
import org.eclipse.jetty.server.DetectorConnectionFactory;
import org.eclipse.jetty.server.HttpConfiguration;
import org.eclipse.jetty.server.HttpConnectionFactory;
import org.eclipse.jetty.server.RequestLog;
import org.eclipse.jetty.server.RequestLogWriter;
import org.eclipse.jetty.server.SecureRequestCustomizer;
import org.eclipse.jetty.server.Server;
import org.eclipse.jetty.server.ServerConnector;
import org.eclipse.jetty.server.SslConnectionFactory;
import org.eclipse.jetty.server.handler.ErrorHandler;
import org.eclipse.jetty.server.handler.InetAccessHandler;
import org.eclipse.jetty.servlet.DefaultServlet;
@@ -34,18 +18,10 @@ import org.eclipse.jetty.servlet.FilterHolder;
import org.eclipse.jetty.servlet.ServletContextHandler;
import org.eclipse.jetty.servlet.ServletHolder;
import org.eclipse.jetty.servlets.CrossOriginFilter;
import org.eclipse.jetty.util.ssl.SslContextFactory;
import org.glassfish.jersey.server.ResourceConfig;
import org.glassfish.jersey.servlet.ServletContainer;
import org.qortal.api.resource.AnnotationPostProcessor;
import org.qortal.api.resource.ApiDefinition;
import org.qortal.api.websocket.ActiveChatsWebSocket;
import org.qortal.api.websocket.AdminStatusWebSocket;
import org.qortal.api.websocket.BlocksWebSocket;
import org.qortal.api.websocket.ChatMessagesWebSocket;
import org.qortal.api.websocket.PresenceWebSocket;
import org.qortal.api.websocket.TradeBotWebSocket;
import org.qortal.api.websocket.TradeOffersWebSocket;
import org.qortal.settings.Settings;
public class ApiService {
@@ -77,57 +53,9 @@ public class ApiService {
public void start() {
try {
// Create API server
// SSL support if requested
String keystorePathname = Settings.getInstance().getSslKeystorePathname();
String keystorePassword = Settings.getInstance().getSslKeystorePassword();
if (keystorePathname != null && keystorePassword != null) {
// SSL version
if (!Files.isReadable(Path.of(keystorePathname)))
throw new RuntimeException("Failed to start SSL API due to broken keystore");
// BouncyCastle-specific SSLContext build
SSLContext sslContext = SSLContext.getInstance("TLS", "BCJSSE");
KeyManagerFactory keyManagerFactory = KeyManagerFactory.getInstance("PKIX", "BCJSSE");
KeyStore keyStore = KeyStore.getInstance(KeyStore.getDefaultType(), "BC");
try (InputStream keystoreStream = Files.newInputStream(Paths.get(keystorePathname))) {
keyStore.load(keystoreStream, keystorePassword.toCharArray());
}
keyManagerFactory.init(keyStore, keystorePassword.toCharArray());
sslContext.init(keyManagerFactory.getKeyManagers(), null, new SecureRandom());
SslContextFactory.Server sslContextFactory = new SslContextFactory.Server();
sslContextFactory.setSslContext(sslContext);
this.server = new Server();
HttpConfiguration httpConfig = new HttpConfiguration();
httpConfig.setSecureScheme("https");
httpConfig.setSecurePort(Settings.getInstance().getApiPort());
SecureRequestCustomizer src = new SecureRequestCustomizer();
httpConfig.addCustomizer(src);
HttpConnectionFactory httpConnectionFactory = new HttpConnectionFactory(httpConfig);
SslConnectionFactory sslConnectionFactory = new SslConnectionFactory(sslContextFactory, HttpVersion.HTTP_1_1.asString());
ServerConnector portUnifiedConnector = new ServerConnector(this.server,
new DetectorConnectionFactory(sslConnectionFactory),
httpConnectionFactory);
portUnifiedConnector.setHost(Settings.getInstance().getBindAddress());
portUnifiedConnector.setPort(Settings.getInstance().getApiPort());
this.server.addConnector(portUnifiedConnector);
} else {
// Non-SSL
InetAddress bindAddr = InetAddress.getByName(Settings.getInstance().getBindAddress());
InetSocketAddress endpoint = new InetSocketAddress(bindAddr, Settings.getInstance().getApiPort());
this.server = new Server(endpoint);
}
InetAddress bindAddr = InetAddress.getByName(Settings.getInstance().getBindAddress());
InetSocketAddress endpoint = new InetSocketAddress(bindAddr, Settings.getInstance().getApiPort());
this.server = new Server(endpoint);
// Error handler
ErrorHandler errorHandler = new ApiErrorHandler();
@@ -195,14 +123,6 @@ public class ApiService {
rewriteHandler.addRule(new RedirectPatternRule("/api-documentation", "/api-documentation/")); // redirect to add trailing slash if missing
}
context.addServlet(AdminStatusWebSocket.class, "/websockets/admin/status");
context.addServlet(BlocksWebSocket.class, "/websockets/blocks");
context.addServlet(ActiveChatsWebSocket.class, "/websockets/chat/active/*");
context.addServlet(ChatMessagesWebSocket.class, "/websockets/chat/messages");
context.addServlet(TradeOffersWebSocket.class, "/websockets/crosschain/tradeoffers");
context.addServlet(TradeBotWebSocket.class, "/websockets/crosschain/tradebot");
context.addServlet(PresenceWebSocket.class, "/websockets/presence");
// Start server
this.server.start();
} catch (Exception e) {

View File

@@ -1,25 +0,0 @@
package org.qortal.api;
import java.math.BigDecimal;
import javax.xml.bind.annotation.adapters.XmlAdapter;
public class RewardSharePercentTypeAdapter extends XmlAdapter<String, Integer> {
@Override
public Integer unmarshal(String input) throws Exception {
if (input == null)
return null;
return new BigDecimal(input).setScale(2).unscaledValue().intValue();
}
@Override
public String marshal(Integer output) throws Exception {
if (output == null)
return null;
return String.format("%d.%02d", output / 100, Math.abs(output % 100));
}
}

View File

@@ -5,20 +5,10 @@ import java.net.UnknownHostException;
import javax.servlet.http.HttpServletRequest;
import org.qortal.settings.Settings;
public abstract class Security {
public static final String API_KEY_HEADER = "X-API-KEY";
public class Security {
// TODO: replace with proper authentication
public static void checkApiCallAllowed(HttpServletRequest request) {
String expectedApiKey = Settings.getInstance().getApiKey();
String passedApiKey = request.getHeader(API_KEY_HEADER);
if ((expectedApiKey != null && !expectedApiKey.equals(passedApiKey)) ||
(passedApiKey != null && !passedApiKey.equals(expectedApiKey)))
throw ApiExceptionFactory.INSTANCE.createException(request, ApiError.UNAUTHORIZED);
InetAddress remoteAddr;
try {
remoteAddr = InetAddress.getByName(request.getRemoteAddr());
@@ -29,5 +19,4 @@ public abstract class Security {
if (!remoteAddr.isLoopbackAddress())
throw ApiExceptionFactory.INSTANCE.createException(request, ApiError.UNAUTHORIZED);
}
}

View File

@@ -1,6 +1,5 @@
package org.qortal.api.model;
import java.util.Collections;
import java.util.EnumMap;
import java.util.Map;
@@ -14,61 +13,17 @@ import org.qortal.transaction.Transaction.TransactionType;
@XmlAccessorType(XmlAccessType.FIELD)
public class ActivitySummary {
private int blockCount;
private int assetsIssued;
private int namesRegistered;
public int blockCount;
public int transactionCount;
public int assetsIssued;
public int namesRegistered;
// Assuming TransactionType values are contiguous so 'length' equals count
@XmlJavaTypeAdapter(TransactionCountMapXmlAdapter.class)
private Map<TransactionType, Integer> transactionCountByType = new EnumMap<>(TransactionType.class);
private int totalTransactionCount = 0;
public Map<TransactionType, Integer> transactionCountByType = new EnumMap<>(TransactionType.class);
public ActivitySummary() {
// Needed for JAXB
}
public int getBlockCount() {
return this.blockCount;
}
public void setBlockCount(int blockCount) {
this.blockCount = blockCount;
}
public int getTotalTransactionCount() {
return this.totalTransactionCount;
}
public int getAssetsIssued() {
return this.assetsIssued;
}
public void setAssetsIssued(int assetsIssued) {
this.assetsIssued = assetsIssued;
}
public int getNamesRegistered() {
return this.namesRegistered;
}
public void setNamesRegistered(int namesRegistered) {
this.namesRegistered = namesRegistered;
}
public Map<TransactionType, Integer> getTransactionCountByType() {
return Collections.unmodifiableMap(this.transactionCountByType);
}
public void setTransactionCountByType(TransactionType transactionType, int transactionCount) {
this.transactionCountByType.put(transactionType, transactionCount);
this.totalTransactionCount = this.transactionCountByType.values().stream().mapToInt(Integer::intValue).sum();
}
public void setTransactionCountByType(Map<TransactionType, Integer> transactionCountByType) {
this.transactionCountByType = new EnumMap<>(transactionCountByType);
this.totalTransactionCount = this.transactionCountByType.values().stream().mapToInt(Integer::intValue).sum();
}
}

View File

@@ -1,10 +1,11 @@
package org.qortal.api.model;
import java.math.BigDecimal;
import javax.xml.bind.Marshaller;
import javax.xml.bind.annotation.XmlAccessType;
import javax.xml.bind.annotation.XmlAccessorType;
import javax.xml.bind.annotation.XmlElement;
import javax.xml.bind.annotation.adapters.XmlJavaTypeAdapter;
import org.qortal.data.asset.OrderData;
@@ -28,14 +29,12 @@ public class AggregatedOrder {
}
@XmlElement(name = "price")
@XmlJavaTypeAdapter(value = org.qortal.api.AmountTypeAdapter.class)
public long getPrice() {
public BigDecimal getPrice() {
return this.orderData.getPrice();
}
@XmlElement(name = "unfulfilled")
@XmlJavaTypeAdapter(value = org.qortal.api.AmountTypeAdapter.class)
public long getUnfulfilled() {
public BigDecimal getUnfulfilled() {
return this.orderData.getAmount();
}

View File

@@ -6,7 +6,7 @@ import javax.xml.bind.annotation.XmlAccessorType;
import org.qortal.crypto.Crypto;
@XmlAccessorType(XmlAccessType.FIELD)
public class BlockSignerSummary {
public class BlockMinterSummary {
// Properties
@@ -20,19 +20,19 @@ public class BlockSignerSummary {
// Constructors
protected BlockSignerSummary() {
protected BlockMinterSummary() {
}
/** Constructs BlockSignerSummary in non-reward-share context. */
public BlockSignerSummary(byte[] blockMinterPublicKey, int blockCount) {
/** Constructs BlockMinterSummary in non-reward-share context. */
public BlockMinterSummary(byte[] blockMinterPublicKey, int blockCount) {
this.blockCount = blockCount;
this.mintingAccountPublicKey = blockMinterPublicKey;
this.mintingAccount = Crypto.toAddress(this.mintingAccountPublicKey);
}
/** Constructs BlockSignerSummary in reward-share context. */
public BlockSignerSummary(byte[] rewardSharePublicKey, int blockCount, byte[] mintingAccountPublicKey, String minterAccount, String recipientAccount) {
/** Constructs BlockMinterSummary in reward-share context. */
public BlockMinterSummary(byte[] rewardSharePublicKey, int blockCount, byte[] mintingAccountPublicKey, String minterAccount, String recipientAccount) {
this.rewardSharePublicKey = rewardSharePublicKey;
this.blockCount = blockCount;

View File

@@ -25,8 +25,7 @@ public class ConnectedPeer {
public String address;
public String version;
public String nodeId;
public Long buildTimestamp;
public Integer lastHeight;
@Schema(example = "base58")
@@ -46,9 +45,10 @@ public class ConnectedPeer {
this.peersConnectedWhen = peer.getPeersConnectionTimestamp();
this.address = peerData.getAddress().toString();
this.version = peer.getPeersVersionString();
this.nodeId = peer.getPeersNodeId();
if (peer.getVersionMessage() != null) {
this.version = peer.getVersionMessage().getVersionString();
this.buildTimestamp = peer.getVersionMessage().getBuildTimestamp();
}
PeerChainTipData peerChainTipData = peer.getChainTipData();
if (peerChainTipData != null) {

View File

@@ -1,34 +0,0 @@
package org.qortal.api.model;
import java.math.BigDecimal;
import javax.xml.bind.annotation.XmlAccessType;
import javax.xml.bind.annotation.XmlAccessorType;
import io.swagger.v3.oas.annotations.media.Schema;
@XmlAccessorType(XmlAccessType.FIELD)
public class CrossChainBitcoinRedeemRequest {
@Schema(description = "Bitcoin HASH160(public key) for refund", example = "2nGDBPPPFS1c9w1h33YwFk4KUJU2")
public byte[] refundPublicKeyHash;
@Schema(description = "Bitcoin PRIVATE KEY for redeem", example = "cUvGNSnu14q6Hr1X7TESjYVTqBpFjj8GGLGjGdpJwD9NhSQKeYUk")
public byte[] redeemPrivateKey;
@Schema(description = "Qortal AT address")
public String atAddress;
@Schema(description = "Bitcoin miner fee", example = "0.00001000")
public BigDecimal bitcoinMinerFee;
@Schema(description = "32-byte secret", example = "6gVbAXCVzJXAWwtAVGAfgAkkXpeXvPUwSciPmCfSfXJG")
public byte[] secret;
@Schema(description = "Bitcoin HASH160(public key) for receiving funds, or omit to derive from private key", example = "u17kBVKkKSp12oUzaxFwNnq1JZf")
public byte[] receivingAccountInfo;
public CrossChainBitcoinRedeemRequest() {
}
}

View File

@@ -1,31 +0,0 @@
package org.qortal.api.model;
import java.math.BigDecimal;
import javax.xml.bind.annotation.XmlAccessType;
import javax.xml.bind.annotation.XmlAccessorType;
import io.swagger.v3.oas.annotations.media.Schema;
@XmlAccessorType(XmlAccessType.FIELD)
public class CrossChainBitcoinRefundRequest {
@Schema(description = "Bitcoin PRIVATE KEY for refund", example = "cSP3zTb6bfm8GATtAcEJ8LqYtNQmzZ9jE2wQUVnZGiBzojDdrwKV")
public byte[] refundPrivateKey;
@Schema(description = "Bitcoin HASH160(public key) for redeem", example = "2daMveGc5pdjRyFacbxBzMksCbyC")
public byte[] redeemPublicKeyHash;
@Schema(description = "Qortal AT address")
public String atAddress;
@Schema(description = "Bitcoin miner fee", example = "0.00001000")
public BigDecimal bitcoinMinerFee;
@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

@@ -1,23 +0,0 @@
package org.qortal.api.model;
import javax.xml.bind.annotation.XmlAccessType;
import javax.xml.bind.annotation.XmlAccessorType;
import io.swagger.v3.oas.annotations.media.Schema;
@XmlAccessorType(XmlAccessType.FIELD)
public class CrossChainBitcoinTemplateRequest {
@Schema(description = "Bitcoin HASH160(public key) for refund", example = "2nGDBPPPFS1c9w1h33YwFk4KUJU2")
public byte[] refundPublicKeyHash;
@Schema(description = "Bitcoin HASH160(public key) for redeem", example = "2daMveGc5pdjRyFacbxBzMksCbyC")
public byte[] redeemPublicKeyHash;
@Schema(description = "Qortal AT address")
public String atAddress;
public CrossChainBitcoinTemplateRequest() {
}
}

View File

@@ -1,31 +0,0 @@
package org.qortal.api.model;
import java.math.BigDecimal;
import javax.xml.bind.annotation.XmlAccessType;
import javax.xml.bind.annotation.XmlAccessorType;
import io.swagger.v3.oas.annotations.media.Schema;
@XmlAccessorType(XmlAccessType.FIELD)
public class CrossChainBitcoinyHTLCStatus {
@Schema(description = "P2SH address", example = "3CdH27kTpV8dcFHVRYjQ8EEV5FJg9X8pSJ (mainnet), 2fMiRRXVsxhZeyfum9ifybZvaMHbQTmwdZw (testnet)")
public String bitcoinP2shAddress;
@Schema(description = "P2SH balance")
public BigDecimal bitcoinP2shBalance;
@Schema(description = "Can HTLC redeem yet?")
public boolean canRedeem;
@Schema(description = "Can HTLC refund yet?")
public boolean canRefund;
@Schema(description = "Secret used by HTLC redeemer")
public byte[] secret;
public CrossChainBitcoinyHTLCStatus() {
}
}

View File

@@ -1,39 +0,0 @@
package org.qortal.api.model;
import javax.xml.bind.annotation.XmlAccessType;
import javax.xml.bind.annotation.XmlAccessorType;
import javax.xml.bind.annotation.adapters.XmlJavaTypeAdapter;
import io.swagger.v3.oas.annotations.media.Schema;
@XmlAccessorType(XmlAccessType.FIELD)
public class CrossChainBuildRequest {
@Schema(description = "AT creator's public key", example = "C6wuddsBV3HzRrXUtezE7P5MoRXp5m3mEDokRDGZB6ry")
public byte[] creatorPublicKey;
@Schema(description = "Final QORT amount paid out on successful trade", example = "80.40200000")
@XmlJavaTypeAdapter(value = org.qortal.api.AmountTypeAdapter.class)
public long qortAmount;
@Schema(description = "QORT amount funding AT, including covering AT execution fees", example = "123.45670000")
@XmlJavaTypeAdapter(value = org.qortal.api.AmountTypeAdapter.class)
public long fundingQortAmount;
@Schema(description = "HASH160 of creator's Bitcoin public key", example = "2daMveGc5pdjRyFacbxBzMksCbyC")
public byte[] bitcoinPublicKeyHash;
@Schema(description = "HASH160 of secret", example = "43vnftqkjxrhb5kJdkU1ZFQLEnWV")
public byte[] hashOfSecretB;
@Schema(description = "Bitcoin P2SH BTC balance for release of secret", example = "0.00864200")
@XmlJavaTypeAdapter(value = org.qortal.api.AmountTypeAdapter.class)
public long bitcoinAmount;
@Schema(description = "Trade time window (minutes) from trade agreement to automatic refund", example = "10080")
public Integer tradeTimeout;
public CrossChainBuildRequest() {
}
}

View File

@@ -1,20 +0,0 @@
package org.qortal.api.model;
import javax.xml.bind.annotation.XmlAccessType;
import javax.xml.bind.annotation.XmlAccessorType;
import io.swagger.v3.oas.annotations.media.Schema;
@XmlAccessorType(XmlAccessType.FIELD)
public class CrossChainCancelRequest {
@Schema(description = "AT creator's public key", example = "K6wuddsBV3HzRrXFFezE7P5MoRXp5m3mEDokRDGZB6ry")
public byte[] creatorPublicKey;
@Schema(description = "Qortal trade AT address")
public String atAddress;
public CrossChainCancelRequest() {
}
}

View File

@@ -1,127 +0,0 @@
package org.qortal.api.model;
import javax.xml.bind.annotation.XmlAccessType;
import javax.xml.bind.annotation.XmlAccessorType;
import javax.xml.bind.annotation.adapters.XmlJavaTypeAdapter;
import org.qortal.crosschain.AcctMode;
import org.qortal.data.crosschain.CrossChainTradeData;
import io.swagger.v3.oas.annotations.media.Schema;
// All properties to be converted to JSON via JAXB
@XmlAccessorType(XmlAccessType.FIELD)
public class CrossChainOfferSummary {
// Properties
@Schema(description = "AT's Qortal address")
private String qortalAtAddress;
@Schema(description = "AT creator's Qortal address")
private String qortalCreator;
@Schema(description = "AT creator's ephemeral trading key-pair represented as Qortal address")
private String qortalCreatorTradeAddress;
@XmlJavaTypeAdapter(value = org.qortal.api.AmountTypeAdapter.class)
private long qortAmount;
@Schema(description = "Bitcoin amount - DEPRECATED: use foreignAmount")
@XmlJavaTypeAdapter(value = org.qortal.api.AmountTypeAdapter.class)
@Deprecated
private long btcAmount;
@Schema(description = "Foreign blockchain amount")
@XmlJavaTypeAdapter(value = org.qortal.api.AmountTypeAdapter.class)
private long foreignAmount;
@Schema(description = "Suggested trade timeout (minutes)", example = "10080")
private int tradeTimeout;
@Schema(description = "Current AT execution mode")
private AcctMode mode;
private long timestamp;
@Schema(description = "Trade partner's Qortal receiving address")
private String partnerQortalReceivingAddress;
private String foreignBlockchain;
private String acctName;
protected CrossChainOfferSummary() {
/* For JAXB */
}
public CrossChainOfferSummary(CrossChainTradeData crossChainTradeData, long timestamp) {
this.qortalAtAddress = crossChainTradeData.qortalAtAddress;
this.qortalCreator = crossChainTradeData.qortalCreator;
this.qortalCreatorTradeAddress = crossChainTradeData.qortalCreatorTradeAddress;
this.qortAmount = crossChainTradeData.qortAmount;
this.foreignAmount = crossChainTradeData.expectedForeignAmount;
this.btcAmount = this.foreignAmount; // Duplicate for deprecated field
this.tradeTimeout = crossChainTradeData.tradeTimeout;
this.mode = crossChainTradeData.mode;
this.timestamp = timestamp;
this.partnerQortalReceivingAddress = crossChainTradeData.qortalPartnerReceivingAddress;
this.foreignBlockchain = crossChainTradeData.foreignBlockchain;
this.acctName = crossChainTradeData.acctName;
}
public String getQortalAtAddress() {
return this.qortalAtAddress;
}
public String getQortalCreator() {
return this.qortalCreator;
}
public String getQortalCreatorTradeAddress() {
return this.qortalCreatorTradeAddress;
}
public long getQortAmount() {
return this.qortAmount;
}
public long getBtcAmount() {
return this.btcAmount;
}
public long getForeignAmount() {
return this.foreignAmount;
}
public int getTradeTimeout() {
return this.tradeTimeout;
}
public AcctMode getMode() {
return this.mode;
}
public long getTimestamp() {
return this.timestamp;
}
public String getPartnerQortalReceivingAddress() {
return this.partnerQortalReceivingAddress;
}
public String getForeignBlockchain() {
return this.foreignBlockchain;
}
public String getAcctName() {
return this.acctName;
}
// For debugging mostly
public String toString() {
return String.format("%s: %s", this.qortalAtAddress, this.mode);
}
}

View File

@@ -1,29 +0,0 @@
package org.qortal.api.model;
import javax.xml.bind.annotation.XmlAccessType;
import javax.xml.bind.annotation.XmlAccessorType;
import io.swagger.v3.oas.annotations.media.Schema;
@XmlAccessorType(XmlAccessType.FIELD)
public class CrossChainSecretRequest {
@Schema(description = "Public key to match AT's trade 'partner'", example = "C6wuddsBV3HzRrXUtezE7P5MoRXp5m3mEDokRDGZB6ry")
public byte[] partnerPublicKey;
@Schema(description = "Qortal AT address")
public String atAddress;
@Schema(description = "secret-A (32 bytes)", example = "FHMzten4he9jZ4HGb4297Utj6F5g2w7serjq2EnAg2s1")
public byte[] secretA;
@Schema(description = "secret-B (32 bytes)", example = "EN2Bgx3BcEMtxFCewmCVSMkfZjVKYhx3KEXC5A21KBGx")
public byte[] secretB;
@Schema(description = "Qortal address for receiving QORT from AT")
public String receivingAddress;
public CrossChainSecretRequest() {
}
}

View File

@@ -1,23 +0,0 @@
package org.qortal.api.model;
import javax.xml.bind.annotation.XmlAccessType;
import javax.xml.bind.annotation.XmlAccessorType;
import io.swagger.v3.oas.annotations.media.Schema;
@XmlAccessorType(XmlAccessType.FIELD)
public class CrossChainTradeRequest {
@Schema(description = "AT creator's 'trade' public key", example = "C6wuddsBV3HzRrXUtezE7P5MoRXp5m3mEDokRDGZB6ry")
public byte[] tradePublicKey;
@Schema(description = "Qortal AT address")
public String atAddress;
@Schema(description = "Signature of trading partner's 'offer' MESSAGE transaction")
public byte[] messageTransactionSignature;
public CrossChainTradeRequest() {
}
}

View File

@@ -1,54 +0,0 @@
package org.qortal.api.model;
import javax.xml.bind.annotation.XmlAccessType;
import javax.xml.bind.annotation.XmlAccessorType;
import javax.xml.bind.annotation.adapters.XmlJavaTypeAdapter;
import org.qortal.data.crosschain.CrossChainTradeData;
import io.swagger.v3.oas.annotations.media.Schema;
// All properties to be converted to JSON via JAXB
@XmlAccessorType(XmlAccessType.FIELD)
public class CrossChainTradeSummary {
private long tradeTimestamp;
@XmlJavaTypeAdapter(value = org.qortal.api.AmountTypeAdapter.class)
private long qortAmount;
@Deprecated
@Schema(description = "DEPRECATED: use foreignAmount instead")
@XmlJavaTypeAdapter(value = org.qortal.api.AmountTypeAdapter.class)
private long btcAmount;
@XmlJavaTypeAdapter(value = org.qortal.api.AmountTypeAdapter.class)
private long foreignAmount;
protected CrossChainTradeSummary() {
/* For JAXB */
}
public CrossChainTradeSummary(CrossChainTradeData crossChainTradeData, long timestamp) {
this.tradeTimestamp = timestamp;
this.qortAmount = crossChainTradeData.qortAmount;
this.foreignAmount = crossChainTradeData.expectedForeignAmount;
this.btcAmount = this.foreignAmount;
}
public long getTradeTimestamp() {
return this.tradeTimestamp;
}
public long getQortAmount() {
return this.qortAmount;
}
public long getBtcAmount() {
return this.btcAmount;
}
public long getForeignAmount() {
return this.foreignAmount;
}
}

View File

@@ -10,8 +10,6 @@ public class NodeInfo {
public long uptime;
public String buildVersion;
public long buildTimestamp;
public String nodeId;
public boolean isTestNet;
public NodeInfo() {
}

View File

@@ -3,31 +3,16 @@ package org.qortal.api.model;
import javax.xml.bind.annotation.XmlAccessType;
import javax.xml.bind.annotation.XmlAccessorType;
import org.qortal.controller.Controller;
import org.qortal.network.Network;
@XmlAccessorType(XmlAccessType.FIELD)
public class NodeStatus {
public final boolean isMintingPossible;
public final boolean isSynchronizing;
public boolean isMintingPossible;
public boolean isSynchronizing;
// Not always present
public final Integer syncPercent;
public final int numberOfConnections;
public final int height;
public Integer syncPercent;
public NodeStatus() {
this.isMintingPossible = Controller.getInstance().isMintingPossible();
this.syncPercent = Controller.getInstance().getSyncPercent();
this.isSynchronizing = this.syncPercent != null;
this.numberOfConnections = Network.getInstance().getHandshakedPeers().size();
this.height = Controller.getInstance().getChainHeight();
}
}

View File

@@ -1,157 +0,0 @@
package org.qortal.api.model;
import java.util.ArrayList;
import java.util.Collections;
import java.util.List;
import java.util.Objects;
import javax.xml.bind.annotation.XmlAccessType;
import javax.xml.bind.annotation.XmlAccessorType;
@XmlAccessorType(XmlAccessType.FIELD)
public class SimpleForeignTransaction {
public static class AddressAmount {
public final String address;
public final long amount;
protected AddressAmount() {
/* For JAXB */
this.address = null;
this.amount = 0;
}
public AddressAmount(String address, long amount) {
this.address = address;
this.amount = amount;
}
}
private String txHash;
private long timestamp;
private List<AddressAmount> inputs;
public static class Output {
public final List<String> addresses;
public final long amount;
protected Output() {
/* For JAXB */
this.addresses = null;
this.amount = 0;
}
public Output(List<String> addresses, long amount) {
this.addresses = addresses;
this.amount = amount;
}
}
private List<Output> outputs;
private long totalAmount;
private long fees;
private Boolean isSentNotReceived;
protected SimpleForeignTransaction() {
/* For JAXB */
}
private SimpleForeignTransaction(Builder builder) {
this.txHash = builder.txHash;
this.timestamp = builder.timestamp;
this.inputs = Collections.unmodifiableList(builder.inputs);
this.outputs = Collections.unmodifiableList(builder.outputs);
Objects.requireNonNull(this.txHash);
if (timestamp <= 0)
throw new IllegalArgumentException("timestamp must be positive");
long totalGrossAmount = this.inputs.stream().map(addressAmount -> addressAmount.amount).reduce(0L, Long::sum);
this.totalAmount = this.outputs.stream().map(addressAmount -> addressAmount.amount).reduce(0L, Long::sum);
this.fees = totalGrossAmount - this.totalAmount;
this.isSentNotReceived = builder.isSentNotReceived;
}
public String getTxHash() {
return this.txHash;
}
public long getTimestamp() {
return this.timestamp;
}
public List<AddressAmount> getInputs() {
return this.inputs;
}
public List<Output> getOutputs() {
return this.outputs;
}
public long getTotalAmount() {
return this.totalAmount;
}
public long getFees() {
return this.fees;
}
public Boolean isSentNotReceived() {
return this.isSentNotReceived;
}
public static class Builder {
private String txHash;
private long timestamp;
private List<AddressAmount> inputs = new ArrayList<>();
private List<Output> outputs = new ArrayList<>();
private Boolean isSentNotReceived;
public Builder txHash(String txHash) {
this.txHash = Objects.requireNonNull(txHash);
return this;
}
public Builder timestamp(long timestamp) {
if (timestamp <= 0)
throw new IllegalArgumentException("timestamp must be positive");
this.timestamp = timestamp;
return this;
}
public Builder input(String address, long amount) {
Objects.requireNonNull(address);
if (amount < 0)
throw new IllegalArgumentException("amount must be zero or positive");
AddressAmount input = new AddressAmount(address, amount);
inputs.add(input);
return this;
}
public Builder output(List<String> addresses, long amount) {
Objects.requireNonNull(addresses);
if (amount < 0)
throw new IllegalArgumentException("amount must be zero or positive");
Output output = new Output(addresses, amount);
outputs.add(output);
return this;
}
public Builder isSentNotReceived(Boolean isSentNotReceived) {
this.isSentNotReceived = isSentNotReceived;
return this;
}
public SimpleForeignTransaction build() {
return new SimpleForeignTransaction(this);
}
}
}

View File

@@ -1,29 +0,0 @@
package org.qortal.api.model.crosschain;
import javax.xml.bind.annotation.XmlAccessType;
import javax.xml.bind.annotation.XmlAccessorType;
import javax.xml.bind.annotation.adapters.XmlJavaTypeAdapter;
import io.swagger.v3.oas.annotations.media.Schema;
@XmlAccessorType(XmlAccessType.FIELD)
public class BitcoinSendRequest {
@Schema(description = "Bitcoin BIP32 extended private key", example = "tprv___________________________________________________________________________________________________________")
public String xprv58;
@Schema(description = "Recipient's Bitcoin address ('legacy' P2PKH only)", example = "1BitcoinEaterAddressDontSendf59kuE")
public String receivingAddress;
@Schema(description = "Amount of BTC to send", type = "number")
@XmlJavaTypeAdapter(value = org.qortal.api.AmountTypeAdapter.class)
public long bitcoinAmount;
@Schema(description = "Transaction fee per byte (optional). Default is 0.00000100 BTC (100 sats) per byte", example = "0.00000100", type = "number")
@XmlJavaTypeAdapter(value = org.qortal.api.AmountTypeAdapter.class)
public Long feePerByte;
public BitcoinSendRequest() {
}
}

View File

@@ -1,29 +0,0 @@
package org.qortal.api.model.crosschain;
import javax.xml.bind.annotation.XmlAccessType;
import javax.xml.bind.annotation.XmlAccessorType;
import javax.xml.bind.annotation.adapters.XmlJavaTypeAdapter;
import io.swagger.v3.oas.annotations.media.Schema;
@XmlAccessorType(XmlAccessType.FIELD)
public class LitecoinSendRequest {
@Schema(description = "Litecoin BIP32 extended private key", example = "tprv___________________________________________________________________________________________________________")
public String xprv58;
@Schema(description = "Recipient's Litecoin address ('legacy' P2PKH only)", example = "LiTecoinEaterAddressDontSendhLfzKD")
public String receivingAddress;
@Schema(description = "Amount of LTC to send", type = "number")
@XmlJavaTypeAdapter(value = org.qortal.api.AmountTypeAdapter.class)
public long litecoinAmount;
@Schema(description = "Transaction fee per byte (optional). Default is 0.00000100 LTC (100 sats) per byte", example = "0.00000100", type = "number")
@XmlJavaTypeAdapter(value = org.qortal.api.AmountTypeAdapter.class)
public Long feePerByte;
public LitecoinSendRequest() {
}
}

View File

@@ -1,46 +0,0 @@
package org.qortal.api.model.crosschain;
import javax.xml.bind.annotation.XmlAccessType;
import javax.xml.bind.annotation.XmlAccessorType;
import javax.xml.bind.annotation.adapters.XmlJavaTypeAdapter;
import org.qortal.crosschain.SupportedBlockchain;
import io.swagger.v3.oas.annotations.media.Schema;
@XmlAccessorType(XmlAccessType.FIELD)
public class TradeBotCreateRequest {
@Schema(description = "Trade creator's public key", example = "2zR1WFsbM7akHghqSCYKBPk6LDP8aKiQSRS1FrwoLvoB")
public byte[] creatorPublicKey;
@Schema(description = "QORT amount paid out on successful trade", example = "80.40000000", type = "number")
@XmlJavaTypeAdapter(value = org.qortal.api.AmountTypeAdapter.class)
public long qortAmount;
@Schema(description = "QORT amount funding AT, including covering AT execution fees", example = "80.50000000", type = "number")
@XmlJavaTypeAdapter(value = org.qortal.api.AmountTypeAdapter.class)
public long fundingQortAmount;
@Deprecated
@Schema(description = "Bitcoin amount wanted in return. DEPRECATED: use foreignAmount instead", example = "0.00864200", type = "number", hidden = true)
@XmlJavaTypeAdapter(value = org.qortal.api.AmountTypeAdapter.class)
public Long bitcoinAmount;
@Schema(description = "Foreign blockchain. Note: default (BITCOIN) to be removed in the future", example = "BITCOIN", implementation = SupportedBlockchain.class)
public SupportedBlockchain foreignBlockchain;
@Schema(description = "Foreign blockchain amount wanted in return", example = "0.00864200", type = "number")
@XmlJavaTypeAdapter(value = org.qortal.api.AmountTypeAdapter.class)
public Long foreignAmount;
@Schema(description = "Suggested trade timeout (minutes)", example = "10080")
public int tradeTimeout;
@Schema(description = "Foreign blockchain address for receiving", example = "1BitcoinEaterAddressDontSendf59kuE")
public String receivingAddress;
public TradeBotCreateRequest() {
}
}

View File

@@ -1,29 +0,0 @@
package org.qortal.api.model.crosschain;
import javax.xml.bind.annotation.XmlAccessType;
import javax.xml.bind.annotation.XmlAccessorType;
import io.swagger.v3.oas.annotations.media.Schema;
@XmlAccessorType(XmlAccessType.FIELD)
public class TradeBotRespondRequest {
@Schema(description = "Qortal AT address", example = "Aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa")
public String atAddress;
@Deprecated
@Schema(description = "Bitcoin BIP32 extended private key. DEPRECATED: use foreignKey instead", hidden = true,
example = "xprv___________________________________________________________________________________________________________")
public String xprv58;
@Schema(description = "Foreign blockchain private key, e.g. BIP32 'm' key for Bitcoin/Litecoin starting with 'xprv'",
example = "xprv___________________________________________________________________________________________________________")
public String foreignKey;
@Schema(description = "Qortal address for receiving QORT from AT", example = "Qqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqq")
public String receivingAddress;
public TradeBotRespondRequest() {
}
}

View File

@@ -7,7 +7,6 @@ import io.swagger.v3.oas.annotations.media.Content;
import io.swagger.v3.oas.annotations.media.Schema;
import io.swagger.v3.oas.annotations.parameters.RequestBody;
import io.swagger.v3.oas.annotations.responses.ApiResponse;
import io.swagger.v3.oas.annotations.security.SecurityRequirement;
import io.swagger.v3.oas.annotations.tags.Tag;
import java.math.BigDecimal;
@@ -29,7 +28,6 @@ import org.qortal.api.ApiError;
import org.qortal.api.ApiErrors;
import org.qortal.api.ApiException;
import org.qortal.api.ApiExceptionFactory;
import org.qortal.api.Security;
import org.qortal.api.model.ApiOnlineAccount;
import org.qortal.api.model.RewardShareKeyRequest;
import org.qortal.asset.Asset;
@@ -38,27 +36,18 @@ import org.qortal.crypto.Crypto;
import org.qortal.data.account.AccountData;
import org.qortal.data.account.RewardShareData;
import org.qortal.data.network.OnlineAccountData;
import org.qortal.data.transaction.PublicizeTransactionData;
import org.qortal.data.transaction.RewardShareTransactionData;
import org.qortal.data.transaction.TransactionData;
import org.qortal.repository.DataException;
import org.qortal.repository.Repository;
import org.qortal.repository.RepositoryManager;
import org.qortal.settings.Settings;
import org.qortal.transaction.PublicizeTransaction;
import org.qortal.transaction.Transaction;
import org.qortal.transaction.Transaction.TransactionType;
import org.qortal.transaction.Transaction.ValidationResult;
import org.qortal.transform.TransformationException;
import org.qortal.transform.Transformer;
import org.qortal.transform.transaction.PublicizeTransactionTransformer;
import org.qortal.transform.transaction.RewardShareTransactionTransformer;
import org.qortal.transform.transaction.TransactionTransformer;
import org.qortal.utils.Amounts;
import org.qortal.utils.Base58;
import com.google.common.primitives.Bytes;
@Path("/addresses")
@Tag(name = "Addresses")
public class AddressesResource {
@@ -77,18 +66,32 @@ public class AddressesResource {
)
}
)
@ApiErrors({ApiError.INVALID_ADDRESS, ApiError.ADDRESS_UNKNOWN, ApiError.REPOSITORY_ISSUE})
@ApiErrors({ApiError.INVALID_ADDRESS, ApiError.REPOSITORY_ISSUE})
public AccountData getAccountInfo(@PathParam("address") String address) {
if (!Crypto.isValidAddress(address))
throw ApiExceptionFactory.INSTANCE.createException(request, ApiError.INVALID_ADDRESS);
try (final Repository repository = RepositoryManager.getRepository()) {
AccountData accountData = repository.getAccountRepository().getAccount(address);
// Not found?
if (accountData == null)
throw ApiExceptionFactory.INSTANCE.createException(request, ApiError.ADDRESS_UNKNOWN);
accountData = new AccountData(address);
else {
// Unconfirmed transactions could update lastReference
Account account = new Account(repository, address);
// Use last reference based on unconfirmed transactions if possible
byte[] unconfirmedLastReference = account.getUnconfirmedLastReference();
if (unconfirmedLastReference != null)
// There are unconfirmed transactions so modify returned data
accountData.setReference(unconfirmedLastReference);
}
return accountData;
} catch (ApiException e) {
throw e;
} catch (DataException e) {
throw ApiExceptionFactory.INSTANCE.createException(request, ApiError.REPOSITORY_ISSUE, e);
}
@@ -97,37 +100,42 @@ public class AddressesResource {
@GET
@Path("/lastreference/{address}")
@Operation(
summary = "Fetch reference for next transaction to be created by address",
description = "Returns the base58-encoded signature of the last confirmed transaction created by address, failing that: the first incoming transaction. Returns \"false\" if there is no last-reference.",
summary = "Fetch reference for next transaction to be created by address, considering unconfirmed transactions",
description = "Returns the base58-encoded signature of the last confirmed/unconfirmed transaction created by address, failing that: the first incoming transaction. Returns \"false\" if there is no transactions.",
responses = {
@ApiResponse(
description = "the base58-encoded last-reference",
description = "the base58-encoded transaction signature",
content = @Content(mediaType = MediaType.TEXT_PLAIN, schema = @Schema(type = "string"))
)
}
)
@ApiErrors({ApiError.INVALID_ADDRESS, ApiError.ADDRESS_UNKNOWN, ApiError.REPOSITORY_ISSUE})
public String getLastReference(@PathParam("address") String address) {
@ApiErrors({ApiError.INVALID_ADDRESS, ApiError.REPOSITORY_ISSUE})
public String getLastReferenceUnconfirmed(@PathParam("address") String address) {
if (!Crypto.isValidAddress(address))
throw ApiExceptionFactory.INSTANCE.createException(request, ApiError.INVALID_ADDRESS);
byte[] lastReference = null;
try (final Repository repository = RepositoryManager.getRepository()) {
AccountData accountData = repository.getAccountRepository().getAccount(address);
// Not found?
if (accountData == null)
throw ApiExceptionFactory.INSTANCE.createException(request, ApiError.ADDRESS_UNKNOWN);
Account account = new Account(repository, address);
lastReference = accountData.getReference();
// Use last reference based on unconfirmed transactions if possible
lastReference = account.getUnconfirmedLastReference();
if (lastReference == null)
// No unconfirmed transactions so fallback to using one save in account data
lastReference = account.getLastReference();
} catch (ApiException e) {
throw e;
} catch (DataException e) {
throw ApiExceptionFactory.INSTANCE.createException(request, ApiError.REPOSITORY_ISSUE, e);
}
if (lastReference == null || lastReference.length == 0)
if(lastReference == null || lastReference.length == 0) {
return "false";
return Base58.encode(lastReference);
} else {
return Base58.encode(lastReference);
}
}
@GET
@@ -206,7 +214,7 @@ public class AddressesResource {
else if (!repository.getAssetRepository().assetExists(assetId))
throw ApiExceptionFactory.INSTANCE.createException(request, ApiError.INVALID_ASSET_ID);
return Amounts.toBigDecimal(account.getConfirmedBalance(assetId));
return account.getBalance(assetId);
} catch (ApiException e) {
throw e;
} catch (DataException e) {
@@ -400,120 +408,4 @@ public class AddressesResource {
}
}
@POST
@Path("/publicize")
@Operation(
summary = "Build raw, unsigned, PUBLICIZE transaction",
requestBody = @RequestBody(
required = true,
content = @Content(
mediaType = MediaType.APPLICATION_JSON,
schema = @Schema(
implementation = PublicizeTransactionData.class
)
)
),
responses = {
@ApiResponse(
description = "raw, unsigned, PUBLICIZE transaction encoded in Base58",
content = @Content(
mediaType = MediaType.TEXT_PLAIN,
schema = @Schema(
type = "string"
)
)
)
}
)
@ApiErrors({ApiError.NON_PRODUCTION, ApiError.TRANSACTION_INVALID, ApiError.TRANSFORMATION_ERROR, ApiError.REPOSITORY_ISSUE})
public String publicize(PublicizeTransactionData transactionData) {
if (Settings.getInstance().isApiRestricted())
throw ApiExceptionFactory.INSTANCE.createException(request, ApiError.NON_PRODUCTION);
try (final Repository repository = RepositoryManager.getRepository()) {
Transaction transaction = Transaction.fromData(repository, transactionData);
ValidationResult result = transaction.isValidUnconfirmed();
if (result != ValidationResult.OK && result != ValidationResult.INCORRECT_NONCE)
throw TransactionsResource.createTransactionInvalidException(request, result);
byte[] bytes = PublicizeTransactionTransformer.toBytes(transactionData);
return Base58.encode(bytes);
} catch (TransformationException e) {
throw ApiExceptionFactory.INSTANCE.createException(request, ApiError.TRANSFORMATION_ERROR, e);
} catch (DataException e) {
throw ApiExceptionFactory.INSTANCE.createException(request, ApiError.REPOSITORY_ISSUE, e);
}
}
@POST
@Path("/publicize/compute")
@Operation(
summary = "Compute nonce for raw, unsigned PUBLICIZE transaction",
requestBody = @RequestBody(
required = true,
content = @Content(
mediaType = MediaType.TEXT_PLAIN,
schema = @Schema(
type = "string",
description = "raw, unsigned PUBLICIZE transaction in base58 encoding",
example = "raw transaction base58"
)
)
),
responses = {
@ApiResponse(
description = "raw, unsigned, PUBLICIZE transaction encoded in Base58",
content = @Content(
mediaType = MediaType.TEXT_PLAIN,
schema = @Schema(
type = "string"
)
)
)
}
)
@ApiErrors({ApiError.TRANSACTION_INVALID, ApiError.INVALID_DATA, ApiError.TRANSFORMATION_ERROR, ApiError.REPOSITORY_ISSUE})
@SecurityRequirement(name = "apiKey")
public String computePublicize(String rawBytes58) {
Security.checkApiCallAllowed(request);
try (final Repository repository = RepositoryManager.getRepository()) {
byte[] rawBytes = Base58.decode(rawBytes58);
// We're expecting unsigned transaction, so append empty signature prior to decoding
rawBytes = Bytes.concat(rawBytes, new byte[TransactionTransformer.SIGNATURE_LENGTH]);
TransactionData transactionData = TransactionTransformer.fromBytes(rawBytes);
if (transactionData == null)
throw ApiExceptionFactory.INSTANCE.createException(request, ApiError.INVALID_DATA);
if (transactionData.getType() != TransactionType.PUBLICIZE)
throw ApiExceptionFactory.INSTANCE.createException(request, ApiError.INVALID_DATA);
PublicizeTransaction publicizeTransaction = (PublicizeTransaction) Transaction.fromData(repository, transactionData);
// Quicker validity check first before we compute nonce
ValidationResult result = publicizeTransaction.isValid();
if (result != ValidationResult.OK && result != ValidationResult.INCORRECT_NONCE)
throw TransactionsResource.createTransactionInvalidException(request, result);
publicizeTransaction.computeNonce();
// Re-check, but ignores signature
result = publicizeTransaction.isValidUnconfirmed();
if (result != ValidationResult.OK)
throw TransactionsResource.createTransactionInvalidException(request, result);
// Strip zeroed signature
transactionData.setSignature(null);
byte[] bytes = PublicizeTransactionTransformer.toBytes(transactionData);
return Base58.encode(bytes);
} catch (TransformationException e) {
throw ApiExceptionFactory.INSTANCE.createException(request, ApiError.TRANSFORMATION_ERROR, e);
} catch (DataException e) {
throw ApiExceptionFactory.INSTANCE.createException(request, ApiError.REPOSITORY_ISSUE, e);
}
}
}

View File

@@ -8,7 +8,6 @@ import io.swagger.v3.oas.annotations.media.Content;
import io.swagger.v3.oas.annotations.media.Schema;
import io.swagger.v3.oas.annotations.parameters.RequestBody;
import io.swagger.v3.oas.annotations.responses.ApiResponse;
import io.swagger.v3.oas.annotations.security.SecurityRequirement;
import io.swagger.v3.oas.annotations.tags.Tag;
import java.io.IOException;
@@ -41,6 +40,7 @@ import org.qortal.account.Account;
import org.qortal.account.PrivateKeyAccount;
import org.qortal.api.ApiError;
import org.qortal.api.ApiErrors;
import org.qortal.api.ApiException;
import org.qortal.api.ApiExceptionFactory;
import org.qortal.api.Security;
import org.qortal.api.model.ActivitySummary;
@@ -57,7 +57,6 @@ import org.qortal.network.PeerAddress;
import org.qortal.repository.DataException;
import org.qortal.repository.Repository;
import org.qortal.repository.RepositoryManager;
import org.qortal.settings.Settings;
import org.qortal.utils.Base58;
import org.qortal.utils.NTP;
@@ -118,8 +117,6 @@ public class AdminResource {
nodeInfo.uptime = System.currentTimeMillis() - Controller.startTime;
nodeInfo.buildVersion = Controller.getInstance().getVersionString();
nodeInfo.buildTimestamp = Controller.getInstance().getBuildTimestamp();
nodeInfo.nodeId = Network.getInstance().getOurNodeId();
nodeInfo.isTestNet = Settings.getInstance().isTestNet();
return nodeInfo;
}
@@ -134,12 +131,17 @@ public class AdminResource {
)
}
)
@SecurityRequirement(name = "apiKey")
public NodeStatus status() {
Security.checkApiCallAllowed(request);
NodeStatus nodeStatus = new NodeStatus();
nodeStatus.isMintingPossible = Controller.getInstance().isMintingPossible();
nodeStatus.isSynchronizing = Controller.getInstance().isSynchronizing();
if (nodeStatus.isSynchronizing)
nodeStatus.syncPercent = Controller.getInstance().getSyncPercent();
return nodeStatus;
}
@@ -155,7 +157,6 @@ public class AdminResource {
)
}
)
@SecurityRequirement(name = "apiKey")
public String shutdown() {
Security.checkApiCallAllowed(request);
@@ -184,10 +185,7 @@ public class AdminResource {
}
)
@ApiErrors({ApiError.REPOSITORY_ISSUE})
@SecurityRequirement(name = "apiKey")
public ActivitySummary summary() {
Security.checkApiCallAllowed(request);
ActivitySummary summary = new ActivitySummary();
LocalDate date = LocalDate.now();
@@ -199,13 +197,16 @@ public class AdminResource {
int startHeight = repository.getBlockRepository().getHeightFromTimestamp(start);
int endHeight = repository.getBlockRepository().getBlockchainHeight();
summary.setBlockCount(endHeight - startHeight);
summary.blockCount = endHeight - startHeight;
summary.setTransactionCountByType(repository.getTransactionRepository().getTransactionSummary(startHeight + 1, endHeight));
summary.transactionCountByType = repository.getTransactionRepository().getTransactionSummary(startHeight + 1, endHeight);
summary.setAssetsIssued(repository.getAssetRepository().getRecentAssetIds(start).size());
for (Integer count : summary.transactionCountByType.values())
summary.transactionCount += count;
summary.setNamesRegistered (repository.getNameRepository().getRecentNames(start).size());
summary.assetsIssued = repository.getAssetRepository().getRecentAssetIds(start).size();
summary.namesRegistered = repository.getNameRepository().getRecentNames(start).size();
return summary;
} catch (DataException e) {
@@ -213,30 +214,6 @@ public class AdminResource {
}
}
@GET
@Path("/enginestats")
@Operation(
summary = "Fetch statistics snapshot for core engine",
responses = {
@ApiResponse(
content = @Content(
mediaType = MediaType.APPLICATION_JSON,
array = @ArraySchema(
schema = @Schema(
implementation = Controller.StatsSnapshot.class
)
)
)
)
}
)
@SecurityRequirement(name = "apiKey")
public Controller.StatsSnapshot getEngineStats() {
Security.checkApiCallAllowed(request);
return Controller.getInstance().getStatsSnapshot();
}
@GET
@Path("/mintingaccounts")
@Operation(
@@ -249,7 +226,6 @@ public class AdminResource {
}
)
@ApiErrors({ApiError.REPOSITORY_ISSUE})
@SecurityRequirement(name = "apiKey")
public List<MintingAccountData> getMintingAccounts() {
Security.checkApiCallAllowed(request);
@@ -296,7 +272,6 @@ public class AdminResource {
}
)
@ApiErrors({ApiError.INVALID_PRIVATE_KEY, ApiError.REPOSITORY_ISSUE, ApiError.CANNOT_MINT})
@SecurityRequirement(name = "apiKey")
public String addMintingAccount(String seed58) {
Security.checkApiCallAllowed(request);
@@ -332,13 +307,13 @@ public class AdminResource {
@DELETE
@Path("/mintingaccounts")
@Operation(
summary = "Remove account/reward-share from use by BlockMinter, using public or private key",
summary = "Remove account/reward-share from use by BlockMinter, using private key",
requestBody = @RequestBody(
required = true,
content = @Content(
mediaType = MediaType.TEXT_PLAIN,
schema = @Schema(
type = "string", example = "public or private key"
type = "string", example = "private key"
)
)
),
@@ -349,14 +324,13 @@ public class AdminResource {
}
)
@ApiErrors({ApiError.INVALID_PRIVATE_KEY, ApiError.REPOSITORY_ISSUE})
@SecurityRequirement(name = "apiKey")
public String deleteMintingAccount(String key58) {
public String deleteMintingAccount(String seed58) {
Security.checkApiCallAllowed(request);
try (final Repository repository = RepositoryManager.getRepository()) {
byte[] key = Base58.decode(key58.trim());
byte[] seed = Base58.decode(seed58.trim());
if (repository.getAccountRepository().delete(key) == 0)
if (repository.getAccountRepository().delete(seed) == 0)
return "false";
repository.saveChanges();
@@ -449,7 +423,6 @@ public class AdminResource {
}
)
@ApiErrors({ApiError.INVALID_HEIGHT, ApiError.REPOSITORY_ISSUE})
@SecurityRequirement(name = "apiKey")
public String orphan(String targetHeightString) {
Security.checkApiCallAllowed(request);
@@ -467,6 +440,8 @@ public class AdminResource {
throw ApiExceptionFactory.INSTANCE.createException(request, ApiError.REPOSITORY_ISSUE, e);
} catch (NumberFormatException e) {
throw ApiExceptionFactory.INSTANCE.createException(request, ApiError.INVALID_HEIGHT);
} catch (ApiException e) {
throw e;
}
}
@@ -491,7 +466,6 @@ public class AdminResource {
}
)
@ApiErrors({ApiError.INVALID_DATA, ApiError.REPOSITORY_ISSUE})
@SecurityRequirement(name = "apiKey")
public String forceSync(String targetPeerAddress) {
Security.checkApiCallAllowed(request);
@@ -523,6 +497,8 @@ public class AdminResource {
return syncResult.name();
} catch (IllegalArgumentException e) {
throw ApiExceptionFactory.INSTANCE.createException(request, ApiError.INVALID_DATA);
} catch (ApiException e) {
throw e;
} catch (UnknownHostException e) {
throw ApiExceptionFactory.INSTANCE.createException(request, ApiError.INVALID_DATA);
} catch (InterruptedException e) {
@@ -530,172 +506,4 @@ public class AdminResource {
}
}
@GET
@Path("/repository/data")
@Operation(
summary = "Export sensitive/node-local data from repository.",
description = "Exports data to .script files on local machine"
)
@ApiErrors({ApiError.INVALID_DATA, ApiError.REPOSITORY_ISSUE})
@SecurityRequirement(name = "apiKey")
public String exportRepository() {
Security.checkApiCallAllowed(request);
try (final Repository repository = RepositoryManager.getRepository()) {
ReentrantLock blockchainLock = Controller.getInstance().getBlockchainLock();
blockchainLock.lockInterruptibly();
try {
repository.exportNodeLocalData();
return "true";
} finally {
blockchainLock.unlock();
}
} catch (InterruptedException e) {
// We couldn't lock blockchain to perform export
return "false";
} catch (DataException e) {
throw ApiExceptionFactory.INSTANCE.createException(request, ApiError.REPOSITORY_ISSUE, e);
}
}
@POST
@Path("/repository/data")
@Operation(
summary = "Import data into repository.",
description = "Imports data from file on local machine. Filename is forced to 'import.script' if apiKey is not set.",
requestBody = @RequestBody(
required = true,
content = @Content(
mediaType = MediaType.TEXT_PLAIN,
schema = @Schema(
type = "string", example = "MintingAccounts.script"
)
)
),
responses = {
@ApiResponse(
description = "\"true\"",
content = @Content(mediaType = MediaType.TEXT_PLAIN, schema = @Schema(type = "string"))
)
}
)
@ApiErrors({ApiError.REPOSITORY_ISSUE})
@SecurityRequirement(name = "apiKey")
public String importRepository(String filename) {
Security.checkApiCallAllowed(request);
// Hard-coded because it's too dangerous to allow user-supplied filenames in weaker security contexts
if (Settings.getInstance().getApiKey() == null)
filename = "import.script";
try (final Repository repository = RepositoryManager.getRepository()) {
ReentrantLock blockchainLock = Controller.getInstance().getBlockchainLock();
blockchainLock.lockInterruptibly();
try {
repository.importDataFromFile(filename);
repository.saveChanges();
return "true";
} finally {
blockchainLock.unlock();
}
} catch (InterruptedException e) {
// We couldn't lock blockchain to perform import
return "false";
} catch (DataException e) {
throw ApiExceptionFactory.INSTANCE.createException(request, ApiError.REPOSITORY_ISSUE, e);
}
}
@POST
@Path("/repository/checkpoint")
@Operation(
summary = "Checkpoint data in repository.",
description = "Forces repository to checkpoint uncommitted writes.",
responses = {
@ApiResponse(
description = "\"true\"",
content = @Content(mediaType = MediaType.TEXT_PLAIN, schema = @Schema(type = "string"))
)
}
)
@ApiErrors({ApiError.REPOSITORY_ISSUE})
@SecurityRequirement(name = "apiKey")
public String checkpointRepository() {
Security.checkApiCallAllowed(request);
RepositoryManager.setRequestedCheckpoint(Boolean.TRUE);
return "true";
}
@POST
@Path("/repository/backup")
@Operation(
summary = "Perform online backup of repository.",
responses = {
@ApiResponse(
description = "\"true\"",
content = @Content(mediaType = MediaType.TEXT_PLAIN, schema = @Schema(type = "string"))
)
}
)
@ApiErrors({ApiError.REPOSITORY_ISSUE})
@SecurityRequirement(name = "apiKey")
public String backupRepository() {
Security.checkApiCallAllowed(request);
try (final Repository repository = RepositoryManager.getRepository()) {
ReentrantLock blockchainLock = Controller.getInstance().getBlockchainLock();
blockchainLock.lockInterruptibly();
try {
repository.backup(true);
repository.saveChanges();
return "true";
} finally {
blockchainLock.unlock();
}
} catch (InterruptedException e) {
// We couldn't lock blockchain to perform backup
return "false";
} catch (DataException e) {
throw ApiExceptionFactory.INSTANCE.createException(request, ApiError.REPOSITORY_ISSUE, e);
}
}
@DELETE
@Path("/repository")
@Operation(
summary = "Perform maintenance on repository.",
description = "Requires enough free space to rebuild repository. This will pause your node for a while."
)
@ApiErrors({ApiError.REPOSITORY_ISSUE})
@SecurityRequirement(name = "apiKey")
public void performRepositoryMaintenance() {
Security.checkApiCallAllowed(request);
try (final Repository repository = RepositoryManager.getRepository()) {
ReentrantLock blockchainLock = Controller.getInstance().getBlockchainLock();
blockchainLock.lockInterruptibly();
try {
repository.performPeriodicMaintenance();
} finally {
blockchainLock.unlock();
}
} catch (InterruptedException e) {
// No big deal
} catch (DataException e) {
throw ApiExceptionFactory.INSTANCE.createException(request, ApiError.REPOSITORY_ISSUE, e);
}
}
}

View File

@@ -1,17 +1,11 @@
package org.qortal.api.resource;
import io.swagger.v3.oas.annotations.OpenAPIDefinition;
import io.swagger.v3.oas.annotations.enums.SecuritySchemeIn;
import io.swagger.v3.oas.annotations.enums.SecuritySchemeType;
import io.swagger.v3.oas.annotations.extensions.Extension;
import io.swagger.v3.oas.annotations.extensions.ExtensionProperty;
import io.swagger.v3.oas.annotations.info.Info;
import io.swagger.v3.oas.annotations.security.SecurityScheme;
import io.swagger.v3.oas.annotations.security.SecuritySchemes;
import io.swagger.v3.oas.annotations.tags.Tag;
import org.qortal.api.Security;
@OpenAPIDefinition(
info = @Info( title = "Qortal API", description = "NOTE: byte-arrays are encoded in Base58" ),
tags = {
@@ -19,10 +13,7 @@ import org.qortal.api.Security;
@Tag(name = "Admin"),
@Tag(name = "Arbitrary"),
@Tag(name = "Assets"),
@Tag(name = "Automated Transactions"),
@Tag(name = "Blocks"),
@Tag(name = "Chat"),
@Tag(name = "Cross-Chain"),
@Tag(name = "Groups"),
@Tag(name = "Names"),
@Tag(name = "Payments"),
@@ -36,9 +27,5 @@ import org.qortal.api.Security;
})
}
)
@SecuritySchemes({
@SecurityScheme(name = "basicAuth", type = SecuritySchemeType.HTTP, scheme = "basic"),
@SecurityScheme(name = "apiKey", type = SecuritySchemeType.APIKEY, in = SecuritySchemeIn.HEADER, paramName = Security.API_KEY_HEADER)
})
public class ApiDefinition {
}

View File

@@ -1,204 +0,0 @@
package org.qortal.api.resource;
import io.swagger.v3.oas.annotations.Operation;
import io.swagger.v3.oas.annotations.Parameter;
import io.swagger.v3.oas.annotations.media.ArraySchema;
import io.swagger.v3.oas.annotations.media.Content;
import io.swagger.v3.oas.annotations.media.Schema;
import io.swagger.v3.oas.annotations.parameters.RequestBody;
import io.swagger.v3.oas.annotations.responses.ApiResponse;
import io.swagger.v3.oas.annotations.tags.Tag;
import java.util.List;
import javax.servlet.http.HttpServletRequest;
import javax.ws.rs.GET;
import javax.ws.rs.POST;
import javax.ws.rs.Path;
import javax.ws.rs.PathParam;
import javax.ws.rs.QueryParam;
import javax.ws.rs.core.Context;
import javax.ws.rs.core.MediaType;
import org.ciyam.at.MachineState;
import org.qortal.api.ApiError;
import org.qortal.api.ApiErrors;
import org.qortal.api.ApiException;
import org.qortal.api.ApiExceptionFactory;
import org.qortal.data.at.ATData;
import org.qortal.data.at.ATStateData;
import org.qortal.data.transaction.DeployAtTransactionData;
import org.qortal.repository.DataException;
import org.qortal.repository.Repository;
import org.qortal.repository.RepositoryManager;
import org.qortal.settings.Settings;
import org.qortal.transaction.Transaction;
import org.qortal.transaction.Transaction.ValidationResult;
import org.qortal.transform.TransformationException;
import org.qortal.transform.transaction.DeployAtTransactionTransformer;
import org.qortal.utils.Base58;
@Path("/at")
@Tag(name = "Automated Transactions")
public class AtResource {
@Context
HttpServletRequest request;
@GET
@Path("/byfunction/{codehash}")
@Operation(
summary = "Find automated transactions with matching functionality (code hash)",
responses = {
@ApiResponse(
description = "automated transactions",
content = @Content(
array = @ArraySchema(
schema = @Schema(
implementation = ATData.class
)
)
)
)
}
)
@ApiErrors({
ApiError.INVALID_CRITERIA, ApiError.REPOSITORY_ISSUE
})
public List<ATData> getByFunctionality(
@PathParam("codehash")
String codeHash58,
@Parameter(description = "whether to include ATs that can run, or not, or both (if omitted)")
@QueryParam("isExecutable")
Boolean isExecutable,
@Parameter( ref = "limit") @QueryParam("limit") Integer limit,
@Parameter( ref = "offset" ) @QueryParam("offset") Integer offset,
@Parameter( ref = "reverse" ) @QueryParam("reverse") Boolean reverse) {
// Decode codeHash
byte[] codeHash;
try {
codeHash = Base58.decode(codeHash58);
} catch (NumberFormatException e) {
throw ApiExceptionFactory.INSTANCE.createException(request, ApiError.INVALID_CRITERIA, e);
}
// codeHash must be present and have correct length
if (codeHash == null || codeHash.length != 32)
throw ApiExceptionFactory.INSTANCE.createException(request, ApiError.INVALID_CRITERIA);
// Impose a limit on 'limit'
if (limit != null && limit > 100)
throw ApiExceptionFactory.INSTANCE.createException(request, ApiError.INVALID_CRITERIA);
try (final Repository repository = RepositoryManager.getRepository()) {
return repository.getATRepository().getATsByFunctionality(codeHash, isExecutable, limit, offset, reverse);
} catch (ApiException e) {
throw e;
} catch (DataException e) {
throw ApiExceptionFactory.INSTANCE.createException(request, ApiError.REPOSITORY_ISSUE, e);
}
}
@GET
@Path("/{ataddress}")
@Operation(
summary = "Fetch info associated with AT address",
responses = {
@ApiResponse(
description = "automated transaction",
content = @Content(
schema = @Schema(implementation = ATData.class)
)
)
}
)
@ApiErrors({
ApiError.REPOSITORY_ISSUE
})
public ATData getByAddress(@PathParam("ataddress") String atAddress) {
try (final Repository repository = RepositoryManager.getRepository()) {
return repository.getATRepository().fromATAddress(atAddress);
} catch (ApiException e) {
throw e;
} catch (DataException e) {
throw ApiExceptionFactory.INSTANCE.createException(request, ApiError.REPOSITORY_ISSUE, e);
}
}
@GET
@Path("/{ataddress}/data")
@Operation(
summary = "Fetch data segment associated with AT address",
responses = {
@ApiResponse(
description = "automated transaction",
content = @Content(
schema = @Schema(implementation = byte[].class)
)
)
}
)
@ApiErrors({
ApiError.REPOSITORY_ISSUE
})
public byte[] getDataByAddress(@PathParam("ataddress") String atAddress) {
try (final Repository repository = RepositoryManager.getRepository()) {
ATStateData atStateData = repository.getATRepository().getLatestATState(atAddress);
byte[] stateData = atStateData.getStateData();
byte[] dataBytes = MachineState.extractDataBytes(stateData);
return dataBytes;
} catch (ApiException e) {
throw e;
} catch (DataException e) {
throw ApiExceptionFactory.INSTANCE.createException(request, ApiError.REPOSITORY_ISSUE, e);
}
}
@POST
@Operation(
summary = "Build raw, unsigned, DEPLOY_AT transaction",
requestBody = @RequestBody(
required = true,
content = @Content(
mediaType = MediaType.APPLICATION_JSON,
schema = @Schema(
implementation = DeployAtTransactionData.class
)
)
),
responses = {
@ApiResponse(
description = "raw, unsigned, DEPLOY_AT transaction encoded in Base58",
content = @Content(
mediaType = MediaType.TEXT_PLAIN,
schema = @Schema(
type = "string"
)
)
)
}
)
@ApiErrors({ApiError.NON_PRODUCTION, ApiError.TRANSACTION_INVALID, ApiError.TRANSFORMATION_ERROR, ApiError.REPOSITORY_ISSUE})
public String createDeployAt(DeployAtTransactionData transactionData) {
if (Settings.getInstance().isApiRestricted())
throw ApiExceptionFactory.INSTANCE.createException(request, ApiError.NON_PRODUCTION);
try (final Repository repository = RepositoryManager.getRepository()) {
Transaction transaction = Transaction.fromData(repository, transactionData);
ValidationResult result = transaction.isValidUnconfirmed();
if (result != ValidationResult.OK)
throw TransactionsResource.createTransactionInvalidException(request, result);
byte[] bytes = DeployAtTransactionTransformer.toBytes(transactionData);
return Base58.encode(bytes);
} catch (TransformationException e) {
throw ApiExceptionFactory.INSTANCE.createException(request, ApiError.TRANSFORMATION_ERROR, e);
} catch (DataException e) {
throw ApiExceptionFactory.INSTANCE.createException(request, ApiError.REPOSITORY_ISSUE, e);
}
}
}

View File

@@ -22,8 +22,9 @@ import javax.ws.rs.core.MediaType;
import org.qortal.api.ApiError;
import org.qortal.api.ApiErrors;
import org.qortal.api.ApiException;
import org.qortal.api.ApiExceptionFactory;
import org.qortal.api.model.BlockSignerSummary;
import org.qortal.api.model.BlockMinterSummary;
import org.qortal.crypto.Crypto;
import org.qortal.data.account.AccountData;
import org.qortal.data.block.BlockData;
@@ -70,11 +71,9 @@ public class BlocksResource {
}
try (final Repository repository = RepositoryManager.getRepository()) {
BlockData blockData = repository.getBlockRepository().fromSignature(signature);
if (blockData == null)
throw ApiExceptionFactory.INSTANCE.createException(request, ApiError.BLOCK_UNKNOWN);
return blockData;
return repository.getBlockRepository().fromSignature(signature);
} catch (ApiException e) {
throw e;
} catch (DataException e) {
throw ApiExceptionFactory.INSTANCE.createException(request, ApiError.REPOSITORY_ISSUE, e);
}
@@ -121,6 +120,8 @@ public class BlocksResource {
throw ApiExceptionFactory.INSTANCE.createException(request, ApiError.BLOCK_UNKNOWN);
return repository.getBlockRepository().getTransactionsFromSignature(signature, limit, offset, reverse);
} catch (ApiException e) {
throw e;
} catch (DataException e) {
throw ApiExceptionFactory.INSTANCE.createException(request, ApiError.REPOSITORY_ISSUE, e);
}
@@ -222,6 +223,8 @@ public class BlocksResource {
throw ApiExceptionFactory.INSTANCE.createException(request, ApiError.BLOCK_UNKNOWN);
return childBlockData;
} catch (ApiException e) {
throw e;
} catch (DataException e) {
throw ApiExceptionFactory.INSTANCE.createException(request, ApiError.REPOSITORY_ISSUE, e);
}
@@ -250,6 +253,8 @@ public class BlocksResource {
public int getHeight() {
try (final Repository repository = RepositoryManager.getRepository()) {
return repository.getBlockRepository().getBlockchainHeight();
} catch (ApiException e) {
throw e;
} catch (DataException e) {
throw ApiExceptionFactory.INSTANCE.createException(request, ApiError.REPOSITORY_ISSUE, e);
}
@@ -292,6 +297,8 @@ public class BlocksResource {
throw ApiExceptionFactory.INSTANCE.createException(request, ApiError.BLOCK_UNKNOWN);
return blockData.getHeight();
} catch (ApiException e) {
throw e;
} catch (DataException e) {
throw ApiExceptionFactory.INSTANCE.createException(request, ApiError.REPOSITORY_ISSUE, e);
}
@@ -323,6 +330,8 @@ public class BlocksResource {
throw ApiExceptionFactory.INSTANCE.createException(request, ApiError.BLOCK_UNKNOWN);
return blockData;
} catch (ApiException e) {
throw e;
} catch (DataException e) {
throw ApiExceptionFactory.INSTANCE.createException(request, ApiError.REPOSITORY_ISSUE, e);
}
@@ -357,6 +366,8 @@ public class BlocksResource {
throw ApiExceptionFactory.INSTANCE.createException(request, ApiError.BLOCK_UNKNOWN);
return blockData;
} catch (ApiException e) {
throw e;
} catch (DataException e) {
throw ApiExceptionFactory.INSTANCE.createException(request, ApiError.REPOSITORY_ISSUE, e);
}
@@ -405,9 +416,9 @@ public class BlocksResource {
}
@GET
@Path("/signer/{address}")
@Path("/minter/{address}")
@Operation(
summary = "Fetch block summaries for blocks signed by address",
summary = "Fetch block summaries for blocks minted by address",
responses = {
@ApiResponse(
description = "block summaries",
@@ -422,7 +433,7 @@ public class BlocksResource {
}
)
@ApiErrors({ApiError.INVALID_ADDRESS, ApiError.PUBLIC_KEY_NOT_FOUND, ApiError.REPOSITORY_ISSUE})
public List<BlockSummaryData> getBlockSummariesBySigner(@PathParam("address") String address, @Parameter(
public List<BlockSummaryData> getBlockSummariesByMinter(@PathParam("address") String address, @Parameter(
ref = "limit"
) @QueryParam("limit") Integer limit, @Parameter(
ref = "offset"
@@ -438,30 +449,32 @@ public class BlocksResource {
if (accountData == null || accountData.getPublicKey() == null)
throw ApiExceptionFactory.INSTANCE.createException(request, ApiError.PUBLIC_KEY_NOT_FOUND);
return repository.getBlockRepository().getBlockSummariesBySigner(accountData.getPublicKey(), limit, offset, reverse);
return repository.getBlockRepository().getBlockSummariesByMinter(accountData.getPublicKey(), limit, offset, reverse);
} catch (ApiException e) {
throw e;
} catch (DataException e) {
throw ApiExceptionFactory.INSTANCE.createException(request, ApiError.REPOSITORY_ISSUE, e);
}
}
@GET
@Path("/signers")
@Path("/minters")
@Operation(
summary = "Show summary of block signers",
description = "Returns count of blocks signed, optionally limited to minters/recipients in passed address(es).",
summary = "Show summary of block minters",
description = "Returns count of blocks minted, optionally limited to minters/recipients in passed address(es).",
responses = {
@ApiResponse(
content = @Content(
array = @ArraySchema(
schema = @Schema(
implementation = BlockSignerSummary.class
implementation = BlockMinterSummary.class
)
)
)
)
}
)
public List<BlockSignerSummary> getBlockSigners(@QueryParam("address") List<String> addresses,
public List<BlockMinterSummary> getBlockMinters(@QueryParam("address") List<String> addresses,
@Parameter(
ref = "limit"
) @QueryParam("limit") Integer limit, @Parameter(
@@ -474,47 +487,7 @@ public class BlocksResource {
if (!Crypto.isValidAddress(address))
throw ApiExceptionFactory.INSTANCE.createException(request, ApiError.INVALID_ADDRESS);
return repository.getBlockRepository().getBlockSigners(addresses, limit, offset, reverse);
} catch (DataException e) {
throw ApiExceptionFactory.INSTANCE.createException(request, ApiError.REPOSITORY_ISSUE, e);
}
}
@GET
@Path("/summaries")
@Operation(
summary = "Fetch only summary info about a range of blocks",
description = "Specify up to 2 out 3 of: start, end and count. If neither start nor end are specified, then end is assumed to be latest block. Where necessary, count is assumed to be 50.",
responses = {
@ApiResponse(
description = "blocks",
content = @Content(
array = @ArraySchema(
schema = @Schema(
implementation = BlockSummaryData.class
)
)
)
)
}
)
@ApiErrors({
ApiError.REPOSITORY_ISSUE
})
public List<BlockSummaryData> getBlockSummaries(
@QueryParam("start") Integer startHeight,
@QueryParam("end") Integer endHeight,
@Parameter(ref = "count") @QueryParam("count") Integer count) {
// Check up to 2 out of 3 params
if (startHeight != null && endHeight != null && count != null)
throw ApiExceptionFactory.INSTANCE.createException(request, ApiError.INVALID_CRITERIA);
// Check values
if ((startHeight != null && startHeight < 1) || (endHeight != null && endHeight < 1) || (count != null && count < 1))
throw ApiExceptionFactory.INSTANCE.createException(request, ApiError.INVALID_CRITERIA);
try (final Repository repository = RepositoryManager.getRepository()) {
return repository.getBlockRepository().getBlockSummaries(startHeight, endHeight, count);
return repository.getBlockRepository().getBlockMinters(addresses, limit, offset, reverse);
} catch (DataException e) {
throw ApiExceptionFactory.INSTANCE.createException(request, ApiError.REPOSITORY_ISSUE, e);
}

View File

@@ -1,250 +0,0 @@
package org.qortal.api.resource;
import io.swagger.v3.oas.annotations.Operation;
import io.swagger.v3.oas.annotations.Parameter;
import io.swagger.v3.oas.annotations.media.ArraySchema;
import io.swagger.v3.oas.annotations.media.Content;
import io.swagger.v3.oas.annotations.media.Schema;
import io.swagger.v3.oas.annotations.parameters.RequestBody;
import io.swagger.v3.oas.annotations.responses.ApiResponse;
import io.swagger.v3.oas.annotations.security.SecurityRequirement;
import io.swagger.v3.oas.annotations.tags.Tag;
import java.util.List;
import javax.servlet.http.HttpServletRequest;
import javax.ws.rs.GET;
import javax.ws.rs.POST;
import javax.ws.rs.Path;
import javax.ws.rs.PathParam;
import javax.ws.rs.QueryParam;
import javax.ws.rs.core.Context;
import javax.ws.rs.core.MediaType;
import org.qortal.api.ApiError;
import org.qortal.api.ApiErrors;
import org.qortal.api.ApiExceptionFactory;
import org.qortal.api.Security;
import org.qortal.crypto.Crypto;
import org.qortal.data.chat.ActiveChats;
import org.qortal.data.chat.ChatMessage;
import org.qortal.data.transaction.ChatTransactionData;
import org.qortal.data.transaction.TransactionData;
import org.qortal.repository.DataException;
import org.qortal.repository.Repository;
import org.qortal.repository.RepositoryManager;
import org.qortal.transaction.ChatTransaction;
import org.qortal.transaction.Transaction;
import org.qortal.transaction.Transaction.TransactionType;
import org.qortal.transaction.Transaction.ValidationResult;
import org.qortal.transform.TransformationException;
import org.qortal.transform.transaction.ChatTransactionTransformer;
import org.qortal.transform.transaction.TransactionTransformer;
import org.qortal.utils.Base58;
import com.google.common.primitives.Bytes;
@Path("/chat")
@Tag(name = "Chat")
public class ChatResource {
@Context
HttpServletRequest request;
@GET
@Path("/messages")
@Operation(
summary = "Find chat messages",
description = "Returns CHAT messages that match criteria. Must provide EITHER 'txGroupId' OR two 'involving' addresses.",
responses = {
@ApiResponse(
description = "CHAT messages",
content = @Content(
array = @ArraySchema(
schema = @Schema(
implementation = ChatMessage.class
)
)
)
)
}
)
@ApiErrors({ApiError.INVALID_CRITERIA, ApiError.INVALID_ADDRESS, ApiError.REPOSITORY_ISSUE})
public List<ChatMessage> searchChat(@QueryParam("before") Long before, @QueryParam("after") Long after,
@QueryParam("txGroupId") Integer txGroupId,
@QueryParam("involving") List<String> involvingAddresses,
@Parameter(ref = "limit") @QueryParam("limit") Integer limit,
@Parameter(ref = "offset") @QueryParam("offset") Integer offset,
@Parameter(ref = "reverse") @QueryParam("reverse") Boolean reverse) {
// Check args meet expectations
if ((txGroupId == null && involvingAddresses.size() != 2)
|| (txGroupId != null && !involvingAddresses.isEmpty()))
throw ApiExceptionFactory.INSTANCE.createException(request, ApiError.INVALID_CRITERIA);
// Check any provided addresses are valid
if (involvingAddresses.stream().anyMatch(address -> !Crypto.isValidAddress(address)))
throw ApiExceptionFactory.INSTANCE.createException(request, ApiError.INVALID_ADDRESS);
if (before != null && before < 1500000000000L)
throw ApiExceptionFactory.INSTANCE.createException(request, ApiError.INVALID_CRITERIA);
if (after != null && after < 1500000000000L)
throw ApiExceptionFactory.INSTANCE.createException(request, ApiError.INVALID_CRITERIA);
try (final Repository repository = RepositoryManager.getRepository()) {
return repository.getChatRepository().getMessagesMatchingCriteria(
before,
after,
txGroupId,
involvingAddresses,
limit, offset, reverse);
} catch (DataException e) {
throw ApiExceptionFactory.INSTANCE.createException(request, ApiError.REPOSITORY_ISSUE, e);
}
}
@GET
@Path("/active/{address}")
@Operation(
summary = "Find active chats (group/direct) involving address",
responses = {
@ApiResponse(
content = @Content(
array = @ArraySchema(
schema = @Schema(
implementation = ActiveChats.class
)
)
)
)
}
)
@ApiErrors({ApiError.INVALID_CRITERIA, ApiError.INVALID_ADDRESS, ApiError.REPOSITORY_ISSUE})
public ActiveChats getActiveChats(@PathParam("address") String address) {
if (address == null || !Crypto.isValidAddress(address))
throw ApiExceptionFactory.INSTANCE.createException(request, ApiError.INVALID_ADDRESS);
try (final Repository repository = RepositoryManager.getRepository()) {
return repository.getChatRepository().getActiveChats(address);
} catch (DataException e) {
throw ApiExceptionFactory.INSTANCE.createException(request, ApiError.REPOSITORY_ISSUE, e);
}
}
@POST
@Operation(
summary = "Build raw, unsigned, CHAT transaction",
description = "Builds a raw, unsigned CHAT transaction but does NOT compute proof-of-work nonce. See POST /chat/compute.",
requestBody = @RequestBody(
required = true,
content = @Content(
mediaType = MediaType.APPLICATION_JSON,
schema = @Schema(
implementation = ChatTransactionData.class
)
)
),
responses = {
@ApiResponse(
description = "raw, unsigned, CHAT transaction encoded in Base58",
content = @Content(
mediaType = MediaType.TEXT_PLAIN,
schema = @Schema(
type = "string"
)
)
)
}
)
@ApiErrors({ApiError.TRANSACTION_INVALID, ApiError.TRANSFORMATION_ERROR, ApiError.REPOSITORY_ISSUE})
@SecurityRequirement(name = "apiKey")
public String buildChat(ChatTransactionData transactionData) {
Security.checkApiCallAllowed(request);
try (final Repository repository = RepositoryManager.getRepository()) {
ChatTransaction chatTransaction = (ChatTransaction) Transaction.fromData(repository, transactionData);
ValidationResult result = chatTransaction.isValidUnconfirmed();
if (result != ValidationResult.OK)
throw TransactionsResource.createTransactionInvalidException(request, result);
byte[] bytes = ChatTransactionTransformer.toBytes(transactionData);
return Base58.encode(bytes);
} catch (TransformationException e) {
throw ApiExceptionFactory.INSTANCE.createException(request, ApiError.TRANSFORMATION_ERROR, e);
} catch (DataException e) {
throw ApiExceptionFactory.INSTANCE.createException(request, ApiError.REPOSITORY_ISSUE, e);
}
}
@POST
@Path("/compute")
@Operation(
summary = "Compute nonce for raw, unsigned CHAT transaction",
requestBody = @RequestBody(
required = true,
content = @Content(
mediaType = MediaType.TEXT_PLAIN,
schema = @Schema(
type = "string",
description = "raw, unsigned CHAT transaction in base58 encoding",
example = "raw transaction base58"
)
)
),
responses = {
@ApiResponse(
description = "raw, unsigned, CHAT transaction encoded in Base58",
content = @Content(
mediaType = MediaType.TEXT_PLAIN,
schema = @Schema(
type = "string"
)
)
)
}
)
@ApiErrors({ApiError.TRANSACTION_INVALID, ApiError.INVALID_DATA, ApiError.TRANSFORMATION_ERROR, ApiError.REPOSITORY_ISSUE})
@SecurityRequirement(name = "apiKey")
public String buildChat(String rawBytes58) {
Security.checkApiCallAllowed(request);
try (final Repository repository = RepositoryManager.getRepository()) {
byte[] rawBytes = Base58.decode(rawBytes58);
// We're expecting unsigned transaction, so append empty signature prior to decoding
rawBytes = Bytes.concat(rawBytes, new byte[TransactionTransformer.SIGNATURE_LENGTH]);
TransactionData transactionData = TransactionTransformer.fromBytes(rawBytes);
if (transactionData == null)
throw ApiExceptionFactory.INSTANCE.createException(request, ApiError.INVALID_DATA);
if (transactionData.getType() != TransactionType.CHAT)
throw ApiExceptionFactory.INSTANCE.createException(request, ApiError.INVALID_DATA);
ChatTransaction chatTransaction = (ChatTransaction) Transaction.fromData(repository, transactionData);
// Quicker validity check first before we compute nonce
ValidationResult result = chatTransaction.isValid();
if (result != ValidationResult.OK)
throw TransactionsResource.createTransactionInvalidException(request, result);
chatTransaction.computeNonce();
// Re-check, but ignores signature
result = chatTransaction.isValidUnconfirmed();
if (result != ValidationResult.OK)
throw TransactionsResource.createTransactionInvalidException(request, result);
// Strip zeroed signature
transactionData.setSignature(null);
byte[] bytes = ChatTransactionTransformer.toBytes(transactionData);
return Base58.encode(bytes);
} catch (TransformationException e) {
throw ApiExceptionFactory.INSTANCE.createException(request, ApiError.TRANSFORMATION_ERROR, e);
} catch (DataException e) {
throw ApiExceptionFactory.INSTANCE.createException(request, ApiError.REPOSITORY_ISSUE, e);
}
}
}

View File

@@ -1,363 +0,0 @@
package org.qortal.api.resource;
import io.swagger.v3.oas.annotations.Operation;
import io.swagger.v3.oas.annotations.media.Content;
import io.swagger.v3.oas.annotations.media.Schema;
import io.swagger.v3.oas.annotations.parameters.RequestBody;
import io.swagger.v3.oas.annotations.responses.ApiResponse;
import io.swagger.v3.oas.annotations.tags.Tag;
import java.util.Arrays;
import java.util.Random;
import javax.servlet.http.HttpServletRequest;
import javax.ws.rs.POST;
import javax.ws.rs.Path;
import javax.ws.rs.core.Context;
import javax.ws.rs.core.MediaType;
import org.qortal.account.PublicKeyAccount;
import org.qortal.api.ApiError;
import org.qortal.api.ApiErrors;
import org.qortal.api.ApiExceptionFactory;
import org.qortal.api.Security;
import org.qortal.api.model.CrossChainBuildRequest;
import org.qortal.api.model.CrossChainSecretRequest;
import org.qortal.api.model.CrossChainTradeRequest;
import org.qortal.asset.Asset;
import org.qortal.crosschain.BitcoinACCTv1;
import org.qortal.crosschain.Bitcoiny;
import org.qortal.crosschain.AcctMode;
import org.qortal.crypto.Crypto;
import org.qortal.data.at.ATData;
import org.qortal.data.crosschain.CrossChainTradeData;
import org.qortal.data.transaction.BaseTransactionData;
import org.qortal.data.transaction.DeployAtTransactionData;
import org.qortal.data.transaction.MessageTransactionData;
import org.qortal.data.transaction.TransactionData;
import org.qortal.group.Group;
import org.qortal.repository.DataException;
import org.qortal.repository.Repository;
import org.qortal.repository.RepositoryManager;
import org.qortal.transaction.DeployAtTransaction;
import org.qortal.transaction.MessageTransaction;
import org.qortal.transaction.Transaction;
import org.qortal.transaction.Transaction.TransactionType;
import org.qortal.transaction.Transaction.ValidationResult;
import org.qortal.transform.TransformationException;
import org.qortal.transform.Transformer;
import org.qortal.transform.transaction.DeployAtTransactionTransformer;
import org.qortal.transform.transaction.MessageTransactionTransformer;
import org.qortal.utils.Base58;
import org.qortal.utils.NTP;
@Path("/crosschain/BitcoinACCTv1")
@Tag(name = "Cross-Chain (BitcoinACCTv1)")
public class CrossChainBitcoinACCTv1Resource {
@Context
HttpServletRequest request;
@POST
@Path("/build")
@Operation(
summary = "Build Bitcoin cross-chain trading AT",
description = "Returns raw, unsigned DEPLOY_AT transaction",
requestBody = @RequestBody(
required = true,
content = @Content(
mediaType = MediaType.APPLICATION_JSON,
schema = @Schema(
implementation = CrossChainBuildRequest.class
)
)
),
responses = {
@ApiResponse(
content = @Content(mediaType = MediaType.TEXT_PLAIN, schema = @Schema(type = "string"))
)
}
)
@ApiErrors({ApiError.INVALID_PUBLIC_KEY, ApiError.INVALID_DATA, ApiError.INVALID_REFERENCE, ApiError.TRANSFORMATION_ERROR, ApiError.REPOSITORY_ISSUE})
public String buildTrade(CrossChainBuildRequest tradeRequest) {
Security.checkApiCallAllowed(request);
byte[] creatorPublicKey = tradeRequest.creatorPublicKey;
if (creatorPublicKey == null || creatorPublicKey.length != Transformer.PUBLIC_KEY_LENGTH)
throw ApiExceptionFactory.INSTANCE.createException(request, ApiError.INVALID_PUBLIC_KEY);
if (tradeRequest.hashOfSecretB == null || tradeRequest.hashOfSecretB.length != Bitcoiny.HASH160_LENGTH)
throw ApiExceptionFactory.INSTANCE.createException(request, ApiError.INVALID_DATA);
if (tradeRequest.tradeTimeout == null)
tradeRequest.tradeTimeout = 7 * 24 * 60; // 7 days
else
if (tradeRequest.tradeTimeout < 10 || tradeRequest.tradeTimeout > 50000)
throw ApiExceptionFactory.INSTANCE.createException(request, ApiError.INVALID_DATA);
if (tradeRequest.qortAmount <= 0)
throw ApiExceptionFactory.INSTANCE.createException(request, ApiError.INVALID_DATA);
if (tradeRequest.fundingQortAmount <= 0)
throw ApiExceptionFactory.INSTANCE.createException(request, ApiError.INVALID_DATA);
// funding amount must exceed initial + final
if (tradeRequest.fundingQortAmount <= tradeRequest.qortAmount)
throw ApiExceptionFactory.INSTANCE.createException(request, ApiError.INVALID_DATA);
if (tradeRequest.bitcoinAmount <= 0)
throw ApiExceptionFactory.INSTANCE.createException(request, ApiError.INVALID_DATA);
try (final Repository repository = RepositoryManager.getRepository()) {
PublicKeyAccount creatorAccount = new PublicKeyAccount(repository, creatorPublicKey);
byte[] creationBytes = BitcoinACCTv1.buildQortalAT(creatorAccount.getAddress(), tradeRequest.bitcoinPublicKeyHash, tradeRequest.hashOfSecretB,
tradeRequest.qortAmount, tradeRequest.bitcoinAmount, tradeRequest.tradeTimeout);
long txTimestamp = NTP.getTime();
byte[] lastReference = creatorAccount.getLastReference();
if (lastReference == null)
throw ApiExceptionFactory.INSTANCE.createException(request, ApiError.INVALID_REFERENCE);
long fee = 0;
String name = "QORT-BTC cross-chain trade";
String description = "Qortal-Bitcoin cross-chain trade";
String atType = "ACCT";
String tags = "QORT-BTC ACCT";
BaseTransactionData baseTransactionData = new BaseTransactionData(txTimestamp, Group.NO_GROUP, lastReference, creatorAccount.getPublicKey(), fee, null);
TransactionData deployAtTransactionData = new DeployAtTransactionData(baseTransactionData, name, description, atType, tags, creationBytes, tradeRequest.fundingQortAmount, Asset.QORT);
Transaction deployAtTransaction = new DeployAtTransaction(repository, deployAtTransactionData);
fee = deployAtTransaction.calcRecommendedFee();
deployAtTransactionData.setFee(fee);
ValidationResult result = deployAtTransaction.isValidUnconfirmed();
if (result != ValidationResult.OK)
throw TransactionsResource.createTransactionInvalidException(request, result);
byte[] bytes = DeployAtTransactionTransformer.toBytes(deployAtTransactionData);
return Base58.encode(bytes);
} catch (TransformationException e) {
throw ApiExceptionFactory.INSTANCE.createException(request, ApiError.TRANSFORMATION_ERROR, e);
} catch (DataException e) {
throw ApiExceptionFactory.INSTANCE.createException(request, ApiError.REPOSITORY_ISSUE, e);
}
}
@POST
@Path("/trademessage")
@Operation(
summary = "Builds raw, unsigned 'trade' MESSAGE transaction that sends cross-chain trade recipient address, triggering 'trade' mode",
description = "Specify address of cross-chain AT that needs to be messaged, and signature of 'offer' MESSAGE from trade partner.<br>"
+ "AT needs to be in 'offer' mode. Messages sent to an AT in any other mode will be ignored, but still cost fees to send!<br>"
+ "You need to sign output with trade private key otherwise the MESSAGE transaction will be invalid.",
requestBody = @RequestBody(
required = true,
content = @Content(
mediaType = MediaType.APPLICATION_JSON,
schema = @Schema(
implementation = CrossChainTradeRequest.class
)
)
),
responses = {
@ApiResponse(
content = @Content(
schema = @Schema(
type = "string"
)
)
)
}
)
@ApiErrors({ApiError.INVALID_PUBLIC_KEY, ApiError.INVALID_ADDRESS, ApiError.INVALID_CRITERIA, ApiError.REPOSITORY_ISSUE})
public String buildTradeMessage(CrossChainTradeRequest tradeRequest) {
Security.checkApiCallAllowed(request);
byte[] tradePublicKey = tradeRequest.tradePublicKey;
if (tradePublicKey == null || tradePublicKey.length != Transformer.PUBLIC_KEY_LENGTH)
throw ApiExceptionFactory.INSTANCE.createException(request, ApiError.INVALID_PUBLIC_KEY);
if (tradeRequest.atAddress == null || !Crypto.isValidAtAddress(tradeRequest.atAddress))
throw ApiExceptionFactory.INSTANCE.createException(request, ApiError.INVALID_ADDRESS);
if (tradeRequest.messageTransactionSignature == null || !Crypto.isValidAddress(tradeRequest.messageTransactionSignature))
throw ApiExceptionFactory.INSTANCE.createException(request, ApiError.INVALID_ADDRESS);
try (final Repository repository = RepositoryManager.getRepository()) {
ATData atData = fetchAtDataWithChecking(repository, tradeRequest.atAddress);
CrossChainTradeData crossChainTradeData = BitcoinACCTv1.getInstance().populateTradeData(repository, atData);
if (crossChainTradeData.mode != AcctMode.OFFERING)
throw ApiExceptionFactory.INSTANCE.createException(request, ApiError.INVALID_CRITERIA);
// Does supplied public key match trade public key?
if (!Crypto.toAddress(tradePublicKey).equals(crossChainTradeData.qortalCreatorTradeAddress))
throw ApiExceptionFactory.INSTANCE.createException(request, ApiError.INVALID_PUBLIC_KEY);
TransactionData transactionData = repository.getTransactionRepository().fromSignature(tradeRequest.messageTransactionSignature);
if (transactionData == null)
throw ApiExceptionFactory.INSTANCE.createException(request, ApiError.TRANSACTION_UNKNOWN);
if (transactionData.getType() != TransactionType.MESSAGE)
throw ApiExceptionFactory.INSTANCE.createException(request, ApiError.TRANSACTION_INVALID);
MessageTransactionData messageTransactionData = (MessageTransactionData) transactionData;
byte[] messageData = messageTransactionData.getData();
BitcoinACCTv1.OfferMessageData offerMessageData = BitcoinACCTv1.extractOfferMessageData(messageData);
if (offerMessageData == null)
throw ApiExceptionFactory.INSTANCE.createException(request, ApiError.TRANSACTION_INVALID);
// Good to make MESSAGE
byte[] aliceForeignPublicKeyHash = offerMessageData.partnerBitcoinPKH;
byte[] hashOfSecretA = offerMessageData.hashOfSecretA;
int lockTimeA = (int) offerMessageData.lockTimeA;
String aliceNativeAddress = Crypto.toAddress(messageTransactionData.getCreatorPublicKey());
int lockTimeB = BitcoinACCTv1.calcLockTimeB(messageTransactionData.getTimestamp(), lockTimeA);
byte[] outgoingMessageData = BitcoinACCTv1.buildTradeMessage(aliceNativeAddress, aliceForeignPublicKeyHash, hashOfSecretA, lockTimeA, lockTimeB);
byte[] messageTransactionBytes = buildAtMessage(repository, tradePublicKey, tradeRequest.atAddress, outgoingMessageData);
return Base58.encode(messageTransactionBytes);
} catch (DataException e) {
throw ApiExceptionFactory.INSTANCE.createException(request, ApiError.REPOSITORY_ISSUE, e);
}
}
@POST
@Path("/redeemmessage")
@Operation(
summary = "Builds raw, unsigned 'redeem' MESSAGE transaction that sends secrets to AT, releasing funds to partner",
description = "Specify address of cross-chain AT that needs to be messaged, both 32-byte secrets and an address for receiving QORT from AT.<br>"
+ "AT needs to be in 'trade' mode. Messages sent to an AT in any other mode will be ignored, but still cost fees to send!<br>"
+ "You need to sign output with account the AT considers the trade 'partner' otherwise the MESSAGE transaction will be invalid.",
requestBody = @RequestBody(
required = true,
content = @Content(
mediaType = MediaType.APPLICATION_JSON,
schema = @Schema(
implementation = CrossChainSecretRequest.class
)
)
),
responses = {
@ApiResponse(
content = @Content(
schema = @Schema(
type = "string"
)
)
)
}
)
@ApiErrors({ApiError.INVALID_PUBLIC_KEY, ApiError.INVALID_ADDRESS, ApiError.INVALID_DATA, ApiError.INVALID_CRITERIA, ApiError.REPOSITORY_ISSUE})
public String buildRedeemMessage(CrossChainSecretRequest secretRequest) {
Security.checkApiCallAllowed(request);
byte[] partnerPublicKey = secretRequest.partnerPublicKey;
if (partnerPublicKey == null || partnerPublicKey.length != Transformer.PUBLIC_KEY_LENGTH)
throw ApiExceptionFactory.INSTANCE.createException(request, ApiError.INVALID_PUBLIC_KEY);
if (secretRequest.atAddress == null || !Crypto.isValidAtAddress(secretRequest.atAddress))
throw ApiExceptionFactory.INSTANCE.createException(request, ApiError.INVALID_ADDRESS);
if (secretRequest.secretA == null || secretRequest.secretA.length != BitcoinACCTv1.SECRET_LENGTH)
throw ApiExceptionFactory.INSTANCE.createException(request, ApiError.INVALID_DATA);
if (secretRequest.secretB == null || secretRequest.secretB.length != BitcoinACCTv1.SECRET_LENGTH)
throw ApiExceptionFactory.INSTANCE.createException(request, ApiError.INVALID_DATA);
if (secretRequest.receivingAddress == null || !Crypto.isValidAddress(secretRequest.receivingAddress))
throw ApiExceptionFactory.INSTANCE.createException(request, ApiError.INVALID_ADDRESS);
try (final Repository repository = RepositoryManager.getRepository()) {
ATData atData = fetchAtDataWithChecking(repository, secretRequest.atAddress);
CrossChainTradeData crossChainTradeData = BitcoinACCTv1.getInstance().populateTradeData(repository, atData);
if (crossChainTradeData.mode != AcctMode.TRADING)
throw ApiExceptionFactory.INSTANCE.createException(request, ApiError.INVALID_CRITERIA);
String partnerAddress = Crypto.toAddress(partnerPublicKey);
// MESSAGE must come from address that AT considers trade partner
if (!crossChainTradeData.qortalPartnerAddress.equals(partnerAddress))
throw ApiExceptionFactory.INSTANCE.createException(request, ApiError.INVALID_ADDRESS);
// Good to make MESSAGE
byte[] messageData = BitcoinACCTv1.buildRedeemMessage(secretRequest.secretA, secretRequest.secretB, secretRequest.receivingAddress);
byte[] messageTransactionBytes = buildAtMessage(repository, partnerPublicKey, secretRequest.atAddress, messageData);
return Base58.encode(messageTransactionBytes);
} catch (DataException e) {
throw ApiExceptionFactory.INSTANCE.createException(request, ApiError.REPOSITORY_ISSUE, e);
}
}
private ATData fetchAtDataWithChecking(Repository repository, String atAddress) throws DataException {
ATData atData = repository.getATRepository().fromATAddress(atAddress);
if (atData == null)
throw ApiExceptionFactory.INSTANCE.createException(request, ApiError.ADDRESS_UNKNOWN);
// Must be correct AT - check functionality using code hash
if (!Arrays.equals(atData.getCodeHash(), BitcoinACCTv1.CODE_BYTES_HASH))
throw ApiExceptionFactory.INSTANCE.createException(request, ApiError.INVALID_CRITERIA);
// No point sending message to AT that's finished
if (atData.getIsFinished())
throw ApiExceptionFactory.INSTANCE.createException(request, ApiError.INVALID_CRITERIA);
return atData;
}
private byte[] buildAtMessage(Repository repository, byte[] senderPublicKey, String atAddress, byte[] messageData) throws DataException {
long txTimestamp = NTP.getTime();
// senderPublicKey could be ephemeral trade public key where there is no corresponding account and hence no reference
String senderAddress = Crypto.toAddress(senderPublicKey);
byte[] lastReference = repository.getAccountRepository().getLastReference(senderAddress);
final boolean requiresPoW = lastReference == null;
if (requiresPoW) {
Random random = new Random();
lastReference = new byte[Transformer.SIGNATURE_LENGTH];
random.nextBytes(lastReference);
}
int version = 4;
int nonce = 0;
long amount = 0L;
Long assetId = null; // no assetId as amount is zero
Long fee = 0L;
BaseTransactionData baseTransactionData = new BaseTransactionData(txTimestamp, Group.NO_GROUP, lastReference, senderPublicKey, fee, null);
TransactionData messageTransactionData = new MessageTransactionData(baseTransactionData, version, nonce, atAddress, amount, assetId, messageData, false, false);
MessageTransaction messageTransaction = new MessageTransaction(repository, messageTransactionData);
if (requiresPoW) {
messageTransaction.computeNonce();
} else {
fee = messageTransaction.calcRecommendedFee();
messageTransactionData.setFee(fee);
}
ValidationResult result = messageTransaction.isValidUnconfirmed();
if (result != ValidationResult.OK)
throw TransactionsResource.createTransactionInvalidException(request, result);
try {
return MessageTransactionTransformer.toBytes(messageTransactionData);
} catch (TransformationException e) {
throw ApiExceptionFactory.INSTANCE.createException(request, ApiError.TRANSFORMATION_ERROR, e);
}
}
}

View File

@@ -1,167 +0,0 @@
package org.qortal.api.resource;
import io.swagger.v3.oas.annotations.Operation;
import io.swagger.v3.oas.annotations.media.ArraySchema;
import io.swagger.v3.oas.annotations.media.Content;
import io.swagger.v3.oas.annotations.media.Schema;
import io.swagger.v3.oas.annotations.parameters.RequestBody;
import io.swagger.v3.oas.annotations.responses.ApiResponse;
import io.swagger.v3.oas.annotations.tags.Tag;
import java.util.List;
import javax.servlet.http.HttpServletRequest;
import javax.ws.rs.POST;
import javax.ws.rs.Path;
import javax.ws.rs.core.Context;
import javax.ws.rs.core.MediaType;
import org.bitcoinj.core.Transaction;
import org.qortal.api.ApiError;
import org.qortal.api.ApiErrors;
import org.qortal.api.ApiExceptionFactory;
import org.qortal.api.Security;
import org.qortal.api.model.crosschain.BitcoinSendRequest;
import org.qortal.crosschain.Bitcoin;
import org.qortal.crosschain.BitcoinyTransaction;
import org.qortal.crosschain.ForeignBlockchainException;
@Path("/crosschain/btc")
@Tag(name = "Cross-Chain (Bitcoin)")
public class CrossChainBitcoinResource {
@Context
HttpServletRequest request;
@POST
@Path("/walletbalance")
@Operation(
summary = "Returns BTC balance for hierarchical, deterministic BIP32 wallet",
description = "Supply BIP32 'm' private/public key in base58, starting with 'xprv'/'xpub' for mainnet, 'tprv'/'tpub' for testnet",
requestBody = @RequestBody(
required = true,
content = @Content(
mediaType = MediaType.TEXT_PLAIN,
schema = @Schema(
type = "string",
description = "BIP32 'm' private/public key in base58",
example = "tpubD6NzVbkrYhZ4XTPc4btCZ6SMgn8CxmWkj6VBVZ1tfcJfMq4UwAjZbG8U74gGSypL9XBYk2R2BLbDBe8pcEyBKM1edsGQEPKXNbEskZozeZc"
)
)
),
responses = {
@ApiResponse(
content = @Content(mediaType = MediaType.TEXT_PLAIN, schema = @Schema(type = "string", description = "balance (satoshis)"))
)
}
)
@ApiErrors({ApiError.INVALID_PRIVATE_KEY, ApiError.FOREIGN_BLOCKCHAIN_NETWORK_ISSUE})
public String getBitcoinWalletBalance(String key58) {
Security.checkApiCallAllowed(request);
Bitcoin bitcoin = Bitcoin.getInstance();
if (!bitcoin.isValidDeterministicKey(key58))
throw ApiExceptionFactory.INSTANCE.createException(request, ApiError.INVALID_PRIVATE_KEY);
Long balance = bitcoin.getWalletBalance(key58);
if (balance == null)
throw ApiExceptionFactory.INSTANCE.createException(request, ApiError.FOREIGN_BLOCKCHAIN_NETWORK_ISSUE);
return balance.toString();
}
@POST
@Path("/wallettransactions")
@Operation(
summary = "Returns transactions for hierarchical, deterministic BIP32 wallet",
description = "Supply BIP32 'm' private/public key in base58, starting with 'xprv'/'xpub' for mainnet, 'tprv'/'tpub' for testnet",
requestBody = @RequestBody(
required = true,
content = @Content(
mediaType = MediaType.TEXT_PLAIN,
schema = @Schema(
type = "string",
description = "BIP32 'm' private/public key in base58",
example = "tpubD6NzVbkrYhZ4XTPc4btCZ6SMgn8CxmWkj6VBVZ1tfcJfMq4UwAjZbG8U74gGSypL9XBYk2R2BLbDBe8pcEyBKM1edsGQEPKXNbEskZozeZc"
)
)
),
responses = {
@ApiResponse(
content = @Content(array = @ArraySchema( schema = @Schema( implementation = BitcoinyTransaction.class ) ) )
)
}
)
@ApiErrors({ApiError.INVALID_PRIVATE_KEY, ApiError.FOREIGN_BLOCKCHAIN_NETWORK_ISSUE})
public List<BitcoinyTransaction> getBitcoinWalletTransactions(String key58) {
Security.checkApiCallAllowed(request);
Bitcoin bitcoin = Bitcoin.getInstance();
if (!bitcoin.isValidDeterministicKey(key58))
throw ApiExceptionFactory.INSTANCE.createException(request, ApiError.INVALID_PRIVATE_KEY);
try {
return bitcoin.getWalletTransactions(key58);
} catch (ForeignBlockchainException e) {
throw ApiExceptionFactory.INSTANCE.createException(request, ApiError.FOREIGN_BLOCKCHAIN_NETWORK_ISSUE);
}
}
@POST
@Path("/send")
@Operation(
summary = "Sends BTC from hierarchical, deterministic BIP32 wallet to specific address",
description = "Currently only supports 'legacy' P2PKH Bitcoin addresses. Supply BIP32 'm' private key in base58, starting with 'xprv' for mainnet, 'tprv' for testnet",
requestBody = @RequestBody(
required = true,
content = @Content(
mediaType = MediaType.APPLICATION_JSON,
schema = @Schema(
implementation = BitcoinSendRequest.class
)
)
),
responses = {
@ApiResponse(
content = @Content(mediaType = MediaType.TEXT_PLAIN, schema = @Schema(type = "string", description = "transaction hash"))
)
}
)
@ApiErrors({ApiError.INVALID_PRIVATE_KEY, ApiError.INVALID_CRITERIA, ApiError.INVALID_ADDRESS, ApiError.FOREIGN_BLOCKCHAIN_BALANCE_ISSUE, ApiError.FOREIGN_BLOCKCHAIN_NETWORK_ISSUE})
public String sendBitcoin(BitcoinSendRequest bitcoinSendRequest) {
Security.checkApiCallAllowed(request);
if (bitcoinSendRequest.bitcoinAmount <= 0)
throw ApiExceptionFactory.INSTANCE.createException(request, ApiError.INVALID_CRITERIA);
if (bitcoinSendRequest.feePerByte != null && bitcoinSendRequest.feePerByte <= 0)
throw ApiExceptionFactory.INSTANCE.createException(request, ApiError.INVALID_CRITERIA);
Bitcoin bitcoin = Bitcoin.getInstance();
if (!bitcoin.isValidAddress(bitcoinSendRequest.receivingAddress))
throw ApiExceptionFactory.INSTANCE.createException(request, ApiError.INVALID_ADDRESS);
if (!bitcoin.isValidDeterministicKey(bitcoinSendRequest.xprv58))
throw ApiExceptionFactory.INSTANCE.createException(request, ApiError.INVALID_PRIVATE_KEY);
Transaction spendTransaction = bitcoin.buildSpend(bitcoinSendRequest.xprv58,
bitcoinSendRequest.receivingAddress,
bitcoinSendRequest.bitcoinAmount,
bitcoinSendRequest.feePerByte);
if (spendTransaction == null)
throw ApiExceptionFactory.INSTANCE.createException(request, ApiError.FOREIGN_BLOCKCHAIN_BALANCE_ISSUE);
try {
bitcoin.broadcastTransaction(spendTransaction);
} catch (ForeignBlockchainException e) {
throw ApiExceptionFactory.INSTANCE.createException(request, ApiError.FOREIGN_BLOCKCHAIN_NETWORK_ISSUE);
}
return spendTransaction.getTxId().toString();
}
}

View File

@@ -1,175 +0,0 @@
package org.qortal.api.resource;
import io.swagger.v3.oas.annotations.Operation;
import io.swagger.v3.oas.annotations.media.Content;
import io.swagger.v3.oas.annotations.media.Schema;
import io.swagger.v3.oas.annotations.responses.ApiResponse;
import io.swagger.v3.oas.annotations.tags.Tag;
import java.math.BigDecimal;
import java.util.List;
import javax.servlet.http.HttpServletRequest;
import javax.ws.rs.GET;
import javax.ws.rs.Path;
import javax.ws.rs.PathParam;
import javax.ws.rs.core.Context;
import javax.ws.rs.core.MediaType;
import org.bitcoinj.core.TransactionOutput;
import org.qortal.api.ApiError;
import org.qortal.api.ApiErrors;
import org.qortal.api.ApiExceptionFactory;
import org.qortal.api.Security;
import org.qortal.api.model.CrossChainBitcoinyHTLCStatus;
import org.qortal.crosschain.Bitcoiny;
import org.qortal.crosschain.ForeignBlockchainException;
import org.qortal.crosschain.SupportedBlockchain;
import org.qortal.crosschain.BitcoinyHTLC;
import org.qortal.utils.NTP;
import com.google.common.hash.HashCode;
@Path("/crosschain/htlc")
@Tag(name = "Cross-Chain (Hash time-locked contracts)")
public class CrossChainHtlcResource {
@Context
HttpServletRequest request;
@GET
@Path("/address/{blockchain}/{refundPKH}/{locktime}/{redeemPKH}/{hashOfSecret}")
@Operation(
summary = "Returns HTLC address based on trade info",
description = "Blockchain can be BITCOIN or LITECOIN. Public key hashes (PKH) and hash of secret should be 20 bytes (hex). Locktime is seconds since epoch.",
responses = {
@ApiResponse(
content = @Content(mediaType = MediaType.TEXT_PLAIN, schema = @Schema(type = "string"))
)
}
)
@ApiErrors({ApiError.INVALID_PUBLIC_KEY, ApiError.INVALID_CRITERIA})
public String deriveHtlcAddress(@PathParam("blockchain") String blockchainName,
@PathParam("refundPKH") String refundHex,
@PathParam("locktime") int lockTime,
@PathParam("redeemPKH") String redeemHex,
@PathParam("hashOfSecret") String hashOfSecretHex) {
SupportedBlockchain blockchain = SupportedBlockchain.valueOf(blockchainName);
if (blockchain == null)
throw ApiExceptionFactory.INSTANCE.createException(request, ApiError.INVALID_CRITERIA);
byte[] refunderPubKeyHash;
byte[] redeemerPubKeyHash;
byte[] hashOfSecret;
try {
refunderPubKeyHash = HashCode.fromString(refundHex).asBytes();
redeemerPubKeyHash = HashCode.fromString(redeemHex).asBytes();
if (refunderPubKeyHash.length != 20 || redeemerPubKeyHash.length != 20)
throw ApiExceptionFactory.INSTANCE.createException(request, ApiError.INVALID_PUBLIC_KEY);
} catch (IllegalArgumentException e) {
throw ApiExceptionFactory.INSTANCE.createException(request, ApiError.INVALID_PUBLIC_KEY);
}
try {
hashOfSecret = HashCode.fromString(hashOfSecretHex).asBytes();
if (hashOfSecret.length != 20)
throw ApiExceptionFactory.INSTANCE.createException(request, ApiError.INVALID_CRITERIA);
} catch (IllegalArgumentException e) {
throw ApiExceptionFactory.INSTANCE.createException(request, ApiError.INVALID_CRITERIA);
}
byte[] redeemScript = BitcoinyHTLC.buildScript(refunderPubKeyHash, lockTime, redeemerPubKeyHash, hashOfSecret);
Bitcoiny bitcoiny = (Bitcoiny) blockchain.getInstance();
return bitcoiny.deriveP2shAddress(redeemScript);
}
@GET
@Path("/status/{blockchain}/{refundPKH}/{locktime}/{redeemPKH}/{hashOfSecret}")
@Operation(
summary = "Checks HTLC status",
description = "Blockchain can be BITCOIN or LITECOIN. Public key hashes (PKH) and hash of secret should be 20 bytes (hex). Locktime is seconds since epoch.",
responses = {
@ApiResponse(
content = @Content(mediaType = MediaType.APPLICATION_JSON, schema = @Schema(implementation = CrossChainBitcoinyHTLCStatus.class))
)
}
)
@ApiErrors({ApiError.INVALID_CRITERIA, ApiError.INVALID_ADDRESS, ApiError.ADDRESS_UNKNOWN})
public CrossChainBitcoinyHTLCStatus checkHtlcStatus(@PathParam("blockchain") String blockchainName,
@PathParam("refundPKH") String refundHex,
@PathParam("locktime") int lockTime,
@PathParam("redeemPKH") String redeemHex,
@PathParam("hashOfSecret") String hashOfSecretHex) {
Security.checkApiCallAllowed(request);
SupportedBlockchain blockchain = SupportedBlockchain.valueOf(blockchainName);
if (blockchain == null)
throw ApiExceptionFactory.INSTANCE.createException(request, ApiError.INVALID_CRITERIA);
byte[] refunderPubKeyHash;
byte[] redeemerPubKeyHash;
byte[] hashOfSecret;
try {
refunderPubKeyHash = HashCode.fromString(refundHex).asBytes();
redeemerPubKeyHash = HashCode.fromString(redeemHex).asBytes();
if (refunderPubKeyHash.length != 20 || redeemerPubKeyHash.length != 20)
throw ApiExceptionFactory.INSTANCE.createException(request, ApiError.INVALID_PUBLIC_KEY);
} catch (IllegalArgumentException e) {
throw ApiExceptionFactory.INSTANCE.createException(request, ApiError.INVALID_PUBLIC_KEY);
}
try {
hashOfSecret = HashCode.fromString(hashOfSecretHex).asBytes();
if (hashOfSecret.length != 20)
throw ApiExceptionFactory.INSTANCE.createException(request, ApiError.INVALID_CRITERIA);
} catch (IllegalArgumentException e) {
throw ApiExceptionFactory.INSTANCE.createException(request, ApiError.INVALID_CRITERIA);
}
byte[] redeemScript = BitcoinyHTLC.buildScript(refunderPubKeyHash, lockTime, redeemerPubKeyHash, hashOfSecret);
Bitcoiny bitcoiny = (Bitcoiny) blockchain.getInstance();
String p2shAddress = bitcoiny.deriveP2shAddress(redeemScript);
long now = NTP.getTime();
try {
int medianBlockTime = bitcoiny.getMedianBlockTime();
// Check P2SH is funded
long p2shBalance = bitcoiny.getConfirmedBalance(p2shAddress.toString());
CrossChainBitcoinyHTLCStatus htlcStatus = new CrossChainBitcoinyHTLCStatus();
htlcStatus.bitcoinP2shAddress = p2shAddress;
htlcStatus.bitcoinP2shBalance = BigDecimal.valueOf(p2shBalance, 8);
List<TransactionOutput> fundingOutputs = bitcoiny.getUnspentOutputs(p2shAddress.toString());
if (p2shBalance > 0L && !fundingOutputs.isEmpty()) {
htlcStatus.canRedeem = now >= medianBlockTime * 1000L;
htlcStatus.canRefund = now >= lockTime * 1000L;
}
if (now >= medianBlockTime * 1000L) {
// See if we can extract secret
htlcStatus.secret = BitcoinyHTLC.findHtlcSecret(bitcoiny, htlcStatus.bitcoinP2shAddress);
}
return htlcStatus;
} catch (ForeignBlockchainException e) {
throw ApiExceptionFactory.INSTANCE.createException(request, ApiError.FOREIGN_BLOCKCHAIN_NETWORK_ISSUE);
}
}
// TODO: refund
// TODO: redeem
}

View File

@@ -1,167 +0,0 @@
package org.qortal.api.resource;
import io.swagger.v3.oas.annotations.Operation;
import io.swagger.v3.oas.annotations.media.ArraySchema;
import io.swagger.v3.oas.annotations.media.Content;
import io.swagger.v3.oas.annotations.media.Schema;
import io.swagger.v3.oas.annotations.parameters.RequestBody;
import io.swagger.v3.oas.annotations.responses.ApiResponse;
import io.swagger.v3.oas.annotations.tags.Tag;
import java.util.List;
import javax.servlet.http.HttpServletRequest;
import javax.ws.rs.POST;
import javax.ws.rs.Path;
import javax.ws.rs.core.Context;
import javax.ws.rs.core.MediaType;
import org.bitcoinj.core.Transaction;
import org.qortal.api.ApiError;
import org.qortal.api.ApiErrors;
import org.qortal.api.ApiExceptionFactory;
import org.qortal.api.Security;
import org.qortal.api.model.crosschain.LitecoinSendRequest;
import org.qortal.crosschain.BitcoinyTransaction;
import org.qortal.crosschain.ForeignBlockchainException;
import org.qortal.crosschain.Litecoin;
@Path("/crosschain/ltc")
@Tag(name = "Cross-Chain (Litecoin)")
public class CrossChainLitecoinResource {
@Context
HttpServletRequest request;
@POST
@Path("/walletbalance")
@Operation(
summary = "Returns LTC balance for hierarchical, deterministic BIP32 wallet",
description = "Supply BIP32 'm' private/public key in base58, starting with 'xprv'/'xpub' for mainnet, 'tprv'/'tpub' for testnet",
requestBody = @RequestBody(
required = true,
content = @Content(
mediaType = MediaType.TEXT_PLAIN,
schema = @Schema(
type = "string",
description = "BIP32 'm' private/public key in base58",
example = "tpubD6NzVbkrYhZ4XTPc4btCZ6SMgn8CxmWkj6VBVZ1tfcJfMq4UwAjZbG8U74gGSypL9XBYk2R2BLbDBe8pcEyBKM1edsGQEPKXNbEskZozeZc"
)
)
),
responses = {
@ApiResponse(
content = @Content(mediaType = MediaType.TEXT_PLAIN, schema = @Schema(type = "string", description = "balance (satoshis)"))
)
}
)
@ApiErrors({ApiError.INVALID_PRIVATE_KEY, ApiError.FOREIGN_BLOCKCHAIN_NETWORK_ISSUE})
public String getLitecoinWalletBalance(String key58) {
Security.checkApiCallAllowed(request);
Litecoin litecoin = Litecoin.getInstance();
if (!litecoin.isValidDeterministicKey(key58))
throw ApiExceptionFactory.INSTANCE.createException(request, ApiError.INVALID_PRIVATE_KEY);
Long balance = litecoin.getWalletBalance(key58);
if (balance == null)
throw ApiExceptionFactory.INSTANCE.createException(request, ApiError.FOREIGN_BLOCKCHAIN_NETWORK_ISSUE);
return balance.toString();
}
@POST
@Path("/wallettransactions")
@Operation(
summary = "Returns transactions for hierarchical, deterministic BIP32 wallet",
description = "Supply BIP32 'm' private/public key in base58, starting with 'xprv'/'xpub' for mainnet, 'tprv'/'tpub' for testnet",
requestBody = @RequestBody(
required = true,
content = @Content(
mediaType = MediaType.TEXT_PLAIN,
schema = @Schema(
type = "string",
description = "BIP32 'm' private/public key in base58",
example = "tpubD6NzVbkrYhZ4XTPc4btCZ6SMgn8CxmWkj6VBVZ1tfcJfMq4UwAjZbG8U74gGSypL9XBYk2R2BLbDBe8pcEyBKM1edsGQEPKXNbEskZozeZc"
)
)
),
responses = {
@ApiResponse(
content = @Content(array = @ArraySchema( schema = @Schema( implementation = BitcoinyTransaction.class ) ) )
)
}
)
@ApiErrors({ApiError.INVALID_PRIVATE_KEY, ApiError.FOREIGN_BLOCKCHAIN_NETWORK_ISSUE})
public List<BitcoinyTransaction> getLitecoinWalletTransactions(String key58) {
Security.checkApiCallAllowed(request);
Litecoin litecoin = Litecoin.getInstance();
if (!litecoin.isValidDeterministicKey(key58))
throw ApiExceptionFactory.INSTANCE.createException(request, ApiError.INVALID_PRIVATE_KEY);
try {
return litecoin.getWalletTransactions(key58);
} catch (ForeignBlockchainException e) {
throw ApiExceptionFactory.INSTANCE.createException(request, ApiError.FOREIGN_BLOCKCHAIN_NETWORK_ISSUE);
}
}
@POST
@Path("/send")
@Operation(
summary = "Sends LTC from hierarchical, deterministic BIP32 wallet to specific address",
description = "Currently only supports 'legacy' P2PKH Litecoin addresses. Supply BIP32 'm' private key in base58, starting with 'xprv' for mainnet, 'tprv' for testnet",
requestBody = @RequestBody(
required = true,
content = @Content(
mediaType = MediaType.APPLICATION_JSON,
schema = @Schema(
implementation = LitecoinSendRequest.class
)
)
),
responses = {
@ApiResponse(
content = @Content(mediaType = MediaType.TEXT_PLAIN, schema = @Schema(type = "string", description = "transaction hash"))
)
}
)
@ApiErrors({ApiError.INVALID_PRIVATE_KEY, ApiError.INVALID_CRITERIA, ApiError.INVALID_ADDRESS, ApiError.FOREIGN_BLOCKCHAIN_BALANCE_ISSUE, ApiError.FOREIGN_BLOCKCHAIN_NETWORK_ISSUE})
public String sendBitcoin(LitecoinSendRequest litecoinSendRequest) {
Security.checkApiCallAllowed(request);
if (litecoinSendRequest.litecoinAmount <= 0)
throw ApiExceptionFactory.INSTANCE.createException(request, ApiError.INVALID_CRITERIA);
if (litecoinSendRequest.feePerByte != null && litecoinSendRequest.feePerByte <= 0)
throw ApiExceptionFactory.INSTANCE.createException(request, ApiError.INVALID_CRITERIA);
Litecoin litecoin = Litecoin.getInstance();
if (!litecoin.isValidAddress(litecoinSendRequest.receivingAddress))
throw ApiExceptionFactory.INSTANCE.createException(request, ApiError.INVALID_ADDRESS);
if (!litecoin.isValidDeterministicKey(litecoinSendRequest.xprv58))
throw ApiExceptionFactory.INSTANCE.createException(request, ApiError.INVALID_PRIVATE_KEY);
Transaction spendTransaction = litecoin.buildSpend(litecoinSendRequest.xprv58,
litecoinSendRequest.receivingAddress,
litecoinSendRequest.litecoinAmount,
litecoinSendRequest.feePerByte);
if (spendTransaction == null)
throw ApiExceptionFactory.INSTANCE.createException(request, ApiError.FOREIGN_BLOCKCHAIN_BALANCE_ISSUE);
try {
litecoin.broadcastTransaction(spendTransaction);
} catch (ForeignBlockchainException e) {
throw ApiExceptionFactory.INSTANCE.createException(request, ApiError.FOREIGN_BLOCKCHAIN_NETWORK_ISSUE);
}
return spendTransaction.getTxId().toString();
}
}

View File

@@ -1,418 +0,0 @@
package org.qortal.api.resource;
import io.swagger.v3.oas.annotations.Operation;
import io.swagger.v3.oas.annotations.Parameter;
import io.swagger.v3.oas.annotations.media.ArraySchema;
import io.swagger.v3.oas.annotations.media.Content;
import io.swagger.v3.oas.annotations.media.Schema;
import io.swagger.v3.oas.annotations.parameters.RequestBody;
import io.swagger.v3.oas.annotations.responses.ApiResponse;
import io.swagger.v3.oas.annotations.security.SecurityRequirement;
import io.swagger.v3.oas.annotations.tags.Tag;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collections;
import java.util.List;
import java.util.Map;
import java.util.Random;
import java.util.function.Supplier;
import javax.servlet.http.HttpServletRequest;
import javax.ws.rs.DELETE;
import javax.ws.rs.GET;
import javax.ws.rs.Path;
import javax.ws.rs.PathParam;
import javax.ws.rs.QueryParam;
import javax.ws.rs.core.Context;
import javax.ws.rs.core.MediaType;
import org.qortal.api.ApiError;
import org.qortal.api.ApiErrors;
import org.qortal.api.ApiExceptionFactory;
import org.qortal.api.Security;
import org.qortal.api.model.CrossChainCancelRequest;
import org.qortal.api.model.CrossChainTradeSummary;
import org.qortal.crosschain.SupportedBlockchain;
import org.qortal.crosschain.ACCT;
import org.qortal.crosschain.AcctMode;
import org.qortal.crypto.Crypto;
import org.qortal.data.at.ATData;
import org.qortal.data.at.ATStateData;
import org.qortal.data.crosschain.CrossChainTradeData;
import org.qortal.data.transaction.BaseTransactionData;
import org.qortal.data.transaction.MessageTransactionData;
import org.qortal.data.transaction.TransactionData;
import org.qortal.group.Group;
import org.qortal.repository.DataException;
import org.qortal.repository.Repository;
import org.qortal.repository.RepositoryManager;
import org.qortal.transaction.MessageTransaction;
import org.qortal.transaction.Transaction.ValidationResult;
import org.qortal.transform.TransformationException;
import org.qortal.transform.Transformer;
import org.qortal.transform.transaction.MessageTransactionTransformer;
import org.qortal.utils.Amounts;
import org.qortal.utils.Base58;
import org.qortal.utils.ByteArray;
import org.qortal.utils.NTP;
@Path("/crosschain")
@Tag(name = "Cross-Chain")
public class CrossChainResource {
@Context
HttpServletRequest request;
@GET
@Path("/tradeoffers")
@Operation(
summary = "Find cross-chain trade offers",
responses = {
@ApiResponse(
content = @Content(
array = @ArraySchema(
schema = @Schema(
implementation = CrossChainTradeData.class
)
)
)
)
}
)
@ApiErrors({ApiError.INVALID_CRITERIA, ApiError.REPOSITORY_ISSUE})
public List<CrossChainTradeData> getTradeOffers(
@Parameter(
description = "Limit to specific blockchain",
example = "LITECOIN",
schema = @Schema(implementation = SupportedBlockchain.class)
) @QueryParam("foreignBlockchain") SupportedBlockchain foreignBlockchain,
@Parameter( ref = "limit") @QueryParam("limit") Integer limit,
@Parameter( ref = "offset" ) @QueryParam("offset") Integer offset,
@Parameter( ref = "reverse" ) @QueryParam("reverse") Boolean reverse) {
// Impose a limit on 'limit'
if (limit != null && limit > 100)
throw ApiExceptionFactory.INSTANCE.createException(request, ApiError.INVALID_CRITERIA);
final boolean isExecutable = true;
List<CrossChainTradeData> crossChainTradesData = new ArrayList<>();
try (final Repository repository = RepositoryManager.getRepository()) {
Map<ByteArray, Supplier<ACCT>> acctsByCodeHash = SupportedBlockchain.getFilteredAcctMap(foreignBlockchain);
for (Map.Entry<ByteArray, Supplier<ACCT>> acctInfo : acctsByCodeHash.entrySet()) {
byte[] codeHash = acctInfo.getKey().value;
ACCT acct = acctInfo.getValue().get();
List<ATData> atsData = repository.getATRepository().getATsByFunctionality(codeHash, isExecutable, limit, offset, reverse);
for (ATData atData : atsData) {
CrossChainTradeData crossChainTradeData = acct.populateTradeData(repository, atData);
crossChainTradesData.add(crossChainTradeData);
}
}
return crossChainTradesData;
} catch (DataException e) {
throw ApiExceptionFactory.INSTANCE.createException(request, ApiError.REPOSITORY_ISSUE, e);
}
}
@GET
@Path("/trade/{ataddress}")
@Operation(
summary = "Show detailed trade info",
responses = {
@ApiResponse(
content = @Content(
schema = @Schema(
implementation = CrossChainTradeData.class
)
)
)
}
)
@ApiErrors({ApiError.ADDRESS_UNKNOWN, ApiError.INVALID_CRITERIA, ApiError.REPOSITORY_ISSUE})
public CrossChainTradeData getTrade(@PathParam("ataddress") String atAddress) {
try (final Repository repository = RepositoryManager.getRepository()) {
ATData atData = repository.getATRepository().fromATAddress(atAddress);
if (atData == null)
throw ApiExceptionFactory.INSTANCE.createException(request, ApiError.ADDRESS_UNKNOWN);
ACCT acct = SupportedBlockchain.getAcctByCodeHash(atData.getCodeHash());
if (acct == null)
throw ApiExceptionFactory.INSTANCE.createException(request, ApiError.INVALID_CRITERIA);
return acct.populateTradeData(repository, atData);
} catch (DataException e) {
throw ApiExceptionFactory.INSTANCE.createException(request, ApiError.REPOSITORY_ISSUE, e);
}
}
@GET
@Path("/trades")
@Operation(
summary = "Find completed cross-chain trades",
description = "Returns summary info about successfully completed cross-chain trades",
responses = {
@ApiResponse(
content = @Content(
array = @ArraySchema(
schema = @Schema(
implementation = CrossChainTradeSummary.class
)
)
)
)
}
)
@ApiErrors({ApiError.INVALID_CRITERIA, ApiError.REPOSITORY_ISSUE})
public List<CrossChainTradeSummary> getCompletedTrades(
@Parameter(
description = "Limit to specific blockchain",
example = "LITECOIN",
schema = @Schema(implementation = SupportedBlockchain.class)
) @QueryParam("foreignBlockchain") SupportedBlockchain foreignBlockchain,
@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) {
// Impose a limit on 'limit'
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;
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<CrossChainTradeSummary> crossChainTrades = new ArrayList<>();
Map<ByteArray, Supplier<ACCT>> acctsByCodeHash = SupportedBlockchain.getFilteredAcctMap(foreignBlockchain);
for (Map.Entry<ByteArray, Supplier<ACCT>> acctInfo : acctsByCodeHash.entrySet()) {
byte[] codeHash = acctInfo.getKey().value;
ACCT acct = acctInfo.getValue().get();
List<ATStateData> atStates = repository.getATRepository().getMatchingFinalATStates(codeHash,
isFinished, acct.getModeByteOffset(), (long) AcctMode.REDEEMED.value, minimumFinalHeight,
limit, offset, reverse);
for (ATStateData atState : atStates) {
CrossChainTradeData crossChainTradeData = acct.populateTradeData(repository, atState);
// We also need block timestamp for use as trade timestamp
long timestamp = repository.getBlockRepository().getTimestampFromHeight(atState.getHeight());
CrossChainTradeSummary crossChainTradeSummary = new CrossChainTradeSummary(crossChainTradeData, timestamp);
crossChainTrades.add(crossChainTradeSummary);
}
}
return crossChainTrades;
} catch (DataException e) {
throw ApiExceptionFactory.INSTANCE.createException(request, ApiError.REPOSITORY_ISSUE, e);
}
}
@GET
@Path("/price/{blockchain}")
@Operation(
summary = "Request current estimated trading price",
description = "Returns price based on most recent completed trades. Price is expressed in terms of QORT per unit foreign currency.",
responses = {
@ApiResponse(
content = @Content(
schema = @Schema(
type = "number"
)
)
)
}
)
@ApiErrors({ApiError.INVALID_CRITERIA, ApiError.REPOSITORY_ISSUE})
public long getTradePriceEstimate(
@Parameter(
description = "foreign blockchain",
example = "LITECOIN",
schema = @Schema(implementation = SupportedBlockchain.class)
) @PathParam("blockchain") SupportedBlockchain foreignBlockchain) {
// foreignBlockchain is required
if (foreignBlockchain == null)
throw ApiExceptionFactory.INSTANCE.createException(request, ApiError.INVALID_CRITERIA);
// We want both a minimum of 5 trades and enough trades to span at least 4 hours
int minimumCount = 5;
long minimumPeriod = 4 * 60 * 60 * 1000L; // ms
Boolean isFinished = Boolean.TRUE;
try (final Repository repository = RepositoryManager.getRepository()) {
Map<ByteArray, Supplier<ACCT>> acctsByCodeHash = SupportedBlockchain.getFilteredAcctMap(foreignBlockchain);
long totalForeign = 0;
long totalQort = 0;
for (Map.Entry<ByteArray, Supplier<ACCT>> acctInfo : acctsByCodeHash.entrySet()) {
byte[] codeHash = acctInfo.getKey().value;
ACCT acct = acctInfo.getValue().get();
List<ATStateData> atStates = repository.getATRepository().getMatchingFinalATStatesQuorum(codeHash,
isFinished, acct.getModeByteOffset(), (long) AcctMode.REDEEMED.value, minimumCount, minimumPeriod);
for (ATStateData atState : atStates) {
CrossChainTradeData crossChainTradeData = acct.populateTradeData(repository, atState);
totalForeign += crossChainTradeData.expectedForeignAmount;
totalQort += crossChainTradeData.qortAmount;
}
}
return Amounts.scaledDivide(totalQort, totalForeign);
} catch (DataException e) {
throw ApiExceptionFactory.INSTANCE.createException(request, ApiError.REPOSITORY_ISSUE, e);
}
}
@DELETE
@Path("/tradeoffer")
@Operation(
summary = "Builds raw, unsigned 'cancel' MESSAGE transaction that cancels cross-chain trade offer",
description = "Specify address of cross-chain AT that needs to be cancelled.<br>"
+ "AT needs to be in 'offer' mode. Messages sent to an AT in 'trade' mode will be ignored.<br>"
+ "Performs MESSAGE proof-of-work.<br>"
+ "You need to sign output with AT creator's private key otherwise the MESSAGE transaction will be invalid.",
requestBody = @RequestBody(
required = true,
content = @Content(
mediaType = MediaType.APPLICATION_JSON,
schema = @Schema(
implementation = CrossChainCancelRequest.class
)
)
),
responses = {
@ApiResponse(
content = @Content(
schema = @Schema(
type = "string"
)
)
)
}
)
@ApiErrors({ApiError.INVALID_PUBLIC_KEY, ApiError.INVALID_ADDRESS, ApiError.INVALID_CRITERIA, ApiError.REPOSITORY_ISSUE})
@SecurityRequirement(name = "apiKey")
public String cancelTrade(CrossChainCancelRequest cancelRequest) {
Security.checkApiCallAllowed(request);
byte[] creatorPublicKey = cancelRequest.creatorPublicKey;
if (creatorPublicKey == null || creatorPublicKey.length != Transformer.PUBLIC_KEY_LENGTH)
throw ApiExceptionFactory.INSTANCE.createException(request, ApiError.INVALID_PUBLIC_KEY);
if (cancelRequest.atAddress == null || !Crypto.isValidAtAddress(cancelRequest.atAddress))
throw ApiExceptionFactory.INSTANCE.createException(request, ApiError.INVALID_ADDRESS);
try (final Repository repository = RepositoryManager.getRepository()) {
ATData atData = fetchAtDataWithChecking(repository, cancelRequest.atAddress);
ACCT acct = SupportedBlockchain.getAcctByCodeHash(atData.getCodeHash());
if (acct == null)
throw ApiExceptionFactory.INSTANCE.createException(request, ApiError.INVALID_ADDRESS);
CrossChainTradeData crossChainTradeData = acct.populateTradeData(repository, atData);
if (crossChainTradeData.mode != AcctMode.OFFERING)
throw ApiExceptionFactory.INSTANCE.createException(request, ApiError.INVALID_CRITERIA);
// Does supplied public key match AT creator's public key?
if (!Arrays.equals(creatorPublicKey, atData.getCreatorPublicKey()))
throw ApiExceptionFactory.INSTANCE.createException(request, ApiError.INVALID_PUBLIC_KEY);
// Good to make MESSAGE
String atCreatorAddress = Crypto.toAddress(creatorPublicKey);
byte[] messageData = acct.buildCancelMessage(atCreatorAddress);
byte[] messageTransactionBytes = buildAtMessage(repository, creatorPublicKey, cancelRequest.atAddress, messageData);
return Base58.encode(messageTransactionBytes);
} catch (DataException e) {
throw ApiExceptionFactory.INSTANCE.createException(request, ApiError.REPOSITORY_ISSUE, e);
}
}
private ATData fetchAtDataWithChecking(Repository repository, String atAddress) throws DataException {
ATData atData = repository.getATRepository().fromATAddress(atAddress);
if (atData == null)
throw ApiExceptionFactory.INSTANCE.createException(request, ApiError.ADDRESS_UNKNOWN);
// No point sending message to AT that's finished
if (atData.getIsFinished())
throw ApiExceptionFactory.INSTANCE.createException(request, ApiError.INVALID_CRITERIA);
return atData;
}
private byte[] buildAtMessage(Repository repository, byte[] senderPublicKey, String atAddress, byte[] messageData) throws DataException {
long txTimestamp = NTP.getTime();
// senderPublicKey could be ephemeral trade public key where there is no corresponding account and hence no reference
String senderAddress = Crypto.toAddress(senderPublicKey);
byte[] lastReference = repository.getAccountRepository().getLastReference(senderAddress);
final boolean requiresPoW = lastReference == null;
if (requiresPoW) {
Random random = new Random();
lastReference = new byte[Transformer.SIGNATURE_LENGTH];
random.nextBytes(lastReference);
}
int version = 4;
int nonce = 0;
long amount = 0L;
Long assetId = null; // no assetId as amount is zero
Long fee = 0L;
BaseTransactionData baseTransactionData = new BaseTransactionData(txTimestamp, Group.NO_GROUP, lastReference, senderPublicKey, fee, null);
TransactionData messageTransactionData = new MessageTransactionData(baseTransactionData, version, nonce, atAddress, amount, assetId, messageData, false, false);
MessageTransaction messageTransaction = new MessageTransaction(repository, messageTransactionData);
if (requiresPoW) {
messageTransaction.computeNonce();
} else {
fee = messageTransaction.calcRecommendedFee();
messageTransactionData.setFee(fee);
}
ValidationResult result = messageTransaction.isValidUnconfirmed();
if (result != ValidationResult.OK)
throw TransactionsResource.createTransactionInvalidException(request, result);
try {
return MessageTransactionTransformer.toBytes(messageTransactionData);
} catch (TransformationException e) {
throw ApiExceptionFactory.INSTANCE.createException(request, ApiError.TRANSFORMATION_ERROR, e);
}
}
}

View File

@@ -1,286 +0,0 @@
package org.qortal.api.resource;
import io.swagger.v3.oas.annotations.Operation;
import io.swagger.v3.oas.annotations.Parameter;
import io.swagger.v3.oas.annotations.media.ArraySchema;
import io.swagger.v3.oas.annotations.media.Content;
import io.swagger.v3.oas.annotations.media.Schema;
import io.swagger.v3.oas.annotations.parameters.RequestBody;
import io.swagger.v3.oas.annotations.responses.ApiResponse;
import io.swagger.v3.oas.annotations.tags.Tag;
import java.util.List;
import java.util.stream.Collectors;
import javax.servlet.http.HttpServletRequest;
import javax.ws.rs.DELETE;
import javax.ws.rs.GET;
import javax.ws.rs.POST;
import javax.ws.rs.Path;
import javax.ws.rs.QueryParam;
import javax.ws.rs.core.Context;
import javax.ws.rs.core.MediaType;
import org.qortal.account.Account;
import org.qortal.account.PublicKeyAccount;
import org.qortal.api.ApiError;
import org.qortal.api.ApiErrors;
import org.qortal.api.ApiExceptionFactory;
import org.qortal.api.Security;
import org.qortal.api.model.crosschain.TradeBotCreateRequest;
import org.qortal.api.model.crosschain.TradeBotRespondRequest;
import org.qortal.asset.Asset;
import org.qortal.controller.tradebot.AcctTradeBot;
import org.qortal.controller.tradebot.TradeBot;
import org.qortal.crosschain.ForeignBlockchain;
import org.qortal.crosschain.SupportedBlockchain;
import org.qortal.crosschain.ACCT;
import org.qortal.crosschain.AcctMode;
import org.qortal.crypto.Crypto;
import org.qortal.data.at.ATData;
import org.qortal.data.crosschain.CrossChainTradeData;
import org.qortal.data.crosschain.TradeBotData;
import org.qortal.repository.DataException;
import org.qortal.repository.Repository;
import org.qortal.repository.RepositoryManager;
import org.qortal.utils.Base58;
@Path("/crosschain/tradebot")
@Tag(name = "Cross-Chain (Trade-Bot)")
public class CrossChainTradeBotResource {
@Context
HttpServletRequest request;
@GET
@Operation(
summary = "List current trade-bot states",
responses = {
@ApiResponse(
content = @Content(
array = @ArraySchema(
schema = @Schema(
implementation = TradeBotData.class
)
)
)
)
}
)
@ApiErrors({ApiError.REPOSITORY_ISSUE})
public List<TradeBotData> getTradeBotStates(
@Parameter(
description = "Limit to specific blockchain",
example = "LITECOIN",
schema = @Schema(implementation = SupportedBlockchain.class)
) @QueryParam("foreignBlockchain") SupportedBlockchain foreignBlockchain) {
Security.checkApiCallAllowed(request);
try (final Repository repository = RepositoryManager.getRepository()) {
List<TradeBotData> allTradeBotData = repository.getCrossChainRepository().getAllTradeBotData();
if (foreignBlockchain == null)
return allTradeBotData;
return allTradeBotData.stream().filter(tradeBotData -> tradeBotData.getForeignBlockchain().equals(foreignBlockchain.name())).collect(Collectors.toList());
} catch (DataException e) {
throw ApiExceptionFactory.INSTANCE.createException(request, ApiError.REPOSITORY_ISSUE, e);
}
}
@POST
@Path("/create")
@Operation(
summary = "Create a trade offer (trade-bot entry)",
requestBody = @RequestBody(
required = true,
content = @Content(
mediaType = MediaType.APPLICATION_JSON,
schema = @Schema(
implementation = TradeBotCreateRequest.class
)
)
),
responses = {
@ApiResponse(
content = @Content(mediaType = MediaType.TEXT_PLAIN, schema = @Schema(type = "string"))
)
}
)
@ApiErrors({ApiError.INVALID_PUBLIC_KEY, ApiError.INVALID_ADDRESS, ApiError.INVALID_CRITERIA, ApiError.INSUFFICIENT_BALANCE, ApiError.REPOSITORY_ISSUE})
@SuppressWarnings("deprecation")
public String tradeBotCreator(TradeBotCreateRequest tradeBotCreateRequest) {
Security.checkApiCallAllowed(request);
if (tradeBotCreateRequest.foreignBlockchain == null)
throw ApiExceptionFactory.INSTANCE.createException(request, ApiError.INVALID_CRITERIA);
ForeignBlockchain foreignBlockchain = tradeBotCreateRequest.foreignBlockchain.getInstance();
// We prefer foreignAmount to deprecated bitcoinAmount
if (tradeBotCreateRequest.foreignAmount == null)
tradeBotCreateRequest.foreignAmount = tradeBotCreateRequest.bitcoinAmount;
if (!foreignBlockchain.isValidAddress(tradeBotCreateRequest.receivingAddress))
throw ApiExceptionFactory.INSTANCE.createException(request, ApiError.INVALID_ADDRESS);
if (tradeBotCreateRequest.tradeTimeout < 60)
throw ApiExceptionFactory.INSTANCE.createException(request, ApiError.INVALID_CRITERIA);
if (tradeBotCreateRequest.foreignAmount == null || tradeBotCreateRequest.foreignAmount <= 0)
throw ApiExceptionFactory.INSTANCE.createException(request, ApiError.INVALID_CRITERIA);
if (tradeBotCreateRequest.qortAmount <= 0 || tradeBotCreateRequest.fundingQortAmount <= 0)
throw ApiExceptionFactory.INSTANCE.createException(request, ApiError.INVALID_CRITERIA);
try (final Repository repository = RepositoryManager.getRepository()) {
// Do some simple checking first
Account creator = new PublicKeyAccount(repository, tradeBotCreateRequest.creatorPublicKey);
if (creator.getConfirmedBalance(Asset.QORT) < tradeBotCreateRequest.fundingQortAmount)
throw ApiExceptionFactory.INSTANCE.createException(request, ApiError.INSUFFICIENT_BALANCE);
byte[] unsignedBytes = TradeBot.getInstance().createTrade(repository, tradeBotCreateRequest);
if (unsignedBytes == null)
throw ApiExceptionFactory.INSTANCE.createException(request, ApiError.INVALID_CRITERIA);
return Base58.encode(unsignedBytes);
} catch (DataException e) {
throw ApiExceptionFactory.INSTANCE.createException(request, ApiError.REPOSITORY_ISSUE, e);
}
}
@POST
@Path("/respond")
@Operation(
summary = "Respond to a trade offer. NOTE: WILL SPEND FUNDS!)",
description = "Start a new trade-bot entry to respond to chosen trade offer.",
requestBody = @RequestBody(
required = true,
content = @Content(
mediaType = MediaType.APPLICATION_JSON,
schema = @Schema(
implementation = TradeBotRespondRequest.class
)
)
),
responses = {
@ApiResponse(
content = @Content(mediaType = MediaType.TEXT_PLAIN, schema = @Schema(type = "string"))
)
}
)
@ApiErrors({ApiError.INVALID_PRIVATE_KEY, ApiError.INVALID_ADDRESS, ApiError.INVALID_CRITERIA, ApiError.FOREIGN_BLOCKCHAIN_BALANCE_ISSUE, ApiError.FOREIGN_BLOCKCHAIN_NETWORK_ISSUE, ApiError.REPOSITORY_ISSUE})
@SuppressWarnings("deprecation")
public String tradeBotResponder(TradeBotRespondRequest tradeBotRespondRequest) {
Security.checkApiCallAllowed(request);
final String atAddress = tradeBotRespondRequest.atAddress;
// We prefer foreignKey to deprecated xprv58
if (tradeBotRespondRequest.foreignKey == null)
tradeBotRespondRequest.foreignKey = tradeBotRespondRequest.xprv58;
if (tradeBotRespondRequest.foreignKey == null)
throw ApiExceptionFactory.INSTANCE.createException(request, ApiError.INVALID_PRIVATE_KEY);
if (atAddress == null || !Crypto.isValidAtAddress(atAddress))
throw ApiExceptionFactory.INSTANCE.createException(request, ApiError.INVALID_ADDRESS);
if (tradeBotRespondRequest.receivingAddress == null || !Crypto.isValidAddress(tradeBotRespondRequest.receivingAddress))
throw ApiExceptionFactory.INSTANCE.createException(request, ApiError.INVALID_ADDRESS);
// Extract data from cross-chain trading AT
try (final Repository repository = RepositoryManager.getRepository()) {
ATData atData = fetchAtDataWithChecking(repository, atAddress);
// TradeBot uses AT's code hash to map to ACCT
ACCT acct = TradeBot.getInstance().getAcctUsingAtData(atData);
if (acct == null)
throw ApiExceptionFactory.INSTANCE.createException(request, ApiError.INVALID_ADDRESS);
if (!acct.getBlockchain().isValidWalletKey(tradeBotRespondRequest.foreignKey))
throw ApiExceptionFactory.INSTANCE.createException(request, ApiError.INVALID_PRIVATE_KEY);
CrossChainTradeData crossChainTradeData = acct.populateTradeData(repository, atData);
if (crossChainTradeData == null)
throw ApiExceptionFactory.INSTANCE.createException(request, ApiError.INVALID_ADDRESS);
if (crossChainTradeData.mode != AcctMode.OFFERING)
throw ApiExceptionFactory.INSTANCE.createException(request, ApiError.INVALID_CRITERIA);
AcctTradeBot.ResponseResult result = TradeBot.getInstance().startResponse(repository, atData, acct, crossChainTradeData,
tradeBotRespondRequest.foreignKey, tradeBotRespondRequest.receivingAddress);
switch (result) {
case OK:
return "true";
case BALANCE_ISSUE:
throw ApiExceptionFactory.INSTANCE.createException(request, ApiError.FOREIGN_BLOCKCHAIN_BALANCE_ISSUE);
case NETWORK_ISSUE:
throw ApiExceptionFactory.INSTANCE.createException(request, ApiError.FOREIGN_BLOCKCHAIN_NETWORK_ISSUE);
default:
return "false";
}
} catch (DataException e) {
throw ApiExceptionFactory.INSTANCE.createException(request, ApiError.REPOSITORY_ISSUE, e);
}
}
@DELETE
@Operation(
summary = "Delete completed trade",
requestBody = @RequestBody(
required = true,
content = @Content(
mediaType = MediaType.TEXT_PLAIN,
schema = @Schema(
type = "string",
example = "93MB2qRDNVLxbmmPuYpLdAqn3u2x9ZhaVZK5wELHueP8"
)
)
),
responses = {
@ApiResponse(
content = @Content(mediaType = MediaType.TEXT_PLAIN, schema = @Schema(type = "string"))
)
}
)
@ApiErrors({ApiError.INVALID_ADDRESS, ApiError.REPOSITORY_ISSUE})
public String tradeBotDelete(String tradePrivateKey58) {
Security.checkApiCallAllowed(request);
final byte[] tradePrivateKey;
try {
tradePrivateKey = Base58.decode(tradePrivateKey58);
if (tradePrivateKey.length != 32)
throw ApiExceptionFactory.INSTANCE.createException(request, ApiError.INVALID_PRIVATE_KEY);
} catch (NumberFormatException e) {
throw ApiExceptionFactory.INSTANCE.createException(request, ApiError.INVALID_PRIVATE_KEY);
}
try (final Repository repository = RepositoryManager.getRepository()) {
// Handed off to TradeBot
return TradeBot.getInstance().deleteEntry(repository, tradePrivateKey) ? "true" : "false";
} catch (DataException e) {
throw ApiExceptionFactory.INSTANCE.createException(request, ApiError.REPOSITORY_ISSUE, e);
}
}
private ATData fetchAtDataWithChecking(Repository repository, String atAddress) throws DataException {
ATData atData = repository.getATRepository().fromATAddress(atAddress);
if (atData == null)
throw ApiExceptionFactory.INSTANCE.createException(request, ApiError.ADDRESS_UNKNOWN);
// No point sending message to AT that's finished
if (atData.getIsFinished())
throw ApiExceptionFactory.INSTANCE.createException(request, ApiError.INVALID_CRITERIA);
return atData;
}
}

View File

@@ -6,12 +6,9 @@ import io.swagger.v3.oas.annotations.media.Content;
import io.swagger.v3.oas.annotations.media.Schema;
import io.swagger.v3.oas.annotations.parameters.RequestBody;
import io.swagger.v3.oas.annotations.responses.ApiResponse;
import io.swagger.v3.oas.annotations.security.SecurityRequirement;
import io.swagger.v3.oas.annotations.tags.Tag;
import java.net.InetSocketAddress;
import java.net.UnknownHostException;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.List;
import java.util.stream.Collectors;
@@ -29,17 +26,10 @@ import org.qortal.api.ApiException;
import org.qortal.api.ApiExceptionFactory;
import org.qortal.api.Security;
import org.qortal.api.model.ConnectedPeer;
import org.qortal.controller.Controller;
import org.qortal.controller.Synchronizer;
import org.qortal.controller.Synchronizer.SynchronizationResult;
import org.qortal.data.block.BlockSummaryData;
import org.qortal.data.network.PeerData;
import org.qortal.network.Network;
import org.qortal.network.Peer;
import org.qortal.network.PeerAddress;
import org.qortal.repository.DataException;
import org.qortal.repository.Repository;
import org.qortal.repository.RepositoryManager;
import org.qortal.utils.ExecuteProduceConsume;
import org.qortal.utils.NTP;
@@ -132,7 +122,6 @@ public class PeersResource {
)
}
)
@SecurityRequirement(name = "apiKey")
public ExecuteProduceConsume.StatsSnapshot getEngineStats() {
Security.checkApiCallAllowed(request);
@@ -170,7 +159,6 @@ public class PeersResource {
@ApiErrors({
ApiError.INVALID_NETWORK_ADDRESS, ApiError.REPOSITORY_ISSUE
})
@SecurityRequirement(name = "apiKey")
public String addPeer(String address) {
Security.checkApiCallAllowed(request);
@@ -181,14 +169,13 @@ public class PeersResource {
try {
PeerAddress peerAddress = PeerAddress.fromString(address);
List<PeerAddress> newPeerAddresses = new ArrayList<>(1);
newPeerAddresses.add(peerAddress);
Network.getInstance().mergePeers("API", addedWhen, Arrays.asList(peerAddress));
boolean addResult = Network.getInstance().mergePeers("API", addedWhen, newPeerAddresses);
return addResult ? "true" : "false";
return "true";
} catch (IllegalArgumentException e) {
throw ApiExceptionFactory.INSTANCE.createException(request, ApiError.INVALID_NETWORK_ADDRESS);
} catch (ApiException e) {
throw e;
} catch (DataException e) {
throw ApiExceptionFactory.INSTANCE.createException(request, ApiError.REPOSITORY_ISSUE, e);
}
@@ -225,7 +212,6 @@ public class PeersResource {
@ApiErrors({
ApiError.INVALID_NETWORK_ADDRESS, ApiError.REPOSITORY_ISSUE
})
@SecurityRequirement(name = "apiKey")
public String removePeer(String address) {
Security.checkApiCallAllowed(request);
@@ -261,7 +247,6 @@ public class PeersResource {
@ApiErrors({
ApiError.REPOSITORY_ISSUE
})
@SecurityRequirement(name = "apiKey")
public String removeKnownPeers(String address) {
Security.checkApiCallAllowed(request);
@@ -274,68 +259,4 @@ public class PeersResource {
}
}
@POST
@Path("/commonblock")
@Operation(
summary = "Report common block with given peer.",
requestBody = @RequestBody(
required = true,
content = @Content(
mediaType = MediaType.TEXT_PLAIN,
schema = @Schema(
type = "string", example = "node2.qortal.org"
)
)
),
responses = {
@ApiResponse(
description = "the block",
content = @Content(
array = @ArraySchema(
schema = @Schema(
implementation = BlockSummaryData.class
)
)
)
)
}
)
@ApiErrors({ApiError.INVALID_DATA, ApiError.REPOSITORY_ISSUE})
@SecurityRequirement(name = "apiKey")
public List<BlockSummaryData> commonBlock(String targetPeerAddress) {
Security.checkApiCallAllowed(request);
try {
// Try to resolve passed address to make things easier
PeerAddress peerAddress = PeerAddress.fromString(targetPeerAddress);
InetSocketAddress resolvedAddress = peerAddress.toSocketAddress();
List<Peer> peers = Network.getInstance().getHandshakedPeers();
Peer targetPeer = peers.stream().filter(peer -> peer.getResolvedAddress().equals(resolvedAddress)).findFirst().orElse(null);
if (targetPeer == null)
throw ApiExceptionFactory.INSTANCE.createException(request, ApiError.INVALID_DATA);
try (final Repository repository = RepositoryManager.getRepository()) {
int ourInitialHeight = Controller.getInstance().getChainHeight();
boolean force = true;
List<BlockSummaryData> peerBlockSummaries = new ArrayList<>();
SynchronizationResult findCommonBlockResult = Synchronizer.getInstance().fetchSummariesFromCommonBlock(repository, targetPeer, ourInitialHeight, force, peerBlockSummaries);
if (findCommonBlockResult != SynchronizationResult.OK)
return null;
return peerBlockSummaries;
}
} catch (IllegalArgumentException e) {
throw ApiExceptionFactory.INSTANCE.createException(request, ApiError.INVALID_DATA);
} catch (UnknownHostException e) {
throw ApiExceptionFactory.INSTANCE.createException(request, ApiError.INVALID_DATA);
} catch (DataException e) {
throw ApiExceptionFactory.INSTANCE.createException(request, ApiError.REPOSITORY_ISSUE, e);
} catch (InterruptedException e) {
return null;
}
}
}

View File

@@ -363,60 +363,6 @@ public class TransactionsResource {
}
}
@GET
@Path("/creator/{publickey}")
@Operation(
summary = "Find matching transactions created by account with given public key",
responses = {
@ApiResponse(
description = "transactions",
content = @Content(
array = @ArraySchema(
schema = @Schema(
implementation = TransactionData.class
)
)
)
)
}
)
@ApiErrors({
ApiError.INVALID_CRITERIA, ApiError.REPOSITORY_ISSUE
})
public List<TransactionData> findCreatorsTransactions(@PathParam("publickey") String publicKey58,
@Parameter(
description = "whether to include confirmed, unconfirmed or both",
required = true
) @QueryParam("confirmationStatus") ConfirmationStatus confirmationStatus, @Parameter(
ref = "limit"
) @QueryParam("limit") Integer limit, @Parameter(
ref = "offset"
) @QueryParam("offset") Integer offset, @Parameter(
ref = "reverse"
) @QueryParam("reverse") Boolean reverse) {
// Decode public key
byte[] publicKey;
try {
publicKey = Base58.decode(publicKey58);
} catch (NumberFormatException e) {
throw ApiExceptionFactory.INSTANCE.createException(request, ApiError.INVALID_PUBLIC_KEY, e);
}
try (final Repository repository = RepositoryManager.getRepository()) {
List<byte[]> signatures = repository.getTransactionRepository().getSignaturesMatchingCriteria(null,
publicKey, confirmationStatus, limit, offset, reverse);
// Expand signatures to transactions
List<TransactionData> transactions = new ArrayList<>(signatures.size());
for (byte[] signature : signatures)
transactions.add(repository.getTransactionRepository().fromSignature(signature));
return transactions;
} catch (DataException e) {
throw ApiExceptionFactory.INSTANCE.createException(request, ApiError.REPOSITORY_ISSUE, e);
}
}
@POST
@Path("/sign")
@Operation(
@@ -510,19 +456,14 @@ public class TransactionsResource {
if (!Controller.getInstance().isUpToDate())
throw ApiExceptionFactory.INSTANCE.createException(request, ApiError.BLOCKCHAIN_NEEDS_SYNC);
byte[] rawBytes = Base58.decode(rawBytes58);
TransactionData transactionData;
try {
transactionData = TransactionTransformer.fromBytes(rawBytes);
} catch (TransformationException e) {
throw ApiExceptionFactory.INSTANCE.createException(request, ApiError.TRANSFORMATION_ERROR, e);
}
if (transactionData == null)
throw ApiExceptionFactory.INSTANCE.createException(request, ApiError.INVALID_DATA);
try (final Repository repository = RepositoryManager.getRepository()) {
byte[] rawBytes = Base58.decode(rawBytes58);
TransactionData transactionData = TransactionTransformer.fromBytes(rawBytes);
if (transactionData == null)
throw ApiExceptionFactory.INSTANCE.createException(request, ApiError.INVALID_DATA);
Transaction transaction = Transaction.fromData(repository, transactionData);
if (!transaction.isSignatureValid())
@@ -536,13 +477,20 @@ public class TransactionsResource {
ValidationResult result = transaction.importAsUnconfirmed();
if (result != ValidationResult.OK)
throw createTransactionInvalidException(request, result);
// Notify controller of new transaction
Controller.getInstance().onNewTransaction(transactionData);
return "true";
} finally {
blockchainLock.unlock();
}
return "true";
} catch (NumberFormatException e) {
throw ApiExceptionFactory.INSTANCE.createException(request, ApiError.INVALID_DATA, e);
} catch (TransformationException e) {
throw ApiExceptionFactory.INSTANCE.createException(request, ApiError.TRANSFORMATION_ERROR, e);
} catch (ApiException e) {
throw e;
} catch (DataException e) {
throw ApiExceptionFactory.INSTANCE.createException(request, ApiError.REPOSITORY_ISSUE, e);
} catch (InterruptedException e) {

View File

@@ -1,96 +0,0 @@
package org.qortal.api.websocket;
import java.io.IOException;
import java.io.StringWriter;
import java.util.Map;
import java.util.concurrent.atomic.AtomicReference;
import org.eclipse.jetty.websocket.api.Session;
import org.eclipse.jetty.websocket.api.WebSocketException;
import org.eclipse.jetty.websocket.api.annotations.OnWebSocketClose;
import org.eclipse.jetty.websocket.api.annotations.OnWebSocketConnect;
import org.eclipse.jetty.websocket.api.annotations.OnWebSocketError;
import org.eclipse.jetty.websocket.api.annotations.OnWebSocketMessage;
import org.eclipse.jetty.websocket.api.annotations.WebSocket;
import org.eclipse.jetty.websocket.servlet.WebSocketServletFactory;
import org.qortal.controller.ChatNotifier;
import org.qortal.crypto.Crypto;
import org.qortal.data.chat.ActiveChats;
import org.qortal.data.transaction.ChatTransactionData;
import org.qortal.repository.DataException;
import org.qortal.repository.Repository;
import org.qortal.repository.RepositoryManager;
@WebSocket
@SuppressWarnings("serial")
public class ActiveChatsWebSocket extends ApiWebSocket {
@Override
public void configure(WebSocketServletFactory factory) {
factory.register(ActiveChatsWebSocket.class);
}
@OnWebSocketConnect
@Override
public void onWebSocketConnect(Session session) {
Map<String, String> pathParams = getPathParams(session, "/{address}");
String address = pathParams.get("address");
if (address == null || !Crypto.isValidAddress(address)) {
session.close(4001, "invalid address");
return;
}
AtomicReference<String> previousOutput = new AtomicReference<>(null);
ChatNotifier.Listener listener = chatTransactionData -> onNotify(session, chatTransactionData, address, previousOutput);
ChatNotifier.getInstance().register(session, listener);
this.onNotify(session, null, address, previousOutput);
}
@OnWebSocketClose
@Override
public void onWebSocketClose(Session session, int statusCode, String reason) {
ChatNotifier.getInstance().deregister(session);
}
@OnWebSocketError
public void onWebSocketError(Session session, Throwable throwable) {
/* ignored */
}
@OnWebSocketMessage
public void onWebSocketMessage(Session session, String message) {
/* ignored */
}
private void onNotify(Session session, ChatTransactionData chatTransactionData, String ourAddress, AtomicReference<String> previousOutput) {
// If CHAT has a recipient (i.e. direct message, not group-based) and we're neither sender nor recipient, then it's of no interest
if (chatTransactionData != null) {
String recipient = chatTransactionData.getRecipient();
if (recipient != null && (!recipient.equals(ourAddress) && !chatTransactionData.getSender().equals(ourAddress)))
return;
}
try (final Repository repository = RepositoryManager.getRepository()) {
ActiveChats activeChats = repository.getChatRepository().getActiveChats(ourAddress);
StringWriter stringWriter = new StringWriter();
marshall(stringWriter, activeChats);
// Only output if something has changed
String output = stringWriter.toString();
if (output.equals(previousOutput.get()))
return;
previousOutput.set(output);
session.getRemote().sendStringByFuture(output);
} catch (DataException | IOException | WebSocketException e) {
// No output this time?
}
}
}

View File

@@ -1,101 +0,0 @@
package org.qortal.api.websocket;
import java.io.IOException;
import java.io.StringWriter;
import java.util.concurrent.atomic.AtomicReference;
import org.eclipse.jetty.websocket.api.Session;
import org.eclipse.jetty.websocket.api.WebSocketException;
import org.eclipse.jetty.websocket.api.annotations.OnWebSocketClose;
import org.eclipse.jetty.websocket.api.annotations.OnWebSocketConnect;
import org.eclipse.jetty.websocket.api.annotations.OnWebSocketError;
import org.eclipse.jetty.websocket.api.annotations.OnWebSocketMessage;
import org.eclipse.jetty.websocket.api.annotations.WebSocket;
import org.eclipse.jetty.websocket.servlet.WebSocketServletFactory;
import org.qortal.api.model.NodeStatus;
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 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) {
this.sendStatus(session, previousOutput.get());
super.onWebSocketConnect(session);
}
@OnWebSocketClose
@Override
public void onWebSocketClose(Session session, int statusCode, String reason) {
super.onWebSocketClose(session, statusCode, reason);
}
@OnWebSocketError
public void onWebSocketError(Session session, Throwable throwable) {
/* We ignore errors for now, but method here to silence log spam */
}
@OnWebSocketMessage
public void onWebSocketMessage(Session session, String message) {
/* ignored */
}
private static String buildStatusString() throws IOException {
NodeStatus nodeStatus = new NodeStatus();
StringWriter stringWriter = new StringWriter();
marshall(stringWriter, nodeStatus);
return stringWriter.toString();
}
private void sendStatus(Session session, String status) {
try {
session.getRemote().sendStringByFuture(status);
} catch (WebSocketException e) {
// No output this time?
}
}
}

View File

@@ -1,122 +0,0 @@
package org.qortal.api.websocket;
import java.io.IOException;
import java.io.StringWriter;
import java.io.Writer;
import java.util.ArrayList;
import java.util.Collection;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import javax.xml.bind.JAXBContext;
import javax.xml.bind.JAXBException;
import javax.xml.bind.Marshaller;
import org.eclipse.jetty.http.pathmap.UriTemplatePathSpec;
import org.eclipse.jetty.websocket.api.Session;
import org.eclipse.jetty.websocket.servlet.ServletUpgradeRequest;
import org.eclipse.jetty.websocket.servlet.WebSocketServlet;
import org.eclipse.persistence.jaxb.JAXBContextFactory;
import org.eclipse.persistence.jaxb.MarshallerProperties;
import org.qortal.api.ApiError;
import org.qortal.api.ApiErrorRoot;
@SuppressWarnings("serial")
abstract class ApiWebSocket extends WebSocketServlet {
private static final Map<Class<? extends ApiWebSocket>, List<Session>> SESSIONS_BY_CLASS = new HashMap<>();
protected static String getPathInfo(Session session) {
ServletUpgradeRequest upgradeRequest = (ServletUpgradeRequest) session.getUpgradeRequest();
return upgradeRequest.getHttpServletRequest().getPathInfo();
}
protected static Map<String, String> getPathParams(Session session, String pathSpec) {
UriTemplatePathSpec uriTemplatePathSpec = new UriTemplatePathSpec(pathSpec);
return uriTemplatePathSpec.getPathParams(getPathInfo(session));
}
protected static void sendError(Session session, ApiError apiError) {
ApiErrorRoot apiErrorRoot = new ApiErrorRoot();
apiErrorRoot.setApiError(apiError);
StringWriter stringWriter = new StringWriter();
try {
marshall(stringWriter, apiErrorRoot);
session.getRemote().sendString(stringWriter.toString());
} catch (IOException e) {
// Remote end probably closed
}
}
protected static void marshall(Writer writer, Object object) throws IOException {
Marshaller marshaller = createMarshaller(object.getClass());
try {
marshaller.marshal(object, writer);
} catch (JAXBException e) {
throw new IOException("Unable to create marshall object for websocket", e);
}
}
protected static void marshall(Writer writer, Collection<?> collection) throws IOException {
// If collection is empty then we're returning "[]" anyway
if (collection.isEmpty()) {
writer.append("[]");
return;
}
// Grab an entry from collection so we can determine type
Object entry = collection.iterator().next();
Marshaller marshaller = createMarshaller(entry.getClass());
try {
marshaller.marshal(collection, writer);
} catch (JAXBException e) {
throw new IOException("Unable to create marshall object for websocket", e);
}
}
private static Marshaller createMarshaller(Class<?> objectClass) {
try {
// Create JAXB context aware of object's class
JAXBContext jc = JAXBContextFactory.createContext(new Class[] { objectClass }, null);
// Create marshaller
Marshaller marshaller = jc.createMarshaller();
// Set the marshaller media type to JSON
marshaller.setProperty(MarshallerProperties.MEDIA_TYPE, "application/json");
// Tell marshaller not to include JSON root element in the output
marshaller.setProperty(MarshallerProperties.JSON_INCLUDE_ROOT, false);
return marshaller;
} catch (JAXBException e) {
throw new RuntimeException("Unable to create websocket marshaller", e);
}
}
public void onWebSocketConnect(Session session) {
synchronized (SESSIONS_BY_CLASS) {
SESSIONS_BY_CLASS.computeIfAbsent(this.getClass(), clazz -> new ArrayList<>()).add(session);
}
}
public void onWebSocketClose(Session session, int statusCode, String reason) {
synchronized (SESSIONS_BY_CLASS) {
List<Session> sessions = SESSIONS_BY_CLASS.get(this.getClass());
if (sessions != null)
sessions.remove(session);
}
}
protected List<Session> getSessions() {
synchronized (SESSIONS_BY_CLASS) {
return new ArrayList<>(SESSIONS_BY_CLASS.get(this.getClass()));
}
}
}

View File

@@ -1,141 +0,0 @@
package org.qortal.api.websocket;
import java.io.IOException;
import java.io.StringWriter;
import java.util.List;
import org.eclipse.jetty.websocket.api.Session;
import org.eclipse.jetty.websocket.api.WebSocketException;
import org.eclipse.jetty.websocket.api.annotations.OnWebSocketClose;
import org.eclipse.jetty.websocket.api.annotations.OnWebSocketConnect;
import org.eclipse.jetty.websocket.api.annotations.OnWebSocketError;
import org.eclipse.jetty.websocket.api.annotations.OnWebSocketMessage;
import org.eclipse.jetty.websocket.api.annotations.WebSocket;
import org.eclipse.jetty.websocket.servlet.WebSocketServletFactory;
import org.qortal.api.ApiError;
import org.qortal.controller.Controller;
import org.qortal.data.block.BlockData;
import org.qortal.data.block.BlockSummaryData;
import org.qortal.event.Event;
import org.qortal.event.EventBus;
import org.qortal.event.Listener;
import org.qortal.repository.DataException;
import org.qortal.repository.Repository;
import org.qortal.repository.RepositoryManager;
import org.qortal.utils.Base58;
@WebSocket
@SuppressWarnings("serial")
public class 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();
BlockSummaryData blockSummary = new BlockSummaryData(blockData);
for (Session session : getSessions())
sendBlockSummary(session, blockSummary);
}
@OnWebSocketConnect
@Override
public void onWebSocketConnect(Session session) {
super.onWebSocketConnect(session);
}
@OnWebSocketClose
@Override
public void onWebSocketClose(Session session, int statusCode, String reason) {
super.onWebSocketClose(session, statusCode, reason);
}
@OnWebSocketError
public void onWebSocketError(Session session, Throwable throwable) {
/* We ignore errors for now, but method here to silence log spam */
}
@OnWebSocketMessage
public void onWebSocketMessage(Session session, String message) {
// We're expecting either a base58 block signature or an integer block height
if (message.length() > 128) {
// Try base58 block signature
byte[] signature;
try {
signature = Base58.decode(message);
} catch (NumberFormatException e) {
sendError(session, ApiError.INVALID_SIGNATURE);
return;
}
try (final Repository repository = RepositoryManager.getRepository()) {
int height = repository.getBlockRepository().getHeightFromSignature(signature);
if (height == 0) {
sendError(session, ApiError.BLOCK_UNKNOWN);
return;
}
List<BlockSummaryData> blockSummaries = repository.getBlockRepository().getBlockSummaries(height, height);
if (blockSummaries == null || blockSummaries.isEmpty()) {
sendError(session, ApiError.BLOCK_UNKNOWN);
return;
}
sendBlockSummary(session, blockSummaries.get(0));
} catch (DataException e) {
sendError(session, ApiError.REPOSITORY_ISSUE);
}
return;
}
if (message.length() > 10)
// Bigger than max integer value, so probably a ping - silently ignore
return;
// Try integer
int height;
try {
height = Integer.parseInt(message);
} catch (NumberFormatException e) {
sendError(session, ApiError.INVALID_HEIGHT);
return;
}
try (final Repository repository = RepositoryManager.getRepository()) {
List<BlockSummaryData> blockSummaries = repository.getBlockRepository().getBlockSummaries(height, height);
if (blockSummaries == null || blockSummaries.isEmpty()) {
sendError(session, ApiError.BLOCK_UNKNOWN);
return;
}
sendBlockSummary(session, blockSummaries.get(0));
} catch (DataException e) {
sendError(session, ApiError.REPOSITORY_ISSUE);
}
}
private void sendBlockSummary(Session session, BlockSummaryData blockSummary) {
StringWriter stringWriter = new StringWriter();
try {
marshall(stringWriter, blockSummary);
session.getRemote().sendStringByFuture(stringWriter.toString());
} catch (IOException | WebSocketException e) {
// No output this time
}
}
}

View File

@@ -1,156 +0,0 @@
package org.qortal.api.websocket;
import java.io.IOException;
import java.io.StringWriter;
import java.util.Arrays;
import java.util.Collections;
import java.util.List;
import java.util.Map;
import org.eclipse.jetty.websocket.api.Session;
import org.eclipse.jetty.websocket.api.WebSocketException;
import org.eclipse.jetty.websocket.api.annotations.OnWebSocketClose;
import org.eclipse.jetty.websocket.api.annotations.OnWebSocketConnect;
import org.eclipse.jetty.websocket.api.annotations.OnWebSocketError;
import org.eclipse.jetty.websocket.api.annotations.OnWebSocketMessage;
import org.eclipse.jetty.websocket.api.annotations.WebSocket;
import org.eclipse.jetty.websocket.servlet.WebSocketServletFactory;
import org.qortal.controller.ChatNotifier;
import org.qortal.data.chat.ChatMessage;
import org.qortal.data.transaction.ChatTransactionData;
import org.qortal.repository.DataException;
import org.qortal.repository.Repository;
import org.qortal.repository.RepositoryManager;
@WebSocket
@SuppressWarnings("serial")
public class ChatMessagesWebSocket extends ApiWebSocket {
@Override
public void configure(WebSocketServletFactory factory) {
factory.register(ChatMessagesWebSocket.class);
}
@OnWebSocketConnect
@Override
public void onWebSocketConnect(Session session) {
Map<String, List<String>> queryParams = session.getUpgradeRequest().getParameterMap();
List<String> txGroupIds = queryParams.get("txGroupId");
if (txGroupIds != null && txGroupIds.size() == 1) {
int txGroupId = Integer.parseInt(txGroupIds.get(0));
try (final Repository repository = RepositoryManager.getRepository()) {
List<ChatMessage> chatMessages = repository.getChatRepository().getMessagesMatchingCriteria(
null,
null,
txGroupId,
null,
null, null, null);
sendMessages(session, chatMessages);
} catch (DataException e) {
// Not a good start
session.close(4001, "Couldn't fetch initial messages from repository");
return;
}
ChatNotifier.Listener listener = chatTransactionData -> onNotify(session, chatTransactionData, txGroupId);
ChatNotifier.getInstance().register(session, listener);
return;
}
List<String> involvingAddresses = queryParams.get("involving");
if (involvingAddresses == null || involvingAddresses.size() != 2) {
session.close(4001, "invalid criteria");
return;
}
try (final Repository repository = RepositoryManager.getRepository()) {
List<ChatMessage> chatMessages = repository.getChatRepository().getMessagesMatchingCriteria(
null,
null,
null,
involvingAddresses,
null, null, null);
sendMessages(session, chatMessages);
} catch (DataException e) {
// Not a good start
session.close(4001, "Couldn't fetch initial messages from repository");
return;
}
ChatNotifier.Listener listener = chatTransactionData -> onNotify(session, chatTransactionData, involvingAddresses);
ChatNotifier.getInstance().register(session, listener);
}
@OnWebSocketClose
@Override
public void onWebSocketClose(Session session, int statusCode, String reason) {
ChatNotifier.getInstance().deregister(session);
}
@OnWebSocketError
public void onWebSocketError(Session session, Throwable throwable) {
/* ignored */
}
@OnWebSocketMessage
public void onWebSocketMessage(Session session, String message) {
/* ignored */
}
private void onNotify(Session session, ChatTransactionData chatTransactionData, int txGroupId) {
if (chatTransactionData == null)
// There has been a group-membership change, but we're not interested
return;
// We only want group-based messages with our txGroupId
if (chatTransactionData.getRecipient() != null || chatTransactionData.getTxGroupId() != txGroupId)
return;
sendChat(session, chatTransactionData);
}
private void onNotify(Session session, ChatTransactionData chatTransactionData, List<String> involvingAddresses) {
// We only want direct/non-group messages where sender/recipient match our addresses
String recipient = chatTransactionData.getRecipient();
if (recipient == null)
return;
List<String> transactionAddresses = Arrays.asList(recipient, chatTransactionData.getSender());
if (!transactionAddresses.containsAll(involvingAddresses))
return;
sendChat(session, chatTransactionData);
}
private void sendMessages(Session session, List<ChatMessage> chatMessages) {
StringWriter stringWriter = new StringWriter();
try {
marshall(stringWriter, chatMessages);
session.getRemote().sendStringByFuture(stringWriter.toString());
} catch (IOException | WebSocketException e) {
// No output this time?
}
}
private void sendChat(Session session, ChatTransactionData chatTransactionData) {
// Convert ChatTransactionData to ChatMessage
ChatMessage chatMessage;
try (final Repository repository = RepositoryManager.getRepository()) {
chatMessage = repository.getChatRepository().toChatMessage(chatTransactionData);
} catch (DataException e) {
// No output this time?
return;
}
sendMessages(session, Collections.singletonList(chatMessage));
}
}

View File

@@ -1,244 +0,0 @@
package org.qortal.api.websocket;
import java.io.IOException;
import java.io.StringWriter;
import java.util.Collections;
import java.util.EnumMap;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.stream.Collectors;
import javax.xml.bind.annotation.XmlAccessType;
import javax.xml.bind.annotation.XmlAccessorType;
import org.eclipse.jetty.websocket.api.Session;
import org.eclipse.jetty.websocket.api.annotations.OnWebSocketClose;
import org.eclipse.jetty.websocket.api.annotations.OnWebSocketConnect;
import org.eclipse.jetty.websocket.api.annotations.OnWebSocketError;
import org.eclipse.jetty.websocket.api.annotations.OnWebSocketMessage;
import org.eclipse.jetty.websocket.api.annotations.WebSocket;
import org.eclipse.jetty.websocket.servlet.WebSocketServletFactory;
import org.qortal.controller.Controller;
import org.qortal.crypto.Crypto;
import org.qortal.data.transaction.PresenceTransactionData;
import org.qortal.data.transaction.TransactionData;
import org.qortal.event.Event;
import org.qortal.event.EventBus;
import org.qortal.event.Listener;
import org.qortal.repository.DataException;
import org.qortal.repository.Repository;
import org.qortal.repository.RepositoryManager;
import org.qortal.transaction.PresenceTransaction.PresenceType;
import org.qortal.transaction.Transaction.TransactionType;
import org.qortal.utils.Base58;
import org.qortal.utils.NTP;
@WebSocket
@SuppressWarnings("serial")
public class PresenceWebSocket extends ApiWebSocket implements Listener {
@XmlAccessorType(XmlAccessType.FIELD)
@SuppressWarnings("unused")
private static class PresenceInfo {
private final PresenceType presenceType;
private final String publicKey;
private final long timestamp;
private final String address;
protected PresenceInfo() {
this.presenceType = null;
this.publicKey = null;
this.timestamp = 0L;
this.address = null;
}
public PresenceInfo(PresenceType presenceType, String pubKey58, long timestamp) {
this.presenceType = presenceType;
this.publicKey = pubKey58;
this.timestamp = timestamp;
this.address = Crypto.toAddress(Base58.decode(this.publicKey));
}
public PresenceType getPresenceType() {
return this.presenceType;
}
public String getPublicKey() {
return this.publicKey;
}
public long getTimestamp() {
return this.timestamp;
}
public String getAddress() {
return this.address;
}
}
/** Outer map key is PresenceType (enum), inner map key is public key in base58, inner map value is timestamp */
private static final Map<PresenceType, Map<String, Long>> currentEntries = Collections.synchronizedMap(new EnumMap<>(PresenceType.class));
/** (Optional) PresenceType used for filtering by that Session. */
private static final Map<Session, PresenceType> sessionPresenceTypes = Collections.synchronizedMap(new HashMap<>());
@Override
public void configure(WebSocketServletFactory factory) {
factory.register(PresenceWebSocket.class);
try (final Repository repository = RepositoryManager.getRepository()) {
populateCurrentInfo(repository);
} catch (DataException e) {
// How to fail properly?
return;
}
EventBus.INSTANCE.addListener(this::listen);
}
@Override
public void listen(Event event) {
// We use NewBlockEvent as a proxy for 1-minute timer
if (!(event instanceof Controller.NewTransactionEvent) && !(event instanceof Controller.NewBlockEvent))
return;
removeOldEntries();
if (event instanceof Controller.NewBlockEvent)
// We only wanted a chance to cull old entries
return;
TransactionData transactionData = ((Controller.NewTransactionEvent) event).getTransactionData();
if (transactionData.getType() != TransactionType.PRESENCE)
return;
PresenceTransactionData presenceData = (PresenceTransactionData) transactionData;
PresenceType presenceType = presenceData.getPresenceType();
// Put/replace for this publickey making sure we keep newest timestamp
String pubKey58 = Base58.encode(presenceData.getCreatorPublicKey());
long ourTimestamp = presenceData.getTimestamp();
long computedTimestamp = mergePresence(presenceType, pubKey58, ourTimestamp);
if (computedTimestamp != ourTimestamp)
// nothing changed
return;
List<PresenceInfo> presenceInfo = Collections.singletonList(new PresenceInfo(presenceType, pubKey58, computedTimestamp));
// Notify sessions
for (Session session : getSessions()) {
PresenceType sessionPresenceType = sessionPresenceTypes.get(session);
if (sessionPresenceType == null || sessionPresenceType == presenceType)
sendPresenceInfo(session, presenceInfo);
}
}
@OnWebSocketConnect
@Override
public void onWebSocketConnect(Session session) {
Map<String, List<String>> queryParams = session.getUpgradeRequest().getParameterMap();
List<String> presenceTypes = queryParams.get("presenceType");
// We only support ONE presenceType
String presenceTypeName = presenceTypes == null || presenceTypes.isEmpty() ? null : presenceTypes.get(0);
PresenceType presenceType = presenceTypeName == null ? null : PresenceType.fromString(presenceTypeName);
// Make sure that if caller does give a presenceType, that it is a valid/known one.
if (presenceTypeName != null && presenceType == null) {
session.close(4003, "unknown presenceType: " + presenceTypeName);
return;
}
// Save session's requested PresenceType, if given
if (presenceType != null)
sessionPresenceTypes.put(session, presenceType);
List<PresenceInfo> presenceInfo;
synchronized (currentEntries) {
presenceInfo = currentEntries.entrySet().stream()
.filter(entry -> presenceType == null ? true : entry.getKey() == presenceType)
.flatMap(entry -> entry.getValue().entrySet().stream().map(innerEntry -> new PresenceInfo(entry.getKey(), innerEntry.getKey(), innerEntry.getValue())))
.collect(Collectors.toList());
}
if (!sendPresenceInfo(session, presenceInfo)) {
session.close(4002, "websocket issue");
return;
}
super.onWebSocketConnect(session);
}
@OnWebSocketClose
@Override
public void onWebSocketClose(Session session, int statusCode, String reason) {
// clean up
sessionPresenceTypes.remove(session);
super.onWebSocketClose(session, statusCode, reason);
}
@OnWebSocketError
public void onWebSocketError(Session session, Throwable throwable) {
/* ignored */
}
@OnWebSocketMessage
public void onWebSocketMessage(Session session, String message) {
/* ignored */
}
private boolean sendPresenceInfo(Session session, List<PresenceInfo> presenceInfo) {
try {
StringWriter stringWriter = new StringWriter();
marshall(stringWriter, presenceInfo);
String output = stringWriter.toString();
session.getRemote().sendStringByFuture(output);
} catch (IOException e) {
// No output this time?
return false;
}
return true;
}
private static void populateCurrentInfo(Repository repository) throws DataException {
// We want ALL PRESENCE transactions
List<TransactionData> presenceTransactionsData = repository.getTransactionRepository().getUnconfirmedTransactions(TransactionType.PRESENCE, null);
for (TransactionData transactionData : presenceTransactionsData) {
PresenceTransactionData presenceData = (PresenceTransactionData) transactionData;
PresenceType presenceType = presenceData.getPresenceType();
// Put/replace for this publickey making sure we keep newest timestamp
String pubKey58 = Base58.encode(presenceData.getCreatorPublicKey());
long ourTimestamp = presenceData.getTimestamp();
mergePresence(presenceType, pubKey58, ourTimestamp);
}
}
private static long mergePresence(PresenceType presenceType, String pubKey58, long ourTimestamp) {
Map<String, Long> typedPubkeyTimestamps = currentEntries.computeIfAbsent(presenceType, someType -> Collections.synchronizedMap(new HashMap<>()));
return typedPubkeyTimestamps.compute(pubKey58, (somePubKey58, currentTimestamp) -> (currentTimestamp == null || currentTimestamp < ourTimestamp) ? ourTimestamp : currentTimestamp);
}
private static void removeOldEntries() {
long now = NTP.getTime();
currentEntries.entrySet().forEach(entry -> {
long expiryThreshold = now - entry.getKey().getLifetime();
entry.getValue().entrySet().removeIf(pubkeyTimestamp -> pubkeyTimestamp.getValue() < expiryThreshold);
});
}
}

View File

@@ -1,157 +0,0 @@
package org.qortal.api.websocket;
import java.io.IOException;
import java.io.StringWriter;
import java.util.Collections;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.stream.Collectors;
import org.eclipse.jetty.websocket.api.Session;
import org.eclipse.jetty.websocket.api.annotations.OnWebSocketClose;
import org.eclipse.jetty.websocket.api.annotations.OnWebSocketConnect;
import org.eclipse.jetty.websocket.api.annotations.OnWebSocketError;
import org.eclipse.jetty.websocket.api.annotations.OnWebSocketMessage;
import org.eclipse.jetty.websocket.api.annotations.WebSocket;
import org.eclipse.jetty.websocket.servlet.WebSocketServletFactory;
import org.qortal.controller.tradebot.TradeBot;
import org.qortal.crosschain.SupportedBlockchain;
import org.qortal.data.crosschain.TradeBotData;
import org.qortal.event.Event;
import org.qortal.event.EventBus;
import org.qortal.event.Listener;
import org.qortal.repository.DataException;
import org.qortal.repository.Repository;
import org.qortal.repository.RepositoryManager;
import org.qortal.utils.Base58;
@WebSocket
@SuppressWarnings("serial")
public class TradeBotWebSocket extends ApiWebSocket implements Listener {
/** Cache of trade-bot entry states, keyed by trade-bot entry's "trade private key" (base58) */
private static final Map<String, Integer> PREVIOUS_STATES = new HashMap<>();
private static final Map<Session, String> sessionBlockchain = Collections.synchronizedMap(new HashMap<>());
@Override
public void configure(WebSocketServletFactory factory) {
factory.register(TradeBotWebSocket.class);
try (final Repository repository = RepositoryManager.getRepository()) {
List<TradeBotData> tradeBotEntries = repository.getCrossChainRepository().getAllTradeBotData();
if (tradeBotEntries == null)
// How do we properly fail here?
return;
PREVIOUS_STATES.putAll(tradeBotEntries.stream().collect(Collectors.toMap(entry -> Base58.encode(entry.getTradePrivateKey()), TradeBotData::getStateValue)));
} catch (DataException e) {
// No output this time
}
EventBus.INSTANCE.addListener(this::listen);
}
@Override
public void listen(Event event) {
if (!(event instanceof TradeBot.StateChangeEvent))
return;
TradeBotData tradeBotData = ((TradeBot.StateChangeEvent) event).getTradeBotData();
String tradePrivateKey58 = Base58.encode(tradeBotData.getTradePrivateKey());
synchronized (PREVIOUS_STATES) {
Integer previousStateValue = PREVIOUS_STATES.get(tradePrivateKey58);
if (previousStateValue != null && previousStateValue == tradeBotData.getStateValue())
// Not changed
return;
PREVIOUS_STATES.put(tradePrivateKey58, tradeBotData.getStateValue());
}
List<TradeBotData> tradeBotEntries = Collections.singletonList(tradeBotData);
for (Session session : getSessions()) {
// Only send if this session has this/no preferred blockchain
String preferredBlockchain = sessionBlockchain.get(session);
if (preferredBlockchain == null || preferredBlockchain.equals(tradeBotData.getForeignBlockchain()))
sendEntries(session, tradeBotEntries);
}
}
@OnWebSocketConnect
@Override
public void onWebSocketConnect(Session session) {
Map<String, List<String>> queryParams = session.getUpgradeRequest().getParameterMap();
List<String> foreignBlockchains = queryParams.get("foreignBlockchain");
final String foreignBlockchain = foreignBlockchains == null ? null : foreignBlockchains.get(0);
// Make sure blockchain (if any) is valid
if (foreignBlockchain != null && SupportedBlockchain.fromString(foreignBlockchain) == null) {
session.close(4003, "unknown blockchain: " + foreignBlockchain);
return;
}
// save session's preferred blockchain (if any)
sessionBlockchain.put(session, foreignBlockchain);
// Send all known trade-bot entries
try (final Repository repository = RepositoryManager.getRepository()) {
List<TradeBotData> tradeBotEntries = repository.getCrossChainRepository().getAllTradeBotData();
// Optional filtering
if (foreignBlockchain != null)
tradeBotEntries = tradeBotEntries.stream()
.filter(tradeBotData -> tradeBotData.getForeignBlockchain().equals(foreignBlockchain))
.collect(Collectors.toList());
if (!sendEntries(session, tradeBotEntries)) {
session.close(4002, "websocket issue");
return;
}
} catch (DataException e) {
session.close(4001, "repository issue fetching trade-bot entries");
return;
}
super.onWebSocketConnect(session);
}
@OnWebSocketClose
@Override
public void onWebSocketClose(Session session, int statusCode, String reason) {
// clean up
sessionBlockchain.remove(session);
super.onWebSocketClose(session, statusCode, reason);
}
@OnWebSocketError
public void onWebSocketError(Session session, Throwable throwable) {
/* ignored */
}
@OnWebSocketMessage
public void onWebSocketMessage(Session session, String message) {
/* ignored */
}
private boolean sendEntries(Session session, List<TradeBotData> tradeBotEntries) {
try {
StringWriter stringWriter = new StringWriter();
marshall(stringWriter, tradeBotEntries);
String output = stringWriter.toString();
session.getRemote().sendStringByFuture(output);
} catch (IOException e) {
// No output this time?
return false;
}
return true;
}
}

View File

@@ -1,351 +0,0 @@
package org.qortal.api.websocket;
import java.io.IOException;
import java.io.StringWriter;
import java.util.ArrayList;
import java.util.Collection;
import java.util.Collections;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.function.Predicate;
import java.util.function.Supplier;
import java.util.stream.Collectors;
import org.apache.logging.log4j.LogManager;
import org.apache.logging.log4j.Logger;
import org.eclipse.jetty.websocket.api.Session;
import org.eclipse.jetty.websocket.api.annotations.OnWebSocketClose;
import org.eclipse.jetty.websocket.api.annotations.OnWebSocketConnect;
import org.eclipse.jetty.websocket.api.annotations.OnWebSocketError;
import org.eclipse.jetty.websocket.api.annotations.OnWebSocketMessage;
import org.eclipse.jetty.websocket.api.annotations.WebSocket;
import org.eclipse.jetty.websocket.servlet.WebSocketServletFactory;
import org.qortal.api.model.CrossChainOfferSummary;
import org.qortal.controller.Controller;
import org.qortal.crosschain.SupportedBlockchain;
import org.qortal.crosschain.ACCT;
import org.qortal.crosschain.AcctMode;
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;
import org.qortal.utils.ByteArray;
import org.qortal.utils.NTP;
@WebSocket
@SuppressWarnings("serial")
public class TradeOffersWebSocket extends ApiWebSocket implements Listener {
private static final Logger LOGGER = LogManager.getLogger(TradeOffersWebSocket.class);
private static class CachedOfferInfo {
public final Map<String, AcctMode> previousAtModes = new HashMap<>();
// OFFERING
public final Map<String, CrossChainOfferSummary> currentSummaries = new HashMap<>();
// REDEEMED/REFUNDED/CANCELLED
public final Map<String, CrossChainOfferSummary> historicSummaries = new HashMap<>();
}
// Manual synchronization
private static final Map<String, CachedOfferInfo> cachedInfoByBlockchain = new HashMap<>();
private static final Predicate<CrossChainOfferSummary> isHistoric = offerSummary
-> offerSummary.getMode() == AcctMode.REDEEMED
|| offerSummary.getMode() == AcctMode.REFUNDED
|| offerSummary.getMode() == AcctMode.CANCELLED;
private static final Map<Session, String> sessionBlockchain = Collections.synchronizedMap(new HashMap<>());
@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);
}
@Override
public void listen(Event event) {
if (!(event instanceof Controller.NewBlockEvent))
return;
BlockData blockData = ((Controller.NewBlockEvent) event).getBlockData();
// Process any new info
try (final Repository repository = RepositoryManager.getRepository()) {
// Find any new/changed trade ATs since this block
final Boolean isFinished = null;
final Integer dataByteOffset = null;
final Long expectedValue = null;
final Integer minimumFinalHeight = blockData.getHeight();
for (SupportedBlockchain blockchain : SupportedBlockchain.values()) {
Map<ByteArray, Supplier<ACCT>> acctsByCodeHash = SupportedBlockchain.getFilteredAcctMap(blockchain);
List<CrossChainOfferSummary> crossChainOfferSummaries = new ArrayList<>();
synchronized (cachedInfoByBlockchain) {
CachedOfferInfo cachedInfo = cachedInfoByBlockchain.computeIfAbsent(blockchain.name(), k -> new CachedOfferInfo());
for (Map.Entry<ByteArray, Supplier<ACCT>> acctInfo : acctsByCodeHash.entrySet()) {
byte[] codeHash = acctInfo.getKey().value;
ACCT acct = acctInfo.getValue().get();
List<ATStateData> atStates = repository.getATRepository().getMatchingFinalATStates(codeHash,
isFinished, dataByteOffset, expectedValue, minimumFinalHeight,
null, null, null);
crossChainOfferSummaries.addAll(produceSummaries(repository, acct, atStates, blockData.getTimestamp()));
}
// Remove any entries unchanged from last time
crossChainOfferSummaries.removeIf(offerSummary -> cachedInfo.previousAtModes.get(offerSummary.getQortalAtAddress()) == offerSummary.getMode());
// Skip to next blockchain if nothing has changed (for this blockchain)
if (crossChainOfferSummaries.isEmpty())
continue;
// Update
for (CrossChainOfferSummary offerSummary : crossChainOfferSummaries) {
String offerAtAddress = offerSummary.getQortalAtAddress();
cachedInfo.previousAtModes.put(offerAtAddress, offerSummary.getMode());
LOGGER.trace(() -> String.format("Block height: %d, AT: %s, mode: %s", blockData.getHeight(), offerAtAddress, offerSummary.getMode().name()));
switch (offerSummary.getMode()) {
case OFFERING:
cachedInfo.currentSummaries.put(offerAtAddress, offerSummary);
cachedInfo.historicSummaries.remove(offerAtAddress);
break;
case REDEEMED:
case REFUNDED:
case CANCELLED:
cachedInfo.currentSummaries.remove(offerAtAddress);
cachedInfo.historicSummaries.put(offerAtAddress, offerSummary);
break;
case TRADING:
cachedInfo.currentSummaries.remove(offerAtAddress);
cachedInfo.historicSummaries.remove(offerAtAddress);
break;
}
}
// Remove any historic offers that are over 24 hours old
final long tooOldTimestamp = NTP.getTime() - 24 * 60 * 60 * 1000L;
cachedInfo.historicSummaries.values().removeIf(historicSummary -> historicSummary.getTimestamp() < tooOldTimestamp);
}
// Notify sessions
for (Session session : getSessions()) {
// Only send if this session has this/no preferred blockchain
String preferredBlockchain = sessionBlockchain.get(session);
if (preferredBlockchain == null || preferredBlockchain.equals(blockchain.name()))
sendOfferSummaries(session, crossChainOfferSummaries);
}
}
} catch (DataException e) {
// No output this time
}
}
@OnWebSocketConnect
@Override
public void onWebSocketConnect(Session session) {
Map<String, List<String>> queryParams = session.getUpgradeRequest().getParameterMap();
final boolean includeHistoric = queryParams.get("includeHistoric") != null;
List<String> foreignBlockchains = queryParams.get("foreignBlockchain");
final String foreignBlockchain = foreignBlockchains == null ? null : foreignBlockchains.get(0);
// Make sure blockchain (if any) is valid
if (foreignBlockchain != null && SupportedBlockchain.fromString(foreignBlockchain) == null) {
session.close(4003, "unknown blockchain: " + foreignBlockchain);
return;
}
// Save session's preferred blockchain, if given
if (foreignBlockchain != null)
sessionBlockchain.put(session, foreignBlockchain);
List<CrossChainOfferSummary> crossChainOfferSummaries = new ArrayList<>();
synchronized (cachedInfoByBlockchain) {
Collection<CachedOfferInfo> cachedInfos;
if (foreignBlockchain == null)
// No preferred blockchain, so iterate through all of them
cachedInfos = cachedInfoByBlockchain.values();
else
cachedInfos = Collections.singleton(cachedInfoByBlockchain.computeIfAbsent(foreignBlockchain, k -> new CachedOfferInfo()));
for (CachedOfferInfo cachedInfo : cachedInfos) {
crossChainOfferSummaries.addAll(cachedInfo.currentSummaries.values());
if (includeHistoric)
crossChainOfferSummaries.addAll(cachedInfo.historicSummaries.values());
}
}
if (!sendOfferSummaries(session, crossChainOfferSummaries)) {
session.close(4002, "websocket issue");
return;
}
super.onWebSocketConnect(session);
}
@OnWebSocketClose
@Override
public void onWebSocketClose(Session session, int statusCode, String reason) {
// clean up
sessionBlockchain.remove(session);
super.onWebSocketClose(session, statusCode, reason);
}
@OnWebSocketError
public void onWebSocketError(Session session, Throwable throwable) {
/* ignored */
}
@OnWebSocketMessage
public void onWebSocketMessage(Session session, String message) {
/* ignored */
}
private boolean sendOfferSummaries(Session session, List<CrossChainOfferSummary> crossChainOfferSummaries) {
try {
StringWriter stringWriter = new StringWriter();
marshall(stringWriter, crossChainOfferSummaries);
String output = stringWriter.toString();
session.getRemote().sendStringByFuture(output);
} catch (IOException e) {
// No output this time?
return false;
}
return true;
}
private static void populateCurrentSummaries(Repository repository) throws DataException {
// We want ALL OFFERING trades
Boolean isFinished = Boolean.FALSE;
Long expectedValue = (long) AcctMode.OFFERING.value;
Integer minimumFinalHeight = null;
for (SupportedBlockchain blockchain : SupportedBlockchain.values()) {
Map<ByteArray, Supplier<ACCT>> acctsByCodeHash = SupportedBlockchain.getFilteredAcctMap(blockchain);
CachedOfferInfo cachedInfo = cachedInfoByBlockchain.computeIfAbsent(blockchain.name(), k -> new CachedOfferInfo());
for (Map.Entry<ByteArray, Supplier<ACCT>> acctInfo : acctsByCodeHash.entrySet()) {
byte[] codeHash = acctInfo.getKey().value;
ACCT acct = acctInfo.getValue().get();
Integer dataByteOffset = acct.getModeByteOffset();
List<ATStateData> initialAtStates = repository.getATRepository().getMatchingFinalATStates(codeHash,
isFinished, dataByteOffset, expectedValue, minimumFinalHeight,
null, null, null);
if (initialAtStates == null)
throw new DataException("Couldn't fetch current trades from repository");
// Save initial AT modes
cachedInfo.previousAtModes.putAll(initialAtStates.stream().collect(Collectors.toMap(ATStateData::getATAddress, atState -> AcctMode.OFFERING)));
// Convert to offer summaries
cachedInfo.currentSummaries.putAll(produceSummaries(repository, acct, initialAtStates, null).stream()
.collect(Collectors.toMap(CrossChainOfferSummary::getQortalAtAddress, offerSummary -> offerSummary)));
}
}
}
private static void populateHistoricSummaries(Repository repository) throws DataException {
// We want REDEEMED/REFUNDED/CANCELLED trades over the last 24 hours
long timestamp = System.currentTimeMillis() - 24 * 60 * 60 * 1000L;
int minimumFinalHeight = repository.getBlockRepository().getHeightFromTimestamp(timestamp);
if (minimumFinalHeight == 0)
throw new DataException("Couldn't fetch block timestamp from repository");
Boolean isFinished = Boolean.TRUE;
Integer dataByteOffset = null;
Long expectedValue = null;
++minimumFinalHeight; // because height is just *before* timestamp
for (SupportedBlockchain blockchain : SupportedBlockchain.values()) {
Map<ByteArray, Supplier<ACCT>> acctsByCodeHash = SupportedBlockchain.getFilteredAcctMap(blockchain);
CachedOfferInfo cachedInfo = cachedInfoByBlockchain.computeIfAbsent(blockchain.name(), k -> new CachedOfferInfo());
for (Map.Entry<ByteArray, Supplier<ACCT>> acctInfo : acctsByCodeHash.entrySet()) {
byte[] codeHash = acctInfo.getKey().value;
ACCT acct = acctInfo.getValue().get();
List<ATStateData> historicAtStates = repository.getATRepository().getMatchingFinalATStates(codeHash,
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, acct, historicAtState, null);
if (!isHistoric.test(historicOfferSummary))
continue;
// Add summary to initial burst
cachedInfo.historicSummaries.put(historicOfferSummary.getQortalAtAddress(), historicOfferSummary);
// Save initial AT mode
cachedInfo.previousAtModes.put(historicOfferSummary.getQortalAtAddress(), historicOfferSummary.getMode());
}
}
}
}
private static CrossChainOfferSummary produceSummary(Repository repository, ACCT acct, ATStateData atState, Long timestamp) throws DataException {
CrossChainTradeData crossChainTradeData = acct.populateTradeData(repository, atState);
long atStateTimestamp;
if (crossChainTradeData.mode == AcctMode.OFFERING)
// We want when trade was created, not when it was last updated
atStateTimestamp = crossChainTradeData.creationTimestamp;
else
atStateTimestamp = timestamp != null ? timestamp : repository.getBlockRepository().getTimestampFromHeight(atState.getHeight());
return new CrossChainOfferSummary(crossChainTradeData, atStateTimestamp);
}
private static List<CrossChainOfferSummary> produceSummaries(Repository repository, ACCT acct, List<ATStateData> atStates, Long timestamp) throws DataException {
List<CrossChainOfferSummary> offerSummaries = new ArrayList<>();
for (ATStateData atState : atStates)
offerSummaries.add(produceSummary(repository, acct, atState, timestamp));
return offerSummaries;
}
}

View File

@@ -1,13 +1,11 @@
package org.qortal.asset;
import org.qortal.crypto.Crypto;
import org.qortal.data.asset.AssetData;
import org.qortal.data.transaction.IssueAssetTransactionData;
import org.qortal.data.transaction.TransactionData;
import org.qortal.data.transaction.UpdateAssetTransactionData;
import org.qortal.repository.DataException;
import org.qortal.repository.Repository;
import org.qortal.utils.Amounts;
public class Asset {
@@ -23,12 +21,12 @@ public class Asset {
// Other useful constants
public static final int MIN_NAME_SIZE = 3;
public static final int MAX_NAME_SIZE = 40;
public static final int MAX_NAME_SIZE = 400;
public static final int MAX_DESCRIPTION_SIZE = 4000;
public static final int MAX_DATA_SIZE = 400000;
public static final long MAX_QUANTITY = 10_000_000_000L * Amounts.MULTIPLIER; // but also to 8 decimal places
public static final long MAX_DIVISIBLE_QUANTITY = 10_000_000_000L; // but also to 8 decimal places
public static final long MAX_INDIVISIBLE_QUANTITY = 1_000_000_000_000_000_000L;
// Properties
private Repository repository;
@@ -44,14 +42,12 @@ public class Asset {
public Asset(Repository repository, IssueAssetTransactionData issueAssetTransactionData) {
this.repository = repository;
String ownerAddress = Crypto.toAddress(issueAssetTransactionData.getCreatorPublicKey());
// NOTE: transaction's reference is used to look up newly assigned assetID on creation!
this.assetData = new AssetData(ownerAddress, issueAssetTransactionData.getAssetName(),
this.assetData = new AssetData(issueAssetTransactionData.getOwner(), issueAssetTransactionData.getAssetName(),
issueAssetTransactionData.getDescription(), issueAssetTransactionData.getQuantity(),
issueAssetTransactionData.isDivisible(), issueAssetTransactionData.getData(),
issueAssetTransactionData.isUnspendable(), issueAssetTransactionData.getTxGroupId(),
issueAssetTransactionData.getSignature(), issueAssetTransactionData.getReducedAssetName());
issueAssetTransactionData.getIsDivisible(), issueAssetTransactionData.getData(),
issueAssetTransactionData.getIsUnspendable(),
issueAssetTransactionData.getTxGroupId(), issueAssetTransactionData.getSignature());
}
public Asset(Repository repository, long assetId) throws DataException {
@@ -122,11 +118,10 @@ public class Asset {
throw new IllegalStateException("Missing referenced transaction when orphaning UPDATE_ASSET");
switch (previousTransactionData.getType()) {
case ISSUE_ASSET: {
case ISSUE_ASSET:
IssueAssetTransactionData previousIssueAssetTransactionData = (IssueAssetTransactionData) previousTransactionData;
String ownerAddress = Crypto.toAddress(previousIssueAssetTransactionData.getCreatorPublicKey());
this.assetData.setOwner(ownerAddress);
this.assetData.setOwner(previousIssueAssetTransactionData.getOwner());
if (needDescription) {
this.assetData.setDescription(previousIssueAssetTransactionData.getDescription());
@@ -138,9 +133,8 @@ public class Asset {
needData = false;
}
break;
}
case UPDATE_ASSET: {
case UPDATE_ASSET:
UpdateAssetTransactionData previousUpdateAssetTransactionData = (UpdateAssetTransactionData) previousTransactionData;
this.assetData.setOwner(previousUpdateAssetTransactionData.getNewOwner());
@@ -158,9 +152,7 @@ public class Asset {
// Get signature for previous transaction in chain, just in case we need it
if (needDescription || needData)
previousTransactionSignature = previousUpdateAssetTransactionData.getOrphanReference();
break;
}
default:
throw new IllegalStateException("Invalid referenced transaction when orphaning UPDATE_ASSET");

View File

@@ -1,8 +1,8 @@
package org.qortal.asset;
import static org.qortal.utils.Amounts.prettyAmount;
import java.math.BigDecimal;
import java.math.BigInteger;
import java.math.RoundingMode;
import java.util.Arrays;
import java.util.List;
@@ -11,12 +11,13 @@ import org.apache.logging.log4j.LogManager;
import org.apache.logging.log4j.Logger;
import org.qortal.account.Account;
import org.qortal.account.PublicKeyAccount;
import org.qortal.block.BlockChain;
import org.qortal.data.asset.AssetData;
import org.qortal.data.asset.OrderData;
import org.qortal.data.asset.TradeData;
import org.qortal.repository.AssetRepository;
import org.qortal.repository.DataException;
import org.qortal.repository.Repository;
import org.qortal.utils.Amounts;
import org.qortal.utils.Base58;
public class Order {
@@ -28,11 +29,9 @@ public class Order {
private OrderData orderData;
// Used quite a bit
private final boolean isOurOrderNewPricing;
private final long haveAssetId;
private final long wantAssetId;
private final boolean isAmountInWantAsset;
private final BigInteger orderAmount;
private final BigInteger orderPrice;
/** Cache of price-pair units e.g. QORT/GOLD, but use getPricePair() instead! */
private String cachedPricePair;
@@ -48,12 +47,9 @@ public class Order {
this.repository = repository;
this.orderData = orderData;
this.isOurOrderNewPricing = this.orderData.getTimestamp() >= BlockChain.getInstance().getNewAssetPricingTimestamp();
this.haveAssetId = this.orderData.getHaveAssetId();
this.wantAssetId = this.orderData.getWantAssetId();
this.isAmountInWantAsset = haveAssetId < wantAssetId;
this.orderAmount = BigInteger.valueOf(this.orderData.getAmount());
this.orderPrice = BigInteger.valueOf(this.orderData.getPrice());
}
// Getters/Setters
@@ -64,16 +60,16 @@ public class Order {
// More information
public static long getAmountLeft(OrderData orderData) {
return orderData.getAmount() - orderData.getFulfilled();
public static BigDecimal getAmountLeft(OrderData orderData) {
return orderData.getAmount().subtract(orderData.getFulfilled());
}
public long getAmountLeft() {
public BigDecimal getAmountLeft() {
return Order.getAmountLeft(this.orderData);
}
public static boolean isFulfilled(OrderData orderData) {
return orderData.getFulfilled() == orderData.getAmount();
return orderData.getFulfilled().compareTo(orderData.getAmount()) == 0;
}
public boolean isFulfilled() {
@@ -90,10 +86,13 @@ public class Order {
* <p>
* @return granularity of matched-amount
*/
public static long calculateAmountGranularity(boolean isAmountAssetDivisible, boolean isReturnAssetDivisible, long price) {
public static BigDecimal calculateAmountGranularity(boolean isAmountAssetDivisible, boolean isReturnAssetDivisible, BigDecimal price) {
// Multiplier to scale BigDecimal fractional amounts into integer domain
BigInteger multiplier = BigInteger.valueOf(1_0000_0000L);
// Calculate the minimum increment for matched-amount using greatest-common-divisor
BigInteger returnAmount = Amounts.MULTIPLIER_BI; // 1 unit * multiplier
BigInteger matchedAmount = BigInteger.valueOf(price);
BigInteger returnAmount = multiplier; // 1 unit (* multiplier)
BigInteger matchedAmount = price.movePointRight(8).toBigInteger();
BigInteger gcd = returnAmount.gcd(matchedAmount);
returnAmount = returnAmount.divide(gcd);
@@ -101,20 +100,20 @@ public class Order {
// Calculate GCD in combination with divisibility
if (isAmountAssetDivisible)
returnAmount = returnAmount.multiply(Amounts.MULTIPLIER_BI);
returnAmount = returnAmount.multiply(multiplier);
if (isReturnAssetDivisible)
matchedAmount = matchedAmount.multiply(Amounts.MULTIPLIER_BI);
matchedAmount = matchedAmount.multiply(multiplier);
gcd = returnAmount.gcd(matchedAmount);
// Calculate the granularity at which we have to buy
BigInteger granularity = returnAmount.multiply(Amounts.MULTIPLIER_BI).divide(gcd);
BigDecimal granularity = new BigDecimal(returnAmount.divide(gcd));
if (isAmountAssetDivisible)
granularity = granularity.divide(Amounts.MULTIPLIER_BI);
granularity = granularity.movePointLeft(8);
// Return
return granularity.longValue();
return granularity;
}
/**
@@ -131,7 +130,7 @@ public class Order {
/** Calculate price pair. (e.g. QORT/GOLD)
* <p>
* Lowest-assetID asset is first,
* Under 'new' pricing scheme, lowest-assetID asset is first,
* so if QORT has assetID 0 and GOLD has assetID 10, then
* the pricing pair is QORT/GOLD.
* <p>
@@ -142,32 +141,32 @@ public class Order {
AssetData haveAssetData = getHaveAsset();
AssetData wantAssetData = getWantAsset();
if (haveAssetId > wantAssetId)
if (isOurOrderNewPricing && haveAssetId > wantAssetId)
cachedPricePair = wantAssetData.getName() + "/" + haveAssetData.getName();
else
cachedPricePair = haveAssetData.getName() + "/" + wantAssetData.getName();
}
/** Returns amount of have-asset to remove from order's creator's balance on placing this order. */
private long calcHaveAssetCommittment() {
// Simple case: amount is in have asset
if (!this.isAmountInWantAsset)
return this.orderData.getAmount();
private BigDecimal calcHaveAssetCommittment() {
BigDecimal committedCost = this.orderData.getAmount();
return Amounts.roundUpScaledMultiply(this.orderAmount, this.orderPrice);
}
// If 'new' pricing and "amount" is in want-asset then we need to convert
if (isOurOrderNewPricing && haveAssetId < wantAssetId)
committedCost = committedCost.multiply(this.orderData.getPrice()).setScale(8, RoundingMode.HALF_UP);
private long calcHaveAssetRefund(long amount) {
// Simple case: amount is in have asset
if (!this.isAmountInWantAsset)
return amount;
return Amounts.roundUpScaledMultiply(BigInteger.valueOf(amount), this.orderPrice);
return committedCost;
}
/** Returns amount of remaining have-asset to refund to order's creator's balance on cancelling this order. */
private long calcHaveAssetRefund() {
return calcHaveAssetRefund(getAmountLeft());
private BigDecimal calcHaveAssetRefund() {
BigDecimal refund = getAmountLeft();
// If 'new' pricing and "amount" is in want-asset then we need to convert
if (isOurOrderNewPricing && haveAssetId < wantAssetId)
refund = refund.multiply(this.orderData.getPrice()).setScale(8, RoundingMode.HALF_UP);
return refund;
}
// Navigation
@@ -193,19 +192,27 @@ public class Order {
/**
* Returns AssetData for asset in effect for "amount" field.
* <p>
* This is the asset with highest assetID.
* For 'old' pricing, this is the have-asset.<br>
* For 'new' pricing, this is the asset with highest assetID.
*/
public AssetData getAmountAsset() throws DataException {
return (wantAssetId > haveAssetId) ? getWantAsset() : getHaveAsset();
if (isOurOrderNewPricing && wantAssetId > haveAssetId)
return getWantAsset();
else
return getHaveAsset();
}
/**
* Returns AssetData for other (return) asset traded.
* <p>
* This is the asset with lowest assetID.
* For 'old' pricing, this is the want-asset.<br>
* For 'new' pricing, this is the asset with lowest assetID.
*/
public AssetData getReturnAsset() throws DataException {
return (haveAssetId < wantAssetId) ? getHaveAsset() : getWantAsset();
if (isOurOrderNewPricing && haveAssetId < wantAssetId)
return getHaveAsset();
else
return getWantAsset();
}
// Processing
@@ -220,6 +227,8 @@ public class Order {
// NOTE: the following values are specific to passed orderData, not the same as class instance values!
final boolean isOrderNewAssetPricing = orderData.getTimestamp() >= BlockChain.getInstance().getNewAssetPricingTimestamp();
// Cached for readability
final long _haveAssetId = orderData.getHaveAssetId();
final long _wantAssetId = orderData.getWantAssetId();
@@ -227,36 +236,43 @@ public class Order {
final AssetData haveAssetData = this.repository.getAssetRepository().fromAssetId(_haveAssetId);
final AssetData wantAssetData = this.repository.getAssetRepository().fromAssetId(_wantAssetId);
final long amountAssetId = (_wantAssetId > _haveAssetId) ? _wantAssetId : _haveAssetId;
final long returnAssetId = (_haveAssetId < _wantAssetId) ? _haveAssetId : _wantAssetId;
final long amountAssetId = (isOurOrderNewPricing && _wantAssetId > _haveAssetId) ? _wantAssetId : _haveAssetId;
final long returnAssetId = (isOurOrderNewPricing && _haveAssetId < _wantAssetId) ? _haveAssetId : _wantAssetId;
final AssetData amountAssetData = this.repository.getAssetRepository().fromAssetId(amountAssetId);
final AssetData returnAssetData = this.repository.getAssetRepository().fromAssetId(returnAssetId);
LOGGER.debug(() -> String.format("%s %s", orderPrefix, Base58.encode(orderData.getOrderId())));
LOGGER.debug(String.format("%s %s", orderPrefix, Base58.encode(orderData.getOrderId())));
LOGGER.trace(() -> String.format("%s have %s, want %s.", weThey, haveAssetData.getName(), wantAssetData.getName()));
LOGGER.trace(String.format("%s have %s, want %s. '%s' pricing scheme.", weThey, haveAssetData.getName(), wantAssetData.getName(), isOrderNewAssetPricing ? "new" : "old"));
LOGGER.trace(() -> String.format("%s amount: %s (ordered) - %s (fulfilled) = %s %s left", ourTheir,
prettyAmount(orderData.getAmount()),
prettyAmount(orderData.getFulfilled()),
prettyAmount(Order.getAmountLeft(orderData)),
LOGGER.trace(String.format("%s amount: %s (ordered) - %s (fulfilled) = %s %s left", ourTheir,
orderData.getAmount().stripTrailingZeros().toPlainString(),
orderData.getFulfilled().stripTrailingZeros().toPlainString(),
Order.getAmountLeft(orderData).stripTrailingZeros().toPlainString(),
amountAssetData.getName()));
long maxReturnAmount = Amounts.roundUpScaledMultiply(Order.getAmountLeft(orderData), orderData.getPrice());
String pricePair = getPricePair();
BigDecimal maxReturnAmount = Order.getAmountLeft(orderData).multiply(orderData.getPrice()).setScale(8, RoundingMode.HALF_UP);
LOGGER.trace(() -> String.format("%s price: %s %s (%s %s tradable)", ourTheir,
prettyAmount(orderData.getPrice()),
pricePair,
prettyAmount(maxReturnAmount),
returnAssetData.getName()));
LOGGER.trace(String.format("%s price: %s %s (%s %s tradable)", ourTheir,
orderData.getPrice().toPlainString(), getPricePair(),
maxReturnAmount.stripTrailingZeros().toPlainString(), returnAssetData.getName()));
}
public void process() throws DataException {
AssetRepository assetRepository = this.repository.getAssetRepository();
AssetData haveAssetData = getHaveAsset();
AssetData wantAssetData = getWantAsset();
/** The asset while working out amount that matches. */
AssetData matchingAssetData = isOurOrderNewPricing ? getAmountAsset() : wantAssetData;
/** The return asset traded if trade completes. */
AssetData returnAssetData = isOurOrderNewPricing ? getReturnAsset() : haveAssetData;
// Subtract have-asset from creator
Account creator = new PublicKeyAccount(this.repository, this.orderData.getCreatorPublicKey());
creator.modifyAssetBalance(haveAssetId, - this.calcHaveAssetCommittment());
creator.setConfirmedBalance(haveAssetId, creator.getConfirmedBalance(haveAssetId).subtract(this.calcHaveAssetCommittment()));
// Save this order into repository so it's available for matching, possibly by itself
this.repository.getAssetRepository().save(this.orderData);
@@ -265,28 +281,36 @@ public class Order {
// Fetch corresponding open orders that might potentially match, hence reversed want/have assetIDs.
// Returned orders are sorted with lowest "price" first.
List<OrderData> orders = this.repository.getAssetRepository().getOpenOrdersForTrading(wantAssetId, haveAssetId, this.orderData.getPrice());
LOGGER.trace(() -> String.format("Open orders fetched from repository: %d", orders.size()));
List<OrderData> orders = assetRepository.getOpenOrdersForTrading(wantAssetId, haveAssetId, isOurOrderNewPricing ? this.orderData.getPrice() : null);
LOGGER.trace("Open orders fetched from repository: " + orders.size());
if (orders.isEmpty())
return;
matchOrders(orders);
}
private void matchOrders(List<OrderData> orders) throws DataException {
AssetData haveAssetData = getHaveAsset();
AssetData wantAssetData = getWantAsset();
/** The asset while working out amount that matches. */
AssetData matchingAssetData = getAmountAsset();
/** The return asset traded if trade completes. */
AssetData returnAssetData = getReturnAsset();
// Attempt to match orders
/*
* Potential matching order example:
* Potential matching order example ("old"):
*
* Our order:
* haveAssetId=[GOLD], wantAssetId=0 (QORT), amount=40 (GOLD), price=486 (QORT/GOLD)
* This translates to "we have 40 GOLD and want QORT at a price of 486 QORT per GOLD"
* If our order matched, we'd end up with 40 * 486 = 19,440 QORT.
*
* Their order:
* haveAssetId=0 (QORT), wantAssetId=[GOLD], amount=20,000 (QORT), price=0.00205761 (GOLD/QORT)
* This translates to "they have 20,000 QORT and want GOLD at a price of 0.00205761 GOLD per QORT"
*
* Their price, converted into 'our' units of QORT/GOLD, is: 1 / 0.00205761 = 486.00074844 QORT/GOLD.
* This is better than our requested 486 QORT/GOLD so this order matches.
*
* Using their price, we end up with 40 * 486.00074844 = 19440.02993760 QORT. They end up with 40 GOLD.
*
* If their order had 19,440 QORT left, only 19,440 * 0.00205761 = 39.99993840 GOLD would be traded.
*/
/*
* Potential matching order example ("new"):
*
* Our order:
* haveAssetId=[GOLD], wantAssetId=0 (QORT), amount=40 (GOLD), price=486 (QORT/GOLD)
@@ -304,107 +328,129 @@ public class Order {
* If their order only had 36 GOLD left, only 36 * 486.00074844 = 17496.02694384 QORT would be traded.
*/
long ourPrice = this.orderData.getPrice();
String pricePair = getPricePair();
BigDecimal ourPrice = this.orderData.getPrice();
for (OrderData theirOrderData : orders) {
logOrder("Considering order", false, theirOrderData);
// Not used:
// boolean isTheirOrderNewAssetPricing = theirOrderData.getTimestamp() >= BlockChain.getInstance().getNewAssetPricingTimestamp();
// Determine their order price
long theirPrice = theirOrderData.getPrice();
LOGGER.trace(() -> String.format("Their price: %s %s", prettyAmount(theirPrice), pricePair));
BigDecimal theirPrice;
if (isOurOrderNewPricing) {
// Pricing units are the same way round for both orders, so no conversion needed.
// Orders under 'old' pricing have been converted during repository update.
theirPrice = theirOrderData.getPrice();
LOGGER.trace(String.format("Their price: %s %s", theirPrice.toPlainString(), getPricePair()));
} else {
// If our order is 'old' pricing then all other existing orders must be 'old' pricing too
// Their order pricing will be inverted, so convert
theirPrice = BigDecimal.ONE.setScale(8).divide(theirOrderData.getPrice(), RoundingMode.DOWN);
LOGGER.trace(String.format("Their price: %s %s per %s", theirPrice.toPlainString(), wantAssetData.getName(), haveAssetData.getName()));
}
// If their price is worse than what we're willing to accept then we're done as prices only get worse as we iterate through list of orders
if ((haveAssetId < wantAssetId && theirPrice > ourPrice) || (haveAssetId > wantAssetId && theirPrice < ourPrice))
break;
if (isOurOrderNewPricing) {
if (haveAssetId < wantAssetId && theirPrice.compareTo(ourPrice) > 0)
break;
if (haveAssetId > wantAssetId && theirPrice.compareTo(ourPrice) < 0)
break;
} else {
// 'old' pricing scheme
if (theirPrice.compareTo(ourPrice) < 0)
break;
}
// Calculate how much we could buy at their price, "amount" is expressed in terms of asset with highest assetID.
long ourMaxAmount = this.getAmountLeft();
LOGGER.trace(() -> String.format("ourMaxAmount (max we could trade at their price): %s %s", prettyAmount(ourMaxAmount), matchingAssetData.getName()));
// Calculate how much we could buy at their price.
BigDecimal ourMaxAmount;
if (isOurOrderNewPricing)
// In 'new' pricing scheme, "amount" is expressed in terms of asset with highest assetID
ourMaxAmount = this.getAmountLeft();
else
// In 'old' pricing scheme, "amount" is expressed in terms of our want-asset.
ourMaxAmount = this.getAmountLeft().multiply(theirPrice).setScale(8, RoundingMode.DOWN);
LOGGER.trace("ourMaxAmount (max we could trade at their price): " + ourMaxAmount.stripTrailingZeros().toPlainString() + " " + matchingAssetData.getName());
// How much is remaining available in their order.
long theirAmountLeft = Order.getAmountLeft(theirOrderData);
LOGGER.trace(() -> String.format("theirAmountLeft (max amount remaining in their order): %s %s", prettyAmount(theirAmountLeft), matchingAssetData.getName()));
BigDecimal theirAmountLeft = Order.getAmountLeft(theirOrderData);
LOGGER.trace("theirAmountLeft (max amount remaining in their order): " + theirAmountLeft.stripTrailingZeros().toPlainString() + " " + matchingAssetData.getName());
// So matchable want-asset amount is the minimum of above two values
long interimMatchedAmount = Math.min(ourMaxAmount, theirAmountLeft);
LOGGER.trace(() -> String.format("matchedAmount: %s %s", prettyAmount(interimMatchedAmount), matchingAssetData.getName()));
BigDecimal matchedAmount = ourMaxAmount.min(theirAmountLeft);
LOGGER.trace("matchedAmount: " + matchedAmount.stripTrailingZeros().toPlainString() + " " + matchingAssetData.getName());
// If we can't buy anything then try another order
if (interimMatchedAmount <= 0)
if (matchedAmount.compareTo(BigDecimal.ZERO) <= 0)
continue;
// Calculate amount granularity, based on price and both assets' divisibility, so that return-amount traded is a valid value (integer or to 8 d.p.)
long granularity = calculateAmountGranularity(matchingAssetData.isDivisible(), returnAssetData.isDivisible(), theirOrderData.getPrice());
LOGGER.trace(() -> String.format("granularity (amount granularity): %s %s", prettyAmount(granularity), matchingAssetData.getName()));
BigDecimal granularity = calculateAmountGranularity(matchingAssetData.getIsDivisible(), returnAssetData.getIsDivisible(), theirOrderData.getPrice());
LOGGER.trace("granularity (amount granularity): " + granularity.stripTrailingZeros().toPlainString() + " " + matchingAssetData.getName());
// Reduce matched amount (if need be) to fit granularity
long matchedAmount = interimMatchedAmount - interimMatchedAmount % granularity;
LOGGER.trace(() -> String.format("matchedAmount adjusted for granularity: %s %s", prettyAmount(matchedAmount), matchingAssetData.getName()));
matchedAmount = matchedAmount.subtract(matchedAmount.remainder(granularity));
LOGGER.trace("matchedAmount adjusted for granularity: " + matchedAmount.stripTrailingZeros().toPlainString() + " " + matchingAssetData.getName());
// If we can't buy anything then try another order
if (matchedAmount <= 0)
if (matchedAmount.compareTo(BigDecimal.ZERO) <= 0)
continue;
// Safety check
checkDivisibility(matchingAssetData, matchedAmount, theirOrderData);
if (!matchingAssetData.getIsDivisible() && matchedAmount.stripTrailingZeros().scale() > 0) {
Account participant = new PublicKeyAccount(this.repository, theirOrderData.getCreatorPublicKey());
String message = String.format("Refusing to trade fractional %s [indivisible assetID %d] for %s",
matchedAmount.toPlainString(), matchingAssetData.getAssetId(), participant.getAddress());
LOGGER.error(message);
throw new DataException(message);
}
// Trade can go ahead!
// Calculate the total cost to us, in return-asset, based on their price
long returnAmountTraded = Amounts.roundDownScaledMultiply(matchedAmount, theirOrderData.getPrice());
LOGGER.trace(() -> String.format("returnAmountTraded: %s %s", prettyAmount(returnAmountTraded), returnAssetData.getName()));
BigDecimal returnAmountTraded = matchedAmount.multiply(theirOrderData.getPrice()).setScale(8, RoundingMode.DOWN);
LOGGER.trace("returnAmountTraded: " + returnAmountTraded.stripTrailingZeros().toPlainString() + " " + returnAssetData.getName());
// Safety check
checkDivisibility(returnAssetData, returnAmountTraded, this.orderData);
if (!returnAssetData.getIsDivisible() && returnAmountTraded.stripTrailingZeros().scale() > 0) {
String message = String.format("Refusing to trade fractional %s [indivisible assetID %d] for %s",
returnAmountTraded.toPlainString(), returnAssetData.getAssetId(), creator.getAddress());
LOGGER.error(message);
throw new DataException(message);
}
long tradedWantAmount = this.isAmountInWantAsset ? matchedAmount : returnAmountTraded;
long tradedHaveAmount = this.isAmountInWantAsset ? returnAmountTraded : matchedAmount;
BigDecimal tradedWantAmount = (isOurOrderNewPricing && haveAssetId > wantAssetId) ? returnAmountTraded : matchedAmount;
BigDecimal tradedHaveAmount = (isOurOrderNewPricing && haveAssetId > wantAssetId) ? matchedAmount : returnAmountTraded;
// We also need to know how much have-asset to refund based on price improvement (only one direction applies)
long haveAssetRefund = this.isAmountInWantAsset ? Amounts.roundDownScaledMultiply(matchedAmount, Math.abs(ourPrice - theirPrice)) : 0;
// We also need to know how much have-asset to refund based on price improvement ('new' pricing only and only one direction applies)
BigDecimal haveAssetRefund = isOurOrderNewPricing && haveAssetId < wantAssetId ? ourPrice.subtract(theirPrice).abs().multiply(matchedAmount).setScale(8, RoundingMode.DOWN) : BigDecimal.ZERO;
LOGGER.trace(() -> String.format("We traded %s %s (have-asset) for %s %s (want-asset), saving %s %s (have-asset)",
prettyAmount(tradedHaveAmount), haveAssetData.getName(),
prettyAmount(tradedWantAmount), wantAssetData.getName(),
prettyAmount(haveAssetRefund), haveAssetData.getName()));
LOGGER.trace(String.format("We traded %s %s (have-asset) for %s %s (want-asset), saving %s %s (have-asset)",
tradedHaveAmount.toPlainString(), haveAssetData.getName(),
tradedWantAmount.toPlainString(), wantAssetData.getName(),
haveAssetRefund.toPlainString(), haveAssetData.getName()));
// Construct trade
TradeData tradeData = new TradeData(this.orderData.getOrderId(), theirOrderData.getOrderId(),
tradedWantAmount, tradedHaveAmount, haveAssetRefund, this.orderData.getTimestamp());
// Process trade, updating corresponding orders in repository
Trade trade = new Trade(this.repository, tradeData);
trade.process();
// Update our order in terms of fulfilment, etc. but do not save into repository as that's handled by Trade above
long amountFulfilled = matchedAmount;
this.orderData.setFulfilled(this.orderData.getFulfilled() + amountFulfilled);
LOGGER.trace(() -> String.format("Updated our order's fulfilled amount to: %s %s", prettyAmount(this.orderData.getFulfilled()), matchingAssetData.getName()));
LOGGER.trace(() -> String.format("Our order's amount remaining: %s %s", prettyAmount(this.getAmountLeft()), matchingAssetData.getName()));
BigDecimal amountFulfilled = isOurOrderNewPricing ? matchedAmount : returnAmountTraded;
this.orderData.setFulfilled(this.orderData.getFulfilled().add(amountFulfilled));
LOGGER.trace("Updated our order's fulfilled amount to: " + this.orderData.getFulfilled().stripTrailingZeros().toPlainString() + " " + matchingAssetData.getName());
LOGGER.trace("Our order's amount remaining: " + this.getAmountLeft().stripTrailingZeros().toPlainString() + " " + matchingAssetData.getName());
// Continue on to process other open orders if we still have amount left to match
if (this.getAmountLeft() <= 0)
if (this.getAmountLeft().compareTo(BigDecimal.ZERO) <= 0)
break;
}
}
/**
* Check amount has no fractional part if asset is indivisible.
*
* @throws DataException if divisibility check fails
*/
private void checkDivisibility(AssetData assetData, long amount, OrderData orderData) throws DataException {
if (assetData.isDivisible() || amount % Amounts.MULTIPLIER == 0)
// Asset is divisible or amount has no fractional part
return;
String message = String.format("Refusing to trade fractional %s [indivisible assetID %d] for order %s",
prettyAmount(amount), assetData.getAssetId(), Base58.encode(orderData.getOrderId()));
LOGGER.error(message);
throw new DataException(message);
}
public void orphan() throws DataException {
// Orphan trades that occurred as a result of this order
for (TradeData tradeData : getTrades())
@@ -418,7 +464,7 @@ public class Order {
// Return asset to creator
Account creator = new PublicKeyAccount(this.repository, this.orderData.getCreatorPublicKey());
creator.modifyAssetBalance(haveAssetId, this.calcHaveAssetCommittment());
creator.setConfirmedBalance(haveAssetId, creator.getConfirmedBalance(haveAssetId).add(this.calcHaveAssetCommittment()));
}
// This is called by CancelOrderTransaction so that an Order can no longer trade
@@ -428,14 +474,14 @@ public class Order {
// Update creator's balance with unfulfilled amount
Account creator = new PublicKeyAccount(this.repository, this.orderData.getCreatorPublicKey());
creator.modifyAssetBalance(haveAssetId, calcHaveAssetRefund());
creator.setConfirmedBalance(haveAssetId, creator.getConfirmedBalance(haveAssetId).add(calcHaveAssetRefund()));
}
// Opposite of cancel() above for use during orphaning
public void reopen() throws DataException {
// Update creator's balance with unfulfilled amount
Account creator = new PublicKeyAccount(this.repository, this.orderData.getCreatorPublicKey());
creator.modifyAssetBalance(haveAssetId, - calcHaveAssetRefund());
creator.setConfirmedBalance(haveAssetId, creator.getConfirmedBalance(haveAssetId).subtract(calcHaveAssetRefund()));
this.orderData.setIsClosed(false);
this.repository.getAssetRepository().save(this.orderData);

View File

@@ -1,7 +1,10 @@
package org.qortal.asset;
import java.math.BigDecimal;
import org.qortal.account.Account;
import org.qortal.account.PublicKeyAccount;
import org.qortal.block.BlockChain;
import org.qortal.data.asset.OrderData;
import org.qortal.data.asset.TradeData;
import org.qortal.repository.AssetRepository;
@@ -14,11 +17,12 @@ public class Trade {
private Repository repository;
private TradeData tradeData;
private boolean isNewPricing;
private AssetRepository assetRepository;
private OrderData initiatingOrder;
private OrderData targetOrder;
private long fulfilled;
private BigDecimal newPricingFulfilled;
// Constructors
@@ -26,6 +30,7 @@ public class Trade {
this.repository = repository;
this.tradeData = tradeData;
this.isNewPricing = this.tradeData.getTimestamp() > BlockChain.getInstance().getNewAssetPricingTimestamp();
this.assetRepository = this.repository.getAssetRepository();
}
@@ -38,9 +43,9 @@ public class Trade {
// Note: targetAmount is amount traded FROM target order
// Note: initiatorAmount is amount traded FROM initiating order
// "amount" and "fulfilled" are the same asset for both orders
// Under 'new' pricing scheme, "amount" and "fulfilled" are the same asset for both orders
// which is the matchedAmount in asset with highest assetID
this.fulfilled = initiatingOrder.getHaveAssetId() < initiatingOrder.getWantAssetId() ? this.tradeData.getTargetAmount() : this.tradeData.getInitiatorAmount();
this.newPricingFulfilled = (initiatingOrder.getHaveAssetId() < initiatingOrder.getWantAssetId()) ? this.tradeData.getTargetAmount() : this.tradeData.getInitiatorAmount();
}
public void process() throws DataException {
@@ -50,16 +55,16 @@ public class Trade {
// Note: targetAmount is amount traded FROM target order
// Note: initiatorAmount is amount traded FROM initiating order
// Update corresponding Orders on both sides of trade
commonPrep();
// Update corresponding Orders on both sides of trade
initiatingOrder.setFulfilled(initiatingOrder.getFulfilled() + fulfilled);
initiatingOrder.setFulfilled(initiatingOrder.getFulfilled().add(isNewPricing ? newPricingFulfilled : tradeData.getInitiatorAmount()));
initiatingOrder.setIsFulfilled(Order.isFulfilled(initiatingOrder));
// Set isClosed to true if isFulfilled now true
initiatingOrder.setIsClosed(initiatingOrder.getIsFulfilled());
assetRepository.save(initiatingOrder);
targetOrder.setFulfilled(targetOrder.getFulfilled() + fulfilled);
targetOrder.setFulfilled(targetOrder.getFulfilled().add(isNewPricing ? newPricingFulfilled : tradeData.getTargetAmount()));
targetOrder.setIsFulfilled(Order.isFulfilled(targetOrder));
// Set isClosed to true if isFulfilled now true
targetOrder.setIsClosed(targetOrder.getIsFulfilled());
@@ -67,31 +72,33 @@ public class Trade {
// Actually transfer asset balances
Account initiatingCreator = new PublicKeyAccount(this.repository, initiatingOrder.getCreatorPublicKey());
initiatingCreator.modifyAssetBalance(initiatingOrder.getWantAssetId(), tradeData.getTargetAmount());
initiatingCreator.setConfirmedBalance(initiatingOrder.getWantAssetId(), initiatingCreator.getConfirmedBalance(initiatingOrder.getWantAssetId()).add(tradeData.getTargetAmount()));
Account targetCreator = new PublicKeyAccount(this.repository, targetOrder.getCreatorPublicKey());
targetCreator.modifyAssetBalance(targetOrder.getWantAssetId(), tradeData.getInitiatorAmount());
targetCreator.setConfirmedBalance(targetOrder.getWantAssetId(), targetCreator.getConfirmedBalance(targetOrder.getWantAssetId()).add(tradeData.getInitiatorAmount()));
// Possible partial saving to refund to initiator
long initiatorSaving = this.tradeData.getInitiatorSaving();
if (initiatorSaving > 0)
initiatingCreator.modifyAssetBalance(initiatingOrder.getHaveAssetId(), initiatorSaving);
BigDecimal initiatorSaving = this.tradeData.getInitiatorSaving();
if (initiatorSaving.compareTo(BigDecimal.ZERO) > 0)
initiatingCreator.setConfirmedBalance(initiatingOrder.getHaveAssetId(), initiatingCreator.getConfirmedBalance(initiatingOrder.getHaveAssetId()).add(initiatorSaving));
}
public void orphan() throws DataException {
AssetRepository assetRepository = this.repository.getAssetRepository();
// Note: targetAmount is amount traded FROM target order
// Note: initiatorAmount is amount traded FROM initiating order
// Revert corresponding Orders on both sides of trade
commonPrep();
// Revert corresponding Orders on both sides of trade
initiatingOrder.setFulfilled(initiatingOrder.getFulfilled() - fulfilled);
initiatingOrder.setFulfilled(initiatingOrder.getFulfilled().subtract(isNewPricing ? newPricingFulfilled : tradeData.getInitiatorAmount()));
initiatingOrder.setIsFulfilled(Order.isFulfilled(initiatingOrder));
// Set isClosed to false if isFulfilled now false
initiatingOrder.setIsClosed(initiatingOrder.getIsFulfilled());
assetRepository.save(initiatingOrder);
targetOrder.setFulfilled(targetOrder.getFulfilled() - fulfilled);
targetOrder.setFulfilled(targetOrder.getFulfilled().subtract(isNewPricing ? newPricingFulfilled : tradeData.getTargetAmount()));
targetOrder.setIsFulfilled(Order.isFulfilled(targetOrder));
// Set isClosed to false if isFulfilled now false
targetOrder.setIsClosed(targetOrder.getIsFulfilled());
@@ -99,15 +106,15 @@ public class Trade {
// Reverse asset transfers
Account initiatingCreator = new PublicKeyAccount(this.repository, initiatingOrder.getCreatorPublicKey());
initiatingCreator.modifyAssetBalance(initiatingOrder.getWantAssetId(), - tradeData.getTargetAmount());
initiatingCreator.setConfirmedBalance(initiatingOrder.getWantAssetId(), initiatingCreator.getConfirmedBalance(initiatingOrder.getWantAssetId()).subtract(tradeData.getTargetAmount()));
Account targetCreator = new PublicKeyAccount(this.repository, targetOrder.getCreatorPublicKey());
targetCreator.modifyAssetBalance(targetOrder.getWantAssetId(), - tradeData.getInitiatorAmount());
targetCreator.setConfirmedBalance(targetOrder.getWantAssetId(), targetCreator.getConfirmedBalance(targetOrder.getWantAssetId()).subtract(tradeData.getInitiatorAmount()));
// Possible partial saving to claw back from initiator
long initiatorSaving = this.tradeData.getInitiatorSaving();
if (initiatorSaving > 0)
initiatingCreator.modifyAssetBalance(initiatingOrder.getHaveAssetId(), - initiatorSaving);
BigDecimal initiatorSaving = this.tradeData.getInitiatorSaving();
if (initiatorSaving.compareTo(BigDecimal.ZERO) > 0)
initiatingCreator.setConfirmedBalance(initiatingOrder.getHaveAssetId(), initiatingCreator.getConfirmedBalance(initiatingOrder.getHaveAssetId()).subtract(initiatorSaving));
// Remove trade from repository
assetRepository.delete(tradeData);

View File

@@ -1,9 +1,11 @@
package org.qortal.at;
import java.math.BigDecimal;
import java.nio.ByteBuffer;
import java.util.List;
import org.ciyam.at.MachineState;
import org.ciyam.at.Timestamp;
import org.qortal.asset.Asset;
import org.qortal.crypto.Crypto;
import org.qortal.data.at.ATData;
import org.qortal.data.at.ATStateData;
@@ -40,28 +42,66 @@ public class AT {
int height = this.repository.getBlockRepository().getBlockchainHeight() + 1;
byte[] creatorPublicKey = deployATTransactionData.getCreatorPublicKey();
long creation = deployATTransactionData.getTimestamp();
byte[] creationBytes = deployATTransactionData.getCreationBytes();
long assetId = deployATTransactionData.getAssetId();
short version = (short) ((creationBytes[0] & 0xff) | (creationBytes[1] << 8)); // Little-endian
// Just enough AT data to allow API to query initial balances, etc.
ATData skeletonAtData = new ATData(atAddress, creatorPublicKey, creation, assetId);
if (version >= 2) {
MachineState machineState = new MachineState(deployATTransactionData.getCreationBytes());
long blockTimestamp = Timestamp.toLong(height, 0);
QortalATAPI api = new QortalATAPI(repository, skeletonAtData, blockTimestamp);
QortalAtLoggerFactory loggerFactory = QortalAtLoggerFactory.getInstance();
this.atData = new ATData(atAddress, creatorPublicKey, creation, machineState.version, assetId, machineState.getCodeBytes(),
machineState.getIsSleeping(), machineState.getSleepUntilHeight(), machineState.getIsFinished(), machineState.getHadFatalError(),
machineState.getIsFrozen(), machineState.getFrozenBalance());
MachineState machineState = new MachineState(api, loggerFactory, deployATTransactionData.getCreationBytes());
byte[] stateData = machineState.toBytes();
byte[] stateHash = Crypto.digest(stateData);
byte[] codeBytes = machineState.getCodeBytes();
byte[] codeHash = Crypto.digest(codeBytes);
this.atStateData = new ATStateData(atAddress, height, creation, stateData, stateHash, BigDecimal.ZERO.setScale(8));
} else {
// Legacy v1 AT
// We would deploy these in 'dead' state as they will never be run on Qortal
// but this breaks import from Qora1 so something else will have to mark them dead at hard-fork
this.atData = new ATData(atAddress, creatorPublicKey, creation, machineState.version, assetId, codeBytes, codeHash,
machineState.isSleeping(), machineState.getSleepUntilHeight(), machineState.isFinished(), machineState.hadFatalError(),
machineState.isFrozen(), machineState.getFrozenBalance());
// Extract code bytes length
ByteBuffer byteBuffer = ByteBuffer.wrap(deployATTransactionData.getCreationBytes());
byte[] stateData = machineState.toBytes();
byte[] stateHash = Crypto.digest(stateData);
// v1 AT header is: version, reserved, code-pages, data-pages, call-stack-pages, user-stack-pages (all shorts)
this.atStateData = new ATStateData(atAddress, height, stateData, stateHash, 0L, true);
// Number of code pages
short numCodePages = byteBuffer.get(2 + 2);
// Skip header and also "minimum activation amount" (long)
byteBuffer.position(6 * 2 + 8);
int codeLen = 0;
// Extract actual code length, stored in minimal-size form (byte, short or int)
if (numCodePages * 256 < 257) {
codeLen = byteBuffer.get() & 0xff;
} else if (numCodePages * 256 < Short.MAX_VALUE + 1) {
codeLen = byteBuffer.getShort() & 0xffff;
} else if (numCodePages * 256 <= Integer.MAX_VALUE) {
codeLen = byteBuffer.getInt();
}
// Extract code bytes
byte[] codeBytes = new byte[codeLen];
byteBuffer.get(codeBytes);
// Create AT
boolean isSleeping = false;
Integer sleepUntilHeight = null;
boolean isFinished = false;
boolean hadFatalError = false;
boolean isFrozen = false;
Long frozenBalance = null;
this.atData = new ATData(atAddress, creatorPublicKey, creation, version, Asset.QORT, codeBytes, isSleeping, sleepUntilHeight, isFinished,
hadFatalError, isFrozen, frozenBalance);
this.atStateData = new ATStateData(atAddress, height, creation, null, null, BigDecimal.ZERO.setScale(8));
}
}
// Getters / setters
@@ -76,7 +116,9 @@ public class AT {
ATRepository atRepository = this.repository.getATRepository();
atRepository.save(this.atData);
atRepository.save(this.atStateData);
// For version 2+ we also store initial AT state data
if (this.atData.getVersion() >= 2)
atRepository.save(this.atStateData);
}
public void undeploy() throws DataException {
@@ -84,80 +126,34 @@ public class AT {
this.repository.getATRepository().delete(this.atData.getATAddress());
}
public List<AtTransaction> run(int blockHeight, long blockTimestamp) throws DataException {
public List<AtTransaction> run(long blockTimestamp) throws DataException {
String atAddress = this.atData.getATAddress();
QortalATAPI api = new QortalATAPI(repository, this.atData, blockTimestamp);
QortalAtLoggerFactory loggerFactory = QortalAtLoggerFactory.getInstance();
QortalATLogger logger = new QortalATLogger();
byte[] codeBytes = this.atData.getCodeBytes();
// Fetch latest ATStateData for this AT
// Fetch latest ATStateData for this AT (if any)
ATStateData latestAtStateData = this.repository.getATRepository().getLatestATState(atAddress);
// There should be at least initial deployment AT state data
// There should be at least initial AT state data
if (latestAtStateData == null)
throw new IllegalStateException("No previous AT state data found");
throw new IllegalStateException("No initial AT state data found");
// [Re]create AT machine state using AT state data or from scratch as applicable
MachineState state = MachineState.fromBytes(api, loggerFactory, latestAtStateData.getStateData(), codeBytes);
try {
state.execute();
} catch (Exception e) {
throw new DataException(String.format("Uncaught exception while running AT '%s'", atAddress), e);
}
MachineState state = MachineState.fromBytes(api, logger, latestAtStateData.getStateData(), codeBytes);
state.execute();
int height = this.repository.getBlockRepository().getBlockchainHeight() + 1;
long creation = this.atData.getCreation();
byte[] stateData = state.toBytes();
byte[] stateHash = Crypto.digest(stateData);
long atFees = api.calcFinalFees(state);
BigDecimal atFees = api.calcFinalFees(state);
this.atStateData = new ATStateData(atAddress, blockHeight, stateData, stateHash, atFees, false);
this.atStateData = new ATStateData(atAddress, height, creation, stateData, stateHash, atFees);
return api.getTransactions();
}
public void update(int blockHeight, long blockTimestamp) throws DataException {
// 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);
// Update AT info in repository too
this.atData.setIsSleeping(state.isSleeping());
this.atData.setSleepUntilHeight(state.getSleepUntilHeight());
this.atData.setIsFinished(state.isFinished());
this.atData.setHadFatalError(state.hadFatalError());
this.atData.setIsFrozen(state.isFrozen());
this.atData.setFrozenBalance(state.getFrozenBalance());
this.repository.getATRepository().save(this.atData);
}
public void revert(int blockHeight, long blockTimestamp) throws DataException {
String atAddress = this.atData.getATAddress();
// Delete old AT state data from repository
this.repository.getATRepository().delete(atAddress, blockHeight);
if (this.atStateData.isInitial())
return;
// Load previous state data
ATStateData previousStateData = this.repository.getATRepository().getLatestATState(atAddress);
if (previousStateData == null)
throw new DataException("Can't find previous AT state data for " + atAddress);
// 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());
this.atData.setSleepUntilHeight(state.getSleepUntilHeight());
this.atData.setIsFinished(state.isFinished());
this.atData.setHadFatalError(state.hadFatalError());
this.atData.setIsFrozen(state.isFrozen());
this.atData.setFrozenBalance(state.getFrozenBalance());
this.repository.getATRepository().save(this.atData);
}
}

View File

@@ -0,0 +1,134 @@
package org.qortal.at;
import static java.util.Arrays.stream;
import static java.util.stream.Collectors.toMap;
import java.math.BigDecimal;
import java.util.List;
import java.util.Map;
import org.ciyam.at.MachineState;
import org.ciyam.at.Timestamp;
import org.qortal.account.Account;
import org.qortal.block.Block;
import org.qortal.data.block.BlockData;
import org.qortal.data.transaction.ATTransactionData;
import org.qortal.data.transaction.PaymentTransactionData;
import org.qortal.data.transaction.TransactionData;
import org.qortal.repository.BlockRepository;
import org.qortal.repository.DataException;
import org.qortal.transaction.Transaction;
public enum BlockchainAPI {
QORTAL(0) {
@Override
public void putTransactionFromRecipientAfterTimestampInA(String recipient, Timestamp timestamp, MachineState state) {
int height = timestamp.blockHeight;
int sequence = timestamp.transactionSequence + 1;
QortalATAPI api = (QortalATAPI) state.getAPI();
BlockRepository blockRepository = api.repository.getBlockRepository();
try {
Account recipientAccount = new Account(api.repository, recipient);
while (height <= blockRepository.getBlockchainHeight()) {
BlockData blockData = blockRepository.fromHeight(height);
if (blockData == null)
throw new DataException("Unable to fetch block " + height + " from repository?");
Block block = new Block(api.repository, blockData);
List<Transaction> transactions = block.getTransactions();
// No more transactions in this block? Try next block
if (sequence >= transactions.size()) {
++height;
sequence = 0;
continue;
}
Transaction transaction = transactions.get(sequence);
// Transaction needs to be sent to specified recipient
if (transaction.getRecipientAccounts().contains(recipientAccount)) {
// Found a transaction
api.setA1(state, new Timestamp(height, timestamp.blockchainId, sequence).longValue());
// Hash transaction's signature into other three A fields for future verification that it's the same transaction
byte[] hash = QortalATAPI.sha192(transaction.getTransactionData().getSignature());
api.setA2(state, QortalATAPI.fromBytes(hash, 0));
api.setA3(state, QortalATAPI.fromBytes(hash, 8));
api.setA4(state, QortalATAPI.fromBytes(hash, 16));
return;
}
// Transaction wasn't for us - keep going
++sequence;
}
// No more transactions - zero A and exit
api.zeroA(state);
} catch (DataException e) {
throw new RuntimeException("AT API unable to fetch next transaction?", e);
}
}
@Override
public long getAmountFromTransactionInA(Timestamp timestamp, MachineState state) {
QortalATAPI api = (QortalATAPI) state.getAPI();
TransactionData transactionData = api.fetchTransaction(state);
switch (transactionData.getType()) {
case PAYMENT:
return ((PaymentTransactionData) transactionData).getAmount().unscaledValue().longValue();
case AT:
BigDecimal amount = ((ATTransactionData) transactionData).getAmount();
if (amount != null)
return amount.unscaledValue().longValue();
else
return 0xffffffffffffffffL;
default:
return 0xffffffffffffffffL;
}
}
},
BTC(1) {
@Override
public void putTransactionFromRecipientAfterTimestampInA(String recipient, Timestamp timestamp, MachineState state) {
// TODO BTC transaction support for ATv2
}
@Override
public long getAmountFromTransactionInA(Timestamp timestamp, MachineState state) {
// TODO BTC transaction support for ATv2
return 0;
}
};
public final int value;
private static final Map<Integer, BlockchainAPI> map = stream(BlockchainAPI.values()).collect(toMap(type -> type.value, type -> type));
BlockchainAPI(int value) {
this.value = value;
}
public static BlockchainAPI valueOf(int value) {
return map.get(value);
}
// Blockchain-specific API methods
public abstract void putTransactionFromRecipientAfterTimestampInA(String recipient, Timestamp timestamp, MachineState state);
public abstract long getAmountFromTransactionInA(Timestamp timestamp, MachineState state);
}

View File

@@ -1,11 +1,11 @@
package org.qortal.at;
import java.math.BigDecimal;
import java.security.MessageDigest;
import java.security.NoSuchAlgorithmException;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.List;
import org.apache.logging.log4j.LogManager;
import org.apache.logging.log4j.Logger;
import org.ciyam.at.API;
import org.ciyam.at.ExecutionException;
import org.ciyam.at.FunctionData;
@@ -14,41 +14,35 @@ import org.ciyam.at.MachineState;
import org.ciyam.at.OpCode;
import org.ciyam.at.Timestamp;
import org.qortal.account.Account;
import org.qortal.account.NullAccount;
import org.qortal.account.GenesisAccount;
import org.qortal.account.PublicKeyAccount;
import org.qortal.asset.Asset;
import org.qortal.block.BlockChain;
import org.qortal.block.BlockChain.CiyamAtSettings;
import org.qortal.crypto.Crypto;
import org.qortal.data.at.ATData;
import org.qortal.data.block.BlockData;
import org.qortal.data.block.BlockSummaryData;
import org.qortal.data.transaction.ATTransactionData;
import org.qortal.data.transaction.BaseTransactionData;
import org.qortal.data.transaction.MessageTransactionData;
import org.qortal.data.transaction.PaymentTransactionData;
import org.qortal.data.transaction.TransactionData;
import org.qortal.group.Group;
import org.qortal.repository.ATRepository;
import org.qortal.repository.DataException;
import org.qortal.repository.Repository;
import org.qortal.transaction.AtTransaction;
import org.qortal.transaction.Transaction.TransactionType;
import org.qortal.utils.Base58;
import org.qortal.utils.BitTwiddling;
import com.google.common.primitives.Bytes;
public class QortalATAPI extends API {
private static final byte[] ADDRESS_PADDING = new byte[32 - Account.ADDRESS_LENGTH];
private static final Logger LOGGER = LogManager.getLogger(QortalATAPI.class);
// Useful constants
private static final BigDecimal FEE_PER_STEP = BigDecimal.valueOf(1.0).setScale(8); // 1 QORT per "step"
private static final int MAX_STEPS_PER_ROUND = 500;
private static final int STEPS_PER_FUNCTION_CALL = 10;
private static final int MINUTES_PER_BLOCK = 10;
// Properties
private Repository repository;
private ATData atData;
private long blockTimestamp;
private final CiyamAtSettings ciyamAtSettings;
Repository repository;
ATData atData;
long blockTimestamp;
/** List of generated AT transactions */
List<AtTransaction> transactions;
@@ -60,42 +54,36 @@ public class QortalATAPI extends API {
this.atData = atData;
this.transactions = new ArrayList<>();
this.blockTimestamp = blockTimestamp;
this.ciyamAtSettings = BlockChain.getInstance().getCiyamAtSettings();
}
// Methods specific to Qortal AT processing, not inherited
public Repository getRepository() {
return this.repository;
}
public List<AtTransaction> getTransactions() {
return this.transactions;
}
public long calcFinalFees(MachineState state) {
return state.getSteps() * this.ciyamAtSettings.feePerStep;
public BigDecimal calcFinalFees(MachineState state) {
return FEE_PER_STEP.multiply(BigDecimal.valueOf(state.getSteps()));
}
// Inherited methods from CIYAM AT API
@Override
public int getMaxStepsPerRound() {
return this.ciyamAtSettings.maxStepsPerRound;
return MAX_STEPS_PER_ROUND;
}
@Override
public int getOpCodeSteps(OpCode opcode) {
if (opcode.value >= OpCode.EXT_FUN.value && opcode.value <= OpCode.EXT_FUN_RET_DAT_2.value)
return this.ciyamAtSettings.stepsPerFunctionCall;
return STEPS_PER_FUNCTION_CALL;
return 1;
}
@Override
public long getFeePerStep() {
return this.ciyamAtSettings.feePerStep;
return FEE_PER_STEP.unscaledValue().longValue();
}
@Override
@@ -117,67 +105,31 @@ public class QortalATAPI extends API {
}
@Override
public void putPreviousBlockHashIntoA(MachineState state) {
public void putPreviousBlockHashInA(MachineState state) {
try {
int previousBlockHeight = this.repository.getBlockRepository().getBlockchainHeight() - 1;
// We only need signature, so only request a block summary
List<BlockSummaryData> blockSummaries = this.repository.getBlockRepository().getBlockSummaries(previousBlockHeight, previousBlockHeight);
if (blockSummaries == null || blockSummaries.size() != 1)
throw new RuntimeException("AT API unable to fetch previous block hash?");
BlockData blockData = this.repository.getBlockRepository().fromHeight(this.getPreviousBlockHeight());
// Block's signature is 128 bytes so we need to reduce this to 4 longs (32 bytes)
// To be able to use hash to look up block, save height (8 bytes) and partial signature (24 bytes)
this.setA1(state, previousBlockHeight);
byte[] blockHash = Crypto.digest(blockData.getSignature());
byte[] signature = blockSummaries.get(0).getSignature();
// Save some of minter's signature and transactions signature, so middle 24 bytes of the full 128 byte signature.
this.setA2(state, BitTwiddling.longFromBEBytes(signature, 52));
this.setA3(state, BitTwiddling.longFromBEBytes(signature, 60));
this.setA4(state, BitTwiddling.longFromBEBytes(signature, 68));
this.setA(state, blockHash);
} catch (DataException e) {
throw new RuntimeException("AT API unable to fetch previous block?", e);
}
}
@Override
public void putTransactionAfterTimestampIntoA(Timestamp timestamp, MachineState state) {
public void putTransactionAfterTimestampInA(Timestamp timestamp, MachineState state) {
// Recipient is this AT
String atAddress = this.atData.getATAddress();
String recipient = this.atData.getATAddress();
int height = timestamp.blockHeight;
int sequence = timestamp.transactionSequence;
if (state.getCurrentBlockHeight() < BlockChain.getInstance().getAtFindNextTransactionFixHeight())
// Off-by-one bug still in effect
sequence += 1;
ATRepository.NextTransactionInfo nextTransactionInfo;
try {
nextTransactionInfo = this.getRepository().getATRepository().findNextTransaction(atAddress, height, sequence);
} catch (DataException e) {
throw new RuntimeException("AT API unable to fetch next transaction?", e);
}
if (nextTransactionInfo == null) {
// No more transactions for AT at this time - zero A and exit
this.zeroA(state);
return;
}
// Found a transaction
this.setA1(state, new Timestamp(nextTransactionInfo.height, timestamp.blockchainId, nextTransactionInfo.sequence).longValue());
// Copy transaction's partial signature into the other three A fields for future verification that it's the same transaction
this.setA2(state, BitTwiddling.longFromBEBytes(nextTransactionInfo.signature, 8));
this.setA3(state, BitTwiddling.longFromBEBytes(nextTransactionInfo.signature, 16));
this.setA4(state, BitTwiddling.longFromBEBytes(nextTransactionInfo.signature, 24));
BlockchainAPI blockchainAPI = BlockchainAPI.valueOf(timestamp.blockchainId);
blockchainAPI.putTransactionFromRecipientAfterTimestampInA(recipient, timestamp, state);
}
@Override
public long getTypeFromTransactionInA(MachineState state) {
TransactionData transactionData = this.getTransactionFromA(state);
TransactionData transactionData = this.fetchTransaction(state);
switch (transactionData.getType()) {
case PAYMENT:
@@ -199,36 +151,22 @@ public class QortalATAPI extends API {
@Override
public long getAmountFromTransactionInA(MachineState state) {
TransactionData transactionData = this.getTransactionFromA(state);
switch (transactionData.getType()) {
case PAYMENT:
return ((PaymentTransactionData) transactionData).getAmount();
case AT:
Long amount = ((ATTransactionData) transactionData).getAmount();
if (amount != null)
return amount;
// fall-through to default
default:
return 0xffffffffffffffffL;
}
Timestamp timestamp = new Timestamp(state.getA1());
BlockchainAPI blockchainAPI = BlockchainAPI.valueOf(timestamp.blockchainId);
return blockchainAPI.getAmountFromTransactionInA(timestamp, state);
}
@Override
public long getTimestampFromTransactionInA(MachineState state) {
// Transaction's "timestamp" already stored in A1
Timestamp timestamp = new Timestamp(this.getA1(state));
Timestamp timestamp = new Timestamp(state.getA1());
return timestamp.longValue();
}
@Override
public long generateRandomUsingTransactionInA(MachineState state) {
// The plan here is to sleep for a block then use next block's signature
// and this transaction's signature to generate pseudo-random, but deterministic, value.
// The plan here is to sleep for a block then use next block's signature and this transaction's signature to generate pseudo-random, but deterministic,
// value.
if (!isFirstOpCodeAfterSleeping(state)) {
// First call
@@ -241,7 +179,7 @@ public class QortalATAPI extends API {
// Second call
// HASH(A and new block hash)
TransactionData transactionData = this.getTransactionFromA(state);
TransactionData transactionData = this.fetchTransaction(state);
try {
BlockData blockData = this.repository.getBlockRepository().getLastBlock();
@@ -253,7 +191,7 @@ public class QortalATAPI extends API {
byte[] hash = Crypto.digest(input);
return BitTwiddling.longFromBEBytes(hash, 0);
return fromBytes(hash, 0);
} catch (DataException e) {
throw new RuntimeException("AT API unable to fetch latest block from repository?", e);
}
@@ -265,50 +203,50 @@ public class QortalATAPI extends API {
// Zero B in case of issues or shorter-than-B message
this.zeroB(state);
TransactionData transactionData = this.getTransactionFromA(state);
TransactionData transactionData = this.fetchTransaction(state);
byte[] messageData = this.getMessageFromTransaction(transactionData);
byte[] messageData = null;
switch (transactionData.getType()) {
case MESSAGE:
messageData = ((MessageTransactionData) transactionData).getData();
break;
case AT:
messageData = ((ATTransactionData) transactionData).getMessage();
break;
default:
return;
}
// Check data length is appropriate, i.e. not larger than B
if (messageData.length > 4 * 8)
return;
// Pad messageData to fit B
if (messageData.length < 4 * 8)
messageData = Bytes.ensureCapacity(messageData, 4 * 8, 0);
byte[] paddedMessageData = Bytes.ensureCapacity(messageData, 4 * 8, 0);
// Endian must be correct here so that (for example) a SHA256 message can be compared to one generated locally
this.setB(state, messageData);
this.setB(state, paddedMessageData);
}
@Override
public void putAddressFromTransactionInAIntoB(MachineState state) {
TransactionData transactionData = this.getTransactionFromA(state);
TransactionData transactionData = this.fetchTransaction(state);
String address;
if (transactionData.getType() == TransactionType.AT) {
// Use AT address from transaction data, as transaction's public key will always be fake
address = ((ATTransactionData) transactionData).getATAddress();
} else {
byte[] publicKey = transactionData.getCreatorPublicKey();
address = Crypto.toAddress(publicKey);
}
// We actually use public key as it has more potential utility (e.g. message verification) than an address
byte[] bytes = transactionData.getCreatorPublicKey();
// Convert to byte form as this only takes 25 bytes,
// compared to string-form's 34 bytes,
// and we only have 32 bytes available.
byte[] addressBytes = Bytes.ensureCapacity(Base58.decode(address), 32, 0); // pad to 32 bytes
this.setB(state, addressBytes);
this.setB(state, bytes);
}
@Override
public void putCreatorAddressIntoB(MachineState state) {
byte[] publicKey = atData.getCreatorPublicKey();
String address = Crypto.toAddress(publicKey);
// We actually use public key as it has more potential utility (e.g. message verification) than an address
byte[] bytes = atData.getCreatorPublicKey();
// Convert to byte form as this only takes 25 bytes,
// compared to string-form's 34 bytes,
// and we only have 32 bytes available.
byte[] addressBytes = Bytes.ensureCapacity(Base58.decode(address), 32, 0); // pad to 32 bytes
this.setB(state, addressBytes);
this.setB(state, bytes);
}
@Override
@@ -316,22 +254,25 @@ public class QortalATAPI extends API {
try {
Account atAccount = this.getATAccount();
return atAccount.getConfirmedBalance(Asset.QORT);
return atAccount.getConfirmedBalance(Asset.QORT).unscaledValue().longValue();
} catch (DataException e) {
throw new RuntimeException("AT API unable to fetch AT's current balance?", e);
}
}
@Override
public void payAmountToB(long amount, MachineState state) {
Account recipient = getAccountFromB(state);
public void payAmountToB(long unscaledAmount, MachineState state) {
byte[] publicKey = state.getB();
PublicKeyAccount recipient = new PublicKeyAccount(this.repository, publicKey);
long timestamp = this.getNextTransactionTimestamp();
byte[] reference = this.getLastReference();
BigDecimal amount = BigDecimal.valueOf(unscaledAmount, 8);
BaseTransactionData baseTransactionData = new BaseTransactionData(timestamp, Group.NO_GROUP, reference, NullAccount.PUBLIC_KEY, 0L, null);
BaseTransactionData baseTransactionData = new BaseTransactionData(timestamp, Group.NO_GROUP, reference, GenesisAccount.PUBLIC_KEY, BigDecimal.ZERO, null);
ATTransactionData atTransactionData = new ATTransactionData(baseTransactionData, this.atData.getATAddress(),
recipient.getAddress(), amount, this.atData.getAssetId());
recipient.getAddress(), amount, this.atData.getAssetId(), new byte[0]);
AtTransaction atTransaction = new AtTransaction(this.repository, atTransactionData);
// Add to our transactions
@@ -340,15 +281,17 @@ public class QortalATAPI extends API {
@Override
public void messageAToB(MachineState state) {
byte[] message = this.getA(state);
Account recipient = getAccountFromB(state);
byte[] message = state.getA();
byte[] publicKey = state.getB();
PublicKeyAccount recipient = new PublicKeyAccount(this.repository, publicKey);
long timestamp = this.getNextTransactionTimestamp();
byte[] reference = this.getLastReference();
BaseTransactionData baseTransactionData = new BaseTransactionData(timestamp, Group.NO_GROUP, reference, NullAccount.PUBLIC_KEY, 0L, null);
BaseTransactionData baseTransactionData = new BaseTransactionData(timestamp, Group.NO_GROUP, reference, GenesisAccount.PUBLIC_KEY, BigDecimal.ZERO, null);
ATTransactionData atTransactionData = new ATTransactionData(baseTransactionData, this.atData.getATAddress(),
recipient.getAddress(), message);
recipient.getAddress(), BigDecimal.ZERO, this.atData.getAssetId(), message);
AtTransaction atTransaction = new AtTransaction(this.repository, atTransactionData);
// Add to our transactions
@@ -360,24 +303,22 @@ public class QortalATAPI extends API {
int blockHeight = timestamp.blockHeight;
// At least one block in the future
blockHeight += Math.max(minutes / this.ciyamAtSettings.minutesPerBlock, 1);
blockHeight += (minutes / MINUTES_PER_BLOCK) + 1;
return new Timestamp(blockHeight, 0).longValue();
}
@Override
public void onFinished(long finalBalance, MachineState state) {
if (finalBalance <= 0)
return;
// Refund remaining balance (if any) to AT's creator
Account creator = this.getCreator();
long timestamp = this.getNextTransactionTimestamp();
byte[] reference = this.getLastReference();
BigDecimal amount = BigDecimal.valueOf(finalBalance, 8);
BaseTransactionData baseTransactionData = new BaseTransactionData(timestamp, Group.NO_GROUP, reference, NullAccount.PUBLIC_KEY, 0L, null);
BaseTransactionData baseTransactionData = new BaseTransactionData(timestamp, Group.NO_GROUP, reference, GenesisAccount.PUBLIC_KEY, BigDecimal.ZERO, null);
ATTransactionData atTransactionData = new ATTransactionData(baseTransactionData, this.atData.getATAddress(),
creator.getAddress(), finalBalance, this.atData.getAssetId());
creator.getAddress(), amount, this.atData.getAssetId(), new byte[0]);
AtTransaction atTransaction = new AtTransaction(this.repository, atTransactionData);
// Add to our transactions
@@ -386,7 +327,7 @@ public class QortalATAPI extends API {
@Override
public void onFatalError(MachineState state, ExecutionException e) {
LOGGER.error("AT " + this.atData.getATAddress() + " suffered fatal error: " + e.getMessage());
state.getLogger().error("AT " + this.atData.getATAddress() + " suffered fatal error: " + e.getMessage());
}
@Override
@@ -397,38 +338,47 @@ public class QortalATAPI extends API {
if (qortalFunctionCode == null)
throw new IllegalFunctionCodeException("Unknown Qortal function code 0x" + String.format("%04x", rawFunctionCode) + " encountered");
qortalFunctionCode.preExecuteCheck(paramCount, returnValueExpected, rawFunctionCode);
qortalFunctionCode.preExecuteCheck(2, true, state, rawFunctionCode);
}
@Override
public void platformSpecificPostCheckExecute(FunctionData functionData, MachineState state, short rawFunctionCode) throws ExecutionException {
QortalFunctionCode qortalFunctionCode = QortalFunctionCode.valueOf(rawFunctionCode);
if (qortalFunctionCode == null)
throw new IllegalFunctionCodeException("Unknown Qortal function code 0x" + String.format("%04x", rawFunctionCode) + " encountered");
qortalFunctionCode.execute(functionData, state, rawFunctionCode);
}
// Utility methods
/** Returns partial transaction signature, used to verify we're operating on the same transaction and not naively using block height & sequence. */
public static byte[] partialSignature(byte[] fullSignature) {
return Arrays.copyOfRange(fullSignature, 8, 32);
/** Convert part of little-endian byte[] to long */
/* package */ static long fromBytes(byte[] bytes, int start) {
return (bytes[start] & 0xffL) | (bytes[start + 1] & 0xffL) << 8 | (bytes[start + 2] & 0xffL) << 16 | (bytes[start + 3] & 0xffL) << 24
| (bytes[start + 4] & 0xffL) << 32 | (bytes[start + 5] & 0xffL) << 40 | (bytes[start + 6] & 0xffL) << 48 | (bytes[start + 7] & 0xffL) << 56;
}
/** Verify transaction's partial signature matches A2 thru A4 */
private void verifyTransaction(TransactionData transactionData, MachineState state) {
// Compare end of transaction's signature against A2 thru A4
byte[] sig = transactionData.getSignature();
/** Returns SHA2-192 digest of input - used to verify transaction signatures */
public static byte[] sha192(byte[] input) {
try {
// SHA2-192
MessageDigest sha192 = MessageDigest.getInstance("SHA-192");
return sha192.digest(input);
} catch (NoSuchAlgorithmException e) {
throw new RuntimeException("SHA-192 not available");
}
}
if (this.getA2(state) != BitTwiddling.longFromBEBytes(sig, 8) || this.getA3(state) != BitTwiddling.longFromBEBytes(sig, 16) || this.getA4(state) != BitTwiddling.longFromBEBytes(sig, 24))
/** Verify transaction's SHA2-192 hashed signature matches A2 thru A4 */
private static void verifyTransaction(TransactionData transactionData, MachineState state) {
// Compare SHA2-192 of transaction's signature against A2 thru A4
byte[] hash = sha192(transactionData.getSignature());
if (state.getA2() != fromBytes(hash, 0) || state.getA3() != fromBytes(hash, 8) || state.getA4() != fromBytes(hash, 16))
throw new IllegalStateException("Transaction signature in A no longer matches signature from repository");
}
/** Returns transaction data from repository using block height & sequence from A1, checking the transaction signatures match too */
/* package */ TransactionData getTransactionFromA(MachineState state) {
Timestamp timestamp = new Timestamp(this.getA1(state));
/* package */ TransactionData fetchTransaction(MachineState state) {
Timestamp timestamp = new Timestamp(state.getA1());
try {
TransactionData transactionData = this.repository.getTransactionRepository().fromHeightAndSequence(timestamp.blockHeight,
@@ -446,20 +396,6 @@ public class QortalATAPI extends API {
}
}
/** Returns message data from transaction. */
/*package*/ byte[] getMessageFromTransaction(TransactionData transactionData) {
switch (transactionData.getType()) {
case MESSAGE:
return ((MessageTransactionData) transactionData).getData();
case AT:
return ((ATTransactionData) transactionData).getMessage();
default:
return null;
}
}
/** Returns AT's account */
/* package */ Account getATAccount() {
return new Account(this.repository, this.atData.getATAddress());
@@ -473,17 +409,29 @@ public class QortalATAPI extends API {
/** Returns the timestamp to use for next AT Transaction */
private long getNextTransactionTimestamp() {
/*
* Use block's timestamp.
* Timestamp is block's timestamp + position in AT-Transactions list.
*
* This is OK because AT transactions are always generated locally and order is preserved in Transaction.getDataComparator().
* We need increasing timestamps to preserve transaction order and hence a correct signature-reference chain when the block is processed.
*
* As Qora blocks must share the same milliseconds component in their timestamps, this allows us to generate up to 1,000 AT-Transactions per AT without
* issue.
*
* As long as ATs are not allowed to generate that many per block, e.g. by limiting maximum steps per execution round, then we should be fine.
*/
return this.blockTimestamp;
// XXX THE ABOVE IS NO LONGER TRUE IN QORTAL!
// return this.blockTimestamp + this.transactions.size();
throw new RuntimeException("AT timestamp code not fixed!");
}
/** Returns AT account's lastReference */
/** Returns AT account's lastReference, taking newly generated ATTransactions into account */
private byte[] getLastReference() {
// Use signature from last AT Transaction we generated
if (!this.transactions.isEmpty())
return this.transactions.get(this.transactions.size() - 1).getTransactionData().getSignature();
try {
// Look up AT's account's last reference from repository
// No transactions yet, so look up AT's account's last reference from repository
Account atAccount = this.getATAccount();
return atAccount.getLastReference();
@@ -492,42 +440,4 @@ public class QortalATAPI extends API {
}
}
/**
* Returns Account (possibly PublicKeyAccount) based on value in B.
* <p>
* If first byte in B starts with either address version bytes,<br>
* and bytes 26 to 32 are zero, then use as an address, but only if valid.
* <p>
* Otherwise, assume B is a public key.
*/
private Account getAccountFromB(MachineState state) {
byte[] bBytes = this.getB(state);
if ((bBytes[0] == Crypto.ADDRESS_VERSION || bBytes[0] == Crypto.AT_ADDRESS_VERSION)
&& Arrays.mismatch(bBytes, Account.ADDRESS_LENGTH, 32, ADDRESS_PADDING, 0, ADDRESS_PADDING.length) == -1) {
// Extract only the bytes containing address
byte[] addressBytes = Arrays.copyOf(bBytes, Account.ADDRESS_LENGTH);
// If address (in byte form) is valid...
if (Crypto.isValidAddress(addressBytes))
// ...then return an Account using address (converted to Base58
return new Account(this.repository, Base58.encode(addressBytes));
}
return new PublicKeyAccount(this.repository, bBytes);
}
/* Convenience methods to allow QortalFunctionCode package-visibility access to A/B-get/set methods. */
protected byte[] getB(MachineState state) {
return super.getB(state);
}
protected void setB(MachineState state, byte[] bBytes) {
super.setB(state, bBytes);
}
protected void zeroB(MachineState state) {
super.zeroB(state);
}
}

View File

@@ -0,0 +1,26 @@
package org.qortal.at;
import org.apache.logging.log4j.LogManager;
import org.apache.logging.log4j.Logger;
public class QortalATLogger implements org.ciyam.at.LoggerInterface {
// NOTE: We're logging on behalf of org.qortal.at.AT, not ourselves!
private static final Logger LOGGER = LogManager.getLogger(AT.class);
@Override
public void error(String message) {
LOGGER.error(message);
}
@Override
public void debug(String message) {
LOGGER.debug(message);
}
@Override
public void echo(String message) {
LOGGER.info(message);
}
}

File diff suppressed because it is too large Load Diff

View File

@@ -1,24 +0,0 @@
package org.qortal.at;
import org.ciyam.at.AtLogger;
public class QortalAtLoggerFactory implements org.ciyam.at.AtLoggerFactory {
private static QortalAtLoggerFactory instance;
private QortalAtLoggerFactory() {
}
public static synchronized QortalAtLoggerFactory getInstance() {
if (instance == null)
instance = new QortalAtLoggerFactory();
return instance;
}
@Override
public AtLogger create(final Class<?> loggerName) {
return QortalAtLogger.create(loggerName);
}
}

View File

@@ -1,19 +1,15 @@
package org.qortal.at;
import java.nio.charset.StandardCharsets;
import java.util.Arrays;
import java.util.Map;
import java.util.stream.Collectors;
import org.apache.logging.log4j.LogManager;
import org.apache.logging.log4j.Logger;
import org.ciyam.at.ExecutionException;
import org.ciyam.at.FunctionData;
import org.ciyam.at.IllegalFunctionCodeException;
import org.ciyam.at.MachineState;
import org.qortal.crosschain.Bitcoin;
import org.qortal.crypto.Crypto;
import org.qortal.data.transaction.TransactionData;
import org.qortal.settings.Settings;
import org.ciyam.at.Timestamp;
/**
* Qortal-specific CIYAM-AT Functions.
@@ -23,105 +19,28 @@ import org.qortal.settings.Settings;
*/
public enum QortalFunctionCode {
/**
* Returns length of message data from transaction in A.<br>
* <tt>0x0500</tt><br>
* Returns current BTC block's "timestamp"
*/
GET_BTC_BLOCK_TIMESTAMP(0x0500, 0, true) {
@Override
protected void postCheckExecute(FunctionData functionData, MachineState state, short rawFunctionCode) throws ExecutionException {
functionData.returnValue = Timestamp.toLong(state.getAPI().getCurrentBlockHeight(), BlockchainAPI.BTC.value, 0);
}
},
/**
* <tt>0x0501</tt><br>
* If transaction has no 'message', returns -1.
* Put transaction from specific recipient after timestamp in A, or zero if none<br>
*/
GET_MESSAGE_LENGTH_FROM_TX_IN_A(0x0501, 0, true) {
PUT_TX_FROM_B_RECIPIENT_AFTER_TIMESTAMP_IN_A(0x0501, 1, false) {
@Override
protected void postCheckExecute(FunctionData functionData, MachineState state, short rawFunctionCode) throws ExecutionException {
QortalATAPI api = (QortalATAPI) state.getAPI();
Timestamp timestamp = new Timestamp(functionData.value2);
TransactionData transactionData = api.getTransactionFromA(state);
String recipient = new String(state.getB(), StandardCharsets.UTF_8);
byte[] messageData = api.getMessageFromTransaction(transactionData);
if (messageData == null)
functionData.returnValue = -1L;
else
functionData.returnValue = (long) messageData.length;
}
},
/**
* Put offset 'message' from transaction in A into B<br>
* <tt>0x0502 start-offset</tt><br>
* Copies up to 32 bytes of message data, starting at <tt>start-offset</tt> into B.<br>
* If transaction has no 'message', or <tt>start-offset</tt> out of bounds, then zero B<br>
* Example 'message' could be 256-bit shared secret
*/
PUT_PARTIAL_MESSAGE_FROM_TX_IN_A_INTO_B(0x0502, 1, false) {
@Override
protected void postCheckExecute(FunctionData functionData, MachineState state, short rawFunctionCode) throws ExecutionException {
QortalATAPI api = (QortalATAPI) state.getAPI();
// In case something goes wrong, or we don't have enough message data.
api.zeroB(state);
if (functionData.value1 < 0 || functionData.value1 > Integer.MAX_VALUE)
return;
int startOffset = functionData.value1.intValue();
TransactionData transactionData = api.getTransactionFromA(state);
byte[] messageData = api.getMessageFromTransaction(transactionData);
if (messageData == null || startOffset > messageData.length)
return;
/*
* Copy up to 32 bytes of message data into B,
* retain order but pad with zeros in lower bytes.
*
* So a 4-byte message "a b c d" would copy thusly:
* a b c d 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0
*/
int byteCount = Math.min(32, messageData.length - startOffset);
byte[] bBytes = new byte[32];
System.arraycopy(messageData, startOffset, bBytes, 0, byteCount);
api.setB(state, bBytes);
}
},
/**
* Convert address in B to 20-byte value in LSB of B1, and all of B2 & B3.<br>
* <tt>0x0510</tt>
*/
CONVERT_B_TO_PKH(0x0510, 0, false) {
@Override
protected void postCheckExecute(FunctionData functionData, MachineState state, short rawFunctionCode) throws ExecutionException {
// Needs to be 'B' sized
byte[] pkh = new byte[32];
// Copy PKH part of B to last 20 bytes
System.arraycopy(getB(state), 32 - 20 - 4, pkh, 32 - 20, 20);
setB(state, pkh);
}
},
/**
* Convert 20-byte value in LSB of B1, and all of B2 & B3 to P2SH.<br>
* <tt>0x0511</tt><br>
* P2SH stored in lower 25 bytes of B.
*/
CONVERT_B_TO_P2SH(0x0511, 0, false) {
@Override
protected void postCheckExecute(FunctionData functionData, MachineState state, short rawFunctionCode) throws ExecutionException {
byte addressPrefix = Settings.getInstance().getBitcoinNet() == Bitcoin.BitcoinNet.MAIN ? 0x05 : (byte) 0xc4;
convertAddressInB(addressPrefix, state);
}
},
/**
* Convert 20-byte value in LSB of B1, and all of B2 & B3 to Qortal address.<br>
* <tt>0x0512</tt><br>
* Qortal address stored in lower 25 bytes of B.
*/
CONVERT_B_TO_QORTAL(0x0512, 0, false) {
@Override
protected void postCheckExecute(FunctionData functionData, MachineState state, short rawFunctionCode) throws ExecutionException {
convertAddressInB(Crypto.ADDRESS_VERSION, state);
BlockchainAPI blockchainAPI = BlockchainAPI.valueOf(timestamp.blockchainId);
blockchainAPI.putTransactionFromRecipientAfterTimestampInA(recipient, timestamp, state);
}
};
@@ -129,9 +48,7 @@ public enum QortalFunctionCode {
public final int paramCount;
public final boolean returnsValue;
private static final Logger LOGGER = LogManager.getLogger(QortalFunctionCode.class);
private static final Map<Short, QortalFunctionCode> map = Arrays.stream(QortalFunctionCode.values())
private final static Map<Short, QortalFunctionCode> map = Arrays.stream(QortalFunctionCode.values())
.collect(Collectors.toMap(functionCode -> functionCode.value, functionCode -> functionCode));
private QortalFunctionCode(int value, int paramCount, boolean returnsValue) {
@@ -144,7 +61,7 @@ public enum QortalFunctionCode {
return map.get((short) value);
}
public void preExecuteCheck(int paramCount, boolean returnValueExpected, short rawFunctionCode) throws IllegalFunctionCodeException {
public void preExecuteCheck(int paramCount, boolean returnValueExpected, MachineState state, short rawFunctionCode) throws IllegalFunctionCodeException {
if (paramCount != this.paramCount)
throw new IllegalFunctionCodeException(
"Passed paramCount (" + paramCount + ") does not match function's required paramCount (" + this.paramCount + ")");
@@ -167,7 +84,7 @@ public enum QortalFunctionCode {
*/
public void execute(FunctionData functionData, MachineState state, short rawFunctionCode) throws ExecutionException {
// Check passed functionData against requirements of this function
preExecuteCheck(functionData.paramCount, functionData.returnValueExpected, rawFunctionCode);
preExecuteCheck(functionData.paramCount, functionData.returnValueExpected, state, rawFunctionCode);
if (functionData.paramCount >= 1 && functionData.value1 == null)
throw new IllegalFunctionCodeException("Passed value1 is null but function has paramCount of (" + this.paramCount + ")");
@@ -175,7 +92,7 @@ public enum QortalFunctionCode {
if (functionData.paramCount == 2 && functionData.value2 == null)
throw new IllegalFunctionCodeException("Passed value2 is null but function has paramCount of (" + this.paramCount + ")");
LOGGER.debug(() -> String.format("Function \"%s\"", this.name()));
state.getLogger().debug("Function \"" + this.name() + "\"");
postCheckExecute(functionData, state, rawFunctionCode);
}
@@ -183,29 +100,4 @@ public enum QortalFunctionCode {
/** Actually execute function */
protected abstract void postCheckExecute(FunctionData functionData, MachineState state, short rawFunctionCode) throws ExecutionException;
private static void convertAddressInB(byte addressPrefix, MachineState state) {
byte[] addressNoChecksum = new byte[1 + 20];
addressNoChecksum[0] = addressPrefix;
System.arraycopy(getB(state), 0, addressNoChecksum, 1, 20);
byte[] checksum = Crypto.doubleDigest(addressNoChecksum);
// Needs to be 'B' sized
byte[] address = new byte[32];
System.arraycopy(addressNoChecksum, 0, address, 32 - 1 - 20 - 4, addressNoChecksum.length);
System.arraycopy(checksum, 0, address, 32 - 4, 4);
setB(state, address);
}
private static byte[] getB(MachineState state) {
QortalATAPI api = (QortalATAPI) state.getAPI();
return api.getB(state);
}
private static void setB(MachineState state, byte[] bBytes) {
QortalATAPI api = (QortalATAPI) state.getAPI();
api.setB(state, bBytes);
}
}

File diff suppressed because it is too large Load Diff

View File

@@ -1,153 +0,0 @@
package org.qortal.block;
import java.io.InputStream;
import java.util.List;
import java.util.stream.Collectors;
import javax.xml.bind.JAXBContext;
import javax.xml.bind.JAXBException;
import javax.xml.bind.UnmarshalException;
import javax.xml.bind.Unmarshaller;
import javax.xml.transform.stream.StreamSource;
import org.apache.logging.log4j.LogManager;
import org.apache.logging.log4j.Logger;
import org.eclipse.persistence.jaxb.JAXBContextFactory;
import org.eclipse.persistence.jaxb.UnmarshallerProperties;
import org.qortal.data.account.AccountBalanceData;
import org.qortal.repository.DataException;
/**
* Block 212937
* <p>
* Somehow a node minted a version of block 212937 that contained one transaction:
* a PAYMENT transaction that attempted to spend more QORT than that account had as QORT balance.
* <p>
* This invalid transaction made block 212937 (rightly) invalid to several nodes,
* which refused to use that block.
* However, it seems there were no other nodes minting an alternative, valid block at that time
* and so the chain stalled for several nodes in the network.
* <p>
* Additionally, the invalid block 212937 affected all new installations, regardless of whether
* they synchronized from scratch (block 1) or used an 'official release' bootstrap.
* <p>
* After lengthy diagnosis, it was discovered that
* the invalid transaction seemed to rely on incorrect balances in a corrupted database.
* Copies of DB files containing the broken chain were also shared around, exacerbating the problem.
* <p>
* There were three options:
* <ol>
* <li>roll back the chain to last known valid block 212936 and re-mint empty blocks to current height</li>
* <li>keep existing chain, but apply database edits at block 212937 to allow current chain to be valid</li>
* <li>attempt to mint an alternative chain, retaining as many valid transactions as possible</li>
* </ol>
* <p>
* Option 1 was highly undesirable due to knock-on effects from wiping 700+ transactions, some of which
* might have affect cross-chain trades, although there were no cross-chain trade completed during
* the decision period.
* <p>
* Option 3 was essentially a slightly better version of option 1 and rejected for similar reasons.
* Attempts at option 3 also rapidly hit cumulative problems with every replacement block due to
* differing block timestamps making some transactions, and then even some blocks themselves, invalid.
* <p>
* This class is the implementation of option 2.
* <p>
* The change in account balances are relatively small, see <tt>block-212937-deltas.json</tt> resource
* for actual values. These values were obtained by exporting the <tt>AccountBalances</tt> table from
* both versions of the database with chain at block 212936, and then comparing. The values were also
* tested by syncing both databases up to block 225500, re-exporting and re-comparing.
* <p>
* The invalid block 212937 signature is: <tt>2J3GVJjv...qavh6KkQ</tt>.
* <p>
* The invalid transaction in block 212937 is:
* <p>
* <code><pre>
{
"amount" : "0.10788294",
"approvalStatus" : "NOT_REQUIRED",
"blockHeight" : 212937,
"creatorAddress" : "QLdw5uabviLJgRGkRiydAFmAtZzxHfNXSs",
"fee" : "0.00100000",
"recipient" : "QZi1mNHDbiLvsytxTgxDr9nhJe4pNZaWpw",
"reference" : "J6JukdTVuXZ3JYbHatfZzwxG2vSiZwVCPDzW5K7PsVQKRj8XZeDtqnkGCGGjaSQZ9bQMtV44ky88NnGM4YBQKU6",
"senderPublicKey" : "DBFfbD2M3uh4jPE5PaUcZVvNPfrrJzVB7seeEtBn5SPs",
"signature" : "qkitxdCEEnKt8w6wRfFixtErbXsxWE6zG2ESNhpqBdScikV1WxeA6WZTTMJVV4tCeZdBFXw3V1X5NVztv6LirWK",
"timestamp" : 1607863074904,
"txGroupId" : 0,
"type" : "PAYMENT"
}
</pre></code>
* <p>
* Account <tt>QLdw5uabviLJgRGkRiydAFmAtZzxHfNXSs</tt> attempted to spend <tt>0.10888294</tt> (including fees)
* when their QORT balance was really only <tt>0.10886665</tt>.
* <p>
* However, on the broken DB nodes, their balance
* seemed to be <tt>0.10890293</tt> which was sufficient to make the transaction valid.
*/
public final class Block212937 {
private static final Logger LOGGER = LogManager.getLogger(Block212937.class);
private static final String ACCOUNT_DELTAS_SOURCE = "block-212937-deltas.json";
private static final List<AccountBalanceData> accountDeltas = readAccountDeltas();
private Block212937() {
/* Do not instantiate */
}
@SuppressWarnings("unchecked")
private static List<AccountBalanceData> readAccountDeltas() {
Unmarshaller unmarshaller;
try {
// Create JAXB context aware of classes we need to unmarshal
JAXBContext jc = JAXBContextFactory.createContext(new Class[] {
AccountBalanceData.class
}, null);
// Create unmarshaller
unmarshaller = jc.createUnmarshaller();
// Set the unmarshaller media type to JSON
unmarshaller.setProperty(UnmarshallerProperties.MEDIA_TYPE, "application/json");
// Tell unmarshaller that there's no JSON root element in the JSON input
unmarshaller.setProperty(UnmarshallerProperties.JSON_INCLUDE_ROOT, false);
} catch (JAXBException e) {
String message = "Failed to setup unmarshaller to read block 212937 deltas";
LOGGER.error(message, e);
throw new RuntimeException(message, e);
}
ClassLoader classLoader = BlockChain.class.getClassLoader();
InputStream in = classLoader.getResourceAsStream(ACCOUNT_DELTAS_SOURCE);
StreamSource jsonSource = new StreamSource(in);
try {
// Attempt to unmarshal JSON stream to BlockChain config
return (List<AccountBalanceData>) unmarshaller.unmarshal(jsonSource, AccountBalanceData.class).getValue();
} catch (UnmarshalException e) {
String message = "Failed to parse block 212937 deltas";
LOGGER.error(message, e);
throw new RuntimeException(message, e);
} catch (JAXBException e) {
String message = "Unexpected JAXB issue while processing block 212937 deltas";
LOGGER.error(message, e);
throw new RuntimeException(message, e);
}
}
public static void processFix(Block block) throws DataException {
block.repository.getAccountRepository().modifyAssetBalances(accountDeltas);
}
public static void orphanFix(Block block) throws DataException {
// Create inverse deltas
List<AccountBalanceData> inverseDeltas = accountDeltas.stream()
.map(delta -> new AccountBalanceData(delta.getAddress(), delta.getAssetId(), 0 - delta.getBalance()))
.collect(Collectors.toList());
block.repository.getAccountRepository().modifyAssetBalances(inverseDeltas);
}
}

View File

@@ -3,6 +3,8 @@ package org.qortal.block;
import java.io.File;
import java.io.FileNotFoundException;
import java.io.InputStream;
import java.math.BigDecimal;
import java.math.MathContext;
import java.sql.SQLException;
import java.util.ArrayList;
import java.util.Collections;
@@ -32,6 +34,7 @@ import org.qortal.repository.DataException;
import org.qortal.repository.Repository;
import org.qortal.repository.RepositoryManager;
import org.qortal.settings.Settings;
import org.qortal.utils.NTP;
import org.qortal.utils.StringLongMapXmlAdapter;
/**
@@ -53,14 +56,12 @@ public class BlockChain {
/** Transaction expiry period, starting from transaction's timestamp, in milliseconds. */
private long transactionExpiryPeriod;
@XmlJavaTypeAdapter(value = org.qortal.api.AmountTypeAdapter.class)
private long unitFee;
private int maxBytesPerUnitFee;
private BigDecimal unitFee;
private BigDecimal maxBytesPerUnitFee;
private BigDecimal minFeePerByte;
/** Maximum acceptable timestamp disagreement offset in milliseconds. */
private long blockTimestampMargin;
/** Maximum block size, in bytes. */
private int maxBlockSize;
@@ -70,8 +71,15 @@ public class BlockChain {
private GenesisBlock.GenesisInfo genesisInfo;
public enum FeatureTrigger {
atFindNextTransactionFix,
newBlockSigHeight;
messageHeight, // block height when MESSAGE transactions are enabled
atHeight, // block height when CIYAM automated transactions are enabled
assetsTimestamp, // timestamp when assets (issue/transfer/payments) are enabled
votingTimestamp, // timestamp when voting is enabled
arbitraryTimestamp, // timestamp when arbitrary transactions are enabled
powfixTimestamp, // timestamp when various legacy fixes come into effect
qortalTimestamp, // timestamp when Qortal changes come into effect
newAssetPricingTimestamp, // timestamp when new asset pricing comes into effect
groupApprovalTimestamp; // timestamp when transaction approval comes into effect
}
/** Map of which blockchain features are enabled when (height/timestamp) */
@@ -87,28 +95,21 @@ public class BlockChain {
/** Block rewards by block height */
public static class RewardByHeight {
public int height;
@XmlJavaTypeAdapter(value = org.qortal.api.AmountTypeAdapter.class)
public long reward;
public BigDecimal reward;
}
private List<RewardByHeight> rewardsByHeight;
List<RewardByHeight> rewardsByHeight;
/** Share of block reward/fees by account level */
public static class AccountLevelShareBin {
public static class ShareByLevel {
public List<Integer> levels;
@XmlJavaTypeAdapter(value = org.qortal.api.AmountTypeAdapter.class)
public long share;
public BigDecimal share;
}
private List<AccountLevelShareBin> sharesByLevel;
/** Generated lookup of share-bin by account level */
private AccountLevelShareBin[] shareBinsByLevel;
List<ShareByLevel> sharesByLevel;
/** Share of block reward/fees to legacy QORA coin holders */
@XmlJavaTypeAdapter(value = org.qortal.api.AmountTypeAdapter.class)
private Long qoraHoldersShare;
BigDecimal qoraHoldersShare;
/** How many legacy QORA per 1 QORT of block reward. */
@XmlJavaTypeAdapter(value = org.qortal.api.AmountTypeAdapter.class)
private Long qoraPerQortReward;
BigDecimal qoraPerQortReward;
/**
* Number of minted blocks required to reach next level from previous.
@@ -119,7 +120,7 @@ public class BlockChain {
* Example: if <tt>blocksNeededByLevel[3]</tt> is 200,<br>
* then level 3 accounts need to mint 200 blocks to reach level 4.
*/
private List<Integer> blocksNeededByLevel;
List<Integer> blocksNeededByLevel;
/**
* Cumulative number of minted blocks required to reach next level from scratch.
@@ -133,7 +134,7 @@ public class BlockChain {
* <p>
* Should NOT be present in blockchain config file!
*/
private List<Integer> cumulativeBlocksByLevel;
List<Integer> cumulativeBlocksByLevel;
/** Block times by block height */
public static class BlockTimingByHeight {
@@ -142,7 +143,7 @@ public class BlockChain {
public long deviation; // ms
public double power;
}
private List<BlockTimingByHeight> blockTimingsByHeight;
List<BlockTimingByHeight> blockTimingsByHeight;
private int minAccountLevelToMint = 1;
private int minAccountLevelToRewardShare;
@@ -154,19 +155,6 @@ public class BlockChain {
/** Maximum time to retain online account signatures (ms) for block validity checks, to allow for clock variance. */
private long onlineAccountSignaturesMaxLifetime;
/** Settings relating to CIYAM AT feature. */
public static class CiyamAtSettings {
/** Fee per step/op-code executed. */
@XmlJavaTypeAdapter(value = org.qortal.api.AmountTypeAdapter.class)
public long feePerStep;
/** Maximum number of steps per execution round, before AT is forced to sleep until next block. */
public int maxStepsPerRound;
/** How many steps for calling a function. */
public int stepsPerFunctionCall;
/** Roughly how many minutes per block. */
public int minutesPerBlock;
}
private CiyamAtSettings ciyamAtSettings;
// Constructors, etc.
@@ -282,14 +270,18 @@ public class BlockChain {
return this.isTestChain;
}
public long getUnitFee() {
public BigDecimal getUnitFee() {
return this.unitFee;
}
public int getMaxBytesPerUnitFee() {
public BigDecimal getMaxBytesPerUnitFee() {
return this.maxBytesPerUnitFee;
}
public BigDecimal getMinFeePerByte() {
return this.minFeePerByte;
}
public long getTransactionExpiryPeriod() {
return this.transactionExpiryPeriod;
}
@@ -319,14 +311,10 @@ public class BlockChain {
return this.rewardsByHeight;
}
public List<AccountLevelShareBin> getAccountLevelShareBins() {
public List<ShareByLevel> getBlockSharesByLevel() {
return this.sharesByLevel;
}
public AccountLevelShareBin[] getShareBinsByAccountLevel() {
return this.shareBinsByLevel;
}
public List<Integer> getBlocksNeededByLevel() {
return this.blocksNeededByLevel;
}
@@ -335,11 +323,11 @@ public class BlockChain {
return this.cumulativeBlocksByLevel;
}
public long getQoraHoldersShare() {
public BigDecimal getQoraHoldersShare() {
return this.qoraHoldersShare;
}
public long getQoraPerQortReward() {
public BigDecimal getQoraPerQortReward() {
return this.qoraPerQortReward;
}
@@ -367,29 +355,53 @@ public class BlockChain {
return this.onlineAccountSignaturesMaxLifetime;
}
public CiyamAtSettings getCiyamAtSettings() {
return this.ciyamAtSettings;
}
// Convenience methods for specific blockchain feature triggers
public int getAtFindNextTransactionFixHeight() {
return this.featureTriggers.get(FeatureTrigger.atFindNextTransactionFix.name()).intValue();
public long getMessageReleaseHeight() {
return featureTriggers.get("messageHeight");
}
public int getNewBlockSigHeight() {
return this.featureTriggers.get(FeatureTrigger.newBlockSigHeight.name()).intValue();
public long getATReleaseHeight() {
return featureTriggers.get("atHeight");
}
public long getPowFixReleaseTimestamp() {
return featureTriggers.get("powfixTimestamp");
}
public long getAssetsReleaseTimestamp() {
return featureTriggers.get("assetsTimestamp");
}
public long getVotingReleaseTimestamp() {
return featureTriggers.get("votingTimestamp");
}
public long getArbitraryReleaseTimestamp() {
return featureTriggers.get("arbitraryTimestamp");
}
public long getQortalTimestamp() {
return featureTriggers.get("qortalTimestamp");
}
public long getNewAssetPricingTimestamp() {
return featureTriggers.get("newAssetPricingTimestamp");
}
public long getGroupApprovalTimestamp() {
return featureTriggers.get("groupApprovalTimestamp");
}
// More complex getters for aspects that change by height or timestamp
public long getRewardAtHeight(int ourHeight) {
public BigDecimal getRewardAtHeight(int ourHeight) {
// Scan through for reward at our height
for (int i = rewardsByHeight.size() - 1; i >= 0; --i)
if (rewardsByHeight.get(i).height <= ourHeight)
return rewardsByHeight.get(i).reward;
return 0;
return null;
}
public BlockTimingByHeight getBlockTimingByHeight(int ourHeight) {
@@ -438,9 +450,6 @@ public class BlockChain {
if (this.founderEffectiveMintingLevel <= 0)
Settings.throwValidationError("Invalid/missing \"founderEffectiveMintingLevel\" in blockchain config");
if (this.ciyamAtSettings == null)
Settings.throwValidationError("No \"ciyamAtSettings\" entry found in blockchain config");
if (this.featureTriggers == null)
Settings.throwValidationError("No \"featureTriggers\" entry found in blockchain config");
@@ -448,20 +457,15 @@ public class BlockChain {
for (FeatureTrigger featureTrigger : FeatureTrigger.values())
if (!this.featureTriggers.containsKey(featureTrigger.name()))
Settings.throwValidationError(String.format("Missing feature trigger \"%s\" in blockchain config", featureTrigger.name()));
// Check block reward share bounds
long totalShare = this.qoraHoldersShare;
// Add share percents for account-level-based rewards
for (AccountLevelShareBin accountLevelShareBin : this.sharesByLevel)
totalShare += accountLevelShareBin.share;
if (totalShare < 0 || totalShare > 1_00000000L)
Settings.throwValidationError("Total non-founder share out of bounds (0<x<1e8)");
}
/** Minor normalization, cached value generation, etc. */
private void fixUp() {
// Calculate cumulative blocks required for each level
this.maxBytesPerUnitFee = this.maxBytesPerUnitFee.setScale(8);
this.unitFee = this.unitFee.setScale(8);
this.minFeePerByte = this.unitFee.divide(this.maxBytesPerUnitFee, MathContext.DECIMAL32);
// Pre-calculate cumulative blocks required for each level
int cumulativeBlocks = 0;
this.cumulativeBlocksByLevel = new ArrayList<>(this.blocksNeededByLevel.size() + 1);
for (int level = 0; level <= this.blocksNeededByLevel.size(); ++level) {
@@ -471,17 +475,6 @@ public class BlockChain {
cumulativeBlocks += this.blocksNeededByLevel.get(level);
}
// Generate lookup-array for account-level share bins
AccountLevelShareBin lastAccountLevelShareBin = this.sharesByLevel.get(this.sharesByLevel.size() - 1);
final int lastLevel = lastAccountLevelShareBin.levels.get(lastAccountLevelShareBin.levels.size() - 1);
this.shareBinsByLevel = new AccountLevelShareBin[lastLevel];
for (AccountLevelShareBin accountLevelShareBin : this.sharesByLevel)
for (int level : accountLevelShareBin.levels)
// level 1 stored at index 0, level 2 stored at index 1, etc.
// level 0 not allowed
this.shareBinsByLevel[level - 1] = accountLevelShareBin;
// Convert collections to unmodifiable form
this.rewardsByHeight = Collections.unmodifiableList(this.rewardsByHeight);
this.sharesByLevel = Collections.unmodifiableList(this.sharesByLevel);
@@ -491,7 +484,7 @@ public class BlockChain {
}
/**
* Some sort of start-up/initialization/checking method.
* Some sort start-up/initialization/checking method.
*
* @throws SQLException
*/
@@ -501,11 +494,7 @@ public class BlockChain {
rebuildBlockchain();
try (final Repository repository = RepositoryManager.getRepository()) {
repository.checkConsistency();
int startHeight = Math.max(repository.getBlockRepository().getBlockchainHeight() - 1440, 1);
BlockData detachedBlockData = repository.getBlockRepository().getDetachedBlockSignature(startHeight);
BlockData detachedBlockData = repository.getBlockRepository().getDetachedBlockSignature();
if (detachedBlockData != null) {
LOGGER.error(String.format("Block %d's reference does not match any block's signature", detachedBlockData.getHeight()));
@@ -523,7 +512,7 @@ public class BlockChain {
}
}
private static boolean isGenesisBlockValid() {
private static boolean isGenesisBlockValid() throws DataException {
try (final Repository repository = RepositoryManager.getRepository()) {
BlockRepository blockRepository = repository.getBlockRepository();
@@ -536,15 +525,12 @@ public class BlockChain {
return false;
return GenesisBlock.isGenesisBlock(blockData);
} catch (DataException e) {
return false;
}
}
private static void rebuildBlockchain() throws DataException {
// (Re)build repository
if (!RepositoryManager.wasPristineAtOpen())
RepositoryManager.rebuild();
RepositoryManager.rebuild();
try (final Repository repository = RepositoryManager.getRepository()) {
GenesisBlock genesisBlock = GenesisBlock.getInstance(repository);
@@ -566,24 +552,18 @@ public class BlockChain {
try {
try (final Repository repository = RepositoryManager.getRepository()) {
int height = repository.getBlockRepository().getBlockchainHeight();
BlockData orphanBlockData = repository.getBlockRepository().fromHeight(height);
while (height > targetHeight) {
for (int height = repository.getBlockRepository().getBlockchainHeight(); height > targetHeight; --height) {
LOGGER.info(String.format("Forcably orphaning block %d", height));
Block block = new Block(repository, orphanBlockData);
BlockData blockData = repository.getBlockRepository().fromHeight(height);
Block block = new Block(repository, blockData);
block.orphan();
repository.saveChanges();
--height;
orphanBlockData = repository.getBlockRepository().fromHeight(height);
repository.discardChanges(); // clear transaction status to prevent deadlocks
Controller.getInstance().onOrphanedBlock(orphanBlockData);
}
BlockData lastBlockData = repository.getBlockRepository().getLastBlock();
Controller.getInstance().setChainTip(lastBlockData);
return true;
}
} finally {
@@ -591,4 +571,33 @@ public class BlockChain {
}
}
public static void trimOldOnlineAccountsSignatures() {
final Long now = NTP.getTime();
if (now == null)
return;
ReentrantLock blockchainLock = Controller.getInstance().getBlockchainLock();
if (!blockchainLock.tryLock())
// Too busy to trim right now, try again later
return;
try {
try (final Repository repository = RepositoryManager.tryRepository()) {
if (repository == null)
return;
int numBlocksTrimmed = repository.getBlockRepository().trimOldOnlineAccountsSignatures(now - BlockChain.getInstance().getOnlineAccountSignaturesMaxLifetime());
if (numBlocksTrimmed > 0)
LOGGER.debug(String.format("Trimmed old online accounts signatures from %d block%s", numBlocksTrimmed, (numBlocksTrimmed != 1 ? "s" : "")));
repository.saveChanges();
} catch (DataException e) {
LOGGER.warn(String.format("Repository issue trying to trim old online accounts signatures: %s", e.getMessage()));
}
} finally {
blockchainLock.unlock();
}
}
}

Some files were not shown because too many files have changed in this diff Show More