forked from Qortal/qortal
Compare commits
14 Commits
Author | SHA1 | Date | |
---|---|---|---|
|
2df045396d | ||
|
6c182a3567 | ||
|
340d6dfc8d | ||
|
eb27b0d3e2 | ||
|
7377893050 | ||
|
fb2c2b1d09 | ||
|
6f2dd6c8d0 | ||
|
4cc0e7845f | ||
|
275146fb55 | ||
|
d81729d9f7 | ||
|
e74a249388 | ||
|
d8c5e557d8 | ||
|
984e8b5227 | ||
|
469bf2a63e |
3
WindowsInstaller/Install Files/AppData/settings.json
Executable file
3
WindowsInstaller/Install Files/AppData/settings.json
Executable file
@@ -0,0 +1,3 @@
|
||||
{
|
||||
"apiDocumentationEnabled": true
|
||||
}
|
70
WindowsInstaller/Install Files/log4j2.properties
Executable file
70
WindowsInstaller/Install Files/log4j2.properties
Executable file
@@ -0,0 +1,70 @@
|
||||
rootLogger.level = info
|
||||
# On Windows, uncomment next line to set dirname:
|
||||
# property.dirname = ${sys:user.home}\\AppData\\Local\\qortal\\
|
||||
property.filename = ${sys:log4j2.filenameTemplate:-log.txt}
|
||||
|
||||
rootLogger.appenderRef.console.ref = stdout
|
||||
rootLogger.appenderRef.rolling.ref = FILE
|
||||
|
||||
# Suppress extraneous bitcoinj library output
|
||||
logger.bitcoinj.name = org.bitcoinj
|
||||
logger.bitcoinj.level = error
|
||||
|
||||
# Override HSQLDB logging level to "warn" as too much is logged at "info"
|
||||
logger.hsqldb.name = hsqldb.db
|
||||
logger.hsqldb.level = warn
|
||||
|
||||
# Support optional, per-session HSQLDB debugging
|
||||
logger.hsqldbRepository.name = org.qortal.repository.hsqldb
|
||||
logger.hsqldbRepository.level = debug
|
||||
|
||||
# Suppress extraneous Jersey warning
|
||||
logger.jerseyInject.name = org.glassfish.jersey.internal.inject.Providers
|
||||
logger.jerseyInject.level = off
|
||||
|
||||
# Suppress extraneous Jersey EOF 'errors' (actually remote peers disconnecting early)
|
||||
logger.jerseyEOF.name = org.glassfish.jersey.server.internal
|
||||
logger.jerseyEOF.level = off
|
||||
|
||||
# Suppress extraneous Jetty entries
|
||||
# 2019-02-14 11:46:27 INFO ContextHandler:851 - Started o.e.j.s.ServletContextHandler@6949e948{/,null,AVAILABLE}
|
||||
# 2019-02-14 11:46:27 INFO AbstractConnector:289 - Started ServerConnector@50ad322b{HTTP/1.1,[http/1.1]}{0.0.0.0:9085}
|
||||
# 2019-02-14 11:46:27 INFO Server:374 - jetty-9.4.11.v20180605; built: 2018-06-05T18:24:03.829Z; git: d5fc0523cfa96bfebfbda19606cad384d772f04c; jvm 1.8.0_181-b13
|
||||
# 2019-02-14 11:46:27 INFO Server:411 - Started @2539ms
|
||||
logger.jetty.name = org.eclipse.jetty
|
||||
logger.jetty.level = warn
|
||||
# Even more extraneous Jetty output
|
||||
# 2019-01-26 02:18:10 WARN ResourceService:718 - java.util.concurrent.TimeoutException: Idle timeout expired: 30000/30000 ms
|
||||
logger.jettyRS.name = org.eclipse.jetty.server.ResourceService
|
||||
logger.jettyRS.level = error
|
||||
|
||||
# Suppress extraneous slf4j entries
|
||||
# 2019-02-14 11:46:27 INFO log:193 - Logging initialized @1636ms to org.eclipse.jetty.util.log.Slf4jLog
|
||||
logger.slf4j.name = org.slf4j
|
||||
logger.slf4j.level = warn
|
||||
|
||||
# Suppress extraneous Reflections entry
|
||||
# 2019-02-27 10:45:25 WARN Reflections:179 - given scan urls are empty. set urls in the configuration
|
||||
logger.orgReflections.name = org.reflections.Reflections
|
||||
logger.orgReflections.level = off
|
||||
logger.sunReflections.name = sun.reflect.Reflection
|
||||
logger.sunReflections.level = off
|
||||
|
||||
appender.console.type = Console
|
||||
appender.console.name = stdout
|
||||
appender.console.layout.type = PatternLayout
|
||||
appender.console.layout.pattern = %d{yyyy-MM-dd HH:mm:ss} %-5p %c{1}:%L - %m%n
|
||||
appender.console.filter.threshold.type = ThresholdFilter
|
||||
appender.console.filter.threshold.level = error
|
||||
|
||||
appender.rolling.type = RollingFile
|
||||
appender.rolling.name = FILE
|
||||
appender.rolling.layout.type = PatternLayout
|
||||
appender.rolling.layout.pattern = %d{yyyy-MM-dd HH:mm:ss} %-5p %c{1}:%L - %m%n
|
||||
appender.rolling.filePattern = ${dirname:-}${filename}.%i
|
||||
appender.rolling.policy.type = SizeBasedTriggeringPolicy
|
||||
appender.rolling.policy.size = 4MB
|
||||
# Set the immediate flush to true (default)
|
||||
# appender.rolling.immediateFlush = true
|
||||
# Set the append to true (default), should not overwrite
|
||||
# appender.rolling.append=true
|
33
WindowsInstaller/Install Files/ntpcfg.bat
Executable file
33
WindowsInstaller/Install Files/ntpcfg.bat
Executable file
@@ -0,0 +1,33 @@
|
||||
@echo off
|
||||
|
||||
:: BatchGotAdmin
|
||||
:-------------------------------------
|
||||
REM --> Check for permissions
|
||||
>nul 2>&1 "%SYSTEMROOT%\system32\cacls.exe" "%SYSTEMROOT%\system32\config\system"
|
||||
|
||||
REM --> If error flag set, we do not have admin.
|
||||
if '%errorlevel%' NEQ '0' (
|
||||
echo Requesting administrative privileges...
|
||||
goto UACPrompt
|
||||
) else ( goto gotAdmin )
|
||||
|
||||
:UACPrompt
|
||||
echo Set UAC = CreateObject^("Shell.Application"^) > "%temp%\getadmin.vbs"
|
||||
echo UAC.ShellExecute "%~s0", "", "", "runas", 1 >> "%temp%\getadmin.vbs"
|
||||
|
||||
"%temp%\getadmin.vbs"
|
||||
exit /B
|
||||
|
||||
:gotAdmin
|
||||
if exist "%temp%\getadmin.vbs" ( del "%temp%\getadmin.vbs" )
|
||||
pushd "%CD%"
|
||||
CD /D "%~dp0"
|
||||
:--------------------------------------
|
||||
|
||||
net stop "Windows Time"
|
||||
|
||||
w32tm /config "/manualpeerlist:pool.ntp.org 0.pool.ntp.org 1.pool.ntp.org 2.pool.ntp.org 3.pool.ntp.org cn.pool.ntp.org 0.cn.pool.ntp.org 1.cn.pool.ntp.org 2.cn.pool.ntp.org 3.cn.pool.ntp.org"
|
||||
|
||||
net start "Windows Time"
|
||||
|
||||
sc config w32time start= auto
|
BIN
WindowsInstaller/Install Files/qortal.jar
Executable file
BIN
WindowsInstaller/Install Files/qortal.jar
Executable file
Binary file not shown.
1268
WindowsInstaller/Qortal.aip
Executable file
1268
WindowsInstaller/Qortal.aip
Executable file
File diff suppressed because it is too large
Load Diff
26
WindowsInstaller/README.md
Normal file
26
WindowsInstaller/README.md
Normal file
@@ -0,0 +1,26 @@
|
||||
# Windows installer
|
||||
|
||||
## Prerequisites
|
||||
|
||||
* AdvancedInstaller v16 or better, and enterprise licence if translations are required
|
||||
* Installed AdoptOpenJDK v11 64bit, full JDK *not* JRE
|
||||
|
||||
## General build instructions
|
||||
|
||||
If this is your first time opening the `qortal.aip` file then you might need to adjust
|
||||
configured paths, or create a dummy `D:` drive with the expected layout.
|
||||
|
||||
Typical build procedure:
|
||||
|
||||
* Overwrite the `qortal.jar` file in `Install-Files\`
|
||||
* Open AdvancedInstaller with qortal.aip file
|
||||
* If releasing a new version, change version number in:
|
||||
+ "Product Information" side menu
|
||||
+ "Product Details" side menu entry
|
||||
+ "Product Details" tab in "Product Details" pane
|
||||
+ "Product Version" entry box
|
||||
* Click away to a different side menu entry, e.g. "Resources" -> "Files and Folders"
|
||||
* You should be prompted whether to generate a new product key, click "Generate New"
|
||||
* Click "Build" button
|
||||
* New EXE should be generated in `Qortal-SetupFiles\` folder with correct version number
|
||||
|
97
WindowsInstaller/dictionary.ail
Executable file
97
WindowsInstaller/dictionary.ail
Executable file
@@ -0,0 +1,97 @@
|
||||
<?xml version="1.0" encoding="UTF-8" standalone="yes"?>
|
||||
<DICTIONARY type="multilanguage">
|
||||
<!-- Control table -->
|
||||
<ENTRY id="Control.Text.CustomizeDataPathDlg#Description">
|
||||
<STRING lang="en" value="Do you want to store the blockchain, and other data, in a specific folder?"/>
|
||||
<STRING lang="zh" value="你想把区块链数据存放在一个特定的文件夹吗?"/>
|
||||
<STRING lang="zh_TW" value="你想把区块链数据存放在一个特定的文件夹吗?"/>
|
||||
</ENTRY>
|
||||
<ENTRY id="Control.Text.CustomizeDataPathDlg#Text">
|
||||
<STRING lang="en" value="Select one of the options below, then click "Next"."/>
|
||||
<STRING lang="zh" value="请选择,然后“下一步”"/>
|
||||
<STRING lang="zh_TW" value="请选择,然后“下一步”"/>
|
||||
</ENTRY>
|
||||
<ENTRY id="Control.Text.CustomizeDataPathDlg#Title">
|
||||
<STRING lang="en" value="Choose Custom Data Storage Folder?"/>
|
||||
<STRING lang="zh" value="选择数据保存的文件夹?"/>
|
||||
<STRING lang="zh_TW" value="选择数据保存的文件夹?"/>
|
||||
</ENTRY>
|
||||
<ENTRY id="Control.Text.CustomizeDbDlg#Description">
|
||||
<STRING lang="en" value="Do you want to store the blockchain, and other data, in a specific folder?"/>
|
||||
<STRING lang="zh" value="Do you want to store the blockchain, and other data, in a specific folder?"/>
|
||||
<STRING lang="zh_TW" value="Do you want to store the blockchain, and other data, in a specific folder?"/>
|
||||
</ENTRY>
|
||||
<ENTRY id="Control.Text.CustomizeDbDlg#Title">
|
||||
<STRING lang="en" value="Choose Custom Data Storage Folder?"/>
|
||||
<STRING lang="zh" value="Choose Custom Data Storage Folder?"/>
|
||||
<STRING lang="zh_TW" value="Choose Custom Data Storage Folder?"/>
|
||||
</ENTRY>
|
||||
<ENTRY id="Control.Text.DataFolderDlg#Description">
|
||||
<STRING lang="en" value="This is the folder where the blockchain, and other data, will be stored."/>
|
||||
<STRING lang="zh" value="这里是区块链及其它数据存放的文件夹"/>
|
||||
<STRING lang="zh_TW" value="这里是区块链及其它数据存放的文件夹"/>
|
||||
</ENTRY>
|
||||
<ENTRY id="Control.Text.DataFolderDlg#Text">
|
||||
<STRING lang="en" value="To store data in this folder, click "[Text_Next]". To store data in a different folder, enter it below or click "Browse"."/>
|
||||
<STRING lang="zh" value="如果存放在这个文件夹,点 “下一步”。如果存放在其它位置,请选择“浏览”。"/>
|
||||
<STRING lang="zh_TW" value="如果存放在这个文件夹,点 “下一步”。如果存放在其它位置,请选择“浏览”。"/>
|
||||
</ENTRY>
|
||||
<ENTRY id="Control.Text.DataFolderDlg#Title">
|
||||
<STRING lang="en" value="Select Data Storage Folder"/>
|
||||
<STRING lang="zh" value="请选择文件存储地方"/>
|
||||
<STRING lang="zh_TW" value="请选择文件存储地方"/>
|
||||
</ENTRY>
|
||||
<ENTRY id="Control.Text.DbFolderDlg#Description">
|
||||
<STRING lang="en" value="This is the folder where the blockchain, and other data, will be stored."/>
|
||||
<STRING lang="zh" value="This is the folder where the blockchain, and other data, will be stored."/>
|
||||
<STRING lang="zh_TW" value="This is the folder where the blockchain, and other data, will be stored."/>
|
||||
</ENTRY>
|
||||
<ENTRY id="Control.Text.DbFolderDlg#Title">
|
||||
<STRING lang="en" value="Select Data Storage Folder"/>
|
||||
<STRING lang="zh" value="请选择文件存储地方"/>
|
||||
<STRING lang="zh_TW" value="请选择文件存储地方"/>
|
||||
</ENTRY>
|
||||
<ENTRY id="Control.Text.NTPDialog#Description">
|
||||
<STRING lang="en" value="Reconfigure Windows for more accurate time?"/>
|
||||
<STRING lang="zh" value="重新配置Windows以获得更准确的时间?"/>
|
||||
</ENTRY>
|
||||
<ENTRY id="Control.Text.NTPDialog#Text_1">
|
||||
<STRING lang="en" value="An accurate Windows clock is required to connect to the [ProductName] network and make transactions."/>
|
||||
<STRING lang="zh" value="需要准确的Windows时钟才能连接到[ProductName]网络并进行交易。"/>
|
||||
</ENTRY>
|
||||
<ENTRY id="Control.Text.NTPDialog#Text_2">
|
||||
<STRING lang="en" value="Select one of the options below, then click "Next"."/>
|
||||
<STRING lang="zh" value="请选择,然后“下一步”"/>
|
||||
</ENTRY>
|
||||
<ENTRY id="Control.Text.NTPDialog#Text_3">
|
||||
<STRING lang="en" value="Your computer's clock needs to be accurate to within 0.5 seconds."/>
|
||||
<STRING lang="zh" value="您的计算机时钟需要准确到0.5秒内。"/>
|
||||
</ENTRY>
|
||||
<ENTRY id="Control.Text.NTPDialog#Title">
|
||||
<STRING lang="en" value="Windows clock accuracy"/>
|
||||
<STRING lang="zh" value="Windows 时钟精度"/>
|
||||
</ENTRY>
|
||||
<ENTRY id="Control.Text.VerifyRemoveDlg#RemoveBlockchainCheckbox">
|
||||
<STRING lang="en" value="Remove downloaded blockchain and other data"/>
|
||||
<STRING lang="zh" value="删除您下载的区块链"/>
|
||||
</ENTRY>
|
||||
<!-- RadioButton table -->
|
||||
<ENTRY id="RadioButton.Text.CUSTOM_DB_BOOL#choose">
|
||||
<STRING lang="en" value="Choose custom data storage folder..."/>
|
||||
<STRING lang="zh" value="选择特定的文件夹存储"/>
|
||||
<STRING lang="zh_TW" value="选择特定的文件夹存储"/>
|
||||
</ENTRY>
|
||||
<ENTRY id="RadioButton.Text.CUSTOM_DB_BOOL#default">
|
||||
<STRING lang="en" value="Use default location "/>
|
||||
<STRING lang="zh" value="使用默认存储地点"/>
|
||||
<STRING lang="zh_TW" value="使用默认存储地点"/>
|
||||
</ENTRY>
|
||||
<ENTRY id="RadioButton.Text.RECONFIG_NTP#1">
|
||||
<STRING lang="en" value="Yes, configure Windows to use internet time servers (Recommended)"/>
|
||||
<STRING lang="zh" value="是,将Windows配置为使用多个Internet时间服务器 (推荐的)"/>
|
||||
</ENTRY>
|
||||
<ENTRY id="RadioButton.Text.RECONFIG_NTP#2">
|
||||
<STRING lang="en" value="No, I will manage clock accuracy myself"/>
|
||||
<STRING lang="zh" value="不,我会自己管理时钟精度。"/>
|
||||
</ENTRY>
|
||||
</DICTIONARY>
|
BIN
WindowsInstaller/qortal.ico
Executable file
BIN
WindowsInstaller/qortal.ico
Executable file
Binary file not shown.
After Width: | Height: | Size: 250 KiB |
4
pom.xml
4
pom.xml
@@ -3,7 +3,7 @@
|
||||
<modelVersion>4.0.0</modelVersion>
|
||||
<groupId>org.qortal</groupId>
|
||||
<artifactId>qortal</artifactId>
|
||||
<version>1.2.0</version>
|
||||
<version>1.2.3</version>
|
||||
<packaging>jar</packaging>
|
||||
<properties>
|
||||
<bitcoinj.version>0.15.5</bitcoinj.version>
|
||||
@@ -17,7 +17,7 @@
|
||||
<hsqldb.version>2.5.0-fixed</hsqldb.version>
|
||||
<hsqldb-sqltool.version>2.5.0</hsqldb-sqltool.version>
|
||||
<jersey.version>2.29.1</jersey.version>
|
||||
<jetty.version>9.4.22.v20191022</jetty.version>
|
||||
<jetty.version>9.4.29.v20200521</jetty.version>
|
||||
<log4j.version>2.12.1</log4j.version>
|
||||
<project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
|
||||
<slf4j.version>1.7.12</slf4j.version>
|
||||
|
@@ -3,6 +3,7 @@ package org.qortal.api;
|
||||
import java.io.IOException;
|
||||
|
||||
import javax.servlet.RequestDispatcher;
|
||||
import javax.servlet.ServletException;
|
||||
import javax.servlet.http.HttpServletRequest;
|
||||
import javax.servlet.http.HttpServletResponse;
|
||||
|
||||
@@ -17,7 +18,7 @@ public class ApiErrorHandler extends ErrorHandler {
|
||||
private static final Logger LOGGER = LogManager.getLogger(ApiErrorHandler.class);
|
||||
|
||||
@Override
|
||||
public void handle(String target, Request baseRequest, HttpServletRequest request, HttpServletResponse response) throws IOException {
|
||||
public void handle(String target, Request baseRequest, HttpServletRequest request, HttpServletResponse response) throws IOException, ServletException {
|
||||
if (Settings.getInstance().isApiLoggingEnabled()) {
|
||||
String requestURI = request.getRequestURI();
|
||||
|
||||
|
@@ -18,9 +18,9 @@ import org.eclipse.jetty.http.HttpVersion;
|
||||
import org.eclipse.jetty.rewrite.handler.RedirectPatternRule;
|
||||
import org.eclipse.jetty.rewrite.handler.RewriteHandler;
|
||||
import org.eclipse.jetty.server.CustomRequestLog;
|
||||
import org.eclipse.jetty.server.DetectorConnectionFactory;
|
||||
import org.eclipse.jetty.server.HttpConfiguration;
|
||||
import org.eclipse.jetty.server.HttpConnectionFactory;
|
||||
import org.eclipse.jetty.server.OptionalSslConnectionFactory;
|
||||
import org.eclipse.jetty.server.RequestLog;
|
||||
import org.eclipse.jetty.server.RequestLogWriter;
|
||||
import org.eclipse.jetty.server.SecureRequestCustomizer;
|
||||
@@ -113,8 +113,7 @@ public class ApiService {
|
||||
SslConnectionFactory sslConnectionFactory = new SslConnectionFactory(sslContextFactory, HttpVersion.HTTP_1_1.asString());
|
||||
|
||||
ServerConnector portUnifiedConnector = new ServerConnector(this.server,
|
||||
new OptionalSslConnectionFactory(sslConnectionFactory, HttpVersion.HTTP_1_1.asString()),
|
||||
sslConnectionFactory,
|
||||
new DetectorConnectionFactory(sslConnectionFactory),
|
||||
httpConnectionFactory);
|
||||
portUnifiedConnector.setHost(Settings.getInstance().getBindAddress());
|
||||
portUnifiedConnector.setPort(Settings.getInstance().getApiPort());
|
||||
|
71
src/main/java/org/qortal/api/model/BlockInfo.java
Normal file
71
src/main/java/org/qortal/api/model/BlockInfo.java
Normal file
@@ -0,0 +1,71 @@
|
||||
package org.qortal.api.model;
|
||||
|
||||
import javax.xml.bind.annotation.XmlAccessType;
|
||||
import javax.xml.bind.annotation.XmlAccessorType;
|
||||
|
||||
import org.qortal.data.account.RewardShareData;
|
||||
import org.qortal.data.block.BlockData;
|
||||
import org.qortal.repository.DataException;
|
||||
import org.qortal.repository.Repository;
|
||||
import org.qortal.repository.RepositoryManager;
|
||||
|
||||
@XmlAccessorType(XmlAccessType.FIELD)
|
||||
public class BlockInfo {
|
||||
|
||||
private byte[] signature;
|
||||
private int height;
|
||||
private long timestamp;
|
||||
private int transactionCount;
|
||||
private String minterAddress;
|
||||
|
||||
protected BlockInfo() {
|
||||
/* For JAXB */
|
||||
}
|
||||
|
||||
public BlockInfo(byte[] signature, int height, long timestamp, int transactionCount, String minterAddress) {
|
||||
this.signature = signature;
|
||||
this.height = height;
|
||||
this.timestamp = timestamp;
|
||||
this.transactionCount = transactionCount;
|
||||
this.minterAddress = minterAddress;
|
||||
}
|
||||
|
||||
public BlockInfo(BlockData blockData) {
|
||||
// Convert BlockData to BlockInfo, using additional data
|
||||
this.minterAddress = "unknown?";
|
||||
|
||||
try (final Repository repository = RepositoryManager.getRepository()) {
|
||||
RewardShareData rewardShareData = repository.getAccountRepository().getRewardShare(blockData.getMinterPublicKey());
|
||||
if (rewardShareData != null)
|
||||
this.minterAddress = rewardShareData.getMintingAccount();
|
||||
} catch (DataException e) {
|
||||
// We'll carry on with placeholder minterAddress then...
|
||||
}
|
||||
|
||||
this.signature = blockData.getSignature();
|
||||
this.height = blockData.getHeight();
|
||||
this.timestamp = blockData.getTimestamp();
|
||||
this.transactionCount = blockData.getTransactionCount();
|
||||
}
|
||||
|
||||
public byte[] getSignature() {
|
||||
return this.signature;
|
||||
}
|
||||
|
||||
public int getHeight() {
|
||||
return this.height;
|
||||
}
|
||||
|
||||
public long getTimestamp() {
|
||||
return this.timestamp;
|
||||
}
|
||||
|
||||
public int getTransactionCount() {
|
||||
return this.transactionCount;
|
||||
}
|
||||
|
||||
public String getMinterAddress() {
|
||||
return this.minterAddress;
|
||||
}
|
||||
|
||||
}
|
@@ -302,13 +302,13 @@ public class AdminResource {
|
||||
@DELETE
|
||||
@Path("/mintingaccounts")
|
||||
@Operation(
|
||||
summary = "Remove account/reward-share from use by BlockMinter, using private key",
|
||||
summary = "Remove account/reward-share from use by BlockMinter, using public or private key",
|
||||
requestBody = @RequestBody(
|
||||
required = true,
|
||||
content = @Content(
|
||||
mediaType = MediaType.TEXT_PLAIN,
|
||||
schema = @Schema(
|
||||
type = "string", example = "private key"
|
||||
type = "string", example = "public or private key"
|
||||
)
|
||||
)
|
||||
),
|
||||
@@ -319,13 +319,13 @@ public class AdminResource {
|
||||
}
|
||||
)
|
||||
@ApiErrors({ApiError.INVALID_PRIVATE_KEY, ApiError.REPOSITORY_ISSUE})
|
||||
public String deleteMintingAccount(String seed58) {
|
||||
public String deleteMintingAccount(String key58) {
|
||||
Security.checkApiCallAllowed(request);
|
||||
|
||||
try (final Repository repository = RepositoryManager.getRepository()) {
|
||||
byte[] seed = Base58.decode(seed58.trim());
|
||||
byte[] key = Base58.decode(key58.trim());
|
||||
|
||||
if (repository.getAccountRepository().delete(seed) == 0)
|
||||
if (repository.getAccountRepository().delete(key) == 0)
|
||||
return "false";
|
||||
|
||||
repository.saveChanges();
|
||||
|
@@ -23,6 +23,7 @@ import javax.ws.rs.core.MediaType;
|
||||
import org.qortal.api.ApiError;
|
||||
import org.qortal.api.ApiErrors;
|
||||
import org.qortal.api.ApiExceptionFactory;
|
||||
import org.qortal.api.model.BlockInfo;
|
||||
import org.qortal.api.model.BlockSignerSummary;
|
||||
import org.qortal.crypto.Crypto;
|
||||
import org.qortal.data.account.AccountData;
|
||||
@@ -480,4 +481,44 @@ public class BlocksResource {
|
||||
}
|
||||
}
|
||||
|
||||
@GET
|
||||
@Path("/summaries")
|
||||
@Operation(
|
||||
summary = "Fetch only summary info about a range of blocks",
|
||||
description = "Specify up to 2 out 3 of: start, end and count. If neither start nor end are specified, then end is assumed to be latest block. Where necessary, count is assumed to be 50.",
|
||||
responses = {
|
||||
@ApiResponse(
|
||||
description = "blocks",
|
||||
content = @Content(
|
||||
array = @ArraySchema(
|
||||
schema = @Schema(
|
||||
implementation = BlockInfo.class
|
||||
)
|
||||
)
|
||||
)
|
||||
)
|
||||
}
|
||||
)
|
||||
@ApiErrors({
|
||||
ApiError.REPOSITORY_ISSUE
|
||||
})
|
||||
public List<BlockInfo> getBlockRange(
|
||||
@QueryParam("start") Integer startHeight,
|
||||
@QueryParam("end") Integer endHeight,
|
||||
@Parameter(ref = "count") @QueryParam("count") Integer count) {
|
||||
// Check up to 2 out of 3 params
|
||||
if (startHeight != null && endHeight != null && count != null)
|
||||
throw ApiExceptionFactory.INSTANCE.createException(request, ApiError.INVALID_CRITERIA);
|
||||
|
||||
// Check values
|
||||
if ((startHeight != null && startHeight < 1) || (endHeight != null && endHeight < 1) || (count != null && count < 1))
|
||||
throw ApiExceptionFactory.INSTANCE.createException(request, ApiError.INVALID_CRITERIA);
|
||||
|
||||
try (final Repository repository = RepositoryManager.getRepository()) {
|
||||
return repository.getBlockRepository().getBlockInfos(startHeight, endHeight, count);
|
||||
} catch (DataException e) {
|
||||
throw ApiExceptionFactory.INSTANCE.createException(request, ApiError.REPOSITORY_ISSUE, e);
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
@@ -6,8 +6,10 @@ import java.util.Map;
|
||||
import java.util.concurrent.atomic.AtomicReference;
|
||||
|
||||
import org.eclipse.jetty.websocket.api.Session;
|
||||
import org.eclipse.jetty.websocket.api.WebSocketException;
|
||||
import org.eclipse.jetty.websocket.api.annotations.OnWebSocketClose;
|
||||
import org.eclipse.jetty.websocket.api.annotations.OnWebSocketConnect;
|
||||
import org.eclipse.jetty.websocket.api.annotations.OnWebSocketError;
|
||||
import org.eclipse.jetty.websocket.api.annotations.OnWebSocketMessage;
|
||||
import org.eclipse.jetty.websocket.api.annotations.WebSocket;
|
||||
import org.eclipse.jetty.websocket.servlet.WebSocketServlet;
|
||||
@@ -52,6 +54,10 @@ public class ActiveChatsWebSocket extends WebSocketServlet implements ApiWebSock
|
||||
ChatNotifier.getInstance().deregister(session);
|
||||
}
|
||||
|
||||
@OnWebSocketError
|
||||
public void onWebSocketError(Session session, Throwable throwable) {
|
||||
}
|
||||
|
||||
@OnWebSocketMessage
|
||||
public void onWebSocketMessage(Session session, String message) {
|
||||
}
|
||||
@@ -78,8 +84,8 @@ public class ActiveChatsWebSocket extends WebSocketServlet implements ApiWebSock
|
||||
return;
|
||||
|
||||
previousOutput.set(output);
|
||||
session.getRemote().sendString(output);
|
||||
} catch (DataException | IOException e) {
|
||||
session.getRemote().sendStringByFuture(output);
|
||||
} catch (DataException | IOException | WebSocketException e) {
|
||||
// No output this time?
|
||||
}
|
||||
}
|
||||
|
@@ -5,8 +5,10 @@ import java.io.StringWriter;
|
||||
import java.util.concurrent.atomic.AtomicReference;
|
||||
|
||||
import org.eclipse.jetty.websocket.api.Session;
|
||||
import org.eclipse.jetty.websocket.api.WebSocketException;
|
||||
import org.eclipse.jetty.websocket.api.annotations.OnWebSocketClose;
|
||||
import org.eclipse.jetty.websocket.api.annotations.OnWebSocketConnect;
|
||||
import org.eclipse.jetty.websocket.api.annotations.OnWebSocketError;
|
||||
import org.eclipse.jetty.websocket.api.annotations.OnWebSocketMessage;
|
||||
import org.eclipse.jetty.websocket.api.annotations.WebSocket;
|
||||
import org.eclipse.jetty.websocket.servlet.WebSocketServlet;
|
||||
@@ -41,6 +43,10 @@ public class AdminStatusWebSocket extends WebSocketServlet implements ApiWebSock
|
||||
StatusNotifier.getInstance().deregister(session);
|
||||
}
|
||||
|
||||
@OnWebSocketError
|
||||
public void onWebSocketError(Session session, Throwable throwable) {
|
||||
}
|
||||
|
||||
@OnWebSocketMessage
|
||||
public void onWebSocketMessage(Session session, String message) {
|
||||
}
|
||||
@@ -59,8 +65,8 @@ public class AdminStatusWebSocket extends WebSocketServlet implements ApiWebSock
|
||||
return;
|
||||
|
||||
previousOutput.set(output);
|
||||
session.getRemote().sendString(output);
|
||||
} catch (DataException | IOException e) {
|
||||
session.getRemote().sendStringByFuture(output);
|
||||
} catch (DataException | IOException | WebSocketException e) {
|
||||
// No output this time?
|
||||
}
|
||||
}
|
||||
|
@@ -2,17 +2,20 @@ package org.qortal.api.websocket;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.io.StringWriter;
|
||||
import java.util.List;
|
||||
|
||||
import org.eclipse.jetty.websocket.api.Session;
|
||||
import org.eclipse.jetty.websocket.api.WebSocketException;
|
||||
import org.eclipse.jetty.websocket.api.annotations.OnWebSocketClose;
|
||||
import org.eclipse.jetty.websocket.api.annotations.OnWebSocketConnect;
|
||||
import org.eclipse.jetty.websocket.api.annotations.OnWebSocketError;
|
||||
import org.eclipse.jetty.websocket.api.annotations.OnWebSocketMessage;
|
||||
import org.eclipse.jetty.websocket.api.annotations.WebSocket;
|
||||
import org.eclipse.jetty.websocket.servlet.WebSocketServlet;
|
||||
import org.eclipse.jetty.websocket.servlet.WebSocketServletFactory;
|
||||
import org.qortal.api.ApiError;
|
||||
import org.qortal.api.model.BlockInfo;
|
||||
import org.qortal.controller.BlockNotifier;
|
||||
import org.qortal.data.block.BlockData;
|
||||
import org.qortal.repository.DataException;
|
||||
import org.qortal.repository.Repository;
|
||||
import org.qortal.repository.RepositoryManager;
|
||||
@@ -29,7 +32,7 @@ public class BlocksWebSocket extends WebSocketServlet implements ApiWebSocket {
|
||||
|
||||
@OnWebSocketConnect
|
||||
public void onWebSocketConnect(Session session) {
|
||||
BlockNotifier.Listener listener = blockData -> onNotify(session, blockData);
|
||||
BlockNotifier.Listener listener = blockInfo -> onNotify(session, blockInfo);
|
||||
BlockNotifier.getInstance().register(session, listener);
|
||||
}
|
||||
|
||||
@@ -38,6 +41,10 @@ public class BlocksWebSocket extends WebSocketServlet implements ApiWebSocket {
|
||||
BlockNotifier.getInstance().deregister(session);
|
||||
}
|
||||
|
||||
@OnWebSocketError
|
||||
public void onWebSocketError(Session session, Throwable throwable) {
|
||||
}
|
||||
|
||||
@OnWebSocketMessage
|
||||
public void onWebSocketMessage(Session session, String message) {
|
||||
// We're expecting either a base58 block signature or an integer block height
|
||||
@@ -53,13 +60,19 @@ public class BlocksWebSocket extends WebSocketServlet implements ApiWebSocket {
|
||||
}
|
||||
|
||||
try (final Repository repository = RepositoryManager.getRepository()) {
|
||||
BlockData blockData = repository.getBlockRepository().fromSignature(signature);
|
||||
if (blockData == null) {
|
||||
int height = repository.getBlockRepository().getHeightFromSignature(signature);
|
||||
if (height == 0) {
|
||||
sendError(session, ApiError.BLOCK_UNKNOWN);
|
||||
return;
|
||||
}
|
||||
|
||||
onNotify(session, blockData);
|
||||
List<BlockInfo> blockInfos = repository.getBlockRepository().getBlockInfos(height, null, 1);
|
||||
if (blockInfos == null || blockInfos.isEmpty()) {
|
||||
sendError(session, ApiError.BLOCK_UNKNOWN);
|
||||
return;
|
||||
}
|
||||
|
||||
onNotify(session, blockInfos.get(0));
|
||||
} catch (DataException e) {
|
||||
sendError(session, ApiError.REPOSITORY_ISSUE);
|
||||
}
|
||||
@@ -82,26 +95,26 @@ public class BlocksWebSocket extends WebSocketServlet implements ApiWebSocket {
|
||||
}
|
||||
|
||||
try (final Repository repository = RepositoryManager.getRepository()) {
|
||||
BlockData blockData = repository.getBlockRepository().fromHeight(height);
|
||||
if (blockData == null) {
|
||||
List<BlockInfo> blockInfos = repository.getBlockRepository().getBlockInfos(height, null, 1);
|
||||
if (blockInfos == null || blockInfos.isEmpty()) {
|
||||
sendError(session, ApiError.BLOCK_UNKNOWN);
|
||||
return;
|
||||
}
|
||||
|
||||
onNotify(session, blockData);
|
||||
onNotify(session, blockInfos.get(0));
|
||||
} catch (DataException e) {
|
||||
sendError(session, ApiError.REPOSITORY_ISSUE);
|
||||
}
|
||||
}
|
||||
|
||||
private void onNotify(Session session, BlockData blockData) {
|
||||
private void onNotify(Session session, BlockInfo blockInfo) {
|
||||
StringWriter stringWriter = new StringWriter();
|
||||
|
||||
try {
|
||||
this.marshall(stringWriter, blockData);
|
||||
this.marshall(stringWriter, blockInfo);
|
||||
|
||||
session.getRemote().sendString(stringWriter.toString());
|
||||
} catch (IOException e) {
|
||||
session.getRemote().sendStringByFuture(stringWriter.toString());
|
||||
} catch (IOException | WebSocketException e) {
|
||||
// No output this time
|
||||
}
|
||||
}
|
||||
|
@@ -8,8 +8,10 @@ import java.util.List;
|
||||
import java.util.Map;
|
||||
|
||||
import org.eclipse.jetty.websocket.api.Session;
|
||||
import org.eclipse.jetty.websocket.api.WebSocketException;
|
||||
import org.eclipse.jetty.websocket.api.annotations.OnWebSocketClose;
|
||||
import org.eclipse.jetty.websocket.api.annotations.OnWebSocketConnect;
|
||||
import org.eclipse.jetty.websocket.api.annotations.OnWebSocketError;
|
||||
import org.eclipse.jetty.websocket.api.annotations.OnWebSocketMessage;
|
||||
import org.eclipse.jetty.websocket.api.annotations.WebSocket;
|
||||
import org.eclipse.jetty.websocket.servlet.WebSocketServlet;
|
||||
@@ -89,6 +91,10 @@ public class ChatMessagesWebSocket extends WebSocketServlet implements ApiWebSoc
|
||||
ChatNotifier.getInstance().deregister(session);
|
||||
}
|
||||
|
||||
@OnWebSocketError
|
||||
public void onWebSocketError(Session session, Throwable throwable) {
|
||||
}
|
||||
|
||||
@OnWebSocketMessage
|
||||
public void onWebSocketMessage(Session session, String message) {
|
||||
}
|
||||
@@ -125,8 +131,8 @@ public class ChatMessagesWebSocket extends WebSocketServlet implements ApiWebSoc
|
||||
try {
|
||||
this.marshall(stringWriter, chatMessages);
|
||||
|
||||
session.getRemote().sendString(stringWriter.toString());
|
||||
} catch (IOException e) {
|
||||
session.getRemote().sendStringByFuture(stringWriter.toString());
|
||||
} catch (IOException | WebSocketException e) {
|
||||
// No output this time?
|
||||
}
|
||||
}
|
||||
|
@@ -1,9 +1,12 @@
|
||||
package org.qortal.controller;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.Collection;
|
||||
import java.util.HashMap;
|
||||
import java.util.Map;
|
||||
|
||||
import org.eclipse.jetty.websocket.api.Session;
|
||||
import org.qortal.api.model.BlockInfo;
|
||||
import org.qortal.data.block.BlockData;
|
||||
|
||||
public class BlockNotifier {
|
||||
@@ -12,7 +15,7 @@ public class BlockNotifier {
|
||||
|
||||
@FunctionalInterface
|
||||
public interface Listener {
|
||||
void notify(BlockData blockData);
|
||||
void notify(BlockInfo blockInfo);
|
||||
}
|
||||
|
||||
private Map<Session, Listener> listenersBySession = new HashMap<>();
|
||||
@@ -27,17 +30,32 @@ public class BlockNotifier {
|
||||
return instance;
|
||||
}
|
||||
|
||||
public synchronized void register(Session session, Listener listener) {
|
||||
this.listenersBySession.put(session, listener);
|
||||
public void register(Session session, Listener listener) {
|
||||
synchronized (this.listenersBySession) {
|
||||
this.listenersBySession.put(session, listener);
|
||||
}
|
||||
}
|
||||
|
||||
public synchronized void deregister(Session session) {
|
||||
this.listenersBySession.remove(session);
|
||||
public void deregister(Session session) {
|
||||
synchronized (this.listenersBySession) {
|
||||
this.listenersBySession.remove(session);
|
||||
}
|
||||
}
|
||||
|
||||
public synchronized void onNewBlock(BlockData blockData) {
|
||||
for (Listener listener : this.listenersBySession.values())
|
||||
listener.notify(blockData);
|
||||
public void onNewBlock(BlockData blockData) {
|
||||
// Convert BlockData to BlockInfo
|
||||
BlockInfo blockInfo = new BlockInfo(blockData);
|
||||
|
||||
for (Listener listener : getAllListeners())
|
||||
listener.notify(blockInfo);
|
||||
}
|
||||
|
||||
private Collection<Listener> getAllListeners() {
|
||||
// Make a copy of listeners to both avoid concurrent modification
|
||||
// and reduce synchronization time
|
||||
synchronized (this.listenersBySession) {
|
||||
return new ArrayList<>(this.listenersBySession.values());
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
@@ -1,5 +1,7 @@
|
||||
package org.qortal.controller;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.Collection;
|
||||
import java.util.HashMap;
|
||||
import java.util.Map;
|
||||
|
||||
@@ -27,22 +29,34 @@ public class ChatNotifier {
|
||||
return instance;
|
||||
}
|
||||
|
||||
public synchronized void register(Session session, Listener listener) {
|
||||
this.listenersBySession.put(session, listener);
|
||||
public void register(Session session, Listener listener) {
|
||||
synchronized (this.listenersBySession) {
|
||||
this.listenersBySession.put(session, listener);
|
||||
}
|
||||
}
|
||||
|
||||
public synchronized void deregister(Session session) {
|
||||
this.listenersBySession.remove(session);
|
||||
public void deregister(Session session) {
|
||||
synchronized (this.listenersBySession) {
|
||||
this.listenersBySession.remove(session);
|
||||
}
|
||||
}
|
||||
|
||||
public synchronized void onNewChatTransaction(ChatTransactionData chatTransactionData) {
|
||||
for (Listener listener : this.listenersBySession.values())
|
||||
public void onNewChatTransaction(ChatTransactionData chatTransactionData) {
|
||||
for (Listener listener : getAllListeners())
|
||||
listener.notify(chatTransactionData);
|
||||
}
|
||||
|
||||
public synchronized void onGroupMembershipChange() {
|
||||
for (Listener listener : this.listenersBySession.values())
|
||||
public void onGroupMembershipChange() {
|
||||
for (Listener listener : getAllListeners())
|
||||
listener.notify(null);
|
||||
}
|
||||
|
||||
private Collection<Listener> getAllListeners() {
|
||||
// Make a copy of listeners to both avoid concurrent modification
|
||||
// and reduce synchronization time
|
||||
synchronized (this.listenersBySession) {
|
||||
return new ArrayList<>(this.listenersBySession.values());
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
@@ -537,8 +537,11 @@ public class Controller extends Thread {
|
||||
|
||||
public SynchronizationResult actuallySynchronize(Peer peer, boolean force) throws InterruptedException {
|
||||
syncPercent = (this.chainTip.getHeight() * 100) / peer.getChainTipData().getLastHeight();
|
||||
isSynchronizing = true;
|
||||
updateSysTray();
|
||||
// Only update SysTray if we're potentially changing height
|
||||
if (syncPercent < 100) {
|
||||
isSynchronizing = true;
|
||||
updateSysTray();
|
||||
}
|
||||
|
||||
BlockData priorChainTip = this.chainTip;
|
||||
|
||||
@@ -584,7 +587,6 @@ public class Controller extends Thread {
|
||||
break;
|
||||
|
||||
case OK:
|
||||
requestSysTrayUpdate = true;
|
||||
// fall-through...
|
||||
case NOTHING_TO_DO: {
|
||||
// Update our list of inferior chain tips
|
||||
@@ -611,14 +613,13 @@ public class Controller extends Thread {
|
||||
// Reset our cache of inferior chains
|
||||
inferiorChainSignatures.clear();
|
||||
|
||||
// Update chain-tip, notify peers, websockets, etc.
|
||||
// Update chain-tip, systray, notify peers, websockets, etc.
|
||||
this.onNewBlock(newChainTip);
|
||||
}
|
||||
|
||||
return syncResult;
|
||||
} finally {
|
||||
isSynchronizing = false;
|
||||
requestSysTrayUpdate = true;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -765,8 +766,10 @@ public class Controller extends Thread {
|
||||
BlockData latestBlockData = getChainTip();
|
||||
network.broadcast(peer -> network.buildHeightMessage(peer, latestBlockData));
|
||||
|
||||
// Send (if outbound) / Request unconfirmed transaction signatures
|
||||
network.broadcast(network::buildGetUnconfirmedTransactionsMessage);
|
||||
// Request unconfirmed transaction signatures, but only if we're up-to-date.
|
||||
// If we're NOT up-to-date then priority is synchronizing first
|
||||
if (isUpToDate())
|
||||
network.broadcast(network::buildGetUnconfirmedTransactionsMessage);
|
||||
}
|
||||
|
||||
public void onMintingPossibleChange(boolean isMintingPossible) {
|
||||
@@ -1040,7 +1043,12 @@ public class Controller extends Thread {
|
||||
|
||||
private void onNetworkGetUnconfirmedTransactionsMessage(Peer peer, Message message) {
|
||||
try (final Repository repository = RepositoryManager.getRepository()) {
|
||||
List<byte[]> signatures = repository.getTransactionRepository().getUnconfirmedTransactionSignatures();
|
||||
List<byte[]> signatures = Collections.emptyList();
|
||||
|
||||
// If we're NOT up-to-date then don't send out unconfirmed transactions
|
||||
// as it's possible they are already included in a later block that we don't have.
|
||||
if (isUpToDate())
|
||||
signatures = repository.getTransactionRepository().getUnconfirmedTransactionSignatures();
|
||||
|
||||
Message transactionSignaturesMessage = new TransactionSignaturesMessage(signatures);
|
||||
if (!peer.sendMessage(transactionSignaturesMessage))
|
||||
|
@@ -1,5 +1,7 @@
|
||||
package org.qortal.controller;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.Collection;
|
||||
import java.util.HashMap;
|
||||
import java.util.Map;
|
||||
|
||||
@@ -26,17 +28,29 @@ public class StatusNotifier {
|
||||
return instance;
|
||||
}
|
||||
|
||||
public synchronized void register(Session session, Listener listener) {
|
||||
this.listenersBySession.put(session, listener);
|
||||
public void register(Session session, Listener listener) {
|
||||
synchronized (this.listenersBySession) {
|
||||
this.listenersBySession.put(session, listener);
|
||||
}
|
||||
}
|
||||
|
||||
public synchronized void deregister(Session session) {
|
||||
this.listenersBySession.remove(session);
|
||||
public void deregister(Session session) {
|
||||
synchronized (this.listenersBySession) {
|
||||
this.listenersBySession.remove(session);
|
||||
}
|
||||
}
|
||||
|
||||
public synchronized void onStatusChange(long now) {
|
||||
for (Listener listener : this.listenersBySession.values())
|
||||
public void onStatusChange(long now) {
|
||||
for (Listener listener : getAllListeners())
|
||||
listener.notify(now);
|
||||
}
|
||||
|
||||
private Collection<Listener> getAllListeners() {
|
||||
// Make a copy of listeners to both avoid concurrent modification
|
||||
// and reduce synchronization time
|
||||
synchronized (this.listenersBySession) {
|
||||
return new ArrayList<>(this.listenersBySession.values());
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
@@ -1,11 +1,15 @@
|
||||
package org.qortal.data.account;
|
||||
|
||||
import java.math.BigDecimal;
|
||||
|
||||
import javax.xml.bind.annotation.XmlAccessType;
|
||||
import javax.xml.bind.annotation.XmlAccessorType;
|
||||
import javax.xml.bind.annotation.XmlElement;
|
||||
import javax.xml.bind.annotation.XmlTransient;
|
||||
import javax.xml.bind.annotation.adapters.XmlJavaTypeAdapter;
|
||||
|
||||
import org.qortal.utils.Base58;
|
||||
|
||||
import io.swagger.v3.oas.annotations.media.Schema;
|
||||
|
||||
// All properties to be converted to JSON via JAXB
|
||||
@@ -71,4 +75,13 @@ public class RewardShareData {
|
||||
return this.minter;
|
||||
}
|
||||
|
||||
// For debugging
|
||||
|
||||
public String toString() {
|
||||
if (this.minter.equals(this.recipient))
|
||||
return String.format("Minter/recipient: %s, reward-share public key: %s", this.minter, Base58.encode(this.rewardSharePublicKey));
|
||||
else
|
||||
return String.format("Minter: %s, recipient: %s (%s %%), reward-share public key: %s", this.minter, this.recipient, BigDecimal.valueOf(this.sharePercent, 2), Base58.encode(this.rewardSharePublicKey));
|
||||
}
|
||||
|
||||
}
|
||||
|
@@ -1,6 +1,8 @@
|
||||
package org.qortal.network;
|
||||
|
||||
import java.util.Arrays;
|
||||
import java.util.concurrent.ExecutorService;
|
||||
import java.util.concurrent.Executors;
|
||||
import java.util.regex.Matcher;
|
||||
import java.util.regex.Pattern;
|
||||
|
||||
@@ -13,7 +15,9 @@ import org.qortal.network.message.ChallengeMessage;
|
||||
import org.qortal.network.message.HelloMessage;
|
||||
import org.qortal.network.message.Message;
|
||||
import org.qortal.network.message.Message.MessageType;
|
||||
import org.qortal.settings.Settings;
|
||||
import org.qortal.network.message.ResponseMessage;
|
||||
import org.qortal.utils.DaemonThreadFactory;
|
||||
import org.qortal.utils.NTP;
|
||||
|
||||
import com.google.common.primitives.Bytes;
|
||||
@@ -27,6 +31,7 @@ public enum Handshake {
|
||||
|
||||
@Override
|
||||
public void action(Peer peer) {
|
||||
/* Never called */
|
||||
}
|
||||
},
|
||||
HELLO(MessageType.HELLO) {
|
||||
@@ -164,6 +169,11 @@ public enum Handshake {
|
||||
|
||||
peer.setPeersNodeId(Crypto.toNodeAddress(peersPublicKey));
|
||||
|
||||
// For inbound peers, we need to go into interim holding state while we compute RESPONSE
|
||||
if (!peer.isOutbound())
|
||||
return RESPONDING;
|
||||
|
||||
// Handshake completed!
|
||||
return COMPLETED;
|
||||
}
|
||||
|
||||
@@ -178,28 +188,50 @@ public enum Handshake {
|
||||
final byte[] data = Crypto.digest(Bytes.concat(sharedSecret, peersChallenge));
|
||||
|
||||
// We do this in a new thread as it can take a while...
|
||||
Thread responseThread = new Thread(() -> {
|
||||
responseExecutor.execute(() -> {
|
||||
// Are we still connected?
|
||||
if (peer.isStopping())
|
||||
// No point computing for dead peer
|
||||
return;
|
||||
|
||||
Integer nonce = MemoryPoW.compute2(data, POW_BUFFER_SIZE, POW_DIFFICULTY);
|
||||
|
||||
Message responseMessage = new ResponseMessage(nonce, data);
|
||||
if (!peer.sendMessage(responseMessage))
|
||||
peer.disconnect("failed to send RESPONSE");
|
||||
});
|
||||
|
||||
responseThread.setDaemon(true);
|
||||
responseThread.start();
|
||||
// For inbound peers, we should actually be in RESPONDING state.
|
||||
// So we need to do the extra work to move to COMPLETED state.
|
||||
if (!peer.isOutbound()) {
|
||||
peer.setHandshakeStatus(COMPLETED);
|
||||
Network.getInstance().onHandshakeCompleted(peer);
|
||||
}
|
||||
});
|
||||
}
|
||||
},
|
||||
COMPLETED(null) {
|
||||
// Interim holding state while we compute RESPONSE to send to inbound peer
|
||||
RESPONDING(null) {
|
||||
@Override
|
||||
public Handshake onMessage(Peer peer, Message message) {
|
||||
// Handshake completed
|
||||
// Should never be called
|
||||
return null;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void action(Peer peer) {
|
||||
// Note: this is only called when we've made outbound connection
|
||||
// Should never be called
|
||||
}
|
||||
},
|
||||
COMPLETED(null) {
|
||||
@Override
|
||||
public Handshake onMessage(Peer peer, Message message) {
|
||||
// Should never be called
|
||||
return null;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void action(Peer peer) {
|
||||
// Note: this is only called if we've made outbound connection
|
||||
}
|
||||
};
|
||||
|
||||
@@ -212,6 +244,7 @@ public enum Handshake {
|
||||
|
||||
private static final int POW_BUFFER_SIZE = 8 * 1024 * 1024; // bytes
|
||||
private static final int POW_DIFFICULTY = 8; // leading zero bits
|
||||
private static final ExecutorService responseExecutor = Executors.newFixedThreadPool(Settings.getInstance().getNetworkPoWComputePoolSize(), new DaemonThreadFactory("Network-PoW"));
|
||||
|
||||
private static final byte[] ZERO_CHALLENGE = new byte[ChallengeMessage.CHALLENGE_LENGTH];
|
||||
|
||||
|
@@ -55,6 +55,7 @@ import org.qortal.utils.ExecuteProduceConsume;
|
||||
// import org.qortal.utils.ExecutorDumper;
|
||||
import org.qortal.utils.ExecuteProduceConsume.StatsSnapshot;
|
||||
import org.qortal.utils.NTP;
|
||||
import org.qortal.utils.NamedThreadFactory;
|
||||
|
||||
// For managing peers
|
||||
public class Network {
|
||||
@@ -151,7 +152,8 @@ public class Network {
|
||||
ExecutorService networkExecutor = new ThreadPoolExecutor(1,
|
||||
Settings.getInstance().getMaxNetworkThreadPoolSize(),
|
||||
NETWORK_EPC_KEEPALIVE, TimeUnit.SECONDS,
|
||||
new SynchronousQueue<Runnable>());
|
||||
new SynchronousQueue<Runnable>(),
|
||||
new NamedThreadFactory("Network-EPC"));
|
||||
networkEPC = new NetworkProcessor(networkExecutor);
|
||||
}
|
||||
|
||||
@@ -355,7 +357,7 @@ public class Network {
|
||||
|
||||
private Task maybeProducePeerPingTask(Long now) {
|
||||
// Ask connected peers whether they need a ping
|
||||
for (Peer peer : getConnectedPeers()) {
|
||||
for (Peer peer : getHandshakedPeers()) {
|
||||
Task peerTask = peer.getPingTask(now);
|
||||
if (peerTask != null)
|
||||
return peerTask;
|
||||
@@ -635,7 +637,7 @@ public class Network {
|
||||
|
||||
/** Called when Peer's thread has setup and is ready to process messages */
|
||||
public void onPeerReady(Peer peer) {
|
||||
this.onMessage(peer, null);
|
||||
onHandshakingMessage(peer, null, Handshake.STARTED);
|
||||
}
|
||||
|
||||
public void onDisconnect(Peer peer) {
|
||||
@@ -777,7 +779,7 @@ public class Network {
|
||||
opportunisticMergePeers(peer.toString(), peerV2Addresses);
|
||||
}
|
||||
|
||||
private void onHandshakeCompleted(Peer peer) {
|
||||
/*pacakge*/ void onHandshakeCompleted(Peer peer) {
|
||||
LOGGER.debug(String.format("Handshake completed with peer %s", peer));
|
||||
|
||||
// Are we already connected to this peer?
|
||||
|
@@ -169,8 +169,8 @@ public interface AccountRepository {
|
||||
|
||||
public void save(MintingAccountData mintingAccountData) throws DataException;
|
||||
|
||||
/** Delete minting account info, used by BlockMinter, from repository using passed private key. */
|
||||
public int delete(byte[] mintingAccountPrivateKey) throws DataException;
|
||||
/** Delete minting account info, used by BlockMinter, from repository using passed public or private key. */
|
||||
public int delete(byte[] mintingAccountKey) throws DataException;
|
||||
|
||||
// Managing QORT from legacy QORA
|
||||
|
||||
|
@@ -2,6 +2,7 @@ package org.qortal.repository;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
import org.qortal.api.model.BlockInfo;
|
||||
import org.qortal.api.model.BlockSignerSummary;
|
||||
import org.qortal.data.block.BlockData;
|
||||
import org.qortal.data.block.BlockSummaryData;
|
||||
@@ -128,6 +129,11 @@ public interface BlockRepository {
|
||||
*/
|
||||
public List<BlockSummaryData> getBlockSummaries(int firstBlockHeight, int lastBlockHeight) throws DataException;
|
||||
|
||||
/**
|
||||
* Returns block infos for the passed height range, for API use.
|
||||
*/
|
||||
public List<BlockInfo> getBlockInfos(Integer startHeight, Integer endHeight, Integer count) throws DataException;
|
||||
|
||||
/**
|
||||
* Trim online accounts signatures from blocks older than passed timestamp.
|
||||
*
|
||||
|
@@ -774,9 +774,9 @@ public class HSQLDBAccountRepository implements AccountRepository {
|
||||
}
|
||||
|
||||
@Override
|
||||
public int delete(byte[] minterPrivateKey) throws DataException {
|
||||
public int delete(byte[] minterKey) throws DataException {
|
||||
try {
|
||||
return this.repository.delete("MintingAccounts", "minter_private_key = ?", minterPrivateKey);
|
||||
return this.repository.delete("MintingAccounts", "minter_private_key = ? OR minter_public_key = ?", minterKey, minterKey);
|
||||
} catch (SQLException e) {
|
||||
throw new DataException("Unable to delete minting account from repository", e);
|
||||
}
|
||||
|
@@ -6,6 +6,7 @@ import java.util.ArrayList;
|
||||
import java.util.Collections;
|
||||
import java.util.List;
|
||||
|
||||
import org.qortal.api.model.BlockInfo;
|
||||
import org.qortal.api.model.BlockSignerSummary;
|
||||
import org.qortal.data.block.BlockData;
|
||||
import org.qortal.data.block.BlockSummaryData;
|
||||
@@ -360,6 +361,90 @@ public class HSQLDBBlockRepository implements BlockRepository {
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public List<BlockInfo> getBlockInfos(Integer startHeight, Integer endHeight, Integer count) throws DataException {
|
||||
StringBuilder sql = new StringBuilder(512);
|
||||
sql.append("SELECT signature, height, minted_when, transaction_count, RewardShares.minter ");
|
||||
|
||||
/*
|
||||
* start end count result
|
||||
* 10 40 null blocks 10 to 39 (excludes end block, ignore count)
|
||||
*
|
||||
* null null null blocks 1 to 50 (assume count=50, maybe start=1)
|
||||
* 30 null null blocks 30 to 79 (assume count=50)
|
||||
* 30 null 10 blocks 30 to 39
|
||||
*
|
||||
* null null 50 last 50 blocks? so if max(blocks.height) is 200, then blocks 151 to 200
|
||||
* null 200 null blocks 150 to 199 (excludes end block, assume count=50)
|
||||
* null 200 10 blocks 190 to 199 (excludes end block)
|
||||
*/
|
||||
|
||||
if (startHeight != null && endHeight != null) {
|
||||
sql.append("FROM Blocks ");
|
||||
sql.append("JOIN RewardShares ON RewardShares.reward_share_public_key = Blocks.minter ");
|
||||
sql.append("WHERE height BETWEEN ");
|
||||
sql.append(startHeight);
|
||||
sql.append(" AND ");
|
||||
sql.append(endHeight - 1);
|
||||
} else if (endHeight != null || (startHeight == null && count != null)) {
|
||||
// we are going to return blocks from the end of the chain
|
||||
if (count == null)
|
||||
count = 50;
|
||||
|
||||
if (endHeight == null) {
|
||||
sql.append("FROM (SELECT height FROM Blocks ORDER BY height DESC LIMIT 1) AS MaxHeights (max_height) ");
|
||||
sql.append("JOIN Blocks ON height BETWEEN (max_height - ");
|
||||
sql.append(count);
|
||||
sql.append(" + 1) AND max_height ");
|
||||
sql.append("JOIN RewardShares ON RewardShares.reward_share_public_key = Blocks.minter");
|
||||
} else {
|
||||
sql.append("FROM Blocks ");
|
||||
sql.append("JOIN RewardShares ON RewardShares.reward_share_public_key = Blocks.minter ");
|
||||
sql.append("WHERE height BETWEEN ");
|
||||
sql.append(endHeight - count);
|
||||
sql.append(" AND ");
|
||||
sql.append(endHeight - 1);
|
||||
}
|
||||
|
||||
} else {
|
||||
// we are going to return blocks from the start of the chain
|
||||
if (startHeight == null)
|
||||
startHeight = 1;
|
||||
|
||||
if (count == null)
|
||||
count = 50;
|
||||
|
||||
sql.append("FROM Blocks ");
|
||||
sql.append("JOIN RewardShares ON RewardShares.reward_share_public_key = Blocks.minter ");
|
||||
sql.append("WHERE height BETWEEN ");
|
||||
sql.append(startHeight);
|
||||
sql.append(" AND ");
|
||||
sql.append(startHeight + count - 1);
|
||||
}
|
||||
|
||||
List<BlockInfo> blockInfos = new ArrayList<>();
|
||||
|
||||
try (ResultSet resultSet = this.repository.checkedExecute(sql.toString())) {
|
||||
if (resultSet == null)
|
||||
return blockInfos;
|
||||
|
||||
do {
|
||||
byte[] signature = resultSet.getBytes(1);
|
||||
int height = resultSet.getInt(2);
|
||||
long timestamp = resultSet.getLong(3);
|
||||
int transactionCount = resultSet.getInt(4);
|
||||
String minterAddress = resultSet.getString(5);
|
||||
|
||||
BlockInfo blockInfo = new BlockInfo(signature, height, timestamp, transactionCount, minterAddress);
|
||||
blockInfos.add(blockInfo);
|
||||
} while (resultSet.next());
|
||||
|
||||
return blockInfos;
|
||||
} catch (SQLException e) {
|
||||
throw new DataException("Unable to fetch height-ranged block infos from repository", e);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public int trimOldOnlineAccountsSignatures(long timestamp) throws DataException {
|
||||
String sql = "UPDATE Blocks set online_accounts_signatures = NULL WHERE minted_when < ? AND online_accounts_signatures IS NOT NULL";
|
||||
|
@@ -47,7 +47,7 @@ public class Settings {
|
||||
// UI servers
|
||||
private int uiPort = 12388;
|
||||
private String[] uiLocalServers = new String[] {
|
||||
"localhost", "172.24.1.1", "qor.tal"
|
||||
"localhost", "127.0.0.1", "172.24.1.1", "qor.tal"
|
||||
};
|
||||
private String[] uiRemoteServers = new String[] {
|
||||
"node1.qortal.org", "node2.qortal.org", "node3.qortal.org", "node4.qortal.org", "node5.qortal.org",
|
||||
@@ -92,6 +92,8 @@ public class Settings {
|
||||
private int maxPeers = 32;
|
||||
/** Maximum number of threads for network engine. */
|
||||
private int maxNetworkThreadPoolSize = 20;
|
||||
/** Maximum number of threads for network proof-of-work compute, used during handshaking. */
|
||||
private int networkPoWComputePoolSize = 2;
|
||||
|
||||
// Which blockchains this node is running
|
||||
private String blockchainConfig = null; // use default from resources
|
||||
@@ -106,7 +108,9 @@ public class Settings {
|
||||
// Auto-update sources
|
||||
private String[] autoUpdateRepos = new String[] {
|
||||
"https://github.com/QORT/qortal/raw/%s/qortal.update",
|
||||
"https://raw.githubusercontent.com@151.101.16.133/QORT/qortal/%s/qortal.update"
|
||||
"https://raw.githubusercontent.com@151.101.16.133/QORT/qortal/%s/qortal.update",
|
||||
"https://github.com/Qortal/qortal/raw/%s/qortal.update",
|
||||
"https://raw.githubusercontent.com@151.101.16.133/Qortal/qortal/%s/qortal.update"
|
||||
};
|
||||
|
||||
/** Array of NTP server hostnames. */
|
||||
@@ -353,6 +357,10 @@ public class Settings {
|
||||
return this.maxNetworkThreadPoolSize;
|
||||
}
|
||||
|
||||
public int getNetworkPoWComputePoolSize() {
|
||||
return this.networkPoWComputePoolSize;
|
||||
}
|
||||
|
||||
public String getBlockchainConfig() {
|
||||
return this.blockchainConfig;
|
||||
}
|
||||
|
@@ -136,7 +136,7 @@ public class RewardShareTransaction extends Transaction {
|
||||
|
||||
// Deleting a non-existent reward-share makes no sense
|
||||
if (isCancellingSharePercent)
|
||||
return ValidationResult.INVALID_REWARD_SHARE_PERCENT;
|
||||
return ValidationResult.REWARD_SHARE_UNKNOWN;
|
||||
|
||||
// Check the minting account hasn't reach maximum number of reward-shares
|
||||
int rewardShareCount = this.repository.getAccountRepository().countRewardShares(creator.getPublicKey());
|
||||
|
@@ -225,6 +225,7 @@ public abstract class Transaction {
|
||||
AT_IS_FINISHED(71),
|
||||
NO_FLAG_PERMISSION(72),
|
||||
NOT_MINTING_ACCOUNT(73),
|
||||
REWARD_SHARE_UNKNOWN(76),
|
||||
INVALID_REWARD_SHARE_PERCENT(77),
|
||||
PUBLIC_KEY_UNKNOWN(78),
|
||||
INVALID_PUBLIC_KEY(79),
|
||||
|
31
src/main/java/org/qortal/utils/DaemonThreadFactory.java
Normal file
31
src/main/java/org/qortal/utils/DaemonThreadFactory.java
Normal file
@@ -0,0 +1,31 @@
|
||||
package org.qortal.utils;
|
||||
|
||||
import java.util.concurrent.Executors;
|
||||
import java.util.concurrent.ThreadFactory;
|
||||
import java.util.concurrent.atomic.AtomicInteger;
|
||||
|
||||
public class DaemonThreadFactory implements ThreadFactory {
|
||||
|
||||
private final String name;
|
||||
private final AtomicInteger threadNumber = new AtomicInteger(1);
|
||||
|
||||
public DaemonThreadFactory(String name) {
|
||||
this.name = name;
|
||||
}
|
||||
|
||||
public DaemonThreadFactory() {
|
||||
this(null);
|
||||
}
|
||||
|
||||
@Override
|
||||
public Thread newThread(Runnable runnable) {
|
||||
Thread thread = Executors.defaultThreadFactory().newThread(runnable);
|
||||
thread.setDaemon(true);
|
||||
|
||||
if (this.name != null)
|
||||
thread.setName(this.name + "-" + this.threadNumber.getAndIncrement());
|
||||
|
||||
return thread;
|
||||
}
|
||||
|
||||
}
|
24
src/main/java/org/qortal/utils/NamedThreadFactory.java
Normal file
24
src/main/java/org/qortal/utils/NamedThreadFactory.java
Normal file
@@ -0,0 +1,24 @@
|
||||
package org.qortal.utils;
|
||||
|
||||
import java.util.concurrent.Executors;
|
||||
import java.util.concurrent.ThreadFactory;
|
||||
import java.util.concurrent.atomic.AtomicInteger;
|
||||
|
||||
public class NamedThreadFactory implements ThreadFactory {
|
||||
|
||||
private final String name;
|
||||
private final AtomicInteger threadNumber = new AtomicInteger(1);
|
||||
|
||||
public NamedThreadFactory(String name) {
|
||||
this.name = name;
|
||||
}
|
||||
|
||||
@Override
|
||||
public Thread newThread(Runnable runnable) {
|
||||
Thread thread = Executors.defaultThreadFactory().newThread(runnable);
|
||||
thread.setName(this.name + "-" + this.threadNumber.getAndIncrement());
|
||||
|
||||
return thread;
|
||||
}
|
||||
|
||||
}
|
13
tools/peer-heights
Executable file
13
tools/peer-heights
Executable file
@@ -0,0 +1,13 @@
|
||||
#!/usr/bin/env bash
|
||||
|
||||
# Requires: 'qort' script in PATH, and 'jq' utility installed
|
||||
|
||||
set -e
|
||||
|
||||
# Any extra args passed to us are also passed to 'qort', just prior to '-p peers'
|
||||
qort $@ -p peers | \
|
||||
jq -r 'def lpad($len):
|
||||
tostring | ($len - length) as $l | (" " * $l)[:$l] + .;
|
||||
.[] |
|
||||
select(has("lastHeight")) |
|
||||
"\(.address | lpad(22)) (\(.version)), height \(.lastHeight), sig: \(.lastBlockSignature[0:8]), ts \(.lastBlockTimestamp / 1e3 | strftime("%Y-%m-%d %H:%M:%S"))"'
|
87
tools/qort
Executable file
87
tools/qort
Executable file
@@ -0,0 +1,87 @@
|
||||
#!/usr/bin/env bash
|
||||
|
||||
# default output post-processor
|
||||
postproc=cat
|
||||
|
||||
# Qortal defaults
|
||||
port=12391
|
||||
example_host=node10.qortal.org
|
||||
|
||||
# called-as name
|
||||
name="${0##*/}"
|
||||
|
||||
while [ -n "$*" ]; do
|
||||
case $1 in
|
||||
-p)
|
||||
shift
|
||||
postproc="json_pp -f json -t json -json_opt utf8,pretty"
|
||||
;;
|
||||
|
||||
[Dd][Ee][Ll][Ee][Tt][Ee])
|
||||
shift
|
||||
method="-X DELETE"
|
||||
;;
|
||||
|
||||
-l)
|
||||
shift
|
||||
src="--interface localhost"
|
||||
;;
|
||||
|
||||
-t)
|
||||
shift
|
||||
testnet=true
|
||||
;;
|
||||
|
||||
-j)
|
||||
shift
|
||||
content_type="Content-Type: application/json"
|
||||
;;
|
||||
|
||||
*)
|
||||
break
|
||||
;;
|
||||
esac
|
||||
done
|
||||
|
||||
if [ "${name}" = "qort" ]; then
|
||||
port=${testnet:+62391}
|
||||
port=${port:-12391}
|
||||
example_host=node10.qortal.org
|
||||
fi
|
||||
|
||||
if [ -z "$*" ]; then
|
||||
echo "usage: $name [-l] [-p] [-t] [DELETE] <url> [<post-data>]"
|
||||
echo "-l: use localhost as source address"
|
||||
echo "-p: pretty-print JSON output"
|
||||
echo "-t: use testnet port"
|
||||
echo "example (using localhost:${port}): $name -p blocks/last"
|
||||
echo "example: $name -p http://${example_host}:${port}/blocks/last"
|
||||
echo "example: BASE_URL=http://${example_host}:${port} $name -p blocks/last"
|
||||
echo "example: BASE_URL=${example_host} $name -p blocks/last"
|
||||
echo "example: $name -l DELETE peers/known"
|
||||
exit
|
||||
fi
|
||||
|
||||
url=$1
|
||||
shift
|
||||
|
||||
if [ "${url:0:4}" != "http" ]; then
|
||||
base_url=${BASE_URL-localhost:${port}}
|
||||
|
||||
if [ "${base_url:0:4}" != "http" ]; then
|
||||
base_url="http://${base_url}"
|
||||
fi
|
||||
|
||||
if [ -n "${base_url/#*:[0-9[0-9]*}" ]; then
|
||||
base_url="${base_url%%/}:${port}"
|
||||
fi
|
||||
|
||||
url="${base_url%%/}/${url#/}"
|
||||
fi
|
||||
|
||||
if [ "$#" != 0 ]; then
|
||||
data="--data"
|
||||
fi
|
||||
|
||||
curl --silent --insecure --connect-timeout 5 ${content_type:+--header} "${content_type}" ${method} ${src} --url ${url} ${data} "$@" | ${postproc}
|
||||
echo
|
353
tools/tx.pl
Executable file
353
tools/tx.pl
Executable file
@@ -0,0 +1,353 @@
|
||||
#!/usr/bin/env perl
|
||||
|
||||
use JSON;
|
||||
use warnings;
|
||||
use strict;
|
||||
|
||||
use Getopt::Std;
|
||||
use File::Basename;
|
||||
|
||||
our %opt;
|
||||
getopts('dpst', \%opt);
|
||||
|
||||
my $proc = basename($0);
|
||||
|
||||
if (@ARGV < 1) {
|
||||
print STDERR "usage: $proc [-d] [-p] [-s] [-t] <tx-type> <privkey> <values> [<key-value pairs>]\n";
|
||||
print STDERR "-d: debug, -p: process (broadcast) transaction, -s: sign, -t: testnet\n";
|
||||
print STDERR "example: $proc PAYMENT P22kW91AJfDNBj32nVii292hhfo5AgvUYPz5W12ExsjE QxxQZiK7LZBjmpGjRz1FAZSx9MJDCoaHqz 0.1\n";
|
||||
print STDERR "example: $proc JOIN_GROUP X92h3hf9k20kBj32nVnoh3XT14o5AgvUYPz5W12ExsjE 3\n";
|
||||
print STDERR "example: BASE_URL=node10.qortal.org $proc JOIN_GROUP CB2DW91AJfd47432nVnoh3XT14o5AgvUYPz5W12ExsjE 3\n";
|
||||
print STDERR "example: $proc -p sign C4ifh827ffDNBj32nVnoh3XT14o5AgvUYPz5W12ExsjE 111jivxUwerRw...Fjtu\n";
|
||||
print STDERR "for help: $proc all\n";
|
||||
print STDERR "for help: $proc REGISTER_NAME\n";
|
||||
exit 2;
|
||||
}
|
||||
|
||||
our $BASE_URL = $ENV{BASE_URL} || $opt{t} ? 'http://localhost:62391' : 'http://localhost:12391';
|
||||
our $DEFAULT_FEE = 0.001;
|
||||
|
||||
our %TRANSACTION_TYPES = (
|
||||
payment => {
|
||||
url => 'payments/pay',
|
||||
required => [qw(recipient amount)],
|
||||
key_name => 'senderPublicKey',
|
||||
},
|
||||
# groups
|
||||
set_group => {
|
||||
url => 'groups/setdefault',
|
||||
required => [qw(defaultGroupId)],
|
||||
key_name => 'creatorPublicKey',
|
||||
},
|
||||
create_group => {
|
||||
url => 'groups/create',
|
||||
required => [qw(groupName description isOpen approvalThreshold)],
|
||||
key_name => 'creatorPublicKey',
|
||||
},
|
||||
update_group => {
|
||||
url => 'groups/update',
|
||||
required => [qw(groupId newOwner newDescription newIsOpen newApprovalThreshold)],
|
||||
key_name => 'ownerPublicKey',
|
||||
},
|
||||
join_group => {
|
||||
url => 'groups/join',
|
||||
required => [qw(groupId)],
|
||||
key_name => 'joinerPublicKey',
|
||||
},
|
||||
leave_group => {
|
||||
url => 'groups/leave',
|
||||
required => [qw(groupId)],
|
||||
key_name => 'leaverPublicKey',
|
||||
},
|
||||
group_invite => {
|
||||
url => 'groups/invite',
|
||||
required => [qw(groupId invitee)],
|
||||
key_name => 'adminPublicKey',
|
||||
},
|
||||
group_kick => {
|
||||
url => 'groups/kick',
|
||||
required => [qw(groupId member reason)],
|
||||
key_name => 'adminPublicKey',
|
||||
},
|
||||
add_group_admin => {
|
||||
url => 'groups/addadmin',
|
||||
required => [qw(groupId member)],
|
||||
key_name => 'ownerPublicKey',
|
||||
},
|
||||
group_approval => {
|
||||
url => 'groups/approval',
|
||||
required => [qw(pendingSignature approval)],
|
||||
key_name => 'adminPublicKey',
|
||||
},
|
||||
# assets
|
||||
issue_asset => {
|
||||
url => 'assets/issue',
|
||||
required => [qw(assetName description quantity isDivisible)],
|
||||
key_name => 'issuerPublicKey',
|
||||
},
|
||||
update_asset => {
|
||||
url => 'assets/update',
|
||||
required => [qw(assetId newOwner)],
|
||||
key_name => 'ownerPublicKey',
|
||||
},
|
||||
transfer_asset => {
|
||||
url => 'assets/transfer',
|
||||
required => [qw(recipient amount assetId)],
|
||||
key_name => 'senderPublicKey',
|
||||
},
|
||||
create_order => {
|
||||
url => 'assets/order',
|
||||
required => [qw(haveAssetId wantAssetId amount price)],
|
||||
key_name => 'creatorPublicKey',
|
||||
},
|
||||
# names
|
||||
register_name => {
|
||||
url => 'names/register',
|
||||
required => [qw(name data)],
|
||||
key_name => 'registrantPublicKey',
|
||||
},
|
||||
update_name => {
|
||||
url => 'names/update',
|
||||
required => [qw(newName newData)],
|
||||
key_name => 'ownerPublicKey',
|
||||
},
|
||||
# reward-shares
|
||||
reward_share => {
|
||||
url => 'addresses/rewardshare',
|
||||
required => [qw(recipient rewardSharePublicKey sharePercent)],
|
||||
key_name => 'minterPublicKey',
|
||||
},
|
||||
# arbitrary
|
||||
arbitrary => {
|
||||
url => 'arbitrary',
|
||||
required => [qw(service dataType data)],
|
||||
key_name => 'senderPublicKey',
|
||||
},
|
||||
# chat
|
||||
chat => {
|
||||
url => 'chat',
|
||||
required => [qw(data)],
|
||||
optional => [qw(recipient isText isEncrypted)],
|
||||
key_name => 'senderPublicKey',
|
||||
defaults => { isText => 'true' },
|
||||
pow_url => 'chat/compute',
|
||||
},
|
||||
# misc
|
||||
publicize => {
|
||||
url => 'addresses/publicize',
|
||||
required => [],
|
||||
key_name => 'senderPublicKey',
|
||||
pow_url => 'addresses/publicize/compute',
|
||||
},
|
||||
# Cross-chain trading
|
||||
build_trade => {
|
||||
url => 'crosschain/build',
|
||||
required => [qw(initialQortAmount finalQortAmount fundingQortAmount secretHash bitcoinAmount)],
|
||||
optional => [qw(tradeTimeout)],
|
||||
key_name => 'creatorPublicKey',
|
||||
defaults => { tradeTimeout => 10800 },
|
||||
},
|
||||
trade_recipient => {
|
||||
url => 'crosschain/tradeoffer/recipient',
|
||||
required => [qw(atAddress recipient)],
|
||||
key_name => 'creatorPublicKey',
|
||||
remove => [qw(timestamp reference fee)],
|
||||
},
|
||||
trade_secret => {
|
||||
url => 'crosschain/tradeoffer/secret',
|
||||
required => [qw(atAddress secret)],
|
||||
key_name => 'recipientPublicKey',
|
||||
remove => [qw(timestamp reference fee)],
|
||||
},
|
||||
# These are fake transaction types to provide utility functions:
|
||||
sign => {
|
||||
url => 'transactions/sign',
|
||||
required => [qw{transactionBytes}],
|
||||
},
|
||||
);
|
||||
|
||||
my $tx_type = lc(shift(@ARGV));
|
||||
|
||||
if ($tx_type eq 'all') {
|
||||
printf STDERR "Transaction types: %s\n", join(', ', sort { $a cmp $b } keys %TRANSACTION_TYPES);
|
||||
exit 2;
|
||||
}
|
||||
|
||||
my $tx_info = $TRANSACTION_TYPES{$tx_type};
|
||||
|
||||
if (!$tx_info) {
|
||||
printf STDERR "Transaction type '%s' unknown\n", uc($tx_type);
|
||||
exit 1;
|
||||
}
|
||||
|
||||
my @required = @{$tx_info->{required}};
|
||||
|
||||
if (@ARGV < @required + 1) {
|
||||
printf STDERR "usage: %s %s <privkey> %s", $proc, uc($tx_type), join(' ', map { "<$_>"} @required);
|
||||
printf STDERR " %s", join(' ', map { "[$_ <$_>]" } @{$tx_info->{optional}}) if exists $tx_info->{optional};
|
||||
print "\n";
|
||||
exit 2;
|
||||
}
|
||||
|
||||
my $priv_key = shift @ARGV;
|
||||
|
||||
my $account = account($priv_key);
|
||||
my $raw;
|
||||
|
||||
if ($tx_type ne 'sign') {
|
||||
my %extras;
|
||||
|
||||
foreach my $required_arg (@required) {
|
||||
$extras{$required_arg} = shift @ARGV;
|
||||
}
|
||||
|
||||
# For CHAT we use a random reference
|
||||
if ($tx_type eq 'chat') {
|
||||
$extras{reference} = api('utils/random?length=64');
|
||||
}
|
||||
|
||||
%extras = (%extras, %{$tx_info->{defaults}}) if exists $tx_info->{defaults};
|
||||
|
||||
%extras = (%extras, @ARGV);
|
||||
|
||||
$raw = build_raw($tx_type, $account, %extras);
|
||||
printf "Raw: %s\n", $raw if $opt{d} || (!$opt{s} && !$opt{p});
|
||||
|
||||
# Some transaction types require proof-of-work, e.g. CHAT
|
||||
if (exists $tx_info->{pow_url}) {
|
||||
$raw = api($tx_info->{pow_url}, $raw);
|
||||
printf "Raw with PoW: %s\n", $raw if $opt{d};
|
||||
}
|
||||
} else {
|
||||
$raw = shift @ARGV;
|
||||
$opt{s}++;
|
||||
}
|
||||
|
||||
if ($opt{s}) {
|
||||
my $signed = sign($account->{private}, $raw);
|
||||
printf "Signed: %s\n", $signed if $opt{d} || $tx_type eq 'sign';
|
||||
|
||||
if ($opt{p}) {
|
||||
my $processed = process($signed);
|
||||
printf "Processed: %s\n", $processed if $opt{d};
|
||||
}
|
||||
|
||||
my $hex = api('utils/frombase58', $signed);
|
||||
# sig is last 64 bytes / 128 chars
|
||||
my $sighex = substr($hex, -128);
|
||||
|
||||
my $sig58 = api('utils/tobase58/{hex}', '', '{hex}', $sighex);
|
||||
printf "Signature: %s\n", $sig58;
|
||||
}
|
||||
|
||||
sub account {
|
||||
my ($creator) = @_;
|
||||
|
||||
my $account = { private => $creator };
|
||||
$account->{public} = api('utils/publickey', $creator);
|
||||
$account->{address} = api('addresses/convert/{publickey}', '', '{publickey}', $account->{public});
|
||||
|
||||
return $account;
|
||||
}
|
||||
|
||||
sub build_raw {
|
||||
my ($type, $account, %extras) = @_;
|
||||
|
||||
my $tx_info = $TRANSACTION_TYPES{$type};
|
||||
die("unknown tx type: $type\n") unless defined $tx_info;
|
||||
|
||||
my $ref = exists $extras{reference} ? $extras{reference} : lastref($account->{address});
|
||||
|
||||
my %json = (
|
||||
timestamp => time * 1000,
|
||||
reference => $ref,
|
||||
fee => $DEFAULT_FEE,
|
||||
);
|
||||
|
||||
$json{$tx_info->{key_name}} = $account->{public} if exists $tx_info->{key_name};
|
||||
|
||||
foreach my $required (@{$tx_info->{required}}) {
|
||||
die("missing tx field: $required\n") unless exists $extras{$required};
|
||||
}
|
||||
|
||||
while (my ($key, $value) = each %extras) {
|
||||
$json{$key} = $value;
|
||||
}
|
||||
|
||||
if (exists $tx_info->{remove}) {
|
||||
foreach my $key (@{$tx_info->{remove}}) {
|
||||
delete $json{$key};
|
||||
}
|
||||
}
|
||||
|
||||
my $json = "{\n";
|
||||
while (my ($key, $value) = each %json) {
|
||||
if (ref($value) eq 'ARRAY') {
|
||||
$json .= "\t\"$key\": [],\n";
|
||||
} else {
|
||||
$json .= "\t\"$key\": \"$value\",\n";
|
||||
}
|
||||
}
|
||||
# remove final comma
|
||||
substr($json, -2, 1) = '';
|
||||
$json .= "}\n";
|
||||
|
||||
printf "%s:\n%s\n", $type, $json if $opt{d};
|
||||
|
||||
my $raw = api($tx_info->{url}, $json);
|
||||
return $raw;
|
||||
}
|
||||
|
||||
sub sign {
|
||||
my ($private, $raw) = @_;
|
||||
|
||||
my $json = <<" __JSON__";
|
||||
{
|
||||
"privateKey": "$private",
|
||||
"transactionBytes": "$raw"
|
||||
}
|
||||
__JSON__
|
||||
|
||||
return api('transactions/sign', $json);
|
||||
}
|
||||
|
||||
sub process {
|
||||
my ($signed) = @_;
|
||||
|
||||
return api('transactions/process', $signed);
|
||||
}
|
||||
|
||||
sub lastref {
|
||||
my ($address) = @_;
|
||||
|
||||
return api('addresses/lastreference/{address}', '', '{address}', $address)
|
||||
}
|
||||
|
||||
sub api {
|
||||
my ($endpoint, $postdata, @args) = @_;
|
||||
|
||||
my $url = $endpoint;
|
||||
my $method = 'GET';
|
||||
|
||||
for (my $i = 0; $i < @args; $i += 2) {
|
||||
my $placemarker = $args[$i];
|
||||
my $value = $args[$i + 1];
|
||||
|
||||
$url =~ s/$placemarker/$value/g;
|
||||
}
|
||||
|
||||
my $curl = "curl --silent --output - --url '$BASE_URL/$url'";
|
||||
if (defined $postdata && $postdata ne '') {
|
||||
$postdata =~ tr|\n| |s;
|
||||
$curl .= " --header 'Content-Type: application/json' --data-binary '$postdata'";
|
||||
$method = 'POST';
|
||||
}
|
||||
my $response = `$curl 2>/dev/null`;
|
||||
chomp $response;
|
||||
|
||||
if ($response eq '' || substr($response, 0, 6) eq '<html>' || $response =~ m/(^\{|,)"error":(\d+)[,}]/) {
|
||||
die("API call '$method $BASE_URL/$endpoint' failed:\n$response\n");
|
||||
}
|
||||
|
||||
return $response;
|
||||
}
|
Reference in New Issue
Block a user